From fb8a5002126bae65ef9a1fd8a89218aaedfa5788 Mon Sep 17 00:00:00 2001 From: Nathan Weizenbaum Date: Sun, 21 Feb 2010 16:17:12 -0800 Subject: [PATCH] [Sass] [SCSS] Make sure SCSS line numbers are reported correctly, even in the presence of SassScript. --- lib/sass/script/funcall.rb | 20 +++++++------- lib/sass/script/lexer.rb | 2 ++ lib/sass/script/literal.rb | 18 +++++++------ lib/sass/script/node.rb | 24 ++++++++++++++++- lib/sass/script/operation.rb | 20 +++++++------- lib/sass/script/parser.rb | 34 +++++++++++++++++++----- lib/sass/script/unary_operation.rb | 30 +++++++++++---------- lib/sass/script/variable.rb | 22 +++++++++------- lib/sass/scss/parser.rb | 7 +++-- test/sass/scss/scss_test.rb | 42 +++++++++++++++++++++++++++++- 10 files changed, 158 insertions(+), 61 deletions(-) diff --git a/lib/sass/script/funcall.rb b/lib/sass/script/funcall.rb index d7ff00a8..cd59cffa 100644 --- a/lib/sass/script/funcall.rb +++ b/lib/sass/script/funcall.rb @@ -29,12 +29,22 @@ module Sass "#{name}(#{args.map {|a| a.inspect}.join(', ')})" end + # Returns the arguments to the function. + # + # @return [Array] + # @see Node#children + def children + @args + end + + protected + # Evaluates the function call. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Literal] The SassScript object that is the value of the function call # @raise [Sass::SyntaxError] if the function call raises an ArgumentError - def perform(environment) + def _perform(environment) args = self.args.map {|a| a.perform(environment)} ruby_name = name.gsub('-', '_') unless Haml::Util.has?(:public_instance_method, Functions, ruby_name) && ruby_name !~ /^__/ @@ -48,14 +58,6 @@ module Sass raise e unless e.backtrace.any? {|t| t =~ /:in `(block in )?(#{name}|perform)'$/} raise Sass::SyntaxError.new("#{e.message} for `#{name}'") end - - # Returns the arguments to the function. - # - # @return [Array] - # @see Node#children - def children - @args - end end end end diff --git a/lib/sass/script/lexer.rb b/lib/sass/script/lexer.rb index 05063b96..7cbd748b 100644 --- a/lib/sass/script/lexer.rb +++ b/lib/sass/script/lexer.rb @@ -154,6 +154,8 @@ module Sass unless value raise SyntaxError.new("Syntax error in '#{@scanner.string}' at character #{current_position}.") end + + value.last.line = @line if value.last.is_a?(Script::Node) Token.new(value.first, value.last, @line, last_match_position) end diff --git a/lib/sass/script/literal.rb b/lib/sass/script/literal.rb index 9bc73ee1..1fa27bee 100644 --- a/lib/sass/script/literal.rb +++ b/lib/sass/script/literal.rb @@ -23,14 +23,6 @@ module Sass::Script @value = value end - # Evaluates the literal. - # - # @param environment [Sass::Environment] The environment in which to evaluate the SassScript - # @return [Literal] This literal - def perform(environment) - self - end - # Returns an empty array. # # @return [Array] empty @@ -206,5 +198,15 @@ MSG def to_s raise Sass::SyntaxError.new("[BUG] All subclasses of Sass::Literal must implement #to_s.") end + + protected + + # Evaluates the literal. + # + # @param environment [Sass::Environment] The environment in which to evaluate the SassScript + # @return [Literal] This literal + def _perform(environment) + self + end end end diff --git a/lib/sass/script/node.rb b/lib/sass/script/node.rb index 7f641a9e..70f9b412 100644 --- a/lib/sass/script/node.rb +++ b/lib/sass/script/node.rb @@ -8,6 +8,11 @@ module Sass::Script # @return [{Symbol => Object}] attr_reader :options + # The line of the document on which this node appeared. + # + # @return [Fixnum] + attr_accessor :line + # Sets the options hash for this node, # as well as for all child nodes. # See {file:SASS_REFERENCE.md#sass_options the Sass options documentation}. @@ -20,10 +25,16 @@ module Sass::Script # Evaluates the node. # + # \{#perform} shouldn't be overridden directly; + # instead, override \{#\_perform}. + # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Literal] The SassScript object that is the value of the SassScript def perform(environment) - raise NotImplementedError.new("All subclasses of Sass::Script::Node must override #perform.") + _perform(environment) + rescue Sass::SyntaxError => e + e.modify_backtrace(:line => line) + raise e end # Returns all child nodes of this node. @@ -32,5 +43,16 @@ module Sass::Script def children raise NotImplementedError.new("All subclasses of Sass::Script::Node must override #children.") end + + protected + + # Evaluates this node. + # + # @param environment [Sass::Environment] The environment in which to evaluate the SassScript + # @return [Literal] The SassScript object that is the value of the SassScript + # @see #perform + def _perform(environment) + raise NotImplementedError.new("All subclasses of Sass::Script::Node must override #_perform.") + end end end diff --git a/lib/sass/script/operation.rb b/lib/sass/script/operation.rb index f30dee55..b119711b 100644 --- a/lib/sass/script/operation.rb +++ b/lib/sass/script/operation.rb @@ -26,12 +26,22 @@ module Sass::Script "(#{@operator.inspect} #{@operand1.inspect} #{@operand2.inspect})" end + # Returns the operands for this operation. + # + # @return [Array] + # @see Node#children + def children + [@operand1, @operand2] + end + + protected + # Evaluates the operation. # # @param environment [Sass::Environment] The environment in which to evaluate the SassScript # @return [Literal] The SassScript object that is the value of the operation # @raise [Sass::SyntaxError] if the operation is undefined for the operands - def perform(environment) + def _perform(environment) literal1 = @operand1.perform(environment) literal2 = @operand2.perform(environment) begin @@ -43,13 +53,5 @@ module Sass::Script raise Sass::SyntaxError.new("Undefined operation: \"#{literal1} #{@operator} #{literal2}\".") end end - - # Returns the operands for this operation. - # - # @return [Array] - # @see Node#children - def children - [@operand1, @operand2] - end end end diff --git a/lib/sass/script/parser.rb b/lib/sass/script/parser.rb index ea6c825e..7b9a42cb 100644 --- a/lib/sass/script/parser.rb +++ b/lib/sass/script/parser.rb @@ -5,6 +5,13 @@ module Sass # The parser for SassScript. # It parses a string of code into a tree of {Script::Node}s. class Parser + # The line number of the parser's current position. + # + # @return [Fixnum] + def line + @lexer.line + end + # @param str [String, StringScanner] The source text to parse # @param line [Fixnum] The line on which the SassScript appears. # Used for error reporting @@ -130,7 +137,9 @@ module Sass def #{name} return unless e = #{sub} while tok = try_tok(#{ops.map {|o| o.inspect}.join(', ')}) + line = @lexer.line e = Operation.new(e, assert_expr(#{sub.inspect}), tok.type) + e.line = line end e end @@ -141,7 +150,10 @@ RUBY class_eval < e - raise e unless e.name.to_s == operator.to_s - raise Sass::SyntaxError.new("Undefined unary operation: \"#{@operator} #{literal}\".") - end - # Returns the operand of the operation. # # @return [Array] @@ -38,5 +24,21 @@ module Sass::Script def children [@operand] end + + protected + + # Evaluates the operation. + # + # @param environment [Sass::Environment] The environment in which to evaluate the SassScript + # @return [Literal] The SassScript object that is the value of the operation + # @raise [Sass::SyntaxError] if the operation is undefined for the operand + def _perform(environment) + operator = "unary_#{@operator}" + literal = @operand.perform(environment) + literal.send(operator) + rescue NoMethodError => e + raise e unless e.name.to_s == operator.to_s + raise Sass::SyntaxError.new("Undefined unary operation: \"#{@operator} #{literal}\".") + end end end diff --git a/lib/sass/script/variable.rb b/lib/sass/script/variable.rb index 29914591..5b7a80d0 100644 --- a/lib/sass/script/variable.rb +++ b/lib/sass/script/variable.rb @@ -17,16 +17,6 @@ module Sass "!#{name}" end - # Evaluates the variable. - # - # @param environment [Sass::Environment] The environment in which to evaluate the SassScript - # @return [Literal] The SassScript object that is the value of the variable - # @raise [Sass::SyntaxError] if the variable is undefined - def perform(environment) - (val = environment.var(name)) && (return val) - raise SyntaxError.new("Undefined variable: \"!#{name}\".") - end - # Returns an empty array. # # @return [Array] empty @@ -34,6 +24,18 @@ module Sass def children [] end + + protected + + # Evaluates the variable. + # + # @param environment [Sass::Environment] The environment in which to evaluate the SassScript + # @return [Literal] The SassScript object that is the value of the variable + # @raise [Sass::SyntaxError] if the variable is undefined + def _perform(environment) + (val = environment.var(name)) && (return val) + raise SyntaxError.new("Undefined variable: \"!#{name}\".") + end end end end diff --git a/lib/sass/scss/parser.rb b/lib/sass/scss/parser.rb index 06b36426..9a82b5dc 100644 --- a/lib/sass/scss/parser.rb +++ b/lib/sass/scss/parser.rb @@ -577,8 +577,11 @@ MESSAGE end def sass_script(*args) - ScriptParser.new(@scanner, @line, - @scanner.pos - (@scanner.string.rindex("\n") || 0)).send(*args) + parser = ScriptParser.new(@scanner, @line, + @scanner.pos - (@scanner.string.rindex("\n") || 0)) + result = parser.send(*args) + @line = parser.line + result end EXPR_NAMES = { diff --git a/test/sass/scss/scss_test.rb b/test/sass/scss/scss_test.rb index 4fa4e275..36b85882 100755 --- a/test/sass/scss/scss_test.rb +++ b/test/sass/scss/scss_test.rb @@ -824,7 +824,7 @@ SCSS assert_equal 2, e.sass_line end - def test_multiline_script_error + def test_multiline_script_syntax_error render < e + assert_equal "Undefined variable: \"!bang\".", e.message + assert_equal 4, e.sass_line + end + + def test_post_multiline_script_runtime_error + render < e + assert_equal "Undefined variable: \"!bop\".", e.message + assert_equal 5, e.sass_line + end + + def test_multiline_property_runtime_error + render < e + assert_equal "Undefined variable: \"!bang\".", e.message + assert_equal 4, e.sass_line + end end