syntax_search 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/check_changelog.yml +13 -0
- data/CHANGELOG.md +24 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -5
- data/README.md +28 -15
- data/assets/syntax_search.gif +0 -0
- data/exe/syntax_search +1 -0
- data/lib/syntax_search.rb +23 -14
- data/lib/syntax_search/around_block_scan.rb +193 -0
- data/lib/syntax_search/block_expand.rb +74 -0
- data/lib/syntax_search/capture_code_context.rb +62 -0
- data/lib/syntax_search/code_block.rb +24 -165
- data/lib/syntax_search/code_frontier.rb +40 -201
- data/lib/syntax_search/code_line.rb +42 -1
- data/lib/syntax_search/code_search.rb +60 -20
- data/lib/syntax_search/display_code_with_line_numbers.rb +56 -0
- data/lib/syntax_search/display_invalid_blocks.rb +46 -45
- data/lib/syntax_search/heredoc_block_parse.rb +30 -0
- data/lib/syntax_search/lex_all.rb +58 -0
- data/lib/syntax_search/parse_blocks_from_indent_line.rb +56 -0
- data/lib/syntax_search/version.rb +1 -1
- data/lib/syntax_search/who_dis_syntax_error.rb +32 -0
- data/syntax_search.gemspec +0 -2
- metadata +13 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66acadf6481512affd39e4a1fb691ed41a6be537a8a63801f71316863d30494d
|
4
|
+
data.tar.gz: 1bcb85706516cbe5c3f53a1ed48bf5215479672952fc62d2e3221c583e468b22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e60777bd284d78436db8608f34d11b4b5aa8456bfe0c9507ddf3a0e3b97b902a2d00d1537433ed914c637a0729ccb253fc9047fb6db9e3b18a8cc8d0294ae06
|
7
|
+
data.tar.gz: 2fb1b35e70507f4b358be4b767cbf266c2d47c55efba1b77d7ca5f260cac4e77d519fc7439f55c78c5294c0c87240d15b4cab3721c8cfd580b518f5051b7cd0c
|
@@ -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
|
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,31 @@
|
|
1
1
|
## HEAD (unreleased)
|
2
2
|
|
3
|
+
## 0.2.0
|
4
|
+
|
5
|
+
- Simplify large file output so minimal context around the invalid section is shown (https://github.com/zombocom/syntax_search/pull/26)
|
6
|
+
- Block expansion is now lexically aware of keywords (def/do/end etc.) (https://github.com/zombocom/syntax_search/pull/24)
|
7
|
+
- Fix bug where not all of a source is lexed which is used in heredoc detection/removal (https://github.com/zombocom/syntax_search/pull/23)
|
8
|
+
|
9
|
+
## 0.1.5
|
10
|
+
|
11
|
+
- Strip out heredocs in documents first (https://github.com/zombocom/syntax_search/pull/19)
|
12
|
+
|
13
|
+
## 0.1.4
|
14
|
+
|
15
|
+
- Parser gem replaced with Ripper (https://github.com/zombocom/syntax_search/pull/17)
|
16
|
+
|
17
|
+
## 0.1.3
|
18
|
+
|
19
|
+
- Internal refactor (https://github.com/zombocom/syntax_search/pull/13)
|
20
|
+
|
21
|
+
## 0.1.2
|
22
|
+
|
23
|
+
- Codeblocks in output are now indented with 4 spaces and "code fences" are removed (https://github.com/zombocom/syntax_search/pull/11)
|
24
|
+
- "Unmatched end" and "missing end" not generate different error text instructions (https://github.com/zombocom/syntax_search/pull/10)
|
25
|
+
|
3
26
|
## 0.1.1
|
4
27
|
|
5
|
-
-
|
28
|
+
- Fire search on both unexpected end-of-input and unexected end (https://github.com/zombocom/syntax_search/pull/8)
|
6
29
|
|
7
30
|
## 0.1.0
|
8
31
|
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,16 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
syntax_search (0.
|
5
|
-
parser
|
4
|
+
syntax_search (0.2.0)
|
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,30 +10,29 @@ 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
|
-
$ syntax_search
|
13
|
+
$ syntax_search path/to/file.rb
|
14
14
|
|
15
|
-
|
15
|
+
SyntaxSearch: Unmatched `end` detected
|
16
16
|
|
17
|
-
This code has an unmatched `end
|
18
|
-
|
19
|
-
|
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.
|
20
20
|
|
21
21
|
file: path/to/file.rb
|
22
22
|
simplified:
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
```
|
24
|
+
1 require 'zoo'
|
25
|
+
2
|
26
|
+
3 class Animal
|
27
|
+
4
|
28
|
+
❯ 5 defdog
|
29
|
+
❯ 7 end
|
30
|
+
8
|
31
|
+
12 end
|
33
32
|
|
34
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!
|
35
34
|
|
36
|
-
## Installation
|
35
|
+
## Installation in your codebase
|
37
36
|
|
38
37
|
To automatically search syntax errors when they happen, add this to your Gemfile:
|
39
38
|
|
@@ -51,6 +50,16 @@ If your application is not calling `Bundler.require` then you must manually add
|
|
51
50
|
require "syntax_search/auto"
|
52
51
|
```
|
53
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
|
+
|
54
63
|
To get the CLI and manually search for syntax errors, install the gem:
|
55
64
|
|
56
65
|
$ gem install syntax_search
|
@@ -74,6 +83,10 @@ We know that source code that does not contain a syntax error can be parsed. We
|
|
74
83
|
|
75
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.
|
76
85
|
|
86
|
+
Here's an example:
|
87
|
+
|
88
|
+
![](assets/syntax_search.gif)
|
89
|
+
|
77
90
|
## How is source code broken up into smaller blocks?
|
78
91
|
|
79
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
|
data/exe/syntax_search
CHANGED
data/lib/syntax_search.rb
CHANGED
@@ -2,10 +2,10 @@
|
|
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
|
@@ -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,24 @@ 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
|
-
|
117
|
-
|
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
|
-
|
124
|
-
|
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"
|
145
|
+
require_relative "syntax_search/lex_all"
|
@@ -0,0 +1,193 @@
|
|
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
|
+
@orig_indent = block.current_indent
|
36
|
+
@skip_array = []
|
37
|
+
@after_array = []
|
38
|
+
@before_array = []
|
39
|
+
@stop_after_kw = false
|
40
|
+
end
|
41
|
+
|
42
|
+
def skip(name)
|
43
|
+
@skip_array << name
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def stop_after_kw
|
48
|
+
@stop_after_kw = true
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def scan_while(&block)
|
53
|
+
stop_next = false
|
54
|
+
|
55
|
+
kw_count = 0
|
56
|
+
end_count = 0
|
57
|
+
@before_index = before_lines.reverse_each.take_while do |line|
|
58
|
+
next false if stop_next
|
59
|
+
next true if @skip_array.detect {|meth| line.send(meth) }
|
60
|
+
|
61
|
+
kw_count += 1 if line.is_kw?
|
62
|
+
end_count += 1 if line.is_end?
|
63
|
+
if @stop_after_kw && kw_count > end_count
|
64
|
+
stop_next = true
|
65
|
+
end
|
66
|
+
|
67
|
+
block.call(line)
|
68
|
+
end.reverse.first&.index
|
69
|
+
|
70
|
+
stop_next = false
|
71
|
+
kw_count = 0
|
72
|
+
end_count = 0
|
73
|
+
@after_index = after_lines.take_while do |line|
|
74
|
+
next false if stop_next
|
75
|
+
next true if @skip_array.detect {|meth| line.send(meth) }
|
76
|
+
|
77
|
+
kw_count += 1 if line.is_kw?
|
78
|
+
end_count += 1 if line.is_end?
|
79
|
+
if @stop_after_kw && end_count > kw_count
|
80
|
+
stop_next = true
|
81
|
+
end
|
82
|
+
|
83
|
+
block.call(line)
|
84
|
+
end.last&.index
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def capture_neighbor_context
|
89
|
+
lines = []
|
90
|
+
kw_count = 0
|
91
|
+
end_count = 0
|
92
|
+
before_lines.reverse.each do |line|
|
93
|
+
next if line.empty?
|
94
|
+
break if line.indent < @orig_indent
|
95
|
+
next if line.indent != @orig_indent
|
96
|
+
|
97
|
+
kw_count += 1 if line.is_kw?
|
98
|
+
end_count += 1 if line.is_end?
|
99
|
+
if kw_count != 0 && kw_count == end_count
|
100
|
+
lines << line
|
101
|
+
break
|
102
|
+
end
|
103
|
+
|
104
|
+
lines << line
|
105
|
+
end
|
106
|
+
|
107
|
+
lines.reverse!
|
108
|
+
|
109
|
+
kw_count = 0
|
110
|
+
end_count = 0
|
111
|
+
after_lines.each do |line|
|
112
|
+
# puts "line: #{line.number} #{line.original_line}, indent: #{line.indent}, #{line.empty?} #{line.indent == @orig_indent}"
|
113
|
+
|
114
|
+
next if line.empty?
|
115
|
+
break if line.indent < @orig_indent
|
116
|
+
next if line.indent != @orig_indent
|
117
|
+
|
118
|
+
kw_count += 1 if line.is_kw?
|
119
|
+
end_count += 1 if line.is_end?
|
120
|
+
if kw_count != 0 && kw_count == end_count
|
121
|
+
lines << line
|
122
|
+
break
|
123
|
+
end
|
124
|
+
|
125
|
+
lines << line
|
126
|
+
end
|
127
|
+
lines.select! {|line| !line.is_comment? }
|
128
|
+
|
129
|
+
lines
|
130
|
+
end
|
131
|
+
|
132
|
+
def on_falling_indent
|
133
|
+
last_indent = @orig_indent
|
134
|
+
before_lines.reverse.each do |line|
|
135
|
+
next if line.empty?
|
136
|
+
if line.indent < last_indent
|
137
|
+
yield line
|
138
|
+
last_indent = line.indent
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
last_indent = @orig_indent
|
143
|
+
after_lines.each do |line|
|
144
|
+
next if line.empty?
|
145
|
+
if line.indent < last_indent
|
146
|
+
yield line
|
147
|
+
last_indent = line.indent
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def scan_neighbors
|
153
|
+
self.scan_while {|line| line.not_empty? && line.indent >= @orig_indent }
|
154
|
+
end
|
155
|
+
|
156
|
+
def scan_adjacent_indent
|
157
|
+
before_indent = @code_lines[@orig_before_index.pred]&.indent || 0
|
158
|
+
after_indent = @code_lines[@orig_after_index.next]&.indent || 0
|
159
|
+
|
160
|
+
indent = [before_indent, after_indent].min
|
161
|
+
self.scan_while {|line| line.not_empty? && line.indent >= indent }
|
162
|
+
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
def start_at_next_line
|
167
|
+
before_index; after_index
|
168
|
+
@before_index -= 1
|
169
|
+
@after_index += 1
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
def code_block
|
174
|
+
CodeBlock.new(lines: @code_lines[before_index..after_index])
|
175
|
+
end
|
176
|
+
|
177
|
+
def before_index
|
178
|
+
@before_index ||= @orig_before_index
|
179
|
+
end
|
180
|
+
|
181
|
+
def after_index
|
182
|
+
@after_index ||= @orig_after_index
|
183
|
+
end
|
184
|
+
|
185
|
+
private def before_lines
|
186
|
+
@code_lines[0...@orig_before_index]
|
187
|
+
end
|
188
|
+
|
189
|
+
private def after_lines
|
190
|
+
@code_lines[@orig_after_index.next..-1]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|