зеркало из https://github.com/github/ruby.git
237 строки
6.1 KiB
Ruby
237 строки
6.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "version"
|
|
|
|
require "tmpdir"
|
|
require "stringio"
|
|
require "pathname"
|
|
require "timeout"
|
|
|
|
# We need Ripper loaded for `Prism.lex_compat` even if we're using Prism
|
|
# for lexing and parsing
|
|
require "ripper"
|
|
|
|
# Prism is the new parser, replacing Ripper
|
|
#
|
|
# We need to "dual boot" both for now because syntax_suggest
|
|
# supports older rubies that do not ship with syntax suggest.
|
|
#
|
|
# We also need the ability to control loading of this library
|
|
# so we can test that both modes work correctly in CI.
|
|
if (value = ENV["SYNTAX_SUGGEST_DISABLE_PRISM"])
|
|
warn "Skipping loading prism due to SYNTAX_SUGGEST_DISABLE_PRISM=#{value}"
|
|
else
|
|
begin
|
|
require "prism"
|
|
rescue LoadError
|
|
end
|
|
end
|
|
|
|
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.use_prism_parser? [Private]
|
|
#
|
|
# Tells us if the prism parser is available for use
|
|
# or if we should fallback to `Ripper`
|
|
def self.use_prism_parser?
|
|
defined?(Prism)
|
|
end
|
|
|
|
# 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 SYNTAX_SUGGEST_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
|
|
alias_dir = dir.join("last")
|
|
FileUtils.rm_rf(alias_dir) if alias_dir.exist?
|
|
FileUtils.ln_sf(time, alias_dir)
|
|
}
|
|
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?`
|
|
if defined?(Prism)
|
|
def self.invalid?(source)
|
|
source = source.join if source.is_a?(Array)
|
|
source = source.to_s
|
|
|
|
Prism.parse(source).failure?
|
|
end
|
|
else
|
|
def self.invalid?(source)
|
|
source = source.join if source.is_a?(Array)
|
|
source = source.to_s
|
|
|
|
Ripper.new(source).tap(&:parse).error?
|
|
end
|
|
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 "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"
|