syntax_search 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ require_relative "../syntax_search"
2
+
3
+ require_relative "auto.rb"
4
+
5
+ SyntaxErrorSearch.send(:remove_const, :SEARCH_SOURCE_ON_ERROR_DEFAULT)
6
+ SyntaxErrorSearch::SEARCH_SOURCE_ON_ERROR_DEFAULT = false
7
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SyntaxErrorSearch
4
+ VERSION = "0.1.0"
5
+ 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: []