summaryrefslogtreecommitdiff
path: root/lib/syntax_suggest/code_line.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/syntax_suggest/code_line.rb')
-rw-r--r--lib/syntax_suggest/code_line.rb244
1 files changed, 244 insertions, 0 deletions
diff --git a/lib/syntax_suggest/code_line.rb b/lib/syntax_suggest/code_line.rb
new file mode 100644
index 0000000000..58197e95d0
--- /dev/null
+++ b/lib/syntax_suggest/code_line.rb
@@ -0,0 +1,244 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Represents a single line of code of a given source file
+ #
+ # This object contains metadata about the line such as
+ # amount of indentation, if it is empty or not, and
+ # lexical data, such as if it has an `end` or a keyword
+ # in it.
+ #
+ # Visibility of lines can be toggled off. Marking a line as invisible
+ # indicates that it should not be used for syntax checks.
+ # It's functionally the same as commenting it out.
+ #
+ # Example:
+ #
+ # line = CodeLine.from_source("def foo\n").first
+ # line.number => 1
+ # line.empty? # => false
+ # line.visible? # => true
+ # line.mark_invisible
+ # line.visible? # => false
+ #
+ class CodeLine
+ TRAILING_SLASH = ("\\" + $/).freeze
+
+ # Returns an array of CodeLine objects
+ # from the source string
+ def self.from_source(source, lines: nil)
+ lines ||= source.lines
+ lex_array_for_line = LexAll.new(source: source, source_lines: lines).each_with_object(Hash.new { |h, k| h[k] = [] }) { |lex, hash| hash[lex.line] << lex }
+ lines.map.with_index do |line, index|
+ CodeLine.new(
+ line: line,
+ index: index,
+ lex: lex_array_for_line[index + 1]
+ )
+ end
+ end
+
+ attr_reader :line, :index, :lex, :line_number, :indent
+ def initialize(line:, index:, lex:)
+ @lex = lex
+ @line = line
+ @index = index
+ @original = line
+ @line_number = @index + 1
+ strip_line = line.dup
+ strip_line.lstrip!
+
+ @indent = if (@empty = strip_line.empty?)
+ line.length - 1 # Newline removed from strip_line is not "whitespace"
+ else
+ line.length - strip_line.length
+ end
+
+ set_kw_end
+ end
+
+ # Used for stable sort via indentation level
+ #
+ # Ruby's sort is not "stable" meaning that when
+ # multiple elements have the same value, they are
+ # not guaranteed to return in the same order they
+ # were put in.
+ #
+ # So when multiple code lines have the same indentation
+ # level, they're sorted by their index value which is unique
+ # and consistent.
+ #
+ # This is mostly needed for consistency of the test suite
+ def indent_index
+ @indent_index ||= [indent, index]
+ end
+ alias_method :number, :line_number
+
+ # Returns true if the code line is determined
+ # to contain a keyword that matches with an `end`
+ #
+ # For example: `def`, `do`, `begin`, `ensure`, etc.
+ def is_kw?
+ @is_kw
+ end
+
+ # Returns true if the code line is determined
+ # to contain an `end` keyword
+ def is_end?
+ @is_end
+ end
+
+ # Used to hide lines
+ #
+ # The search alorithm will group lines into blocks
+ # then if those blocks are determined to represent
+ # valid code they will be hidden
+ def mark_invisible
+ @line = ""
+ end
+
+ # Means the line was marked as "invisible"
+ # Confusingly, "empty" lines are visible...they
+ # just don't contain any source code other than a newline ("\n").
+ def visible?
+ !line.empty?
+ end
+
+ # Opposite or `visible?` (note: different than `empty?`)
+ def hidden?
+ !visible?
+ end
+
+ # An `empty?` line is one that was originally left
+ # empty in the source code, while a "hidden" line
+ # is one that we've since marked as "invisible"
+ def empty?
+ @empty
+ end
+
+ # Opposite of `empty?` (note: different than `visible?`)
+ def not_empty?
+ !empty?
+ end
+
+ # Renders the given line
+ #
+ # Also allows us to represent source code as
+ # an array of code lines.
+ #
+ # When we have an array of code line elements
+ # calling `join` on the array will call `to_s`
+ # on each element, which essentially converts
+ # it back into it's original source string.
+ def to_s
+ line
+ end
+
+ # When the code line is marked invisible
+ # we retain the original value of it's line
+ # this is useful for debugging and for
+ # showing extra context
+ #
+ # DisplayCodeWithLineNumbers will render
+ # all lines given to it, not just visible
+ # lines, it uses the original method to
+ # obtain them.
+ attr_reader :original
+
+ # Comparison operator, needed for equality
+ # and sorting
+ def <=>(other)
+ index <=> other.index
+ end
+
+ # [Not stable API]
+ #
+ # Lines that have a `on_ignored_nl` type token and NOT
+ # a `BEG` type seem to be a good proxy for the ability
+ # to join multiple lines into one.
+ #
+ # This predicate method is used to determine when those
+ # two criteria have been met.
+ #
+ # The one known case this doesn't handle is:
+ #
+ # Ripper.lex <<~EOM
+ # a &&
+ # b ||
+ # c
+ # EOM
+ #
+ # For some reason this introduces `on_ignore_newline` but with BEG type
+ def ignore_newline_not_beg?
+ @ignore_newline_not_beg
+ end
+
+ # Determines if the given line has a trailing slash
+ #
+ # lines = CodeLine.from_source(<<~EOM)
+ # it "foo" \
+ # EOM
+ # expect(lines.first.trailing_slash?).to eq(true)
+ #
+ if SyntaxSuggest.use_prism_parser?
+ def trailing_slash?
+ last = @lex.last
+ last&.type == :on_tstring_end
+ end
+ else
+ def trailing_slash?
+ last = @lex.last
+ return false unless last
+ return false unless last.type == :on_sp
+
+ last.token == TRAILING_SLASH
+ end
+ end
+
+ # Endless method detection
+ #
+ # From https://github.com/ruby/irb/commit/826ae909c9c93a2ddca6f9cfcd9c94dbf53d44ab
+ # Detecting a "oneliner" seems to need a state machine.
+ # This can be done by looking mostly at the "state" (last value):
+ #
+ # ENDFN -> BEG (token = '=' ) -> END
+ #
+ private def set_kw_end
+ oneliner_count = 0
+ in_oneliner_def = nil
+
+ kw_count = 0
+ end_count = 0
+
+ @ignore_newline_not_beg = false
+ @lex.each do |lex|
+ kw_count += 1 if lex.is_kw?
+ end_count += 1 if lex.is_end?
+
+ if lex.type == :on_ignored_nl
+ @ignore_newline_not_beg = !lex.expr_beg?
+ end
+
+ if in_oneliner_def.nil?
+ in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN)
+ elsif lex.state.allbits?(Ripper::EXPR_ENDFN)
+ # Continue
+ elsif lex.state.allbits?(Ripper::EXPR_BEG)
+ in_oneliner_def = :BODY if lex.token == "="
+ elsif lex.state.allbits?(Ripper::EXPR_END)
+ # We found an endless method, count it
+ oneliner_count += 1 if in_oneliner_def == :BODY
+
+ in_oneliner_def = nil
+ else
+ in_oneliner_def = nil
+ end
+ end
+
+ kw_count -= oneliner_count
+
+ @is_kw = (kw_count - end_count) > 0
+ @is_end = (end_count - kw_count) > 0
+ end
+ end
+end