diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml index 4556215f1..3de7288eb 100644 --- a/.github/workflows/plugins.yml +++ b/.github/workflows/plugins.yml @@ -118,9 +118,12 @@ jobs: - name: clone https://github.com/lekemula/solargraph-rspec/ run: | cd .. - git clone https://github.com/lekemula/solargraph-rspec.git + # git clone https://github.com/lekemula/solargraph-rspec.git + # pending https://github.com/lekemula/solargraph-rspec/pull/31 + git clone https://github.com/apiology/solargraph-rspec.git cd solargraph-rspec + git checkout test_solargraph_prereleases - name: Set up Ruby uses: ruby/setup-ruby@v1 with: diff --git a/.gitignore b/.gitignore index 2819165b1..75510d96b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ coverage /Makefile /.pryrc /.rspec-local +vendor/cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 315ba0c73..e07381830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +## 0.58.2 - January 19, 2026 +- Avoid rbs pollution (#1146) +- Fix 'solargraph pin --references ClassName' private method call (#1150) +- Improve memory efficiency of Position class (#1054) +- Raise InvalidOffsetError for offsets > text (#1155) + ## 0.58.1 - January 2, 2026 -- Normalize line endings to LF (#1142) +- Normalize line endings to LF (#1142) ## 0.58.0 - January 1, 2026 - Faster constant resolution (#1083) diff --git a/lib/solargraph/pin/reference.rb b/lib/solargraph/pin/reference.rb index 13e603d6e..4ad64fcb8 100644 --- a/lib/solargraph/pin/reference.rb +++ b/lib/solargraph/pin/reference.rb @@ -12,6 +12,22 @@ class Reference < Base attr_reader :generic_values + # A Reference is a pin that associates a type with another type. + # The existing type is marked as the closure. The name of the + # type we're associating with it is the 'name' field, and + # subtypes are in the 'generic_values' field. + # + # These pins are a little different - the name is a rooted name, + # which may be relative or absolute, preceded with ::, not a + # fully qualified namespace, which is implicitly in the root + # namespace and is never preceded by ::. + # + # @todo can the above be represented in a less subtle way? + # @todo consider refactoring so that we can replicate more + # complex types like Hash{String => Integer} and has both key + # types and subtypes. + # + # @param name [String] rooted name of the referenced type # @param generic_values [Array] def initialize generic_values: [], **splat super(**splat) diff --git a/lib/solargraph/position.rb b/lib/solargraph/position.rb index 53c7b61ba..332ffa527 100644 --- a/lib/solargraph/position.rb +++ b/lib/solargraph/position.rb @@ -57,7 +57,24 @@ def inspect # @return [Integer] def self.to_offset text, position return 0 if text.empty? - text.lines[0...position.line].sum(&:length) + position.character + + newline_index = -1 + line = -1 + last_line_index = 0 + + # @sg-ignore flow sensitive typing should be able to handle redefinition + while (newline_index = text.index("\n", newline_index + 1)) && line <= position.line + line += 1 + break if line == position.line + + # @sg-ignore oflow sensitive typing should be able to handle redefinition + line_length = newline_index - last_line_index + last_line_index = newline_index + end + + last_line_index += 1 if position.line > 0 + # @sg-ignore flow sensitive typing should be able to handle redefinition + last_line_index + position.character end # Get a numeric offset for the specified text and a position identified @@ -73,23 +90,24 @@ def self.line_char_to_offset text, line, character # Get a position for the specified text and offset. # + # @raise [InvalidOffsetError] if the offset is outside the text range + # # @param text [String] # @param offset [Integer] # @return [Position] def self.from_offset text, offset + raise InvalidOffsetError if offset > text.length + cursor = 0 line = 0 - # @type [Integer, nil] - character = nil - text.lines.each do |l| - line_length = l.length - char_length = l.chomp.length - if cursor + char_length >= offset - character = offset - cursor - break - end - cursor += line_length + character = offset + newline_index = -1 + + # @sg-ignore flow sensitive typing should be able to handle redefinition + while (newline_index = text.index("\n", newline_index + 1)) && newline_index < offset line += 1 + # @sg-ignore flow sensitive typing should be able to handle redefinition + character = offset - newline_index - 1 end character = 0 if character.nil? and (cursor - offset).between?(0, 1) raise InvalidOffsetError if character.nil? diff --git a/lib/solargraph/rbs_map/conversions.rb b/lib/solargraph/rbs_map/conversions.rb index b958895fb..0182a11ea 100644 --- a/lib/solargraph/rbs_map/conversions.rb +++ b/lib/solargraph/rbs_map/conversions.rb @@ -23,7 +23,7 @@ def initialize visibility = :public end # @param loader [RBS::EnvironmentLoader] - def initialize(loader:) + def initialize loader: @loader = loader @pins = [] load_environment_to_pins(loader) @@ -43,12 +43,14 @@ def type_aliases end # @param loader [RBS::EnvironmentLoader] + # # @return [void] - def load_environment_to_pins(loader) + def load_environment_to_pins loader environment = RBS::Environment.from_loader(loader).resolve_type_names - cursor = pins.length if environment.declarations.empty? - Solargraph.logger.info "No RBS declarations found in environment for core_root #{loader.core_root.inspect}, libraries #{loader.libs} and directories #{loader.dirs}" + Solargraph.logger.info 'No RBS declarations found in environment for core_root ' \ + "#{loader.core_root.inspect}, libraries #{loader.libs} and " \ + "directories #{loader.dirs}" return end environment.declarations.each { |decl| convert_decl_to_pin(decl, Solargraph::Pin::ROOT_PIN) } @@ -60,22 +62,54 @@ def load_environment_to_pins(loader) def convert_decl_to_pin decl, closure case decl when RBS::AST::Declarations::Class + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on class #{decl.inspect}") + end class_decl_to_pin decl when RBS::AST::Declarations::Interface + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on interface #{decl.inspect}") + end # STDERR.puts "Skipping interface #{decl.name.relative!}" - interface_decl_to_pin decl, closure + interface_decl_to_pin decl when RBS::AST::Declarations::TypeAlias + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + # @sg-ignore flow sensitive typing should support case/when + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on alias type name #{decl.name.to_s}") + end # @sg-ignore flow sensitive typing should support case/when type_aliases[decl.name.to_s] = decl when RBS::AST::Declarations::Module + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + # @sg-ignore flow sensitive typing should support case/when + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on alias type name #{decl.name.to_s}") + end module_decl_to_pin decl when RBS::AST::Declarations::Constant + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.name.absolute? + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on constant #{decl.inspect}") + end constant_decl_to_pin decl when RBS::AST::Declarations::ClassAlias + # @sg-ignore flow sensitive typing should support case/when + unless closure.name == '' || decl.new_name.absolute? + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on class alias #{decl.inspect}") + end class_alias_decl_to_pin decl when RBS::AST::Declarations::ModuleAlias + unless closure.name == '' + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on module alias #{decl.inspect}") + end module_alias_decl_to_pin decl when RBS::AST::Declarations::Global + unless closure.name == '' + Solargraph.assert_or_log(:rbs_closure, "Ignoring closure #{closure.inspect} on global decl #{decl.inspect}") + end global_decl_to_pin decl else Solargraph.logger.warn "Skipping declaration #{decl.class}" @@ -86,7 +120,59 @@ def convert_decl_to_pin decl, closure # @param module_pin [Pin::Namespace] # @return [void] def convert_self_types_to_pins decl, module_pin - decl.self_types.each { |self_type| context = convert_self_type_to_pins(self_type, module_pin) } + decl.self_types.each { |self_type| convert_self_type_to_pins(self_type, module_pin) } + end + + # @type [Hash{String => String}] + RBS_TO_CLASS = { + 'bool' => 'Boolean', + 'string' => 'String', + 'int' => 'Integer', + }.freeze + private_constant :RBS_TO_CLASS + + # rooted names (namespaces) use the prefix of :: when they are + # relative to the root namespace, or not if they are relative to + # the current namespace. + # + # @param type_name [RBS::TypeName] + # + # @return [String] + def rooted_name(type_name) + name = type_name.to_s + RBS_TO_CLASS.fetch(name, name) + end + + # fqns names are implicitly fully qualified - they are relative + # to the root namespace and are not prefixed with :: + # + # @param [RBS::TypeName] + # + # @return [String] + def fqns(type_name) + unless type_name.absolute? + Solargraph.assert_or_log(:rbs_fqns, "Received unexpected unqualified type name: #{type_name}") + end + ns = type_name.relative!.to_s + RBS_TO_CLASS.fetch(ns, ns) + end + + # @param type_name [RBS::TypeName] + # @param type_args [Enumerable] + # @return [ComplexType::UniqueType] + def build_type type_name, type_args = [] + # we use .absolute? below to tell the type object what to + # expect + rbs_name = type_name.relative!.to_s + base = RBS_TO_CLASS.fetch(rbs_name, rbs_name) + + params = type_args.map { |a| other_type_to_type(a) } + # tuples have their own class and are handled in other_type_to_type + if base == 'Hash' && params.length == 2 + ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: type_name.absolute?, parameters_type: :hash) + else + ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: type_name.absolute?, parameters_type: :list) + end end # @param decl [RBS::AST::Declarations::Module::Self] @@ -94,9 +180,9 @@ def convert_self_types_to_pins decl, module_pin # @return [void] def convert_self_type_to_pins decl, closure type = build_type(decl.name, decl.args) - generic_values = type.all_params.map(&:to_s) + generic_values = type.all_params.map(&:rooted_tags) include_pin = Solargraph::Pin::Reference::Include.new( - name: type.rooted_name, + name: type.name, type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -165,18 +251,31 @@ def convert_member_to_pin member, closure, context context end + # Pull the name of type variables for a generic - not the + # values, the names (e.g., T, U, V). As such, "rooting" isn't a + # thing, these are all in the global namespace. + # + # @param decl [RBS::AST::Declarations::Class, RBS::AST::Declarations::Interface, + # RBS::AST::Declarations::Module, RBS::MethodType] + # + # @return [Array] + def type_parameter_names decl + decl.type_params.map(&:name).map(&:to_s) + end + # @param decl [RBS::AST::Declarations::Class] # @return [void] def class_decl_to_pin decl - generics = decl.type_params.map(&:name).map(&:to_s) + # @type [Hash{String => ComplexType, ComplexType::UniqueType}] generic_defaults = {} decl.type_params.each do |param| - if param.default_type - tag = other_type_to_tag param.default_type - generic_defaults[param.name.to_s] = ComplexType.parse(tag).force_rooted - end + generic_defaults[param.name.to_s] = other_type_to_type param.default_type if param.default_type end - class_name = decl.name.relative!.to_s + + class_name = fqns(decl.name) + + generics = type_parameter_names(decl) + class_pin = Solargraph::Pin::Namespace.new( type: :class, name: class_name, @@ -193,13 +292,12 @@ def class_decl_to_pin decl pins.push class_pin if decl.super_class type = build_type(decl.super_class.name, decl.super_class.args) - generic_values = type.all_params.map(&:to_s) - superclass_name = decl.super_class.name.to_s + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Superclass.new( type_location: location_decl_to_pin_location(decl.super_class.location), closure: class_pin, generic_values: generic_values, - name: superclass_name, + name: type.rooted_name, # reference pins use rooted names source: :rbs ) end @@ -208,16 +306,15 @@ def class_decl_to_pin decl end # @param decl [RBS::AST::Declarations::Interface] - # @param closure [Pin::Closure] # @return [void] - def interface_decl_to_pin decl, closure + def interface_decl_to_pin decl class_pin = Solargraph::Pin::Namespace.new( type: :module, type_location: location_decl_to_pin_location(decl.location), - name: decl.name.relative!.to_s, + name: fqns(decl.name), closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, - generics: decl.type_params.map(&:name).map(&:to_s), + generics: type_parameter_names(decl), # HACK: Using :hidden to keep interfaces from appearing in # autocompletion visibility: :hidden, @@ -233,11 +330,11 @@ def interface_decl_to_pin decl, closure def module_decl_to_pin decl module_pin = Solargraph::Pin::Namespace.new( type: :module, - name: decl.name.relative!.to_s, + name: fqns(decl.name), type_location: location_decl_to_pin_location(decl.location), closure: Solargraph::Pin::ROOT_PIN, comments: decl.comment&.string, - generics: decl.type_params.map(&:name).map(&:to_s), + generics: type_parameter_names(decl), source: :rbs ) pins.push module_pin @@ -249,32 +346,37 @@ def module_decl_to_pin decl add_mixins decl, module_pin.closure end - # @param name [String] - # @param tag [String] + # @param fqns [String] + # @param type [ComplexType, ComplexType::UniqueType] # @param comments [String, nil] - # @param decl [RBS::AST::Declarations::ClassAlias, RBS::AST::Declarations::Constant, RBS::AST::Declarations::ModuleAlias] - # @param base [String, nil] Optional conversion of tag to base + # @param decl [RBS::AST::Declarations::ClassAlias, + # RBS::AST::Declarations::Constant, + # RBS::AST::Declarations::ModuleAlias] + # @param base [String, nil] Optional conversion of tag to + # base - valid values are Class and Module # # @return [Solargraph::Pin::Constant] - def create_constant(name, tag, comments, decl, base = nil) - parts = name.split('::') + def create_constant fqns, type, comments, decl, base = nil + parts = fqns.split('::') if parts.length > 1 - name = parts.last + fqns = parts.last # @sg-ignore Need to add nil check here closure = pins.select { |pin| pin && pin.path == parts[0..-2].join('::') }.first else - name = parts.first + fqns = parts.first closure = Solargraph::Pin::ROOT_PIN end constant_pin = Solargraph::Pin::Constant.new( - name: name, + name: fqns, closure: closure, type_location: location_decl_to_pin_location(decl.location), comments: comments, source: :rbs ) - tag = "#{base}<#{tag}>" if base - rooted_tag = ComplexType.parse(tag).force_rooted.rooted_tags + rooted_tag = type.rooted_tags + if base + rooted_tag = "#{base}<#{rooted_tag}>" + end constant_pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) constant_pin end @@ -283,27 +385,27 @@ def create_constant(name, tag, comments, decl, base = nil) # @return [void] def class_alias_decl_to_pin decl # See https://www.rubydoc.info/gems/rbs/3.4.3/RBS/AST/Declarations/ClassAlias - new_name = decl.new_name.relative!.to_s - old_name = decl.old_name.relative!.to_s - - pins.push create_constant(new_name, old_name, decl.comment&.string, decl, 'Class') + new_name = fqns(decl.new_name) + old_type = build_type(decl.old_name) + pins.push create_constant(new_name, old_type, decl.comment&.string, decl, '::Class') end # @param decl [RBS::AST::Declarations::ModuleAlias] # @return [void] def module_alias_decl_to_pin decl # See https://www.rubydoc.info/gems/rbs/3.4.3/RBS/AST/Declarations/ModuleAlias - new_name = decl.new_name.relative!.to_s - old_name = decl.old_name.relative!.to_s + new_name = fqns(decl.new_name) + old_type = build_type(decl.old_name) - pins.push create_constant(new_name, old_name, decl.comment&.string, decl, 'Module') + pins.push create_constant(new_name, old_type, decl.comment&.string, decl, '::Module') end # @param decl [RBS::AST::Declarations::Constant] # @return [void] def constant_decl_to_pin decl - tag = other_type_to_tag(decl.type) - pins.push create_constant(decl.name.relative!.to_s, tag, decl.comment&.string, decl) + target_type = other_type_to_type(decl.type) + constant_name = fqns(decl.name) + pins.push create_constant(constant_name, target_type, decl.comment&.string, decl) end # @param decl [RBS::AST::Declarations::Global] @@ -318,12 +420,11 @@ def global_decl_to_pin decl type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end - # Visibility overrides that will allow the Solargraph project # and plugins to pass typechecking using SOLARGRAPH_ASSERTS=on, # so that we can detect any regressions/issues elsewhere in the @@ -339,41 +440,45 @@ def global_decl_to_pin decl # allow that to be extended via .solargraph.yml # @type [Hash{Array(String, Symbol, String) => Symbol} VISIBILITY_OVERRIDE = { - ["Rails::Engine", :instance, "run_tasks_blocks"] => :protected, + ['Rails::Engine', :instance, 'run_tasks_blocks'] => :protected, # Should have been marked as both instance and class method in module -e.g., 'module_function' - ["Kernel", :instance, "pretty_inspect"] => :private, + ['Kernel', :instance, 'pretty_inspect'] => :private, # marked incorrectly in RBS - ["WEBrick::HTTPUtils::FormData", :instance, "next_data"] => :protected, - ["Rails::Command", :class, "command_type"] => :private, - ["Rails::Command", :class, "lookup_paths"] => :private, - ["Rails::Command", :class, "file_lookup_paths"] => :private, - ["Rails::Railtie", :instance, "run_console_blocks"] => :protected, - ["Rails::Railtie", :instance, "run_generators_blocks"] => :protected, - ["Rails::Railtie", :instance, "run_runner_blocks"] => :protected, - ["Rails::Railtie", :instance, "run_tasks_blocks"] => :protected, - ["ActionController::Base", :instance, "_protected_ivars"] => :private, - ["ActionView::Template", :instance, "method_name"] => :public, - ["Module", :instance, "ruby2_keywords"] => :private, - ["Nokogiri::XML::Node", :instance, "coerce"] => :protected, - ["Nokogiri::XML::Document", :class, "empty_doc?"] => :private, - ["Nokogiri::Decorators::Slop", :instance, "respond_to_missing?"] => :public, - ["RuboCop::Cop::RangeHelp", :instance, "source_range"] => :private, - ["AST::Node", :instance, "original_dup"] => :private, - ["Rainbow::Presenter", :instance, "wrap_with_sgr"] => :private, - } - - # @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor] + ['WEBrick::HTTPUtils::FormData', :instance, 'next_data'] => :protected, + ['Rails::Command', :class, 'command_type'] => :private, + ['Rails::Command', :class, 'lookup_paths'] => :private, + ['Rails::Command', :class, 'file_lookup_paths'] => :private, + ['Rails::Railtie', :instance, 'run_console_blocks'] => :protected, + ['Rails::Railtie', :instance, 'run_generators_blocks'] => :protected, + ['Rails::Railtie', :instance, 'run_runner_blocks'] => :protected, + ['Rails::Railtie', :instance, 'run_tasks_blocks'] => :protected, + ['ActionController::Base', :instance, '_protected_ivars'] => :private, + ['ActionView::Template', :instance, 'method_name'] => :public, + ['Module', :instance, 'ruby2_keywords'] => :private, + ['Nokogiri::XML::Node', :instance, 'coerce'] => :protected, + ['Nokogiri::XML::Document', :class, 'empty_doc?'] => :private, + ['Nokogiri::Decorators::Slop', :instance, 'respond_to_missing?'] => :public, + ['RuboCop::Cop::RangeHelp', :instance, 'source_range'] => :private, + ['AST::Node', :instance, 'original_dup'] => :private, + ['Rainbow::Presenter', :instance, 'wrap_with_sgr'] => :private + }.freeze + private_constant :VISIBILITY_OVERRIDE + + # @param decl [RBS::AST::Members::MethodDefinition, RBS::AST::Members::AttrReader, + # RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor] # @param closure [Pin::Closure] # @param context [Context] # @param scope [Symbol] :instance or :class # @param name [String] The name of the method # @return [Symbol] - def calculate_method_visibility(decl, context, closure, scope, name) + def calculate_method_visibility decl, context, closure, scope, name override_key = [closure.path, scope, name] visibility = VISIBILITY_OVERRIDE[override_key] simple_override_key = [closure.path, scope] visibility ||= VISIBILITY_OVERRIDE[simple_override_key] - visibility ||= :private if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(decl.name) + if closure.path == 'Kernel' && Kernel.private_instance_methods(false).include?(decl.name) + visibility ||= :private + end if decl.kind == :singleton_instance # this is a 'module function' visibility ||= :private @@ -393,7 +498,9 @@ def method_def_to_pin decl, closure, context # having different type params / orders - we may need to match # this data model and have generics live in signatures to # handle those correctly - generics = decl.overloads.map(&:method_type).flat_map(&:type_params).map(&:name).map(&:to_s).uniq + generics = decl.overloads.map(&:method_type).map do |method_type| + type_parameter_names method_type + end if decl.instance? name = decl.name.to_s @@ -417,24 +524,23 @@ def method_def_to_pin decl, closure, context pin.instance_variable_set(:@return_type, ComplexType::VOID) end end - if decl.singleton? - final_scope = :class - name = decl.name.to_s - visibility = calculate_method_visibility(decl, context, closure, final_scope, name) - pin = Solargraph::Pin::Method.new( - name: name, - closure: closure, - comments: decl.comment&.string, - type_location: location_decl_to_pin_location(decl.location), - visibility: visibility, - scope: final_scope, - signatures: [], - generics: generics, - source: :rbs - ) - pin.signatures.concat method_def_to_sigs(decl, pin) - pins.push pin - end + return unless decl.singleton? + final_scope = :class + name = decl.name.to_s + visibility = calculate_method_visibility(decl, context, closure, final_scope, name) + pin = Solargraph::Pin::Method.new( + name: name, + closure: closure, + comments: decl.comment&.string, + type_location: location_decl_to_pin_location(decl.location), + visibility: visibility, + scope: final_scope, + signatures: [], + generics: generics, + source: :rbs + ) + pin.signatures.concat method_def_to_sigs(decl, pin) + pins.push pin end # @param decl [RBS::AST::Members::MethodDefinition] @@ -444,22 +550,24 @@ def method_def_to_sigs decl, pin # @param overload [RBS::AST::Members::MethodDefinition::Overload] decl.overloads.map do |overload| type_location = location_decl_to_pin_location(overload.method_type.location) - generics = overload.method_type.type_params.map(&:name).map(&:to_s) + generics = type_parameter_names(overload.method_type) signature_parameters, signature_return_type = parts_of_function(overload.method_type, pin) rbs_block = overload.method_type.block block = if rbs_block block_parameters, block_return_type = parts_of_function(rbs_block, pin) - Pin::Signature.new(generics: generics, parameters: block_parameters, return_type: block_return_type, source: :rbs, + Pin::Signature.new(generics: generics, parameters: block_parameters, + return_type: block_return_type, source: :rbs, type_location: type_location, closure: pin) end - Pin::Signature.new(generics: generics, parameters: signature_parameters, return_type: signature_return_type, block: block, source: :rbs, + Pin::Signature.new(generics: generics, parameters: signature_parameters, + return_type: signature_return_type, block: block, source: :rbs, type_location: type_location, closure: pin) end end # @param location [RBS::Location, nil] # @return [Solargraph::Location, nil] - def location_decl_to_pin_location(location) + def location_decl_to_pin_location location return nil if location&.name.nil? # @sg-ignore flow sensitive typing should handle return nil if location&.name.nil? @@ -471,15 +579,16 @@ def location_decl_to_pin_location(location) Location.new(location.name.to_s, range) end - # @param type [RBS::MethodType,RBS::Types::Block] + # @param type [RBS::MethodType, RBS::Types::Block] # @param pin [Pin::Method] # @return [Array(Array, ComplexType)] def parts_of_function type, pin type_location = pin.type_location if defined?(RBS::Types::UntypedFunction) && type.type.is_a?(RBS::Types::UntypedFunction) return [ - [Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs, type_location: type_location)], - ComplexType.try_parse(method_type_to_tag(type)).force_rooted + [Solargraph::Pin::Parameter.new(decl: :restarg, name: 'arg', closure: pin, source: :rbs, + type_location: type_location)], + method_type_to_type(type) ] end @@ -488,41 +597,43 @@ def parts_of_function type, pin type.type.required_positionals.each do |param| # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" - # @sg-ignore RBS generic type understanding issue - parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, source: :rbs, type_location: type_location) + parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, + # @sg-ignore RBS generic type understanding issue + return_type: other_type_to_type(param.type), + source: :rbs, type_location: type_location) end type.type.optional_positionals.each do |param| # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :optarg, name: name, closure: pin, # @sg-ignore RBS generic type understanding issue - return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, + return_type: other_type_to_type(param.type), type_location: type_location, source: :rbs) end if type.type.rest_positionals name = type.type.rest_positionals.name ? type.type.rest_positionals.name.to_s : "arg_#{arg_num += 1}" - inner_rest_positional_type = - ComplexType.try_parse(other_type_to_tag(type.type.rest_positionals.type)) + inner_rest_positional_type = other_type_to_type(type.type.rest_positionals.type) rest_positional_type = ComplexType::UniqueType.new('Array', [], [inner_rest_positional_type], rooted: true, parameters_type: :list) parameters.push Solargraph::Pin::Parameter.new(decl: :restarg, name: name, closure: pin, source: :rbs, type_location: type_location, - return_type: rest_positional_type,) + return_type: rest_positional_type) end type.type.trailing_positionals.each do |param| # @sg-ignore RBS generic type understanding issue name = param.name ? param.name.to_s : "arg_#{arg_num += 1}" - parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, source: :rbs, type_location: type_location) + parameters.push Solargraph::Pin::Parameter.new(decl: :arg, name: name, closure: pin, source: :rbs, + type_location: type_location) end type.type.required_keywords.each do |orig, param| # @sg-ignore RBS generic type understanding issue name = orig ? orig.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwarg, name: name, closure: pin, # @sg-ignore RBS generic type understanding issue - return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, + return_type: other_type_to_type(param.type), source: :rbs, type_location: type_location) end type.type.optional_keywords.each do |orig, param| @@ -530,18 +641,18 @@ def parts_of_function type, pin name = orig ? orig.to_s : "arg_#{arg_num += 1}" parameters.push Solargraph::Pin::Parameter.new(decl: :kwoptarg, name: name, closure: pin, # @sg-ignore RBS generic type understanding issue - return_type: ComplexType.try_parse(other_type_to_tag(param.type)).force_rooted, + return_type: other_type_to_type(param.type), type_location: type_location, source: :rbs) end if type.type.rest_keywords name = type.type.rest_keywords.name ? type.type.rest_keywords.name.to_s : "arg_#{arg_num += 1}" - parameters.push Solargraph::Pin::Parameter.new(decl: :kwrestarg, name: type.type.rest_keywords.name.to_s, closure: pin, + parameters.push Solargraph::Pin::Parameter.new(decl: :kwrestarg, + name: type.type.rest_keywords.name.to_s, closure: pin, source: :rbs, type_location: type_location) end - rooted_tag = method_type_to_tag(type) - return_type = ComplexType.try_parse(rooted_tag).force_rooted + return_type = method_type_to_type(type) [parameters, return_type] end @@ -549,7 +660,7 @@ def parts_of_function type, pin # @param closure [Pin::Namespace] # @param context [Context] # @return [void] - def attr_reader_to_pin(decl, closure, context) + def attr_reader_to_pin decl, closure, context name = decl.name.to_s final_scope = decl.kind == :instance ? :instance : :class visibility = calculate_method_visibility(decl, context, closure, final_scope, name) @@ -563,9 +674,11 @@ def attr_reader_to_pin(decl, closure, context) visibility: visibility, source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) - logger.debug { "Conversions#attr_reader_to_pin(name=#{name.inspect}, visibility=#{visibility.inspect}) => #{pin.inspect}" } + logger.debug do + "Conversions#attr_reader_to_pin(name=#{name.inspect}, visibility=#{visibility.inspect}) => #{pin.inspect}" + end pins.push pin end @@ -573,9 +686,9 @@ def attr_reader_to_pin(decl, closure, context) # @param closure [Pin::Namespace] # @param context [Context] # @return [void] - def attr_writer_to_pin(decl, closure, context) + def attr_writer_to_pin decl, closure, context final_scope = decl.kind == :instance ? :instance : :class - name = "#{decl.name.to_s}=" + name = "#{decl.name}=" visibility = calculate_method_visibility(decl, context, closure, final_scope, name) type_location = location_decl_to_pin_location(decl.location) pin = Solargraph::Pin::Method.new( @@ -592,13 +705,13 @@ def attr_writer_to_pin(decl, closure, context) pin.parameters << Solargraph::Pin::Parameter.new( name: 'value', - return_type: ComplexType.try_parse(other_type_to_tag(decl.type)).force_rooted, + return_type: other_type_to_type(decl.type), source: :rbs, closure: pin, type_location: type_location ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags - pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tag)) + rooted_tags = other_type_to_type(decl.type).rooted_tags + pin.docstring.add_tag(YARD::Tags::Tag.new(:return, '', rooted_tags)) pins.push pin end @@ -606,7 +719,7 @@ def attr_writer_to_pin(decl, closure, context) # @param closure [Pin::Namespace] # @param context [Context] # @return [void] - def attr_accessor_to_pin(decl, closure, context) + def attr_accessor_to_pin decl, closure, context attr_reader_to_pin(decl, closure, context) attr_writer_to_pin(decl, closure, context) end @@ -614,7 +727,7 @@ def attr_accessor_to_pin(decl, closure, context) # @param decl [RBS::AST::Members::InstanceVariable] # @param closure [Pin::Namespace] # @return [void] - def ivar_to_pin(decl, closure) + def ivar_to_pin decl, closure pin = Solargraph::Pin::InstanceVariable.new( name: decl.name.to_s, closure: closure, @@ -622,7 +735,7 @@ def ivar_to_pin(decl, closure) comments: decl.comment&.string, source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -630,7 +743,7 @@ def ivar_to_pin(decl, closure) # @param decl [RBS::AST::Members::ClassVariable] # @param closure [Pin::Namespace] # @return [void] - def cvar_to_pin(decl, closure) + def cvar_to_pin decl, closure name = decl.name.to_s pin = Solargraph::Pin::ClassVariable.new( name: name, @@ -639,7 +752,7 @@ def cvar_to_pin(decl, closure) type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -647,7 +760,7 @@ def cvar_to_pin(decl, closure) # @param decl [RBS::AST::Members::ClassInstanceVariable] # @param closure [Pin::Namespace] # @return [void] - def civar_to_pin(decl, closure) + def civar_to_pin decl, closure name = decl.name.to_s pin = Solargraph::Pin::InstanceVariable.new( name: name, @@ -656,7 +769,7 @@ def civar_to_pin(decl, closure) type_location: location_decl_to_pin_location(decl.location), source: :rbs ) - rooted_tag = ComplexType.parse(other_type_to_tag(decl.type)).force_rooted.rooted_tags + rooted_tag = other_type_to_type(decl.type).rooted_tags pin.docstring.add_tag(YARD::Tags::Tag.new(:type, '', rooted_tag)) pins.push pin end @@ -666,9 +779,9 @@ def civar_to_pin(decl, closure) # @return [void] def include_to_pin decl, closure type = build_type(decl.name, decl.args) - generic_values = type.all_params.map(&:to_s) + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Include.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), generic_values: generic_values, closure: closure, @@ -680,9 +793,12 @@ def include_to_pin decl, closure # @param closure [Pin::Namespace] # @return [void] def prepend_to_pin decl, closure + type = build_type(decl.name, decl.args) + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Prepend.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), + generic_values: generic_values, closure: closure, source: :rbs ) @@ -692,9 +808,12 @@ def prepend_to_pin decl, closure # @param closure [Pin::Namespace] # @return [void] def extend_to_pin decl, closure + type = build_type(decl.name, decl.args) + generic_values = type.all_params.map(&:rooted_tags) pins.push Solargraph::Pin::Reference::Extend.new( - name: decl.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(decl.location), + generic_values: generic_values, closure: closure, source: :rbs ) @@ -711,114 +830,98 @@ def alias_to_pin decl, closure original: decl.old_name.to_s, closure: closure, scope: final_scope, - source: :rbs, + source: :rbs ) end - RBS_TO_YARD_TYPE = { - 'bool' => 'Boolean', - 'string' => 'String', - 'int' => 'Integer', - 'untyped' => '', - 'NilClass' => 'nil' - } - # @param type [RBS::MethodType, RBS::Types::Block] - # @return [String] - def method_type_to_tag type + # @return [ComplexType, ComplexType::UniqueType] + def method_type_to_type type if type_aliases.key?(type.type.return_type.to_s) - other_type_to_tag(type_aliases[type.type.return_type.to_s].type) - else - other_type_to_tag type.type.return_type - end - end - - # @param type_name [RBS::TypeName] - # @param type_args [Enumerable] - # @return [ComplexType::UniqueType] - def build_type(type_name, type_args = []) - base = RBS_TO_YARD_TYPE[type_name.relative!.to_s] || type_name.relative!.to_s - params = type_args.map { |a| other_type_to_tag(a) }.map do |t| - ComplexType.try_parse(t).force_rooted - end - if base == 'Hash' && params.length == 2 - ComplexType::UniqueType.new(base, [params.first], [params.last], rooted: true, parameters_type: :hash) + other_type_to_type(type_aliases[type.type.return_type.to_s].type) else - ComplexType::UniqueType.new(base, [], params.reject(&:undefined?), rooted: true, parameters_type: :list) + other_type_to_type type.type.return_type end end - # @param type_name [RBS::TypeName] - # @param type_args [Enumerable] - # @return [String] - def type_tag(type_name, type_args = []) - build_type(type_name, type_args).tags - end - # @param type [RBS::Types::Bases::Base,Object] RBS type object. # Note: Generally these extend from RBS::Types::Bases::Base, # but not all. - # @return [String] - def other_type_to_tag type - if type.is_a?(RBS::Types::Optional) - "#{other_type_to_tag(type.type)}, nil" - elsif type.is_a?(RBS::Types::Bases::Any) - 'undefined' - elsif type.is_a?(RBS::Types::Bases::Bool) - 'Boolean' - elsif type.is_a?(RBS::Types::Tuple) - "Array(#{type.types.map { |t| other_type_to_tag(t) }.join(', ')})" - elsif type.is_a?(RBS::Types::Literal) - type.literal.inspect - elsif type.is_a?(RBS::Types::Union) - type.types.map { |t| other_type_to_tag(t) }.join(', ') - elsif type.is_a?(RBS::Types::Record) + # + # @return [ComplexType, ComplexType::UniqueType] + def other_type_to_type type + case type + when RBS::Types::Optional + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.new([other_type_to_type(type.type), + ComplexType::UniqueType::NIL]) + when RBS::Types::Bases::Any + ComplexType::UNDEFINED + when RBS::Types::Bases::Bool + ComplexType::BOOLEAN + when RBS::Types::Tuple + # @sg-ignore flow based typing needs to understand case when class pattern + tuple_types = type.types.map { |t| other_type_to_type(t) } + ComplexType::UniqueType.new('Array', [], tuple_types, rooted: true, parameters_type: :fixed) + when RBS::Types::Literal + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.try_parse(type.literal.inspect).force_rooted + when RBS::Types::Union + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.new(type.types.map { |t| other_type_to_type(t) }) + when RBS::Types::Record # @todo Better record support - 'Hash' - elsif type.is_a?(RBS::Types::Bases::Nil) - 'nil' - elsif type.is_a?(RBS::Types::Bases::Self) - 'self' - elsif type.is_a?(RBS::Types::Bases::Void) - 'void' - elsif type.is_a?(RBS::Types::Variable) - "#{Solargraph::ComplexType::GENERIC_TAG_NAME}<#{type.name}>" - elsif type.is_a?(RBS::Types::ClassInstance) #&& !type.args.empty? - type_tag(type.name, type.args) - elsif type.is_a?(RBS::Types::Bases::Instance) - 'self' - elsif type.is_a?(RBS::Types::Bases::Top) + ComplexType::UniqueType.new('Hash', rooted: true) + when RBS::Types::Bases::Nil + ComplexType::NIL + when RBS::Types::Bases::Self + ComplexType::SELF + when RBS::Types::Bases::Void + ComplexType::VOID + when RBS::Types::Variable + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.parse("generic<#{type.name}>").force_rooted + when RBS::Types::ClassInstance # && !type.args.empty? + # @sg-ignore flow based typing needs to understand case when class pattern + build_type(type.name, type.args) + when RBS::Types::Bases::Instance + ComplexType::SELF + when RBS::Types::Bases::Top # top is the most super superclass - 'BasicObject' - elsif type.is_a?(RBS::Types::Bases::Bottom) + ComplexType::UniqueType.new('BasicObject', rooted: true) + when RBS::Types::Bases::Bottom # bottom is used in contexts where nothing will ever return # - e.g., it could be the return type of 'exit()' or 'raise' # # @todo define a specific bottom type and use it to # determine dead code - 'undefined' - elsif type.is_a?(RBS::Types::Intersection) - type.types.map { |member| other_type_to_tag(member) }.join(', ') - elsif type.is_a?(RBS::Types::Proc) - 'Proc' - elsif type.is_a?(RBS::Types::Alias) + ComplexType::UNDEFINED + when RBS::Types::Intersection + # @sg-ignore flow based typing needs to understand case when class pattern + ComplexType.new(type.types.map { |member| other_type_to_type(member) }) + when RBS::Types::Proc + ComplexType::UniqueType.new('Proc', rooted: true) + when RBS::Types::Alias # type-level alias use - e.g., 'bool' in "type bool = true | false" # @todo ensure these get resolved after processing all aliases # @todo handle recursive aliases - type_tag(type.name, type.args) - elsif type.is_a?(RBS::Types::Interface) + # @sg-ignore flow based typing needs to understand case when class pattern + build_type(type.name, type.args) + when RBS::Types::Interface # represents a mix-in module which can be considered a # subtype of a consumer of it - type_tag(type.name, type.args) - elsif type.is_a?(RBS::Types::ClassSingleton) + # @sg-ignore flow based typing needs to understand case when class pattern + build_type(type.name, type.args) + when RBS::Types::ClassSingleton # e.g., singleton(String) - type_tag(type.name) + # @sg-ignore flow based typing needs to understand case when class pattern + build_type(type.name) else # RBS doesn't provide a common base class for its type AST nodes # # @sg-ignore all types should include location Solargraph.logger.warn "Unrecognized RBS type: #{type.class} at #{type.location}" - 'undefined' + ComplexType::UNDEFINED end end @@ -831,9 +934,9 @@ def add_mixins decl, namespace # @todo are we handling prepend correctly? klass = mixin.is_a?(RBS::AST::Members::Include) ? Pin::Reference::Include : Pin::Reference::Extend type = build_type(mixin.name, mixin.args) - generic_values = type.all_params.map(&:to_s) + generic_values = type.all_params.map(&:rooted_tags) pins.push klass.new( - name: mixin.name.relative!.to_s, + name: type.rooted_name, # reference pins use rooted names type_location: location_decl_to_pin_location(mixin.location), generic_values: generic_values, closure: namespace, diff --git a/lib/solargraph/version.rb b/lib/solargraph/version.rb index bfe833039..00cc77b02 100755 --- a/lib/solargraph/version.rb +++ b/lib/solargraph/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Solargraph - VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.59.0.dev.1') + VERSION = ENV.fetch('SOLARGRAPH_FORCE_VERSION', '0.59.0.dev.2') end diff --git a/spec/position_spec.rb b/spec/position_spec.rb index fa30cf7d9..e8dab1960 100644 --- a/spec/position_spec.rb +++ b/spec/position_spec.rb @@ -12,9 +12,38 @@ expect(orig).to be(norm) end + it 'finds offset from position' do + text = "\n class Foo\n def bar baz, boo = 'boo'\n end\n end\n " + expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(0, 0))).to eq(0) + expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(0, 4))).to eq(4) + expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(2, 12))).to eq(29) + expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(2, 27))).to eq(44) + expect(Solargraph::Position.to_offset(text, Solargraph::Position.new(3, 8))).to eq(58) + end + + it 'constructs position from offset' do + text = "\n class Foo\n def bar baz, boo = 'boo'\n end\n end\n " + expect(Solargraph::Position.from_offset(text, 0)).to eq(Solargraph::Position.new(0, 0)) + expect(Solargraph::Position.from_offset(text, 4)).to eq(Solargraph::Position.new(1, 3)) + expect(Solargraph::Position.from_offset(text, 29)).to eq(Solargraph::Position.new(2, 12)) + expect(Solargraph::Position.from_offset(text, 44)).to eq(Solargraph::Position.new(2, 27)) + end + it "raises an error for objects that cannot be normalized" do expect { Solargraph::Position.normalize('0, 1') }.to raise_error(ArgumentError) end + + it 'avoids fencepost errors' do + text = " class Foo\n def bar baz, boo = 'boo'\n end\n end\n " + offset = Solargraph::Position.to_offset(text, Solargraph::Position.new(3, 6)) + expect(offset).to eq(67) + end + + it 'avoids fencepost errors with multiple blank lines' do + text = " class Foo\n def bar baz, boo = 'boo'\n\n end\n end\n " + offset = Solargraph::Position.to_offset(text, Solargraph::Position.new(4, 6)) + expect(offset).to eq(68) + end end diff --git a/spec/rbs_map/core_map_spec.rb b/spec/rbs_map/core_map_spec.rb index cada2754c..e2d9b41d5 100644 --- a/spec/rbs_map/core_map_spec.rb +++ b/spec/rbs_map/core_map_spec.rb @@ -80,7 +80,7 @@ # correctly. It would be better to test RbsMap or RbsMap::Conversions # with an RBS fixture. core_map = Solargraph::RbsMap::CoreMap.new - pins = core_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == 'Enumerable' } + pins = core_map.pins.select { |pin| pin.is_a?(Solargraph::Pin::Reference::Include) && pin.name == '::Enumerable' } expect(pins.map(&:closure).map(&:namespace)).to include('Enumerator') end @@ -97,16 +97,6 @@ class Foo expect(clip.infer.to_s).to eq('Foo') end - it "generates rooted pins from RBS for core" do - map = Solargraph::RbsMap::CoreMap.new - map.pins.each do |pin| - expect(pin).to be_all_rooted - unless pin.is_a?(Solargraph::Pin::Keyword) - expect(pin.closure).to_not be_nil, ->(){ "Pin #{pin.inspect} (#{pin.path}) has no closure" } - end - end - end - it 'renders string literals from RBS in a useful way' do source = Solargraph::Source.load_string(%( foo = nil