syntax_search 0.1.0 → 0.1.5

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: 7bc1e7685829016c797af5b036c68d0ba625a3a195c134486ca2852fda4aa339
4
- data.tar.gz: be5bb2c996fe5a4f9762257be2ff93d028ac3c2faa8db1e27c239c56ad65a552
3
+ metadata.gz: 77f054eda8b4e25dcaf0306818ae17bb1ff3da9c52f2d606141ff44cd343c51d
4
+ data.tar.gz: 44f4347d5078d0250286e78a08ff730b02094aeb05897c9088963381a95e9071
5
5
  SHA512:
6
- metadata.gz: ab335dbb7813fca47d939f9e88af8f669eddec5a7e773a846f09fe04082982bc59d79b1f8d7a056c3441e79226536161544aada44fd3eabea72d5363303cc616
7
- data.tar.gz: 47c0ccd38ec581c3266001db6028d71d3fdec089e51edc603ca4d3518eae042753b1fb885e9c194dc697b7522ec5aca8ac23447ff58f79755d7f1291fdef26d6
6
+ metadata.gz: 6ccc7e2ed620689614bdfae8943b8c15b81b4567d3b42e2c734ac7974699ed231272ce52929eff9d49eb25edd843ae3f7fa2398d0552b77665be779a4412461f
7
+ data.tar.gz: 1e866b4aaafb534f946da5e00efb50536a4d20bf0e1527f8641fb298cca5cc6b7d0e1f744f63da443b9920500d7d4ddf0113ffa8bde2b4689cd6793538a12b6e
@@ -0,0 +1,13 @@
1
+ name: Check Changelog
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, reopened, edited, synchronize]
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v1
11
+ - name: Check that CHANGELOG is touched
12
+ run: |
13
+ cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
@@ -0,0 +1,26 @@
1
+ ## HEAD (unreleased)
2
+
3
+ ## 0.1.5
4
+
5
+ - Strip out heredocs in documents first (https://github.com/zombocom/syntax_search/pull/19)
6
+
7
+ ## 0.1.4
8
+
9
+ - Parser gem replaced with Ripper (https://github.com/zombocom/syntax_search/pull/17)
10
+
11
+ ## 0.1.3
12
+
13
+ - Internal refactor (https://github.com/zombocom/syntax_search/pull/13)
14
+
15
+ ## 0.1.2
16
+
17
+ - Codeblocks in output are now indented with 4 spaces and "code fences" are removed (https://github.com/zombocom/syntax_search/pull/11)
18
+ - "Unmatched end" and "missing end" not generate different error text instructions (https://github.com/zombocom/syntax_search/pull/10)
19
+
20
+ ## 0.1.1
21
+
22
+ - Fire search on both unexpected end-of-input and unexected end (https://github.com/zombocom/syntax_search/pull/8)
23
+
24
+ ## 0.1.0
25
+
26
+ - Initial release
data/Gemfile CHANGED
@@ -7,3 +7,4 @@ gemspec
7
7
 
8
8
  gem "rake", "~> 12.0"
9
9
  gem "rspec", "~> 3.0"
10
+ gem "stackprof"
@@ -1,16 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- syntax_search (0.1.0)
5
- parser
4
+ syntax_search (0.1.5)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
9
8
  specs:
10
- ast (2.4.1)
11
9
  diff-lcs (1.4.4)
12
- parser (2.7.2.0)
13
- ast (~> 2.4.1)
14
10
  rake (12.3.3)
15
11
  rspec (3.10.0)
16
12
  rspec-core (~> 3.10.0)
@@ -25,6 +21,7 @@ GEM
25
21
  diff-lcs (>= 1.2.0, < 2.0)
26
22
  rspec-support (~> 3.10.0)
27
23
  rspec-support (3.10.0)
24
+ stackprof (0.2.16)
28
25
 
29
26
  PLATFORMS
30
27
  ruby
@@ -32,6 +29,7 @@ PLATFORMS
32
29
  DEPENDENCIES
33
30
  rake (~> 12.0)
34
31
  rspec (~> 3.0)
32
+ stackprof
35
33
  syntax_search!
36
34
 
37
35
  BUNDLED WITH
data/README.md CHANGED
@@ -10,34 +10,31 @@ What happened? Likely you forgot a `def`, `do`, or maybe you deleted some code a
10
10
 
11
11
  What if I told you, that there was a library that helped find your missing `def`s and missing `do`s. What if instead of searching through hundreds of lines of source for the cause of your syntax error, there was a way to highlight just code in the file that contained syntax errors.
12
12
 
13
- ```
14
- $ syntax_search <path/to/file.rb>
13
+ $ syntax_search path/to/file.rb
15
14
 
16
- SyntaxErrorSearch: A syntax error was detected
15
+ SyntaxSearch: Unmatched `end` detected
17
16
 
18
- This code has an unmatched `end` this is caused by either
19
- missing a syntax keyword (`def`, `do`, etc.) or inclusion
20
- of an extra `end` line
17
+ This code has an unmatched `end`. Ensure that all `end` lines
18
+ in your code have a matching syntax keyword (`def`, `do`, etc.)
19
+ and that you don't have any extra `end` lines.
21
20
 
22
- file: path/to/file.rb
23
- simplified:
21
+ file: path/to/file.rb
22
+ simplified:
24
23
 
25
- ```
26
- 1 require 'animals'
24
+ 1 require 'zoo'
27
25
  2
28
- 10 defdog
29
- ❯ 15 end
30
- 16
31
- 20 def cat
32
- 22 end
33
- ```
34
- ```
26
+ 3 class Animal
27
+ 4
28
+ 5 defdog
29
+ 7 end
30
+ 8
31
+ 12 end
35
32
 
36
33
  How much would you pay for such a library? A million, a billion, a trillion? Well friends, today is your lucky day because you can use this library today for free!
37
34
 
38
- ## Installation
35
+ ## Installation in your codebase
39
36
 
40
- Add this line to your application's Gemfile:
37
+ To automatically search syntax errors when they happen, add this to your Gemfile:
41
38
 
42
39
  ```ruby
43
40
  gem 'syntax_search', require: "syntax_search/auto"
@@ -47,10 +44,28 @@ And then execute:
47
44
 
48
45
  $ bundle install
49
46
 
50
- Or install it yourself as:
47
+ If your application is not calling `Bundler.require` then you must manually add a require:
48
+
49
+ ```ruby
50
+ require "syntax_search/auto"
51
+ ```
52
+
53
+ If you're using rspec add this to your `.rspec` file:
54
+
55
+ ```
56
+ --require syntax_search/auto
57
+ ```
58
+
59
+ > This is needed because people can execute a single test file via `bundle exec rspec path/to/file_spec.rb` and if that file has a syntax error, it won't load `spec_helper.rb` to trigger any requires.
60
+
61
+ ## Install the CLI
62
+
63
+ To get the CLI and manually search for syntax errors, install the gem:
51
64
 
52
65
  $ gem install syntax_search
53
66
 
67
+ This gives you the CLI command `$ syntax_search` for more info run `$ syntax_search --help`.
68
+
54
69
  ## What does it do?
55
70
 
56
71
  When your code triggers a SyntaxError due to an "unexpected `end'" in a file, this library fires to narrow down your search to the most likely offending locations.
@@ -68,6 +83,10 @@ We know that source code that does not contain a syntax error can be parsed. We
68
83
 
69
84
  Since there can be multiple syntax errors in a document it's not good enough to check individual code blocks, we've got to check multiple at the same time. We will keep creating and adding new blocks to our search until we detect that our "frontier" (which contains all of our blocks) contains the syntax error. After this, we can stop our search and instead focus on filtering to find the smallest subset of blocks that contain the syntax error.
70
85
 
86
+ Here's an example:
87
+
88
+ ![](assets/syntax_search.gif)
89
+
71
90
  ## How is source code broken up into smaller blocks?
72
91
 
73
92
  By definition source code with a syntax error in it cannot be parsed, so we have to guess how to chunk up the file into smaller pieces. Once we've split up the file we can safely rule out or zoom into a specific piece of code to determine the location of the syntax error. This libary uses indentation and empty lines to make guesses about what might be a "block" of code. Once we've got a chunk of code, we can test it.
Binary file
@@ -2,17 +2,17 @@
2
2
 
3
3
  require_relative "syntax_search/version"
4
4
 
5
- require 'parser/current'
6
5
  require 'tmpdir'
7
6
  require 'stringio'
8
7
  require 'pathname'
8
+ require 'ripper'
9
9
 
10
10
  module SyntaxErrorSearch
11
11
  class Error < StandardError; end
12
12
  SEARCH_SOURCE_ON_ERROR_DEFAULT = true
13
13
 
14
14
  def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT)
15
- raise e if !e.message.include?("expecting end-of-input")
15
+ raise e if !e.message.include?("end-of-input")
16
16
 
17
17
  filename = e.message.split(":").first
18
18
 
@@ -23,7 +23,7 @@ module SyntaxErrorSearch
23
23
  self.call(
24
24
  source: Pathname(filename).read,
25
25
  filename: filename,
26
- terminal: true
26
+ terminal: true,
27
27
  )
28
28
  end
29
29
 
@@ -40,6 +40,8 @@ module SyntaxErrorSearch
40
40
  blocks: blocks,
41
41
  filename: filename,
42
42
  terminal: terminal,
43
+ code_lines: search.code_lines,
44
+ invalid_type: invalid_type(source),
43
45
  io: $stderr
44
46
  ).call
45
47
  end
@@ -78,6 +80,13 @@ module SyntaxErrorSearch
78
80
  end
79
81
  end
80
82
 
83
+ def self.invalid?(source)
84
+ source = source.join if source.is_a?(Array)
85
+ source = source.to_s
86
+
87
+ Ripper.new(source).tap(&:parse).error?
88
+ end
89
+
81
90
  # Returns truthy if a given input source is valid syntax
82
91
  #
83
92
  # SyntaxErrorSearch.valid?(<<~EOM) # => true
@@ -113,24 +122,23 @@ module SyntaxErrorSearch
113
122
  # so passing a CodeLine in as an object or as an array
114
123
  # will convert it to it's code representation.
115
124
  def self.valid?(source)
116
- source = source.join if source.is_a?(Array)
117
- source = source.to_s
125
+ !invalid?(source)
126
+ end
118
127
 
119
- # Parser writes to stderr even if you catch the error
120
- stderr = $stderr
121
- $stderr = StringIO.new
122
128
 
123
- Parser::CurrentRuby.parse(source)
124
- true
125
- rescue Parser::SyntaxError
126
- false
127
- ensure
128
- $stderr = stderr if stderr
129
+ def self.invalid_type(source)
130
+ WhoDisSyntaxError.new(source).call.error_symbol
129
131
  end
130
132
  end
131
133
 
132
134
  require_relative "syntax_search/code_line"
133
135
  require_relative "syntax_search/code_block"
134
136
  require_relative "syntax_search/code_frontier"
135
- require_relative "syntax_search/code_search"
136
137
  require_relative "syntax_search/display_invalid_blocks"
138
+ require_relative "syntax_search/around_block_scan"
139
+ require_relative "syntax_search/block_expand"
140
+ require_relative "syntax_search/parse_blocks_from_indent_line"
141
+
142
+ require_relative "syntax_search/code_search"
143
+ require_relative "syntax_search/who_dis_syntax_error"
144
+ require_relative "syntax_search/heredoc_block_parse"
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ module SyntaxErrorSearch
4
+ # This class is useful for exploring contents before and after
5
+ # a block
6
+ #
7
+ # It searches above and below the passed in block to match for
8
+ # whatever criteria you give it:
9
+ #
10
+ # Example:
11
+ #
12
+ # def dog
13
+ # puts "bark"
14
+ # puts "bark"
15
+ # end
16
+ #
17
+ # scan = AroundBlockScan.new(
18
+ # code_lines: code_lines
19
+ # block: CodeBlock.new(lines: code_lines[1])
20
+ # )
21
+ #
22
+ # scan.scan_while { true }
23
+ #
24
+ # puts scan.before_index # => 0
25
+ # puts scan.after_index # => 3
26
+ #
27
+ # Contents can also be filtered using AroundBlockScan#skip
28
+ #
29
+ # To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent
30
+ class AroundBlockScan
31
+ def initialize(code_lines: , block:)
32
+ @code_lines = code_lines
33
+ @orig_before_index = block.lines.first.index
34
+ @orig_after_index = block.lines.last.index
35
+ @skip_array = []
36
+ @after_array = []
37
+ @before_array = []
38
+ end
39
+
40
+ def skip(name)
41
+ @skip_array << name
42
+ self
43
+ end
44
+
45
+ def scan_while(&block)
46
+ @before_index = before_lines.reverse_each.take_while do |line|
47
+ next true if @skip_array.detect {|meth| line.send(meth) }
48
+
49
+ block.call(line)
50
+ end.reverse.first&.index
51
+
52
+ @after_index = after_lines.take_while do |line|
53
+ next true if @skip_array.detect {|meth| line.send(meth) }
54
+
55
+ block.call(line)
56
+ end.last&.index
57
+ self
58
+ end
59
+
60
+ def scan_adjacent_indent
61
+ before_indent = @code_lines[@orig_before_index.pred]&.indent || 0
62
+ after_indent = @code_lines[@orig_after_index.next]&.indent || 0
63
+
64
+ indent = [before_indent, after_indent].min
65
+ @before_index = before_index.pred if before_indent >= indent
66
+ @after_index = after_index.next if after_indent >= indent
67
+
68
+ self
69
+ end
70
+
71
+ def code_block
72
+ CodeBlock.new(lines: @code_lines[before_index..after_index])
73
+ end
74
+
75
+ def before_index
76
+ @before_index || @orig_before_index
77
+ end
78
+
79
+ def after_index
80
+ @after_index || @orig_after_index
81
+ end
82
+
83
+ private def before_lines
84
+ @code_lines[0...@orig_before_index]
85
+ end
86
+
87
+ private def after_lines
88
+ @code_lines[@orig_after_index.next..-1]
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ module SyntaxErrorSearch
3
+ # This class is responsible for taking a code block that exists
4
+ # at a far indentaion and then iteratively increasing the block
5
+ # so that it captures everything within the same indentation block.
6
+ #
7
+ # def dog
8
+ # puts "bow"
9
+ # puts "wow"
10
+ # end
11
+ #
12
+ # block = BlockExpand.new(code_lines: code_lines)
13
+ # .call(CodeBlock.new(lines: code_lines[1]))
14
+ #
15
+ # puts block.to_s
16
+ # # => puts "bow"
17
+ # puts "wow"
18
+ #
19
+ #
20
+ # Once a code block has captured everything at a given indentation level
21
+ # then it will expand to capture surrounding indentation.
22
+ #
23
+ # block = BlockExpand.new(code_lines: code_lines)
24
+ # .call(block)
25
+ #
26
+ # block.to_s
27
+ # # => def dog
28
+ # puts "bow"
29
+ # puts "wow"
30
+ # end
31
+ #
32
+ class BlockExpand
33
+ def initialize(code_lines: )
34
+ @code_lines = code_lines
35
+ end
36
+
37
+ def call(block)
38
+ if (next_block = expand_neighbors(block, grab_empty: true))
39
+ return next_block
40
+ end
41
+
42
+ expand_indent(block)
43
+ end
44
+
45
+ def expand_indent(block)
46
+ block = AroundBlockScan.new(code_lines: @code_lines, block: block)
47
+ .scan_adjacent_indent
48
+ .code_block
49
+
50
+ # Handle if/else/end case
51
+ if (next_block = expand_neighbors(block, grab_empty: false))
52
+ return next_block
53
+ else
54
+ return block
55
+ end
56
+ end
57
+
58
+ def expand_neighbors(block, grab_empty: true)
59
+ scan = AroundBlockScan.new(code_lines: @code_lines, block: block)
60
+ .skip(:hidden?)
61
+ .scan_while {|line| line.not_empty? && line.indent >= block.current_indent }
62
+
63
+ # Slurp up empties
64
+ if grab_empty
65
+ scan = AroundBlockScan.new(code_lines: @code_lines, block: scan.code_block)
66
+ .scan_while {|line| line.empty? || line.hidden? }
67
+ end
68
+
69
+ new_block = scan.code_block
70
+
71
+ if block.lines == new_block.lines
72
+ return nil
73
+ else
74
+ return new_block
75
+ end
76
+ end
77
+ end
78
+ end
@@ -3,11 +3,7 @@
3
3
  module SyntaxErrorSearch
4
4
  # Multiple lines form a singular CodeBlock
5
5
  #
6
- # Source code is made of multiple CodeBlocks. A code block
7
- # has a reference to the source code that created itself, this allows
8
- # a code block to "expand" when needed
9
- #
10
- # The most important ability of a CodeBlock is this ability to expand:
6
+ # Source code is made of multiple CodeBlocks.
11
7
  #
12
8
  # Example:
13
9
  #
@@ -16,33 +12,35 @@ module SyntaxErrorSearch
16
12
  # # puts "foo"
17
13
  # # end
18
14
  #
19
- # code_block.expand_until_next_boundry
15
+ # code_block.valid? # => true
16
+ # code_block.in_valid? # => false
20
17
  #
21
- # code_block.to_s # =>
22
- # # class Foo
23
- # # def foo
24
- # # puts "foo"
25
- # # end
26
- # # end
27
18
  #
28
19
  class CodeBlock
29
20
  attr_reader :lines
30
21
 
31
- def initialize(code_lines: nil, lines: [])
22
+ def initialize(lines: [])
32
23
  @lines = Array(lines)
33
- @code_lines = code_lines
24
+ end
25
+
26
+ def mark_invisible
27
+ @lines.map(&:mark_invisible)
34
28
  end
35
29
 
36
30
  def is_end?
37
31
  to_s.strip == "end"
38
32
  end
39
33
 
34
+ def hidden?
35
+ @lines.all?(&:hidden?)
36
+ end
37
+
40
38
  def starts_at
41
- @lines.first&.line_number
39
+ @starts_at ||= @lines.first&.line_number
42
40
  end
43
41
 
44
- def code_lines
45
- @code_lines
42
+ def ends_at
43
+ @ends_at ||= @lines.last&.line_number
46
44
  end
47
45
 
48
46
  # This is used for frontier ordering, we are searching from
@@ -53,155 +51,8 @@ module SyntaxErrorSearch
53
51
  self.current_indent <=> other.current_indent
54
52
  end
55
53
 
56
- # Only the lines that are not empty and visible
57
- def visible_lines
58
- @lines
59
- .select(&:not_empty?)
60
- .select(&:visible?)
61
- end
62
-
63
- # This method is used to expand a code block to capture it's calling context
64
- def expand_until_next_boundry
65
- expand_to_indent(next_indent)
66
- self
67
- end
68
-
69
- # This method expands the given code block until it captures
70
- # its nearest neighbors. This is used to expand a single line of code
71
- # to its smallest likely block.
72
- #
73
- # code_block.to_s # =>
74
- # # puts "foo"
75
- # code_block.expand_until_neighbors
76
- #
77
- # code_block.to_s # =>
78
- # # puts "foo"
79
- # # puts "bar"
80
- # # puts "baz"
81
- #
82
- def expand_until_neighbors
83
- expand_to_indent(current_indent)
84
-
85
- expand_hidden_parner_line if self.to_s.strip == "end"
86
- self
87
- end
88
-
89
- def expand_hidden_parner_line
90
- index = @lines.first.index
91
- indent = current_indent
92
- partner_line = code_lines.select {|line| line.index < index && line.indent == indent }.last
93
-
94
- if partner_line&.hidden?
95
- partner_line.mark_visible
96
- @lines.prepend(partner_line)
97
- end
98
- end
99
-
100
- # This method expands the existing code block up (before)
101
- # and down (after). It will break on change in indentation
102
- # and empty lines.
103
- #
104
- # code_block.to_s # =>
105
- # # def foo
106
- # # puts "foo"
107
- # # end
108
- #
109
- # code_block.expand_to_indent(0)
110
- # code_block.to_s # =>
111
- # # class Foo
112
- # # def foo
113
- # # puts "foo"
114
- # # end
115
- # # end
116
- #
117
- private def expand_to_indent(indent)
118
- array = []
119
- before_lines(skip_empty: false).each do |line|
120
- if line.empty?
121
- array.prepend(line)
122
- break
123
- end
124
-
125
- if line.indent == indent
126
- array.prepend(line)
127
- else
128
- break
129
- end
130
- end
131
-
132
- array << @lines
133
-
134
- after_lines(skip_empty: false).each do |line|
135
- if line.empty?
136
- array << line
137
- break
138
- end
139
-
140
- if line.indent == indent
141
- array << line
142
- else
143
- break
144
- end
145
- end
146
-
147
- @lines = array.flatten
148
- end
149
-
150
- def next_indent
151
- [
152
- before_line&.indent || 0,
153
- after_line&.indent || 0
154
- ].max
155
- end
156
-
157
54
  def current_indent
158
- lines.detect(&:not_empty?)&.indent || 0
159
- end
160
-
161
- def before_line
162
- before_lines.first
163
- end
164
-
165
- def after_line
166
- after_lines.first
167
- end
168
-
169
- def before_lines(skip_empty: true)
170
- index = @lines.first.index
171
- lines = code_lines.select {|line| line.index < index }
172
- lines.select!(&:not_empty?) if skip_empty
173
- lines.select!(&:visible?)
174
- lines.reverse!
175
-
176
- lines
177
- end
178
-
179
- def after_lines(skip_empty: true)
180
- index = @lines.last.index
181
- lines = code_lines.select {|line| line.index > index }
182
- lines.select!(&:not_empty?) if skip_empty
183
- lines.select!(&:visible?)
184
- lines
185
- end
186
-
187
- # Returns a code block of the source that does not include
188
- # the current lines. This is useful for checking if a source
189
- # with the given lines removed parses successfully. If so
190
- #
191
- # Then it's proof that the current block is invalid
192
- def block_without
193
- @block_without ||= CodeBlock.new(
194
- source: @source,
195
- lines: @source.code_lines - @lines
196
- )
197
- end
198
-
199
- def document_valid_without?
200
- block_without.valid?
201
- end
202
-
203
- def valid_without?
204
- block_without.valid?
55
+ @current_indent ||= lines.select(&:not_empty?).map(&:indent).min || 0
205
56
  end
206
57
 
207
58
  def invalid?