syntax_search 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|