dead_end 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,44 +4,47 @@ module DeadEnd
4
4
  # Represents a single line of code of a given source file
5
5
  #
6
6
  # This object contains metadata about the line such as
7
- # amount of indentation. An if it is empty or not.
7
+ # amount of indentation, if it is empty or not, and
8
+ # lexical data, such as if it has an `end` or a keyword
9
+ # in it.
8
10
  #
9
- # While a given search for syntax errors is being performed
10
- # state about the search can be stored in individual lines such
11
- # as :valid or :invalid.
12
- #
13
- # Visibility of lines can be toggled on and off.
11
+ # Visibility of lines can be toggled off. Marking a line as invisible
12
+ # indicates that it should not be used for syntax checks.
13
+ # It's functionally the same as commenting it out.
14
14
  #
15
15
  # Example:
16
16
  #
17
- # line = CodeLine.new(line: "def foo\n", index: 0)
18
- # line.line_number => 1
17
+ # line = CodeLine.from_source("def foo\n").first
18
+ # line.number => 1
19
19
  # line.empty? # => false
20
20
  # line.visible? # => true
21
21
  # line.mark_invisible
22
22
  # line.visible? # => false
23
23
  #
24
- # A CodeBlock is made of multiple CodeLines
25
- #
26
- # Marking a line as invisible indicates that it should not be used
27
- # for syntax checks. It's essentially the same as commenting it out
28
- #
29
- # Marking a line as invisible also lets the overall program know
30
- # that it should not check that area for syntax errors.
31
24
  class CodeLine
32
25
  TRAILING_SLASH = ("\\" + $/).freeze
33
26
 
34
- def self.parse(source)
27
+ # Returns an array of CodeLine objects
28
+ # from the source string
29
+ def self.from_source(source)
30
+ lex_array_for_line = LexAll.new(source: source).each_with_object(Hash.new { |h, k| h[k] = [] }) { |lex, hash| hash[lex.line] << lex }
35
31
  source.lines.map.with_index do |line, index|
36
- CodeLine.new(line: line, index: index)
32
+ CodeLine.new(
33
+ line: line,
34
+ index: index,
35
+ lex: lex_array_for_line[index + 1]
36
+ )
37
37
  end
38
38
  end
39
39
 
40
- attr_reader :line, :index, :indent, :original_line
40
+ attr_reader :line, :index, :lex, :line_number, :indent
41
+ def initialize(line:, index:, lex:)
42
+ @lex = lex
43
+ @line = line
44
+ @index = index
45
+ @original = line.freeze
46
+ @line_number = @index + 1
41
47
 
42
- def initialize(line:, index:)
43
- @original_line = line.freeze
44
- @line = @original_line
45
48
  if line.strip.empty?
46
49
  @empty = true
47
50
  @indent = 0
@@ -49,102 +52,182 @@ module DeadEnd
49
52
  @empty = false
50
53
  @indent = SpaceCount.indent(line)
51
54
  end
52
- @index = index
53
- @status = nil # valid, invalid, unknown
54
- @invalid = false
55
55
 
56
- lex_detect!
57
- end
58
-
59
- private def lex_detect!
60
- lex_array = LexAll.new(source: line)
61
56
  kw_count = 0
62
57
  end_count = 0
63
- lex_array.each_with_index do |lex, index|
64
- next unless lex.type == :on_kw
65
-
66
- case lex.token
67
- when "if", "unless", "while", "until"
68
- # Only count if/unless when it's not a "trailing" if/unless
69
- # https://github.com/ruby/ruby/blob/06b44f819eb7b5ede1ff69cecb25682b56a1d60c/lib/irb/ruby-lex.rb#L374-L375
70
- kw_count += 1 unless lex.expr_label?
71
- when "def", "case", "for", "begin", "class", "module", "do"
72
- kw_count += 1
73
- when "end"
74
- end_count += 1
75
- end
58
+ @lex.each do |lex|
59
+ kw_count += 1 if lex.is_kw?
60
+ end_count += 1 if lex.is_end?
76
61
  end
