Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(PUP-6964) Make all modules visible when making function calls #7019

@@ -385,6 +385,7 @@ def self.init_dispatch(a_closure)
end
end


# Public api methods of the DispatcherBuilder are available within dispatch()
# blocks declared in a Puppet::Function.create_function() call.
#
@@ -679,6 +680,128 @@ def call_function_with_scope(scope, function_name, *args, &block)
end
end

class Function3x < InternalFunction

# Table of optimized parameter names - 0 to 5 parameters
PARAM_NAMES = [
[],
['p0'.freeze].freeze,
['p0'.freeze, 'p1'.freeze].freeze,
['p0'.freeze, 'p1'.freeze, 'p2'.freeze].freeze,
['p0'.freeze, 'p1'.freeze, 'p2'.freeze, 'p3'.freeze].freeze,
['p0'.freeze, 'p1'.freeze, 'p2'.freeze, 'p3'.freeze, 'p4'.freeze].freeze,
]

# Creates an anonymous Function3x class that wraps a 3x function
#
# @api private
def self.create_function(func_name, func_info, loader)
func_name = func_name.to_s

# Creates an anonymous class to represent the function
# The idea being that it is garbage collected when there are no more
# references to it.
#
# (Do not give the class the block here, as instance variables should be set first)
the_class = Class.new(Function3x)

unless loader.nil?
the_class.instance_variable_set(:'@loader', loader.private_loader)
end

the_class.instance_variable_set(:'@func_name', func_name)
the_class.instance_variable_set(:'@method3x', :"function_#{func_name}")

# Make the anonymous class appear to have the class-name <func_name>
# Even if this class is not bound to such a symbol in a global ruby scope and
# must be resolved via the loader.
# This also overrides any attempt to define a name method in the given block
# (Since it redefines it)
#
the_class.instance_eval do
def name
@func_name
end

def loader
@loader
end

def method3x
@method3x
end
end

# Add the method that is called - it simply delegates to
# the 3.x function by calling it via the calling scope using the @method3x symbol
# :"function_#{name}".
#
# When function is not an rvalue function, make sure it produces nil
#
the_class.class_eval do

# Bypasses making the call via the dispatcher to make sure errors
# are reported exactly the same way as in 3x. The dispatcher is still needed as it is
# used to support other features than calling.
#
def call(scope, *args, &block)
begin
result = catch(:return) do
mapped_args = Puppet::Pops::Evaluator::Runtime3FunctionArgumentConverter.map_args(args, scope, '')
# this is the scope.function_xxx(...) call
return scope.send(self.class.method3x, mapped_args)
end
return result.value
rescue Puppet::Pops::Evaluator::Next => jumper
begin
throw :next, jumper.value
rescue Puppet::Parser::Scope::UNCAUGHT_THROW_EXCEPTION
raise Puppet::ParseError.new("next() from context where this is illegal", jumper.file, jumper.line)
end
rescue Puppet::Pops::Evaluator::Return => jumper
begin
throw :return, jumper
rescue Puppet::Parser::Scope::UNCAUGHT_THROW_EXCEPTION
raise Puppet::ParseError.new("return() from context where this is illegal", jumper.file, jumper.line)
end
end
end
end

# Create a dispatcher based on func_info
type, names = Puppet::Functions.any_signature(*from_to_names(func_info))
last_captures_rest = (type.size_range[1] == Float::INFINITY)

# The method '3x_function' here is a dummy as the dispatcher is not used for calling, only for information.
the_class.dispatcher.add(Puppet::Pops::Functions::Dispatch.new(type, '3x_function', names, last_captures_rest))
# The function class is returned as the result of the create function method
the_class
end

# Compute min and max number of arguments and a list of constructed
# parameter names p0 - pn (since there are no parameter names in 3x functions).
#
# @api private
def self.from_to_names(func_info)
arity = func_info[:arity]
if arity.nil?
arity = -1
end
if arity < 0
from = -arity - 1 # arity -1 is 0 min param, -2 is min 1 param
to = :default # infinite range
count = -arity # the number of named parameters
else
count = from = to = arity
end
# Names of parameters, up to 5 are optimized and use frozen version
# Note that (0..count-1) produces expected empty array for count == 0, 0-n for count >= 1
names = count <= 5 ? PARAM_NAMES[count] : (0..count-1).map {|n| "p#{n}" }
[from, to, names]
end
end


