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.
@@ -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: []