1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

Import spec examples from ruby/syntax_suggest

This commit is contained in:
Hiroshi SHIBATA 2022-08-19 15:37:45 +09:00
parent 3504be1bc1
commit 0d9f4ea0d4
Notes: git 2022-08-26 12:16:18 +09:00
26 changed files with 14536 additions and 0 deletions

View file

@ -0,0 +1,74 @@
# frozen_string_literal: true
# Tree structure used to store and sort require memory costs
# RequireTree.new('get_process_mem')
module DerailedBenchmarks
class RequireTree
REQUIRED_BY = {}
attr_reader :name
attr_writer :cost
attr_accessor :parent
def initialize(name)
@name = name
@children = {}
@cost = 0
def self.reset!
REQUIRED_BY.clear
if defined?(Kernel::REQUIRE_STACK)
Kernel::REQUIRE_STACK.clear
Kernel::REQUIRE_STACK.push(TOP_REQUIRE)
end
end
def <<(tree)
@children[tree.name.to_s] = tree
tree.parent = self
(REQUIRED_BY[tree.name.to_s] ||= []) << self.name
end
def [](name)
@children[name.to_s]
end
# Returns array of child nodes
def children
@children.values
end
def cost
@cost || 0
end
# Returns sorted array of child nodes from Largest to Smallest
def sorted_children
children.sort { |c1, c2| c2.cost <=> c1.cost }
end
def to_string
str = String.new("#{name}: #{cost.round(4)} MiB")
if parent && REQUIRED_BY[self.name.to_s]
names = REQUIRED_BY[self.name.to_s].uniq - [parent.name.to_s]
if names.any?
str << " (Also required by: #{ names.first(2).join(", ") }"
str << ", and #{names.count - 2} others" if names.count > 3
str << ")"
end
end
str
end
# Recursively prints all child nodes
def print_sorted_children(level = 0, out = STDOUT)
return if cost < ENV['CUT_OFF'].to_f
out.puts " " * level + self.to_string
level += 1
sorted_children.each do |child|
child.print_sorted_children(level, out)
end
end
end
end

View file

@ -0,0 +1,569 @@
#!/usr/bin/env ruby
#
# rexe - Ruby Command Line Executor Filter
#
# Inspired by https://github.com/thisredone/rb
# frozen_string_literal: true
require 'bundler'
require 'date'
require 'optparse'
require 'ostruct'
require 'shellwords'
class Rexe
VERSION = '1.5.1'
PROJECT_URL = 'https://github.com/keithrbennett/rexe'
module Helpers
# Try executing code. If error raised, print message (but not stack trace) & exit -1.
def try
begin
yield
rescue Exception => e
unless e.class == SystemExit
$stderr.puts("rexe: #{e}")
$stderr.puts("Use the -h option to get help.")
exit(-1)
end
end
end
end
class Options < Struct.new(
:input_filespec,
:input_format,
:input_mode,
:loads,
:output_format,
:output_format_tty,
:output_format_block,
:requires,
:log_format,
:noop)
def initialize
super
clear
end
def clear
self.input_filespec = nil
self.input_format = :none
self.input_mode = :none
self.output_format = :none
self.output_format_tty = :none
self.output_format_block = :none
self.loads = []
self.requires = []
self.log_format = :none
self.noop = false
end
end
class Lookups
def input_modes
@input_modes ||= {
'l' => :line,
'e' => :enumerator,
'b' => :one_big_string,
'n' => :none
}
end
def input_formats
@input_formats ||= {
'j' => :json,
'm' => :marshal,
'n' => :none,
'y' => :yaml,
}
end
def input_parsers
@input_parsers ||= {
json: ->(string) { JSON.parse(string) },
marshal: ->(string) { Marshal.load(string) },
none: ->(string) { string },
yaml: ->(string) { YAML.load(string) },
}
end
def output_formats
@output_formats ||= {
'a' => :amazing_print,
'i' => :inspect,
'j' => :json,
'J' => :pretty_json,
'm' => :marshal,
'n' => :none,
'p' => :puts, # default
'P' => :pretty_print,
's' => :to_s,
'y' => :yaml,
}
end
def formatters
@formatters ||= {
amazing_print: ->(obj) { obj.ai + "\n" },
inspect: ->(obj) { obj.inspect + "\n" },
json: ->(obj) { obj.to_json },
marshal: ->(obj) { Marshal.dump(obj) },
none: ->(_obj) { nil },
pretty_json: ->(obj) { JSON.pretty_generate(obj) },
pretty_print: ->(obj) { obj.pretty_inspect },
puts: ->(obj) { require 'stringio'; sio = StringIO.new; sio.puts(obj); sio.string },
to_s: ->(obj) { obj.to_s + "\n" },
yaml: ->(obj) { obj.to_yaml },
}
end
def format_requires
@format_requires ||= {
json: 'json',
pretty_json: 'json',
amazing_print: 'amazing_print',
pretty_print: 'pp',
yaml: 'yaml'
}
end
end
class CommandLineParser
include Helpers
attr_reader :lookups, :options
def initialize
@lookups = Lookups.new
@options = Options.new
end
# Inserts contents of REXE_OPTIONS environment variable at the beginning of ARGV.
private def prepend_environment_options
env_opt_string = ENV['REXE_OPTIONS']
if env_opt_string
args_to_prepend = Shellwords.shellsplit(env_opt_string)
ARGV.unshift(args_to_prepend).flatten!
end
end
private def add_format_requires_to_requires_list
formats = [options.input_format, options.output_format, options.log_format]
requires = formats.map { |format| lookups.format_requires[format] }.uniq.compact
requires.each { |r| options.requires << r }
end
private def help_text
unless @help_text
@help_text ||= <<~HEREDOC
rexe -- Ruby Command Line Executor/Filter -- v#{VERSION} -- #{PROJECT_URL}
Executes Ruby code on the command line,
optionally automating management of standard input and standard output,
and optionally parsing input and formatting output with YAML, JSON, etc.
rexe [options] [Ruby source code]
Options:
-c --clear_options Clear all previous command line options specified up to now
-f --input_file Use this file instead of stdin for preprocessed input;
if filespec has a YAML and JSON file extension,
sets input format accordingly and sets input mode to -mb
-g --log_format FORMAT Log format, logs to stderr, defaults to -gn (none)
(see -o for format options)
-h, --help Print help and exit
-i, --input_format FORMAT Input format, defaults to -in (None)
-ij JSON
-im Marshal
-in None (default)
-iy YAML
-l, --load RUBY_FILE(S) Ruby file(s) to load, comma separated;
! to clear all, or precede a name with '-' to remove
-m, --input_mode MODE Input preprocessing mode (determines what `self` will be)
defaults to -mn (none)
-ml line; each line is ingested as a separate string
-me enumerator (each_line on STDIN or File)
-mb big string; all lines combined into one string
-mn none (default); no input preprocessing;
self is an Object.new
-n, --[no-]noop Do not execute the code (useful with -g);
For true: yes, true, y, +; for false: no, false, n
-o, --output_format FORMAT Output format, defaults to -on (no output):
-oa Amazing Print
-oi Inspect
-oj JSON
-oJ Pretty JSON
-om Marshal
-on No Output (default)
-op Puts
-oP Pretty Print
-os to_s
-oy YAML
If 2 letters are provided, 1st is for tty devices, 2nd for block
--project-url Outputs project URL on Github, then exits
-r, --require REQUIRE(S) Gems and built-in libraries to require, comma separated;
! to clear all, or precede a name with '-' to remove
-v, --version Prints version and exits
---------------------------------------------------------------------------------------
In many cases you will need to enclose your source code in single or double quotes.
If source code is not specified, it will default to 'self',
which is most likely useful only in a filter mode (-ml, -me, -mb).
If there is a .rexerc file in your home directory, it will be run as Ruby code
before processing the input.
If there is a REXE_OPTIONS environment variable, its content will be prepended
to the command line so that you can specify options implicitly
(e.g. `export REXE_OPTIONS="-r amazing_print,yaml"`)
HEREDOC
@help_text.freeze
end
@help_text
end
# File file input mode; detects the input mode (JSON, YAML, or None) from the extension.
private def autodetect_file_format(filespec)
extension = File.extname(filespec).downcase
if extension == '.json'
:json
elsif extension == '.yml' || extension == '.yaml'
:yaml
else
:none
end
end
private def open_resource(resource_identifier)
command = case (`uname`.chomp)
when 'Darwin'
'open'
when 'Linux'
'xdg-open'
else
'start'
end
`#{command} #{resource_identifier}`
end
# Using 'optparse', parses the command line.
# Settings go into this instance's properties (see Struct declaration).
def parse
prepend_environment_options
OptionParser.new do |parser|
parser.on('-c', '--clear_options', "Clear all previous command line options") do |v|
options.clear
end
parser.on('-f', '--input_file FILESPEC',
'Use this file instead of stdin; autodetects YAML and JSON file extensions') do |v|
unless File.exist?(v)
raise "File #{v} does not exist."
end
options.input_filespec = v
options.input_format = autodetect_file_format(v)
if [:json, :yaml].include?(options.input_format)
options.input_mode = :one_big_string
end
end
parser.on('-g', '--log_format FORMAT', 'Log format, logs to stderr, defaults to none (see -o for format options)') do |v|
options.log_format = lookups.output_formats[v]
if options.log_format.nil?
raise("Output mode was '#{v}' but must be one of #{lookups.output_formats.keys}.")
end
end
parser.on("-h", "--help", "Show help") do |_help_requested|
puts help_text
exit
end
parser.on('-i', '--input_format FORMAT',
'Mode with which to parse input values (n = none (default), j = JSON, m = Marshal, y = YAML') do |v|
options.input_format = lookups.input_formats[v]
if options.input_format.nil?
raise("Input mode was '#{v}' but must be one of #{lookups.input_formats.keys}.")
end
end
parser.on('-l', '--load RUBY_FILE(S)', 'Ruby file(s) to load, comma separated, or ! to clear') do |v|
if v == '!'
options.loads.clear
else
loadfiles = v.split(',').map(&:strip).map { |s| File.expand_path(s) }
removes, adds = loadfiles.partition { |filespec| filespec[0] == '-' }
existent, nonexistent = adds.partition { |filespec| File.exists?(filespec) }
if nonexistent.any?
raise("\nDid not find the following files to load: #{nonexistent}\n\n")
else
existent.each { |filespec| options.loads << filespec }
end
removes.each { |filespec| options.loads -= [filespec[1..-1]] }
end
end
parser.on('-m', '--input_mode MODE',
'Mode with which to handle input (-ml, -me, -mb, -mn (default)') do |v|
options.input_mode = lookups.input_modes[v]
if options.input_mode.nil?
raise("Input mode was '#{v}' but must be one of #{lookups.input_modes.keys}.")
end
end
# See https://stackoverflow.com/questions/54576873/ruby-optionparser-short-code-for-boolean-option
# for an excellent explanation of this optparse incantation.
# According to the answer, valid options are:
# -n no, -n yes, -n false, -n true, -n n, -n y, -n +, but not -n -.
parser.on('-n', '--[no-]noop [FLAG]', TrueClass, "Do not execute the code (useful with -g)") do |v|
options.noop = (v.nil? ? true : v)
end
parser.on('-o', '--output_format FORMAT',
'Mode with which to format values for output (`-o` + [aijJmnpsy])') do |v|
options.output_format_tty = lookups.output_formats[v[0]]
options.output_format_block = lookups.output_formats[v[-1]]
options.output_format = ($stdout.tty? ? options.output_format_tty : options.output_format_block)
if [options.output_format_tty, options.output_format_block].include?(nil)
raise("Bad output mode '#{v}'; each must be one of #{lookups.output_formats.keys}.")
end
end
parser.on('-r', '--require REQUIRE(S)',
'Gems and built-in libraries (e.g. shellwords, yaml) to require, comma separated, or ! to clear') do |v|
if v == '!'
options.requires.clear
else
v.split(',').map(&:strip).each do |r|
if r[0] == '-'
options.requires -= [r[1..-1]]
else
options.requires << r
end
end
end
end
parser.on('-v', '--version', 'Print version') do
puts VERSION
exit(0)
end
# Undocumented feature: open Github project with default web browser on a Mac
parser.on('', '--open-project') do
open_resource(PROJECT_URL)
exit(0)
end
parser.on('', '--project-url') do
puts PROJECT_URL
exit(0)
end
end.parse!
# We want to do this after all options have been processed because we don't want any clearing of the
# options (by '-c', etc.) to result in exclusion of these needed requires.
add_format_requires_to_requires_list
options.requires = options.requires.sort.uniq
options.loads.uniq!
options
end
end
class Main
include Helpers
attr_reader :callable, :input_parser, :lookups,
:options, :output_formatter,
:log_formatter, :start_time, :user_source_code
def initialize
@lookups = Lookups.new
@start_time = DateTime.now
end
private def load_global_config_if_exists
filespec = File.join(Dir.home, '.rexerc')
load(filespec) if File.exists?(filespec)
end
private def init_parser_and_formatters
@input_parser = lookups.input_parsers[options.input_format]
@output_formatter = lookups.formatters[options.output_format]
@log_formatter = lookups.formatters[options.log_format]
end
# Executes the user specified code in the manner appropriate to the input mode.
# Performs any optionally specified parsing on input and formatting on output.
private def execute(eval_context_object, code)
if options.input_format != :none && options.input_mode != :none
eval_context_object = input_parser.(eval_context_object)
end
value = eval_context_object.instance_eval(&code)
unless options.output_format == :none
print output_formatter.(value)
end
rescue Errno::EPIPE
exit(-13)
end
# The global $RC (Rexe Context) OpenStruct is available in your user code.
# In order to make it possible to access this object in your loaded files, we are not creating
# it here; instead we add properties to it. This way, you can initialize an OpenStruct yourself
# in your loaded code and it will still work. If you do that, beware, any properties you add will be
# included in the log output. If the to_s of your added objects is large, that might be a pain.
private def init_rexe_context
$RC ||= OpenStruct.new
$RC.count = 0
$RC.rexe_version = VERSION
$RC.start_time = start_time.iso8601
$RC.source_code = user_source_code
$RC.options = options.to_h
def $RC.i; count end # `i` aliases `count` so you can more concisely get the count in your user code
end
private def create_callable
eval("Proc.new { #{user_source_code} }")
end
private def lookup_action(mode)
input = options.input_filespec ? File.open(options.input_filespec) : STDIN
{
line: -> { input.each { |l| execute(l.chomp, callable); $RC.count += 1 } },
enumerator: -> { execute(input.each_line, callable); $RC.count += 1 },
one_big_string: -> { big_string = input.read; execute(big_string, callable); $RC.count += 1 },
none: -> { execute(Object.new, callable) }
}.fetch(mode)
end
private def output_log_entry
if options.log_format != :none
$RC.duration_secs = Time.now - start_time.to_time
STDERR.puts(log_formatter.($RC.to_h))
end
end
# Bypasses Bundler's restriction on loading gems
# (see https://stackoverflow.com/questions/55144094/bundler-doesnt-permit-using-gems-in-project-home-directory)
private def require!(the_require)
begin
require the_require
rescue LoadError => error
gem_path = `gem which #{the_require}`
if gem_path.chomp.strip.empty?
raise error # re-raise the error, can't fix it
else
load_dir = File.dirname(gem_path)
$LOAD_PATH += load_dir
require the_require
end
end
end
# This class' entry point.
def call
try do
@options = CommandLineParser.new.parse
options.requires.each { |r| require!(r) }
load_global_config_if_exists
options.loads.each { |file| load(file) }
@user_source_code = ARGV.join(' ')
@user_source_code = 'self' if @user_source_code == ''
@callable = create_callable
init_rexe_context
init_parser_and_formatters
# This is where the user's source code will be executed; the action will in turn call `execute`.
lookup_action(options.input_mode).call unless options.noop
output_log_entry
end
end
end
end
def bundler_run(&block)
# This used to be an unconditional call to with_clean_env but that method is now deprecated:
# [DEPRECATED] `Bundler.with_clean_env` has been deprecated in favor of `Bundler.with_unbundled_env`.
# If you instead want the environment before bundler was originally loaded,
# use `Bundler.with_original_env`
if Bundler.respond_to?(:with_unbundled_env)
Bundler.with_unbundled_env { block.call }
else
Bundler.with_clean_env { block.call }
end
end
bundler_run { Rexe::Main.new.call }

