dead_end 3.0.1 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c232ccfce002bd3a79d6a5508ea46b27684a9a0ae2bb5d6f843d49e5ba59c0ac
4
- data.tar.gz: 745847314c5c3ff09cb85b525a5d583f89c1c597e1f5b785ed54e01299bb72c4
3
+ metadata.gz: 64abeaee0636d3ded8df0f0dc124674a70f954287b37302dff7151f04b746557
4
+ data.tar.gz: 02e6eea73a9e9e832b898f9294c1ce3f7f38f9b8996554a8f6527abf7027d115
5
5
  SHA512:
6
- metadata.gz: c49f5b58a7a2f222606a01efc3256a2a0eceade66ff6e46f7c15172cf972a48aa4b6d55212eb714ad69dde1b6d80e91cf8589a9b7026ad2b3e9baafabb8c2e6c
7
- data.tar.gz: f70fc9947a92d8de2d8f05e08ab803a791c9fc396f03a0bed812b0a88bdb74af33658b23ebdf148adbc716cc41870bee8fabe57c3ae62bd6c9e9b063717f0bbc
6
+ metadata.gz: e7381458f44ef92f5053ac3171ac8bc59e89a805d793dde1c48ce1eda855dcea624ef6dc7a1faa3da9c20abc53983675fc66a7b08e3e02893801b4b4ff0edf65
7
+ data.tar.gz: 8b92dee56fac32777f31b76e25a0b778b2a4ffa1ff6914f24ef835bbf5ab79898f4a3a5607a9bebc641f86b05f2a88224504f0f8aaab83c3262b1d067ea32af0
data/.circleci/config.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  version: 2.1
2
2
  orbs:
3
- ruby: circleci/ruby@1.1.2
3
+ ruby: circleci/ruby@1.2.0
4
4
  references:
5
5
  unit: &unit
6
6
  run:
@@ -45,6 +45,17 @@ jobs:
45
45
  - ruby/install-deps
46
46
  - <<: *unit
47
47
 
48
+ "ruby-3-1":
49
+ docker:
50
+ - image: 'cimg/base:stable'
51
+ steps:
52
+ - checkout
53
+ - ruby/install:
54
+ version: '3.1.0-preview1'
55
+ - run: ruby -v
56
+ - ruby/install-deps
57
+ - <<: *unit
58
+
48
59
  "lint":
49
60
  docker:
50
61
  - image: circleci/ruby:3.0
@@ -61,4 +72,5 @@ workflows:
61
72
  - "ruby-2-6"
62
73
  - "ruby-2-7"
63
74
  - "ruby-3-0"
75
+ - "ruby-3-1"
64
76
  - "lint"
data/CHANGELOG.md CHANGED
@@ -1,11 +1,33 @@
1
1
  ## HEAD (unreleased)
2
2
 
