syntax_search 0.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 +7 -0
- data/.circleci/config.yml +41 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +98 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/syntax_search +73 -0
- data/lib/syntax_search.rb +136 -0
- data/lib/syntax_search/auto.rb +51 -0
- data/lib/syntax_search/code_block.rb +219 -0
- data/lib/syntax_search/code_frontier.rb +312 -0
- data/lib/syntax_search/code_line.rb +87 -0
- data/lib/syntax_search/code_search.rb +114 -0
- data/lib/syntax_search/display_invalid_blocks.rb +99 -0
- data/lib/syntax_search/fyi.rb +7 -0
- data/lib/syntax_search/version.rb +5 -0
- data/syntax_search.gemspec +30 -0
- metadata +84 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxErrorSearch
|
4
|
+
# Represents a single line of code of a given source file
|
5
|
+
#
|
6
|
+
# This object contains metadata about the line such as
|
7
|
+
# amount of indentation. An if it is empty or not.
|
8
|
+
#
|
9
|
+
# While a given search for syntax errors is being performed
|
10
|
+
# state about the search can be stored in individual lines such
|
11
|
+
# as :valid or :invalid.
|
12
|
+
#
|
13
|
+
# Visibility of lines can be toggled on and off.
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
#
|
17
|
+
# line = CodeLine.new(line: "def foo\n", index: 0)
|
18
|
+
# line.line_number => 1
|
19
|
+
# line.empty? # => false
|
20
|
+
# line.visible? # => true
|
21
|
+
# line.mark_invisible
|
22
|
+
# line.visible? # => false
|
23
|
+
#
|
24
|
+
# A CodeBlock is made of multiple CodeLines
|
25
|
+
#
|
26
|
+
# Marking a line as invisible indicates that it should not be used
|
27
|
+
# for syntax checks. It's essentially the same as commenting it out
|
28
|
+
#
|
29
|
+
# Marking a line as invisible also lets the overall program know
|
30
|
+
# that it should not check that area for syntax errors.
|
31
|
+
class CodeLine
|
32
|
+
attr_reader :line, :index, :indent
|
33
|
+
|
34
|
+
def initialize(line: , index:)
|
35
|
+
@original_line = line.freeze
|
36
|
+
@line = @original_line
|
37
|
+
@empty = line.strip.empty?
|
38
|
+
@index = index
|
39
|
+
@indent = SpaceCount.indent(line)
|
40
|
+
@status = nil # valid, invalid, unknown
|
41
|
+
@invalid = false
|
42
|
+
end
|
43
|
+
|
44
|
+
def mark_invalid
|
45
|
+
@invalid = true
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def marked_invalid?
|
50
|
+
@invalid
|
51
|
+
end
|
52
|
+
|
53
|
+
def mark_invisible
|
54
|
+
@line = ""
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def mark_visible
|
59
|
+
@line = @original_line
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def visible?
|
64
|
+
!line.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
def hidden?
|
68
|
+
!visible?
|
69
|
+
end
|
70
|
+
|
71
|
+
def line_number
|
72
|
+
index + 1
|
73
|
+
end
|
74
|
+
|
75
|
+
def not_empty?
|
76
|
+
!empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
def empty?
|
80
|
+
@empty
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
self.line
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxErrorSearch
|
4
|
+
# Searches code for a syntax error
|
5
|
+
#
|
6
|
+
# The bulk of the heavy lifting is done by the CodeFrontier
|
7
|
+
#
|
8
|
+
# The flow looks like this:
|
9
|
+
#
|
10
|
+
# ## Syntax error detection
|
11
|
+
#
|
12
|
+
# When the frontier holds the syntax error, we can stop searching
|
13
|
+
#
|
14
|
+
#
|
15
|
+
# search = CodeSearch.new(<<~EOM)
|
16
|
+
# def dog
|
17
|
+
# def lol
|
18
|
+
# end
|
19
|
+
# EOM
|
20
|
+
#
|
21
|
+
# search.call
|
22
|
+
#
|
23
|
+
# search.invalid_blocks.map(&:to_s) # =>
|
24
|
+
# # => ["def lol\n"]
|
25
|
+
#
|
26
|
+
#
|
27
|
+
class CodeSearch
|
28
|
+
private; attr_reader :frontier; public
|
29
|
+
public; attr_reader :invalid_blocks, :record_dir, :code_lines
|
30
|
+
|
31
|
+
def initialize(string, record_dir: ENV["SYNTAX_SEARCH_RECORD_DIR"])
|
32
|
+
if record_dir
|
33
|
+
@time = Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')
|
34
|
+
@record_dir = Pathname(record_dir).join(@time).tap {|p| p.mkpath }
|
35
|
+
@write_count = 0
|
36
|
+
end
|
37
|
+
@code_lines = string.lines.map.with_index do |line, i|
|
38
|
+
CodeLine.new(line: line, index: i)
|
39
|
+
end
|
40
|
+
@frontier = CodeFrontier.new(code_lines: @code_lines)
|
41
|
+
@invalid_blocks = []
|
42
|
+
@name_tick = Hash.new {|hash, k| hash[k] = 0 }
|
43
|
+
@tick = 0
|
44
|
+
@scan = IndentScan.new(code_lines: @code_lines)
|
45
|
+
end
|
46
|
+
|
47
|
+
def record(block:, name: "record")
|
48
|
+
return if !@record_dir
|
49
|
+
@name_tick[name] += 1
|
50
|
+
filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt"
|
51
|
+
@record_dir.join(filename).open(mode: "a") do |f|
|
52
|
+
display = DisplayInvalidBlocks.new(
|
53
|
+
blocks: block,
|
54
|
+
terminal: false
|
55
|
+
)
|
56
|
+
f.write(display.indent display.code_with_lines)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def push_if_invalid(block, name: )
|
61
|
+
frontier.register(block)
|
62
|
+
record(block: block, name: name)
|
63
|
+
|
64
|
+
if block.valid?
|
65
|
+
block.lines.each(&:mark_invisible)
|
66
|
+
frontier << block
|
67
|
+
else
|
68
|
+
frontier << block
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_invalid_blocks
|
73
|
+
max_indent = frontier.next_indent_line&.indent
|
74
|
+
|
75
|
+
while (line = frontier.next_indent_line) && (line.indent == max_indent)
|
76
|
+
neighbors = @scan.neighbors_from_top(frontier.next_indent_line)
|
77
|
+
|
78
|
+
@scan.each_neighbor_block(frontier.next_indent_line) do |block|
|
79
|
+
record(block: block, name: "add")
|
80
|
+
if block.valid?
|
81
|
+
block.lines.each(&:mark_invisible)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
block = CodeBlock.new(lines: neighbors, code_lines: @code_lines)
|
86
|
+
push_if_invalid(block, name: "add")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def expand_invalid_block
|
91
|
+
block = frontier.pop
|
92
|
+
return unless block
|
93
|
+
|
94
|
+
block.expand_until_next_boundry
|
95
|
+
push_if_invalid(block, name: "expand")
|
96
|
+
end
|
97
|
+
|
98
|
+
def call
|
99
|
+
until frontier.holds_all_syntax_errors?
|
100
|
+
@tick += 1
|
101
|
+
|
102
|
+
if frontier.expand?
|
103
|
+
expand_invalid_block
|
104
|
+
else
|
105
|
+
add_invalid_blocks
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
@invalid_blocks.concat(frontier.detect_invalid_blocks )
|
110
|
+
@invalid_blocks.sort_by! {|block| block.starts_at }
|
111
|
+
self
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SyntaxErrorSearch
|
4
|
+
# Used for formatting invalid blocks
|
5
|
+
class DisplayInvalidBlocks
|
6
|
+
attr_reader :filename
|
7
|
+
|
8
|
+
def initialize(blocks:, io: $stderr, filename: nil, terminal: false)
|
9
|
+
@terminal = terminal
|
10
|
+
@filename = filename
|
11
|
+
@io = io
|
12
|
+
|
13
|
+
@blocks = Array(blocks)
|
14
|
+
@lines = @blocks.map(&:lines).flatten
|
15
|
+
@code_lines = @blocks.first&.code_lines || []
|
16
|
+
@digit_count = @code_lines.last&.line_number.to_s.length
|
17
|
+
|
18
|
+
@invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true }
|
19
|
+
end
|
20
|
+
|
21
|
+
def call
|
22
|
+
if @blocks.any?
|
23
|
+
found_invalid_blocks
|
24
|
+
else
|
25
|
+
@io.puts "Syntax OK"
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
private def no_invalid_blocks
|
31
|
+
@io.puts <<~EOM
|
32
|
+
EOM
|
33
|
+
end
|
34
|
+
|
35
|
+
private def found_invalid_blocks
|
36
|
+
@io.puts <<~EOM
|
37
|
+
|
38
|
+
SyntaxErrorSearch: A syntax error was detected
|
39
|
+
|
40
|
+
This code has an unmatched `end` this is caused by either
|
41
|
+
missing a syntax keyword (`def`, `do`, etc.) or inclusion
|
42
|
+
of an extra `end` line
|
43
|
+
|
44
|
+
EOM
|
45
|
+
@io.puts("file: #{filename}") if filename
|
46
|
+
@io.puts <<~EOM
|
47
|
+
simplified:
|
48
|
+
|
49
|
+
#{indent(code_block)}
|
50
|
+
EOM
|
51
|
+
end
|
52
|
+
|
53
|
+
def indent(string, with: " ")
|
54
|
+
string.each_line.map {|l| with + l }.join
|
55
|
+
end
|
56
|
+
|
57
|
+
def code_block
|
58
|
+
string = String.new("")
|
59
|
+
string << "```\n"
|
60
|
+
# string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename
|
61
|
+
string << code_with_lines
|
62
|
+
string << "```\n"
|
63
|
+
string
|
64
|
+
end
|
65
|
+
|
66
|
+
def terminal_end
|
67
|
+
"\e[0m"
|
68
|
+
end
|
69
|
+
|
70
|
+
def terminal_highlight
|
71
|
+
"\e[1;3m" # Bold, italics
|
72
|
+
end
|
73
|
+
|
74
|
+
def code_with_lines
|
75
|
+
@code_lines.map do |line|
|
76
|
+
next if line.hidden?
|
77
|
+
|
78
|
+
string = String.new("")
|
79
|
+
if @invalid_line_hash[line]
|
80
|
+
string << "❯ "
|
81
|
+
else
|
82
|
+
string << " "
|
83
|
+
end
|
84
|
+
|
85
|
+
number = line.line_number.to_s.rjust(@digit_count)
|
86
|
+
string << number.to_s
|
87
|
+
if line.empty?
|
88
|
+
string << line.to_s
|
89
|
+
else
|
90
|
+
string << " "
|
91
|
+
string << terminal_highlight if @terminal && @invalid_line_hash[line] # Bold, italics
|
92
|
+
string << line.to_s
|
93
|
+
string << terminal_end if @terminal
|
94
|
+
end
|
95
|
+
string
|
96
|
+
end.join
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/syntax_search/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "syntax_search"
|
7
|
+
spec.version = SyntaxErrorSearch::VERSION
|
8
|
+
spec.authors = ["schneems"]
|
9
|
+
spec.email = ["richard.schneeman+foo@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = %q{Find syntax errors in your source in a snap}
|
12
|
+
spec.description = %q{When you get an "unexpected end" in your syntax this gem helps you find it}
|
13
|
+
spec.homepage = "https://github.com/zombocom/syntax_search.git"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/zombocom/syntax_search.git"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = "exe"
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ["lib"]
|
28
|
+
|
29
|
+
spec.add_dependency "parser"
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: syntax_search
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- schneems
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: parser
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: When you get an "unexpected end" in your syntax this gem helps you find
|
28
|
+
it
|
29
|
+
email:
|
30
|
+
- richard.schneeman+foo@gmail.com
|
31
|
+
executables:
|
32
|
+
- syntax_search
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".circleci/config.yml"
|
37
|
+
- ".gitignore"
|
38
|
+
- ".rspec"
|
39
|
+
- ".travis.yml"
|
40
|
+
- CODE_OF_CONDUCT.md
|
41
|
+
- Gemfile
|
42
|
+
- Gemfile.lock
|
43
|
+
- LICENSE.txt
|
44
|
+
- README.md
|
45
|
+
- Rakefile
|
46
|
+
- bin/console
|
47
|
+
- bin/setup
|
48
|
+
- exe/syntax_search
|
49
|
+
- lib/syntax_search.rb
|
50
|
+
- lib/syntax_search/auto.rb
|
51
|
+
- lib/syntax_search/code_block.rb
|
52
|
+
- lib/syntax_search/code_frontier.rb
|
53
|
+
- lib/syntax_search/code_line.rb
|
54
|
+
- lib/syntax_search/code_search.rb
|
55
|
+
- lib/syntax_search/display_invalid_blocks.rb
|
56
|
+
- lib/syntax_search/fyi.rb
|
57
|
+
- lib/syntax_search/version.rb
|
58
|
+
- syntax_search.gemspec
|
59
|
+
homepage: https://github.com/zombocom/syntax_search.git
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata:
|
63
|
+
homepage_uri: https://github.com/zombocom/syntax_search.git
|
64
|
+
source_code_uri: https://github.com/zombocom/syntax_search.git
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 2.5.0
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubygems_version: 3.0.3
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: Find syntax errors in your source in a snap
|
84
|
+
test_files: []
|