# Injection and Weaving of parameters
# ---
# It is possible to inject and weave a set of well known parameters into a call.
@@ -13,6 +13,7 @@ module Loader
require 'puppet/pops/loader/static_loader'
require 'puppet/pops/loader/runtime3_type_loader'
require 'puppet/pops/loader/ruby_function_instantiator'
require 'puppet/pops/loader/ruby_legacy_function_instantiator'
require 'puppet/pops/loader/ruby_data_type_instantiator'
require 'puppet/pops/loader/puppet_function_instantiator'
require 'puppet/pops/loader/type_definition_instantiator'
@@ -171,7 +171,9 @@ def self.newfunction(name, options = {}, &block)
elsif arity < 0 and args[0].size < (arity+1).abs
raise ArgumentError, _("%{name}(): Wrong number of arguments given (%{arg_count} for minimum %{min_arg_count})") % { name: name, arg_count: args[0].size, min_arg_count: (arity+1).abs }
end
self.send(real_fname, args[0])
r = Puppet::Pops::Evaluator::Runtime3FunctionArgumentConverter.convert_return(self.send(real_fname, args[0]))
# avoid leaking aribtrary value if not being an rvalue function
options[:type] == :rvalue ? r : nil
else
raise ArgumentError, _("custom functions must be called with a single array that contains the arguments. For example, function_example([1]) instead of function_example(1)")
end
@@ -196,6 +196,22 @@ def convert_Timestamp(o, scope, undef_value)
o.to_s
end

# Converts result back to 4.x by replacing :undef with nil in Array and Hash objects
#
def self.convert_return(val3x)
if val3x == :undef
nil
elsif val3x.is_a?(Array)
val3x.map {|v| convert_return(v) }
elsif val3x.is_a?(Hash)
hsh = {}
val3x.each_pair {|k,v| hsh[convert_return(k)] = convert_return(v)}
hsh
else
val3x
end
end

@instance = self.new
end

@@ -305,15 +305,14 @@ def call_function(name, args, o, scope, &block)
return Kernel.eval('_func.call(scope, *args, &block)'.freeze, Kernel.binding, file || '', line)
end
end
# Call via 3x API if function exists there
# Call via 3x API if function exists there without having been autoloaded
fail(Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name)

# Arguments must be mapped since functions are unaware of the new and magical creatures in 4x.
# NOTE: Passing an empty string last converts nil/:undef to empty string
mapped_args = Runtime3FunctionArgumentConverter.map_args(args, scope, '')
result = Puppet::Pops::PuppetStack.stack(file, line, scope, "function_#{name}", [mapped_args], &block)
# Prevent non r-value functions from leaking their result (they are not written to care about this)
Puppet::Parser::Functions.rvalue?(name) ? result : nil
# The 3x function performs return value mapping and returns nil if it is not of rvalue type
Puppet::Pops::PuppetStack.stack(file, line, scope, "function_#{name}", [mapped_args], &block)
end

# The o is used for source reference
@@ -29,7 +29,7 @@ class Loader
attr_reader :loader_name

# Describes the kinds of things that loaders can load
LOADABLE_KINDS = [:func_4x, :func_4xpp, :datatype, :type_pp, :resource_type_pp, :plan, :task].freeze
LOADABLE_KINDS = [:func_4x, :func_4xpp, :func_3x, :datatype, :type_pp, :resource_type_pp, :plan, :task].freeze

# @param [String] name the name of the loader. Must be unique among all loaders maintained by a {Loader} instance
def initialize(loader_name)
@@ -107,7 +107,7 @@ def loaded_entry(typed_name, check_dependencies = false)
#
# @api private
#
def [] (typed_name)
def [](typed_name)
if found = get_entry(typed_name)
found.value
else
@@ -25,7 +25,9 @@ def self.relative_paths_for_type(type, loader)
if loader.loadables.include?(:func_4xpp)
result << FunctionPathPP.new(loader)
end
# When wanted also add FunctionPath3x to load 3x functions
if loader.loadables.include?(:func_3x)
result << FunctionPath3x.new(loader)
end
when :plan
result << PlanPathPP.new(loader)
when :task
@@ -31,7 +31,7 @@ def self.system_loader_from(parent_loader, loaders)
nil,
puppet_lib, # may or may not have a 'lib' above 'puppet'
'puppet_system',
[:func_4x, :datatype] # only load ruby functions and types from "puppet"
[:func_4x, :func_3x, :datatype] # only load ruby functions and types from "puppet"
)
end

