blob: 70d01ec4526a0a91784815cc691f99edd92a2eae [file] [log] [blame]
require 'sass/script/functions'
require 'sass/util'
module Sass::Script::Tree
# A SassScript parse node representing a function call.
#
# A function call either calls one of the functions in
# {Sass::Script::Functions}, or if no function with the given name exists it
# returns a string representation of the function call.
class Funcall < Node
# The name of the function.
#
# @return [String]
attr_reader :name
# The callable to be invoked
#
# @return [Sass::Callable] or nil if no callable is provided.
attr_reader :callable
# The arguments to the function.
#
# @return [Array<Node>]
attr_reader :args
# The keyword arguments to the function.
#
# @return [Sass::Util::NormalizedMap<Node>]
attr_reader :keywords
# The first splat argument for this function, if one exists.
#
# This could be a list of positional arguments, a map of keyword
# arguments, or an arglist containing both.
#
# @return [Node?]
attr_accessor :splat
# The second splat argument for this function, if one exists.
#
# If this exists, it's always a map of keyword arguments, and
# \{#splat} is always either a list or an arglist.
#
# @return [Node?]
attr_accessor :kwarg_splat
# @param name_or_callable [String, Sass::Callable] See \{#name}
# @param args [Array<Node>] See \{#args}
# @param keywords [Sass::Util::NormalizedMap<Node>] See \{#keywords}
# @param splat [Node] See \{#splat}
# @param kwarg_splat [Node] See \{#kwarg_splat}
def initialize(name_or_callable, args, keywords, splat, kwarg_splat)
if name_or_callable.is_a?(Sass::Callable)
@callable = name_or_callable
@name = name_or_callable.name
else
@callable = nil
@name = name_or_callable
end
@args = args
@keywords = keywords
@splat = splat
@kwarg_splat = kwarg_splat
super()
end
# @return [String] A string representation of the function call
def inspect
args = @args.map {|a| a.inspect}.join(', ')
keywords = @keywords.as_stored.to_a.map {|k, v| "$#{k}: #{v.inspect}"}.join(', ')
# rubocop:disable RedundantSelf
if self.splat
splat = args.empty? && keywords.empty? ? "" : ", "
splat = "#{splat}#{self.splat.inspect}..."
splat = "#{splat}, #{kwarg_splat.inspect}..." if kwarg_splat
end
# rubocop:enable RedundantSelf
"#{name}(#{args}#{', ' unless args.empty? || keywords.empty?}#{keywords}#{splat})"
end
# @see Node#to_sass
def to_sass(opts = {})
arg_to_sass = lambda do |arg|
sass = arg.to_sass(opts)
sass = "(#{sass})" if arg.is_a?(Sass::Script::Tree::ListLiteral) && arg.separator == :comma
sass
end
args = @args.map(&arg_to_sass)
keywords = @keywords.as_stored.to_a.map {|k, v| "$#{dasherize(k, opts)}: #{arg_to_sass[v]}"}
# rubocop:disable RedundantSelf
if self.splat
splat = "#{arg_to_sass[self.splat]}..."
kwarg_splat = "#{arg_to_sass[self.kwarg_splat]}..." if self.kwarg_splat
end
# rubocop:enable RedundantSelf
arglist = [args, splat, keywords, kwarg_splat].flatten.compact.join(', ')
"#{dasherize(name, opts)}(#{arglist})"
end
# Returns the arguments to the function.
#
# @return [Array<Node>]
# @see Node#children
def children
res = @args + @keywords.values
res << @splat if @splat
res << @kwarg_splat if @kwarg_splat
res
end
# @see Node#deep_copy
def deep_copy
node = dup
node.instance_variable_set('@args', args.map {|a| a.deep_copy})
copied_keywords = Sass::Util::NormalizedMap.new
@keywords.as_stored.each {|k, v| copied_keywords[k] = v.deep_copy}
node.instance_variable_set('@keywords', copied_keywords)
node
end
protected
# Evaluates the function call.
#
# @param environment [Sass::Environment] The environment in which to evaluate the SassScript
# @return [Sass::Script::Value] The SassScript object that is the value of the function call
# @raise [Sass::SyntaxError] if the function call raises an ArgumentError
def _perform(environment)
args = @args.each_with_index.
map {|a, i| perform_arg(a, environment, signature && signature.args[i])}
keywords = Sass::Util.map_hash(@keywords) do |k, v|
[k, perform_arg(v, environment, k.tr('-', '_'))]
end
splat = Sass::Tree::Visitors::Perform.perform_splat(
@splat, keywords, @kwarg_splat, environment)
fn = @callable || environment.function(@name)
if fn && fn.origin == :stylesheet
environment.stack.with_function(filename, line, name) do
return without_original(perform_sass_fn(fn, args, splat, environment))
end
end
args = construct_ruby_args(ruby_name, args, splat, environment)
if Sass::Script::Functions.callable?(ruby_name) && (!fn || fn.origin == :builtin)
local_environment = Sass::Environment.new(environment.global_env, environment.options)
local_environment.caller = Sass::ReadOnlyEnvironment.new(environment, environment.options)
result = local_environment.stack.with_function(filename, line, name) do
opts(Sass::Script::Functions::EvaluationContext.new(
local_environment).send(ruby_name, *args))
end
without_original(result)
else
opts(to_literal(args))
end
rescue ArgumentError => e
reformat_argument_error(e)
end
# Compass historically overrode this before it changed name to {Funcall#to_value}.
# We should get rid of it in the future.
def to_literal(args)
to_value(args)
end
# This method is factored out from `_perform` so that compass can override
# it with a cross-browser implementation for functions that require vendor prefixes
# in the generated css.
def to_value(args)
Sass::Script::Value::String.new("#{name}(#{args.join(', ')})")
end
private
def ruby_name
@ruby_name ||= @name.tr('-', '_')
end
def perform_arg(argument, environment, name)
return argument if signature && signature.delayed_args.include?(name)
argument.perform(environment)
end
def signature
@signature ||= Sass::Script::Functions.signature(name.to_sym, @args.size, @keywords.size)
end
def without_original(value)
return value unless value.is_a?(Sass::Script::Value::Number)
value = value.dup
value.original = nil
value
end
def construct_ruby_args(name, args, splat, environment)
args += splat.to_a if splat
# All keywords are contained in splat.keywords for consistency,
# even if there were no splats passed in.
old_keywords_accessed = splat.keywords_accessed
keywords = splat.keywords
splat.keywords_accessed = old_keywords_accessed
unless (signature = Sass::Script::Functions.signature(name.to_sym, args.size, keywords.size))
return args if keywords.empty?
raise Sass::SyntaxError.new("Function #{name} doesn't support keyword arguments")
end
# If the user passes more non-keyword args than the function expects,
# but it does expect keyword args, Ruby's arg handling won't raise an error.
# Since we don't want to make functions think about this,
# we'll handle it for them here.
if signature.var_kwargs && !signature.var_args && args.size > signature.args.size
raise Sass::SyntaxError.new(
"#{args[signature.args.size].inspect} is not a keyword argument for `#{name}'")
elsif keywords.empty?
args << {} if signature.var_kwargs
return args
end
argnames = signature.args[args.size..-1] || []
deprecated_argnames = (signature.deprecated && signature.deprecated[args.size..-1]) || []
args += argnames.zip(deprecated_argnames).map do |(argname, deprecated_argname)|
if keywords.has_key?(argname)
keywords.delete(argname)
elsif deprecated_argname && keywords.has_key?(deprecated_argname)
deprecated_argname = keywords.denormalize(deprecated_argname)
Sass::Util.sass_warn("DEPRECATION WARNING: The `$#{deprecated_argname}' argument for " +
"`#{@name}()' has been renamed to `$#{argname}'.")
keywords.delete(deprecated_argname)
else
raise Sass::SyntaxError.new("Function #{name} requires an argument named $#{argname}")
end
end
if keywords.size > 0
if signature.var_kwargs
# Don't pass a NormalizedMap to a Ruby function.
args << keywords.to_hash
else
argname = keywords.keys.sort.first
if signature.args.include?(argname)
raise Sass::SyntaxError.new(
"Function #{name} was passed argument $#{argname} both by position and by name")
else
raise Sass::SyntaxError.new(
"Function #{name} doesn't have an argument named $#{argname}")
end
end
end
args
end
def perform_sass_fn(function, args, splat, environment)
Sass::Tree::Visitors::Perform.perform_arguments(function, args, splat, environment) do |env|
env.caller = Sass::Environment.new(environment)
val = catch :_sass_return do
function.tree.each {|c| Sass::Tree::Visitors::Perform.visit(c, env)}
raise Sass::SyntaxError.new("Function #{@name} finished without @return")
end
val
end
end
def reformat_argument_error(e)
message = e.message
# If this is a legitimate Ruby-raised argument error, re-raise it.
# Otherwise, it's an error in the user's stylesheet, so wrap it.
if Sass::Util.rbx?
# Rubinius has a different error report string than vanilla Ruby. It
# also doesn't put the actual method for which the argument error was
# thrown in the backtrace, nor does it include `send`, so we look for
# `_perform`.
if e.message =~ /^method '([^']+)': given (\d+), expected (\d+)/
error_name, given, expected = $1, $2, $3
raise e if error_name != ruby_name || e.backtrace[0] !~ /:in `_perform'$/
message = "wrong number of arguments (#{given} for #{expected})"
end
elsif Sass::Util.jruby?
should_maybe_raise =
e.message =~ /^wrong number of arguments calling `[^`]+` \((\d+) for (\d+)\)/
given, expected = $1, $2
if should_maybe_raise
# JRuby 1.7 includes __send__ before send and _perform.
trace = e.backtrace.dup
raise e if trace.shift !~ /:in `__send__'$/
# JRuby (as of 1.7.2) doesn't put the actual method
# for which the argument error was thrown in the backtrace, so we
# detect whether our send threw an argument error.
if !(trace[0] =~ /:in `send'$/ && trace[1] =~ /:in `_perform'$/)
raise e
else
# JRuby 1.7 doesn't use standard formatting for its ArgumentErrors.
message = "wrong number of arguments (#{given} for #{expected})"
end
end
elsif (md = /^wrong number of arguments \(given (\d+), expected (\d+)\)/.match(e.message)) &&
e.backtrace[0] =~ /:in `#{ruby_name}'$/
# Handle ruby 2.3 error formatting
message = "wrong number of arguments (#{md[1]} for #{md[2]})"
elsif e.message =~ /^wrong number of arguments/ &&
e.backtrace[0] !~ /:in `(block in )?#{ruby_name}'$/
raise e
end
raise Sass::SyntaxError.new("#{message} for `#{name}'")
end
end
end