# A visitor for checking that all nodes are properly nested. class Sass::Tree::Visitors::CheckNesting < Sass::Tree::Visitors::Base protected def initialize @parents = [] @parent = nil @current_mixin_def = nil end def visit(node) if (error = @parent && ( try_send(@parent.class.invalid_child_method_name, @parent, node) || try_send(node.class.invalid_parent_method_name, @parent, node))) raise Sass::SyntaxError.new(error) end super rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.filename, :line => node.line) raise e end CONTROL_NODES = [Sass::Tree::EachNode, Sass::Tree::ForNode, Sass::Tree::IfNode, Sass::Tree::WhileNode, Sass::Tree::TraceNode] SCRIPT_NODES = [Sass::Tree::ImportNode] + CONTROL_NODES def visit_children(parent) old_parent = @parent # When checking a static tree, resolve at-roots to be sure they won't send # nodes where they don't belong. if parent.is_a?(Sass::Tree::AtRootNode) && parent.resolved_value old_parents = @parents @parents = @parents.reject {|p| parent.exclude_node?(p)} @parent = Sass::Util.enum_with_index(@parents.reverse). find {|p, i| !transparent_parent?(p, @parents[-i - 2])}.first begin return super ensure @parents = old_parents @parent = old_parent end end unless transparent_parent?(parent, old_parent) @parent = parent end @parents.push parent begin super ensure @parent = old_parent @parents.pop end end def visit_root(node) yield rescue Sass::SyntaxError => e e.sass_template ||= node.template raise e end def visit_import(node) yield rescue Sass::SyntaxError => e e.modify_backtrace(:filename => node.children.first.filename) e.add_backtrace(:filename => node.filename, :line => node.line) raise e end def visit_mixindef(node) @current_mixin_def, old_mixin_def = node, @current_mixin_def yield ensure @current_mixin_def = old_mixin_def end def invalid_content_parent?(parent, child) if @current_mixin_def @current_mixin_def.has_content = true nil else "@content may only be used within a mixin." end end def invalid_charset_parent?(parent, child) "@charset may only be used at the root of a document." unless parent.is_a?(Sass::Tree::RootNode) end VALID_EXTEND_PARENTS = [Sass::Tree::RuleNode, Sass::Tree::MixinDefNode, Sass::Tree::MixinNode] def invalid_extend_parent?(parent, child) return if is_any_of?(parent, VALID_EXTEND_PARENTS) "Extend directives may only be used within rules." end INVALID_IMPORT_PARENTS = CONTROL_NODES + [Sass::Tree::MixinDefNode, Sass::Tree::MixinNode] def invalid_import_parent?(parent, child) unless (@parents.map {|p| p.class} & INVALID_IMPORT_PARENTS).empty? return "Import directives may not be used within control directives or mixins." end return if parent.is_a?(Sass::Tree::RootNode) return "CSS import directives may only be used at the root of a document." if child.css_import? rescue Sass::SyntaxError => e e.modify_backtrace(:filename => child.imported_file.options[:filename]) e.add_backtrace(:filename => child.filename, :line => child.line) raise e end def invalid_mixindef_parent?(parent, child) return if (@parents.map {|p| p.class} & INVALID_IMPORT_PARENTS).empty? "Mixins may not be defined within control directives or other mixins." end def invalid_function_parent?(parent, child) return if (@parents.map {|p| p.class} & INVALID_IMPORT_PARENTS).empty? "Functions may not be defined within control directives or other mixins." end VALID_FUNCTION_CHILDREN = [ Sass::Tree::CommentNode, Sass::Tree::DebugNode, Sass::Tree::ReturnNode, Sass::Tree::VariableNode, Sass::Tree::WarnNode, Sass::Tree::ErrorNode ] + CONTROL_NODES def invalid_function_child?(parent, child) return if is_any_of?(child, VALID_FUNCTION_CHILDREN) "Functions can only contain variable declarations and control directives." end VALID_PROP_CHILDREN = CONTROL_NODES + [Sass::Tree::CommentNode, Sass::Tree::PropNode, Sass::Tree::MixinNode] def invalid_prop_child?(parent, child) return if is_any_of?(child, VALID_PROP_CHILDREN) "Illegal nesting: Only properties may be nested beneath properties." end VALID_PROP_PARENTS = [Sass::Tree::RuleNode, Sass::Tree::KeyframeRuleNode, Sass::Tree::PropNode, Sass::Tree::MixinDefNode, Sass::Tree::DirectiveNode, Sass::Tree::MixinNode] def invalid_prop_parent?(parent, child) return if is_any_of?(parent, VALID_PROP_PARENTS) "Properties are only allowed within rules, directives, mixin includes, or other properties." + child.pseudo_class_selector_message end def invalid_return_parent?(parent, child) "@return may only be used within a function." unless parent.is_a?(Sass::Tree::FunctionNode) end private # Whether `parent` should be assigned to `@parent`. def transparent_parent?(parent, grandparent) is_any_of?(parent, SCRIPT_NODES) || (parent.bubbles? && !grandparent.is_a?(Sass::Tree::RootNode) && !grandparent.is_a?(Sass::Tree::AtRootNode)) end def is_any_of?(val, classes) classes.each do |c| return true if val.is_a?(c) end false end def try_send(method, *args) return unless respond_to?(method, true) send(method, *args) end end