dead_end 2.0.2 → 3.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeadEnd
2
4
  # Ripper.lex is not guaranteed to lex the entire source document
3
5
  #
@@ -8,20 +10,22 @@ module DeadEnd
8
10
  class LexAll
9
11
  include Enumerable
10
12
 
11
- def initialize(source:)
12
- @lex = Ripper.lex(source)
13
- lineno = @lex.last.first.first + 1
14
- source_lines = source.lines
15
- last_lineno = source_lines.count
13
+ def initialize(source:, source_lines: nil)
14
+ @lex = Ripper::Lexer.new(source, "-", 1).parse.sort_by(&:pos)
15
+ lineno = @lex.last.pos.first + 1
16
+ source_lines ||= source.lines
17
+ last_lineno = source_lines.length
16
18
 
17
19
  until lineno >= last_lineno
18
20
  lines = source_lines[lineno..-1]
19
21
 
20
- @lex.concat(Ripper.lex(lines.join, "-", lineno + 1))
21
- lineno = @lex.last.first.first + 1
22
+ @lex.concat(
23
+ Ripper::Lexer.new(lines.join, "-", lineno + 1).parse.sort_by(&:pos)
24
+ )
25
+ lineno = @lex.last.pos.first + 1
22
26
  end
23
27
 
24
- @lex.map! { |(line, _), type, token, state| LexValue.new(line, type, token, state) }
28
+ @lex.map! { |elem| LexValue.new(elem.pos.first, elem.event, elem.tok, elem.state) }
25
29
  end
26
30
 
27
31
  def to_a
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DeadEnd
2
4
  # Value object for accessing lex values
3
5
  #
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadEnd
4
+ # Converts a SyntaxError message to a path
5
+ #
6
+ # Handles the case where the filename has a colon in it
7
+ # such as on a windows file system: https://github.com/zombocom/dead_end/issues/111
8
+ #
9
+ # Example:
10
+ #
11
+ # message = "/tmp/scratch:2:in `require_relative': /private/tmp/bad.rb:1: syntax error, unexpected `end' (SyntaxError)"
12
+ # puts PathnameFromMessage.new(message).call.name
13
+ # # => "/tmp/scratch.rb"
14
+ #
15
+ class PathnameFromMessage
16
+ attr_reader :name
17
+
18
+ def initialize(message, io: $stderr)
19
+ @line = message.lines.first
20
+ @parts = @line.split(":")
21
+ @guess = []
22
+ @name = nil
23
+ @io = io
24
+ end
25
+
26
+ def call
27
+ until stop?
28
+ @guess << @parts.shift
29
+ @name = Pathname(@guess.join(":"))
30
+ end
31
+
32
+ if @parts.empty?
33
+ @io.puts "DeadEnd: could not find filename from #{@line.inspect}"
34
+ @name = nil
35
+ end
36
+
37
+ self
38
+ end
39
+
40
+ def stop?
41
+ return true if @parts.empty?
42
+ return false if @guess.empty?
43
+
44
+ @name&.exist?
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,36 @@
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
+ alias_method :on_alias_error, :on_parse_error
22
+ alias_method :on_assign_error, :on_parse_error
23
+ alias_method :on_class_name_error, :on_parse_error
24
+ alias_method :on_param_error, :on_parse_error
25
+ alias_method :compile_error, :on_parse_error
26
+
27
+ def call
28
+ @run_once ||= begin
29
+ @errors = []
30
+ parse
31
+ true
32
+ end
33
+ self
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadEnd
4
- VERSION = "2.0.2"
4
+ VERSION = "3.0.3"
5
5
  end