View file

@ -0,0 +1,121 @@
Rails.application.routes.draw do
constraints -> { Rails.application.config.non_production } do
namespace :foo do
resource :bar
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
constraints -> { Rails.application.config.non_production } do
namespace :bar do
resource :baz
end
end
namespace :admin do
resource :session
match "/foobar(*path)", via: :all, to: redirect { |_params, req|
uri = URI(req.path.gsub("foobar", "foobaz"))
uri.query = req.query_string.presence
uri.to_s
}
end

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,64 @@
module SyntaxErrorSearch
# Used for formatting invalid blocks
class DisplayInvalidBlocks
attr_reader :filename
def initialize(block_array, io: $stderr, filename: nil)
@filename = filename
@io = io
@blocks = block_array
@lines = @blocks.map(&:lines).flatten
@digit_count = @lines.last.line_number.to_s.length
@code_lines = @blocks.first.code_lines
@invalid_line_hash = @lines.each_with_object({}) {|line, h| h[line] = true}
end
def call
@io.puts <<~EOM
SyntaxSuggest: 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:
EOM
@io.puts(<<~EOM) if filename
file: #{filename}
EOM
@io.puts <<~EOM
#{code_with_filename}
EOM
end
def filename
def code_with_filename
string = String.new("")
string << "```\n"
string << "#".rjust(@digit_count) + " filename: #{filename}\n\n" if filename
string << code_with_lines
string << "```\n"
string
end
def code_with_lines
@code_lines.map do |line|
next if line.hidden?
number = line.line_number.to_s.rjust(@digit_count)
if line.empty?
"#{number.to_s}#{line}"
else
string = String.new
string << "\e[1;3m" if @invalid_line_hash[line] # Bold, italics
string << "#{number.to_s} "
string << line.to_s
string << "\e[0m"
string
end
end.join
end
end
end

View file

@ -0,0 +1,35 @@
describe "webmock tests" do
before(:each) do
WebMock.enable!
end
after(:each) do
WebMock.disable!
end
it "port" do
port = rand(1000...9999)
stub_request(:any, "localhost:#{port}")
query = Cutlass::FunctionQuery.new(
port: port
).call
expect(WebMock).to have_requested(:post, "localhost:#{port}").
with(body: "{}")
end
it "body" do
body = { lol: "hi" }
port = 8080
stub_request(:any, "localhost:#{port}")
query = Cutlass::FunctionQuery.new(
port: port
body: body
).call
expect(WebMock).to have_requested(:post, "localhost:#{port}").
with(body: body.to_json)
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe "exe" do
def exe_path
root_dir.join("exe").join("syntax_suggest")
end
def exe(cmd)
out = run!("#{exe_path} #{cmd}", raise_on_nonzero_exit: false)
puts out if ENV["SYNTAX_SUGGEST_DEBUG"]
out
end
it "prints the version" do
out = exe("-v")
expect(out.strip).to include(SyntaxSuggest::VERSION)
end
end
end

View file