77
62
 
78
- @is_comment = lex_array.detect { |lex| lex.type != :on_sp }&.type == :on_comment
79
- return if @is_comment
63
+ kw_count -= oneliner_method_count
64
+
80
65
  @is_kw = (kw_count - end_count) > 0
81
66
  @is_end = (end_count - kw_count) > 0
82
- @is_trailing_slash = lex_array.last.token == TRAILING_SLASH
83
- end
84
-
85
- alias_method :original, :original_line
86
-
87
- def trailing_slash?
88
- @is_trailing_slash
89
67
  end
90
68
 
69
+ # Used for stable sort via indentation level
70
+ #
71
+ # Ruby's sort is not "stable" meaning that when
72
+ # multiple elements have the same value, they are
73
+ # not guaranteed to return in the same order they
74
+ # were put in.
75
+ #
76
+ # So when multiple code lines have the same indentation
77
+ # level, they're sorted by their index value which is unique
78
+ # and consistent.
79
+ #
80
+ # This is mostly needed for consistency of the test suite
91
81
  def indent_index
92
82
  @indent_index ||= [indent, index]
93
83
  end
84
+ alias_method :number, :line_number
94
85
 
95
- def <=>(other)
96
- index <=> other.index
97
- end
98
-
99
- def is_comment?
100
- @is_comment
101
- end
102
-
103
- def not_comment?
104
- !is_comment?
105
- end
106
-
86
+ # Returns true if the code line is determined
87
+ # to contain a keyword that matches with an `end`
88
+ #
89
+ # For example: `def`, `do`, `begin`, `ensure`, etc.
107
90
  def is_kw?
108
91
  @is_kw
109
92
  end
110
93
 
94
+ # Returns true if the code line is determined
95
+ # to contain an `end` keyword
111
96
  def is_end?
112
97
  @is_end
113
98
  end
114
99
 
100
+ # Used to hide lines
101
+ #
102
+ # The search alorithm will group lines into blocks
103
+ # then if those blocks are determined to represent
104
+ # valid code they will be hidden
115
105
  def mark_invisible
116
106
  @line = ""
117
- self
118
- end
119
-
120
- def mark_visible
121
- @line = @original_line
122
- self
123
107
  end
124
108
 
109
+ # Means the line was marked as "invisible"
110
+ # Confusingly, "empty" lines are visible...they
111
+ # just don't contain any source code other than a newline ("\n").
125
112
  def visible?
126
113
  !line.empty?
127
114
  end
128
115
 
116
+ # Opposite or `visible?` (note: different than `empty?`)
129
117
  def hidden?
130
118
  !visible?
131
119
  end
132
120
 
133
- def line_number
134
- index + 1
121
+ # An `empty?` line is one that was originally left
122
+ # empty in the source code, while a "hidden" line
123
+ # is one that we've since marked as "invisible"
124
+ def empty?
125
+ @empty
135
126
  end
136
- alias_method :number, :line_number
137
127
 
128
+ # Opposite of `empty?` (note: different than `visible?`)
138
129
  def not_empty?
139
130
  !empty?
140
131
  end
141
132
 
142
- def empty?
143
- @empty
144
- end
145
-
133
+ # Renders the given line
134
+ #
135
+ # Also allows us to represent source code as
136
+ # an array of code lines.
137
+ #
138
+ # When we have an array of code line elements
139
+ # calling `join` on the array will call `to_s`
140
+ # on each element, which essentially converts
141
+ # it back into it's original source string.
146
142
  def to_s
147
143
  line
148
144
  end