data/lib/dead_end.rb CHANGED
@@ -1,4 +1,164 @@
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
+ file = PathnameFromMessage.new(e.message).call.name
21
+ raise e unless file
22
+
23
+ $stderr.sync = true
24
+
25
+ call(
26
+ source: file.read,
27
+ filename: file
28
+ )
29
+
30
+ raise e
31
+ end
32
+
33
+ def self.record_dir(dir)
34
+ time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
35
+ dir = Pathname(dir)
36
+ symlink = dir.join("last").tap { |path| path.delete if path.exist? }
37
+ dir.join(time).tap { |path|
38
+ path.mkpath
39
+ FileUtils.symlink(path.basename, symlink)
40
+ }
41
+ end
42
+
43
+ def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
44
+ search = nil
45
+ filename = nil if filename == DEFAULT_VALUE
46
+ Timeout.timeout(timeout) do
47
+ record_dir ||= ENV["DEBUG"] ? "tmp" : nil
48
+ search = CodeSearch.new(source, record_dir: record_dir).call
49
+ end
50
+
51
+ blocks = search.invalid_blocks
52
+ DisplayInvalidBlocks.new(
53
+ io: io,
54
+ blocks: blocks,
55
+ filename: filename,
56
+ terminal: terminal,
57
+ code_lines: search.code_lines
58
+ ).call
59
+ rescue Timeout::Error => e
60
+ io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
61
+ io.puts e.backtrace.first(3).join($/)
62
+ end
63
+
64
+ # Used for counting spaces
65
+ module SpaceCount
66
+ def self.indent(string)
67
+ string.split(/\S/).first&.length || 0
68
+ end
69
+ end
70
+
71
+ # This will tell you if the `code_lines` would be valid
72
+ # if you removed the `without_lines`. In short it's a
73
+ # way to detect if we've found the lines with syntax errors
74
+ # in our document yet.
75
+ #
76
+ # code_lines = [
77
+ # CodeLine.new(line: "def foo\n", index: 0)
78
+ # CodeLine.new(line: " def bar\n", index: 1)
79
+ # CodeLine.new(line: "end\n", index: 2)
80
+ # ]
81
+ #
82
+ # DeadEnd.valid_without?(
83
+ # without_lines: code_lines[1],
84
+ # code_lines: code_lines
85
+ # ) # => true
86
+ #
87
+ # DeadEnd.valid?(code_lines) # => false
88
+ def self.valid_without?(without_lines:, code_lines:)
89
+ lines = code_lines - Array(without_lines).flatten
90
+
91
+ if lines.empty?
92
+ true
93
+ else
94
+ valid?(lines)
95
+ end
96
+ end
97
+
98
+ def self.invalid?(source)
99
+ source = source.join if source.is_a?(Array)
100
+ source = source.to_s
101
+
102
+ Ripper.new(source).tap(&:parse).error?
103
+ end
104
+
105
+ # Returns truthy if a given input source is valid syntax
106
+ #
107
+ # DeadEnd.valid?(<<~EOM) # => true
108
+ # def foo
109
+ # end
110
+ # EOM
111
+ #
112
+ # DeadEnd.valid?(<<~EOM) # => false
113
+ # def foo
114
+ # def bar # Syntax error here
115
+ # end
116
+ # EOM
117
+ #
118
+ # You can also pass in an array of lines and they'll be
119
+ # joined before evaluating
120
+ #
121
+ # DeadEnd.valid?(
122
+ # [
123
+ # "def foo\n",
124
+ # "end\n"
125
+ # ]
126
+ # ) # => true
127
+ #
128
+ # DeadEnd.valid?(
129
+ # [
130
+ # "def foo\n",
131
+ # " def bar\n", # Syntax error here
132
+ # "end\n"
133
+ # ]
134
+ # ) # => false
135
+ #
136
+ # As an FYI the CodeLine class instances respond to `to_s`
137
+ # so passing a CodeLine in as an object or as an array
138
+ # will convert it to it's code representation.
139
+ def self.valid?(source)
140
+ !invalid?(source)
141
+ end
142
+ end
143
+
144
+ # Integration
145
+ require_relative "dead_end/cli"
4
146
  require_relative "dead_end/auto"