@ -0,0 +1,151 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe "Requires with ruby cli" do
it "namespaces all monkeypatched methods" do
Dir.mktmpdir do |dir|
tmpdir = Pathname(dir)
script = tmpdir.join("script.rb")
script.write <<~'EOM'
puts Kernel.private_methods
EOM
syntax_suggest_methods_file = tmpdir.join("syntax_suggest_methods.txt")
api_only_methods_file = tmpdir.join("api_only_methods.txt")
kernel_methods_file = tmpdir.join("kernel_methods.txt")
d_pid = Process.spawn("ruby -I#{lib_dir} -rsyntax_suggest #{script} 2>&1 > #{syntax_suggest_methods_file}")
k_pid = Process.spawn("ruby #{script} 2>&1 >> #{kernel_methods_file}")
r_pid = Process.spawn("ruby -I#{lib_dir} -rsyntax_suggest/api #{script} 2>&1 > #{api_only_methods_file}")
Process.wait(k_pid)
Process.wait(d_pid)
Process.wait(r_pid)
kernel_methods_array = kernel_methods_file.read.strip.lines.map(&:strip)
syntax_suggest_methods_array = syntax_suggest_methods_file.read.strip.lines.map(&:strip)
api_only_methods_array = api_only_methods_file.read.strip.lines.map(&:strip)
# In ruby 3.1.0-preview1 the `timeout` file is already required
# we can remove it if it exists to normalize the output for
# all ruby versions
[syntax_suggest_methods_array, kernel_methods_array, api_only_methods_array].each do |array|
array.delete("timeout")
end
methods = (syntax_suggest_methods_array - kernel_methods_array).sort
if methods.any?
expect(methods).to eq(["syntax_suggest_original_load", "syntax_suggest_original_require", "syntax_suggest_original_require_relative"])
end
methods = (api_only_methods_array - kernel_methods_array).sort
expect(methods).to eq([])
end
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
load "#{script.expand_path}"
EOM
out = `ruby -I#{lib_dir} -rsyntax_suggest #{require_rb} 2>&1`
expect($?.success?).to be_falsey
expect(out).to include(' 5 it "flerg"').once
end
end
it "annotates a syntax error in Ruby 3.2+ when require is not used" do
pending("Support for SyntaxError#detailed_message monkeypatch needed https://gist.github.com/schneems/09f45cc23b9a8c46e9af6acbb6e6840d?permalink_comment_id=4172585#gistcomment-4172585")
skip if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2")
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
out = `ruby -I#{lib_dir} -rsyntax_suggest #{script} 2>&1`
expect($?.success?).to be_falsey
expect(out).to include(' 5 it "flerg"').once
end
end
it "does not load internals into memory if no syntax error" do
Dir.mktmpdir do |dir|
tmpdir = Pathname(dir)
script = tmpdir.join("script.rb")
script.write <<~EOM
class Dog
end
if defined?(SyntaxSuggest::DEFAULT_VALUE)
puts "SyntaxSuggest is loaded"
else
puts "SyntaxSuggest is NOT loaded"
end
EOM
require_rb = tmpdir.join("require.rb")
require_rb.write <<~EOM
load "#{script.expand_path}"
EOM
out = `ruby -I#{lib_dir} -rsyntax_suggest #{require_rb} 2>&1`
expect($?.success?).to be_truthy
expect(out).to include("SyntaxSuggest is NOT loaded").once
end
end
it "ignores eval" do
Dir.mktmpdir do |dir|
tmpdir = Pathname(dir)
script = tmpdir.join("script.rb")
script.write <<~'EOM'
$stderr = STDOUT
eval("def lol")
EOM
out = `ruby -I#{lib_dir} -rsyntax_suggest #{script} 2>&1`
expect($?.success?).to be_falsey
expect(out).to include("(eval):1")
expect(out).to_not include("SyntaxSuggest")
expect(out).to_not include("Could not find filename")
end
end
end
end

View file

@ -0,0 +1,211 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe "Integration tests that don't spawn a process (like using the cli)" do
it "does not timeout on massive files" do
next unless ENV["SYNTAX_SUGGEST_TIMEOUT"]
file = fixtures_dir.join("syntax_tree.rb.txt")
lines = file.read.lines
lines.delete_at(768 - 1)
io = StringIO.new
benchmark = Benchmark.measure do
debug_perf do
SyntaxSuggest.call(
io: io,
source: lines.join,
filename: file
)
end
debug_display(io.string)
debug_display(benchmark)
end
expect(io.string).to include(<<~'EOM')
6 class SyntaxTree < Ripper
170 def self.parse(source)
174 end
754 def on_args_add(arguments, argument)
776 class ArgsAddBlock
810 end
9233 end
EOM
end
it "re-checks all block code, not just what's visible issues/95" do
file = fixtures_dir.join("ruby_buildpack.rb.txt")
io = StringIO.new
debug_perf do
benchmark = Benchmark.measure do
SyntaxSuggest.call(
io: io,
source: file.read,
filename: file
)
end
debug_display(io.string)
debug_display(benchmark)
end
expect(io.string).to_not include("def ruby_install_binstub_path")
expect(io.string).to include(<<~'EOM')
1067 def add_yarn_binary
1068 return [] if yarn_preinstalled?
1069 |
1075 end
EOM
end
it "returns good results on routes.rb" do
source = fixtures_dir.join("routes.rb.txt").read
io = StringIO.new
SyntaxSuggest.call(
io: io,
source: source
)
debug_display(io.string)
expect(io.string).to include(<<~'EOM')
1 Rails.application.routes.draw do
113 namespace :admin do
116 match "/foobar(*path)", via: :all, to: redirect { |_params, req|
120 }
121 end
EOM
end
it "handles multi-line-methods issues/64" do
source = fixtures_dir.join("webmock.rb.txt").read
io = StringIO.new
SyntaxSuggest.call(
io: io,
source: source
)
debug_display(io.string)
expect(io.string).to include(<<~'EOM')
1 describe "webmock tests" do
22 it "body" do
27 query = Cutlass::FunctionQuery.new(
28 port: port
29 body: body
30 ).call
34 end
35 end
EOM
end
it "handles derailed output issues/50" do
source = fixtures_dir.join("derailed_require_tree.rb.txt").read
io = StringIO.new
SyntaxSuggest.call(
io: io,
source: source
)
debug_display(io.string)
expect(io.string).to include(<<~'EOM')
5 module DerailedBenchmarks
6 class RequireTree
7 REQUIRED_BY = {}
9 attr_reader :name
10 attr_writer :cost
13 def initialize(name)
18 def self.reset!
25 end
73 end
74 end
EOM
end
it "handles heredocs" do
lines = fixtures_dir.join("rexe.rb.txt").read.lines
lines.delete_at(85 - 1)
io = StringIO.new
SyntaxSuggest.call(
io: io,
source: lines.join
)
out = io.string
debug_display(out)
expect(out).to include(<<~EOM)
16 class Rexe
77 class Lookups
78 def input_modes
148 end
551 end
EOM
end
it "rexe" do
lines = fixtures_dir.join("rexe.rb.txt").read.lines
lines.delete_at(148 - 1)
source = lines.join
io = StringIO.new
SyntaxSuggest.call(
io: io,
source: source
)
out = io.string
expect(out).to include(<<~EOM)
16 class Rexe
18 VERSION = '1.5.1'
77 class Lookups
140 def format_requires
148 end
551 end
EOM
end
it "ambiguous end" do
source = <<~'EOM'
def call # 0
print "lol" # 1
end # one # 2
end # two # 3
EOM
io = StringIO.new
SyntaxSuggest.call(
io: io,
source: source
)
out = io.string
expect(out).to include(<<~EOM)
1 def call # 0
3 end # one # 2
4 end # two # 3
EOM
end
it "simple regression" do
source = <<~'EOM'
class Dog
def bark
puts "woof"
end
EOM
io = StringIO.new
SyntaxSuggest.call(
io: io,
source: source
)
out = io.string
expect(out).to include(<<~EOM)
1 class Dog
2 def bark
4 end
EOM
end
end
end

View file

@ -0,0 +1,90 @@
# frozen_string_literal: true
require "bundler/setup"
require "syntax_suggest/api"
require "benchmark"
require "tempfile"
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
# Used for debugging modifications to
# display output
def debug_display(output)
return unless ENV["DEBUG_DISPLAY"]
puts
puts output
puts
end
def spec_dir
Pathname(__dir__)
end
def lib_dir
root_dir.join("lib")
end
def root_dir
spec_dir.join("..")
end
def fixtures_dir
spec_dir.join("fixtures")
end
def code_line_array(source)
SyntaxSuggest::CleanDocument.new(source: source).call.lines
end
autoload :RubyProf, "ruby-prof"
def debug_perf
raise "No block given" unless block_given?
if ENV["DEBUG_PERF"]
out = nil
result = RubyProf.profile do
out = yield
end
dir = SyntaxSuggest.record_dir("tmp")
printer = RubyProf::MultiPrinter.new(result, [:flat, :graph, :graph_html, :tree, :call_tree, :stack, :dot])
printer.print(path: dir, profile: "profile")
out
else
yield
end
end
def run!(cmd, raise_on_nonzero_exit: true)
out = `#{cmd} 2>&1`
raise "Command: #{cmd} failed: #{out}" if !$?.success? && raise_on_nonzero_exit
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.
class String
def indent(number)
lines.map do |line|
if line.chomp.empty?
line
else
" " * number + line
end
end.join
end
end

View file