@@ -0,0 +1,54 @@
# The RubyLegacyFunctionInstantiator instantiates a Puppet::Functions::Function given the ruby source
# that calls Puppet::Functions.create_function.
#
class Puppet::Pops::Loader::RubyLegacyFunctionInstantiator
# Produces an instance of the Function class with the given typed_name, or fails with an error if the
# given ruby source does not produce this instance when evaluated.
#
# @param loader [Puppet::Pops::Loader::Loader] The loader the function is associated with
# @param typed_name [Puppet::Pops::Loader::TypedName] the type / name of the function to load
# @param source_ref [URI, String] a reference to the source / origin of the ruby code to evaluate
# @param ruby_code_string [String] ruby code in a string
#
# @return [Puppet::Pops::Functions.Function] - an instantiated function with global scope closure associated with the given loader
#
def self.create(loader, typed_name, source_ref, ruby_code_string)
unless ruby_code_string.is_a?(String) && ruby_code_string =~ /Puppet\:\:Parser\:\:Functions.*newfunction/m
raise ArgumentError, _("The code loaded from %{source_ref} does not seem to be a Puppet 3x API function - no 'newfunction' call.") % { source_ref: source_ref }
end
# make the private loader available in a binding to allow it to be passed on
loader_for_function = loader.private_loader
here = get_binding(loader_for_function)

# This will to do the 3x loading and define the "function_<name>" and "real_function_<name>" methods
# in the anonymous module used to hold function definitions.
#
func_info = eval(ruby_code_string, here, source_ref, 1)

unless func_info.is_a?(Hash)
raise ArgumentError, _("The code loaded from %{source_ref} did not produce the expected 3x function info Hash when evaluated. Got '%{klass}'") % { source_ref: source_ref, klass: created.class }
end
unless func_info[:name] == "function_#{typed_name.name()}"
raise ArgumentError, _("The code loaded from %{source_ref} produced mis-matched name, expected 'function_%{type_name}', got %{created_name}") % {
source_ref: source_ref, type_name: typed_name.name, created_name: func_info[:name] }
end

created = Puppet::Functions::Function3x.create_function(typed_name.name(), func_info, loader_for_function)

# create the function instance - it needs closure (scope), and loader (i.e. where it should start searching for things
# when calling functions etc.
# It should be bound to global scope

# Sets closure scope to nil, to let it be picked up at runtime from Puppet.lookup(:global_scope)
# If function definition used the loader from the binding to create a new loader, that loader wins
created.new(nil, loader_for_function)
end

# Produces a binding where the given loader is bound as a local variable (loader_injected_arg). This variable can be used in loaded
# ruby code - e.g. to call Puppet::Function.create_loaded_function(:name, loader,...)
#
def self.get_binding(loader_injected_arg)
binding
end
private_class_method :get_binding
end
@@ -505,8 +505,8 @@ def resolve(module_data)
nil
else
module_data.private_loader =
if module_data.restrict_to_dependencies? && !Puppet[:tasks]
create_loader_with_only_dependencies_visible(module_data)
if module_data.restrict_to_dependencies?
create_loader_with_dependencies_first(module_data)
else
create_loader_with_all_modules_visible(module_data)
end
@@ -516,29 +516,13 @@ def resolve(module_data)
private

def create_loader_with_all_modules_visible(from_module_data)
Puppet.debug{"ModuleLoader: module '#{from_module_data.name}' has unknown dependencies - it will have all other modules visible"}

@loaders.add_loader_by_name(Loader::DependencyLoader.new(from_module_data.public_loader, "#{from_module_data.name} private", all_module_loaders()))
end

