dead_end 2.0.1 → 3.0.2

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.
@@ -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,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 = "2.0.1"
4
+ VERSION = "3.0.2"
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.1
4
+ version: 3.0.2
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-19 00:00:00.000000000 Z
11
+ date: 2021-11-12 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,154 +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
- class Error < StandardError; end
16
- SEARCH_SOURCE_ON_ERROR_DEFAULT = true
17
- TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
18
-
19
- def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT)
20
- raise e unless e.message.include?("end-of-input")
21
-
22
- filename = e.message.split(":").first
23
-
24
- $stderr.sync = true
25
- warn "Run `$ dead_end #{filename}` for more options\n"
26
-
27
- if search_source_on_error
28
- call(
29
- source: Pathname(filename).read,
30
- filename: filename,
31
- terminal: true
32
- )
33
- end
34
-
35
- raise e
36
- end
37
-
38
- def self.call(source:, filename:, terminal: false, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
39
- search = nil
40
- Timeout.timeout(timeout) do
41
- record_dir ||= ENV["DEBUG"] ? "tmp" : nil
42
- search = CodeSearch.new(source, record_dir: record_dir).call
43
- end
44
-
45
- blocks = search.invalid_blocks
46
- DisplayInvalidBlocks.new(
47
- blocks: blocks,
48
- filename: filename,
49
- terminal: terminal,
50
- code_lines: search.code_lines,
51
- invalid_obj: invalid_type(source),
52
- io: io
53
- ).call
54
- rescue Timeout::Error => e
55
- io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
56
- io.puts e.backtrace.first(3).join($/)
57
- end
58
-
59
- # Used for counting spaces
60
- module SpaceCount
61
- def self.indent(string)
62
- string.split(/\S/).first&.length || 0
63
- end
64
- end
65
-
66
- # This will tell you if the `code_lines` would be valid
67
- # if you removed the `without_lines`. In short it's a
68
- # way to detect if we've found the lines with syntax errors
69
- # in our document yet.
70
- #
71
- # code_lines = [
72
- # CodeLine.new(line: "def foo\n", index: 0)
73
- # CodeLine.new(line: " def bar\n", index: 1)
74
- # CodeLine.new(line: "end\n", index: 2)
75
- # ]
76
- #
77
- # DeadEnd.valid_without?(
78
- # without_lines: code_lines[1],
79
- # code_lines: code_lines
80
- # ) # => true
81
- #
82
- # DeadEnd.valid?(code_lines) # => false
83
- def self.valid_without?(without_lines:, code_lines:)
84
- lines = code_lines - Array(without_lines).flatten
85
-
86
- if lines.empty?
87
- true
88
- else
89
- valid?(lines)
90
- end
91
- end
92
-
93
- def self.invalid?(source)
94
- source = source.join if source.is_a?(Array)
95
- source = source.to_s
96
-
97
- Ripper.new(source).tap(&:parse).error?
98
- end
99
-
100
- # Returns truthy if a given input source is valid syntax
101
- #
102
- # DeadEnd.valid?(<<~EOM) # => true
103
- # def foo
104
- # end
105
- # EOM
106
- #
107
- # DeadEnd.valid?(<<~EOM) # => false
108
- # def foo
109
- # def bar # Syntax error here
110
- # end
111
- # EOM
112
- #
113
- # You can also pass in an array of lines and they'll be
114
- # joined before evaluating
115
- #
116
- # DeadEnd.valid?(
117
- # [
118
- # "def foo\n",
119
- # "end\n"
120
- # ]
121
- # ) # => true
122
- #
123
- # DeadEnd.valid?(
124
- # [
125
- # "def foo\n",
126
- # " def bar\n", # Syntax error here
127
- # "end\n"
128
- # ]
129
- # ) # => false
130
- #
131
- # As an FYI the CodeLine class instances respond to `to_s`
132
- # so passing a CodeLine in as an object or as an array
133
- # will convert it to it's code representation.
134
- def self.valid?(source)
135
- !invalid?(source)
136
- end
137
-
138
- def self.invalid_type(source)
139
- WhoDisSyntaxError.new(source).call
140
- end
141
- end
142
-
143
- require_relative "code_line"
144
- require_relative "code_block"
145
- require_relative "code_search"
146
- require_relative "code_frontier"
147
- require_relative "clean_document"
148
-
149
- require_relative "lex_all"
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"
@@ -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