@ -0,0 +1,83 @@
# frozen_string_literal: true
require_relative "../spec_helper"
require "ruby-prof"
module SyntaxSuggest
RSpec.describe "Top level SyntaxSuggest api" do
it "has a `handle_error` interface" do
fake_error = Object.new
def fake_error.message
"#{__FILE__}:216: unterminated string meets end of file "
end
def fake_error.is_a?(v)
true
end
io = StringIO.new
SyntaxSuggest.handle_error(
fake_error,
re_raise: false,
io: io
)
expect(io.string.strip).to eq("Syntax OK")
end
it "raises original error with warning if a non-syntax error is passed" do
error = NameError.new("blerg")
io = StringIO.new
expect {
SyntaxSuggest.handle_error(
error,
re_raise: false,
io: io
)
}.to raise_error { |e|
expect(io.string).to include("Must pass a SyntaxError")
expect(e).to eq(error)
}
end
it "raises original error with warning if file is not found" do
fake_error = SyntaxError.new
def fake_error.message
"#does/not/exist/lol/doesnotexist:216: unterminated string meets end of file "
end
io = StringIO.new
expect {
SyntaxSuggest.handle_error(
fake_error,
re_raise: false,
io: io
)
}.to raise_error { |e|
expect(io.string).to include("Could not find filename")
expect(e).to eq(fake_error)
}
end
it "respects highlight API" do
skip if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2")
error = SyntaxError.new("#{fixtures_dir.join("this_project_extra_def.rb.txt")}:1 ")
require "syntax_suggest/core_ext"
expect(error.detailed_message(highlight: true)).to include(SyntaxSuggest::DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT)
expect(error.detailed_message(highlight: false)).to_not include(SyntaxSuggest::DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT)
end
it "can be disabled via falsey kwarg" do
skip if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2")
error = SyntaxError.new("#{fixtures_dir.join("this_project_extra_def.rb.txt")}:1 ")
require "syntax_suggest/core_ext"
expect(error.detailed_message(syntax_suggest: true)).to_not eq(error.detailed_message(syntax_suggest: false))
end
end
end

View file

@ -0,0 +1,165 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe AroundBlockScan do
it "continues scan from last location even if scan is false" do
source = <<~'EOM'
print 'omg'
print 'lol'
print 'haha'
EOM
code_lines = CodeLine.from_source(source)
block = CodeBlock.new(lines: code_lines[1])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
.scan_neighbors
expect(expand.code_block.to_s).to eq(source)
expand.scan_while { |line| false }
expect(expand.code_block.to_s).to eq(source)
end
it "scan_adjacent_indent works on first or last line" do
source_string = <<~EOM
def foo
if [options.output_format_tty, options.output_format_block].include?(nil)
raise("Bad output mode '\#{v}'; each must be one of \#{lookups.output_formats.keys}.")
end
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(lines: code_lines[4])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
.scan_adjacent_indent
expect(expand.code_block.to_s).to eq(<<~EOM)
def foo
if [options.output_format_tty, options.output_format_block].include?(nil)
raise("Bad output mode '\#{v}'; each must be one of \#{lookups.output_formats.keys}.")
end
end
EOM
end
it "expands indentation" do
source_string = <<~EOM
def foo
if [options.output_format_tty, options.output_format_block].include?(nil)
raise("Bad output mode '\#{v}'; each must be one of \#{lookups.output_formats.keys}.")
end
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(lines: code_lines[2])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
.stop_after_kw
.scan_adjacent_indent
expect(expand.code_block.to_s).to eq(<<~EOM.indent(2))
if [options.output_format_tty, options.output_format_block].include?(nil)
raise("Bad output mode '\#{v}'; each must be one of \#{lookups.output_formats.keys}.")
end
EOM
end
it "can stop before hitting another end" do
source_string = <<~EOM
def lol
end
def foo
puts "lol"
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(lines: code_lines[3])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
expand.stop_after_kw
expand.scan_while { true }
expect(expand.code_block.to_s).to eq(<<~EOM)
def foo
puts "lol"
end
EOM
end
it "captures multiple empty and hidden lines" do
source_string = <<~EOM
def foo
Foo.call
puts "lol"
end
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(lines: code_lines[3])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
expand.scan_while { true }
expect(expand.before_index).to eq(0)
expect(expand.after_index).to eq(6)
expect(expand.code_block.to_s).to eq(source_string)
end
it "only takes what you ask" do
source_string = <<~EOM
def foo
Foo.call
puts "lol"
end
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(lines: code_lines[3])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
expand.scan_while { |line| line.not_empty? }
expect(expand.code_block.to_s).to eq(<<~EOM.indent(4))
puts "lol"
EOM
end
it "skips what you want" do
source_string = <<~EOM
def foo
Foo.call
puts "haha"
# hide me
puts "lol"
end
end
EOM
code_lines = code_line_array(source_string)
code_lines[4].mark_invisible
block = CodeBlock.new(lines: code_lines[3])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
expand.skip(:empty?)
expand.skip(:hidden?)
expand.scan_neighbors
expect(expand.code_block.to_s).to eq(<<~EOM.indent(4))
puts "haha"
puts "lol"
EOM
end
end
end

View file

@ -0,0 +1,200 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe BlockExpand do
it "captures multiple empty and hidden lines" do
source_string = <<~EOM
def foo
Foo.call
puts "lol"
# hidden
end
end
EOM
code_lines = code_line_array(source_string)
code_lines[6].mark_invisible
block = CodeBlock.new(lines: [code_lines[3]])
expansion = BlockExpand.new(code_lines: code_lines)
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM.indent(4))
puts "lol"
EOM
end
it "captures multiple empty lines" do
source_string = <<~EOM
def foo
Foo.call
puts "lol"
end
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(lines: [code_lines[3]])
expansion = BlockExpand.new(code_lines: code_lines)
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM.indent(4))
puts "lol"
EOM
end
it "expands neighbors then indentation" do
source_string = <<~EOM
def foo
Foo.call
puts "hey"
puts "lol"
puts "sup"
end
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(lines: [code_lines[3]])
expansion = BlockExpand.new(code_lines: code_lines)
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM.indent(4))
puts "hey"
puts "lol"
puts "sup"
EOM
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM.indent(2))
Foo.call
puts "hey"
puts "lol"
puts "sup"
end
EOM
end
it "handles else code" do
source_string = <<~EOM
Foo.call
if blerg
puts "lol"
else
puts "haha"
end
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(lines: [code_lines[2]])
expansion = BlockExpand.new(code_lines: code_lines)
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM.indent(2))
if blerg
puts "lol"
else
puts "haha"
end
EOM
end
it "expand until next boundry (indentation)" do
source_string = <<~EOM
describe "what" do
Foo.call
end
describe "hi"
Bar.call do
Foo.call
end
end
it "blerg" do
end
EOM
code_lines = code_line_array(source_string)
block = CodeBlock.new(
lines: code_lines[6]
)
expansion = BlockExpand.new(code_lines: code_lines)
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM.indent(2))
Bar.call do
Foo.call
end
EOM
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM)
describe "hi"
Bar.call do
Foo.call
end
end
EOM
end
it "expand until next boundry (empty lines)" do
source_string = <<~EOM
describe "what" do
end
describe "hi"
end
it "blerg" do
end
EOM
code_lines = code_line_array(source_string)
expansion = BlockExpand.new(code_lines: code_lines)
block = CodeBlock.new(lines: code_lines[3])
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM)
describe "hi"
end
EOM
block = expansion.call(block)
expect(block.to_s).to eq(<<~EOM)
describe "what" do
end
describe "hi"
end
it "blerg" do
end
EOM
end
end
end

View file

@ -0,0 +1,202 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe CaptureCodeContext do
it "capture_before_after_kws" do
source = <<~'EOM'
def sit
end
def bark
def eat
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
block = CodeBlock.new(lines: code_lines[0])
display = CaptureCodeContext.new(
blocks: [block],
code_lines: code_lines
)
lines = display.call
expect(lines.join).to eq(<<~'EOM')
def sit
end
def bark
def eat
end
EOM
end
it "handles ambiguous end" do
source = <<~'EOM'
def call # 0
print "lol" # 1
end # one # 2
end # two # 3
EOM
code_lines = CleanDocument.new(source: source).call.lines
code_lines[0..2].each(&:mark_invisible)
block = CodeBlock.new(lines: code_lines)
display = CaptureCodeContext.new(
blocks: [block],
code_lines: code_lines
)
lines = display.call
lines = lines.sort.map(&:original)
expect(lines.join).to eq(<<~'EOM')
def call # 0
end # one # 2
end # two # 3
EOM
end
it "shows ends of captured block" do
lines = fixtures_dir.join("rexe.rb.txt").read.lines
lines.delete_at(148 - 1)
source = lines.join
code_lines = CleanDocument.new(source: source).call.lines
code_lines[0..75].each(&:mark_invisible)
code_lines[77..-1].each(&:mark_invisible)
expect(code_lines.join.strip).to eq("class Lookups")
block = CodeBlock.new(lines: code_lines[76..149])
display = CaptureCodeContext.new(
blocks: [block],
code_lines: code_lines
)
lines = display.call
lines = lines.sort.map(&:original)
expect(lines.join).to include(<<~'EOM'.indent(2))
class Lookups
def format_requires
end
EOM
end
it "shows ends of captured block" do
source = <<~'EOM'
class Dog
def bark
puts "woof"
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
block = CodeBlock.new(lines: code_lines)
code_lines[1..-1].each(&:mark_invisible)
expect(block.to_s.strip).to eq("class Dog")
display = CaptureCodeContext.new(
blocks: [block],
code_lines: code_lines
)
lines = display.call.sort.map(&:original)
expect(lines.join).to eq(<<~'EOM')
class Dog
def bark
end
EOM
end
it "captures surrounding context on falling indent" do
source = <<~'EOM'
class Blerg
end
class OH
def hello
it "foo" do
end
end
class Zerg
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
block = CodeBlock.new(lines: code_lines[6])
expect(block.to_s.strip).to eq('it "foo" do')
display = CaptureCodeContext.new(
blocks: [block],
code_lines: code_lines
)
lines = display.call.sort.map(&:original)
expect(lines.join).to eq(<<~'EOM')
class OH
def hello
it "foo" do
end
end
EOM
end
it "captures surrounding context on same indent" do
source = <<~'EOM'
class Blerg
end
class OH
def nope
end
def lol
end
end # here
def haha
end
def nope
end
end
class Zerg
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
block = CodeBlock.new(lines: code_lines[7..10])
expect(block.to_s).to eq(<<~'EOM'.indent(2))
def lol
end
end # here
EOM
code_context = CaptureCodeContext.new(
blocks: [block],
code_lines: code_lines
)
lines = code_context.call
out = DisplayCodeWithLineNumbers.new(
lines: lines
).call
expect(out).to eq(<<~'EOM'.indent(2))
3 class OH
8 def lol
9 end
11 end # here
18 end
EOM
end
end
end

