Skip to content

Schneems/search ish #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Nov 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
syntax_error_search (0.1.0)
syntax_search (0.1.0)
parser

GEM
Expand Down Expand Up @@ -32,7 +32,7 @@ PLATFORMS
DEPENDENCIES
rake (~> 12.0)
rspec (~> 3.0)
syntax_error_search!
syntax_search!

BUNDLED WITH
2.1.4
72 changes: 26 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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 <path/to/file.rb>

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!
Expand All @@ -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:
Expand All @@ -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?

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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).
2 changes: 1 addition & 1 deletion bin/console
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
73 changes: 73 additions & 0 deletions exe/syntax_search
Original file line number Diff line number Diff line change
@@ -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 <file> [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=<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 <dir>", "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]
)
47 changes: 41 additions & 6 deletions lib/syntax_error_search.rb → lib/syntax_search.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
51 changes: 51 additions & 0 deletions lib/syntax_search/auto.rb
Original file line number Diff line number Diff line change
@@ -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

Loading