147
+
148
+ # Core logic
149
+ require_relative "dead_end/code_search"
150
+ require_relative "dead_end/code_frontier"
151
+ require_relative "dead_end/explain_syntax"
152
+ require_relative "dead_end/clean_document"
153
+
154
+ # Helpers
155
+ require_relative "dead_end/lex_all"
156
+ require_relative "dead_end/code_line"
157
+ require_relative "dead_end/code_block"
158
+ require_relative "dead_end/block_expand"
159
+ require_relative "dead_end/ripper_errors"
160
+ require_relative "dead_end/insertion_sort"
161
+ require_relative "dead_end/around_block_scan"
162
+ require_relative "dead_end/pathname_from_message"
163
+ require_relative "dead_end/display_invalid_blocks"
164
+ require_relative "dead_end/parse_blocks_from_indent_line"
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: 2.0.2
4
+ version: 3.0.3
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-31 00:00:00.000000000 Z
11
+ date: 2021-11-17 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
@@ -38,23 +38,25 @@ files:
38
38
  - lib/dead_end.rb
39
39
  - lib/dead_end/around_block_scan.rb
40
40
  - lib/dead_end/auto.rb
41
- - lib/dead_end/banner.rb
42
41
  - lib/dead_end/block_expand.rb
43
42
  - lib/dead_end/capture_code_context.rb
44
43
  - lib/dead_end/clean_document.rb
44
+ - lib/dead_end/cli.rb
45
45
  - lib/dead_end/code_block.rb
46
46
  - lib/dead_end/code_frontier.rb
47
47
  - lib/dead_end/code_line.rb
48
48
  - lib/dead_end/code_search.rb
49
49
  - lib/dead_end/display_code_with_line_numbers.rb
50
50
  - lib/dead_end/display_invalid_blocks.rb
51
- - lib/dead_end/fyi.rb
52
- - lib/dead_end/internals.rb
51
+ - lib/dead_end/explain_syntax.rb
52
+ - lib/dead_end/insertion_sort.rb
53
+ - lib/dead_end/left_right_lex_count.rb
53
54
  - lib/dead_end/lex_all.rb
54
55
  - lib/dead_end/lex_value.rb
55
56
  - lib/dead_end/parse_blocks_from_indent_line.rb
57
+ - lib/dead_end/pathname_from_message.rb
58
+ - lib/dead_end/ripper_errors.rb
56
59
  - lib/dead_end/version.rb
57
- - lib/dead_end/who_dis_syntax_error.rb
58
60
  homepage: https://github.com/zombocom/dead_end.git
59
61
  licenses:
