dead_end 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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