View file

@ -0,0 +1,259 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe CleanDocument do
it "heredocs" do
source = fixtures_dir.join("this_project_extra_def.rb.txt").read
code_lines = CleanDocument.new(source: source).call.lines
expect(code_lines[18 - 1].to_s).to eq(<<-'EOL')
@io.puts <<~EOM
SyntaxSuggest: 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:
EOM
EOL
expect(code_lines[18].to_s).to eq("")
expect(code_lines[27 - 1].to_s).to eq(<<-'EOL')
@io.puts(<<~EOM) if filename
file: #{filename}
EOM
EOL
expect(code_lines[27].to_s).to eq("")
expect(code_lines[31 - 1].to_s).to eq(<<-'EOL')
@io.puts <<~EOM
#{code_with_filename}
EOM
EOL
expect(code_lines[31].to_s).to eq("")
end
it "joins: multi line methods" do
source = <<~EOM
User
.where(name: 'schneems')
.first
EOM
doc = CleanDocument.new(source: source).join_consecutive!
expect(doc.lines[0].to_s).to eq(source)
expect(doc.lines[1].to_s).to eq("")
expect(doc.lines[2].to_s).to eq("")
expect(doc.lines[3]).to eq(nil)
lines = doc.lines
expect(
DisplayCodeWithLineNumbers.new(
lines: lines
).call
).to eq(<<~'EOM'.indent(2))
1 User
2 .where(name: 'schneems')
3 .first
EOM
expect(
DisplayCodeWithLineNumbers.new(
lines: lines,
highlight_lines: lines[0]
).call
).to eq(<<~'EOM')
1 User
2 .where(name: 'schneems')
3 .first
EOM
end
it "helper method: take_while_including" do
source = <<~EOM
User
.where(name: 'schneems')
.first
EOM
doc = CleanDocument.new(source: source)
lines = doc.take_while_including { |line| !line.to_s.include?("where") }
expect(lines.count).to eq(2)
end
it "comments: removes comments" do
source = <<~EOM
# lol
puts "what"
# yolo
EOM
out = CleanDocument.new(source: source).lines.join
expect(out.to_s).to eq(<<~EOM)
puts "what"
EOM
end
it "whitespace: removes whitespace" do
source = " \n" + <<~EOM
puts "what"
EOM
out = CleanDocument.new(source: source).lines.join
expect(out.to_s).to eq(<<~EOM)
puts "what"
EOM
expect(source.lines.first.to_s).to_not eq("\n")
expect(out.lines.first.to_s).to eq("\n")
end
it "trailing slash: does not join trailing do" do
# Some keywords and syntaxes trigger the "ignored line"
# lex output, we ignore them by filtering by BEG
#
# The `do` keyword is one of these:
# https://gist.github.com/schneems/6a7d7f988d3329fb3bd4b5be3e2efc0c
source = <<~EOM
foo do
puts "lol"
end
EOM
doc = CleanDocument.new(source: source).join_consecutive!
expect(doc.lines[0].to_s).to eq(source.lines[0])
expect(doc.lines[1].to_s).to eq(source.lines[1])
expect(doc.lines[2].to_s).to eq(source.lines[2])
end
it "trailing slash: formats output" do
source = <<~'EOM'
context "timezones workaround" do
it "should receive a time in UTC format and return the time with the"\
"office's UTC offset substracted from it" do
travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do
office = build(:office)
end
end
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
expect(
DisplayCodeWithLineNumbers.new(
lines: code_lines.select(&:visible?)
).call
).to eq(<<~'EOM'.indent(2))
1 context "timezones workaround" do
2 it "should receive a time in UTC format and return the time with the"\
3 "office's UTC offset substracted from it" do
4 travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do
5 office = build(:office)
6 end
7 end
8 end
EOM
expect(
DisplayCodeWithLineNumbers.new(
lines: code_lines.select(&:visible?),
highlight_lines: code_lines[1]
).call
).to eq(<<~'EOM')
1 context "timezones workaround" do
2 it "should receive a time in UTC format and return the time with the"\
3 "office's UTC offset substracted from it" do
4 travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do
5 office = build(:office)
6 end
7 end
8 end
EOM
end
it "trailing slash: basic detection" do
source = <<~'EOM'
it "trailing s" \
"lash" do
EOM
code_lines = CleanDocument.new(source: source).call.lines
expect(code_lines[0]).to_not be_hidden
expect(code_lines[1]).to be_hidden
expect(
code_lines.join
).to eq(code_lines.map(&:original).join)
end
it "trailing slash: joins multiple lines" do
source = <<~'EOM'
it "should " \
"keep " \
"going " do
end
EOM
doc = CleanDocument.new(source: source).join_trailing_slash!
expect(doc.lines[0].to_s).to eq(source.lines[0..2].join)
expect(doc.lines[1].to_s).to eq("")
expect(doc.lines[2].to_s).to eq("")
expect(doc.lines[3].to_s).to eq(source.lines[3])
lines = doc.lines
expect(
DisplayCodeWithLineNumbers.new(
lines: lines
).call
).to eq(<<~'EOM'.indent(2))
1 it "should " \
2 "keep " \
3 "going " do
4 end
EOM
expect(
DisplayCodeWithLineNumbers.new(
lines: lines,
highlight_lines: lines[0]
).call
).to eq(<<~'EOM')
1 it "should " \
2 "keep " \
3 "going " do
4 end
EOM
end
it "trailing slash: no false positives" do
source = <<~'EOM'
def formatters
@formatters ||= {
amazing_print: ->(obj) { obj.ai + "\n" },
inspect: ->(obj) { obj.inspect + "\n" },
json: ->(obj) { obj.to_json },
marshal: ->(obj) { Marshal.dump(obj) },
none: ->(_obj) { nil },
pretty_json: ->(obj) { JSON.pretty_generate(obj) },
pretty_print: ->(obj) { obj.pretty_inspect },
puts: ->(obj) { require 'stringio'; sio = StringIO.new; sio.puts(obj); sio.string },
to_s: ->(obj) { obj.to_s + "\n" },
yaml: ->(obj) { obj.to_yaml },
}
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
expect(code_lines.join).to eq(code_lines.join)
end
end
end

View file

@ -0,0 +1,224 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
class FakeExit
def initialize
@called = false
@value = nil
end
def exit(value = nil)
@called = true
@value = value
end
def called?
@called
end
attr_reader :value
end
RSpec.describe Cli do
it "parses valid code" do
Dir.mktmpdir do |dir|
dir = Pathname(dir)
file = dir.join("script.rb")
file.write("puts 'lol'")
io = StringIO.new
exit_obj = FakeExit.new
Cli.new(
io: io,
argv: [file.to_s],
exit_obj: exit_obj
).call
expect(exit_obj.called?).to be_truthy
expect(exit_obj.value).to eq(0)
expect(io.string.strip).to eq("Syntax OK")
end
end
it "parses invalid code" do
file = fixtures_dir.join("this_project_extra_def.rb.txt")
io = StringIO.new
exit_obj = FakeExit.new
Cli.new(
io: io,
argv: [file.to_s],
exit_obj: exit_obj
).call
out = io.string
debug_display(out)
expect(exit_obj.called?).to be_truthy
expect(exit_obj.value).to eq(1)
expect(out.strip).to include(" 36 def filename")
end
it "parses valid code with flags" do
Dir.mktmpdir do |dir|
dir = Pathname(dir)
file = dir.join("script.rb")
file.write("puts 'lol'")
io = StringIO.new
exit_obj = FakeExit.new
cli = Cli.new(
io: io,
argv: ["--terminal", file.to_s],
exit_obj: exit_obj
)
cli.call
expect(exit_obj.called?).to be_truthy
expect(exit_obj.value).to eq(0)
expect(cli.options[:terminal]).to be_truthy
expect(io.string.strip).to eq("Syntax OK")
end
end
it "errors when no file given" do
io = StringIO.new
exit_obj = FakeExit.new
cli = Cli.new(
io: io,
argv: ["--terminal"],
exit_obj: exit_obj
)
cli.call
expect(exit_obj.called?).to be_truthy
expect(exit_obj.value).to eq(1)
expect(io.string.strip).to eq("No file given")
end
it "errors when file does not exist" do
io = StringIO.new
exit_obj = FakeExit.new
cli = Cli.new(
io: io,
argv: ["lol-i-d-o-not-ex-ist-yololo.txtblerglol"],
exit_obj: exit_obj
)
cli.call
expect(exit_obj.called?).to be_truthy
expect(exit_obj.value).to eq(1)
expect(io.string.strip).to include("file not found:")
end
# We cannot execute the parser here
# because it calls `exit` and it will exit
# our tests, however we can assert that the
# parser has the right value for version
it "-v version" do
io = StringIO.new
exit_obj = FakeExit.new
parser = Cli.new(
io: io,
argv: ["-v"],
exit_obj: exit_obj
).parser
expect(parser.version).to include(SyntaxSuggest::VERSION.to_s)
end
it "SYNTAX_SUGGEST_RECORD_DIR" do
io = StringIO.new
exit_obj = FakeExit.new
cli = Cli.new(
io: io,
argv: [],
env: {"SYNTAX_SUGGEST_RECORD_DIR" => "hahaha"},
exit_obj: exit_obj
).parse
expect(exit_obj.called?).to be_falsey
expect(cli.options[:record_dir]).to eq("hahaha")
end
it "--record-dir=<dir>" do
io = StringIO.new
exit_obj = FakeExit.new
cli = Cli.new(
io: io,
argv: ["--record=lol"],
exit_obj: exit_obj
).parse
expect(exit_obj.called?).to be_falsey
expect(cli.options[:record_dir]).to eq("lol")
end
it "terminal default to respecting TTY" do
io = StringIO.new
exit_obj = FakeExit.new
cli = Cli.new(
io: io,
argv: [],
exit_obj: exit_obj
).parse
expect(exit_obj.called?).to be_falsey
expect(cli.options[:terminal]).to eq(SyntaxSuggest::DEFAULT_VALUE)
end
it "--terminal" do
io = StringIO.new
exit_obj = FakeExit.new
cli = Cli.new(
io: io,
argv: ["--terminal"],
exit_obj: exit_obj
).parse
expect(exit_obj.called?).to be_falsey
expect(cli.options[:terminal]).to be_truthy
end
it "--no-terminal" do
io = StringIO.new
exit_obj = FakeExit.new
cli = Cli.new(
io: io,
argv: ["--no-terminal"],
exit_obj: exit_obj
).parse
expect(exit_obj.called?).to be_falsey
expect(cli.options[:terminal]).to be_falsey
end
it "--help outputs help" do
io = StringIO.new
exit_obj = FakeExit.new
Cli.new(
io: io,
argv: ["--help"],
exit_obj: exit_obj
).call
expect(exit_obj.called?).to be_truthy
expect(io.string).to include("Usage: syntax_suggest <file> [options]")
end
it "<empty args> outputs help" do
io = StringIO.new
exit_obj = FakeExit.new
Cli.new(
io: io,
argv: [],
exit_obj: exit_obj
).call
expect(exit_obj.called?).to be_truthy
expect(io.string).to include("Usage: syntax_suggest <file> [options]")
end
end
end

