Skip to content

Instantly share code, notes, and snippets.

@tompng
Last active November 6, 2023 04:39
Show Gist options
  • Save tompng/38eccb5eb9c034ec66e7650e75a8730d to your computer and use it in GitHub Desktop.
Save tompng/38eccb5eb9c034ec66e7650e75a8730d to your computer and use it in GitHub Desktop.
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