3
+ ## 3.1.1
4
+
5
+ - Fix case where Ripper lexing identified incorrect code as a keyword (https://github.com/zombocom/dead_end/pull/122)
6
+
7
+ ## 3.1.0
8
+
9
+ - Add support for Ruby 3.1 by updating `require_relative` logic (https://github.com/zombocom/dead_end/pull/120)
10
+ - Requiring `dead_end/auto` is now deprecated please require `dead_end` instead (https://github.com/zombocom/dead_end/pull/119)
11
+ - Requiring `dead_end/api` now loads code without monkeypatching core extensions (https://github.com/zombocom/dead_end/pull/119)
12
+ - The interface `DeadEnd.handle_error` is declared public and stable (https://github.com/zombocom/dead_end/pull/119)
13
+
14
+ ## 3.0.3
15
+
16
+ - Expand explanations coming from additional Ripper errors (https://github.com/zombocom/dead_end/pull/117)
17
+ - Fix explanation involving shorthand syntax for literals like `%w[]` and `%Q{}` (https://github.com/zombocom/dead_end/pull/116)
18
+
19
+ ## 3.0.2
20
+
21
+ - Fix windows filename detection (https://github.com/zombocom/dead_end/pull/114)
22
+ - Update links on readme and code of conduct (https://github.com/zombocom/dead_end/pull/107)
23
+
3
24
  ## 3.0.1
4
25
 
5
26
  - Fix CLI parsing when flags come before filename (https://github.com/zombocom/dead_end/pull/102)
6
27
 
7
28
  ## 3.0.0
8
29
 
30
+ - [Breaking] CLI now outputs to STDOUT instead of STDERR (https://github.com/zombocom/dead_end/pull/98)
9
31
  - [Breaking] Remove previously deprecated `require "dead_end/fyi"` interface (https://github.com/zombocom/dead_end/pull/94)
10
32
  - Fix double output bug (https://github.com/zombocom/dead_end/pull/99)
11
33
  - Fix bug causing poor results (fix #95, fix #88) (https://github.com/zombocom/dead_end/pull/96)
data/CODE_OF_CONDUCT.md CHANGED
@@ -68,7 +68,7 @@ members of the project's leadership.
68
68
  ## Attribution
69
69
 
70
70
  This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at [https://contributor-covenant.org/version/1/4][version]
71
+ available at [https://contributor-covenant.org/version/1/4/code-of-conduct/][version]
72
72
 
73
73
  [homepage]: https://contributor-covenant.org
74
- [version]: https://contributor-covenant.org/version/1/4/
74
+ [version]: https://contributor-covenant.org/version/1/4/code-of-conduct/
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dead_end (3.0.1)
4
+ dead_end (3.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -62,4 +62,4 @@ DEPENDENCIES
62
62
  standard
63
63
 
64
64
  BUNDLED WITH
65
- 2.2.30
65
+ 2.3.4
data/README.md CHANGED
@@ -45,6 +45,13 @@ To get the CLI and manually search for syntax errors (but not automatically anno
45
45
 
46
46
  This gives you the CLI command `$ dead_end` for more info run `$ dead_end --help`.
47
47
 
48
+ ## Editor integration
49
+
50
+ An extension is available for VSCode:
51
+
52
+ - Extension: https://marketplace.visualstudio.com/items?itemName=Zombocom.dead-end-vscode
53
+ - GitHub: https://github.com/zombocom/dead_end-vscode
54
+
48
55
  ## What syntax errors does it handle?
49
56
 
50
57
  Dead end will fire against all syntax errors and can isolate any syntax error. In addition, dead_end attempts to produce human readable descriptions of what needs to be done to resolve the issue. For example:
@@ -159,6 +166,16 @@ Here's an example:
159
166
 
160
167
  ![](assets/syntax_search.gif)
161
168
 
169
+ ## Use internals
170
+
171
+ To use the `dead_end` gem without monkeypatching you can `require 'dead_en/api'`. This will allow you to load `dead_end` and use its internals without mutating `require`.
172
+
173
+ Stable internal interface(s):
174
+
175
+ - `DeadEnd.handle_error(e)`
176
+
177
+ Any other entrypoints are subject to change without warning. If you want to use an internal interface from `dead_end` not on this list, open an issue to explain your use case.
178
+
162
179
  ## Development
163
180
 
164
181
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -195,7 +212,7 @@ $ qcachegrind tmp/last/profile.callgrind.out.<numbers>
195
212
 
196
213
  ## Contributing
197
214
 
198
- Bug reports and pull requests are welcome on GitHub at https://github.com/zombocom/dead_end. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/zombocom/dead_end/blob/master/CODE_OF_CONDUCT.md).
215
+ Bug reports and pull requests are welcome on GitHub at https://github.com/zombocom/dead_end. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/zombocom/dead_end/blob/main/CODE_OF_CONDUCT.md).
199
216
 
200
217
 
201
218
  ## License
@@ -204,4 +221,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
204
221
 
205
222
  ## Code of Conduct
206
223
 
207
- Everyone interacting in the DeadEnd project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/zombocom/dead_end/blob/master/CODE_OF_CONDUCT.md).
224
+ Everyone interacting in the DeadEnd project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/zombocom/dead_end/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,196 @@
1
+ require_relative "version"
2
+
3
+ require "tmpdir"
4
+ require "stringio"
5
+ require "pathname"
6
+ require "ripper"
7
+ require "timeout"
8
+
9
+ module DeadEnd
10
+ # Used to indicate a default value that cannot
11
+ # be confused with another input.
12
+ DEFAULT_VALUE = Object.new.freeze
13
+
14
+ class Error < StandardError; end
15
+ TIMEOUT_DEFAULT = ENV.fetch("DEAD_END_TIMEOUT", 1).to_i
16
+
17
+ # DeadEnd.handle_error [Public]
18
+ #
19
+ # Takes a `SyntaxError`` exception, uses the
20
+ # error message to locate the file. Then the file
21
+ # will be analyzed to find the location of the syntax
22
+ # error and emit that location to stderr.
23
+ #
24
+ # Example:
25
+ #
26
+ # begin
27
+ # require 'bad_file'
28
+ # rescue => e
29
+ # DeadEnd.handle_error(e)
30
+ # end
31
+ #
32
+ # By default it will re-raise the exception unless
33
+ # `re_raise: false`. The message output location
34
+ # can be configured using the `io: $stderr` input.
35
+ #
36
+ # If a valid filename cannot be determined, the original
37
+ # exception will be re-raised (even with
38
+ # `re_raise: false`).
39
+ def self.handle_error(e, re_raise: true, io: $stderr)
40
+ unless e.is_a?(SyntaxError)
41
+ io.puts("DeadEnd: Must pass a SyntaxError, got: #{e.class}")
42
+ raise e
43
+ end
44
+
45
+ file = PathnameFromMessage.new(e.message, io: io).call.name
46
+ raise e unless file
47
+
48
+ io.sync = true
49
+
50
+ call(
51
+ io: io,
52
+ source: file.read,
53
+ filename: file
54
+ )
55
+
56
+ raise e if re_raise
57
+ end
58
+
59
+ # DeadEnd.call [Private]
60
+ #
61
+ # Main private interface
62
+ def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
63
+ search = nil
64
+ filename = nil if filename == DEFAULT_VALUE
65
+ Timeout.timeout(timeout) do
66
+ record_dir ||= ENV["DEBUG"] ? "tmp" : nil
67
+ search = CodeSearch.new(source, record_dir: record_dir).call
68
+ end
69
+
70
+ blocks = search.invalid_blocks
71
+ DisplayInvalidBlocks.new(
72
+ io: io,
73
+ blocks: blocks,
74
+ filename: filename,
75
+ terminal: terminal,
76
+ code_lines: search.code_lines
77
+ ).call
78
+ rescue Timeout::Error => e
79
+ io.puts "Search timed out DEAD_END_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
80
+ io.puts e.backtrace.first(3).join($/)
81
+ end
82
+
83
+ # DeadEnd.record_dir [Private]
84
+ #
85
+ # Used to generate a unique directory to record
86
+ # search steps for debugging
87
+ def self.record_dir(dir)
88
+ time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
89
+ dir = Pathname(dir)
90
+ symlink = dir.join("last").tap { |path| path.delete if path.exist? }
91
+ dir.join(time).tap { |path|
92
+ path.mkpath
93
+ FileUtils.symlink(path.basename, symlink)
94
+ }
95
+ end
96
+
97
+ # DeadEnd.valid_without? [Private]
98
+ #
99
+ # This will tell you if the `code_lines` would be valid
100
+ # if you removed the `without_lines`. In short it's a
101
+ # way to detect if we've found the lines with syntax errors
102
+ # in our document yet.
103
+ #
104
+ # code_lines = [
105
+ # CodeLine.new(line: "def foo\n", index: 0)
106
+ # CodeLine.new(line: " def bar\n", index: 1)
107
+ # CodeLine.new(line: "end\n", index: 2)
108
+ # ]
109
+ #
110
+ # DeadEnd.valid_without?(
111
+ # without_lines: code_lines[1],
112
+ # code_lines: code_lines
113
+ # ) # => true
114
+ #
115
+ # DeadEnd.valid?(code_lines) # => false
116
+ def self.valid_without?(without_lines:, code_lines:)
117
+ lines = code_lines - Array(without_lines).flatten
118
+
119
+ if lines.empty?
120
+ true
121
+ else
122
+ valid?(lines)
123
+ end
124
+ end
125
+
126
+ # DeadEnd.invalid? [Private]
127
+ #
128
+ # Opposite of `DeadEnd.valid?`
129
+ def self.invalid?(source)
130
+ source = source.join if source.is_a?(Array)
131
+ source = source.to_s
132
+
133
+ Ripper.new(source).tap(&:parse).error?
134
+ end
135
+
136
+ # DeadEnd.valid? [Private]
137
+ #
138
+ # Returns truthy if a given input source is valid syntax
139
+ #
140
+ # DeadEnd.valid?(<<~EOM) # => true
141
+ # def foo
142
+ # end
143
+ # EOM
144
+ #
145
+ # DeadEnd.valid?(<<~EOM) # => false
146
+ # def foo
147
+ # def bar # Syntax error here
148
+ # end
149
+ # EOM
150
+ #
151
+ # You can also pass in an array of lines and they'll be
152
+ # joined before evaluating
153
+ #
154
+ # DeadEnd.valid?(
155
+ # [
156
+ # "def foo\n",
157
+ # "end\n"
158
+ # ]
159
+ # ) # => true
160
+ #
161
+ # DeadEnd.valid?(
162
+ # [
163
+ # "def foo\n",
164
+ # " def bar\n", # Syntax error here
165
+ # "end\n"
166
+ # ]
167
+ # ) # => false
168
+ #
169
+ # As an FYI the CodeLine class instances respond to `to_s`
170
+ # so passing a CodeLine in as an object or as an array
171
+ # will convert it to it's code representation.
172
+ def self.valid?(source)
173
+ !invalid?(source)
174
+ end
175
+ end
176
+
177
+ # Integration
178
+ require_relative "cli"
179
+
180
+ # Core logic
181
+ require_relative "code_search"
182
+ require_relative "code_frontier"
183
+ require_relative "explain_syntax"
184
+ require_relative "clean_document"
185
+
186
+ # Helpers
187
+ require_relative "lex_all"
188
+ require_relative "code_line"
189
+ require_relative "code_block"
190
+ require_relative "block_expand"
191
+ require_relative "ripper_errors"
192
+ require_relative "insertion_sort"
193
+ require_relative "around_block_scan"
194
+ require_relative "pathname_from_message"
195
+ require_relative "display_invalid_blocks"
196
+ require_relative "parse_blocks_from_indent_line"
data/lib/dead_end/auto.rb CHANGED
@@ -1,35 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../dead_end"
4
+ require_relative "core_ext"
4
5
 
5
- # Monkey patch kernel to ensure that all `require` calls call the same
6
- # method
7
- module Kernel
8
- module_function
9
-
10
- alias_method :dead_end_original_require, :require
11
- alias_method :dead_end_original_require_relative, :require_relative
12
- alias_method :dead_end_original_load, :load
13
-
14
- def load(file, wrap = false)
15
- dead_end_original_load(file)
16
- rescue SyntaxError => e
17
- DeadEnd.handle_error(e)
18
- end
19
-
20
- def require(file)
21
- dead_end_original_require(file)
22
- rescue SyntaxError => e
23
- DeadEnd.handle_error(e)
24
- end
25
-
26
- def require_relative(file)
27
- if Pathname.new(file).absolute?
28
- dead_end_original_require file
29
- else
30
- dead_end_original_require File.expand_path("../#{file}", Kernel.caller_locations(1, 1)[0].absolute_path)
31
- end
32
- rescue SyntaxError => e
33
- DeadEnd.handle_error(e)
34
- end
35
- end
6
+ warn "Calling `require 'dead_end/auto'` is deprecated, please `require 'dead_end'` instead."
@@ -85,8 +85,8 @@ module DeadEnd
85
85
  #
86
86
  class CleanDocument
87
87
  def initialize(source:)
88
- @source = clean_sweep(source: source)
89
- @document = CodeLine.from_source(@source)
88
+ lines = clean_sweep(source: source)
89
+ @document = CodeLine.from_source(lines.join, lines: lines)
90
90
  end
91
91
 
92
92
  # Call all of the document "cleaners"
@@ -161,7 +161,7 @@ module DeadEnd
161
161
  else
162
162
  line
163
163
  end
164
- end.join
164
+ end
165
165
  end
166
166
 
167
167
  # Smushes all heredoc lines into one line
@@ -54,6 +54,8 @@ module DeadEnd
54
54
  @code_lines = code_lines
55
55
  @frontier = InsertionSort.new
56
56
  @unvisited_lines = @code_lines.sort_by(&:indent_index)
57
+ @visited_lines = {}
58
+
57
59
  @has_run = false
58
60
  @check_next = true
59
61
  end
@@ -128,7 +130,13 @@ module DeadEnd
128
130
  end
129
131
 
130
132
  def register_indent_block(block)
131
- @unvisited_lines -= block.lines
133
+ block.lines.each do |line|
134
+ next if @visited_lines[line]
135
+ @visited_lines[line] = true
136
+
137
+ index = @unvisited_lines.bsearch_index { |l| line.indent_index <=> l.indent_index }
138
+ @unvisited_lines.delete_at(index)
139
+ end
132
140
  self
133
141
  end
134
142
 
@@ -26,9 +26,10 @@ module DeadEnd
26
26
 
27
27
  # Returns an array of CodeLine objects
28
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 }
31
- source.lines.map.with_index do |line, index|
29
+ def self.from_source(source, lines: nil)
30
+ lines ||= source.lines
31
+ lex_array_for_line = LexAll.new(source: source, source_lines: lines).each_with_object(Hash.new { |h, k| h[k] = [] }) { |lex, hash| hash[lex.line] << lex }
32
+ lines.map.with_index do |line, index|
32
33
  CodeLine.new(
33
34
  line: line,
34
35
  index: index,
@@ -42,28 +43,20 @@ module DeadEnd
42
43
  @lex = lex
43
44
  @line = line
44
45
  @index = index
45
- @original = line.freeze
46
+ @original = line
46
47
  @line_number = @index + 1
48
+ strip_line = line.dup
49
+ strip_line.lstrip!
47
50
 
48
- if line.strip.empty?
51
+ if strip_line.empty?
49
52
  @empty = true
50
53
  @indent = 0
51
54
  else
52
55
  @empty = false
53
- @indent = SpaceCount.indent(line)
56
+ @indent = line.length - strip_line.length
54
57
  end
55
58
 
56
- kw_count = 0
57
- end_count = 0
58
- @lex.each do |lex|
59
- kw_count += 1 if lex.is_kw?
60
- end_count += 1 if lex.is_end?
61
- end
62
-
63
- kw_count -= oneliner_method_count
64
-
65
- @is_kw = (kw_count - end_count) > 0
66
- @is_end = (end_count - kw_count) > 0
59
+ set_kw_end
67
60
  end
68
61
 
69
62
  # Used for stable sort via indentation level
@@ -179,8 +172,7 @@ module DeadEnd
179
172
  #
180
173
  # For some reason this introduces `on_ignore_newline` but with BEG type
181
174
  def ignore_newline_not_beg?
182
- lex_value = lex.detect { |l| l.type == :on_ignored_nl }
183
- !!(lex_value && !lex_value.expr_beg?)
175
+ @ignore_newline_not_beg
184
176
  end
185
177
 
186
178
  # Determines if the given line has a trailing slash
@@ -206,11 +198,22 @@ module DeadEnd
206
198
  #
207
199
  # ENDFN -> BEG (token = '=' ) -> END
208
200
  #
209
- private def oneliner_method_count
201
+ private def set_kw_end
210
202
  oneliner_count = 0
211
203
  in_oneliner_def = nil
212
204
 
205
+ kw_count = 0
206
+ end_count = 0
207
+
208
+ @ignore_newline_not_beg = false
213
209
  @lex.each do |lex|
210
+ kw_count += 1 if lex.is_kw?
211
+ end_count += 1 if lex.is_end?
212
+
213
+ if lex.type == :on_ignored_nl
214
+ @ignore_newline_not_beg = !lex.expr_beg?
215
+ end
216
+
214
217
  if in_oneliner_def.nil?
215
218
  in_oneliner_def = :ENDFN if lex.state.allbits?(Ripper::EXPR_ENDFN)
216
219
  elsif lex.state.allbits?(Ripper::EXPR_ENDFN)
@@ -227,7 +230,10 @@ module DeadEnd
227
230
  end
228
231
  end
229
232
 
230
- oneliner_count
233
+ kw_count -= oneliner_count
234
+
235
+ @is_kw = (kw_count - end_count) > 0
236
+ @is_end = (end_count - kw_count) > 0
231
237
  end
232
238
  end
233
239
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Monkey patch kernel to ensure that all `require` calls call the same
4
+ # method
5
+ module Kernel
6
+ module_function
7
+
8
+ alias_method :dead_end_original_require, :require
9
+ alias_method :dead_end_original_require_relative, :require_relative
10
+ alias_method :dead_end_original_load, :load
11
+
12
+ def load(file, wrap = false)
13
+ dead_end_original_load(file)
14
+ rescue SyntaxError => e
15
+ DeadEnd.handle_error(e)
16
+ end
17
+
18
+ def require(file)
19
+ dead_end_original_require(file)
20
+ rescue SyntaxError => e
21
+ DeadEnd.handle_error(e)
22
+ end
23
+
24
+ def require_relative(file)
25
+ if Pathname.new(file).absolute?
26
+ dead_end_original_require file
27
+ else
28
+ relative_from = caller_locations(1..1).first
29
+ relative_from_path = relative_from.absolute_path || relative_from.path
30
+ dead_end_original_require File.expand_path("../#{file}", relative_from_path)
31
+ end
32
+ rescue SyntaxError => e
33
+ DeadEnd.handle_error(e)
34
+ end
35
+ end
@@ -61,6 +61,17 @@ module DeadEnd
61
61
  # ^^^
62
62
  # Means it's a string or a symbol `"{"` rather than being
63
63
  # part of a data structure (like a hash) `{ a: b }`
64
+ # ignore it.
65
+ when :on_words_beg, :on_symbos_beg, :on_qwords_beg,
66
+ :on_qsymbols_beg, :on_regexp_beg, :on_tstring_beg
67
+ # ^^^
68
+ # Handle shorthand syntaxes like `%Q{ i am a string }`
69
+ #
70
+ # The start token will be the full thing `%Q{` but we
71
+ # need to count it as if it's a `{`. Any token
72
+ # can be used
73
+ char = lex.token[-1]
74
+ @count_for_char[char] += 1 if @count_for_char.key?(char)
64
75
  when :on_embexpr_beg
65
76
  # ^^^
66
77
  # Embedded string expressions like `"#{foo} <-embed"`
@@ -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,25 @@ 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
+ last_lex = nil
29
+ @lex.map! { |elem|
30
+ last_lex = LexValue.new(elem.pos.first, elem.event, elem.tok, elem.state, last_lex)
31
+ }
25
32
  end
26
33
 
27
34
  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
  #
@@ -13,19 +15,21 @@ module DeadEnd
13
15
  class LexValue
14
16
  attr_reader :line, :type, :token, :state
15
17
 
16
- def initialize(line, type, token, state)
18
+ def initialize(line, type, token, state, last_lex = nil)
17
19
  @line = line
18
20
  @type = type
19
21
  @token = token
20
22
  @state = state
21
23
 
22
- set_kw_end
24
+ set_kw_end(last_lex)
23
25
  end
24
26
 
25
- private def set_kw_end
27
+ private def set_kw_end(last_lex)
26
28
  @is_end = false
27
29
  @is_kw = false
28
30
  return if type != :on_kw
31
+ #
32
+ return if last_lex && last_lex.fname? # https://github.com/ruby/ruby/commit/776759e300e4659bb7468e2b97c8c2d4359a2953
29
33
 
30
34
  case token
31
35
  when "if", "unless", "while", "until"
@@ -39,6 +43,10 @@ module DeadEnd
39
43
  end
40
44
  end
41
45
 
46
+ def fname?
47
+ state.allbits?(Ripper::EXPR_FNAME)
48
+ end
49
+
42
50
  def ignore_newline?
43
51
  type == :on_ignored_nl
44
52
  end
@@ -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
@@ -18,6 +18,12 @@ module DeadEnd
18
18
  @errors << msg
19
19
  end
20
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
+
21
27
  def call
22
28
  @run_once ||= begin
23
29
  @errors = []
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadEnd
4
- VERSION = "3.0.1"
4
+ VERSION = "3.1.1"
5
5
  end
data/lib/dead_end.rb CHANGED
@@ -1,159 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
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.record_dir(dir)
32
- time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
33
- dir = Pathname(dir)
34
- symlink = dir.join("last").tap { |path| path.delete if path.exist? }
35
- dir.join(time).tap { |path|
36
- path.mkpath
37
- FileUtils.symlink(path.basename, symlink)
38
- }
39
- end
40
-
41
- def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: nil, timeout: TIMEOUT_DEFAULT, io: $stderr)
42
- search = nil
43
- filename = nil if filename == DEFAULT_VALUE
44
- Timeout.timeout(timeout) do
45
- record_dir ||= ENV["DEBUG"] ? "tmp" : nil
46
- search = CodeSearch.new(source, record_dir: record_dir).call
47
- end
48
-
49
- blocks = search.invalid_blocks
50
- DisplayInvalidBlocks.new(
51
- io: io,
52
- blocks: blocks,
53
- filename: filename,
54
- terminal: terminal,
55
- code_lines: search.code_lines
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
- end
141
-
142
- require_relative "dead_end/code_line"
143
- require_relative "dead_end/code_block"
144
- require_relative "dead_end/code_search"
145
- require_relative "dead_end/code_frontier"
146
- require_relative "dead_end/clean_document"
147
-
148
- require_relative "dead_end/lex_all"
149
- require_relative "dead_end/block_expand"
150
- require_relative "dead_end/insertion_sort"
151
- require_relative "dead_end/around_block_scan"
152
- require_relative "dead_end/ripper_errors"
153
- require_relative "dead_end/display_invalid_blocks"
154
- require_relative "dead_end/parse_blocks_from_indent_line"
155
-
156
- require_relative "dead_end/explain_syntax"
157
-
158
- require_relative "dead_end/auto"
159
- require_relative "dead_end/cli"
3
+ require_relative "dead_end/api"
4
+ require_relative "dead_end/core_ext"
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: 3.0.1
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - schneems
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-11-04 00:00:00.000000000 Z
11
+ date: 2022-01-04 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
@@ -36,6 +36,7 @@ files:
36
36
  - dead_end.gemspec
37
37
  - exe/dead_end
38
38
  - lib/dead_end.rb
39
+ - lib/dead_end/api.rb
39
40
  - lib/dead_end/around_block_scan.rb
40
41
  - lib/dead_end/auto.rb
41
42
  - lib/dead_end/block_expand.rb
@@ -46,6 +47,7 @@ files:
46
47
  - lib/dead_end/code_frontier.rb
47
48
  - lib/dead_end/code_line.rb
48
49
  - lib/dead_end/code_search.rb
50
+ - lib/dead_end/core_ext.rb
49
51
  - lib/dead_end/display_code_with_line_numbers.rb
50
52
  - lib/dead_end/display_invalid_blocks.rb
51
53
  - lib/dead_end/explain_syntax.rb
@@ -54,6 +56,7 @@ files:
54
56
  - lib/dead_end/lex_all.rb
55
57
  - lib/dead_end/lex_value.rb
56
58
  - lib/dead_end/parse_blocks_from_indent_line.rb
59
+ - lib/dead_end/pathname_from_message.rb
57
60
  - lib/dead_end/ripper_errors.rb
58
61
  - lib/dead_end/version.rb
59
62
  homepage: https://github.com/zombocom/dead_end.git
@@ -77,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
80
  - !ruby/object:Gem::Version
78
81
  version: '0'
79
82
  requirements: []
80
- rubygems_version: 3.2.22
83
+ rubygems_version: 3.2.32
81
84
  signing_key:
82
85
  specification_version: 4
83
86
  summary: Find syntax errors in your source in a snap