View file

@ -0,0 +1,77 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe CodeBlock do
it "can detect if it's valid or not" do
code_lines = code_line_array(<<~EOM)
def foo
puts 'lol'
end
EOM
block = CodeBlock.new(lines: code_lines[1])
expect(block.valid?).to be_truthy
end
it "can be sorted in indentation order" do
code_lines = code_line_array(<<~EOM)
def foo
puts 'lol'
end
EOM
block_0 = CodeBlock.new(lines: code_lines[0])
block_1 = CodeBlock.new(lines: code_lines[1])
block_2 = CodeBlock.new(lines: code_lines[2])
expect(block_0 <=> block_0.dup).to eq(0)
expect(block_1 <=> block_0).to eq(1)
expect(block_1 <=> block_2).to eq(-1)
array = [block_2, block_1, block_0].sort
expect(array.last).to eq(block_2)
block = CodeBlock.new(lines: CodeLine.new(line: " " * 8 + "foo", index: 4, lex: []))
array.prepend(block)
expect(array.max).to eq(block)
end
it "knows it's current indentation level" do
code_lines = code_line_array(<<~EOM)
def foo
puts 'lol'
end
EOM
block = CodeBlock.new(lines: code_lines[1])
expect(block.current_indent).to eq(2)
block = CodeBlock.new(lines: code_lines[0])
expect(block.current_indent).to eq(0)
end
it "knows it's current indentation level when mismatched indents" do
code_lines = code_line_array(<<~EOM)
def foo
puts 'lol'
end
EOM
block = CodeBlock.new(lines: [code_lines[1], code_lines[2]])
expect(block.current_indent).to eq(1)
end
it "before lines and after lines" do
code_lines = code_line_array(<<~EOM)
def foo
bar; end
end
EOM
block = CodeBlock.new(lines: code_lines[1])
expect(block.valid?).to be_falsey
end
end
end

View file

@ -0,0 +1,135 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe CodeFrontier do
it "detect_bad_blocks" do
code_lines = code_line_array(<<~EOM)
describe "lol" do
end
end
it "lol" do
end
end
EOM
frontier = CodeFrontier.new(code_lines: code_lines)
blocks = []
blocks << CodeBlock.new(lines: code_lines[1])
blocks << CodeBlock.new(lines: code_lines[5])
blocks.each do |b|
frontier << b
end
expect(frontier.detect_invalid_blocks.sort).to eq(blocks.sort)
end
it "self.combination" do
expect(
CodeFrontier.combination([:a, :b, :c, :d])
).to eq(
[
[:a], [:b], [:c], [:d],
[:a, :b],
[:a, :c],
[:a, :d],
[:b, :c],
[:b, :d],
[:c, :d],
[:a, :b, :c],
[:a, :b, :d],
[:a, :c, :d],
[:b, :c, :d],
[:a, :b, :c, :d]
]
)
end
it "doesn't duplicate blocks" do
code_lines = code_line_array(<<~EOM)
def foo
puts "lol"
puts "lol"
puts "lol"
end
EOM
frontier = CodeFrontier.new(code_lines: code_lines)
frontier << CodeBlock.new(lines: [code_lines[2]])
expect(frontier.count).to eq(1)
frontier << CodeBlock.new(lines: [code_lines[1], code_lines[2], code_lines[3]])
# expect(frontier.count).to eq(1)
expect(frontier.pop.to_s).to eq(<<~EOM.indent(2))
puts "lol"
puts "lol"
puts "lol"
EOM
expect(frontier.pop).to be_nil
code_lines = code_line_array(<<~EOM)
def foo
puts "lol"
puts "lol"
puts "lol"
end
EOM
frontier = CodeFrontier.new(code_lines: code_lines)
frontier << CodeBlock.new(lines: [code_lines[2]])
expect(frontier.count).to eq(1)
frontier << CodeBlock.new(lines: [code_lines[3]])
expect(frontier.count).to eq(2)
expect(frontier.pop.to_s).to eq(<<~EOM.indent(2))
puts "lol"
EOM
end
it "detects if multiple syntax errors are found" do
code_lines = code_line_array(<<~EOM)
def foo
end
end
EOM
frontier = CodeFrontier.new(code_lines: code_lines)
frontier << CodeBlock.new(lines: code_lines[1])
block = frontier.pop
expect(block.to_s).to eq(<<~EOM.indent(2))
end
EOM
frontier << block
expect(frontier.holds_all_syntax_errors?).to be_truthy
end
it "detects if it has not captured all syntax errors" do
code_lines = code_line_array(<<~EOM)
def foo
puts "lol"
end
describe "lol"
end
it "lol"
end
EOM
frontier = CodeFrontier.new(code_lines: code_lines)
frontier << CodeBlock.new(lines: [code_lines[1]])
block = frontier.pop
expect(block.to_s).to eq(<<~EOM.indent(2))
puts "lol"
EOM
frontier << block
expect(frontier.holds_all_syntax_errors?).to be_falsey
end
end
end

View file

@ -0,0 +1,164 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe CodeLine do
it "bug in keyword detection" do
lines = CodeLine.from_source(<<~'EOM')
def to_json(*opts)
{
type: :module,
}.to_json(*opts)
end
EOM
expect(lines.count(&:is_kw?)).to eq(1)
expect(lines.count(&:is_end?)).to eq(1)
end
it "supports endless method definitions" do
skip("Unsupported ruby version") unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3")
line = CodeLine.from_source(<<~'EOM').first
def square(x) = x * x
EOM
expect(line.is_kw?).to be_falsey
expect(line.is_end?).to be_falsey
end
it "retains original line value, after being marked invisible" do
line = CodeLine.from_source(<<~'EOM').first
puts "lol"
EOM
expect(line.line).to match('puts "lol"')
line.mark_invisible
expect(line.line).to eq("")
expect(line.original).to match('puts "lol"')
end
it "knows which lines can be joined" do
code_lines = CodeLine.from_source(<<~'EOM')
user = User.
where(name: 'schneems').
first
puts user.name
EOM
# Indicates line 1 can join 2, 2 can join 3, but 3 won't join it's next line
expect(code_lines.map(&:ignore_newline_not_beg?)).to eq([true, true, false, false])
end
it "trailing if" do
code_lines = CodeLine.from_source(<<~'EOM')
puts "lol" if foo
if foo
end
EOM
expect(code_lines.map(&:is_kw?)).to eq([false, true, false])
end
it "trailing unless" do
code_lines = CodeLine.from_source(<<~'EOM')
puts "lol" unless foo
unless foo
end
EOM
expect(code_lines.map(&:is_kw?)).to eq([false, true, false])
end
it "trailing slash" do
code_lines = CodeLine.from_source(<<~'EOM')
it "trailing s" \
"lash" do
EOM
expect(code_lines.map(&:trailing_slash?)).to eq([true, false])
code_lines = CodeLine.from_source(<<~'EOM')
amazing_print: ->(obj) { obj.ai + "\n" },
EOM
expect(code_lines.map(&:trailing_slash?)).to eq([false])
end
it "knows it's got an end" do
line = CodeLine.from_source(" end").first
expect(line.is_end?).to be_truthy
expect(line.is_kw?).to be_falsey
end
it "knows it's got a keyword" do
line = CodeLine.from_source(" if").first
expect(line.is_end?).to be_falsey
expect(line.is_kw?).to be_truthy
end
it "ignores marked lines" do
code_lines = CodeLine.from_source(<<~EOM)
def foo
Array(value) |x|
end
end
EOM
expect(SyntaxSuggest.valid?(code_lines)).to be_falsey
expect(code_lines.join).to eq(<<~EOM)
def foo
Array(value) |x|
end
end
EOM
expect(code_lines[0].visible?).to be_truthy
expect(code_lines[3].visible?).to be_truthy
code_lines[0].mark_invisible
code_lines[3].mark_invisible
expect(code_lines[0].visible?).to be_falsey
expect(code_lines[3].visible?).to be_falsey
expect(code_lines.join).to eq(<<~EOM.indent(2))
Array(value) |x|
end
EOM
expect(SyntaxSuggest.valid?(code_lines)).to be_falsey
end
it "knows empty lines" do
code_lines = CodeLine.from_source(<<~EOM)
# Not empty
# Not empty
EOM
expect(code_lines.map(&:empty?)).to eq([false, true, false])
expect(code_lines.map(&:not_empty?)).to eq([true, false, true])
expect(code_lines.map { |l| SyntaxSuggest.valid?(l) }).to eq([true, true, true])
end
it "counts indentations" do
code_lines = CodeLine.from_source(<<~EOM)
def foo
Array(value) |x|
puts 'lol'
end
end
EOM
expect(code_lines.map(&:indent)).to eq([0, 2, 4, 2, 0])
end
it "doesn't count empty lines as having an indentation" do
code_lines = CodeLine.from_source(<<~EOM)
EOM
expect(code_lines.map(&:indent)).to eq([0, 0])
end
end
end