60
62
  - MIT
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DeadEnd
4
- class Banner
5
- attr_reader :invalid_obj
6
-
7
- def initialize(invalid_obj:)
8
- @invalid_obj = invalid_obj
9
- end
10
-
11
- def call
12
- case invalid_obj.error_symbol
13
- when :missing_end
14
- <<~EOM
15
- DeadEnd: Missing `end` detected
16
-
17
- This code has a missing `end`. Ensure that all
18
- syntax keywords (`def`, `do`, etc.) have a matching `end`.
19
- EOM
20
- when :unmatched_syntax
21
- case unmatched_symbol
22
- when :end
23
- <<~EOM
24
- DeadEnd: Unmatched `end` detected
25
-
26
- This code has an unmatched `end`. Ensure that all `end` lines
27
- in your code have a matching syntax keyword (`def`, `do`, etc.)
28
- and that you don't have any extra `end` lines.
29
- EOM
30
- when :|
31
- <<~EOM
32
- DeadEnd: Unmatched `|` character detected
33
-
34
- Example:
35
-
36
- `do |x` should be `do |x|`
37
- EOM
38
- when *WhoDisSyntaxError::CHARACTERS.keys
39
- <<~EOM
40
- DeadEnd: Unmatched `#{unmatched_symbol}` character detected
41
-
42
- It appears a `#{missing_character}` might be missing.
43
- EOM
44
- else
45
- "DeadEnd: Unmatched `#{unmatched_symbol}` detected"
46
- end
47
- end
48
- end
49
-
50
- private def unmatched_symbol
51
- invalid_obj.unmatched_symbol
52
- end
53
-
54
- private def missing_character
55
- WhoDisSyntaxError::CHARACTERS[unmatched_symbol]
56
- end
57
- end
58
- end
data/lib/dead_end/fyi.rb DELETED
@@ -1,8 +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
7
-
8
- warn "DEPRECATED: calling `require 'dead_end/fyi'` is deprecated, `require 'dead_end'` instead"
@@ -1,157 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # This is the top level file, but is moved to `internals`
4
- # so the top level require can instead enable the "automatic" behavior
5
-
6
- require_relative "version"
7
-
8
- require "tmpdir"
9
- require "stringio"
10
- require "pathname"
11
- require "ripper"
12
- require "timeout"
13
-
14
- module DeadEnd
15
- # Used to indicate a default value that cannot
16
- # be confused with another input
17
- DEFAULT_VALUE = Object.new.freeze
18
-
19
- class Error < StandardError; end
20
- SEARCH_SOURCE_ON_ERROR_DEFAULT = true
21
- TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
22
-
23
- def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT)
24
- raise e unless e.message.include?("end-of-input")
25
-
26
- filename = e.message.split(":").first
27
-
28
- $stderr.sync = true
29
- warn "Run `$ dead_end #{filename}` for more options\n"
30
-
31
- if search_source_on_error
32
- call(
33
- source: Pathname(filename).read,
34
- filename: filename
35
- )
36
- end
37
-
38
- raise e
39
- end
40
-
41
- def self.call(source:, filename:, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
42
- search = nil
43
- Timeout.timeout(timeout) do
44
- record_dir ||= ENV["DEBUG"] ? "tmp" : nil
45
- search = CodeSearch.new(source, record_dir: record_dir).call
46
- end
47
-
48
- blocks = search.invalid_blocks
49
- DisplayInvalidBlocks.new(
50
- blocks: blocks,
51
- filename: filename,
52
- terminal: terminal,
53
- code_lines: search.code_lines,
54
- invalid_obj: invalid_type(source),
55
- io: io
56
- ).call
57
- rescue Timeout::Error => e
58
- io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
59
- io.puts e.backtrace.first(3).join($/)
60
- end
61
-
62
- # Used for counting spaces
63
- module SpaceCount
64
- def self.indent(string)
65
- string.split(/\S/).first&.length || 0
66
- end
67
- end
68
-
69
- # This will tell you if the `code_lines` would be valid
70
- # if you removed the `without_lines`. In short it's a
71
- # way to detect if we've found the lines with syntax errors
72
- # in our document yet.
73
- #
74
- # code_lines = [
75
- # CodeLine.new(line: "def foo\n", index: 0)
76
- # CodeLine.new(line: " def bar\n", index: 1)
77
- # CodeLine.new(line: "end\n", index: 2)
78
- # ]
79
- #
80
- # DeadEnd.valid_without?(
81
- # without_lines: code_lines[1],
82
- # code_lines: code_lines
83
- # ) # => true
84
- #
85
- # DeadEnd.valid?(code_lines) # => false
86
- def self.valid_without?(without_lines:, code_lines:)
87
- lines = code_lines - Array(without_lines).flatten
88
-
89
- if lines.empty?
90
- true
91
- else
92
- valid?(lines)
93
- end
94
- end
95
-
96
- def self.invalid?(source)
97
- source = source.join if source.is_a?(Array)
98
- source = source.to_s
99
-
100
- Ripper.new(source).tap(&:parse).error?
101
- end
102
-
103
- # Returns truthy if a given input source is valid syntax
104
- #
105
- # DeadEnd.valid?(<<~EOM) # => true
106
- # def foo
107
- # end
108
- # EOM
109
- #
110
- # DeadEnd.valid?(<<~EOM) # => false
111
- # def foo
112
- # def bar # Syntax error here
113
- # end
114
- # EOM
115
- #
116
- # You can also pass in an array of lines and they'll be
117
- # joined before evaluating
118
- #
119
- # DeadEnd.valid?(
120
- # [
121
- # "def foo\n",
122
- # "end\n"
123
- # ]
124
- # ) # => true
125
- #
126
- # DeadEnd.valid?(
127
- # [
128
- # "def foo\n",
129
- # " def bar\n", # Syntax error here
130
- # "end\n"
131
- # ]
132
- # ) # => false
133
- #
134
- # As an FYI the CodeLine class instances respond to `to_s`
135
- # so passing a CodeLine in as an object or as an array
136
- # will convert it to it's code representation.
137
- def self.valid?(source)
138
- !invalid?(source)
139
- end
140
-
141
- def self.invalid_type(source)
142
- WhoDisSyntaxError.new(source).call
143
- end
144
- end
145
-
146
- require_relative "code_line"
147
- require_relative "code_block"
148
- require_relative "code_search"
149
- require_relative "code_frontier"
150
- require_relative "clean_document"
151
-
152
- require_relative "lex_all"
153
- require_relative "block_expand"
154
- require_relative "around_block_scan"
155
- require_relative "who_dis_syntax_error"
156
- require_relative "display_invalid_blocks"
157
- require_relative "parse_blocks_from_indent_line"
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DeadEnd
4
- # Determines what type of syntax error that is in the source
5
- #
6
- # Example:
7
- #
8
- # puts WhoDisSyntaxError.new("def foo;").call.error_symbol
9
- # # => :missing_end
10
- class WhoDisSyntaxError < Ripper
11
- CHARACTERS = {"{": :"}", "}": :"{", "[": :"]", "]": :"[", "(": :")", ")": :"("}
12
- class Null
13
- def error_symbol
14
- :missing_end
15
- end
16
-
17
- def unmatched_symbol
18
- :end
19
- end
20
- end
21
- attr_reader :error, :run_once
22
-
23
- # Return options:
24
- # - :missing_end
25
- # - :unmatched_syntax
26
- # - :unknown
27
- def error_symbol
28
- call
29
- @error_symbol
30
- end
31
-
32
- # Return options:
33
- # - :end
34
- # - :|
35
- # - :}
36
- # - :unknown
37
- def unmatched_symbol
38
- call
39
- @unmatched_symbol
40
- end
41
-
42
- def call
43
- @run_once ||= begin
44
- parse
45
- true
46
- end
47
- self
48
- end
49
-
50
- def on_parse_error(msg)
51
- return if @error_symbol && @unmatched_symbol
52
-
53
- @error = msg
54
- @unmatched_symbol = :unknown
55
-
56
- case @error
57
- when /unexpected end-of-input/
58
- @error_symbol = :missing_end
59
- when /expecting end-of-input/
60
- @unmatched_symbol = :end
61
- @error_symbol = :unmatched_syntax
62
- when /unexpected .* expecting ['`]?(?<unmatched_symbol>[^']*)/
63
- if $1
64
- character = $1.to_sym
65
- @unmatched_symbol = CHARACTERS[character] || character
66
- @unmatched_symbol = :end if @unmatched_symbol == :keyword_end
67
- end
68
- @error_symbol = :unmatched_syntax
69
- when /unexpected '(?<unmatched_symbol>.*)'/
70
- @unmatched_symbol = $1.to_sym
71
- @unmatched_symbol = :end if @unmatched_symbol == :keyword_end
72
- @error_symbol = :unmatched_syntax
73
- when /unexpected `end'/, # Ruby 2.7 and 3.0
74
- /unexpected end/, # Ruby 2.6
75
- /unexpected keyword_end/i # Ruby 2.5
76
-
77
- @error_symbol = :unmatched_syntax
78
- else
79
- @error_symbol = :unknown
80
- end
81
- end
82
- end
83
- end