diff --git a/lib/puppet/parser/ast/pops_bridge.rb b/lib/puppet/parser/ast/pops_bridge.rb index 63f36494d..9c6a31744 100644 --- a/lib/puppet/parser/ast/pops_bridge.rb +++ b/lib/puppet/parser/ast/pops_bridge.rb @@ -1,246 +1,252 @@ require 'puppet/parser/ast/top_level_construct' require 'puppet/pops' # The AST::Bridge contains classes that bridges between the new Pops based model # and the 3.x AST. This is required to be able to reuse the Puppet::Resource::Type which is # fundamental for the rest of the logic. # class Puppet::Parser::AST::PopsBridge # Bridges to one Pops Model Expression # The @value is the expression # This is used to represent the body of a class, definition, or node, and for each parameter's default value # expression. # class Expression < Puppet::Parser::AST::Leaf def initialize args super @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new() end def to_s Puppet::Pops::Model::ModelTreeDumper.new.dump(@value) end def evaluate(scope) @@evaluator.evaluate(scope, @value) end # Adapts to 3x where top level constructs needs to have each to iterate over children. Short circuit this # by yielding self. By adding this there is no need to wrap a pops expression inside an AST::BlockExpression # def each yield self end def sequence_with(other) if value.nil? # This happens when testing and not having a complete setup other else # When does this happen ? Ever ? raise "sequence_with called on Puppet::Parser::AST::PopsBridge::Expression - please report use case" # What should be done if the above happens (We don't want this to happen). # Puppet::Parser::AST::BlockExpression.new(:children => [self] + other.children) end end # The 3x requires code plugged in to an AST to have this in certain positions in the tree. The purpose # is to either print the content, or to look for things that needs to be defined. This implementation # cheats by always returning an empty array. (This allows simple files to not require a "Program" at the top. # def children [] end end class NilAsUndefExpression < Expression def evaluate(scope) result = super result.nil? ? :undef : result end end # Bridges the top level "Program" produced by the pops parser. # Its main purpose is to give one point where all definitions are instantiated (actually defined since the # Puppet 3x terminology is somewhat misleading - the definitions are instantiated, but instances of the created types # are not created, that happens when classes are included / required, nodes are matched and when resources are instantiated # by a resource expression (which is also used to instantiate a host class). # class Program < Puppet::Parser::AST::TopLevelConstruct attr_reader :program_model, :context def initialize(program_model, context = {}) @program_model = program_model @context = context @ast_transformer ||= Puppet::Pops::Model::AstTransformer.new(@context[:file]) @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser.new() end # This is the 3x API, the 3x AST searches through all code to find the instructions that can be instantiated. # This Pops-model based instantiation relies on the parser to build this list while parsing (which is more # efficient as it avoids one full scan of all logic via recursive enumeration/yield) # def instantiate(modname) @program_model.definitions.collect do |d| case d when Puppet::Pops::Model::HostClassDefinition instantiate_HostClassDefinition(d, modname) when Puppet::Pops::Model::ResourceTypeDefinition instantiate_ResourceTypeDefinition(d, modname) when Puppet::Pops::Model::NodeDefinition instantiate_NodeDefinition(d, modname) else raise Puppet::ParseError, "Internal Error: Unknown type of definition - got '#{d.class}'" end end.flatten().compact() # flatten since node definition may have returned an array # Compact since functions are not understood by compiler end def evaluate(scope) @@evaluator.evaluate(scope, program_model) end # Adapts to 3x where top level constructs needs to have each to iterate over children. Short circuit this # by yielding self. This means that the HostClass container will call this bridge instance with `instantiate`. # def each yield self end private def instantiate_Parameter(o) # 3x needs parameters as an array of `[name]` or `[name, value_expr]` # One problem is that the parameter evaluation takes place in the wrong context in 3x (the caller's and # can thus reference all sorts of information. Here the value expression is wrapped in an AST Bridge to a Pops # expression since the Pops side can not control the evaluation if o.value [o.name, NilAsUndefExpression.new(:value => o.value)] else [o.name] end end def create_type_map(definition) result = {} # No need to do anything if there are no parameters return result unless definition.parameters.size > 0 # No need to do anything if there are no typed parameters typed_parameters = definition.parameters.select {|p| p.type_expr } return result if typed_parameters.empty? # If there are typed parameters, they need to be evaluated to produce the corresponding type # instances. This evaluation requires a scope. A scope is not available when doing deserialization # (there is also no initialized evaluator). When running apply and test however, the environment is # reused and we may reenter without a scope (which is fine). A debug message is then output in case # there is the need to track down the odd corner case. See {#obtain_scope}. # if scope = obtain_scope typed_parameters.each do |p| result[p.name] = @@evaluator.evaluate(scope, p.type_expr) end end result end # Obtains the scope or issues a warning if :global_scope is not bound def obtain_scope scope = Puppet.lookup(:global_scope) do # This occurs when testing and when applying a catalog (there is no scope available then), and # when running tests that run a partial setup. # This is bad if the logic is trying to compile, but a warning can not be issues since it is a normal # use case that there is no scope when requesting the type in order to just get the parameters. Puppet.debug("Instantiating Resource with type checked parameters - scope is missing, skipping type checking.") nil end scope end # Produces a hash with data for Definition and HostClass def args_from_definition(o, modname) args = { :arguments => o.parameters.collect {|p| instantiate_Parameter(p) }, :argument_types => create_type_map(o), :module_name => modname } unless is_nop?(o.body) args[:code] = Expression.new(:value => o.body) end @ast_transformer.merge_location(args, o) end def instantiate_HostClassDefinition(o, modname) args = args_from_definition(o, modname) - args[:parent] = o.parent_class + args[:parent] = absolute_reference(o.parent_class) Puppet::Resource::Type.new(:hostclass, o.name, @context.merge(args)) end def instantiate_ResourceTypeDefinition(o, modname) Puppet::Resource::Type.new(:definition, o.name, @context.merge(args_from_definition(o, modname))) end def instantiate_NodeDefinition(o, modname) args = { :module_name => modname } unless is_nop?(o.body) args[:code] = Expression.new(:value => o.body) end unless is_nop?(o.parent) args[:parent] = @ast_transformer.hostname(o.parent) end host_matches = @ast_transformer.hostname(o.host_matches) @ast_transformer.merge_location(args, o) host_matches.collect do |name| Puppet::Resource::Type.new(:node, name, @context.merge(args)) end end # Propagates a found Function to the appropriate loader. # This is for 4x future-evaluator/loader # def instantiate_FunctionDefinition(function_definition, modname) loaders = (Puppet.lookup(:loaders) { nil }) unless loaders raise Puppet::ParseError, "Internal Error: Puppet Context ':loaders' missing - cannot define any functions" end loader = if modname.nil? || modname == "" # TODO : Later when functions can be private, a decision is needed regarding what that means. # A private environment loader could be used for logic outside of modules, then only that logic # would see the function. # # Use the private loader, this function may see the environment's dependencies (currently, all modules) loaders.private_environment_loader() else # TODO : Later check if function is private, and then add it to # private_loader_for_module # loaders.public_loader_for_module(modname) end unless loader raise Puppet::ParseError, "Internal Error: did not find public loader for module: '#{modname}'" end # Instantiate Function, and store it in the environment loader typed_name, f = Puppet::Pops::Loader::PuppetFunctionInstantiator.create_from_model(function_definition, loader) loader.set_entry(typed_name, f, Puppet::Pops::Adapters::SourcePosAdapter.adapt(function_definition).to_uri) nil # do not want the function to inadvertently leak into 3x end def code() Expression.new(:value => @value) end def is_nop?(o) @ast_transformer.is_nop?(o) end + def absolute_reference(ref) + if ref.nil? || ref.empty? || ref.start_with?('::') + ref + else + "::#{ref}" + end + end end - end diff --git a/spec/integration/parser/class_spec.rb b/spec/integration/parser/class_spec.rb new file mode 100644 index 000000000..5ac03ae06 --- /dev/null +++ b/spec/integration/parser/class_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' +require 'puppet_spec/language' + +describe "Class expressions" do + extend PuppetSpec::Language + + before :each do + Puppet[:parser] = 'future' + end + + produces( + "class hi { }" => [], + + "class hi { } include hi" => ['Class[Hi]'], + "include(hi) class hi { }" => ['Class[Hi]'], + + "class hi { } class { hi: }" => ['Class[Hi]'], + "class { hi: } class hi { }" => ['Class[Hi]'], + + "class bye { } class hi inherits bye { } include hi" => ['Class[Hi]', 'Class[Bye]']) + + produces(<<-EXAMPLE => ['Notify[foo]', 'Notify[bar]']) + class bar { notify { 'bar': } } + class foo::bar { notify { 'foo::bar': } } + class foo inherits bar { notify { 'foo': } } + + include foo + EXAMPLE + + produces(<<-EXAMPLE => ['Notify[foo]', 'Notify[bar]']) + class bar { notify { 'bar': } } + class foo::bar { notify { 'foo::bar': } } + class foo inherits ::bar { notify { 'foo': } } + + include foo + EXAMPLE +end diff --git a/spec/integration/parser/scope_spec.rb b/spec/integration/parser/scope_spec.rb index b6672b126..c10caa7f0 100644 --- a/spec/integration/parser/scope_spec.rb +++ b/spec/integration/parser/scope_spec.rb @@ -1,741 +1,741 @@ require 'spec_helper' require 'puppet_spec/compiler' describe "Two step scoping for variables" do include PuppetSpec::Compiler def expect_the_message_to_be(message, node = Puppet::Node.new('the node')) catalog = compile_to_catalog(yield, node) catalog.resource('Notify', 'something')[:message].should == message end before :each do Puppet.expects(:deprecation_warning).never end context 'using current parser' do describe "using plussignment to change in a new scope" do it "does not change a string in the parent scope" do # Expects to be able to concatenate string using += expect_the_message_to_be('top_msg') do <<-MANIFEST $var = "top_msg" class override { $var += "override" include foo } class foo { notify { 'something': message => $var, } } include override MANIFEST end end end end context 'using future parser' do before(:each) do Puppet[:parser] = 'future' end describe "using unsupported operators" do it "issues an error for +=" do expect do catalog = compile_to_catalog(<<-MANIFEST) $var = ["top_msg"] node default { $var += ["override"] } MANIFEST end.to raise_error(/The operator '\+=' is no longer supported/) end it "issues an error for -=" do expect do catalog = compile_to_catalog(<<-MANIFEST) $var = ["top_msg"] node default { $var -= ["top_msg"] } MANIFEST end.to raise_error(/The operator '-=' is no longer supported/) end end it "when using a template ignores the dynamic value of the var when using the @varname syntax" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include foo } class foo { $var = "foo_msg" include bar } class bar { notify { 'something': message => inline_template("<%= @var %>"), } } MANIFEST end end it "when using a template gets the var from an inherited class when using the @varname syntax" do expect_the_message_to_be('Barbamama') do <<-MANIFEST node default { $var = "node_msg" include bar_bamama include foo } class bar_bamama { $var = "Barbamama" } class foo { $var = "foo_msg" include bar } class bar inherits bar_bamama { notify { 'something': message => inline_template("<%= @var %>"), } } MANIFEST end end it "when using a template ignores the dynamic var when it is not present in an inherited class" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include bar_bamama include foo } class bar_bamama { } class foo { $var = "foo_msg" include bar } class bar inherits bar_bamama { notify { 'something': message => inline_template("<%= @var %>"), } } MANIFEST end end end shared_examples_for "the scope" do describe "fully qualified variable names" do it "keeps nodescope separate from topscope" do expect_the_message_to_be('topscope') do <<-MANIFEST $c = "topscope" node default { $c = "nodescope" notify { 'something': message => $::c } } MANIFEST end end end describe "when colliding class and variable names" do it "finds a topscope variable with the same name as a class" do expect_the_message_to_be('topscope') do <<-MANIFEST $c = "topscope" class c { } node default { include c notify { 'something': message => $c } } MANIFEST end end it "finds a node scope variable with the same name as a class" do expect_the_message_to_be('nodescope') do <<-MANIFEST class c { } node default { $c = "nodescope" include c notify { 'something': message => $c } } MANIFEST end end it "finds a class variable when the class collides with a nodescope variable" do expect_the_message_to_be('class') do <<-MANIFEST class c { $b = "class" } node default { $c = "nodescope" include c notify { 'something': message => $c::b } } MANIFEST end end it "finds a class variable when the class collides with a topscope variable" do expect_the_message_to_be('class') do <<-MANIFEST $c = "topscope" class c { $b = "class" } node default { include c notify { 'something': message => $::c::b } } MANIFEST end end end describe "when using shadowing and inheritance" do it "finds values in its local scope" do expect_the_message_to_be('local_msg') do <<-MANIFEST node default { include baz } class foo { } class bar inherits foo { $var = "local_msg" notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "finds values in its inherited scope" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { include baz } class foo { $var = "foo_msg" } class bar inherits foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "prefers values in its local scope over values in the inherited scope" do expect_the_message_to_be('local_msg') do <<-MANIFEST include bar class foo { $var = "inherited" } class bar inherits foo { $var = "local_msg" notify { 'something': message => $var, } } MANIFEST end end it "finds a qualified variable by following parent scopes of the specified scope" do expect_the_message_to_be("from node") do <<-MANIFEST class c { notify { 'something': message => "$a::b" } } class a { } node default { $b = "from node" include a include c } MANIFEST end end it "finds values in its inherited scope when the inherited class is qualified to the top" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { include baz } class foo { $var = "foo_msg" } class bar inherits ::foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "prefers values in its local scope over values in the inherited scope when the inherited class is fully qualified" do expect_the_message_to_be('local_msg') do <<-MANIFEST include bar class foo { $var = "inherited" } class bar inherits ::foo { $var = "local_msg" notify { 'something': message => $var, } } MANIFEST end end it "finds values in top scope when the inherited class is qualified to the top" do expect_the_message_to_be('top msg') do <<-MANIFEST $var = "top msg" class foo { } class bar inherits ::foo { notify { 'something': message => $var, } } include bar MANIFEST end end it "finds values in its inherited scope when the inherited class is a nested class that shadows another class at the top" do expect_the_message_to_be('inner baz') do <<-MANIFEST node default { include foo::bar } class baz { $var = "top baz" } class foo { class baz { $var = "inner baz" } - class bar inherits baz { + class bar inherits foo::baz { notify { 'something': message => $var, } } } MANIFEST end end it "finds values in its inherited scope when the inherited class is qualified to a nested class and qualified to the top" do expect_the_message_to_be('top baz') do <<-MANIFEST node default { include foo::bar } class baz { $var = "top baz" } class foo { class baz { $var = "inner baz" } class bar inherits ::baz { notify { 'something': message => $var, } } } MANIFEST end end it "finds values in its inherited scope when the inherited class is qualified" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { include bar } class foo { class baz { $var = "foo_msg" } } class bar inherits foo::baz { notify { 'something': message => $var, } } MANIFEST end end it "prefers values in its inherited scope over those in the node (with intermediate inclusion)" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { $var = "node_msg" include baz } class foo { $var = "foo_msg" } class bar inherits foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "prefers values in its inherited scope over those in the node (without intermediate inclusion)" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { $var = "node_msg" include bar } class foo { $var = "foo_msg" } class bar inherits foo { notify { 'something': message => $var, } } MANIFEST end end it "prefers values in its inherited scope over those from where it is included" do expect_the_message_to_be('foo_msg') do <<-MANIFEST node default { include baz } class foo { $var = "foo_msg" } class bar inherits foo { notify { 'something': message => $var, } } class baz { $var = "baz_msg" include bar } MANIFEST end end it "does not used variables from classes included in the inherited scope" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include bar } class quux { $var = "quux_msg" } class foo inherits quux { } class baz { include foo } class bar inherits baz { notify { 'something': message => $var, } } MANIFEST end end it "does not use a variable from a scope lexically enclosing it" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include other::bar } class other { $var = "other_msg" class bar { notify { 'something': message => $var, } } } MANIFEST end end it "finds values in its node scope" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include baz } class foo { } class bar inherits foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "finds values in its top scope" do expect_the_message_to_be('top_msg') do <<-MANIFEST $var = "top_msg" node default { include baz } class foo { } class bar inherits foo { notify { 'something': message => $var, } } class baz { include bar } MANIFEST end end it "prefers variables from the node over those in the top scope" do expect_the_message_to_be('node_msg') do <<-MANIFEST $var = "top_msg" node default { $var = "node_msg" include foo } class foo { notify { 'something': message => $var, } } MANIFEST end end it "finds top scope variables referenced inside a defined type" do expect_the_message_to_be('top_msg') do <<-MANIFEST $var = "top_msg" node default { foo { "testing": } } define foo() { notify { 'something': message => $var, } } MANIFEST end end it "finds node scope variables referenced inside a defined type" do expect_the_message_to_be('node_msg') do <<-MANIFEST $var = "top_msg" node default { $var = "node_msg" foo { "testing": } } define foo() { notify { 'something': message => $var, } } MANIFEST end end end describe "in situations that used to have dynamic lookup" do it "ignores the dynamic value of the var" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include foo } class baz { $var = "baz_msg" include bar } class foo inherits baz { } class bar { notify { 'something': message => $var, } } MANIFEST end end it "finds nil when the only set variable is in the dynamic scope" do expect_the_message_to_be(nil) do <<-MANIFEST node default { include baz } class foo { } class bar inherits foo { notify { 'something': message => $var, } } class baz { $var = "baz_msg" include bar } MANIFEST end end it "ignores the value in the dynamic scope for a defined type" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include foo } class foo { $var = "foo_msg" bar { "testing": } } define bar() { notify { 'something': message => $var, } } MANIFEST end end it "when using a template ignores the dynamic value of the var when using scope.lookupvar" do expect_the_message_to_be('node_msg') do <<-MANIFEST node default { $var = "node_msg" include foo } class foo { $var = "foo_msg" include bar } class bar { notify { 'something': message => inline_template("<%= scope.lookupvar('var') %>"), } } MANIFEST end end end describe "when using an enc" do it "places enc parameters in top scope" do enc_node = Puppet::Node.new("the node", { :parameters => { "var" => 'from_enc' } }) expect_the_message_to_be('from_enc', enc_node) do <<-MANIFEST notify { 'something': message => $var, } MANIFEST end end it "does not allow the enc to specify an existing top scope var" do enc_node = Puppet::Node.new("the_node", { :parameters => { "var" => 'from_enc' } }) expect { compile_to_catalog("$var = 'top scope'", enc_node) }.to raise_error( Puppet::Error, /Cannot reassign variable var at line 1(\:6)? on node the_node/ ) end it "evaluates enc classes in top scope when there is no node" do enc_node = Puppet::Node.new("the node", { :classes => ['foo'], :parameters => { "var" => 'from_enc' } }) expect_the_message_to_be('from_enc', enc_node) do <<-MANIFEST class foo { notify { 'something': message => $var, } } MANIFEST end end it "overrides enc variables from a node scope var" do enc_node = Puppet::Node.new("the_node", { :classes => ['foo'], :parameters => { 'enc_var' => 'Set from ENC.' } }) expect_the_message_to_be('ENC overridden in node', enc_node) do <<-MANIFEST node the_node { $enc_var = "ENC overridden in node" } class foo { notify { 'something': message => $enc_var, } } MANIFEST end end end end describe 'using classic parser' do before :each do Puppet[:parser] = 'current' end it_behaves_like 'the scope' it "finds value define in the inherited node" do expect_the_message_to_be('parent_msg') do <<-MANIFEST $var = "top_msg" node parent { $var = "parent_msg" } node default inherits parent { include foo } class foo { notify { 'something': message => $var, } } MANIFEST end end it "finds top scope when the class is included before the node defines the var" do expect_the_message_to_be('top_msg') do <<-MANIFEST $var = "top_msg" node parent { include foo } node default inherits parent { $var = "default_msg" } class foo { notify { 'something': message => $var, } } MANIFEST end end it "finds top scope when the class is included before the node defines the var" do expect_the_message_to_be('top_msg') do <<-MANIFEST $var = "top_msg" node parent { include foo } node default inherits parent { $var = "default_msg" } class foo { notify { 'something': message => $var, } } MANIFEST end end it "evaluates enc classes in the node scope when there is a matching node" do enc_node = Puppet::Node.new("the_node", { :classes => ['foo'] }) expect_the_message_to_be('from matching node', enc_node) do <<-MANIFEST node inherited { $var = "from inherited" } node the_node inherits inherited { $var = "from matching node" } class foo { notify { 'something': message => $var, } } MANIFEST end end end describe 'using future parser' do before :each do Puppet[:parser] = 'future' end it_behaves_like 'the scope' end end