145
+
146
+ # When the code line is marked invisible
147
+ # we retain the original value of it's line
148
+ # this is useful for debugging and for
149
+ # showing extra context
150
+ #
151
+ # DisplayCodeWithLineNumbers will render
152
+ # all lines given to it, not just visible
153
+ # lines, it uses the original method to
154
+ # obtain them.
155
+ attr_reader :original
156
+
157
+ # Comparison operator, needed for equality
158
+ # and sorting
159
+ def <=>(other)
160
+ index <=> other.index
161
+ end
162
+
163
+ # [Not stable API]
164
+ #
165
+ # Lines that have a `on_ignored_nl` type token and NOT
166
+ # a `BEG` type seem to be a good proxy for the ability
167
+ # to join multiple lines into one.
168
+ #
169
+ # This predicate method is used to determine when those
170
+ # two criteria have been met.
171
+ #
172
+ # The one known case this doesn't handle is:
173
+ #
174
+ # Ripper.lex <<~EOM
175
+ # a &&
176
+ # b ||
177
+ # c
178
+ # EOM
179
+ #
180
+ # For some reason this introduces `on_ignore_newline` but with BEG type
181
+ def ignore_newline_not_beg?
182
+ lex_value = lex.detect { |l| l.type == :on_ignored_nl }
183
+ !!(lex_value && !lex_value.expr_beg?)
184
+ end
185
+
186
+ # Determines if the given line has a trailing slash
187
+ #
188
+ # lines = CodeLine.from_source(<<~EOM)
189
+ # it "foo" \
190
+ # EOM
191
+ # expect(lines.first.trailing_slash?).to eq(true)
192
+ #
193
+ def trailing_slash?
194
+ last = @lex.last
195
+ return false unless last
196
+ return false unless last.type == :on_sp
197
+
198
+ last.token == TRAILING_SLASH
199
+ end
200
+
201
+ # Endless method detection
202
+ #
203
+ # From https://github.com/ruby/irb/commit/826ae909c9c93a2ddca6f9cfcd9c94dbf53d44ab
204
+ # Detecting a "oneliner" seems to need a state machine.
205
+ # This can be done by looking mostly at the "state" (last value):
206
+ #
207
+ # ENDFN -> BEG (token = '=' ) -> END
208
+ #
209
+ private def oneliner_method_count
210
+ oneliner_count = 0
211
+ in_oneliner_def = nil
212
+
213
+ @lex.each do |lex|
214
+ if in_oneliner_def.nil?
215
+ in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN)
216
+ elsif lex.state.allbits?(Ripper::EXPR_ENDFN)
217
+ # Continue
218
+ elsif lex.state.allbits?(Ripper::EXPR_BEG)
219
+ in_oneliner_def = :BODY if lex.token == "="
220
+ elsif lex.state.allbits?(Ripper::EXPR_END)
221
+ # We found an endless method, count it
222
+ oneliner_count += 1 if in_oneliner_def == :BODY
223
+
224
+ in_oneliner_def = nil
225
+ else
226
+ in_oneliner_def = nil
227
+ end
228
+ end
229
+
230
+ oneliner_count
231
+ end
149
232
  end
150
233
  end
@@ -3,11 +3,19 @@
3
3
  module DeadEnd
4
4
  # Searches code for a syntax error
5
5
  #
6
+ # There are three main phases in the algorithm:
7
+ #
8
+ # 1. Sanitize/format input source
9
+ # 2. Search for invalid blocks
10
+ # 3. Format invalid blocks into something meaninful
11
+ #
12
+ # This class handles the part.
13
+ #
6
14
  # The bulk of the heavy lifting is done in:
7
15
  #
8
16
  # - CodeFrontier (Holds information for generating blocks and determining if we can stop searching)
9
17
  # - ParseBlocksFromLine (Creates blocks into the frontier)
10
- # - BlockExpand (Expands existing blocks to search more code
18
+ # - BlockExpand (Expands existing blocks to search more code)
11
19
  #
12
20
  # ## Syntax error detection
13
21
  #
@@ -31,28 +39,24 @@ module DeadEnd
31
39
 
32
40
  public
33
41
 
34
- public
35
-
36
42
  attr_reader :invalid_blocks, :record_dir, :code_lines
37
43
 
