dead_end 1.2.0 → 3.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.
@@ -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