Skip to content

Commit

Permalink
Keep keyword argument information attached to argument list objects.
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Aug 10, 2012
1 parent 623bb25 commit 0394886
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 23 deletions.
52 changes: 52 additions & 0 deletions lib/sass/script/arg_list.rb
@@ -0,0 +1,52 @@
module Sass::Script
# A SassScript object representing a variable argument list. This works just
# like a normal list, but can also contain keyword arguments.
#
# The keyword arguments attached to this list are unused except when this is
# passed as a glob argument to a function or mixin.
class ArgList < List
# Whether \{#keywords} has been accessed. If so, we assume that all keywords
# were valid for the function that created this ArgList.
#
# @return [Boolean]
attr_reader :keywords_accessed

# Creates a new argument list.
#
# @param value [Array<Literal>] See \{List#value}.
# @param keywords [Hash<String, Literal>] See \{#keywords}
# @param separator [String] See \{List#separator}.
def initialize(value, keywords, separator)
super(value, separator)
@keywords = keywords
end

# The keyword arguments attached to this list.
#
# @return [Hash<String, Literal>]
def keywords
@keywords_accessed = true
@keywords
end

# @see Node#children
def children
super + @keywords.values
end

# @see Node#deep_copy
def deep_copy
node = super
node.instance_variable_set('@keywords',
Sass::Util.map_hash(@keywords) {|k, v| [k, v.deep_copy]})
node
end

protected

# @see Node#_perform
def _perform(environment)
self
end
end
end
11 changes: 6 additions & 5 deletions lib/sass/script/funcall.rb
Expand Up @@ -167,12 +167,13 @@ def construct_ruby_args(name, args, environment)
end

def perform_sass_fn(function, args, keywords, splat)
environment = Sass::Tree::Visitors::Perform.perform_arguments(function, args, keywords, splat)
val = catch :_sass_return do
function.tree.each {|c| Sass::Tree::Visitors::Perform.visit(c, environment)}
raise Sass::SyntaxError.new("Function #{@name} finished without @return")
Sass::Tree::Visitors::Perform.perform_arguments(function, args, keywords, splat) do |env|
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
val
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/sass/script/list.rb
Expand Up @@ -34,7 +34,7 @@ def deep_copy
# @see Node#eq
def eq(other)
Sass::Script::Bool.new(
self.class == other.class && self.value == other.value &&
other.is_a?(List) && self.value == other.value &&
self.separator == other.separator)
end

Expand Down
1 change: 1 addition & 0 deletions lib/sass/script/literal.rb
Expand Up @@ -11,6 +11,7 @@ class Literal < Node
require 'sass/script/bool'
require 'sass/script/null'
require 'sass/script/list'
require 'sass/script/arg_list'

# Returns the Ruby value of the literal.
# The type of this value varies based on the subclass.
Expand Down
62 changes: 45 additions & 17 deletions lib/sass/tree/visitors/perform.rb
Expand Up @@ -12,15 +12,22 @@ def self.perform_arguments(callable, args, keywords, splat)
desc = "#{callable.type.capitalize} #{callable.name}"
downcase_desc = "#{callable.type} #{callable.name}"

if keywords.any?
unknown_args = keywords.keys - callable.args.map {|var| var.first.underscored_name }
if callable.splat && unknown_args.include?(callable.splat.underscored_name)
raise Sass::SyntaxError.new("Argument $#{callable.splat.name} of #{downcase_desc} cannot be used as a named argument.")
elsif unknown_args.any?
raise Sass::SyntaxError.new("#{desc} doesn't have #{unknown_args.length > 1 ? 'the following arguments:' : 'an argument named'} #{unknown_args.map{|name| "$#{name}"}.join ', '}.")
begin
unless keywords.empty?
unknown_args = keywords.keys - callable.args.map {|var| var.first.underscored_name}
if callable.splat && unknown_args.include?(callable.splat.underscored_name)
raise Sass::SyntaxError.new("Argument $#{callable.splat.name} of #{downcase_desc} cannot be used as a named argument.")
elsif unknown_args.any?
raise Sass::SyntaxError.new("#{desc} doesn't have #{unknown_args.length > 1 ? 'the following arguments:' : 'an argument named'} #{unknown_args.map{|name| "$#{name}"}.join ', '}.")
end
end
rescue Sass::SyntaxError => keyword_exception
end

# If there's no splat, raise the keyword exception immediately. The actual
# raising happens in the ensure clause at the end of this function.
return if keyword_exception && !callable.splat

if args.size > callable.args.size && !callable.splat
takes = callable.args.size
passed = args.size
Expand All @@ -33,28 +40,48 @@ def self.perform_arguments(callable, args, keywords, splat)
if splat
args += splat.to_a
splat_sep = splat.separator if splat.is_a?(Sass::Script::List)
# If the splat argument exists, there won't be any keywords passed in
# manually, so we can safely overwrite rather than merge here.
keywords = splat.keywords if splat.is_a?(Sass::Script::ArgList)
end

keywords = keywords.dup
env = Sass::Environment.new(callable.environment)
callable.args.zip(args[0...callable.args.length]) do |(var, default), value|
if value && keywords.include?(var.underscored_name)
raise Sass::SyntaxError.new("#{desc} was passed argument $#{var.name} both by position and by name.")
end

value ||= keywords[var.underscored_name]
value ||= keywords.delete(var.underscored_name)
value ||= default && default.perform(env)
raise Sass::SyntaxError.new("#{desc} is missing argument #{var.inspect}.") unless value
env.set_local_var(var.name, value)
end

if callable.splat
rest = args[callable.args.length..-1]
list = Sass::Script::List.new(rest, splat_sep)
list.options = env.options
env.set_local_var(callable.splat.name, list)
arg_list = Sass::Script::ArgList.new(rest, keywords.dup, splat_sep)
arg_list.options = env.options
env.set_local_var(callable.splat.name, arg_list)
end

env
yield env
ensure
# If there's a keyword exception, we don't want to throw it immediately,
# because the invalid keywords may be part of a glob argument that should be
# passed on to another function. So we only raise it if we reach the end of
# this function *and* the keywords attached to the argument list glob object
# haven't been accessed.
#
# The keyword exception takes precedence over any Sass errors, but not over
# non-Sass exceptions.
if keyword_exception &&
!(arg_list && arg_list.keywords_accessed) &&
($!.nil? || $!.is_a?(Sass::SyntaxError))
raise keyword_exception
elsif $!
raise $!
end
end

protected
Expand Down Expand Up @@ -228,13 +255,14 @@ def visit_mixin(node)
keywords = Sass::Util.map_hash(node.keywords) {|k, v| [k, v.perform(@environment)]}
splat = node.splat.perform(@environment) if node.splat

environment = self.class.perform_arguments(mixin, args, keywords, splat)
environment.caller = Sass::Environment.new(@environment)
environment.content = node.children if node.has_children
self.class.perform_arguments(mixin, args, keywords, splat) do |env|
env.caller = Sass::Environment.new(@environment)
env.content = node.children if node.has_children

trace_node = Sass::Tree::TraceNode.from_node(node.name, node)
with_environment(environment) {trace_node.children = mixin.tree.map {|c| visit(c)}.flatten}
trace_node
trace_node = Sass::Tree::TraceNode.from_node(node.name, node)
with_environment(env) {trace_node.children = mixin.tree.map {|c| visit(c)}.flatten}
trace_node
end
rescue Sass::SyntaxError => e
unless include_loop
e.modify_backtrace(:mixin => node.name, :line => node.line)
Expand Down

0 comments on commit 0394886

Please sign in to comment.