dead_end 3.0.0 → 3.1.0

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: 72373898d38363c0ea0c3cec7fe2747399724ce55dae4eca1ce57b8f59dfc767
4
- data.tar.gz: 9e99a5fddb8a054b839aef4fe1c2b87792bc9b4161bf5a6689156f2999dca935
3
+ metadata.gz: 80d945bb6aff86e5dce0ef0998bb524e0b18b813412c21a2442fb5824b641ea9
4
+ data.tar.gz: 5cfa47620a0e21e5cf5de02c96a9d30da43aee18bace9ef386da54397518dedb
5
5
  SHA512:
6
- metadata.gz: 7312a010846453f222bbb8717738831f9a2e23572eace345ed7738f00429c126d1a4760548ad9e7552237aee057b5c5bae90b7a0fe197144c3a9dc9d3db2ab0b
7
- data.tar.gz: 0db2b356ab237f01eda6dc5ebeac0156c317d94535c2b59d099f766e1716d5d6f6b429edcc65bf439dea1bc15e1866f4d66a41cd28d927a29a666f54f54ee804
6
+ metadata.gz: 94ada1bbdead52d89e883f18466d0bc0abc1fcb278e9ff4c191503392571f9ca00b33c71d6ed52ef83a50d7f7bf016b1f85ddbde0d0cb617b7201de680dac5be
7
+ data.tar.gz: 79eb132ca9d28196646823b00fd1b57feb9395fe88d4590272c809fd2af4074370853cc6e19ccd45fead55136586c5f50486329703eb138da178c5290139601b
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,7 +1,29 @@
1
1
  ## HEAD (unreleased)
2
2
 
