diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b0e6a7..4f62b8c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,31 +10,25 @@ references: jobs: "ruby-2-5": docker: - - image: 'cimg/base:stable' + - image: circleci/ruby:2.5 steps: - checkout - - ruby/install: - version: "2.5" - ruby/install-deps - <<: *unit "ruby-2-6": docker: - - image: 'cimg/base:stable' + - image: circleci/ruby:2.6 steps: - checkout - - ruby/install: - version: "2.6" - ruby/install-deps - <<: *unit "ruby-2-7": docker: - - image: 'cimg/base:stable' + - image: circleci/ruby:2.7 steps: - checkout - - ruby/install: - version: "2.7" - ruby/install-deps - <<: *unit diff --git a/Gemfile b/Gemfile index bb44deb..57fb696 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" -# Specify your gem's dependencies in syntax_error_search.gemspec +# Specify your gem's dependencies in syntax_search.gemspec gemspec gem "rake", "~> 12.0" diff --git a/Gemfile.lock b/Gemfile.lock index bdaca7f..5029f2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_error_search (0.1.0) + syntax_search (0.1.0) parser GEM @@ -32,7 +32,7 @@ PLATFORMS DEPENDENCIES rake (~> 12.0) rspec (~> 3.0) - syntax_error_search! + syntax_search! BUNDLED WITH 2.1.4 diff --git a/README.md b/README.md index 040f1ee..2ab6f2d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SyntaxErrorSearch +# SyntaxSearch Imagine you're programming, everything is awesome. You write code, it runs. Still awesome. You write more code, you save it, you run it and then: @@ -11,7 +11,26 @@ What happened? Likely you forgot a `def`, `do`, or maybe you deleted some code a 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. ``` -# TODO example here +$ syntax_search + +SyntaxErrorSearch: A syntax error was detected + +This code has an unmatched `end` this is caused by either +missing a syntax keyword (`def`, `do`, etc.) or inclusion +of an extra `end` line + +file: path/to/file.rb +simplified: + + ``` + 1 require 'animals' + 2 + ❯ 10 defdog + ❯ 15 end + ❯ 16 + 20 def cat + 22 end + ``` ``` 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! @@ -21,7 +40,7 @@ How much would you pay for such a library? A million, a billion, a trillion? Wel Add this line to your application's Gemfile: ```ruby -gem 'syntax_error_search' +gem 'syntax_search', require: "syntax_search/auto" ``` And then execute: @@ -30,7 +49,7 @@ And then execute: Or install it yourself as: - $ gem install syntax_error_search + $ gem install syntax_search ## What does it do? @@ -55,48 +74,9 @@ By definition source code with a syntax error in it cannot be parsed, so we have At the end of the day we can't say where the syntax error is FOR SURE, but we can get pretty close. It sounds simple when spelled out like this, but it's a very complicated problem. Even when code is not correctly indented/formatted we can still likely tell you where to start searching even if we can't point at the exact problem line or location. -## Complicating concerns - -The biggest issue with searching for syntax errors stemming from "unexpected end" is that while the `end` in the code triggered the error, the problem actually came from somewhere else. Effectively these syntax errors always involve 2 or more lines of code, but one of those lines (without the end) may be syntatically valid on its own. For example: - -``` -1 Foo.call -2 -3 puts "lol -4 end -``` - -Here there's a missing `do` after `Foo.call` however `Foo.call` by itself is perfectly valid ruby code syntax. We don't find the error until we remove the `end` even though the problem is caused on the first line. This means that if our clode blocks aren't sliced totally correctly the error output might just point at: - -``` -4 end -``` - -Instead of: - -``` -1 Foo.call -4 end -``` - -Here's a similar issue, but with more `end` lines in the code to demonstrate. The same line of code causes the issue: - -``` -1 it "foo" do -2 Foo.call -3 -4 puts "lol -5 end -6 end -``` - -In this example we could make this code valid by either the end on line 5 or 6. As far as the program is concerned it's effectively got one too many ends and it won't care which you remove. The "correct" line to remove would be for the inner block, but it's hard to know this programatically. Whitespace can help guide us, but it's still a guess. - -One of the biggest challenges then is not finding code that can be removed to make the program syntatically correct (just remove an `end` and it works) but to also provide a reasonable guess as to the "pair" line that would have otherwise required an end (such as a `do` or a `def`). - ## How does this gem know when a syntax error occured in my code? -While I wish you hadn't asked: If you must know, we're monkey-patching require. It sounds scary, but bootsnap does essentially the same thing and we're way less invasive. +Right now the search isn't performed automatically when you get a syntax error. Instead we append a warning message letting you know how to test the file. Eventually we'll enable the seach by default instead of printing a warning message. To do both of these we have to monkeypatch `require` in the same way that bootsnap does. ## Development @@ -106,7 +86,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/syntax_error_search. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/syntax_error_search/blob/master/CODE_OF_CONDUCT.md). +Bug reports and pull requests are welcome on GitHub at https://github.com/zombocom/syntax_search. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/syntax_search/blob/master/CODE_OF_CONDUCT.md). ## License @@ -115,4 +95,4 @@ The gem is available as open source under the terms of the [MIT License](https:/ ## Code of Conduct -Everyone interacting in the SyntaxErrorSearch project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/syntax_error_search/blob/master/CODE_OF_CONDUCT.md). +Everyone interacting in the SyntaxErrorSearch project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/zombocom/syntax_search/blob/master/CODE_OF_CONDUCT.md). diff --git a/bin/console b/bin/console index 6e6d590..5b9e60d 100755 --- a/bin/console +++ b/bin/console @@ -1,7 +1,7 @@ #!/usr/bin/env ruby require "bundler/setup" -require "syntax_error_search" +require "syntax_search" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. diff --git a/exe/syntax_search b/exe/syntax_search new file mode 100755 index 0000000..d506a9b --- /dev/null +++ b/exe/syntax_search @@ -0,0 +1,73 @@ +#!/usr/bin/env ruby + +require 'pathname' +require "optparse" +require_relative "../lib/syntax_search.rb" + +options = {} +options[:terminal] = true +options[:record_dir] = ENV["SYNTAX_SEARCH_RECORD_DIR"] + +parser = OptionParser.new do |opts| + opts.banner = <<~EOM + Usage: syntax_search [options] + + Parses a ruby source file and searches for syntax error(s) unexpected `end', expecting end-of-input. + + Example: + + $ syntax_search dog.rb + + # ... + + ``` + 1 require 'animals' + 2 + ❯ 10 defdog + ❯ 15 end + ❯ 16 + 20 def cat + 22 end + ``` + + Env options: + + SYNTAX_SEARCH_RECORD_DIR= + + When enabled, records the steps used to search for a syntax error to the given directory + + Options: + EOM + + opts.on("--help", "Help - displays this message") do |v| + puts opts + exit + end + + opts.on("--record ", "When enabled, records the steps used to search for a syntax error to the given directory") do |v| + options[:record_dir] = v + end + + opts.on("--no-terminal", "Disable terminal highlighting") do |v| + options[:terminal] = false + end +end +parser.parse! + +file = ARGV[0] + +if file.nil? || file.empty? + # Display help if raw command + parser.parse! %w[--help] +end + +file = Pathname(file) + +$stderr.puts "Record dir: #{options[:record_dir]}" if options[:record_dir] + +SyntaxErrorSearch.call( + source: file.read, + filename: file.expand_path, + terminal: options[:terminal], + record_dir: options[:record_dir] +) diff --git a/lib/syntax_error_search.rb b/lib/syntax_search.rb similarity index 66% rename from lib/syntax_error_search.rb rename to lib/syntax_search.rb index 6c7a7a3..2513716 100644 --- a/lib/syntax_error_search.rb +++ b/lib/syntax_search.rb @@ -1,13 +1,48 @@ # frozen_string_literal: true -require "syntax_error_search/version" +require_relative "syntax_search/version" require 'parser/current' require 'tmpdir' +require 'stringio' require 'pathname' module SyntaxErrorSearch class Error < StandardError; end + SEARCH_SOURCE_ON_ERROR_DEFAULT = true + + def self.handle_error(e, search_source_on_error: SEARCH_SOURCE_ON_ERROR_DEFAULT) + raise e if !e.message.include?("expecting end-of-input") + + filename = e.message.split(":").first + + $stderr.sync = true + $stderr.puts "Run `$ syntax_search #{filename}` for more options\n" + + if search_source_on_error + self.call( + source: Pathname(filename).read, + filename: filename, + terminal: true + ) + end + + $stderr.puts "" + $stderr.puts "" + raise e + end + + def self.call(source: , filename: , terminal: false, record_dir: nil) + search = CodeSearch.new(source, record_dir: record_dir).call + + blocks = search.invalid_blocks + DisplayInvalidBlocks.new( + blocks: blocks, + filename: filename, + terminal: terminal, + io: $stderr + ).call + end # Used for counting spaces module SpaceCount @@ -94,8 +129,8 @@ def self.valid?(source) end end -require_relative "syntax_error_search/code_line" -require_relative "syntax_error_search/code_block" -require_relative "syntax_error_search/code_frontier" -require_relative "syntax_error_search/code_search" -require_relative "syntax_error_search/display_invalid_blocks" +require_relative "syntax_search/code_line" +require_relative "syntax_search/code_block" +require_relative "syntax_search/code_frontier" +require_relative "syntax_search/code_search" +require_relative "syntax_search/display_invalid_blocks" diff --git a/lib/syntax_search/auto.rb b/lib/syntax_search/auto.rb new file mode 100644 index 0000000..ad47389 --- /dev/null +++ b/lib/syntax_search/auto.rb @@ -0,0 +1,51 @@ +require_relative "../syntax_search" + +# Monkey patch kernel to ensure that all `require` calls call the same +# method +module Kernel + alias_method :original_require, :require + alias_method :original_require_relative, :require_relative + alias_method :original_load, :load + + def load(file, wrap = false) + original_load(file) + rescue SyntaxError => e + SyntaxErrorSearch.handle_error(e) + end + + def require(file) + original_require(file) + rescue SyntaxError => e + SyntaxErrorSearch.handle_error(e) + end + + def require_relative(file) + if Pathname.new(file).absolute? + original_require file + else + original_require File.expand_path("../#{file}", caller_locations(1, 1)[0].absolute_path) + end + rescue SyntaxError => e + SyntaxErrorSearch.handle_error(e) + end +end + +# I honestly have no idea why this Object delegation is needed +# I keep staring at bootsnap and it doesn't have to do this +# is there a bug in their implementation they haven't caught or +# am I doing something different? +class Object + private + def load(path, wrap = false) + Kernel.load(path, wrap) + rescue SyntaxError => e + SyntaxErrorSearch.handle_error(e) + end + + def require(path) + Kernel.require(path) + rescue SyntaxError => e + SyntaxErrorSearch.handle_error(e) + end +end + diff --git a/lib/syntax_error_search/code_block.rb b/lib/syntax_search/code_block.rb similarity index 97% rename from lib/syntax_error_search/code_block.rb rename to lib/syntax_search/code_block.rb index 86386e1..2f53a11 100644 --- a/lib/syntax_error_search/code_block.rb +++ b/lib/syntax_search/code_block.rb @@ -28,7 +28,7 @@ module SyntaxErrorSearch class CodeBlock attr_reader :lines - def initialize(code_lines:, lines: []) + def initialize(code_lines: nil, lines: []) @lines = Array(lines) @code_lines = code_lines end @@ -200,6 +200,14 @@ def document_valid_without? block_without.valid? end + def valid_without? + block_without.valid? + end + + def invalid? + !valid? + end + def valid? SyntaxErrorSearch.valid?(self.to_s) end diff --git a/lib/syntax_error_search/code_frontier.rb b/lib/syntax_search/code_frontier.rb similarity index 88% rename from lib/syntax_error_search/code_frontier.rb rename to lib/syntax_search/code_frontier.rb index 6921bfe..79cd042 100644 --- a/lib/syntax_error_search/code_frontier.rb +++ b/lib/syntax_search/code_frontier.rb @@ -140,6 +140,39 @@ module SyntaxErrorSearch # ] # # Once invalid blocks are found and filtered, then they can be passed to a formatter. + # + # + # + + class IndentScan + attr_reader :code_lines + + def initialize(code_lines: ) + @code_lines = code_lines + end + + def neighbors_from_top(top_line) + code_lines + .select {|l| l.index >= top_line.index } + .select {|l| l.not_empty? } + .select {|l| l.visible? } + .take_while {|l| l.indent >= top_line.indent } + end + + def each_neighbor_block(top_line) + neighbors = neighbors_from_top(top_line) + + until neighbors.empty? + lines = [neighbors.pop] + while (block = CodeBlock.new(lines: lines, code_lines: code_lines)) && block.invalid? && neighbors.any? + lines.prepend neighbors.pop + end + + yield block if block + end + end + end + class CodeFrontier def initialize(code_lines: ) @code_lines = code_lines @@ -183,6 +216,19 @@ def next_block? !@indent_hash.empty? end + + def indent_hash_indent + @indent_hash.keys.sort.last + end + + def next_indent_line + indent = @indent_hash.keys.sort.last + @indent_hash[indent]&.first + end + + def generate_blocks + end + def next_block indent = @indent_hash.keys.sort.last lines = @indent_hash[indent].first @@ -196,6 +242,13 @@ def next_block block end + def expand? + return false if @frontier.empty? + return true if @indent_hash.empty? + + @frontier.last.current_indent >= @indent_hash.keys.sort.last + end + # This method is responsible for determining if a new code # block should be generated instead of evaluating an already # existing block in the frontier diff --git a/lib/syntax_error_search/code_line.rb b/lib/syntax_search/code_line.rb similarity index 100% rename from lib/syntax_error_search/code_line.rb rename to lib/syntax_search/code_line.rb diff --git a/lib/syntax_error_search/code_search.rb b/lib/syntax_search/code_search.rb similarity index 58% rename from lib/syntax_error_search/code_search.rb rename to lib/syntax_search/code_search.rb index 23ce509..33a03fc 100644 --- a/lib/syntax_error_search/code_search.rb +++ b/lib/syntax_search/code_search.rb @@ -26,12 +26,13 @@ module SyntaxErrorSearch # class CodeSearch private; attr_reader :frontier; public - public; attr_reader :invalid_blocks, :record_dir + public; attr_reader :invalid_blocks, :record_dir, :code_lines def initialize(string, record_dir: ENV["SYNTAX_SEARCH_RECORD_DIR"]) if record_dir @time = Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N') - @record_dir = Pathname(record_dir).join(@time) + @record_dir = Pathname(record_dir).join(@time).tap {|p| p.mkpath } + @write_count = 0 end @code_lines = string.lines.map.with_index do |line, i| CodeLine.new(line: line, index: i) @@ -40,13 +41,14 @@ def initialize(string, record_dir: ENV["SYNTAX_SEARCH_RECORD_DIR"]) @invalid_blocks = [] @name_tick = Hash.new {|hash, k| hash[k] = 0 } @tick = 0 + @scan = IndentScan.new(code_lines: @code_lines) end def record(block:, name: "record") return if !@record_dir @name_tick[name] += 1 - file = @record_dir.join("#{@tick}-#{name}-#{@name_tick[name]}.txt").tap {|p| p.dirname.mkpath } - file.open(mode: "a") do |f| + filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}.txt" + @record_dir.join(filename).open(mode: "a") do |f| display = DisplayInvalidBlocks.new( blocks: block, terminal: false @@ -55,37 +57,53 @@ def record(block:, name: "record") end end - def expand_frontier - return if !frontier.next_block? - block = frontier.next_block - record(block: block, name: "add") + def push_if_invalid(block, name: ) + frontier.register(block) + record(block: block, name: name) + if block.valid? block.lines.each(&:mark_invisible) - return expand_frontier + frontier << block else frontier << block end - block end - def search + def add_invalid_blocks + max_indent = frontier.next_indent_line&.indent + + while (line = frontier.next_indent_line) && (line.indent == max_indent) + neighbors = @scan.neighbors_from_top(frontier.next_indent_line) + + @scan.each_neighbor_block(frontier.next_indent_line) do |block| + record(block: block, name: "add") + if block.valid? + block.lines.each(&:mark_invisible) + end + end + + block = CodeBlock.new(lines: neighbors, code_lines: @code_lines) + push_if_invalid(block, name: "add") + end + end + + def expand_invalid_block block = frontier.pop + return unless block block.expand_until_next_boundry - record(block: block, name: "expand") - if block.valid? - block.lines.each(&:mark_invisible) - else - frontier << block - end + push_if_invalid(block, name: "expand") end def call until frontier.holds_all_syntax_errors? @tick += 1 - expand_frontier - break if frontier.holds_all_syntax_errors? # Need to check after every time something is added to frontier - search + + if frontier.expand? + expand_invalid_block + else + add_invalid_blocks + end end @invalid_blocks.concat(frontier.detect_invalid_blocks ) diff --git a/lib/syntax_error_search/display_invalid_blocks.rb b/lib/syntax_search/display_invalid_blocks.rb similarity index 85% rename from lib/syntax_error_search/display_invalid_blocks.rb rename to lib/syntax_search/display_invalid_blocks.rb index c329161..1a5db93 100644 --- a/lib/syntax_error_search/display_invalid_blocks.rb +++ b/lib/syntax_search/display_invalid_blocks.rb @@ -9,15 +9,30 @@ def initialize(blocks:, io: $stderr, filename: nil, terminal: false) @terminal = terminal @filename = filename @io = io + @blocks = Array(blocks) @lines = @blocks.map(&:lines).flatten - @code_lines = @blocks.first.code_lines - @digit_count = @code_lines.last.line_number.to_s.length + @code_lines = @blocks.first&.code_lines || [] + @digit_count = @code_lines.last&.line_number.to_s.length @invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true } end def call + if @blocks.any? + found_invalid_blocks + else + @io.puts "Syntax OK" + end + self + end + + private def no_invalid_blocks + @io.puts <<~EOM + EOM + end + + private def found_invalid_blocks @io.puts <<~EOM SyntaxErrorSearch: A syntax error was detected @@ -33,7 +48,6 @@ def call #{indent(code_block)} EOM - self end def indent(string, with: " ") @@ -60,6 +74,7 @@ def terminal_highlight def code_with_lines @code_lines.map do |line| next if line.hidden? + string = String.new("") if @invalid_line_hash[line] string << "❯ " diff --git a/lib/syntax_search/fyi.rb b/lib/syntax_search/fyi.rb new file mode 100644 index 0000000..0bfc17c --- /dev/null +++ b/lib/syntax_search/fyi.rb @@ -0,0 +1,7 @@ +require_relative "../syntax_search" + +require_relative "auto.rb" + +SyntaxErrorSearch.send(:remove_const, :SEARCH_SOURCE_ON_ERROR_DEFAULT) +SyntaxErrorSearch::SEARCH_SOURCE_ON_ERROR_DEFAULT = false + diff --git a/lib/syntax_error_search/version.rb b/lib/syntax_search/version.rb similarity index 100% rename from lib/syntax_error_search/version.rb rename to lib/syntax_search/version.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 26ae3fe..d9cf0bd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "bundler/setup" -require "syntax_error_search" +require "syntax_search" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure @@ -19,6 +19,10 @@ def spec_dir Pathname(__dir__) end +def lib_dir + root_dir.join("lib") +end + def root_dir spec_dir.join("..") end @@ -35,6 +39,12 @@ def code_line_array(string) code_lines end +def run!(cmd) + out = `#{cmd} 2>&1` + raise "Command: #{cmd} failed: #{out}" unless $?.success? + out +end + # Allows us to write cleaner tests since <<~EOM block quotes # strip off all leading indentation and we need it to be preserved # sometimes. diff --git a/spec/syntax_error_search_spec.rb b/spec/syntax_error_search_spec.rb deleted file mode 100644 index 64bb37d..0000000 --- a/spec/syntax_error_search_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module SyntaxErrorSearch - RSpec.describe SyntaxErrorSearch do - it "has a version number" do - expect(SyntaxErrorSearch::VERSION).not_to be nil - end - - def ruby(script) - `ruby -I#{lib_dir} -rdid_you_do #{script} 2>&1` - end - - describe "foo" do - around(:each) do |example| - Dir.mktmpdir do |dir| - @tmpdir = Pathname(dir) - @script = @tmpdir.join("script.rb") - example.run - end - end - - it "blerg" do - @script.write <<~EOM - describe "things" do - it "blerg" do - end - - it "flerg" - end - - it "zlerg" do - end - end - EOM - - require_rb = @tmpdir.join("require.rb") - require_rb.write <<~EOM - require_relative "./script.rb" - EOM - - # out = ruby(require_rb) - # puts out - end - end - end -end diff --git a/spec/unit/code_block_spec.rb b/spec/unit/code_block_spec.rb index 7fbbcc3..5c19a09 100644 --- a/spec/unit/code_block_spec.rb +++ b/spec/unit/code_block_spec.rb @@ -4,6 +4,46 @@ module SyntaxErrorSearch RSpec.describe CodeBlock do + + it "expand until next boundry (indentation)" do + source_string = <<~EOM + def foo + Foo.call + end + end + EOM + + code_lines = code_line_array(source_string) + + scan = IndentScan.new(code_lines: code_lines) + neighbors = scan.neighbors_from_top(code_lines[1]) + + block = CodeBlock.new( + lines: neighbors.last, + code_lines: code_lines + ) + + expect(block.valid?).to be_falsey + expect(block.to_s).to eq(<<~EOM.indent(2)) + end + EOM + + frontier = [] + + scan.each_neighbor_block(code_lines[1]) do |block| + if block.valid? + block.lines.map(&:mark_valid) + else + frontier << block + end + end + + expect(frontier.join).to eq(<<~EOM.indent(2)) + Foo.call + end + EOM + end + it "expand until next boundry (indentation)" do source_string = <<~EOM describe "what" do diff --git a/spec/unit/code_search_spec.rb b/spec/unit/code_search_spec.rb index ce1c8f6..9ce3e71 100644 --- a/spec/unit/code_search_spec.rb +++ b/spec/unit/code_search_spec.rb @@ -19,7 +19,7 @@ def hai expect(search.record_dir.entries.map(&:to_s)).to include("1-add-1.txt") expect(search.record_dir.join("1-add-1.txt").read).to eq(<<~EOM.indent(2)) 1 class OH - ❯ 2 def hello + 2 def hello ❯ 3 def hai ❯ 4 end 5 end @@ -64,24 +64,98 @@ def hai expect(search.invalid_blocks.join).to eq(<<~EOM.indent(2)) def hello - def hai - end EOM end + describe "real world cases" do + it "finds hanging def in this project" do + search = CodeSearch.new( + fixtures_dir.join("this_project_extra_def.rb.txt").read, + ) + + search.call + + blocks = search.invalid_blocks + io = StringIO.new + display = DisplayInvalidBlocks.new( + blocks: blocks, + io: io, + ) + display.call + # puts io.string + + expect(display.code_with_lines.strip_control_codes).to include(<<~EOM) + ❯ 36 def filename + EOM + end + + it "Format Code blocks real world example" do + search = CodeSearch.new(<<~EOM) + require 'rails_helper' + + RSpec.describe AclassNameHere, type: :worker do + describe "thing" do + context "when" do + let(:thing) { stuff } + let(:another_thing) { moarstuff } + subject { foo.new.perform(foo.id, true) } + + it "stuff" do + subject + + expect(foo.foo.foo).to eq(true) + end + end + end # line 16 accidental end, but valid block + + context "stuff" do + let(:thing) { create(:foo, foo: stuff) } + let(:another_thing) { create(:stuff) } + + subject { described_class.new.perform(foo.id, false) } + + it "more stuff" do + subject + + expect(foo.foo.foo).to eq(false) + end + end + end # mismatched due to 16 + end + EOM + search.call + + blocks = search.invalid_blocks + io = StringIO.new + display = DisplayInvalidBlocks.new(blocks: blocks, io: io, filename: "fake/spec/lol.rb") + display.call + # io.string + + expect(display.code_with_lines).to include(<<~EOM) + 1 require 'rails_helper' + 2 + 3 RSpec.describe AclassNameHere, type: :worker do + ❯ 12 + ❯ 30 end # mismatched due to 16 + 31 end + EOM + end + end + + # For code that's not perfectly formatted, we ideally want to do our best # These examples represent the results that exist today, but I would like to improve upon them describe "needs improvement" do describe "missing describe/do line" do it "blerg" do - code_lines = code_line_array fixtures_dir.join("this_project_extra_def.rb.txt").read - block = CodeBlock.new( - lines: code_lines[27], - code_lines: code_lines - ) - expect(block.to_s).to eq(<<~EOM.indent(8)) - file: \#{filename} - EOM + # code_lines = code_line_array fixtures_dir.join("this_project_extra_def.rb.txt").read + # block = CodeBlock.new( + # lines: code_lines[31], + # code_lines: code_lines + # ) + # expect(block.to_s).to eq(<<~EOM.indent(8)) + # \#{code_with_filename} + # EOM # puts block.before_line.to_s.inspect # puts block.before_line.to_s.split(/\S/).inspect @@ -91,86 +165,51 @@ def hai # puts block.after_line.to_s.split(/\S/).inspect # puts block.after_line.indent - # puts block.next_indent # puts block.expand_until_next_boundry end + end - it "this project" do - search = CodeSearch.new( - fixtures_dir.join("this_project_extra_def.rb.txt").read, - ) - + describe "mis-matched-indentation" do + it "extra space before end" do + search = CodeSearch.new(<<~EOM) + Foo.call + def foo + puts "lol" + puts "lol" + end # one + end # two + EOM search.call - blocks = search.invalid_blocks - io = StringIO.new - display = DisplayInvalidBlocks.new( - blocks: blocks, - io: io, - ) - display.call - # puts io.string - - expect(display.code_with_lines.strip_control_codes).to include(<<~EOM) - ❯ 36 def filename + # TODO improve here, grab the two end instead of one + expect(search.invalid_blocks.join).to eq(<<~EOM.indent(3)) + end # one EOM end - it "Format Code blocks real world example" do + it "stacked ends 2" do search = CodeSearch.new(<<~EOM) - require 'rails_helper' - - RSpec.describe AclassNameHere, type: :worker do - describe "thing" do - context "when" do - let(:thing) { stuff } - let(:another_thing) { moarstuff } - subject { foo.new.perform(foo.id, true) } - - it "stuff" do - subject - - expect(foo.foo.foo).to eq(true) - end - end - end # here - - context "stuff" do - let(:thing) { create(:foo, foo: stuff) } - let(:another_thing) { create(:stuff) } - - subject { described_class.new.perform(foo.id, false) } + def lol + blerg + end - it "more stuff" do - subject + Foo.call do + end # one + end # two - expect(foo.foo.foo).to eq(false) - end - end - end + def lol end EOM search.call - blocks = search.invalid_blocks - io = StringIO.new - display = DisplayInvalidBlocks.new(blocks: blocks, io: io, filename: "fake/spec/lol.rb") - display.call - # puts io.string - - expect(display.code_with_lines.strip_control_codes).to eq(<<~EOM) - 1 require 'rails_helper' - 2 - 3 RSpec.describe AclassNameHere, type: :worker do - ❯ 4 describe "thing" do - ❯ 16 end # here - ❯ 30 end - 31 end + expect(search.invalid_blocks.join).to eq(<<~EOM) + Foo.call do + end # one + end # two + EOM end - end - describe "mis-matched-indentation" do it "stacked ends " do search = CodeSearch.new(<<~EOM) Foo.call @@ -182,6 +221,7 @@ def foo EOM search.call + # TODO improve here, eliminate inner def foo expect(search.invalid_blocks.join).to eq(<<~EOM) Foo.call def foo @@ -190,26 +230,10 @@ def foo EOM end - it "extra space before end" do - search = CodeSearch.new(<<~EOM) - Foo.call - def foo - puts "lol" - puts "lol" - end - end - EOM - search.call - - # Does not include the line with the error Foo.call - expect(search.invalid_blocks.join).to eq(<<~EOM.indent(3)) - end - EOM - end - it "missing space before end" do search = CodeSearch.new(<<~EOM) Foo.call + def foo puts "lol" puts "lol" @@ -218,6 +242,7 @@ def foo EOM search.call + # expand-1 and expand-2 seem to be broken? expect(search.invalid_blocks.join).to eq(<<~EOM) Foo.call end @@ -232,14 +257,14 @@ def foo def foo puts "lol" puts "lol" - end - end + end # one + end # two EOM search.call expect(search.invalid_blocks.join).to eq(<<~EOM) Foo.call - end + end # two EOM end @@ -271,12 +296,9 @@ def foo EOM search.call - expect(search.invalid_blocks.join).to eq(<<~EOM.indent(0)) - describe "hi" do + expect(search.invalid_blocks.join).to eq(<<~EOM.indent(2)) Foo.call end - end - Bar.call end EOM @@ -332,5 +354,19 @@ def foo expect(search.invalid_blocks).to eq([]) end + + it "expands frontier by eliminating valid lines" do + search = CodeSearch.new(<<~EOM) + def foo + puts 'lol' + end + EOM + search.add_invalid_blocks + + expect(search.code_lines.join).to eq(<<~EOM) + def foo + end + EOM + end end end diff --git a/spec/unit/display_invalid_blocks_spec.rb b/spec/unit/display_invalid_blocks_spec.rb index 868ada9..ba2afa4 100644 --- a/spec/unit/display_invalid_blocks_spec.rb +++ b/spec/unit/display_invalid_blocks_spec.rb @@ -4,6 +4,26 @@ module SyntaxErrorSearch RSpec.describe DisplayInvalidBlocks do + it "works with valid code" do + syntax_string = <<~EOM + class OH + def hello + end + def hai + end + end + EOM + + io = StringIO.new + display = DisplayInvalidBlocks.new( + blocks: CodeSearch.new(syntax_string).call.invalid_blocks, + terminal: false, + io: io + ) + display.call + expect(io.string).to include("Syntax OK") + end + it "outputs to io when using `call`" do code_lines = code_line_array(<<~EOM) class OH diff --git a/spec/unit/exe_spec.rb b/spec/unit/exe_spec.rb new file mode 100644 index 0000000..fcb47d2 --- /dev/null +++ b/spec/unit/exe_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require_relative "../spec_helper.rb" + +module SyntaxErrorSearch + RSpec.describe "exe" do + def exe_path + root_dir.join("exe").join("syntax_search") + end + + def exe(cmd) + run!("#{exe_path} #{cmd}") + end + + it "parses valid code" do + ruby_file = exe_path + out = exe(ruby_file) + expect(out.strip).to include("Syntax OK") + end + + it "parses invalid code" do + ruby_file = fixtures_dir.join("this_project_extra_def.rb.txt") + out = exe("#{ruby_file} --no-terminal") + + expect(out.strip).to include("A syntax error was detected") + expect(out.strip).to include("❯ 36 def filename") + end + + it "records search" do + Dir.mktmpdir do |dir| + dir = Pathname(dir) + tmp_dir = dir.join("tmp").tap(&:mkpath) + ruby_file = dir.join("file.rb") + ruby_file.write("def foo\n end\nend") + + expect(tmp_dir).to be_empty + + out = exe("#{ruby_file} --record #{tmp_dir}") + + expect(out.strip).to include("A syntax error was detected") + expect(tmp_dir).to_not be_empty + end + end + end +end diff --git a/spec/unit/syntax_search_spec.rb b/spec/unit/syntax_search_spec.rb new file mode 100644 index 0000000..97e6e98 --- /dev/null +++ b/spec/unit/syntax_search_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require_relative "../spec_helper.rb" + +module SyntaxErrorSearch + RSpec.describe SyntaxErrorSearch do + it "has a version number" do + expect(SyntaxErrorSearch::VERSION).not_to be nil + end + + it "detects require error and adds a message with auto mode" do + Dir.mktmpdir do |dir| + @tmpdir = Pathname(dir) + @script = @tmpdir.join("script.rb") + @script.write <<~EOM + describe "things" do + it "blerg" do + end + + it "flerg" + end + + it "zlerg" do + end + end + EOM + + require_rb = @tmpdir.join("require.rb") + require_rb.write <<~EOM + require_relative "./script.rb" + EOM + + out = `ruby -I#{lib_dir} -rsyntax_search/auto #{require_rb} 2>&1` + + expect(out).to include("This code has an unmatched") + expect(out).to include("Run `$ syntax_search") + expect($?.success?).to be_falsey + end + end + + it "detects require error and adds a message with fyi mode" do + Dir.mktmpdir do |dir| + @tmpdir = Pathname(dir) + @script = @tmpdir.join("script.rb") + @script.write <<~EOM + describe "things" do + it "blerg" do + end + + it "flerg" + end + + it "zlerg" do + end + end + EOM + + require_rb = @tmpdir.join("require.rb") + require_rb.write <<~EOM + require_relative "./script.rb" + EOM + + out = `ruby -I#{lib_dir} -rsyntax_search/fyi #{require_rb} 2>&1` + + expect(out).to_not include("This code has an unmatched") + expect(out).to include("Run `$ syntax_search") + expect($?.success?).to be_falsey + end + end + end +end diff --git a/syntax_error_search.gemspec b/syntax_search.gemspec similarity index 84% rename from syntax_error_search.gemspec rename to syntax_search.gemspec index 786e3a6..cd5d11d 100644 --- a/syntax_error_search.gemspec +++ b/syntax_search.gemspec @@ -1,22 +1,21 @@ # frozen_string_literal: true -require_relative 'lib/syntax_error_search/version' +require_relative 'lib/syntax_search/version' Gem::Specification.new do |spec| - spec.name = "syntax_error_search" + spec.name = "syntax_search" spec.version = SyntaxErrorSearch::VERSION spec.authors = ["schneems"] spec.email = ["richard.schneeman+foo@gmail.com"] spec.summary = %q{Find syntax errors in your source in a snap} spec.description = %q{When you get an "unexpected end" in your syntax this gem helps you find it} - spec.homepage = "https://github.com/zombocom/syntax_error_search.git" + spec.homepage = "https://github.com/zombocom/syntax_search.git" spec.license = "MIT" spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0") - spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "https://github.com/zombocom/syntax_error_search.git" + spec.metadata["source_code_uri"] = "https://github.com/zombocom/syntax_search.git" # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git.