dead_end 2.0.2 → 3.0.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: ec9e05716b47eb5d96ac063126a59977d98b8cf3e6f09ba0a0065d425a3c15e5
4
- data.tar.gz: c78047c6dd36c1c716e1970e9bec5887bbcaf1fcc0341cbb5c11405043a07105
3
+ metadata.gz: 72373898d38363c0ea0c3cec7fe2747399724ce55dae4eca1ce57b8f59dfc767
4
+ data.tar.gz: 9e99a5fddb8a054b839aef4fe1c2b87792bc9b4161bf5a6689156f2999dca935
5
5
  SHA512:
6
- metadata.gz: 2eca38b35ef1371cae5fd104fa88dad33d44b17cb6bf29b01b5dea0c650d3d21c0dfbf2b1ac96f0a6f307e7d541cf2a2f7be062d59f1626b16c82f1a2f7d3a3e
7
- data.tar.gz: 1a09171cb208465cf48ff5ac0346240d5bf155aebb886c27b6259742f325bc58a0b73ec9998fcbb1544440e6df7363fa7ab57a04dba9e7887207f30916e8c3a9
6
+ metadata.gz: 7312a010846453f222bbb8717738831f9a2e23572eace345ed7738f00429c126d1a4760548ad9e7552237aee057b5c5bae90b7a0fe197144c3a9dc9d3db2ab0b
7
+ data.tar.gz: 0db2b356ab237f01eda6dc5ebeac0156c317d94535c2b59d099f766e1716d5d6f6b429edcc65bf439dea1bc15e1866f4d66a41cd28d927a29a666f54f54ee804
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## HEAD (unreleased)
2
2
 
