mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
200 lines
5.1 KiB
Ruby
200 lines
5.1 KiB
Ruby
|
# frozen_string_literal: true
|
||
|
|
||
|
require_relative "version"
|
||
|
|
||
|
require "tmpdir"
|
||
|
require "stringio"
|
||
|
require "pathname"
|
||
|
require "ripper"
|
||
|
require "timeout"
|
||
|
|
||
|
module SyntaxSuggest
|
||
|
# Used to indicate a default value that cannot
|
||
|
# be confused with another input.
|
||
|
DEFAULT_VALUE = Object.new.freeze
|
||
|
|
||
|
class Error < StandardError; end
|
||
|
TIMEOUT_DEFAULT = ENV.fetch("SYNTAX_SUGGEST_TIMEOUT", 1).to_i
|
||
|
|
||
|
# SyntaxSuggest.handle_error [Public]
|
||
|
#
|
||
|
# Takes a `SyntaxError` exception, uses the
|
||
|
# error message to locate the file. Then the file
|
||
|
# will be analyzed to find the location of the syntax
|
||
|
# error and emit that location to stderr.
|
||
|
#
|
||
|
# Example:
|
||
|
#
|
||
|
# begin
|
||
|
# require 'bad_file'
|
||
|
# rescue => e
|
||
|
# SyntaxSuggest.handle_error(e)
|
||
|
# end
|
||
|
#
|
||
|
# By default it will re-raise the exception unless
|
||
|
# `re_raise: false`. The message output location
|
||
|
# can be configured using the `io: $stderr` input.
|
||
|
#
|
||
|
# If a valid filename cannot be determined, the original
|
||
|
# exception will be re-raised (even with
|
||
|
# `re_raise: false`).
|
||
|
def self.handle_error(e, re_raise: true, io: $stderr)
|
||
|
unless e.is_a?(SyntaxError)
|
||
|
io.puts("SyntaxSuggest: Must pass a SyntaxError, got: #{e.class}")
|
||
|
raise e
|
||
|
end
|
||
|
|
||
|
file = PathnameFromMessage.new(e.message, io: io).call.name
|
||
|
raise e unless file
|
||
|
|
||
|
io.sync = true
|
||
|
|
||
|
call(
|
||
|
io: io,
|
||
|
source: file.read,
|
||
|
filename: file
|
||
|
)
|
||
|
|
||
|
raise e if re_raise
|
||
|
end
|
||
|
|
||
|
# SyntaxSuggest.call [Private]
|
||
|
#
|
||
|
# Main private interface
|
||
|
def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_dir: DEFAULT_VALUE, timeout: TIMEOUT_DEFAULT, io: $stderr)
|
||
|
search = nil
|
||
|
filename = nil if filename == DEFAULT_VALUE
|
||
|
Timeout.timeout(timeout) do
|
||
|
record_dir ||= ENV["DEBUG"] ? "tmp" : nil
|
||
|
search = CodeSearch.new(source, record_dir: record_dir).call
|
||
|
end
|
||
|
|
||
|
blocks = search.invalid_blocks
|
||
|
DisplayInvalidBlocks.new(
|
||
|
io: io,
|
||
|
blocks: blocks,
|
||
|
filename: filename,
|
||
|
terminal: terminal,
|
||
|
code_lines: search.code_lines
|
||
|
).call
|
||
|
rescue Timeout::Error => e
|
||
|
io.puts "Search timed out SYNTAX_SUGGEST_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
|
||
|
io.puts e.backtrace.first(3).join($/)
|
||
|
end
|
||
|
|
||
|
# SyntaxSuggest.record_dir [Private]
|
||
|
#
|
||
|
# Used to generate a unique directory to record
|
||
|
# search steps for debugging
|
||
|
def self.record_dir(dir)
|
||
|
time = Time.now.strftime("%Y-%m-%d-%H-%M-%s-%N")
|
||
|
dir = Pathname(dir)
|
||
|
dir.join(time).tap { |path|
|
||
|
path.mkpath
|
||
|
FileUtils.ln_sf(time, dir.join("last"))
|
||
|
}
|
||
|
end
|
||
|
|
||
|
# SyntaxSuggest.valid_without? [Private]
|
||
|
#
|
||
|
# This will tell you if the `code_lines` would be valid
|
||
|
# if you removed the `without_lines`. In short it's a
|
||
|
# way to detect if we've found the lines with syntax errors
|
||
|
# in our document yet.
|
||
|
#
|
||
|
# code_lines = [
|
||
|
# CodeLine.new(line: "def foo\n", index: 0)
|
||
|
# CodeLine.new(line: " def bar\n", index: 1)
|
||
|
# CodeLine.new(line: "end\n", index: 2)
|
||
|
# ]
|
||
|
#
|
||
|
# SyntaxSuggest.valid_without?(
|
||
|
# without_lines: code_lines[1],
|
||
|
# code_lines: code_lines
|
||
|
# ) # => true
|
||
|
#
|
||
|
# SyntaxSuggest.valid?(code_lines) # => false
|
||
|
def self.valid_without?(without_lines:, code_lines:)
|
||
|
lines = code_lines - Array(without_lines).flatten
|
||
|
|
||
|
if lines.empty?
|
||
|
true
|
||
|
else
|
||
|
valid?(lines)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# SyntaxSuggest.invalid? [Private]
|
||
|
#
|
||
|
# Opposite of `SyntaxSuggest.valid?`
|
||
|
def self.invalid?(source)
|
||
|
source = source.join if source.is_a?(Array)
|
||
|
source = source.to_s
|
||
|
|
||
|
Ripper.new(source).tap(&:parse).error?
|
||
|
end
|
||
|
|
||
|
# SyntaxSuggest.valid? [Private]
|
||
|
#
|
||
|
# Returns truthy if a given input source is valid syntax
|
||
|
#
|
||
|
# SyntaxSuggest.valid?(<<~EOM) # => true
|
||
|
# def foo
|
||
|
# end
|
||
|
# EOM
|
||
|
#
|
||
|
# SyntaxSuggest.valid?(<<~EOM) # => false
|
||
|
# def foo
|
||
|
# def bar # Syntax error here
|
||
|
# end
|
||
|
# EOM
|
||
|
#
|
||
|
# You can also pass in an array of lines and they'll be
|
||
|
# joined before evaluating
|
||
|
#
|
||
|
# SyntaxSuggest.valid?(
|
||
|
# [
|
||
|
# "def foo\n",
|
||
|
# "end\n"
|
||
|
# ]
|
||
|
# ) # => true
|
||
|
#
|
||
|
# SyntaxSuggest.valid?(
|
||
|
# [
|
||
|
# "def foo\n",
|
||
|
# " def bar\n", # Syntax error here
|
||
|
# "end\n"
|
||
|
# ]
|
||
|
# ) # => false
|
||
|
#
|
||
|
# As an FYI the CodeLine class instances respond to `to_s`
|
||
|
# so passing a CodeLine in as an object or as an array
|
||
|
# will convert it to it's code representation.
|
||
|
def self.valid?(source)
|
||
|
!invalid?(source)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# Integration
|
||
|
require_relative "cli"
|
||
|
|
||
|
# Core logic
|
||
|
require_relative "code_search"
|
||
|
require_relative "code_frontier"
|
||
|
require_relative "explain_syntax"
|
||
|
require_relative "clean_document"
|
||
|
|
||
|
# Helpers
|
||
|
require_relative "lex_all"
|
||
|
require_relative "code_line"
|
||
|
require_relative "code_block"
|
||
|
require_relative "block_expand"
|
||
|
require_relative "ripper_errors"
|
||
|
require_relative "priority_queue"
|
||
|
require_relative "unvisited_lines"
|
||
|
require_relative "around_block_scan"
|
||
|
require_relative "priority_engulf_queue"
|
||
|
require_relative "pathname_from_message"
|
||
|
require_relative "display_invalid_blocks"
|
||
|
require_relative "parse_blocks_from_indent_line"
|