All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gems.scss_lint-0.40.1.lib.scss_lint.linter.indentation.rb Maven / Gradle / Ivy

There is a newer version: 3.7.2
Show newest version
module SCSSLint
  # Checks for consistent indentation of nested declarations and rule sets.
  class Linter::Indentation < Linter # rubocop:disable ClassLength
    include LinterRegistry

    def visit_root(_node)
      @indent_width = config['width'].to_i
      @indent_character = config['character'] || 'space'
      @indent = 0
      yield
    end

    def check_and_visit_children(node)
      # Don't continue checking children as the moment a parent's indentation is
      # off it's likely the children will be as will. We don't display the child
      # indentation problems as that would likely make the lint too noisy.
      return if check_indentation(node)

      @indent += @indent_width
      yield
      @indent -= @indent_width
    end

    def check_indentation(node)
      return unless node.line

      # Ignore the case where the node is on the same line as its previous
      # sibling or its parent, as indentation isn't possible
      return if nodes_on_same_line?(previous_node(node), node)

      if @indent_character == 'tab'
        other_character = ' '
        other_character_name = 'space'
      else
        other_character = "\t"
        other_character_name = 'tab'
      end

      check_indent_width(node, other_character, @indent_character, other_character_name)
    end

    def check_indent_width(node, other_character, character_name, other_character_name)
      actual_indent = node_indent(node)

      if actual_indent.include?(other_character)
        add_lint(node.line,
                 "Line should be indented with #{character_name}s, " \
                 "not #{other_character_name}s")
        return true
      end

      if config['allow_non_nested_indentation']
        check_arbitrary_indent(node, actual_indent.length, character_name)
      else
        check_regular_indent(node, actual_indent.length, character_name)
      end
    end

    # Deal with `else` statements, which require special care since they are
    # considered children of `if` statements.
    def visit_if(node)
      check_indentation(node)

      if config['allow_non_nested_indentation']
        yield # Continue linting else statement
      else
        visit(node.else) if node.else
      end
    end

    # Need to define this explicitly since @at-root directives can contain
    # inline selectors which produces the same parse tree as if the selector was
    # nested within it. For example:
    #
    #   @at-root {
    #     .something {
    #       ...
    #     }
    #   }
    #
    # ...and...
    #
    #   @at-root .something {
    #     ...
    #   }
    #
    # ...produce the same parse tree, but result in different indentation
    # levels.
    def visit_atroot(node, &block)
      if at_root_contains_inline_selector?(node)
        return if check_indentation(node)
        yield
      else
        check_and_visit_children(node, &block)
      end
    end

    def visit_import(node)
      prev = previous_node(node)
      return if prev.is_a?(Sass::Tree::ImportNode) && source_from_range(prev.source_range) =~ /,$/
      check_indentation(node)
    end

    # Define node types that increase indentation level
    alias_method :visit_directive, :check_and_visit_children
    alias_method :visit_each,      :check_and_visit_children
    alias_method :visit_for,       :check_and_visit_children
    alias_method :visit_function,  :check_and_visit_children
    alias_method :visit_media,     :check_and_visit_children
    alias_method :visit_mixin,     :check_and_visit_children
    alias_method :visit_mixindef,  :check_and_visit_children
    alias_method :visit_prop,      :check_and_visit_children
    alias_method :visit_rule,      :check_and_visit_children
    alias_method :visit_supports,  :check_and_visit_children
    alias_method :visit_while,     :check_and_visit_children

    # Define node types to check indentation of (notice comments are left out)
    alias_method :visit_charset,   :check_indentation
    alias_method :visit_content,   :check_indentation
    alias_method :visit_cssimport, :check_indentation
    alias_method :visit_extend,    :check_indentation
    alias_method :visit_return,    :check_indentation
    alias_method :visit_variable,  :check_indentation
    alias_method :visit_warn,      :check_indentation

  private

    def nodes_on_same_line?(node1, node2)
      return unless node1

      node1.line == node2.line ||
        (node1.source_range && node1.source_range.end_pos.line == node2.line)
    end

    def at_root_contains_inline_selector?(node)
      return unless node.children.any?
      return unless first_child_source = node.children.first.source_range

      same_position?(node.source_range.end_pos, first_child_source.start_pos)
    end

    def check_regular_indent(node, actual_indent, character_name)
      return if actual_indent == @indent

      add_lint(node.line,
               "Line should be indented #{@indent} #{character_name}s, " \
               "but was indented #{actual_indent} #{character_name}s")
      true
    end

    def check_arbitrary_indent(node, actual_indent, character_name) # rubocop:disable CyclomaticComplexity, MethodLength, LineLength
      # Allow rulesets to be indented any amount when the indent is zero, as
      # long as it's a multiple of the indent width
      if ruleset_under_root_node?(node)
        unless actual_indent % @indent_width == 0
          add_lint(node.line,
                   "Line must be indented a multiple of #{@indent_width} " \
                   "#{character_name}s, but was indented #{actual_indent} #{character_name}s")
          return true
        end
      end

      if @indent == 0
        unless node.is_a?(Sass::Tree::RuleNode) || actual_indent == 0
          add_lint(node.line,
                   "Line should be indented 0 #{character_name}s, " \
                   "but was indented #{actual_indent} #{character_name}s")
          return true
        end
      elsif !one_shift_greater_than_parent?(node, actual_indent)
        parent_indent = node_indent(node_indent_parent(node)).length
        expected_indent = parent_indent + @indent_width

        add_lint(node.line,
                 "Line should be indented #{expected_indent} #{character_name}s, " \
                 "but was indented #{actual_indent} #{character_name}s")
        return true
      end
    end

    # Returns whether node is a ruleset not nested within any other ruleset.
    #
    # @param node [Sass::Tree::Node]
    # @return [true,false]
    def ruleset_under_root_node?(node)
      @indent == 0 && node.is_a?(Sass::Tree::RuleNode)
    end

    # Returns whether node is indented exactly one indent width greater than its
    # parent.
    #
    # @param node [Sass::Tree::Node]
    # @return [true,false]
    def one_shift_greater_than_parent?(node, actual_indent)
      parent_indent = node_indent(node_indent_parent(node)).length
      expected_indent = parent_indent + @indent_width
      expected_indent == actual_indent
    end

    # Return indentation of a node.
    #
    # @param node [Sass::Tree::Node]
    # @return [Integer]
    def node_indent(node)
      engine.lines[node.line - 1][/^(\s*)/, 1]
    end

    def node_indent_parent(node)
      if else_node?(node)
        while node.node_parent.is_a?(Sass::Tree::IfNode) &&
              node.node_parent.else == node
          node = node.node_parent
        end
      end

      node.node_parent
    end
  end
end




© 2015 - 2025 Weber Informatics LLC | Privacy Policy