def create_loader_with_only_dependencies_visible(from_module_data)
if from_module_data.unmet_dependencies?
if Puppet[:strict] != :off
msg = "ModuleLoader: module '#{from_module_data.name}' has unresolved dependencies" \
" - it will only see those that are resolved." \
" Use 'puppet module list --tree' to see information about modules"
case Puppet[:strict]
when :error
raise LoaderError.new(msg)
when :warning
Puppet.warn_once(:unresolved_module_dependencies,
"unresolved_dependencies_for_module_#{from_module_data.name}",
msg)
end
end
end
def create_loader_with_dependencies_first(from_module_data)
dependency_loaders = from_module_data.dependency_names.collect { |name| @index[name].public_loader }
@loaders.add_loader_by_name(Loader::DependencyLoader.new(from_module_data.public_loader, "#{from_module_data.name} private", dependency_loaders))
visible_loaders = dependency_loaders + (all_module_loaders() - dependency_loaders)
@loaders.add_loader_by_name(Loader::DependencyLoader.new(from_module_data.public_loader, "#{from_module_data.name} private", visible_loaders))
end
end
end
@@ -1102,15 +1102,15 @@ def assert_no_undef(x)
end

it 'does not map :undef to empty string in arrays' do
Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0][0] }
expect(parser.evaluate_string(scope, "$a = {} $b = [$a[nope]] bazinga($b)", __FILE__)).to eq(:undef)
expect(parser.evaluate_string(scope, "bazinga([undef])", __FILE__)).to eq(:undef)
Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0][0] == :undef}
expect(parser.evaluate_string(scope, "$a = {} $b = [$a[nope]] bazinga($b)", __FILE__)).to eq(true)
expect(parser.evaluate_string(scope, "bazinga([undef])", __FILE__)).to eq(true)
end

it 'does not map :undef to empty string in hashes' do
Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0]['a'] }
expect(parser.evaluate_string(scope, "$a = {} $b = {a => $a[nope]} bazinga($b)", __FILE__)).to eq(:undef)
expect(parser.evaluate_string(scope, "bazinga({a => undef})", __FILE__)).to eq(:undef)
Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0]['a'] == :undef }
expect(parser.evaluate_string(scope, "$a = {} $b = {a => $a[nope]} bazinga($b)", __FILE__)).to eq(true)
expect(parser.evaluate_string(scope, "bazinga({a => undef})", __FILE__)).to eq(true)
end
end
end
@@ -271,7 +271,10 @@ def compile_and_get_notifications(code)
File.stubs(:read).with(usee_metadata_path, {:encoding => 'utf-8'}).raises Errno::ENOENT
File.stubs(:read).with(usee2_metadata_path, {:encoding => 'utf-8'}).raises Errno::ENOENT
Puppet[:code] = "$case_number = #{case_number}\ninclude ::user"
expect { compiler.compile }.to raise_error(Puppet::Error, /Unknown function/)
catalog = compiler.compile
resource = catalog.resource('Notify', "case_#{case_number}")
expect(resource).not_to be_nil
expect(resource['message']).to eq(desc[:expects])
end
end
end
@@ -576,22 +579,22 @@ class tstt {
expect(type).to be_a(Puppet::Pops::Types::PIntegerType)
end

it 'will not resolve implicit transitive dependencies, a -> c' do
it 'will resolve implicit transitive dependencies, a -> c' do
type = Puppet::Pops::Types::TypeParser.singleton.parse('A::N', Puppet::Pops::Loaders.find_loader('a'))
expect(type).to be_a(Puppet::Pops::Types::PTypeAliasType)
expect(type.name).to eql('A::N')
type = type.resolved_type
expect(type).to be_a(Puppet::Pops::Types::PTypeReferenceType)
expect(type.type_string).to eql('C::C')
expect(type).to be_a(Puppet::Pops::Types::PTypeAliasType)
expect(type.name).to eql('C::C')
end

it 'will not resolve reverse dependencies, b -> a' do
it 'will resolve reverse dependencies, b -> a' do
type = Puppet::Pops::Types::TypeParser.singleton.parse('B::X', Puppet::Pops::Loaders.find_loader('b'))
expect(type).to be_a(Puppet::Pops::Types::PTypeAliasType)
expect(type.name).to eql('B::X')
type = type.resolved_type
expect(type).to be_a(Puppet::Pops::Types::PTypeReferenceType)
expect(type.type_string).to eql('A::A')
expect(type).to be_a(Puppet::Pops::Types::PTypeAliasType)
expect(type.name).to eql('A::A')
end

it 'does not resolve init_typeset when more qualified type is found in typeset' do
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.