3
+ ## 3.1.0
4
+
5
+ - Add support for Ruby 3.1 by updating `require_relative` logic (https://github.com/zombocom/dead_end/pull/120)
6
+ - Requiring `dead_end/auto` is now deprecated please require `dead_end` instead (https://github.com/zombocom/dead_end/pull/119)
7
+ - Requiring `dead_end/api` now loads code without monkeypatching core extensions (https://github.com/zombocom/dead_end/pull/119)
8
+ - The interface `DeadEnd.handle_error` is declared public and stable (https://github.com/zombocom/dead_end/pull/119)
9
+
10
+ ## 3.0.3
11
+
12
+ - Expand explanations coming from additional Ripper errors (https://github.com/zombocom/dead_end/pull/117)
13
+ - Fix explanation involving shorthand syntax for literals like `%w[]` and `%Q{}` (https://github.com/zombocom/dead_end/pull/116)
14
+
15
+ ## 3.0.2
16
+
17
+ - Fix windows filename detection (https://github.com/zombocom/dead_end/pull/114)
18
+ - Update links on readme and code of conduct (https://github.com/zombocom/dead_end/pull/107)
19
+
20
+ ## 3.0.1
21
+
22
+ - Fix CLI parsing when flags come before filename (https://github.com/zombocom/dead_end/pull/102)
23
+
3
24
  ## 3.0.0
4
25
 
26
+ - [Breaking] CLI now outputs to STDOUT instead of STDERR (https://github.com/zombocom/dead_end/pull/98)
5
27
  - [Breaking] Remove previously deprecated `require "dead_end/fyi"` interface (https://github.com/zombocom/dead_end/pull/94)
6
28
  - Fix double output bug (https://github.com/zombocom/dead_end/pull/99)
7
29
  - 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 CHANGED
@@ -9,3 +9,4 @@ gem "rake", "~> 12.0"
9
9
  gem "rspec", "~> 3.0"
10
10
  gem "stackprof"
11
11
  gem "standard"
12
+ gem "ruby-prof"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dead_end (3.0.0)
4
+ dead_end (3.1.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -42,6 +42,7 @@ GEM
42
42
  rubocop-performance (1.11.5)
43
43
  rubocop (>= 1.7.0, < 2.0)
44
44
  rubocop-ast (>= 0.4.0)
45
+ ruby-prof (1.4.3)
45
46
  ruby-progressbar (1.11.0)
46
47
  stackprof (0.2.16)
47
48
  standard (1.3.0)
@@ -56,8 +57,9 @@ DEPENDENCIES
56
57
  dead_end!
57
58
  rake (~> 12.0)
58
59
  rspec (~> 3.0)
60
+ ruby-prof
59
61
  stackprof
60
62
  standard
61
63
 
62
64
  BUNDLED WITH
63
- 2.2.29
65
+ 2.2.30
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.
@@ -170,12 +187,32 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
170
187
  You can see changes to output against a variety of invalid code by running specs and using the `DEBUG_DISPLAY=1` environment variable. For example:
171
188
 
172
189
  ```
173
- $ DEBUG_DISPLAY=1 be rspec spec/ --format=failures
190
+ $ DEBUG_DISPLAY=1 bundle exec rspec spec/ --format=failures
191
+ ```
192
+
193
+ ### Run profiler
194
+
195
+ You can output profiler data to the `tmp` directory by running:
196
+
197
+ ```
198
+ $ DEBUG_PERF=1 bundle exec rspec spec/integration/dead_end_spec.rb
199
+ ```
200
+
201
+ Some outputs are in text format, some are html, the raw marshaled data is available in `raw.rb.marshal`. See https://ruby-prof.github.io/#reports for more info. One interesting one, is the "kcachegrind" interface. To view this on mac:
202
+
203
+ ```
204
+ $ brew install qcachegrind
205
+ ```
206
+
207
+ Open:
208
+
209
+ ```
210
+ $ qcachegrind tmp/last/profile.callgrind.out.<numbers>
174
211
  ```
175
212
 
176
213
  ## Contributing
177
214
 
178
- 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).
179
216
 
180
217
 
181
218
  ## License
@@ -184,4 +221,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
184
221
 
185
222
  ## Code of Conduct
186
223
 
187
- 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,17 +85,16 @@ module DeadEnd
85
85
  #
86
86
  class CleanDocument
87
87
  def initialize(source:)
88
- @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"
93
93
  # and return self
94
94
  def call
95
- clean_sweep
96
- .join_trailing_slash!
97
- .join_consecutive!
98
- .join_heredoc!
95
+ join_trailing_slash!
96
+ join_consecutive!
97
+ join_heredoc!
99
98
 
100
99
  self
101
100
  end
@@ -122,17 +121,15 @@ module DeadEnd
122
121
  # puts "world"
123
122
  # EOM
124
123
  #
125
- # lines = CleanDocument.new(source: source).clean_sweep.lines
124
+ # lines = CleanDocument.new(source: source).lines
126
125
  # expect(lines[0].to_s).to eq("\n")
127
126
  # expect(lines[1].to_s).to eq("puts "hello")
128
127
  # expect(lines[2].to_s).to eq("\n")
129
128
  # expect(lines[3].to_s).to eq("puts "world")
130
129
  #
131
- # WARNING:
132
- # If you run this after any of the "join" commands, they
133
- # will be un-joined.
130
+ # Important: This must be done before lexing.
134
131
  #
135
- # After this change is made, we re-lex the document because
132
+ # After this change is made, we lex the document because
136
133
  # removing comments can change how the doc is parsed.
137
134
  #
138
135
  # For example:
@@ -142,7 +139,9 @@ module DeadEnd
142
139
  # # comment
143
140
  # where(name: 'schneems')
144
141
  # EOM
145
- # expect(values.count {|v| v.type == :on_ignored_nl}).to eq(1)
142
+ # expect(
143
+ # values.count {|v| v.type == :on_ignored_nl}
144
+ # ).to eq(1)
146
145
  #
147
146
  # After the comment is removed:
148
147
  #
@@ -151,26 +150,18 @@ module DeadEnd
151
150
  #
152
151
  # where(name: 'schneems')
153
152
  # EOM
154
- # expect(values.count {|v| v.type == :on_ignored_nl}).to eq(2)
153
+ # expect(
154
+ # values.count {|v| v.type == :on_ignored_nl}
155
+ # ).to eq(2)
155
156
  #
156
- def clean_sweep
157
- source = @document.map do |code_line|
158
- # Clean trailing whitespace on empty line
159
- if code_line.line.strip.empty?
160
- next CodeLine.new(line: "\n", index: code_line.index, lex: [])
157
+ def clean_sweep(source:)
158
+ source.lines.map do |line|
159
+ if line.match?(/^\s*(#[^{].*)?$/) # https://rubular.com/r/LLE10D8HKMkJvs
160
+ $/
161
+ else
162
+ line
161
163
  end
162
-
163
- # Remove comments
164
- if code_line.lex.detect { |lex| lex.type != :on_sp }&.type == :on_comment
165
- next CodeLine.new(line: "\n", index: code_line.index, lex: [])
166
- end
167
-
168
- code_line
169
- end.join
170
-
171
- @source = source
172
- @document = CodeLine.from_source(source)
173
- self
164
+ end
174
165
  end
175
166
 
176
167
  # Smushes all heredoc lines into one line
data/lib/dead_end/cli.rb CHANGED
@@ -12,7 +12,7 @@ module DeadEnd
12
12
  # Cli.new(argv: ["<path/to/file>.rb", "--terminal"]).call
13
13
  #
14
14
  class Cli
15
- attr_accessor :options, :file_name
15
+ attr_accessor :options
16
16
 
17
17
  # ARGV is Everything passed to the executable, does not include executable name
18
18
  #
@@ -26,22 +26,33 @@ module DeadEnd
26
26
 
27
27
  @io = io
28
28
  @argv = argv
29
- @file_name = argv[0]
30
29
  @exit_obj = exit_obj
31
30
  end
32
31
 
33
32
  def call
34
- if file_name.nil? || file_name.empty?
33
+ if @argv.empty?
35
34
  # Display help if raw command
36
35
  parser.parse! %w[--help]
36
+ return
37
37
  else
38
+ # Mutates @argv
38
39
  parse
40
+ return if options[:exit]
39
41
  end
40
42
 
41
- # Needed for testing since we fake exit
42
- return if options[:exit]
43
+ file_name = @argv.first
44
+ if file_name.nil?
45
+ @io.puts "No file given"
46
+ @exit_obj.exit(1)
47
+ return
48
+ end
43
49
 
44
50
  file = Pathname(file_name)
51
+ if !file.exist?
52
+ @io.puts "file not found: #{file.expand_path} "
53
+ @exit_obj.exit(1)
54
+ return
55
+ end
45
56
 
46
57
  @io.puts "Record dir: #{options[:record_dir]}" if options[:record_dir]
47
58
 
@@ -52,14 +52,16 @@ module DeadEnd
52
52
  class CodeFrontier
53
53
  def initialize(code_lines:)
54
54
  @code_lines = code_lines
55
- @frontier = []
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
60
62
 
61
63
  def count
62
- @frontier.count
64
+ @frontier.to_a.length
63
65
  end
64
66
 
65
67
  # Performance optimization
@@ -89,7 +91,7 @@ module DeadEnd
89
91
  def holds_all_syntax_errors?(block_array = @frontier, can_cache: true)
90
92
  return false if can_cache && can_skip_check?
91
93
 
92
- without_lines = block_array.flat_map do |block|
94
+ without_lines = block_array.to_a.flat_map do |block|
93
95
  block.lines
94
96
  end
95
97
 
@@ -101,7 +103,7 @@ module DeadEnd
101
103
 
102
104
  # Returns a code block with the largest indentation possible
103
105
  def pop
104
- @frontier.pop
106
+ @frontier.to_a.pop
105
107
  end
106
108
 
107
109
  def next_indent_line
@@ -109,15 +111,15 @@ module DeadEnd
109
111
  end
110
112
 
111
113
  def expand?
112
- return false if @frontier.empty?
113
- return true if @unvisited_lines.empty?
114
+ return false if @frontier.to_a.empty?
115
+ return true if @unvisited_lines.to_a.empty?
114
116
 
115
- frontier_indent = @frontier.last.current_indent
117
+ frontier_indent = @frontier.to_a.last.current_indent
116
118
  unvisited_indent = next_indent_line.indent
117
119
 
118
120
  if ENV["DEBUG"]
119
121
  puts "```"
120
- puts @frontier.last.to_s
122
+ puts @frontier.to_a.last.to_s
121
123
  puts "```"
122
124
  puts " @frontier indent: #{frontier_indent}"
123
125
  puts " @unvisited indent: #{unvisited_indent}"
@@ -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
 
@@ -141,13 +149,13 @@ module DeadEnd
141
149
  register_indent_block(block)
142
150
 
143
151
  # Make sure we don't double expand, if a code block fully engulfs another code block, keep the bigger one
144
- @frontier.reject! { |b|
152
+ @frontier.to_a.reject! { |b|
145
153
  b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
146
154
  }
147
155
 
148
156
  @check_next = true if block.invalid?
149
157
  @frontier << block
150
- @frontier.sort!
158
+ # @frontier.sort!
151
159
 
152
160
  self
153
161
  end
@@ -167,7 +175,7 @@ module DeadEnd
167
175
  # Given that we know our syntax error exists somewhere in our frontier, we want to find
168
176
  # the smallest possible set of blocks that contain all the syntax errors
169
177
  def detect_invalid_blocks
170
- self.class.combination(@frontier.select(&:invalid?)).detect do |block_array|
178
+ self.class.combination(@frontier.to_a.select(&:invalid?)).detect do |block_array|
171
179
  holds_all_syntax_errors?(block_array, can_cache: false)
172
180
  end || []
173
181
  end
@@ -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
@@ -43,8 +43,7 @@ module DeadEnd
43
43
 
44
44
  def initialize(source, record_dir: ENV["DEAD_END_RECORD_DIR"] || ENV["DEBUG"] ? "tmp" : nil)
45
45
  if record_dir
46
- @time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
47
- @record_dir = Pathname(record_dir).join(@time).tap { |p| p.mkpath }
46
+ @record_dir = DeadEnd.record_dir(record_dir)
48
47
  @write_count = 0
49
48
  end
50
49
 
@@ -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
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadEnd
4
+ # Sort elements on insert
5
+ #
6
+ # Instead of constantly calling `sort!`, put
7
+ # the element where it belongs the first time
8
+ # around
9
+ #
10
+ # Example:
11
+ #
12
+ # sorted = InsertionSort.new
13
+ # sorted << 33
14
+ # sorted << 44
15
+ # sorted << 1
16
+ # puts sorted.to_a
17
+ # # => [1, 44, 33]
18
+ #
19
+ class InsertionSort
20
+ def initialize
21
+ @array = []
22
+ end
23
+
24
+ def <<(value)
25
+ insert_in = @array.length
26
+ @array.each.with_index do |existing, index|
27
+ case value <=> existing
28
+ when -1
29
+ insert_in = index
30
+ break
31
+ when 0
32
+ insert_in = index
33
+ break
34
+ when 1
35
+ # Keep going
36
+ end
37
+ end
38
+
39
+ @array.insert(insert_in, value)
40
+ end
41
+
42
+ def to_a
43
+ @array
44
+ end
45
+ end
46
+ 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,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
@@ -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.0"
4
+ VERSION = "3.1.0"
5
5
  end
data/lib/dead_end.rb CHANGED
@@ -1,148 +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.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
-
147
- require_relative "dead_end/auto"
148
- 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.0
4
+ version: 3.1.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-11-03 00:00:00.000000000 Z
11
+ date: 2021-11-21 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,13 +47,16 @@ 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
+ - lib/dead_end/insertion_sort.rb
52
55
  - lib/dead_end/left_right_lex_count.rb
53
56
  - lib/dead_end/lex_all.rb
54
57
  - lib/dead_end/lex_value.rb
55
58
  - lib/dead_end/parse_blocks_from_indent_line.rb
59
+ - lib/dead_end/pathname_from_message.rb
56
60
  - lib/dead_end/ripper_errors.rb
57
61
  - lib/dead_end/version.rb
58
62
  homepage: https://github.com/zombocom/dead_end.git
@@ -76,7 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
80
  - !ruby/object:Gem::Version
77
81
  version: '0'
78
82
  requirements: []
79
- rubygems_version: 3.2.22
83
+ rubygems_version: 3.3.0.dev
80
84
  signing_key:
81
85
  specification_version: 4
82
86
  summary: Find syntax errors in your source in a snap