dead_end 1.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadEnd
4
+ # Find mis-matched syntax based on lexical count
5
+ #
6
+ # Used for detecting missing pairs of elements
7
+ # each keyword needs an end, each '{' needs a '}'
8
+ # etc.
9
+ #
10
+ # Example:
11
+ #
12
+ # left_right = LeftRightLexCount.new
13
+ # left_right.count_kw
14
+ # left_right.missing.first
15
+ # # => "end"
16
+ #
17
+ # left_right = LeftRightLexCount.new
18
+ # source = "{ a: b, c: d" # Note missing '}'
19
+ # LexAll.new(source: source).each do |lex|
20
+ # left_right.count_lex(lex)
21
+ # end
22
+ # left_right.missing.first
23
+ # # => "}"
24
+ class LeftRightLexCount
25
+ def initialize
26
+ @kw_count = 0
27
+ @end_count = 0
28
+
29
+ @count_for_char = {
30
+ "{" => 0,
31
+ "}" => 0,
32
+ "[" => 0,
33
+ "]" => 0,
34
+ "(" => 0,
35
+ ")" => 0,
36
+ "|" => 0
37
+ }
38
+ end
39
+
40
+ def count_kw
41
+ @kw_count += 1
42
+ end
43
+
44
+ def count_end
45
+ @end_count += 1
46
+ end
47
+
48
+ # Count source code characters
49
+ #
50
+ # Example:
51
+ #
52
+ # left_right = LeftRightLexCount.new
53
+ # left_right.count_lex(LexValue.new(1, :on_lbrace, "{", Ripper::EXPR_BEG))
54
+ # left_right.count_for_char("{")
55
+ # # => 1
56
+ # left_right.count_for_char("}")
57
+ # # => 0
58
+ def count_lex(lex)
59
+ case lex.type
60
+ when :on_tstring_content
61
+ # ^^^
62
+ # Means it's a string or a symbol `"{"` rather than being
63
+ # part of a data structure (like a hash) `{ a: b }`
64
+ when :on_embexpr_beg
65
+ # ^^^
66
+ # Embedded string expressions like `"#{foo} <-embed"`
67
+ # are parsed with chars:
68
+ #
69
+ # `#{` as :on_embexpr_beg
70
+ # `}` as :on_embexpr_end
71
+ #
72
+ # We cannot ignore both :on_emb_expr_beg and :on_embexpr_end
73
+ # because sometimes the lexer thinks something is an embed
74
+ # string end, when it is not like `lol = }` (no clue why).
75
+ #
76
+ # When we see `#{` count it as a `{` or we will
77
+ # have a mis-match count.
78
+ #
79
+ case lex.token
80
+ when "\#{"
81
+ @count_for_char["{"] += 1
82
+ end
83
+ else
84
+ @end_count += 1 if lex.is_end?
85
+ @kw_count += 1 if lex.is_kw?
86
+ @count_for_char[lex.token] += 1 if @count_for_char.key?(lex.token)
87
+ end
88
+ end
89
+
90
+ def count_for_char(char)
91
+ @count_for_char[char]
92
+ end
93
+
94
+ # Returns an array of missing syntax characters
95
+ # or `"end"` or `"keyword"`
96
+ #
97
+ # left_right.missing
98
+ # # => ["}"]
99
+ def missing
100
+ out = missing_pairs
101
+ out << missing_pipe
102
+ out << missing_keyword_end
103
+ out.compact!
104
+ out
105
+ end
106
+
107
+ PAIRS = {
108
+ "{" => "}",
109
+ "[" => "]",
110
+ "(" => ")"
111
+ }.freeze
112
+
113
+ # Opening characters like `{` need closing characters # like `}`.
114
+ #
115
+ # When a mis-match count is detected, suggest the
116
+ # missing member.
117
+ #
118
+ # For example if there are 3 `}` and only two `{`
119
+ # return `"{"`
120
+ private def missing_pairs
121
+ PAIRS.map do |(left, right)|
122
+ case @count_for_char[left] <=> @count_for_char[right]
123
+ when 1
124
+ right
125
+ when 0
126
+ nil
127
+ when -1
128
+ left
129
+ end
130
+ end
131
+ end
132
+
133
+ # Keywords need ends and ends need keywords
134
+ #
135
+ # If we have more keywords, there's a missing `end`
136
+ # if we have more `end`-s, there's a missing keyword
137
+ private def missing_keyword_end
138
+ case @kw_count <=> @end_count
139
+ when 1
140
+ "end"
141
+ when 0
142
+ nil
143
+ when -1
144
+ "keyword"
145
+ end
146
+ end
147
+
148
+ # Pipes come in pairs.
149
+ # If there's an odd number of pipes then we
150
+ # are missing one
151
+ private def missing_pipe
152
+ if @count_for_char["|"].odd?
153
+ "|"
154
+ end
155
+ end
156
+ end
157
+ end
@@ -15,7 +15,7 @@ module DeadEnd
15
15
  last_lineno = source_lines.count