38
44
  def initialize(source, record_dir: ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
39
- @source = source
40
45
  if record_dir
41
46
  @time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
42
47
  @record_dir = Pathname(record_dir).join(@time).tap { |p| p.mkpath }
43
48
  @write_count = 0
44
49
  end
45
- code_lines = source.lines.map.with_index do |line, i|
46
- CodeLine.new(line: line, index: i)
47
- end
48
50
 
49
- @code_lines = TrailingSlashJoin.new(code_lines: code_lines).call
51
+ @tick = 0
52
+ @source = source
53
+ @name_tick = Hash.new { |hash, k| hash[k] = 0 }
54
+ @invalid_blocks = []
55
+
56
+ @code_lines = CleanDocument.new(source: source).call.lines
50
57
 
51
58
  @frontier = CodeFrontier.new(code_lines: @code_lines)
52
- @invalid_blocks = []
53
- @name_tick = Hash.new { |hash, k| hash[k] = 0 }
54
- @tick = 0
55
- @block_expand = BlockExpand.new(code_lines: code_lines)
59
+ @block_expand = BlockExpand.new(code_lines: @code_lines)
56
60
  @parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
57
61
  end
58
62
 
@@ -63,10 +67,10 @@ module DeadEnd
63
67
  filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt"
64
68
  if ENV["DEBUG"]
65
69
  puts "\n\n==== #{filename} ===="
66
- puts "\n```#{block.starts_at}:#{block.ends_at}"
70
+ puts "\n```#{block.starts_at}..#{block.ends_at}"
67
71
  puts block.to_s
68
72
  puts "```"
69
- puts " block indent: #{block.current_indent}"
73
+ puts " block indent: #{block.current_indent}"
70
74
  end
71
75
  @record_dir.join(filename).open(mode: "a") do |f|
72
76
  display = DisplayInvalidBlocks.new(
@@ -122,26 +126,8 @@ module DeadEnd
122
126
  push(block, name: "expand")
123
127
  end
124
128
 
125
- def sweep_heredocs
126
- HeredocBlockParse.new(
127
- source: @source,
128
- code_lines: @code_lines
129
- ).call.each do |block|
130
- push(block, name: "heredoc")
131
- end
132
- end
133
-
134
- def sweep_comments
135
- lines = @code_lines.select(&:is_comment?)
136
- return if lines.empty?
137
- block = CodeBlock.new(lines: lines)
138
- sweep(block: block, name: "comments")
139
- end
140
-
141
129
  # Main search loop
142
130
  def call
143
- sweep_heredocs
144
- sweep_comments
145
131
  until frontier.holds_all_syntax_errors?
146
132
  @tick += 1
147
133
 
@@ -7,7 +7,6 @@ module DeadEnd
7
7
  # even if it is "marked invisible" any filtering of
8
8
  # output should be done before calling this class.
9
9
  #
10
- #
11
10
  # DisplayCodeWithLineNumbers.new(
12
11
  # lines: lines,
13
12
  # highlight_lines: [lines[2], lines[3]]
@@ -37,8 +37,10 @@ module DeadEnd
37
37
 
38
38
  private def found_invalid_blocks
39
39
  @io.puts
40
- @io.puts banner
41
- @io.puts
40
+ if banner
41
+ @io.puts banner
42
+ @io.puts
43
+ end
42
44
  @io.puts("file: #{filename}") if filename
43
45
  @io.puts <<~EOM
44
46
  simplified:
data/lib/dead_end/fyi.rb CHANGED
@@ -4,3 +4,5 @@ require_relative "auto"
4
4
 
5
5
  DeadEnd.send(:remove_const, :SEARCH_SOURCE_ON_ERROR_DEFAULT)
6
6
  DeadEnd::SEARCH_SOURCE_ON_ERROR_DEFAULT = false
7
+
8
+ warn "DEPRECATED: calling `require 'dead_end/fyi'` is deprecated, `require 'dead_end'` instead"
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- #
4
3
  # This is the top level file, but is moved to `internals`
5
- # so the top level file can instead enable the "automatic" behavior
4
+ # so the top level require can instead enable the "automatic" behavior
6
5
 
7
6
  require_relative "version"
8
7
 
@@ -15,7 +14,7 @@ require "timeout"
15
14
  module DeadEnd
16
15
  class Error < StandardError; end
17
16
  SEARCH_SOURCE_ON_ERROR_DEFAULT = true
18
- TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 5).to_i
17
+ TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
19
18
 
20
19
  def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT)
