From 8e86bddc3ba7c34db77dd1b8d0f1350fa69be307 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 17 Feb 2020 21:23:11 +0900 Subject: [PATCH 1/9] Extract parsing and type checking --- lib/steep/project/file.rb | 80 ++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/lib/steep/project/file.rb b/lib/steep/project/file.rb index e986154c6..e515425f3 100644 --- a/lib/steep/project/file.rb +++ b/lib/steep/project/file.rb @@ -44,45 +44,55 @@ def errors end end + def self.parse(source_code, path:, factory:) + Source.parse(source_code, path: path.to_s, factory: factory, labeling: ASTUtils::Labeling.new) + end + + def self.type_check(source, subtyping:) + typing = Typing.new + + if source + annotations = source.annotations(block: source.node, factory: subtyping.factory, current_module: AST::Namespace.root) + const_env = TypeInference::ConstantEnv.new(factory: subtyping.factory, context: nil) + type_env = TypeInference::TypeEnv.build(annotations: annotations, + subtyping: subtyping, + const_env: const_env, + signatures: subtyping.factory.env) + + construction = TypeConstruction.new( + checker: subtyping, + annotations: annotations, + source: source, + context: TypeInference::Context.new( + block_context: nil, + module_context: TypeInference::Context::ModuleContext.new( + instance_type: nil, + module_type: nil, + implement_name: nil, + current_namespace: AST::Namespace.root, + const_env: const_env, + class_name: nil + ), + method_context: nil, + break_context: nil, + self_type: AST::Builtin::Object.instance_type, + type_env: type_env + ), + typing: typing + ) + + construction.synthesize(source.node) + end + + typing + end + def type_check(subtyping, env_updated_at) # skip type check return false if status.is_a?(TypeCheckStatus) && env_updated_at <= status.timestamp parse(subtyping.factory) do |source| - typing = Typing.new - - if source - annotations = source.annotations(block: source.node, factory: subtyping.factory, current_module: AST::Namespace.root) - const_env = TypeInference::ConstantEnv.new(factory: subtyping.factory, context: nil) - type_env = TypeInference::TypeEnv.build(annotations: annotations, - subtyping: subtyping, - const_env: const_env, - signatures: subtyping.factory.env) - - construction = TypeConstruction.new( - checker: subtyping, - annotations: annotations, - source: source, - context: TypeInference::Context.new( - block_context: nil, - module_context: TypeInference::Context::ModuleContext.new( - instance_type: nil, - module_type: nil, - implement_name: nil, - current_namespace: AST::Namespace.root, - const_env: const_env, - class_name: nil - ), - method_context: nil, - break_context: nil, - self_type: AST::Builtin::Object.instance_type, - type_env: type_env - ), - typing: typing - ) - - construction.synthesize(source.node) - end + typing = self.class.type_check(source, subtyping: subtyping) @status = TypeCheckStatus.new( typing: typing, @@ -100,7 +110,7 @@ def parse(factory) if status.is_a?(TypeCheckStatus) yield status.source else - yield Source.parse(content, path: path.to_s, factory: factory, labeling: ASTUtils::Labeling.new) + yield self.class.parse(content, path: path, factory: factory) end rescue AnnotationParser::SyntaxError => exn Steep.logger.warn { "Annotation syntax error on #{path}: #{exn.inspect}" } From 21aa1b4147f254c381bfb21cf3942c01ec9b8acc Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 18 Feb 2020 20:51:57 +0900 Subject: [PATCH 2/9] Add Factory#method_type_1 --- lib/steep/ast/types/factory.rb | 109 ++++++++++++++++++++++----------- test/type_factory_test.rb | 24 ++++++++ 2 files changed, 96 insertions(+), 37 deletions(-) diff --git a/lib/steep/ast/types/factory.rb b/lib/steep/ast/types/factory.rb index cfe701270..1d3a9ec46 100644 --- a/lib/steep/ast/types/factory.rb +++ b/lib/steep/ast/types/factory.rb @@ -187,47 +187,82 @@ def params(type) end def method_type(method_type, self_type:) - case method_type - when Ruby::Signature::MethodType - fvs = self_type.free_variables() - - type_params = [] - alpha_vars = [] - alpha_types = [] - - method_type.type_params.map do |name| - if fvs.include?(name) - type = Types::Var.fresh(name) - alpha_vars << name - alpha_types << type - type_params << type.name - else - type_params << name - end + fvs = self_type.free_variables() + + type_params = [] + alpha_vars = [] + alpha_types = [] + + method_type.type_params.map do |name| + if fvs.include?(name) + type = Types::Var.fresh(name) + alpha_vars << name + alpha_types << type + type_params << type.name + else + type_params << name end - subst = Interface::Substitution.build(alpha_vars, alpha_types) - - type = Interface::MethodType.new( - type_params: type_params, - return_type: type(method_type.type.return_type).subst(subst), - params: params(method_type.type).subst(subst), - location: nil, - block: method_type.block&.yield_self do |block| - Interface::Block.new( - optional: !block.required, - type: Proc.new(params: params(block.type).subst(subst), - return_type: type(block.type.return_type).subst(subst), location: nil) - ) - end - ) + end + subst = Interface::Substitution.build(alpha_vars, alpha_types) + + type = Interface::MethodType.new( + type_params: type_params, + return_type: type(method_type.type.return_type).subst(subst), + params: params(method_type.type).subst(subst), + location: nil, + block: method_type.block&.yield_self do |block| + Interface::Block.new( + optional: !block.required, + type: Proc.new(params: params(block.type).subst(subst), + return_type: type(block.type.return_type).subst(subst), location: nil) + ) + end + ) + + if block_given? + yield type + else + type + end + end + + def method_type_1(method_type, self_type:) + fvs = self_type.free_variables() - if block_given? - yield type + type_params = [] + alpha_vars = [] + alpha_types = [] + + method_type.type_params.map do |name| + if fvs.include?(name) + type = Ruby::Signature::Types::Variable.new(name: name, location: nil), + alpha_vars << name + alpha_types << type + type_params << type.name else - type + type_params << name end - when :any - :any + end + subst = Interface::Substitution.build(alpha_vars, alpha_types) + + type = Ruby::Signature::MethodType.new( + type_params: type_params, + type: function_1(method_type.params.subst(subst), method_type.return_type.subst(subst)), + block: method_type.block&.yield_self do |block| + block_type = block.type.subst(subst) + + Ruby::Signature::MethodType::Block.new( + type: function_1(block_type.params, block_type.return_type), + required: !block.optional + ) + end, + location: nil + ) + + if block_given? + yield type + else + type end end diff --git a/test/type_factory_test.rb b/test/type_factory_test.rb index 1bb47612c..08881c2f8 100644 --- a/test/type_factory_test.rb +++ b/test/type_factory_test.rb @@ -225,6 +225,30 @@ def test_method_type end end + def test_method_type_1 + with_factory() do |factory| + self_type = factory.type(parse_type("::Array[X]", variables: [:X])) + + parse_method_type("[A] (A) { (A, B) -> nil } -> void").tap do |original| + type = factory.method_type_1(factory.method_type(original, self_type: self_type), + self_type: self_type) + assert_equal original, type + end + + parse_method_type("[A] (A) -> void").tap do |original| + type = factory.method_type_1(factory.method_type(original, self_type: self_type), + self_type: self_type) + assert_equal original, type + end + + parse_method_type("[A] () ?{ () -> A } -> void").tap do |original| + type = factory.method_type_1(factory.method_type(original, self_type: self_type), + self_type: self_type) + assert_equal original, type + end + end + end + def test_interface_instance with_factory({ "foo.rbs" => < Date: Sat, 22 Feb 2020 18:22:34 +0900 Subject: [PATCH 3/9] Put correct context for class/module definitions --- lib/steep/type_construction.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb index 980cee750..156d92cac 100644 --- a/lib/steep/type_construction.rb +++ b/lib/steep/type_construction.rb @@ -941,9 +941,9 @@ def synthesize(node, hint: nil) if constructor.module_context&.implement_name && !namespace_module?(node) constructor.validate_method_definitions(node, constructor.module_context.implement_name) end - end - typing.add_typing(node, AST::Builtin.nil_type, context) + typing.add_typing(node, AST::Builtin.nil_type, constructor.context) + end end when :module @@ -954,9 +954,9 @@ def synthesize(node, hint: nil) if constructor.module_context&.implement_name && !namespace_module?(node) constructor.validate_method_definitions(node, constructor.module_context.implement_name) end - end - typing.add_typing(node, AST::Builtin.nil_type, context) + typing.add_typing(node, AST::Builtin.nil_type, constructor.context) + end end when :self From 02d0019f024b756bdc8397cd7a7cf53f66ee7f24 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Sat, 22 Feb 2020 19:07:22 +0900 Subject: [PATCH 4/9] Completion for methods, local variables, and instance variables --- lib/steep.rb | 1 + lib/steep/drivers/langserver.rb | 168 ++++++++++++++- lib/steep/project/completion_provider.rb | 257 +++++++++++++++++++++++ test/completion_provider_test.rb | 149 +++++++++++++ test/langserver_test.rb | 3 + test/test_helper.rb | 8 +- 6 files changed, 579 insertions(+), 7 deletions(-) create mode 100644 lib/steep/project/completion_provider.rb create mode 100644 test/completion_provider_test.rb diff --git a/lib/steep.rb b/lib/steep.rb index 0aa750f28..66ee7dd5d 100644 --- a/lib/steep.rb +++ b/lib/steep.rb @@ -74,6 +74,7 @@ require "steep/project/dsl" require "steep/project/file_loader" require "steep/project/hover_content" +require "steep/project/completion_provider" require "steep/drivers/utils/driver_helper" require "steep/drivers/check" require "steep/drivers/validate" diff --git a/lib/steep/drivers/langserver.rb b/lib/steep/drivers/langserver.rb index 99cf7e51b..361ebbb49 100644 --- a/lib/steep/drivers/langserver.rb +++ b/lib/steep/drivers/langserver.rb @@ -84,10 +84,50 @@ def handle_request(request) change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL ), hover_provider: true, + completion_provider: LanguageServer::Protocol::Interface::CompletionOptions.new( + trigger_characters: [".", "@"], ) + ) ) enqueue_type_check nil + + when :"textDocument/completion" + Steep.logger.error request.inspect + + params = request[:params] + uri = URI.parse(params[:textDocument][:uri]) + path = project.relative_path(Pathname(uri.path)) + target = project.targets.find {|target| target.source_file?(path) } + case (status = target&.status) + when Project::Target::TypeCheckStatus + subtyping = status.subtyping + source = target.source_files[path] + + line, column = params[:position].yield_self {|hash| [hash[:line]+1, hash[:character]] } + trigger = params[:context][:triggerCharacter] + + Steep.logger.error "line: #{line}, column: #{column}, trigger: #{trigger}" + + provider = Project::CompletionProvider.new(source_text: source.content, path: path, subtyping: subtyping) + items = begin + provider.run(line: line, column: column) + rescue Parser::SyntaxError + [] + end + + completion_items = items.map do |item| + format_completion_item(item) + end + + Steep.logger.error "items = #{completion_items.inspect}" + + yield id, LanguageServer::Protocol::Interface::CompletionList.new( + is_incomplete: false, + items: completion_items + ) + end + when :"textDocument/didChange" uri = URI.parse(request[:params][:textDocument][:uri]) path = project.relative_path(Pathname(uri.path)) @@ -185,13 +225,11 @@ def run_type_check() source.errors.map {|error| diagnostic_for_type_error(error) } when Project::SourceFile::AnnotationSyntaxErrorStatus [diagnostics_raw(source.status.error.message, source.status.location)] - when Project::SourceFile::ParseErrorStatus - [] - when Project::SourceFile::TypeCheckErrorStatus - [] end - report_diagnostics source.path, diagnostics + if diagnostics + report_diagnostics source.path, diagnostics + end end when Project::Target::SignatureSyntaxErrorStatus Steep.logger.info { "Signature syntax error" } @@ -331,6 +369,126 @@ def #{content.method_name}: #{content.method_type} "`#{content.type}`" end end + + def format_completion_item(item) + range = LanguageServer::Protocol::Interface::Range.new( + start: LanguageServer::Protocol::Interface::Position.new( + line: item.range.start.line-1, + character: item.range.start.column + ), + end: LanguageServer::Protocol::Interface::Position.new( + line: item.range.end.line-1, + character: item.range.end.column + ) + ) + + case item + when Project::CompletionProvider::LocalVariableItem + LanguageServer::Protocol::Interface::CompletionItem.new( + label: item.identifier, + kind: LanguageServer::Protocol::Constant::CompletionItemKind::VARIABLE, + detail: "#{item.identifier}: #{item.type}", + text_edit: LanguageServer::Protocol::Interface::TextEdit.new( + range: range, + new_text: "#{item.identifier}" + ) + ) + when Project::CompletionProvider::MethodNameItem + label = "def #{item.identifier}: #{item.method_type}" + method_type_snippet = method_type_to_snippet(item.method_type) + LanguageServer::Protocol::Interface::CompletionItem.new( + label: label, + kind: LanguageServer::Protocol::Constant::CompletionItemKind::METHOD, + text_edit: LanguageServer::Protocol::Interface::TextEdit.new( + new_text: "#{item.identifier}#{method_type_snippet}", + range: range + ), + documentation: item.definition.comment&.string, + insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET + ) + when Project::CompletionProvider::InstanceVariableItem + label = "#{item.identifier}: #{item.type}" + LanguageServer::Protocol::Interface::CompletionItem.new( + label: label, + kind: LanguageServer::Protocol::Constant::CompletionItemKind::FIELD, + text_edit: LanguageServer::Protocol::Interface::TextEdit.new( + range: range, + new_text: item.identifier, + ), + insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET + ) + end + end + + def method_type_to_snippet(method_type) + params = if method_type.type.each_param.count == 0 + "" + else + "(#{params_to_snippet(method_type.type)})" + end + + + block = if method_type.block + open, space, close = if method_type.block.type.return_type.is_a?(Ruby::Signature::Types::Bases::Void) + ["do", " ", "end"] + else + ["{", "", "}"] + end + + if method_type.block.type.each_param.count == 0 + " #{open} $0 #{close}" + else + " #{open}#{space}|#{params_to_snippet(method_type.block.type)}| $0 #{close}" + end + else + "" + end + + "#{params}#{block}" + end + + def params_to_snippet(fun) + params = [] + + index = 1 + + fun.required_positionals.each do |param| + if name = param.name + params << "${#{index}:#{param.type}}" + else + params << "${#{index}:#{param.type}}" + end + + index += 1 + end + + if fun.rest_positionals + params << "${#{index}:*#{fun.rest_positionals.type}}" + index += 1 + end + + fun.trailing_positionals.each do |param| + if name = param.name + params << "${#{index}:#{param.type}}" + else + params << "${#{index}:#{param.type}}" + end + + index += 1 + end + + fun.required_keywords.each do |keyword, param| + if name = param.name + params << "#{keyword}: ${#{index}:#{name}_}" + else + params << "#{keyword}: ${#{index}:#{param.type}_}" + end + + index += 1 + end + + params.join(", ") + end end end end diff --git a/lib/steep/project/completion_provider.rb b/lib/steep/project/completion_provider.rb new file mode 100644 index 000000000..871d6d3a7 --- /dev/null +++ b/lib/steep/project/completion_provider.rb @@ -0,0 +1,257 @@ +module Steep + class Project + class CompletionProvider + Position = Struct.new(:line, :column, keyword_init: true) do + def -(size) + Position.new(line: line, column: column - size) + end + end + Range = Struct.new(:start, :end, keyword_init: true) + + InstanceVariableItem = Struct.new(:identifier, :range, :type, keyword_init: true) + LocalVariableItem = Struct.new(:identifier, :range, :type, keyword_init: true) + MethodNameItem = Struct.new(:identifier, :range, :definition, :method_type, keyword_init: true) + + attr_reader :source_text + attr_reader :path + attr_reader :subtyping + attr_reader :modified_text + attr_reader :source + attr_reader :typing + + def initialize(source_text:, path:, subtyping:) + @source_text = source_text + @path = path + @subtyping = subtyping + end + + def type_check!(text) + @modified_text = text + @source = SourceFile.parse(text, path: path, factory: subtyping.factory) + @typing = SourceFile.type_check(source, subtyping: subtyping) + end + + def run(line:, column:) + source_text = self.source_text.dup + index = index_for(source_text, line:line, column: column) + possible_trigger = source_text[index-1] + + Steep.logger.debug "possible_trigger: #{possible_trigger.inspect}" + + position = Position.new(line: line, column: column) + + begin + type_check!(source_text) + items_for_trigger(position: position) + rescue Parser::SyntaxError + case possible_trigger + when "." + source_text[index-1] = " " + type_check!(source_text) + items_for_dot(position: position) + when "@" + source_text[index-1] = " " + type_check!(source_text) + items_for_atmark(position: position) + else + [] + end + end + end + + def range_from_loc(loc) + Range.new( + start: Position.new(line: loc.line, column: loc.column), + end: Position.new(line: loc.last_line, column: loc.last_line) + ) + end + + def at_end?(pos, of:) + of.last_line == pos.line && of.last_column == pos.column + end + + def range_for(position, prefix: "") + if prefix.empty? + Range.new(start: position, end: position) + else + Range.new(start: position - prefix.size, end: position) + end + end + + def items_for_trigger(position:) + node, *parents = source.find_nodes(line: position.line, column: position.column) + node ||= source.node + + return [] unless node + + items = [] + + case + when node.type == :send && node.children[0] == nil && at_end?(position, of: node.loc.selector) + # foo ← + context = typing.context_of(node: node) + prefix = node.children[1].to_s + + method_items_for_receiver_type(context.self_type, + include_private: true, + prefix: prefix, + position: position, + items: items) + local_variable_items_for_context(context, position: position, prefix: prefix, items: items) + + when node.type == :lvar && at_end?(position, of: node.loc) + # foo ← (lvar) + context = typing.context_of(node: node) + local_variable_items_for_context(context, position: position, prefix: node.children[0].name.to_s, items: items) + + when node.type == :send && node.children[0] && at_end?(position, of: node.loc.selector) + # foo.ba ← + context = typing.context_of(node: node) + receiver_type = case (type = typing.type_of(node: node.children[0])) + when AST::Types::Self + context.self_type + else + type + end + prefix = node.children[1].to_s + + method_items_for_receiver_type(receiver_type, + include_private: false, + prefix: prefix, + position: position, + items: items) + when node.type == :ivar && at_end?(position, of: node.loc) + # @fo ← + context = typing.context_of(node: node) + instance_variable_items_for_context(context, position: position, prefix: node.children[0].to_s, items: items) + + else + context = typing.context_of(node: node) + + method_items_for_receiver_type(context.self_type, + include_private: true, + prefix: "", + position: position, + items: items) + local_variable_items_for_context(context, position: position, prefix: "", items: items) + instance_variable_items_for_context(context, position: position, prefix: "", items: items) + end + + items + end + + def items_for_dot(position:) + # foo. ← + shift_pos = position-1 + node, *parents = source.find_nodes(line: shift_pos.line, column: shift_pos.column) + node ||= source.node + + return [] unless node + + if at_end?(shift_pos, of: node.loc) + context = typing.context_of(node: node) + receiver_type = case (type = typing.type_of(node: node)) + when AST::Types::Self + context.self_type + else + type + end + + items = [] + method_items_for_receiver_type(receiver_type, + include_private: false, + prefix: "", + position: position, + items: items) + items + end + end + + def items_for_atmark(position:) + # @ ← + shift_pos = position-1 + node, *parents = source.find_nodes(line: shift_pos.line, column: shift_pos.column) + node ||= source.node + + return [] unless node + + context = typing.context_of(node: node) + items = [] + instance_variable_items_for_context(context, prefix: "", position: position, items: items) + items + end + + def method_items_for_receiver_type(type, include_private:, prefix:, position:, items:) + range = range_for(position, prefix: prefix) + definition = case type + when AST::Types::Name::Instance + type_name = subtyping.factory.type_name_1(type.name) + subtyping.factory.definition_builder.build_instance(type_name) + when AST::Types::Name::Class, AST::Types::Name::Module + type_name = subtyping.factory.type_name_1(type.name) + subtyping.factory.definition_builder.build_singleton(type_name) + when AST::Types::Name::Interface + + end + + if definition + definition.methods.each do |name, method| + if include_private || method.public? + if name.to_s.start_with?(prefix) + if word_name?(name.to_s) + method.method_types.each do |method_type| + items << MethodNameItem.new(identifier: name, + range: range, + definition: method, + method_type: method_type) + end + end + end + end + end + end + end + + def word_name?(name) + name =~ /\w/ + end + + def local_variable_items_for_context(context, position:, prefix:, items:) + range = range_for(position, prefix: prefix) + context.type_env.lvar_types.each do |name, type| + if name.to_s.start_with?(prefix) + items << LocalVariableItem.new(identifier: name, + range: range, + type: type) + end + end + end + + def instance_variable_items_for_context(context, position:, prefix:, items:) + range = range_for(position, prefix: prefix) + context.type_env.ivar_types.map do |name, type| + if name.to_s.start_with?(prefix) + items << InstanceVariableItem.new(identifier: name, + range: range, + type: type) + end + end + end + + def index_for(string, line:, column:) + index = 0 + + string.each_line.with_index do |s, i| + if i+1 == line + index += column + break + else + index += s.size + end + end + + index + end + end + end +end diff --git a/test/completion_provider_test.rb b/test/completion_provider_test.rb new file mode 100644 index 000000000..e6040ab37 --- /dev/null +++ b/test/completion_provider_test.rb @@ -0,0 +1,149 @@ +require_relative "test_helper" + +class CompletionProviderTest < Minitest::Test + CompletionProvider = Steep::Project::CompletionProvider + + include FactoryHelper + include SubtypingHelper + + def test_on_lower_identifier + with_checker < void +end +EOF + CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| +req + +lvar1 = 1 +lvar2 = "2" +lva +lvar1 + EOR + + provider.run(line: 1, column: 3).tap do |items| + assert_equal [:require], items.map(&:identifier) + end + + provider.run(line: 5, column: 3).tap do |items| + assert_equal [:lvar1, :lvar2], items.map(&:identifier) + end + + provider.run(line: 6, column: 5).tap do |items| + assert_equal [:lvar1], items.map(&:identifier) + end + end + end + end + + def test_on_method_identifier + with_checker do + CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| +self.cl + EOR + + provider.run(line: 1, column: 7).tap do |items| + assert_equal [:class], items.map(&:identifier) + end + end + end + end + + def test_on_ivar_identifier + with_checker < void +end +EOF + CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| +class Hello + def world + @foo + @foo2 + end +end + EOR + + provider.run(line: 3, column: 8).tap do |items| + assert_equal [:@foo1, :@foo2], items.map(&:identifier) + end + + provider.run(line: 4, column: 9).tap do |items| + assert_equal [:@foo2], items.map(&:identifier) + end + end + end + end + + def test_dot_trigger + with_checker do + CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| +" ". + EOR + + provider.run(line: 1, column: 4).tap do |items| + assert_equal [:class, :initialize, :itself, :nil?, :size, :tap, :to_s, :to_str], + items.map(&:identifier).sort + end + end + end + end + + def test_on_atmark + with_checker < void +end +EOF + CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| +class Hello + def world + @ + end +end + EOR + + provider.run(line: 3, column: 5).tap do |items| + assert_equal [:@foo1, :@foo2], items.map(&:identifier).sort + end + end + end + end + + def test_on_trigger + with_checker < void +end +EOF + CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| +class Hello + def world + + end + +end + EOR + + provider.run(line: 3, column: 0).tap do |items| + assert_equal [:@foo1, :@foo2, :class, :gets, :initialize, :itself, :nil?, :puts, :require, :tap, :to_s, :world], + items.map(&:identifier).sort + end + + provider.run(line: 5, column: 0).tap do |items| + assert_equal [:attr_reader, :block_given?, :class, :gets, :initialize, :itself, :new, :nil?, :puts, :require, :tap, :to_s], + items.map(&:identifier).sort + end + end + end + end +end diff --git a/test/langserver_test.rb b/test/langserver_test.rb index 13e1a0160..dd6fe6f8e 100644 --- a/test/langserver_test.rb +++ b/test/langserver_test.rb @@ -37,6 +37,7 @@ def test_initialize capabilities: { textDocumentSync: { change: 1 }, hoverProvider: true, + completionProvider: { triggerCharacters: [".", "@"] } } }, jsonrpc: "2.0" @@ -76,6 +77,7 @@ def test_did_change capabilities: { textDocumentSync: { change: 1 }, hoverProvider: true, + completionProvider: { triggerCharacters: [".", "@"] } } }, jsonrpc: "2.0" @@ -196,6 +198,7 @@ def foo(x, y = :y) capabilities: { textDocumentSync: { change: 1 }, hoverProvider: true, + completionProvider: { triggerCharacters: [".", "@"] } } }, jsonrpc: "2.0" diff --git a/test/test_helper.rb b/test/test_helper.rb index e97b90a5a..2e3d7f18a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -212,14 +212,18 @@ def initialize: () -> void class Object < BasicObject def class: -> class def tap: { (instance) -> untyped } -> instance - def gets: -> String? def to_s: -> String def nil?: -> bool def !: -> bool def itself: -> self + +private + def require: (String) -> void + def puts: (*String) -> void + def gets: -> String? end -class Class +class Class < Module end class Module From d3ee9045cc41f7491cb39aa8e3cf8fba05a53dcd Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Sun, 23 Feb 2020 00:02:09 +0900 Subject: [PATCH 5/9] Fix completion on method call doc --- lib/steep/project/completion_provider.rb | 17 +++++++++++++++++ test/completion_provider_test.rb | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/lib/steep/project/completion_provider.rb b/lib/steep/project/completion_provider.rb index 871d6d3a7..39c14af0a 100644 --- a/lib/steep/project/completion_provider.rb +++ b/lib/steep/project/completion_provider.rb @@ -120,6 +120,23 @@ def items_for_trigger(position:) prefix: prefix, position: position, items: items) + + when node.type == :send && at_end?(position, of: node.loc.dot) + # foo.← ba + context = typing.context_of(node: node) + receiver_type = case (type = typing.type_of(node: node.children[0])) + when AST::Types::Self + context.self_type + else + type + end + + method_items_for_receiver_type(receiver_type, + include_private: false, + prefix: "", + position: position, + items: items) + when node.type == :ivar && at_end?(position, of: node.loc) # @fo ← context = typing.context_of(node: node) diff --git a/test/completion_provider_test.rb b/test/completion_provider_test.rb index e6040ab37..359e7b094 100644 --- a/test/completion_provider_test.rb +++ b/test/completion_provider_test.rb @@ -45,6 +45,10 @@ def test_on_method_identifier provider.run(line: 1, column: 7).tap do |items| assert_equal [:class], items.map(&:identifier) end + + provider.run(line: 1, column: 5).tap do |items| + assert_equal [:class, :initialize, :itself, :nil?, :tap, :to_s], items.map(&:identifier).sort + end end end end From dcd536de5ba02a4a15fa7163035a02f9e3d31b47 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 24 Feb 2020 15:14:28 +0900 Subject: [PATCH 6/9] Support upcase method name --- lib/steep/project/completion_provider.rb | 11 +++++++++++ test/completion_provider_test.rb | 23 ++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/steep/project/completion_provider.rb b/lib/steep/project/completion_provider.rb index 39c14af0a..55504dbfc 100644 --- a/lib/steep/project/completion_provider.rb +++ b/lib/steep/project/completion_provider.rb @@ -121,6 +121,17 @@ def items_for_trigger(position:) position: position, items: items) + when node.type == :const && node.children[0] == nil && at_end?(position, of: node.loc) + # Foo ← (const) + context = typing.context_of(node: node) + prefix = node.children[1].to_s + + method_items_for_receiver_type(context.self_type, + include_private: false, + prefix: prefix, + position: position, + items: items) + when node.type == :send && at_end?(position, of: node.loc.dot) # foo.← ba context = typing.context_of(node: node) diff --git a/test/completion_provider_test.rb b/test/completion_provider_test.rb index 359e7b094..cd77ed58b 100644 --- a/test/completion_provider_test.rb +++ b/test/completion_provider_test.rb @@ -7,11 +7,7 @@ class CompletionProviderTest < Minitest::Test include SubtypingHelper def test_on_lower_identifier - with_checker < void -end -EOF + with_checker do CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| req @@ -36,6 +32,23 @@ def Pathname: () -> void end end + def test_on_upper_identifier + with_checker < Array[untyped] +end +EOF + CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| +Arr + EOR + + provider.run(line: 1, column: 3).tap do |items| + assert_equal [:Array], items.map(&:identifier) + end + end + end + end + def test_on_method_identifier with_checker do CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| From 842d18fe2ca046b9e77e92b7c44c989282d00123 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 24 Feb 2020 15:15:06 +0900 Subject: [PATCH 7/9] Fix #items_for_dot --- lib/steep/project/completion_provider.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/steep/project/completion_provider.rb b/lib/steep/project/completion_provider.rb index 55504dbfc..20e734973 100644 --- a/lib/steep/project/completion_provider.rb +++ b/lib/steep/project/completion_provider.rb @@ -192,6 +192,8 @@ def items_for_dot(position:) position: position, items: items) items + else + [] end end From 177740566ee0e3803a1837a3323e28958c234ee6 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 24 Feb 2020 15:15:48 +0900 Subject: [PATCH 8/9] Add debug prints --- lib/steep.rb | 8 ++++++++ lib/steep/drivers/langserver.rb | 2 +- lib/steep/project/completion_provider.rb | 25 ++++++++++++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/lib/steep.rb b/lib/steep.rb index 66ee7dd5d..01f2c023f 100644 --- a/lib/steep.rb +++ b/lib/steep.rb @@ -115,4 +115,12 @@ def self.log_output=(output) @logger = nil self.log_output = STDERR + + def self.measure(message) + start = Time.now + yield.tap do + time = Time.now - start + self.logger.info "#{message} took #{time} seconds" + end + end end diff --git a/lib/steep/drivers/langserver.rb b/lib/steep/drivers/langserver.rb index 361ebbb49..89495f6c8 100644 --- a/lib/steep/drivers/langserver.rb +++ b/lib/steep/drivers/langserver.rb @@ -120,7 +120,7 @@ def handle_request(request) format_completion_item(item) end - Steep.logger.error "items = #{completion_items.inspect}" + Steep.logger.debug "items = #{completion_items.inspect}" yield id, LanguageServer::Protocol::Interface::CompletionList.new( is_incomplete: false, diff --git a/lib/steep/project/completion_provider.rb b/lib/steep/project/completion_provider.rb index 20e734973..e94dc1175 100644 --- a/lib/steep/project/completion_provider.rb +++ b/lib/steep/project/completion_provider.rb @@ -27,8 +27,14 @@ def initialize(source_text:, path:, subtyping:) def type_check!(text) @modified_text = text - @source = SourceFile.parse(text, path: path, factory: subtyping.factory) - @typing = SourceFile.type_check(source, subtyping: subtyping) + + Steep.measure "parsing" do + @source = SourceFile.parse(text, path: path, factory: subtyping.factory) + end + + Steep.measure "typechecking" do + @typing = SourceFile.type_check(source, subtyping: subtyping) + end end def run(line:, column:) @@ -36,14 +42,21 @@ def run(line:, column:) index = index_for(source_text, line:line, column: column) possible_trigger = source_text[index-1] - Steep.logger.debug "possible_trigger: #{possible_trigger.inspect}" + Steep.logger.error "possible_trigger: #{possible_trigger.inspect}" position = Position.new(line: line, column: column) begin - type_check!(source_text) - items_for_trigger(position: position) - rescue Parser::SyntaxError + Steep.measure "type_check!" do + type_check!(source_text) + end + + Steep.measure "completion item collection" do + items_for_trigger(position: position) + end + + rescue Parser::SyntaxError => exn + Steep.logger.error "recovering syntax error: #{exn.inspect}" case possible_trigger when "." source_text[index-1] = " " From 1fa71e81d8211df35678591d8789d55e68e900ca Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Mon, 24 Feb 2020 16:41:40 +0900 Subject: [PATCH 9/9] Support interface methods --- lib/steep/project/completion_provider.rb | 4 +++- test/completion_provider_test.rb | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/steep/project/completion_provider.rb b/lib/steep/project/completion_provider.rb index e94dc1175..d5a0d1f8b 100644 --- a/lib/steep/project/completion_provider.rb +++ b/lib/steep/project/completion_provider.rb @@ -234,7 +234,9 @@ def method_items_for_receiver_type(type, include_private:, prefix:, position:, i type_name = subtyping.factory.type_name_1(type.name) subtyping.factory.definition_builder.build_singleton(type_name) when AST::Types::Name::Interface - + type_name = subtyping.factory.type_name_1(type.name) + interface = subtyping.factory.env.find_class(type_name) + subtyping.factory.definition_builder.build_interface(type_name, interface) end if definition diff --git a/test/completion_provider_test.rb b/test/completion_provider_test.rb index cd77ed58b..69cf7fe23 100644 --- a/test/completion_provider_test.rb +++ b/test/completion_provider_test.rb @@ -163,4 +163,25 @@ def world end end end + + def test_on_interface + with_checker < String +end +EOF + CompletionProvider.new(source_text: <<-EOR, path: Pathname("foo.rb"), subtyping: checker).tap do |provider| +# @type var x: _ToStr +x = _ = nil + +x. + EOR + + provider.run(line: 4, column: 2).tap do |items| + assert_equal [:to_str], + items.map(&:identifier).sort + end + end + end + end end