16
16
 
17
17
  until lineno >= last_lineno
18
- lines = source_lines[lineno..]
18
+ lines = source_lines[lineno..-1]
19
19
 
20
20
  @lex.concat(Ripper.lex(lines.join, "-", lineno + 1))
21
21
  lineno = @lex.last.first.first + 1
@@ -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
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadEnd
4
+ # Capture parse errors from ripper
5
+ #
6
+ # Example:
7
+ #
8
+ # puts RipperErrors.new(" def foo").call.errors
9
+ # # => ["syntax error, unexpected end-of-input, expecting ';' or '\\n'"]
10
+ class RipperErrors < Ripper
11
+ attr_reader :errors
12
+
13
+ # Comes from ripper, called
14
+ # on every parse error, msg
15
+ # is a string
16
+ def on_parse_error(msg)
17
+ @errors ||= []
18
+ @errors << msg
19
+ end
20
+
21
+ def call
22
+ @run_once ||= begin
23
+ @errors = []
24
+ parse
25
+ true
26
+ end
27
+ self
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadEnd
4
- VERSION = "1.2.0"
4
+ VERSION = "3.0.0"
5
5
  end
data/lib/dead_end.rb CHANGED
@@ -1,4 +1,148 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "dead_end/internals"
3
+ require_relative "dead_end/version"
4
+
5
+ require "tmpdir"
6
+ require "stringio"
7
+ require "pathname"
8
+ require "ripper"
9
+ require "timeout"
10
+
11
+ module DeadEnd
12
+ # Used to indicate a default value that cannot
13
+ # be confused with another input
14
+ DEFAULT_VALUE = Object.new.freeze
15
+
16
+ class Error < StandardError; end
17
+ TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
18
+
19
+ def self.handle_error(e)
20
+ filename = e.message.split(":").first
21
+ $stderr.sync = true
22
+
23
+ call(
24
+ source: Pathname(filename).read,
25
+ filename: filename
26
+ )
27
+
28
+ raise e
29
+ end
30
+
31
+ def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
32
+ search = nil
33
+ filename = nil if filename == DEFAULT_VALUE
34
+ Timeout.timeout(timeout) do
35
+ record_dir ||= ENV["DEBUG"] ? "tmp" : nil
36
+ search = CodeSearch.new(source, record_dir: record_dir).call
37
+ end
38
+
39
+ blocks = search.invalid_blocks
40
+ DisplayInvalidBlocks.new(
41
+ io: io,
42
+ blocks: blocks,
43
+ filename: filename,
44
+ terminal: terminal,
45
+ code_lines: search.code_lines
46
+ ).call
47
+ rescue Timeout::Error => e
48
+ io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
49
+ io.puts e.backtrace.first(3).join($/)
50
+ end
51
+
52
+ # Used for counting spaces
53
+ module SpaceCount
54
+ def self.indent(string)
55
+ string.split(/\S/).first&.length || 0
56
+ end
57
+ end
58
+
59
+ # This will tell you if the `code_lines` would be valid
60
+ # if you removed the `without_lines`. In short it's a
61
+ # way to detect if we've found the lines with syntax errors
62
+ # in our document yet.
63
+ #
64
+ # code_lines = [
65
+ # CodeLine.new(line: "def foo\n", index: 0)
66
+ # CodeLine.new(line: " def bar\n", index: 1)
67
+ # CodeLine.new(line: "end\n", index: 2)
68
+ # ]
69
+ #
70
+ # DeadEnd.valid_without?(
71
+ # without_lines: code_lines[1],
72
+ # code_lines: code_lines
73
+ # ) # => true
74
+ #
75
+ # DeadEnd.valid?(code_lines) # => false
76
+ def self.valid_without?(without_lines:, code_lines:)
77
+ lines = code_lines - Array(without_lines).flatten
78
+
79
+ if lines.empty?
80
+ true
81
+ else
82
+ valid?(lines)
83
+ end
84
+ end
85
+
86
+ def self.invalid?(source)
87
+ source = source.join if source.is_a?(Array)
88
+ source = source.to_s
89
+
90
+ Ripper.new(source).tap(&:parse).error?
91
+ end
92
+
93
+ # Returns truthy if a given input source is valid syntax
94
+ #
95
+ # DeadEnd.valid?(<<~EOM) # => true
96
+ # def foo
97
+ # end
98
+ # EOM
99
+ #
100
+ # DeadEnd.valid?(<<~EOM) # => false
101
+ # def foo
102
+ # def bar # Syntax error here
103
+ # end
104
+ # EOM
105
+ #
106
+ # You can also pass in an array of lines and they'll be
107
+ # joined before evaluating
108
+ #
109
+ # DeadEnd.valid?(
110
+ # [
111
+ # "def foo\n",
112
+ # "end\n"
113
+ # ]
114
+ # ) # => true
115
+ #
116
+ # DeadEnd.valid?(
117
+ # [
118
+ # "def foo\n",
119
+ # " def bar\n", # Syntax error here
120
+ # "end\n"
121
+ # ]
122
+ # ) # => false
123
+ #
124
+ # As an FYI the CodeLine class instances respond to `to_s`
125
+ # so passing a CodeLine in as an object or as an array
126
+ # will convert it to it's code representation.
127
+ def self.valid?(source)
128
+ !invalid?(source)
129
+ end
130
+ end
131
+
132
+ require_relative "dead_end/code_line"
133
+ require_relative "dead_end/code_block"
134
+ require_relative "dead_end/code_search"
135
+ require_relative "dead_end/code_frontier"
136
+ require_relative "dead_end/clean_document"
137
+
138
+ require_relative "dead_end/lex_all"
139
+ require_relative "dead_end/block_expand"
140
+ require_relative "dead_end/around_block_scan"
141
+ require_relative "dead_end/ripper_errors"
142
+ require_relative "dead_end/display_invalid_blocks"
143
+ require_relative "dead_end/parse_blocks_from_indent_line"
144
+
145
+ require_relative "dead_end/explain_syntax"
146
+
4
147
  require_relative "dead_end/auto"
148
+ require_relative "dead_end/cli"
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: 3.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-11-03 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,20 +40,21 @@ 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
44
+ - lib/dead_end/cli.rb
43
45
  - lib/dead_end/code_block.rb
44
46
  - lib/dead_end/code_frontier.rb
45
47
  - lib/dead_end/code_line.rb
46
48
  - lib/dead_end/code_search.rb
47
49
  - lib/dead_end/display_code_with_line_numbers.rb
48
50
  - lib/dead_end/display_invalid_blocks.rb
49
- - lib/dead_end/fyi.rb
50
- - lib/dead_end/heredoc_block_parse.rb
51
- - lib/dead_end/internals.rb
51
+ - lib/dead_end/explain_syntax.rb
52
+ - lib/dead_end/left_right_lex_count.rb
52
53
  - lib/dead_end/lex_all.rb
54
+ - lib/dead_end/lex_value.rb
53
55
  - lib/dead_end/parse_blocks_from_indent_line.rb
54
- - lib/dead_end/trailing_slash_join.rb
56
+ - lib/dead_end/ripper_errors.rb
55
57
  - lib/dead_end/version.rb
56
- - lib/dead_end/who_dis_syntax_error.rb
57
58
  homepage: https://github.com/zombocom/dead_end.git
58
59
  licenses:
59
60
  - MIT
data/lib/dead_end/fyi.rb DELETED
@@ -1,6 +0,0 @@
1
- require_relative "../dead_end/internals"
2
-
3
- require_relative "auto"
4
-
5
- DeadEnd.send(:remove_const, :SEARCH_SOURCE_ON_ERROR_DEFAULT)
6
- DeadEnd::SEARCH_SOURCE_ON_ERROR_DEFAULT = false
@@ -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