View file

@ -0,0 +1,505 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe CodeSearch do
it "rexe regression" do
lines = fixtures_dir.join("rexe.rb.txt").read.lines
lines.delete_at(148 - 1)
source = lines.join
search = CodeSearch.new(source)
search.call
expect(search.invalid_blocks.join.strip).to eq(<<~'EOM'.strip)
class Lookups
EOM
end
it "squished do regression" do
source = <<~'EOM'
def call
trydo
@options = CommandLineParser.new.parse
options.requires.each { |r| require!(r) }
load_global_config_if_exists
options.loads.each { |file| load(file) }
@user_source_code = ARGV.join(' ')
@user_source_code = 'self' if @user_source_code == ''
@callable = create_callable
init_rexe_context
init_parser_and_formatters
# This is where the user's source code will be executed; the action will in turn call `execute`.
lookup_action(options.input_mode).call unless options.noop
output_log_entry
end # one
end # two
EOM
search = CodeSearch.new(source)
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2))
trydo
end # one
EOM
end
it "regression test ambiguous end" do
source = <<~'EOM'
def call # 0
print "lol" # 1
end # one # 2
end # two # 3
EOM
search = CodeSearch.new(source)
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM')
end # two # 3
EOM
end
it "regression dog test" do
source = <<~'EOM'
class Dog
def bark
puts "woof"
end
EOM
search = CodeSearch.new(source)
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM')
class Dog
EOM
expect(search.invalid_blocks.first.lines.length).to eq(4)
end
it "handles mismatched |" do
source = <<~EOM
class Blerg
Foo.call do |a
end # one
puts lol
class Foo
end # two
end # three
EOM
search = CodeSearch.new(source)
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2))
Foo.call do |a
end # one
EOM
end
it "handles mismatched }" do
source = <<~EOM
class Blerg
Foo.call do {
puts lol
class Foo
end # two
end # three
EOM
search = CodeSearch.new(source)
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2))
Foo.call do {
EOM
end
it "handles no spaces between blocks and trailing slash" do
source = <<~'EOM'
require "rails_helper"
RSpec.describe Foo, type: :model do
describe "#bar" do
context "context" do
it "foos the bar with a foo and then bazes the foo with a bar to"\
"fooify the barred bar" do
travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do
foo = build(:foo)
end
end
end
end
describe "#baz?" do
context "baz has barred the foo" do
it "returns true" do # <== HERE
end
end
end
EOM
search = CodeSearch.new(source)
search.call
expect(search.invalid_blocks.join.strip).to eq('it "returns true" do # <== HERE')
end
it "handles no spaces between blocks" do
source = <<~'EOM'
context "foo bar" do
it "bars the foo" do
travel_to DateTime.new(2020, 10, 1, 10, 0, 0) do
end
end
end
context "test" do
it "should" do
end
EOM
search = CodeSearch.new(source)
search.call
expect(search.invalid_blocks.join.strip).to eq('it "should" do')
end
it "records debugging steps to a directory" do
Dir.mktmpdir do |dir|
dir = Pathname(dir)
search = CodeSearch.new(<<~'EOM', record_dir: dir)
class OH
def hello
def hai
end
end
EOM
search.call
expect(search.record_dir.entries.map(&:to_s)).to include("1-add-1-(3__4).txt")
expect(search.record_dir.join("1-add-1-(3__4).txt").read).to include(<<~EOM)
1 class OH
2 def hello
3 def hai
4 end
5 end
EOM
end
end
it "def with missing end" do
search = CodeSearch.new(<<~'EOM')
class OH
def hello
def hai
puts "lol"
end
end
EOM
search.call
expect(search.invalid_blocks.join.strip).to eq("def hello")
search = CodeSearch.new(<<~'EOM')
class OH
def hello
def hai
end
end
EOM
search.call
expect(search.invalid_blocks.join.strip).to eq("def hello")
search = CodeSearch.new(<<~'EOM')
class OH
def hello
def hai
end
end
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2))
def hello
EOM
end
describe "real world cases" do
it "finds hanging def in this project" do
source_string = fixtures_dir.join("this_project_extra_def.rb.txt").read
search = CodeSearch.new(source_string)
search.call
document = DisplayCodeWithLineNumbers.new(
lines: search.code_lines.select(&:visible?),
terminal: false,
highlight_lines: search.invalid_blocks.flat_map(&:lines)
).call
expect(document).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
document = DisplayCodeWithLineNumbers.new(
lines: search.code_lines.select(&:visible?),
terminal: false,
highlight_lines: search.invalid_blocks.flat_map(&:lines)
).call
expect(document).to include(<<~'EOM')
1 require 'rails_helper'
2
3 RSpec.describe AclassNameHere, type: :worker do
4 describe "thing" do
16 end # line 16 accidental end, but valid block
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 "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
expect(search.invalid_blocks.join).to eq(<<~'EOM')
Foo.call
end # two
EOM
end
it "stacked ends 2" do
search = CodeSearch.new(<<~'EOM')
def cat
blerg
end
Foo.call do
end # one
end # two
def dog
end
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM')
Foo.call do
end # one
end # two
EOM
end
it "stacked ends " do
search = CodeSearch.new(<<~'EOM')
Foo.call
def foo
puts "lol"
puts "lol"
end
end
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM')
Foo.call
end
EOM
end
it "missing space before end" do
search = CodeSearch.new(<<~'EOM')
Foo.call
def foo
puts "lol"
puts "lol"
end
end
EOM
search.call
# expand-1 and expand-2 seem to be broken?
expect(search.invalid_blocks.join).to eq(<<~'EOM')
Foo.call
end
EOM
end
end
end
it "returns syntax error in outer block without inner block" do
search = CodeSearch.new(<<~'EOM')
Foo.call
def foo
puts "lol"
puts "lol"
end # one
end # two
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM')
Foo.call
end # two
EOM
end
it "doesn't just return an empty `end`" do
search = CodeSearch.new(<<~'EOM')
Foo.call
end
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM')
Foo.call
end
EOM
end
it "finds multiple syntax errors" do
search = CodeSearch.new(<<~'EOM')
describe "hi" do
Foo.call
end
end
it "blerg" do
Bar.call
end
end
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2))
Foo.call
end
Bar.call
end
EOM
end
it "finds a typo def" do
search = CodeSearch.new(<<~'EOM')
defzfoo
puts "lol"
end
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM')
defzfoo
end
EOM
end
it "finds a mis-matched def" do
search = CodeSearch.new(<<~'EOM')
def foo
def blerg
end
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2))
def blerg
EOM
end
it "finds a naked end" do
search = CodeSearch.new(<<~'EOM')
def foo
end # one
end # two
EOM
search.call
expect(search.invalid_blocks.join).to eq(<<~'EOM'.indent(2))
end # one
EOM
end
it "returns when no invalid blocks are found" do
search = CodeSearch.new(<<~'EOM')
def foo
puts 'lol'
end
EOM
search.call
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.create_blocks_from_untracked_lines
expect(search.code_lines.join).to eq(<<~'EOM')
def foo
end
EOM
end
end
end

View file

@ -0,0 +1,172 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe DisplayInvalidBlocks do
it "works with valid code" do
syntax_string = <<~EOM
class OH
def hello
end
def hai
end
end
EOM
search = CodeSearch.new(syntax_string)
search.call
io = StringIO.new
display = DisplayInvalidBlocks.new(
io: io,
blocks: search.invalid_blocks,
terminal: false,
code_lines: search.code_lines
)
display.call
expect(io.string).to include("Syntax OK")
end
it "selectively prints to terminal if input is a tty by default" do
source = <<~EOM
class OH
def hello
def hai
end
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
io = StringIO.new
def io.isatty
true
end
block = CodeBlock.new(lines: code_lines[1])
display = DisplayInvalidBlocks.new(
io: io,
blocks: block,
code_lines: code_lines
)
display.call
expect(io.string).to include([
" 2 ",
DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT,
" def hello"
].join)
io = StringIO.new
def io.isatty
false
end
block = CodeBlock.new(lines: code_lines[1])
display = DisplayInvalidBlocks.new(
io: io,
blocks: block,
code_lines: code_lines
)
display.call
expect(io.string).to include(" 2 def hello")
end
it "outputs to io when using `call`" do
source = <<~EOM
class OH
def hello
def hai
end
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
io = StringIO.new
block = CodeBlock.new(lines: code_lines[1])
display = DisplayInvalidBlocks.new(
io: io,
blocks: block,
terminal: false,
code_lines: code_lines
)
display.call
expect(io.string).to include(" 2 def hello")
end
it " wraps code with github style codeblocks" do
source = <<~EOM
class OH
def hello
def hai
end
end
EOM
code_lines = CleanDocument.new(source: source).call.lines
block = CodeBlock.new(lines: code_lines[1])
io = StringIO.new
DisplayInvalidBlocks.new(
io: io,
blocks: block,
terminal: false,
code_lines: code_lines
).call
expect(io.string).to include(<<~EOM)
1 class OH
2 def hello
4 def hai
5 end
6 end
EOM
end
it "shows terminal characters" do
code_lines = code_line_array(<<~EOM)
class OH
def hello
def hai
end
end
EOM
io = StringIO.new
block = CodeBlock.new(lines: code_lines[1])
DisplayInvalidBlocks.new(
io: io,
blocks: block,
terminal: false,
code_lines: code_lines
).call
expect(io.string).to include([
" 1 class OH",
" 2 def hello",
" 4 end",
" 5 end",
""
].join($/))
block = CodeBlock.new(lines: code_lines[1])
io = StringIO.new
DisplayInvalidBlocks.new(
io: io,
blocks: block,
terminal: true,
code_lines: code_lines
).call
expect(io.string).to include(
[
" 1 class OH",
[" 2 ", DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT, " def hello"].join,
" 4 end",
" 5 end",
""
].join($/ + DisplayCodeWithLineNumbers::TERMINAL_END)
)
end
end
end