21
20
  raise e unless e.message.include?("end-of-input")
@@ -33,8 +32,6 @@ module DeadEnd
33
32
  )
34
33
  end
35
34
 
36
- warn ""
37
- warn ""
38
35
  raise e
39
36
  end
40
37
 
@@ -145,14 +142,13 @@ end
145
142
 
146
143
  require_relative "code_line"
147
144
  require_relative "code_block"
145
+ require_relative "code_search"
148
146
  require_relative "code_frontier"
149
- require_relative "display_invalid_blocks"
150
- require_relative "around_block_scan"
151
- require_relative "block_expand"
152
- require_relative "parse_blocks_from_indent_line"
147
+ require_relative "clean_document"
153
148
 
154
- require_relative "code_search"
155
- require_relative "who_dis_syntax_error"
156
- require_relative "heredoc_block_parse"
157
149
  require_relative "lex_all"
158
- require_relative "trailing_slash_join"
150
+ require_relative "block_expand"
151
+ require_relative "around_block_scan"
152
+ require_relative "who_dis_syntax_error"
153
+ require_relative "display_invalid_blocks"
154
+ require_relative "parse_blocks_from_indent_line"
@@ -24,6 +24,10 @@ module DeadEnd
24
24
  @lex.map! { |(line, _), type, token, state| LexValue.new(line, type, token, state) }
25
25
  end
26
26
 
27
+ def to_a
28
+ @lex
29
+ end
30
+
27
31
  def each
28
32
  return @lex.each unless block_given?
29
33
  @lex.each do |x|
@@ -31,34 +35,14 @@ module DeadEnd
31
35
  end
32
36
  end
33
37
 
34
- def last
35
- @lex.last
38
+ def [](index)
39
+ @lex[index]
36
40
  end
37
41
 
38
- # Value object for accessing lex values
39
- #
40
- # This lex:
41
- #
42
- # [1, 0], :on_ident, "describe", CMDARG
43
- #
44
- # Would translate into:
45
- #
46
- # lex.line # => 1
47
- # lex.type # => :on_indent
48
- # lex.token # => "describe"
49
- class LexValue
50
- attr_reader :line, :type, :token, :state
51
-
52
- def initialize(line, type, token, state)
53
- @line = line
54
- @type = type
55
- @token = token
56
- @state = state
57
- end
58
-
59
- def expr_label?
60
- state.allbits?(Ripper::EXPR_LABEL)
61
- end
42
+ def last
43
+ @lex.last
62
44
  end
63
45
  end
64
46
  end