3
+ ## 3.0.0
4
+
5
+ - [Breaking] Remove previously deprecated `require "dead_end/fyi"` interface (https://github.com/zombocom/dead_end/pull/94)
6
+ - Fix double output bug (https://github.com/zombocom/dead_end/pull/99)
7
+ - Fix bug causing poor results (fix #95, fix #88) (https://github.com/zombocom/dead_end/pull/96)
8
+ - DeadEnd is now fired on EVERY syntax error (https://github.com/zombocom/dead_end/pull/94)
9
+ - Output format changes:
10
+ - Parse errors emitted per-block rather than for the whole document (https://github.com/zombocom/dead_end/pull/94)
11
+ - The "banner" is now based on lexical analysis rather than parser regex (fix #68, fix #87) (https://github.com/zombocom/dead_end/pull/96)
12
+
3
13
  ## 2.0.2
4
14
 
5
15
  - Don't print terminal color codes when output is not tty (https://github.com/zombocom/dead_end/pull/91)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dead_end (2.0.2)
4
+ dead_end (3.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -2,19 +2,14 @@
2
2
 
3
3
  An error in your code forces you to stop. DeadEnd helps you find those errors to get you back on your way faster.
4
4
 
5
- DeadEnd: Unmatched `end` detected
6
-
7
- This code has an unmatched `end`. Ensure that all `end` lines
8
- in your code have a matching syntax keyword (`def`, `do`, etc.)
9
- and that you don't have any extra `end` lines.
10
-
11
- file: path/to/dog.rb
12
- simplified:
5
+ ```
6
+ Unmatched `end', missing keyword (`do', `def`, `if`, etc.) ?
13
7
 
14
- 3 class Dog
15
- 5 defbark
16
- 7 end
17
- 12 end
8
+ 1 class Dog
9
+ 2 defbark
10
+ 4 end
11
+ 5 end
12
+ ```
18
13
 
19
14
  ## Installation in your codebase
20
15
 
@@ -52,34 +47,99 @@ This gives you the CLI command `$ dead_end` for more info run `$ dead_end --help
52
47
 
53
48
  ## What syntax errors does it handle?
54
49
 
50
+ 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:
51
+
55
52
  - Missing `end`:
56
53
 
54
+ <!--
57
55
  ```ruby
58
56
  class Dog
59
57
  def bark
60
58
  puts "bark"
61
-
62
- def woof
63
- puts "woof"
64
- end
65
59
  end
66
- # => scratch.rb:8: syntax error, unexpected end-of-input, expecting `end'
67
60
  ```
61
+ -->
68
62
 
69
- - Unexpected `end`
63
+ ```
64
+ Unmatched keyword, missing `end' ?
70
65
 
66
+ ❯ 1 class Dog
67
+ ❯ 2 def bark
68
+ ❯ 4 end
69
+ ```
70
+
71
+ - Missing keyword
72
+ <!--
71
73
  ```ruby
72
74
  class Dog
73
75
  def speak
74
- @sounds.each |sound| # Note the missing `do` here
76
+ @sounds.each |sound|
75
77
  puts sound
76
78
  end
77
79
  end
78
80
  end
79
- # => scratch.rb:7: syntax error, unexpected `end', expecting end-of-input
80
81
  ```
82
+ -->
81
83
 
82
- As well as unmatched `|` and unmatched `}`. These errors can be time consuming to debug because Ruby often only tells you the last line in the file. The command `ruby -wc path/to/file.rb` can narrow it down a little bit, but this library does a better job.
84
+ ```
85
+ Unmatched `end', missing keyword (`do', `def`, `if`, etc.) ?
86
+
87
+ 1 class Dog
88
+ 2 def speak
89
+ ❯ 3 @sounds.each |sound|
90
+ ❯ 5 end
91
+ 6 end
92
+ 7 end
93
+ ```
94
+
95
+ - Missing pair characters (like `{}`, `[]`, `()` , or `|<var>|`)
96
+ <!--
97
+
98
+ ```ruby
99
+ class Dog
100
+ def speak(sound
101
+ puts sound
102
+ end
103
+ end
104
+ ```
105
+ -->
106
+
107
+ ```
108
+ Unmatched `(', missing `)' ?
109
+
110
+ 1 class Dog
111
+ ❯ 2 def speak(sound
112
+ ❯ 4 end
113
+ 5 end
114
+ ```
115
+
116
+ - Any ambiguous or unknown errors will be annotated by the original ripper error output:
117
+
118
+ <!--
119
+ class Dog
120
+ def meals_last_month
121
+ puts 3 *
122
+ end
123
+ end
124
+ -->
125
+
126
+ ```
127
+ syntax error, unexpected end-of-input
128
+
129
+ 1 class Dog
130
+ 2 def meals_last_month
131
+ ❯ 3 puts 3 *
132
+ 4 end
133
+ 5 end
134
+ ```
135
+
136
+ ## How is it better than `ruby -wc`?
137
+
138
+ Ruby allows you to syntax check a file with warnings using `ruby -wc`. This emits a parser error instead of a human focused error. Ruby's parse errors attempt to narrow down the location and can tell you if there is a glaring indentation error involving `end`.
139
+
140
+ The `dead_end` algorithm doesn't just guess at the location of syntax errors, it re-parses the document to prove that it captured them.
141
+
142
+ This library focuses on the human side of syntax errors. It cares less about why the document could not be parsed (computer problem) and more on what the programmer needs (human problem) to fix the problem.
83
143
 
84
144
  ## Sounds cool, but why isn't this baked into Ruby directly?
85
145
 
@@ -105,6 +165,14 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
105
165
 
106
166
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
107
167
 
168
+ ### How to debug changes to output display
169
+
170
+ 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
+
172
+ ```
173
+ $ DEBUG_DISPLAY=1 be rspec spec/ --format=failures
174
+ ```
175
+
108
176
  ## Contributing
109
177
 
110
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).
data/exe/dead_end CHANGED
@@ -1,81 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "pathname"
4
- require "optparse"
5
3
  require_relative "../lib/dead_end"
6
4
 
7
- options = {}
8
- options[:record_dir] = ENV["DEAD_END_RECORD_DIR"]
9
-
10
- parser = OptionParser.new do |opts|
11
- opts.banner = <<~EOM
12
- Usage: dead_end <file> [options]
13
-
14
- Parses a ruby source file and searches for syntax error(s) such as
15
- unexpected `end', expecting end-of-input.
16
-
17
- Example:
18
-
19
- $ dead_end dog.rb
20
-
21
- # ...
22
-
23
- ❯ 10 defdog
24
- ❯ 15 end
25
- ❯ 16
26
-
27
- Env options:
28
-
29
- DEAD_END_RECORD_DIR=<dir>
30
-
31
- When enabled, records the steps used to search for a syntax error to the
32
- given directory
33
-
34
- Options:
35
- EOM
36
-
37
- opts.version = DeadEnd::VERSION
38
-
39
- opts.on("--help", "Help - displays this message") do |v|
40
- puts opts
41
- exit
42
- end
43
-
44
- opts.on("--record <dir>", "When enabled, records the steps used to search for a syntax error to the given directory") do |v|
45
- options[:record_dir] = v
46
- end
47
-
48
- opts.on("--terminal", "Enable terminal highlighting") do |v|
49
- options[:terminal] = true
50
- end
51
-
52
- opts.on("--no-terminal", "Disable terminal highlighting") do |v|
53
- options[:terminal] = false
54
- end
55
- end
56
- parser.parse!
57
-
58
- file = ARGV[0]
59
-
60
- if file.nil? || file.empty?
61
- # Display help if raw command
62
- parser.parse! %w[--help]
63
- end
64
-
65
- file = Pathname(file)
66
- options[:record_dir] = "tmp" if ENV["DEBUG"]
67
-
68
- warn "Record dir: #{options[:record_dir]}" if options[:record_dir]
69
-
70
- display = DeadEnd.call(
71
- source: file.read,
72
- filename: file.expand_path,
73
- terminal: options.fetch(:terminal, DeadEnd::DEFAULT_VALUE),
74
- record_dir: options[:record_dir]
75
- )
76
-
77
- if display.document_ok?
78
- exit(0)
79
- else
80
- exit(1)
81
- end
5
+ DeadEnd::Cli.new(
6
+ argv: ARGV
7
+ ).call
data/lib/dead_end/auto.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../dead_end/internals"
3
+ require_relative "../dead_end"
4
4
 
5
5
  # Monkey patch kernel to ensure that all `require` calls call the same
6
6
  # method
@@ -33,23 +33,3 @@ module Kernel
33
33
  DeadEnd.handle_error(e)
34
34
  end
35
35
  end
36
-
37
- # I honestly have no idea why this Object delegation is needed
38
- # I keep staring at bootsnap and it doesn't have to do this
39
- # is there a bug in their implementation they haven't caught or
40
- # am I doing something different?
41
- class Object
42
- private
43
-
44
- def load(path, wrap = false)
45
- Kernel.load(path, wrap)
46
- rescue SyntaxError => e
47
- DeadEnd.handle_error(e)
48
- end
49
-
50
- def require(path)
51
- Kernel.require(path)
52
- rescue SyntaxError => e
53
- DeadEnd.handle_error(e)
54
- end
55
- end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "optparse"
5
+
6
+ module DeadEnd
7
+ # All the logic of the exe/dead_end CLI in one handy spot
8
+ #
9
+ # Cli.new(argv: ["--help"]).call
10
+ # Cli.new(argv: ["<path/to/file>.rb"]).call
11
+ # Cli.new(argv: ["<path/to/file>.rb", "--record=tmp"]).call
12
+ # Cli.new(argv: ["<path/to/file>.rb", "--terminal"]).call
13
+ #
14
+ class Cli
15
+ attr_accessor :options, :file_name
16
+
17
+ # ARGV is Everything passed to the executable, does not include executable name
18
+ #
19
+ # All other intputs are dependency injection for testing
20
+ def initialize(argv:, exit_obj: Kernel, io: $stdout, env: ENV)
21
+ @options = {}
22
+ @parser = nil
23
+ options[:record_dir] = env["DEAD_END_RECORD_DIR"]
24
+ options[:record_dir] = "tmp" if env["DEBUG"]
25
+ options[:terminal] = DeadEnd::DEFAULT_VALUE
26
+
27
+ @io = io
28
+ @argv = argv
29
+ @file_name = argv[0]
30
+ @exit_obj = exit_obj
31
+ end
32
+
33
+ def call
34
+ if file_name.nil? || file_name.empty?
35
+ # Display help if raw command
36
+ parser.parse! %w[--help]
37
+ else
38
+ parse
39
+ end
40
+
41
+ # Needed for testing since we fake exit
42
+ return if options[:exit]
43
+
44
+ file = Pathname(file_name)
45
+
46
+ @io.puts "Record dir: #{options[:record_dir]}" if options[:record_dir]
47
+
48
+ display = DeadEnd.call(
49
+ io: @io,
50
+ source: file.read,
51
+ filename: file.expand_path,
52
+ terminal: options.fetch(:terminal, DeadEnd::DEFAULT_VALUE),
53
+ record_dir: options[:record_dir]
54
+ )
55
+
56
+ if display.document_ok?
57
+ @exit_obj.exit(0)
58
+ else
59
+ @exit_obj.exit(1)
60
+ end
61
+ end
62
+
63
+ def parse
64
+ parser.parse!(@argv)
65
+
66
+ self
67
+ end
68
+
69
+ def parser
70
+ @parser ||= OptionParser.new do |opts|
71
+ opts.banner = <<~EOM
72
+ Usage: dead_end <file> [options]
73
+
74
+ Parses a ruby source file and searches for syntax error(s) such as
75
+ unexpected `end', expecting end-of-input.
76
+
77
+ Example:
78
+
79
+ $ dead_end dog.rb
80
+
81
+ # ...
82
+
83
+ ❯ 10 defdog
84
+ ❯ 15 end
85
+
86
+ ENV options:
87
+
88
+ DEAD_END_RECORD_DIR=<dir>
89
+
90
+ Records the steps used to search for a syntax error
91
+ to the given directory
92
+
93
+ Options:
94
+ EOM
95
+
96
+ opts.version = DeadEnd::VERSION
97
+
98
+ opts.on("--help", "Help - displays this message") do |v|
99
+ @io.puts opts
100
+ options[:exit] = true
101
+ @exit_obj.exit
102
+ end
103
+
104
+ opts.on("--record <dir>", "Records the steps used to search for a syntax error to the given directory") do |v|
105
+ options[:record_dir] = v
106
+ end
107
+
108
+ opts.on("--terminal", "Enable terminal highlighting") do |v|
109
+ options[:terminal] = true
110
+ end
111
+
112
+ opts.on("--no-terminal", "Disable terminal highlighting") do |v|
113
+ options[:terminal] = false
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -70,8 +70,24 @@ module DeadEnd
70
70
  end
71
71
 
72
72
  def valid?
73
- return @valid if @valid != UNSET
74
- @valid = DeadEnd.valid?(to_s)
73
+ if @valid == UNSET
74
+ # Performance optimization
75
+ #
76
+ # If all the lines were previously hidden
77
+ # and we expand to capture additional empty
78
+ # lines then the result cannot be invalid
79
+ #
80
+ # That means there's no reason to re-check all
81
+ # lines with ripper (which is expensive).
82
+ # Benchmark in commit message
83
+ @valid = if lines.all? { |l| l.hidden? || l.empty? }
84
+ true
85
+ else
86
+ DeadEnd.valid?(lines.map(&:original).join)
87
+ end
88
+ else
89
+ @valid
90
+ end
75
91
  end
76
92
 
77
93
  def to_s
@@ -54,18 +54,42 @@ module DeadEnd
54
54
  @code_lines = code_lines
55
55
  @frontier = []
56
56
  @unvisited_lines = @code_lines.sort_by(&:indent_index)
57
+ @has_run = false
58
+ @check_next = true
57
59
  end
58
60
 
59
61
  def count
60
62
  @frontier.count
61
63
  end
62
64
 
65
+ # Performance optimization
66
+ #
67
+ # Parsing with ripper is expensive
68
+ # If we know we don't have any blocks with invalid
69
+ # syntax, then we know we cannot have found
70
+ # the incorrect syntax yet.
71
+ #
72
+ # When an invalid block is added onto the frontier
73
+ # check document state
74
+ private def can_skip_check?
75
+ check_next = @check_next
76
+ @check_next = false
77
+
78
+ if check_next
79
+ false
80
+ else
81
+ true
82
+ end
83
+ end
84
+
63
85
  # Returns true if the document is valid with all lines
64
86
  # removed. By default it checks all blocks in present in
65
87
  # the frontier array, but can be used for arbitrary arrays
66
88
  # of codeblocks as well
67
- def holds_all_syntax_errors?(block_array = @frontier)
68
- without_lines = block_array.map do |block|
89
+ def holds_all_syntax_errors?(block_array = @frontier, can_cache: true)
90
+ return false if can_cache && can_skip_check?
91
+
92
+ without_lines = block_array.flat_map do |block|
69
93
  block.lines
70
94
  end
71
95
 
@@ -120,6 +144,8 @@ module DeadEnd
120
144
  @frontier.reject! { |b|
121
145
  b.starts_at >= block.starts_at && b.ends_at <= block.ends_at
122
146
  }
147
+
148
+ @check_next = true if block.invalid?
123
149
  @frontier << block
124
150
  @frontier.sort!
125
151
 
@@ -142,7 +168,7 @@ module DeadEnd
142
168
  # the smallest possible set of blocks that contain all the syntax errors
143
169
  def detect_invalid_blocks
144
170
  self.class.combination(@frontier.select(&:invalid?)).detect do |block_array|
145
- holds_all_syntax_errors?(block_array)
171
+ holds_all_syntax_errors?(block_array, can_cache: false)
146
172
  end || []
147
173
  end
148
174
  end
@@ -73,12 +73,13 @@ module DeadEnd
73
73
  puts " block indent: #{block.current_indent}"
74
74
  end
75
75
  @record_dir.join(filename).open(mode: "a") do |f|
76
- display = DisplayInvalidBlocks.new(
77
- blocks: block,
76
+ document = DisplayCodeWithLineNumbers.new(
77
+ lines: @code_lines.select(&:visible?),
78
78
  terminal: false,
79
- code_lines: @code_lines
80
- )
81
- f.write(display.indent(display.code_with_lines))
79
+ highlight_lines: block.lines
80
+ ).call
81
+
82
+ f.write(document)
82
83
  end
83
84
  end
84
85
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "banner"
4
3
  require_relative "capture_code_context"
5
4
  require_relative "display_code_with_line_numbers"
6
5
 
@@ -9,18 +8,13 @@ module DeadEnd
9
8
  class DisplayInvalidBlocks
10
9
  attr_reader :filename
11
10
 
12
- def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: DEFAULT_VALUE, invalid_obj: WhoDisSyntaxError::Null.new)
13
- @terminal = terminal == DEFAULT_VALUE ? io.isatty : terminal
14
-
15
- @filename = filename
11
+ def initialize(code_lines:, blocks:, io: $stderr, filename: nil, terminal: DEFAULT_VALUE)
16
12
  @io = io
17
-
18
13
  @blocks = Array(blocks)
19
-
20
- @invalid_lines = @blocks.map(&:lines).flatten
14
+ @filename = filename
21
15
  @code_lines = code_lines
22
16
 
23
- @invalid_obj = invalid_obj
17
+ @terminal = terminal == DEFAULT_VALUE ? io.isatty : terminal
24
18
  end
25
19
 
26
20
  def document_ok?
@@ -30,61 +24,58 @@ module DeadEnd
30
24
  def call
31
25
  if document_ok?
32
26
  @io.puts "Syntax OK"
33
- else
34
- found_invalid_blocks
27
+ return self
35
28
  end
36
- self
37
- end
38
29
 
39
- private def no_invalid_blocks
40
- @io.puts <<~EOM
41
- EOM
42
- end
43
-
44
- private def found_invalid_blocks
45
- @io.puts
46
- if banner
47
- @io.puts banner
30
+ if filename
31
+ @io.puts("--> #{filename}")
48
32
  @io.puts
49
33
  end
50
- @io.puts("file: #{filename}") if filename
51
- @io.puts <<~EOM
52
- simplified:
53
-
54
- #{indent(code_block)}
55
- EOM
56
- end
57
-
58
- def banner
59
- Banner.new(invalid_obj: @invalid_obj).call
60
- end
34
+ @blocks.each do |block|
35
+ display_block(block)
36
+ end
61
37
 
62
- def indent(string, with: " ")
63
- string.each_line.map { |l| with + l }.join
38
+ self
64
39
  end
65
40
 
66
- def code_block
67
- string = +""
68
- string << code_with_context
69
- string
70
- end
41
+ private def display_block(block)
42
+ # Build explanation
43
+ explain = ExplainSyntax.new(
44
+ code_lines: block.lines
45
+ ).call
71
46
 
72
- def code_with_context
47
+ # Enhance code output
48
+ # Also handles several ambiguious cases
73
49
  lines = CaptureCodeContext.new(
74
- blocks: @blocks,
50
+ blocks: block,
75
51
  code_lines: @code_lines
76
52
  ).call
77
53
 
78
- DisplayCodeWithLineNumbers.new(
54
+ # Build code output
55
+ document = DisplayCodeWithLineNumbers.new(
79
56
  lines: lines,
80
57
  terminal: @terminal,
81
- highlight_lines: @invalid_lines
58
+ highlight_lines: block.lines
82
59
  ).call
60
+
61
+ # Output syntax error explanation
62
+ explain.errors.each do |e|
63
+ @io.puts e
64
+ end
65
+ @io.puts
66
+
67
+ # Output code
68
+ @io.puts(document)
83
69
  end
84
70
 
85
- def code_with_lines
71
+ private def code_with_context
72
+ lines = CaptureCodeContext.new(
73
+ blocks: @blocks,
74
+ code_lines: @code_lines
75
+ ).call
76
+
86
77
  DisplayCodeWithLineNumbers.new(
87
- lines: @code_lines.select(&:visible?),
78
+ lines: lines,
88
79
  terminal: @terminal,
89
80
  highlight_lines: @invalid_lines
90
81
  ).call