View file

@ -0,0 +1,255 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe "ExplainSyntax" do
it "handles shorthand syntaxes with non-bracket characters" do
source = <<~EOM
%Q* lol
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq([])
expect(explain.errors.join).to include("unterminated string")
end
it "handles %w[]" do
source = <<~EOM
node.is_a?(Op) && %w[| ||].include?(node.value) &&
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq([])
end
it "doesn't falsely identify strings or symbols as critical chars" do
source = <<~EOM
a = ['(', '{', '[', '|']
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq([])
source = <<~EOM
a = [:'(', :'{', :'[', :'|']
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq([])
end
it "finds missing |" do
source = <<~EOM
Foo.call do |
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["|"])
expect(explain.errors).to eq([explain.why("|")])
end
it "finds missing {" do
source = <<~EOM
class Cat
lol = {
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["}"])
expect(explain.errors).to eq([explain.why("}")])
end
it "finds missing }" do
source = <<~EOM
def foo
lol = "foo" => :bar }
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["{"])
expect(explain.errors).to eq([explain.why("{")])
end
it "finds missing [" do
source = <<~EOM
class Cat
lol = [
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["]"])
expect(explain.errors).to eq([explain.why("]")])
end
it "finds missing ]" do
source = <<~EOM
def foo
lol = ]
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["["])
expect(explain.errors).to eq([explain.why("[")])
end
it "finds missing (" do
source = "def initialize; ); end"
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["("])
expect(explain.errors).to eq([explain.why("(")])
end
it "finds missing )" do
source = "def initialize; (; end"
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq([")"])
expect(explain.errors).to eq([explain.why(")")])
end
it "finds missing keyword" do
source = <<~EOM
class Cat
end
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["keyword"])
expect(explain.errors).to eq([explain.why("keyword")])
end
it "finds missing end" do
source = <<~EOM
class Cat
def meow
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["end"])
expect(explain.errors).to eq([explain.why("end")])
end
it "falls back to ripper on unknown errors" do
source = <<~EOM
class Cat
def meow
1 *
end
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq([])
expect(explain.errors).to eq(RipperErrors.new(source).call.errors)
end
it "handles an unexpected rescue" do
source = <<~EOM
def foo
if bar
"baz"
else
"foo"
rescue FooBar
nil
end
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["end"])
end
# String embeds are `"#{foo} <-- here`
#
# We need to count a `#{` as a `{`
# otherwise it will report that we are
# missing a curly when we are using valid
# string embed syntax
it "is not confused by valid string embed" do
source = <<~'EOM'
foo = "#{hello}"
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq([])
end
# Missing string embed beginnings are not a
# syntax error. i.e. `"foo}"` or `"{foo}` or "#foo}"
# would just be strings with extra characters.
#
# However missing the end curly will trigger
# an error: i.e. `"#{foo`
#
# String embed beginning is a `#{` rather than
# a `{`, make sure we handle that case and
# report the correct missing `}` diagnosis
it "finds missing string embed end" do
source = <<~'EOM'
"#{foo
EOM
explain = ExplainSyntax.new(
code_lines: CodeLine.from_source(source)
).call
expect(explain.missing).to eq(["}"])
end
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe "EndBlockParse" do
it "finds blocks based on `end` keyword" do
source = <<~EOM
describe "cat" # 1
Cat.call do # 2
end # 3
end # 4
# 5
it "dog" do # 6
Dog.call do # 7
end # 8
end # 9
EOM
# raw_lex = Ripper.lex(source)
# expect(raw_lex.to_s).to_not include("dog")
lex = LexAll.new(source: source)
expect(lex.map(&:token).to_s).to include("dog")
expect(lex.first.line).to eq(1)
expect(lex.last.line).to eq(9)
end
end
end

View file

@ -0,0 +1,56 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe "PathnameFromMessage" do
it "handles filenames with colons in them" do
Dir.mktmpdir do |dir|
dir = Pathname(dir)
file = dir.join("scr:atch.rb").tap { |p| FileUtils.touch(p) }
message = "#{file}:2:in `require_relative': /private/tmp/bad.rb:1: syntax error, unexpected `end' (SyntaxError)"
file = PathnameFromMessage.new(message).call.name
expect(file).to be_truthy
end
end
it "checks if the file exists" do
Dir.mktmpdir do |dir|
dir = Pathname(dir)
file = dir.join("scratch.rb")
# No touch, file does not exist
expect(file.exist?).to be_falsey
message = "#{file}:2:in `require_relative': /private/tmp/bad.rb:1: syntax error, unexpected `end' (SyntaxError)"
io = StringIO.new
file = PathnameFromMessage.new(message, io: io).call.name
expect(io.string).to include(file.to_s)
expect(file).to be_falsey
end
end
it "does not output error message on syntax error inside of an (eval)" do
message = "(eval):1: invalid multibyte char (UTF-8) (SyntaxError)\n"
io = StringIO.new
file = PathnameFromMessage.new(message, io: io).call.name
expect(io.string).to eq("")
expect(file).to be_falsey
end
it "does not output error message on syntax error inside of streamed code" do
# An example of streamed code is: $ echo "def foo" | ruby
message = "-:1: syntax error, unexpected end-of-input\n"
io = StringIO.new
file = PathnameFromMessage.new(message, io: io).call.name
expect(io.string).to eq("")
expect(file).to be_falsey
end
end
end

View file

@ -0,0 +1,95 @@
# frozen_string_literal: true
require_relative "../spec_helper"
module SyntaxSuggest
class CurrentIndex
attr_reader :current_indent
def initialize(value)
@current_indent = value
end
def <=>(other)
@current_indent <=> other.current_indent
end
def inspect
@current_indent
end
end
RSpec.describe CodeFrontier do
it "works" do
q = PriorityQueue.new
q << 1
q << 2
expect(q.elements).to eq([2, 1])
q << 3
expect(q.elements).to eq([3, 1, 2])
expect(q.pop).to eq(3)
expect(q.pop).to eq(2)
expect(q.pop).to eq(1)
expect(q.pop).to eq(nil)
array = []
q = PriorityQueue.new
array.reverse_each do |v|
q << v
end
expect(q.elements).to eq(array)
array = [100, 36, 17, 19, 25, 0, 3, 1, 7, 2]
array.reverse_each do |v|
q << v
end
expect(q.pop).to eq(100)
expect(q.elements).to eq([36, 25, 19, 17, 0, 1, 7, 2, 3])
# expected [36, 25, 19, 17, 0, 1, 7, 2, 3]
expect(q.pop).to eq(36)
expect(q.pop).to eq(25)
expect(q.pop).to eq(19)
expect(q.pop).to eq(17)
expect(q.pop).to eq(7)
expect(q.pop).to eq(3)
expect(q.pop).to eq(2)
expect(q.pop).to eq(1)
expect(q.pop).to eq(0)
expect(q.pop).to eq(nil)
end
it "priority queue" do
frontier = PriorityQueue.new
frontier << CurrentIndex.new(0)
frontier << CurrentIndex.new(1)
expect(frontier.sorted.map(&:current_indent)).to eq([0, 1])
frontier << CurrentIndex.new(1)
expect(frontier.sorted.map(&:current_indent)).to eq([0, 1, 1])
frontier << CurrentIndex.new(0)
expect(frontier.sorted.map(&:current_indent)).to eq([0, 0, 1, 1])
frontier << CurrentIndex.new(10)
expect(frontier.sorted.map(&:current_indent)).to eq([0, 0, 1, 1, 10])
frontier << CurrentIndex.new(2)
expect(frontier.sorted.map(&:current_indent)).to eq([0, 0, 1, 1, 2, 10])
frontier = PriorityQueue.new
values = [18, 18, 0, 18, 0, 18, 18, 18, 18, 16, 18, 8, 18, 8, 8, 8, 16, 6, 0, 0, 16, 16, 4, 14, 14, 12, 12, 12, 10, 12, 12, 12, 12, 8, 10, 10, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 8, 10, 6, 6, 6, 6, 6, 6, 8, 10, 8, 8, 10, 8, 10, 8, 10, 8, 6, 8, 8, 6, 8, 6, 6, 8, 0, 8, 0, 0, 8, 8, 0, 8, 0, 8, 8, 0, 8, 8, 8, 0, 8, 0, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 6, 8, 6, 6, 6, 6, 8, 6, 8, 6, 6, 4, 4, 6, 6, 4, 6, 4, 6, 6, 4, 6, 4, 4, 6, 6, 6, 6, 4, 4, 4, 2, 4, 4, 4, 4, 4, 4, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 6, 6, 2]
values.each do |v|
value = CurrentIndex.new(v)
frontier << value # CurrentIndex.new(v)
end
expect(frontier.sorted.map(&:current_indent)).to eq(values.sort)
end
end
end