47
+
48
+ require_relative "lex_value"
@@ -0,0 +1,62 @@
1
+ module DeadEnd
2
+ # Value object for accessing lex values
3
+ #
4
+ # This lex:
5
+ #
6
+ # [1, 0], :on_ident, "describe", CMDARG
7
+ #
8
+ # Would translate into:
9
+ #
10
+ # lex.line # => 1
11
+ # lex.type # => :on_indent
12
+ # lex.token # => "describe"
13
+ class LexValue
14
+ attr_reader :line, :type, :token, :state
15
+
16
+ def initialize(line, type, token, state)
17
+ @line = line
18
+ @type = type
19
+ @token = token
20
+ @state = state
21
+
22
+ set_kw_end
23
+ end
24
+
25
+ private def set_kw_end
26
+ @is_end = false
27
+ @is_kw = false
28
+ return if type != :on_kw
29
+
30
+ case token
31
+ when "if", "unless", "while", "until"
32
+ # Only count if/unless when it's not a "trailing" if/unless
33
+ # https://github.com/ruby/ruby/blob/06b44f819eb7b5ede1ff69cecb25682b56a1d60c/lib/irb/ruby-lex.rb#L374-L375
34
+ @is_kw = true unless expr_label?
35
+ when "def", "case", "for", "begin", "class", "module", "do"
36
+ @is_kw = true
37
+ when "end"
38
+ @is_end = true
39
+ end
40
+ end
41
+
42
+ def ignore_newline?
43
+ type == :on_ignored_nl
44
+ end
45
+
46
+ def is_end?
47
+ @is_end
48
+ end
49
+
50
+ def is_kw?
51
+ @is_kw
52
+ end
53
+
54
+ def expr_beg?
55
+ state.anybits?(Ripper::EXPR_BEG)
56
+ end
57
+
58
+ def expr_label?
59
+ state.allbits?(Ripper::EXPR_LABEL)
60
+ end
61
+ end
62
+ end
@@ -4,7 +4,7 @@ module DeadEnd
4
4
  # This class is responsible for generating initial code blocks
5
5
  # that will then later be expanded.
6
6
  #
7
- # The biggest concern when guessing about code blocks, is accidentally
7
+ # The biggest concern when guessing code blocks, is accidentally
8
8
  # grabbing one that contains only an "end". In this example:
9
9
  #
10
10
  # def dog
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadEnd
4
- VERSION = "1.2.0"
4
+ VERSION = "2.0.0"
5
5
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadEnd
4
- # Determines what type of syntax error is in the source
4
+ # Determines what type of syntax error that is in the source
5
5
  #
6
6
  # Example:
7
7
  #
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dead_end
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - schneems
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-08 00:00:00.000000000 Z
11
+ date: 2021-10-11 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: When you get an "unexpected end" in your syntax this gem helps you find
14
14
  it
@@ -40,6 +40,7 @@ files:
40
40
  - lib/dead_end/auto.rb
41
41
  - lib/dead_end/block_expand.rb
42
42
  - lib/dead_end/capture_code_context.rb
43
+ - lib/dead_end/clean_document.rb
43
44
  - lib/dead_end/code_block.rb
44
45
  - lib/dead_end/code_frontier.rb
45
46
  - lib/dead_end/code_line.rb
@@ -47,11 +48,10 @@ files:
47
48
  - lib/dead_end/display_code_with_line_numbers.rb
48
49
  - lib/dead_end/display_invalid_blocks.rb
49
50
  - lib/dead_end/fyi.rb
50
- - lib/dead_end/heredoc_block_parse.rb
51
51
  - lib/dead_end/internals.rb
52
52
  - lib/dead_end/lex_all.rb
53
+ - lib/dead_end/lex_value.rb
53
54
  - lib/dead_end/parse_blocks_from_indent_line.rb
54
- - lib/dead_end/trailing_slash_join.rb
55
55
  - lib/dead_end/version.rb
56
56
  - lib/dead_end/who_dis_syntax_error.rb
57
57
  homepage: https://github.com/zombocom/dead_end.git
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DeadEnd
4
- # Takes in a source, and returns blocks containing each heredoc
5
- class HeredocBlockParse
6
- private
7
-
8
- attr_reader :code_lines, :lex
9
-
10
- public
11
-
12
- def initialize(source:, code_lines:)
13
- @code_lines = code_lines
14
- @lex = LexAll.new(source: source)
15
- end
16
-
17
- def call
18
- blocks = []
19
- beginning = []
20
- @lex.each do |lex|
21
- case lex.type
22
- when :on_heredoc_beg
23
- beginning << lex.line
24
- when :on_heredoc_end
25
- start_index = beginning.pop - 1
26
- end_index = lex.line - 1
27
- blocks << CodeBlock.new(lines: code_lines[start_index..end_index])
28
- end
29
- end
30
-
31
- blocks
32
- end
33
- end
34
- end