turboladen/tailor

View on GitHub
lib/tailor/rulers/indentation_spaces_ruler.rb

Summary

Maintainability
B
4 hrs
Test Coverage
require 'nokogiri'
require_relative '../ruler'
require_relative '../lexed_line'
require_relative '../lexer/token'
require_relative 'indentation_spaces_ruler/argument_alignment'
require_relative 'indentation_spaces_ruler/indentation_manager'
require_relative 'indentation_spaces_ruler/line_continuations'

class Tailor
  module Rulers
    class IndentationSpacesRuler < Tailor::Ruler
      def initialize(config, options)
        super(config, options)
        add_lexer_observers(
          :comment,
          :embexpr_beg,
          :embexpr_end,
          :file_beg,
          :ignored_nl,
          :kw,
          :lbrace,
          :lbracket,
          :lparen,
          :nl,
          :rbrace,
          :rbracket,
          :rparen,
          :tstring_beg,
          :tstring_end
        )
        @manager = IndentationManager.new(@config)
        @embexpr_nesting = []
        @tstring_nesting = []
      end

      def comment_update(token, lexed_line, file_text, lineno, column)
        if token.fake_backslash_line_end?
          log 'Line was altered by tailor to accommodate trailing backslash'
          @manager.add_indent_reason(:trailing_backslash, :trailing_backslash,
            lineno)
        end

        # trailing comment?
        if token.ends_with_newline?
          log 'Comment ends with newline.  Removing comment...'
          log "Old lexed line: #{lexed_line.inspect}"

          new_lexed_line = lexed_line.remove_trailing_comment(file_text)

          log "New lexed line: #{new_lexed_line.inspect}"

          if new_lexed_line.ends_with_ignored_nl?
            log 'New lexed line ends with :on_ignored_nl.'
            ignored_nl_update(new_lexed_line, lineno, column)
          elsif new_lexed_line.ends_with_nl?
            log 'New lexed line ends with :on_nl.'
            nl_update(new_lexed_line, lineno, column)
          end
        end
      end

      def embexpr_beg_update(lexed_line, lineno, column)
        @embexpr_nesting << true

        token = Tailor::Lexer::Token.new('{')
        @manager.update_for_opening_reason(:on_embexpr_beg, token, lineno)
      end

      # Due to a Ripper bug that was fixed in ruby 2.0.0-p0, this will not get
      # triggered if you're using 1.9.x.
      # More info: https://bugs.ruby-lang.org/issues/6211
      def embexpr_end_update(current_lexed_line, lineno, column)
        @embexpr_nesting.pop
        @manager.update_for_closing_reason(:on_embexpr_end, current_lexed_line)
      end

      def file_beg_update(file_name)
        # For statements that continue over multiple lines we may want to treat
        # the second and subsequent lines differently and ident them further,
        # controlled by the :line_continuations option.
        @lines = LineContinuations.new(file_name) if line_continuations?
        @args = ArgumentAlignment.new(file_name) if argument_alignment?
      end

      def ignored_nl_update(current_lexed_line, lineno, column)
        log "indent reasons on entry: #{@manager.indent_reasons}"

        if current_lexed_line.only_spaces?
          log 'Line of only spaces.  Moving on.'
          # todo: maybe i shouldn't return here? ...do transitions?
          return
        end

        if @manager.line_ends_with_single_token_indenter?(current_lexed_line)
          log 'Line ends with single-token indent token.'

          unless @manager.in_an_enclosure? &&
            current_lexed_line.ends_with_comma?
            log 'Line-ending single-token indenter found.'
            token_event = current_lexed_line.last_non_line_feed_event

            unless @manager.line_ends_with_same_as_last token_event
              msg = 'Line ends with different type of single-token '
              msg << "indenter: #{token_event}"
              log msg
              @manager.add_indent_reason(token_event[1], token_event.last,
                lineno)
            end
          end
        end

        @manager.update_actual_indentation(current_lexed_line)
        measure(lineno, column)

        log "indent reasons on exit: #{@manager.indent_reasons}"
        # prep for next line
        @manager.transition_lines
      end

      def kw_update(token, lexed_line, lineno, column)
        if lexed_line.keyword_is_symbol?
          log "Keyword is prefaced by a :, indicating it's really a Symbol."
          return
        end

        if token == 'end'
          @manager.update_for_closing_reason(:on_kw, lexed_line)
          return
        end

        if token.continuation_keyword?
          log "Continuation keyword found: '#{token}'."
          @manager.update_for_continuation_reason(token, lexed_line, lineno)
          return
        end

        if token.keyword_to_indent?
          log "Indent keyword found: '#{token}'."
          @manager.update_for_opening_reason(:on_kw, token, lineno)
        end
      end

      def lbrace_update(lexed_line, lineno, column)
        token = Tailor::Lexer::Token.new('{')
        @manager.update_for_opening_reason(:on_lbrace, token, lineno)
      end

      def lbracket_update(lexed_line, lineno, column)
        token = Tailor::Lexer::Token.new('[')
        @manager.update_for_opening_reason(:on_lbracket, token, lineno)
      end

      def lparen_update(lineno, column)
        token = Tailor::Lexer::Token.new('(')
        @manager.update_for_opening_reason(:on_lparen, token, lineno)
      end

      def nl_update(current_lexed_line, lineno, column)
        log "indent reasons on entry: #{@manager.indent_reasons}"
        @manager.update_actual_indentation(current_lexed_line)

        if @manager.last_indent_reason_type != :on_kw &&
          @manager.last_indent_reason_type != :on_lbrace &&
          @manager.last_indent_reason_type != :on_lbracket &&
          @manager.last_indent_reason_type != :on_lparen &&
          !@manager.last_indent_reason_type.nil?
          log "last indent reason type: #{@manager.last_indent_reason_type}"
          log 'I think this is a single-token closing line...'

          @manager.update_for_closing_reason(@manager.indent_reasons.
            last[:event_type], current_lexed_line)
        end

        unless current_lexed_line.end_of_multi_line_string?
          measure(lineno, column)
        end

        log "indent reasons on exit: #{@manager.indent_reasons}"
        @manager.transition_lines
      end

      # Since Ripper in Ruby 1.9.x parses the } in a #{} as :on_rbrace instead of
      # :on_embexpr_end, this works around that by using +@embexpr_beg to track
      # the state of that event.  As such, this should only be called from
      # #rbrace_update.
      #
      # @return [Boolean]
      def in_embexpr?
        !@embexpr_nesting.empty?
      end

      def rbrace_update(current_lexed_line, lineno, column)
        # Is this an rbrace that should've been parsed as an embexpr_end?
        if in_embexpr? && RUBY_VERSION < '2.0.0'
          msg = 'Got :rbrace and @embexpr_beg is true. '
          msg << ' Must be at an @embexpr_end.'
          log msg
          @embexpr_nesting.pop
          @manager.update_for_closing_reason(:on_embexpr_end,
            current_lexed_line)

          return
        end

        @manager.update_for_closing_reason(:on_rbrace, current_lexed_line)
      end

      def rbracket_update(current_lexed_line, lineno, column)
        @manager.update_for_closing_reason(:on_rbracket, current_lexed_line)
      end

      def rparen_update(current_lexed_line, lineno, column)
        @manager.update_for_closing_reason(:on_rparen, current_lexed_line)
      end

      def tstring_beg_update(lexed_line, lineno)
        @tstring_nesting << lineno
        @manager.update_actual_indentation(lexed_line)
        log "tstring_nesting is now: #{@tstring_nesting}"
        @manager.stop
      end

      def tstring_end_update(current_line)
        unless @tstring_nesting.empty?
          tstring_start_line = @tstring_nesting.pop

          if tstring_start_line < current_line
            measure(tstring_start_line, @manager.actual_indentation)
          end
        end

        @manager.start unless in_tstring?
      end

      def in_tstring?
        !@tstring_nesting.empty?
      end

      def line_continuations?
        @options[:line_continuations] and @options[:line_continuations] != :off
      end

      def with_line_continuations(lineno, should_be_at)
        return should_be_at unless line_continuations?
        if @lines.line_is_continuation?(lineno) and
          @lines.line_has_nested_statements?(lineno)
          should_be_at + @config
        else
          should_be_at
        end
      end

      def argument_alignment?
        @options[:argument_alignment] and @options[:argument_alignment] != :off
      end

      def with_argument_alignment(lineno, should_be_at)
        return should_be_at unless argument_alignment?
        @args.expected_column(lineno, should_be_at)
      end

      # Checks if the line's indentation level is appropriate.
      #
      # @param [Fixnum] lineno The line the potential problem is on.
      # @param [Fixnum] column The column the potential problem is on.
      def measure(lineno, column)
        log 'Measuring...'

        should_be_at = with_line_continuations(lineno, @manager.should_be_at)
        should_be_at = with_argument_alignment(lineno, should_be_at)

        if @manager.actual_indentation != should_be_at
          msg = "Line is indented to column #{@manager.actual_indentation}, "
          msg << "but should be at #{should_be_at}."

          @problems << Problem.new(problem_type, lineno,
            @manager.actual_indentation, msg, @options[:level])
        end
      end
    end
  end
end