Last active
November 6, 2023 04:39
-
-
Save tompng/38eccb5eb9c034ec66e7650e75a8730d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Dir.chdir File.dirname __FILE__ | |
katakata_dir = '../../../../../tompng/katakata_irb' | |
def replace_module_names(code) | |
code | |
.gsub('module KatakataIrb', 'module IRB::TypeCompletion') | |
.gsub('KatakataIrb::TypeAnalyzer', 'TypeAnalyzer') | |
.gsub('KatakataIrb::Types', 'Types') | |
.gsub('KatakataIrb::Scope', 'Scope') | |
.gsub(/ +(OBJECT|MODULE|CLASS)_[A-Z_]+_METHOD =.+\n+/, '') | |
.gsub(/(OBJECT|MODULE|CLASS)_[A-Z_]+_METHOD/){'Methods::' + _1} | |
.gsub(/.+KatakataIRB failed to.+\n.+\n/, '') | |
.gsub(/.*log_puts.*\n/, '') | |
end | |
def find_code(code, from, to, indent = 0) | |
lines = code.split("\n") | |
code = lines.select { !!(_1 == from .. _1 == to) }[1...-1].map do | |
"#{' '*indent}#{_1}".rstrip | |
end.join("\n") | |
replace_module_names code | |
end | |
scope_rb = File.read("#{katakata_dir}/lib/katakata_irb/scope.rb") | |
File.write 'scope.rb', <<RUBY | |
# frozen_string_literal: true | |
require 'set' | |
require_relative 'types' | |
module IRB | |
module TypeCompletion | |
#{find_code(scope_rb, 'module KatakataIrb', 'end', 1)} | |
end | |
end | |
RUBY | |
types_rb = File.read("#{katakata_dir}/lib/katakata_irb/types.rb") | |
File.write 'types.rb', <<RUBY | |
# frozen_string_literal: true | |
require_relative 'methods' | |
module IRB | |
module TypeCompletion | |
module Types | |
#{find_code(types_rb, 'module KatakataIrb::Types', 'end', 2)} | |
end | |
end | |
end | |
RUBY | |
analyzer_rb = File.read("#{katakata_dir}/lib/katakata_irb/type_analyzer.rb") | |
File.write 'type_analyzer.rb', <<RUBY | |
# frozen_string_literal: true | |
require 'set' | |
require_relative 'types' | |
require_relative 'scope' | |
require 'prism' | |
module IRB | |
module TypeCompletion | |
class TypeAnalyzer | |
#{find_code(analyzer_rb, 'class KatakataIrb::TypeAnalyzer', 'end', 2)} | |
end | |
end | |
end | |
RUBY | |
completor = File.read("#{katakata_dir}/lib/katakata_irb/completor.rb") | |
File.write 'completor.rb', <<RUBY | |
# frozen_string_literal: true | |
require 'prism' | |
require 'irb/completion' | |
require_relative 'type_analyzer' | |
module IRB | |
module TypeCompletion | |
class Completor < BaseCompletor # :nodoc: | |
HIDDEN_METHODS = %w[Namespace TypeName] # defined by rbs, should be hidden | |
class << self | |
attr_accessor :last_completion_error | |
end | |
def inspect | |
name = 'TypeCompletion::Completor' | |
prism_info = "Prism: \#{Prism::VERSION}" | |
if Types.rbs_builder | |
"\#{name}(\#{prism_info}, RBS: \#{RBS::VERSION})" | |
elsif Types.rbs_load_error | |
"\#{name}(\#{prism_info}, RBS: \#{Types.rbs_load_error.inspect})" | |
else | |
"\#{name}(\#{prism_info}, RBS: loading)" | |
end | |
end | |
def completion_candidates(preposing, target, _postposing, bind:) | |
@preposing = preposing | |
verbose, $VERBOSE = $VERBOSE, nil | |
code = "\#{preposing}\#{target}" | |
@result = analyze code, bind | |
name, candidates = candidates_from_result(@result) | |
all_symbols_pattern = /\\A[ -\\/:-@\\[-`\\{-~]*\\z/ | |
candidates.map(&:to_s).select { !_1.match?(all_symbols_pattern) && _1.start_with?(name) }.uniq.sort.map do | |
target + _1[name.size..] | |
end | |
rescue SyntaxError, StandardError => e | |
Completor.last_completion_error = e | |
handle_error(e) | |
[] | |
ensure | |
$VERBOSE = verbose | |
end | |
def doc_namespace(preposing, matched, postposing, bind:) | |
name = matched[/[a-zA-Z_0-9]*[!?=]?\\z/] | |
#{ | |
find_code( | |
completor.gsub('KatakataIrb::Completor.prev_analyze_result', '@result'), | |
' name = input[/[a-zA-Z_0-9]*[!?=]?\z/]', | |
' end', | |
1 | |
) | |
} | |
end | |
def candidates_from_result(result) | |
#{ | |
find_code(completor, ' def self.candidates_from_result(result)', ' end', 2) | |
.gsub('scope.self_type.all_methods.map(&:to_s) | scope.local_variables') { "#{_1} | ReservedWords"} | |
.gsub("scope.constants.sort\n") { "#{_1.chomp} | ReservedWords\n" } | |
.gsub(/ +in \[:require \| :require_relative => method, name\](.|\n)+in \[:call_or_const, type, name, self_call\]/, | |
" in [:require, name]\n" + | |
" retrieve_files_to_require_from_load_path\n" + | |
" in [:require_relative, name]\n" + | |
" retrieve_files_to_require_relative_from_current_dir\n" + | |
" in [:call_or_const, type, name, self_call]" | |
) | |
.gsub(/if IRB.const_defined\? :RegexpCompletor(.|\n)+elsif method == :require/, 'if method == :require') | |
.gsub('path_completor.retrieve_files_to_require', 'retrieve_files_to_require') | |
} | |
end | |
def analyze(code, binding = Object::TOPLEVEL_BINDING) | |
#{find_code completor, ' def self.analyze(code, binding = Object::TOPLEVEL_BINDING)', ' end', 2} | |
end | |
def find_target(node, position) | |
#{find_code completor, ' def self.find_target(node, position)', ' end', 2} | |
end | |
def handle_error(e) | |
end | |
end | |
end | |
end | |
RUBY | |
require 'irb/completion' | |
require_relative './completor' | |
IRB::TypeCompletion::Types.preload_in_thread.join | |
p IRB::TypeCompletion::Completor.new.analyze 'a=1; a.times.map(&:to_s).map{_1.' | |
test_dir = '../../../test/irb/type_completion' | |
Dir.mkdir test_dir unless Dir.exist? test_dir | |
ruby_version_condition = <<RUBY.chomp | |
return unless RUBY_VERSION >= '3.0.0' | |
return if RUBY_ENGINE == 'truffleruby' # needs endless method definition | |
RUBY | |
File.write "#{test_dir}/test_scope.rb", <<RUBY | |
# frozen_string_literal: true | |
#{ruby_version_condition} | |
require 'irb/type_completion/scope' | |
require_relative '../helper' | |
module TestIRB | |
class TypeCompletionScopeTest < TestCase | |
#{ | |
find_code( | |
File.read("#{katakata_dir}/test/test_scope.rb"), | |
' NIL = Types::NIL', | |
'end', | |
1 | |
).gsub(/Types|RootScope|Scope/) { "IRB::TypeCompletion::#{_1}" }.gsub('NIL', 'IRB::TypeCompletion::Types::NIL') | |
} | |
end | |
end | |
RUBY | |
File.write "#{test_dir}/test_types.rb", <<RUBY | |
# frozen_string_literal: true | |
#{ruby_version_condition} | |
require 'irb/type_completion/types' | |
require_relative '../helper' | |
module TestIRB | |
class TypeCompletionTypesTest < TestCase | |
#{ | |
find_code( | |
File.read("#{katakata_dir}/test/test_type.rb"), | |
'class TestType < Minitest::Test', | |
'end', | |
1 | |
).gsub('Types') { "IRB::TypeCompletion::#{_1}" } | |
.gsub('KatakataIrb', 'IRB::TypeCompletion') | |
} | |
end | |
end | |
RUBY | |
ruby_gem_condition = <<RUBY | |
# Run test only when Ruby >= 3.0 and %w[prism rbs] are available | |
#{ruby_version_condition} | |
begin | |
require 'prism' | |
require 'rbs' | |
rescue LoadError | |
return | |
end | |
RUBY | |
File.write "#{test_dir}/test_type_analyze.rb", <<RUBY | |
# frozen_string_literal: true | |
#{ruby_gem_condition} | |
require 'irb/completion' | |
require 'irb/type_completion/completor' | |
require_relative '../helper' | |
module TestIRB | |
class TypeCompletionAnalyzeTest < TestCase | |
def setup | |
IRB::TypeCompletion::Types.preload_in_thread.join | |
end | |
def empty_binding | |
binding | |
end | |
def analyze(code, binding: nil) | |
completor = IRB::TypeCompletion::Completor.new | |
def completor.handle_error(e) | |
raise e | |
end | |
completor.analyze(code, binding || empty_binding) | |
end | |
def assert_analyze_type(code, type, token = nil, binding: empty_binding) | |
result_type, result_token = analyze(code, binding: binding) | |
assert_equal type, result_type | |
assert_equal token, result_token if token | |
end | |
def assert_call(code, include: nil, exclude: nil, binding: nil) | |
#{ | |
find_code( | |
File.read("#{katakata_dir}/test/test_type_analyze.rb"), | |
' def assert_call(code, include: nil, exclude: nil, binding: nil)', | |
'end', | |
1 | |
) | |
.gsub(/ +def test_sig_dir\n(.*\n){5}/, '') | |
.gsub('KatakataIrb', 'IRB') | |
.gsub("assert_call('module IRB::TypeCompletion;", "assert_call('module IRB;") | |
.gsub(/ def test_rescue_assign_no_log\n((?:.*\n){8})/) { | |
" def test_rescue_assign\n#{$1.lines[1...7].map{_1[2..]}.join}" | |
}.gsub( | |
"assert (analyze('begin; rescue => A') in [:const, _, 'A', _])", | |
"assert_equal [:const, 'A'], analyze('begin; rescue => A').values_at(0, 2)" | |
).gsub( | |
"assert (analyze('begin; rescue => a.b') in [:call, _, 'b', _])", | |
"assert_equal [:call, 'b'], analyze('begin; rescue => a.b').values_at(0, 2)" | |
) | |
} | |
end | |
end | |
RUBY |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment