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:
parent
3504be1bc1
commit
0d9f4ea0d4
Notes:
git
2022-08-26 12:16:18 +09:00
26 changed files with 14536 additions and 0 deletions
74
spec/syntax_suggest/fixtures/derailed_require_tree.rb.txt
Normal file
74
spec/syntax_suggest/fixtures/derailed_require_tree.rb.txt
Normal 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
|
569
spec/syntax_suggest/fixtures/rexe.rb.txt
Executable file
569
spec/syntax_suggest/fixtures/rexe.rb.txt
Executable 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 }
|
121
spec/syntax_suggest/fixtures/routes.rb.txt
Normal file
121
spec/syntax_suggest/fixtures/routes.rb.txt
Normal 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
|
1344
spec/syntax_suggest/fixtures/ruby_buildpack.rb.txt
Normal file
1344
spec/syntax_suggest/fixtures/ruby_buildpack.rb.txt
Normal file
File diff suppressed because it is too large
Load diff
9234
spec/syntax_suggest/fixtures/syntax_tree.rb.txt
Normal file
9234
spec/syntax_suggest/fixtures/syntax_tree.rb.txt
Normal file
File diff suppressed because it is too large
Load diff
64
spec/syntax_suggest/fixtures/this_project_extra_def.rb.txt
Normal file
64
spec/syntax_suggest/fixtures/this_project_extra_def.rb.txt
Normal 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
|
35
spec/syntax_suggest/fixtures/webmock.rb.txt
Normal file
35
spec/syntax_suggest/fixtures/webmock.rb.txt
Normal 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
|
22
spec/syntax_suggest/integration/exe_cli_spec.rb
Normal file
22
spec/syntax_suggest/integration/exe_cli_spec.rb
Normal 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
|
151
spec/syntax_suggest/integration/ruby_command_line_spec.rb
Normal file
151
spec/syntax_suggest/integration/ruby_command_line_spec.rb
Normal 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
|
211
spec/syntax_suggest/integration/syntax_suggest_spec.rb
Normal file
211
spec/syntax_suggest/integration/syntax_suggest_spec.rb
Normal 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
|
90
spec/syntax_suggest/spec_helper.rb
Normal file
90
spec/syntax_suggest/spec_helper.rb
Normal 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
|
83
spec/syntax_suggest/unit/api_spec.rb
Normal file
83
spec/syntax_suggest/unit/api_spec.rb
Normal 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
|
165
spec/syntax_suggest/unit/around_block_scan_spec.rb
Normal file
165
spec/syntax_suggest/unit/around_block_scan_spec.rb
Normal 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
|
200
spec/syntax_suggest/unit/block_expand_spec.rb
Normal file
200
spec/syntax_suggest/unit/block_expand_spec.rb
Normal 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
|
202
spec/syntax_suggest/unit/capture_code_context_spec.rb
Normal file
202
spec/syntax_suggest/unit/capture_code_context_spec.rb
Normal 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
|
259
spec/syntax_suggest/unit/clean_document_spec.rb
Normal file
259
spec/syntax_suggest/unit/clean_document_spec.rb
Normal 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
|
224
spec/syntax_suggest/unit/cli_spec.rb
Normal file
224
spec/syntax_suggest/unit/cli_spec.rb
Normal 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
|
77
spec/syntax_suggest/unit/code_block_spec.rb
Normal file
77
spec/syntax_suggest/unit/code_block_spec.rb
Normal 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
|
135
spec/syntax_suggest/unit/code_frontier_spec.rb
Normal file
135
spec/syntax_suggest/unit/code_frontier_spec.rb
Normal 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
|
164
spec/syntax_suggest/unit/code_line_spec.rb
Normal file
164
spec/syntax_suggest/unit/code_line_spec.rb
Normal 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
|
505
spec/syntax_suggest/unit/code_search_spec.rb
Normal file
505
spec/syntax_suggest/unit/code_search_spec.rb
Normal 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
|
172
spec/syntax_suggest/unit/display_invalid_blocks_spec.rb
Normal file
172
spec/syntax_suggest/unit/display_invalid_blocks_spec.rb
Normal 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
|
255
spec/syntax_suggest/unit/explain_syntax_spec.rb
Normal file
255
spec/syntax_suggest/unit/explain_syntax_spec.rb
Normal 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
|
29
spec/syntax_suggest/unit/lex_all_spec.rb
Normal file
29
spec/syntax_suggest/unit/lex_all_spec.rb
Normal 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
|
56
spec/syntax_suggest/unit/pathname_from_message_spec.rb
Normal file
56
spec/syntax_suggest/unit/pathname_from_message_spec.rb
Normal 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
|
95
spec/syntax_suggest/unit/priority_queue_spec.rb
Normal file
95
spec/syntax_suggest/unit/priority_queue_spec.rb
Normal 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
|
Loading…
Reference in a new issue