diff --git a/.rubocop.yml b/.rubocop.yml index 80b3a433d..16c85e112 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -25,6 +25,15 @@ Sorbet/FalseSigil: Sorbet/TrueSigil: Enabled: true Include: - - "**/*.rb" + - "test/**/*.rb" Exclude: - "**/*.rake" + - "lib/**/*.rb" + +Sorbet/StrictSigil: + Enabled: true + Include: + - "lib/**/*.rb" + Exclude: + - "**/*.rake" + - "test/**/*.rb" diff --git a/lib/internal.rb b/lib/internal.rb index 5511fe495..7bb9aa3e8 100644 --- a/lib/internal.rb +++ b/lib/internal.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "sorbet-runtime" diff --git a/lib/ruby-lsp.rb b/lib/ruby-lsp.rb index 0e15c5d2f..e3388fcfd 100644 --- a/lib/ruby-lsp.rb +++ b/lib/ruby-lsp.rb @@ -1,6 +1,6 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp - VERSION = File.read(File.expand_path("../VERSION", __dir__)).strip + VERSION = T.let(File.read(File.expand_path("../VERSION", __dir__)).strip, String) end diff --git a/lib/ruby_lsp/document.rb b/lib/ruby_lsp/document.rb index 5f0912a36..036a5052d 100644 --- a/lib/ruby_lsp/document.rb +++ b/lib/ruby_lsp/document.rb @@ -32,7 +32,13 @@ def ==(other) @source == other.source end - sig { params(request_name: Symbol, block: T.proc.params(document: Document).returns(T.untyped)).returns(T.untyped) } + sig do + type_parameters(:T) + .params( + request_name: Symbol, + block: T.proc.params(document: Document).returns(T.type_parameter(:T)) + ).returns(T.type_parameter(:T)) + end def cache_fetch(request_name, &block) cached = @cache[request_name] return cached if cached diff --git a/lib/ruby_lsp/handler.rb b/lib/ruby_lsp/handler.rb index fe28a1d5e..60d74b3bc 100644 --- a/lib/ruby_lsp/handler.rb +++ b/lib/ruby_lsp/handler.rb @@ -114,14 +114,14 @@ def respond_with_capabilities(enabled_features) sig { params(uri: String).returns(T::Array[LanguageServer::Protocol::Interface::DocumentSymbol]) } def respond_with_document_symbol(uri) store.cache_fetch(uri, :document_symbol) do |document| - RubyLsp::Requests::DocumentSymbol.run(document) + RubyLsp::Requests::DocumentSymbol.new(document).run end end sig { params(uri: String).returns(T::Array[LanguageServer::Protocol::Interface::FoldingRange]) } def respond_with_folding_ranges(uri) store.cache_fetch(uri, :folding_ranges) do |document| - Requests::FoldingRanges.run(document) + Requests::FoldingRanges.new(document).run end end @@ -129,11 +129,11 @@ def respond_with_folding_ranges(uri) params( uri: String, positions: T::Array[Document::PositionShape] - ).returns(T::Array[RubyLsp::Requests::Support::SelectionRange]) + ).returns(T::Array[T.nilable(RubyLsp::Requests::Support::SelectionRange)]) end def respond_with_selection_ranges(uri, positions) ranges = store.cache_fetch(uri, :selection_ranges) do |document| - Requests::SelectionRanges.run(document) + Requests::SelectionRanges.new(document).run end # Per the selection range request spec (https://microsoft.github.io/language-server-protocol/specification#textDocument_selectionRange), @@ -150,19 +150,22 @@ def respond_with_selection_ranges(uri, positions) sig { params(uri: String).returns(LanguageServer::Protocol::Interface::SemanticTokens) } def respond_with_semantic_highlighting(uri) store.cache_fetch(uri, :semantic_highlighting) do |document| - Requests::SemanticHighlighting.new(document, encoder: Requests::Support::SemanticTokenEncoder.new).run + T.cast( + Requests::SemanticHighlighting.new(document, encoder: Requests::Support::SemanticTokenEncoder.new).run, + LanguageServer::Protocol::Interface::SemanticTokens + ) end end - sig { params(uri: String).returns(T::Array[LanguageServer::Protocol::Interface::TextEdit]) } + sig { params(uri: String).returns(T.nilable(T::Array[LanguageServer::Protocol::Interface::TextEdit])) } def respond_with_formatting(uri) - Requests::Formatting.run(uri, store.get(uri)) + Requests::Formatting.new(uri, store.get(uri)).run end sig { params(uri: String).void } def send_diagnostics(uri) response = store.cache_fetch(uri, :diagnostics) do |document| - Requests::Diagnostics.run(uri, document) + Requests::Diagnostics.new(uri, document).run end @writer.write( @@ -175,11 +178,11 @@ def send_diagnostics(uri) end sig do - params(uri: String, range: T::Range[Integer]).returns(T::Array[LanguageServer::Protocol::Interface::Diagnostic]) + params(uri: String, range: T::Range[Integer]).returns(T::Array[LanguageServer::Protocol::Interface::CodeAction]) end def respond_with_code_actions(uri, range) store.cache_fetch(uri, :code_actions) do |document| - Requests::CodeActions.run(uri, document, range) + Requests::CodeActions.new(uri, document, range).run end end @@ -190,10 +193,16 @@ def respond_with_code_actions(uri, range) ).returns(T::Array[LanguageServer::Protocol::Interface::DocumentHighlight]) end def respond_with_document_highlight(uri, position) - Requests::DocumentHighlight.run(store.get(uri), position) + Requests::DocumentHighlight.new(store.get(uri), position).run end - sig { params(request: T::Hash[Symbol, T.untyped], block: T.proc.void).returns(T.untyped) } + sig do + type_parameters(:T) + .params( + request: T::Hash[Symbol, T.untyped], + block: T.proc.returns(T.type_parameter(:T)) + ).returns(T.type_parameter(:T)) + end def with_telemetry(request, &block) result = T.let(nil, T.untyped) error = T.let(nil, T.nilable(StandardError)) diff --git a/lib/ruby_lsp/requests.rb b/lib/ruby_lsp/requests.rb index 7f49ea0a2..ab919a610 100644 --- a/lib/ruby_lsp/requests.rb +++ b/lib/ruby_lsp/requests.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp diff --git a/lib/ruby_lsp/requests/base_request.rb b/lib/ruby_lsp/requests/base_request.rb index c6d52c731..9e94da439 100644 --- a/lib/ruby_lsp/requests/base_request.rb +++ b/lib/ruby_lsp/requests/base_request.rb @@ -1,24 +1,26 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp module Requests # :nodoc: class BaseRequest < SyntaxTree::Visitor - def self.run(document) - new(document).run - end + extend T::Sig + extend T::Helpers + + abstract! + sig { params(document: Document).void } def initialize(document) @document = document super() end - def run - raise NotImplementedError, "#{self.class}#run must be implemented" - end + sig { abstract.returns(Object) } + def run; end + sig { params(node: SyntaxTree::Node).returns(LanguageServer::Protocol::Interface::Range) } def range_from_syntax_tree_node(node) loc = node.location diff --git a/lib/ruby_lsp/requests/code_actions.rb b/lib/ruby_lsp/requests/code_actions.rb index ad0fe5d48..129933b44 100644 --- a/lib/ruby_lsp/requests/code_actions.rb +++ b/lib/ruby_lsp/requests/code_actions.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp @@ -14,23 +14,32 @@ module Requests # puts "Hello" # --> code action: quick fix indentation # end # ``` - class CodeActions - def self.run(uri, document, range) - new(uri, document, range).run - end + class CodeActions < BaseRequest + extend T::Sig + sig do + params( + uri: String, + document: Document, + range: T::Range[Integer] + ).void + end def initialize(uri, document, range) - @document = document + super(document) + @uri = uri @range = range end + sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::CodeAction], Object)) } def run - diagnostics = Diagnostics.run(@uri, @document) - corrections = diagnostics.select { |diagnostic| diagnostic.correctable? && diagnostic.in_range?(@range) } + diagnostics = Diagnostics.new(@uri, @document).run + corrections = diagnostics.select do |diagnostic| + diagnostic.correctable? && T.cast(diagnostic, Support::RuboCopDiagnostic).in_range?(@range) + end return [] if corrections.empty? - corrections.map!(&:to_lsp_code_action) + T.cast(corrections, T::Array[Support::RuboCopDiagnostic]).map!(&:to_lsp_code_action) end end end diff --git a/lib/ruby_lsp/requests/diagnostics.rb b/lib/ruby_lsp/requests/diagnostics.rb index beb23ed46..7b4c97b96 100644 --- a/lib/ruby_lsp/requests/diagnostics.rb +++ b/lib/ruby_lsp/requests/diagnostics.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp @@ -15,6 +15,16 @@ module Requests # end # ``` class Diagnostics < RuboCopRequest + extend T::Sig + + sig do + override.returns( + T.any( + T.all(T::Array[Support::RuboCopDiagnostic], Object), + T.all(T::Array[Support::SyntaxErrorDiagnostic], Object), + ) + ) + end def run return syntax_error_diagnostics if @document.syntax_errors? @@ -23,12 +33,14 @@ def run @diagnostics end + sig { params(_file: String, offenses: T::Array[RuboCop::Cop::Offense]).void } def file_finished(_file, offenses) @diagnostics = offenses.map { |offense| Support::RuboCopDiagnostic.new(offense, @uri) } end private + sig { returns(T::Array[Support::SyntaxErrorDiagnostic]) } def syntax_error_diagnostics @document.syntax_error_edits.map { |e| Support::SyntaxErrorDiagnostic.new(e) } end diff --git a/lib/ruby_lsp/requests/document_highlight.rb b/lib/ruby_lsp/requests/document_highlight.rb index 64211c9e4..009de528a 100644 --- a/lib/ruby_lsp/requests/document_highlight.rb +++ b/lib/ruby_lsp/requests/document_highlight.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp @@ -21,18 +21,28 @@ module Requests # end # ``` class DocumentHighlight < BaseRequest - def self.run(document, position) - new(document, position).run + extend T::Sig + + VarNodes = T.type_alias do + T.any( + SyntaxTree::GVar, + SyntaxTree::Ident, + SyntaxTree::IVar, + SyntaxTree::Const, + SyntaxTree::CVar + ) end + sig { params(document: Document, position: Document::PositionShape).void } def initialize(document, position) - @highlights = [] + @highlights = T.let([], T::Array[LanguageServer::Protocol::Interface::DocumentHighlight]) position = Document::Scanner.new(document.source).find_position(position) - @target = find(document.tree, position) + @target = T.let(find(document.tree, position), T.nilable(VarNodes)) super(document) end + sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::DocumentHighlight], Object)) } def run # no @target means the target is not highlightable return [] unless @target @@ -41,6 +51,7 @@ def run @highlights end + sig { params(node: SyntaxTree::VarField).void } def visit_var_field(node) if matches_target?(node.value) add_highlight( @@ -52,6 +63,7 @@ def visit_var_field(node) super end + sig { params(node: SyntaxTree::VarRef).void } def visit_var_ref(node) if matches_target?(node.value) add_highlight( @@ -65,6 +77,7 @@ def visit_var_ref(node) private + sig { params(node: SyntaxTree::Node, position: Integer).returns(T.nilable(VarNodes)) } def find(node, position) matched = node.child_nodes.compact.bsearch do |child| @@ -83,10 +96,12 @@ def find(node, position) end end + sig { params(node: SyntaxTree::Node).returns(T::Boolean) } def matches_target?(node) - node.is_a?(@target.class) && node.value == @target.value + node.is_a?(@target.class) && T.cast(node, VarNodes).value == T.must(@target).value end + sig { params(node: SyntaxTree::Node, kind: Integer).void } def add_highlight(node, kind) range = range_from_syntax_tree_node(node) @highlights << LanguageServer::Protocol::Interface::DocumentHighlight.new(range: range, kind: kind) diff --git a/lib/ruby_lsp/requests/document_symbol.rb b/lib/ruby_lsp/requests/document_symbol.rb index 618eab2e5..6418f1eec 100644 --- a/lib/ruby_lsp/requests/document_symbol.rb +++ b/lib/ruby_lsp/requests/document_symbol.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp @@ -25,7 +25,9 @@ module Requests # end # ``` class DocumentSymbol < BaseRequest - SYMBOL_KIND = { + extend T::Sig + + SYMBOL_KIND = T.let({ file: 1, module: 2, namespace: 3, @@ -52,30 +54,40 @@ class DocumentSymbol < BaseRequest event: 24, operator: 25, typeparameter: 26, - }.freeze + }.freeze, T::Hash[Symbol, Integer]) - ATTR_ACCESSORS = ["attr_reader", "attr_writer", "attr_accessor"].freeze + ATTR_ACCESSORS = T.let(["attr_reader", "attr_writer", "attr_accessor"].freeze, T::Array[String]) class SymbolHierarchyRoot + extend T::Sig + + sig { returns(T::Array[LanguageServer::Protocol::Interface::DocumentSymbol]) } attr_reader :children + sig { void } def initialize - @children = [] + @children = T.let([], T::Array[LanguageServer::Protocol::Interface::DocumentSymbol]) end end + sig { params(document: Document).void } def initialize(document) super - @root = SymbolHierarchyRoot.new - @stack = [@root] + @root = T.let(SymbolHierarchyRoot.new, SymbolHierarchyRoot) + @stack = T.let( + [@root], + T::Array[T.any(SymbolHierarchyRoot, LanguageServer::Protocol::Interface::DocumentSymbol)] + ) end + sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::DocumentSymbol], Object)) } def run visit(@document.tree) @root.children end + sig { params(node: SyntaxTree::ClassDeclaration).void } def visit_class(node) symbol = create_document_symbol( name: node.constant.constant.value, @@ -89,6 +101,7 @@ def visit_class(node) @stack.pop end + sig { params(node: SyntaxTree::Command).void } def visit_command(node) return unless ATTR_ACCESSORS.include?(node.message.value) @@ -104,6 +117,7 @@ def visit_command(node) end end + sig { params(node: SyntaxTree::ConstPathField).void } def visit_const_path_field(node) create_document_symbol( name: node.constant.value, @@ -113,6 +127,7 @@ def visit_const_path_field(node) ) end + sig { params(node: SyntaxTree::Def).void } def visit_def(node) name = node.name.value @@ -128,6 +143,7 @@ def visit_def(node) @stack.pop end + sig { params(node: SyntaxTree::DefEndless).void } def visit_def_endless(node) name = node.name.value @@ -143,6 +159,7 @@ def visit_def_endless(node) @stack.pop end + sig { params(node: SyntaxTree::Defs).void } def visit_defs(node) symbol = create_document_symbol( name: "self.#{node.name.value}", @@ -156,6 +173,7 @@ def visit_defs(node) @stack.pop end + sig { params(node: SyntaxTree::ModuleDeclaration).void } def visit_module(node) symbol = create_document_symbol( name: node.constant.constant.value, @@ -169,6 +187,7 @@ def visit_module(node) @stack.pop end + sig { params(node: SyntaxTree::TopConstField).void } def visit_top_const_field(node) create_document_symbol( name: node.constant.value, @@ -178,6 +197,7 @@ def visit_top_const_field(node) ) end + sig { params(node: SyntaxTree::VarField).void } def visit_var_field(node) kind = case node.value when SyntaxTree::Const @@ -198,6 +218,14 @@ def visit_var_field(node) private + sig do + params( + name: String, + kind: Symbol, + range_node: SyntaxTree::Node, + selection_range_node: SyntaxTree::Node + ).returns(LanguageServer::Protocol::Interface::DocumentSymbol) + end def create_document_symbol(name:, kind:, range_node:, selection_range_node:) symbol = LanguageServer::Protocol::Interface::DocumentSymbol.new( name: name, @@ -207,7 +235,7 @@ def create_document_symbol(name:, kind:, range_node:, selection_range_node:) children: [], ) - @stack.last.children << symbol + T.must(@stack.last).children << symbol symbol end diff --git a/lib/ruby_lsp/requests/folding_ranges.rb b/lib/ruby_lsp/requests/folding_ranges.rb index aaff00372..024116685 100644 --- a/lib/ruby_lsp/requests/folding_ranges.rb +++ b/lib/ruby_lsp/requests/folding_ranges.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp @@ -13,7 +13,9 @@ module Requests # end # <-- folding range end # ``` class FoldingRanges < BaseRequest - SIMPLE_FOLDABLES = [ + extend T::Sig + + SIMPLE_FOLDABLES = T.let([ SyntaxTree::ArrayLiteral, SyntaxTree::BraceBlock, SyntaxTree::Case, @@ -30,24 +32,37 @@ class FoldingRanges < BaseRequest SyntaxTree::Unless, SyntaxTree::Until, SyntaxTree::While, - ].freeze + ].freeze, T::Array[T.class_of(SyntaxTree::Node)]) - NODES_WITH_STATEMENTS = [ + NODES_WITH_STATEMENTS = T.let([ SyntaxTree::Else, SyntaxTree::Elsif, SyntaxTree::Ensure, SyntaxTree::In, SyntaxTree::Rescue, SyntaxTree::When, - ].freeze + ].freeze, T::Array[T.class_of(SyntaxTree::Node)]) + + StatementNode = T.type_alias do + T.any( + SyntaxTree::Else, + SyntaxTree::Elsif, + SyntaxTree::Ensure, + SyntaxTree::In, + SyntaxTree::Rescue, + SyntaxTree::When, + ) + end + sig { params(document: Document).void } def initialize(document) super - @ranges = [] - @partial_range = nil + @ranges = T.let([], T::Array[LanguageServer::Protocol::Interface::FoldingRange]) + @partial_range = T.let(nil, T.nilable(PartialRange)) end + sig { override.returns(T.all(T::Array[LanguageServer::Protocol::Interface::FoldingRange], Object)) } def run visit(@document.tree) emit_partial_range @@ -56,14 +71,15 @@ def run private + sig { params(node: T.nilable(SyntaxTree::Node)).void } def visit(node) return unless handle_partial_range(node) case node when *SIMPLE_FOLDABLES - add_node_range(node) + add_node_range(T.must(node)) when *NODES_WITH_STATEMENTS - add_statements_range(node, node.statements) + add_statements_range(T.must(node), T.cast(node, StatementNode).statements) when SyntaxTree::Begin add_statements_range(node, node.bodystmt.statements) when SyntaxTree::Call, SyntaxTree::CommandCall @@ -80,27 +96,38 @@ def visit(node) end class PartialRange - attr_reader :kind, :end_line + extend T::Sig + sig { returns(String) } + attr_reader :kind + + sig { returns(Integer) } + attr_reader :end_line + + sig { params(node: SyntaxTree::Node, kind: String).returns(PartialRange) } def self.from(node, kind) new(node.location.start_line - 1, node.location.end_line - 1, kind) end + sig { params(start_line: Integer, end_line: Integer, kind: String).void } def initialize(start_line, end_line, kind) @start_line = start_line @end_line = end_line @kind = kind end + sig { params(node: SyntaxTree::Node).returns(PartialRange) } def extend_to(node) @end_line = node.location.end_line - 1 self end + sig { params(node: SyntaxTree::Node).returns(T::Boolean) } def new_section?(node) node.is_a?(SyntaxTree::Comment) && @end_line + 1 != node.location.start_line - 1 end + sig { returns(LanguageServer::Protocol::Interface::FoldingRange) } def to_range LanguageServer::Protocol::Interface::FoldingRange.new( start_line: @start_line, @@ -110,6 +137,7 @@ def to_range end end + sig { params(node: T.nilable(SyntaxTree::Node)).returns(T::Boolean) } def handle_partial_range(node) kind = partial_range_kind(node) @@ -118,18 +146,20 @@ def handle_partial_range(node) return true end + target_node = T.must(node) @partial_range = if @partial_range.nil? - PartialRange.from(node, kind) - elsif @partial_range.kind != kind || @partial_range.new_section?(node) + PartialRange.from(target_node, kind) + elsif @partial_range.kind != kind || @partial_range.new_section?(target_node) emit_partial_range - PartialRange.from(node, kind) + PartialRange.from(target_node, kind) else - @partial_range.extend_to(node) + @partial_range.extend_to(target_node) end false end + sig { params(node: T.nilable(SyntaxTree::Node)).returns(T.nilable(String)) } def partial_range_kind(node) case node when SyntaxTree::Comment @@ -141,6 +171,7 @@ def partial_range_kind(node) end end + sig { void } def emit_partial_range return if @partial_range.nil? @@ -148,6 +179,7 @@ def emit_partial_range @partial_range = nil end + sig { params(node: T.any(SyntaxTree::Call, SyntaxTree::CommandCall)).void } def add_call_range(node) receiver = T.let(node.receiver, SyntaxTree::Node) loop do @@ -168,6 +200,7 @@ def add_call_range(node) visit(node.arguments) end + sig { params(node: T.any(SyntaxTree::Def, SyntaxTree::Defs)).void } def add_def_range(node) params_location = node.params.location @@ -180,10 +213,12 @@ def add_def_range(node) visit(node.bodystmt.statements) end + sig { params(node: SyntaxTree::Node, statements: SyntaxTree::Statements).void } def add_statements_range(node, statements) add_lines_range(node.location.start_line, statements.location.end_line) unless statements.empty? end + sig { params(node: SyntaxTree::StringConcat).void } def add_string_concat(node) left = T.let(node.left, SyntaxTree::Node) left = left.left while left.is_a?(SyntaxTree::StringConcat) @@ -191,14 +226,13 @@ def add_string_concat(node) add_lines_range(left.location.start_line, node.right.location.end_line) end + sig { params(node: SyntaxTree::Node).void } def add_node_range(node) - add_location_range(node.location) - end - - def add_location_range(location) + location = node.location add_lines_range(location.start_line, location.end_line) end + sig { params(start_line: Integer, end_line: Integer).void } def add_lines_range(start_line, end_line) return if start_line >= end_line diff --git a/lib/ruby_lsp/requests/formatting.rb b/lib/ruby_lsp/requests/formatting.rb index f9ce59064..70a278495 100644 --- a/lib/ruby_lsp/requests/formatting.rb +++ b/lib/ruby_lsp/requests/formatting.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp @@ -15,13 +15,17 @@ module Requests # end # ``` class Formatting < RuboCopRequest - RUBOCOP_FLAGS = (COMMON_RUBOCOP_FLAGS + ["--autocorrect"]).freeze + extend T::Sig + RUBOCOP_FLAGS = T.let((COMMON_RUBOCOP_FLAGS + ["--autocorrect"]).freeze, T::Array[String]) + + sig { params(uri: String, document: Document).void } def initialize(uri, document) super - @formatted_text = nil + @formatted_text = T.let(nil, T.nilable(String)) end + sig { override.returns(T.nilable(T.all(T::Array[LanguageServer::Protocol::Interface::TextEdit], Object))) } def run super @@ -44,6 +48,7 @@ def run private + sig { returns(T::Array[String]) } def rubocop_flags RUBOCOP_FLAGS end diff --git a/lib/ruby_lsp/requests/rubocop_request.rb b/lib/ruby_lsp/requests/rubocop_request.rb index 8debab674..9a75626b8 100644 --- a/lib/ruby_lsp/requests/rubocop_request.rb +++ b/lib/ruby_lsp/requests/rubocop_request.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "rubocop" @@ -8,23 +8,31 @@ module RubyLsp module Requests # :nodoc: class RuboCopRequest < RuboCop::Runner - COMMON_RUBOCOP_FLAGS = [ + extend T::Sig + extend T::Helpers + + abstract! + + COMMON_RUBOCOP_FLAGS = T.let([ "--stderr", # Print any output to stderr so that our stdout does not get polluted "--format", "RuboCop::Formatter::BaseFormatter", # Suppress any output by using the base formatter - ].freeze + ].freeze, T::Array[String]) - attr_reader :file, :text + sig { returns(String) } + attr_reader :file - def self.run(uri, document) - new(uri, document).run - end + sig { returns(String) } + attr_reader :text + sig { params(uri: String, document: Document).void } def initialize(uri, document) - @file = CGI.unescape(URI.parse(uri).path) + @file = T.let(CGI.unescape(URI.parse(uri).path), String) @document = document - @text = document.source + @text = T.let(document.source, String) @uri = uri + @options = T.let({}, T::Hash[Symbol, T.untyped]) + @diagnostics = T.let([], T::Array[Support::RuboCopDiagnostic]) super( ::RuboCop::Options.new.parse(rubocop_flags).first, @@ -32,6 +40,7 @@ def initialize(uri, document) ) end + sig { overridable.returns(Object) } def run # We communicate with Rubocop via stdin @options[:stdin] = text @@ -42,6 +51,7 @@ def run private + sig { returns(T::Array[String]) } def rubocop_flags COMMON_RUBOCOP_FLAGS end diff --git a/lib/ruby_lsp/requests/selection_ranges.rb b/lib/ruby_lsp/requests/selection_ranges.rb index c0cdafe9e..7524a56b6 100644 --- a/lib/ruby_lsp/requests/selection_ranges.rb +++ b/lib/ruby_lsp/requests/selection_ranges.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp @@ -17,7 +17,9 @@ module Requests # end # ``` class SelectionRanges < BaseRequest - NODES_THAT_CAN_BE_PARENTS = [ + extend T::Sig + + NODES_THAT_CAN_BE_PARENTS = T.let([ SyntaxTree::Assign, SyntaxTree::ArrayLiteral, SyntaxTree::Begin, @@ -54,15 +56,17 @@ class SelectionRanges < BaseRequest SyntaxTree::VCall, SyntaxTree::When, SyntaxTree::While, - ].freeze + ].freeze, T::Array[T.class_of(SyntaxTree::Node)]) + sig { params(document: Document).void } def initialize(document) super(document) - @ranges = [] - @stack = [] + @ranges = T.let([], T::Array[Support::SelectionRange]) + @stack = T.let([], T::Array[Support::SelectionRange]) end + sig { override.returns(T.all(T::Array[Support::SelectionRange], Object)) } def run visit(@document.tree) @ranges.reverse! @@ -70,6 +74,7 @@ def run private + sig { params(node: T.nilable(SyntaxTree::Node)).void } def visit(node) return if node.nil? @@ -83,6 +88,12 @@ def visit(node) @stack.pop if NODES_THAT_CAN_BE_PARENTS.include?(node.class) end + sig do + params( + location: SyntaxTree::Location, + parent: T.nilable(Support::SelectionRange) + ).returns(Support::SelectionRange) + end def create_selection_range(location, parent = nil) RubyLsp::Requests::Support::SelectionRange.new( range: LanguageServer::Protocol::Interface::Range.new( diff --git a/lib/ruby_lsp/requests/semantic_highlighting.rb b/lib/ruby_lsp/requests/semantic_highlighting.rb index 72852ea5b..083392de2 100644 --- a/lib/ruby_lsp/requests/semantic_highlighting.rb +++ b/lib/ruby_lsp/requests/semantic_highlighting.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp @@ -17,12 +17,14 @@ module Requests # end # ``` class SemanticHighlighting < BaseRequest - TOKEN_TYPES = [ + extend T::Sig + + TOKEN_TYPES = T.let([ :variable, :method, - ].freeze + ].freeze, T::Array[Symbol]) - TOKEN_MODIFIERS = { + TOKEN_MODIFIERS = T.let({ declaration: 0, definition: 1, readonly: 2, @@ -33,18 +35,32 @@ class SemanticHighlighting < BaseRequest modification: 7, documentation: 8, default_library: 9, - }.freeze + }.freeze, T::Hash[Symbol, Integer]) - SemanticToken = Struct.new(:location, :length, :type, :modifier) + class SemanticToken < T::Struct + const :location, SyntaxTree::Location + const :length, Integer + const :type, Integer + const :modifier, T::Array[Integer] + end + sig { params(document: Document, encoder: T.nilable(Support::SemanticTokenEncoder)).void } def initialize(document, encoder: nil) super(document) @encoder = encoder - @tokens = [] - @tree = document.tree + @tokens = T.let([], T::Array[SemanticToken]) + @tree = T.let(document.tree, SyntaxTree::Node) end + sig do + override.returns( + T.any( + LanguageServer::Protocol::Interface::SemanticTokens, + T.all(T::Array[SemanticToken], Object), + ) + ) + end def run visit(@tree) return @tokens unless @encoder @@ -52,12 +68,14 @@ def run @encoder.encode(@tokens) end + sig { params(node: SyntaxTree::Def).void } def visit_def(node) add_token(node.name.location, :method, [:declaration]) visit(node.params) visit(node.bodystmt) end + sig { params(node: SyntaxTree::DefEndless).void } def visit_def_endless(node) add_token(node.name.location, :method, [:declaration]) visit(node.paren) @@ -65,6 +83,7 @@ def visit_def_endless(node) visit(node.statement) end + sig { params(node: SyntaxTree::Defs).void } def visit_defs(node) visit(node.target) visit(node.operator) @@ -73,12 +92,14 @@ def visit_defs(node) visit(node.bodystmt) end + sig { params(node: SyntaxTree::MAssign).void } def visit_m_assign(node) node.target.parts.each do |var_ref| add_token(var_ref.value.location, :variable) end end + sig { params(node: SyntaxTree::VarField).void } def visit_var_field(node) case node.value when SyntaxTree::Ident @@ -86,6 +107,7 @@ def visit_var_field(node) end end + sig { params(node: SyntaxTree::VarRef).void } def visit_var_ref(node) case node.value when SyntaxTree::Ident @@ -93,40 +115,54 @@ def visit_var_ref(node) end end + sig { params(node: SyntaxTree::ARefField).void } def visit_a_ref_field(node) add_token(node.collection.value.location, :variable) end + sig { params(node: SyntaxTree::Call).void } def visit_call(node) visit(node.receiver) add_token(node.message.location, :method) visit(node.arguments) end + sig { params(node: SyntaxTree::Command).void } def visit_command(node) add_token(node.message.location, :method) visit(node.arguments) end + sig { params(node: SyntaxTree::CommandCall).void } def visit_command_call(node) visit(node.receiver) add_token(node.message.location, :method) visit(node.arguments) end + sig { params(node: SyntaxTree::FCall).void } def visit_fcall(node) add_token(node.value.location, :method) visit(node.arguments) end + sig { params(node: SyntaxTree::VCall).void } def visit_vcall(node) add_token(node.value.location, :method) end + sig { params(location: SyntaxTree::Location, type: Symbol, modifiers: T::Array[Symbol]).void } def add_token(location, type, modifiers = []) length = location.end_char - location.start_char modifiers_indices = modifiers.filter_map { |modifier| TOKEN_MODIFIERS[modifier] } - @tokens.push(SemanticToken.new(location, length, TOKEN_TYPES.index(type), modifiers_indices)) + @tokens.push( + SemanticToken.new( + location: location, + length: length, + type: T.must(TOKEN_TYPES.index(type)), + modifier: modifiers_indices + ) + ) end end end diff --git a/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb b/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb index 21f40ebdf..7c5c4be78 100644 --- a/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +++ b/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb @@ -1,35 +1,45 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp module Requests module Support class RuboCopDiagnostic - RUBOCOP_TO_LSP_SEVERITY = { + extend T::Sig + + RUBOCOP_TO_LSP_SEVERITY = T.let({ convention: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION, info: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION, refactor: LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION, warning: LanguageServer::Protocol::Constant::DiagnosticSeverity::WARNING, error: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR, fatal: LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR, - }.freeze + }.freeze, T::Hash[Symbol, Integer]) + sig { returns(T::Array[LanguageServer::Protocol::Interface::TextEdit]) } attr_reader :replacements + sig { params(offense: RuboCop::Cop::Offense, uri: String).void } def initialize(offense, uri) @offense = offense @uri = uri - @replacements = offense.correctable? ? offense_replacements : [] + @replacements = T.let( + offense.correctable? ? offense_replacements : [], + T::Array[LanguageServer::Protocol::Interface::TextEdit] + ) end + sig { returns(T::Boolean) } def correctable? @offense.correctable? end + sig { params(range: T::Range[Integer]).returns(T::Boolean) } def in_range?(range) range.cover?(@offense.line - 1) end + sig { returns(LanguageServer::Protocol::Interface::CodeAction) } def to_lsp_code_action LanguageServer::Protocol::Interface::CodeAction.new( title: "Autocorrect #{@offense.cop_name}", @@ -49,6 +59,7 @@ def to_lsp_code_action ) end + sig { returns(LanguageServer::Protocol::Interface::Diagnostic) } def to_lsp_diagnostic LanguageServer::Protocol::Interface::Diagnostic.new( message: @offense.message, @@ -70,6 +81,7 @@ def to_lsp_diagnostic private + sig { returns(T::Array[LanguageServer::Protocol::Interface::TextEdit]) } def offense_replacements @offense.corrector.as_replacements.map do |range, replacement| LanguageServer::Protocol::Interface::TextEdit.new( diff --git a/lib/ruby_lsp/requests/support/selection_range.rb b/lib/ruby_lsp/requests/support/selection_range.rb index cad4235d3..832dd4e02 100644 --- a/lib/ruby_lsp/requests/support/selection_range.rb +++ b/lib/ruby_lsp/requests/support/selection_range.rb @@ -1,10 +1,13 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp module Requests module Support class SelectionRange < LanguageServer::Protocol::Interface::SelectionRange + extend T::Sig + + sig { params(position: Document::PositionShape).returns(T::Boolean) } def cover?(position) line_range = (range.start.line..range.end.line) character_range = (range.start.character..range.end.character) diff --git a/lib/ruby_lsp/requests/support/semantic_token_encoder.rb b/lib/ruby_lsp/requests/support/semantic_token_encoder.rb index 1bd290fe8..a9c06428e 100644 --- a/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +++ b/lib/ruby_lsp/requests/support/semantic_token_encoder.rb @@ -1,15 +1,23 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp module Requests module Support class SemanticTokenEncoder + extend T::Sig + + sig { void } def initialize - @current_row = 0 - @current_column = 0 + @current_row = T.let(0, Integer) + @current_column = T.let(0, Integer) end + sig do + params( + tokens: T::Array[SemanticHighlighting::SemanticToken] + ).returns(LanguageServer::Protocol::Interface::SemanticTokens) + end def encode(tokens) delta = tokens .sort_by do |token| @@ -31,6 +39,7 @@ def encode(tokens) # For more information on how each number is calculated, read: # https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens + sig { params(token: SemanticHighlighting::SemanticToken).returns(T::Array[Integer]) } def compute_delta(token) row = token.location.start_line - 1 column = token.location.start_column @@ -49,6 +58,7 @@ def compute_delta(token) # For example, [:default_library] will be encoded as # 0b1000000000, as :default_library is the 10th bit according # to the token modifiers index map. + sig { params(modifiers: T::Array[Integer]).returns(Integer) } def encode_modifiers(modifiers) modifiers.inject(0) do |encoded_modifiers, modifier| encoded_modifiers | (1 << modifier) diff --git a/lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb b/lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb index 71272da53..76baffe6d 100644 --- a/lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb +++ b/lib/ruby_lsp/requests/support/syntax_error_diagnostic.rb @@ -1,18 +1,23 @@ -# typed: true +# typed: strict # frozen_string_literal: true module RubyLsp module Requests module Support class SyntaxErrorDiagnostic + extend T::Sig + + sig { params(edit: Document::EditShape).void } def initialize(edit) @edit = edit end + sig { returns(FalseClass) } def correctable? false end + sig { returns(LanguageServer::Protocol::Interface::Diagnostic) } def to_lsp_diagnostic LanguageServer::Protocol::Interface::Diagnostic.new( message: "Syntax error", diff --git a/lib/ruby_lsp/store.rb b/lib/ruby_lsp/store.rb index 51e1ed62f..8e219678b 100644 --- a/lib/ruby_lsp/store.rb +++ b/lib/ruby_lsp/store.rb @@ -46,11 +46,12 @@ def delete(uri) end sig do - params( - uri: String, - request_name: Symbol, - block: T.proc.params(document: Document).returns(T.untyped) - ).returns(T.untyped) + type_parameters(:T) + .params( + uri: String, + request_name: Symbol, + block: T.proc.params(document: Document).returns(T.type_parameter(:T)) + ).returns(T.type_parameter(:T)) end def cache_fetch(uri, request_name, &block) get(uri).cache_fetch(request_name, &block) diff --git a/rakelib/check_docs.rake b/rakelib/check_docs.rake index 8724c3b3e..67e95ed9e 100644 --- a/rakelib/check_docs.rake +++ b/rakelib/check_docs.rake @@ -3,6 +3,7 @@ desc "Check if all LSP requests are documented" task :check_docs do + require "sorbet-runtime" require "language_server-protocol" require "syntax_tree" require "logger" diff --git a/sorbet/tapioca/require.rb b/sorbet/tapioca/require.rb index 0ecce3992..f4173a01c 100644 --- a/sorbet/tapioca/require.rb +++ b/sorbet/tapioca/require.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true # Add your extra requires here (`bin/tapioca require` can be used to boostrap this list) diff --git a/test/expectations/expectations_test_runner.rb b/test/expectations/expectations_test_runner.rb index c7d6ade7a..1eb04079a 100644 --- a/test/expectations/expectations_test_runner.rb +++ b/test/expectations/expectations_test_runner.rb @@ -14,7 +14,7 @@ def expectations_tests(handler_class, expectation_suffix) module ExpectationsRunnerMethods def run_expectations(source) document = RubyLsp::Document.new(source) - #{handler_class}.run(document) + #{handler_class}.new(document).run end def assert_expectations(source, expected) diff --git a/test/requests/code_actions_test.rb b/test/requests/code_actions_test.rb index 87fc492e4..777e18b85 100644 --- a/test/requests/code_actions_test.rb +++ b/test/requests/code_actions_test.rb @@ -31,10 +31,10 @@ def foo def assert_code_actions(source, code_actions, range) document = RubyLsp::Document.new(source) - result = T.let(nil, T.nilable(T::Array[LanguageServer::Protocol::Interface::Diagnostic])) + result = T.let(nil, T.nilable(T::Array[LanguageServer::Protocol::Interface::CodeAction])) stdout, _ = capture_io do - result = RubyLsp::Requests::CodeActions.run("file://#{__FILE__}", document, range) + result = RubyLsp::Requests::CodeActions.new("file://#{__FILE__}", document, range).run end assert_empty(stdout) diff --git a/test/requests/diagnostics_expectations_test.rb b/test/requests/diagnostics_expectations_test.rb index d6bf920e5..497d549c6 100644 --- a/test/requests/diagnostics_expectations_test.rb +++ b/test/requests/diagnostics_expectations_test.rb @@ -9,7 +9,7 @@ class DiagnosticsExpectationsTest < ExpectationsTestRunner def run_expectations(source) document = RubyLsp::Document.new(source) - RubyLsp::Requests::Diagnostics.run("file://#{__FILE__}", document) + RubyLsp::Requests::Diagnostics.new("file://#{__FILE__}", document).run end def assert_expectations(source, expected) diff --git a/test/requests/diagnostics_test.rb b/test/requests/diagnostics_test.rb index dc9fdfcb5..4124550ab 100644 --- a/test/requests/diagnostics_test.rb +++ b/test/requests/diagnostics_test.rb @@ -17,7 +17,7 @@ class Foo document.push_edits([error_edit]) - result = RubyLsp::Requests::Diagnostics.run("file://#{__FILE__}", document) + result = RubyLsp::Requests::Diagnostics.new("file://#{__FILE__}", document).run assert_equal(syntax_error_diagnostics([error_edit]).to_json, result.map(&:to_lsp_diagnostic).to_json) end diff --git a/test/requests/document_highlight_test.rb b/test/requests/document_highlight_test.rb index 9ed0ad117..e9b39c0ec 100644 --- a/test/requests/document_highlight_test.rb +++ b/test/requests/document_highlight_test.rb @@ -130,7 +130,7 @@ def foo def assert_highlight(source, position, expected) document = RubyLsp::Document.new(source) - actual = RubyLsp::Requests::DocumentHighlight.run(document, position) + actual = RubyLsp::Requests::DocumentHighlight.new(document, position).run ranges = JSON.parse(actual.to_json, symbolize_names: true) assert_equal(expected.count, ranges.count) diff --git a/test/requests/document_symbol_expectations_test.rb b/test/requests/document_symbol_expectations_test.rb index 95d06ba73..015afdc16 100644 --- a/test/requests/document_symbol_expectations_test.rb +++ b/test/requests/document_symbol_expectations_test.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "test_helper" diff --git a/test/requests/folding_ranges_expectations_test.rb b/test/requests/folding_ranges_expectations_test.rb index 1af5af2fd..87ecf8609 100644 --- a/test/requests/folding_ranges_expectations_test.rb +++ b/test/requests/folding_ranges_expectations_test.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "test_helper" diff --git a/test/requests/formatting_expectations_test.rb b/test/requests/formatting_expectations_test.rb index 3c5bce624..eb12b2692 100644 --- a/test/requests/formatting_expectations_test.rb +++ b/test/requests/formatting_expectations_test.rb @@ -9,7 +9,7 @@ class FormattingExpectationsTest < ExpectationsTestRunner def run_expectations(source) document = RubyLsp::Document.new(source) - RubyLsp::Requests::Formatting.run("file://#{__FILE__}", document).first.new_text + RubyLsp::Requests::Formatting.new("file://#{__FILE__}", document).run&.first&.new_text end def assert_expectations(source, expected) diff --git a/test/requests/selection_ranges_test.rb b/test/requests/selection_ranges_test.rb index 00f5994bc..fc36c87f8 100644 --- a/test/requests/selection_ranges_test.rb +++ b/test/requests/selection_ranges_test.rb @@ -1175,7 +1175,7 @@ def test_selecting_pattern_matching def assert_ranges(source, positions, expected_ranges) document = RubyLsp::Document.new(source) - actual = RubyLsp::Requests::SelectionRanges.run(document) + actual = RubyLsp::Requests::SelectionRanges.new(document).run filtered = positions.map { |position| actual.find { |range| range.cover?(position) } } assert_equal(expected_ranges, JSON.parse(filtered.to_json, symbolize_names: true)) diff --git a/test/requests/support/semantic_token_encoder_test.rb b/test/requests/support/semantic_token_encoder_test.rb index 19e0c30f3..800d6313d 100644 --- a/test/requests/support/semantic_token_encoder_test.rb +++ b/test/requests/support/semantic_token_encoder_test.rb @@ -4,15 +4,12 @@ require "test_helper" class SemanticTokenEncoderTest < Minitest::Test - TokenStub = RubyLsp::Requests::SemanticHighlighting::SemanticToken - LocationStub = Struct.new(:start_line, :start_column) - def test_tokens_encoded_to_relative_positioning tokens = [ - TokenStub.new(LocationStub.new(1, 2), 1, 0, [0]), - TokenStub.new(LocationStub.new(1, 4), 2, 9, [0]), - TokenStub.new(LocationStub.new(2, 2), 3, 0, [6]), - TokenStub.new(LocationStub.new(5, 6), 10, 4, [4]), + stub_token(1, 2, 1, 0, [0]), + stub_token(1, 4, 2, 9, [0]), + stub_token(2, 2, 3, 0, [6]), + stub_token(5, 6, 10, 4, [4]), ] expected_encoding = [ @@ -28,10 +25,10 @@ def test_tokens_encoded_to_relative_positioning def test_tokens_sorted_before_encoded tokens = [ - TokenStub.new(LocationStub.new(1, 2), 1, 0, [0]), - TokenStub.new(LocationStub.new(5, 6), 10, 4, [4]), - TokenStub.new(LocationStub.new(2, 2), 3, 0, [6]), - TokenStub.new(LocationStub.new(1, 4), 2, 9, [0]), + stub_token(1, 2, 1, 0, [0]), + stub_token(5, 6, 10, 4, [4]), + stub_token(2, 2, 3, 0, [6]), + stub_token(1, 4, 2, 9, [0]), ] expected_encoding = [ @@ -59,4 +56,22 @@ def test_encoded_modifiers_with_some_modifiers bit_flag = RubyLsp::Requests::Support::SemanticTokenEncoder.new.encode_modifiers([1, 3, 9, 7, 5]) assert_equal(0b1010101010, bit_flag) end + + private + + def stub_token(start_line, start_column, length, type, modifier) + RubyLsp::Requests::SemanticHighlighting::SemanticToken.new( + location: SyntaxTree::Location.new( + start_line: start_line, + start_column: start_column, + start_char: 0, + end_char: 0, + end_column: 0, + end_line: 0, + ), + length: length, + type: type, + modifier: modifier + ) + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 2bb29bda0..f4bfce834 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))