lib/ruby_indexer/lib/ruby_indexer/entry.rb in ruby-lsp-0.17.17 vs lib/ruby_indexer/lib/ruby_indexer/entry.rb in ruby-lsp-0.18.0
- old
+ new
@@ -22,22 +22,19 @@
sig { returns(RubyIndexer::Location) }
attr_reader :location
alias_method :name_location, :location
- sig { returns(T::Array[String]) }
- attr_reader :comments
-
sig { returns(Visibility) }
attr_accessor :visibility
sig do
params(
name: String,
file_path: String,
location: T.any(Prism::Location, RubyIndexer::Location),
- comments: T::Array[String],
+ comments: T.nilable(String),
).void
end
def initialize(name, file_path, location, comments)
@name = name
@file_path = file_path
@@ -77,10 +74,41 @@
sig { returns(String) }
def file_name
File.basename(@file_path)
end
+ sig { returns(String) }
+ def comments
+ @comments ||= begin
+ # Parse only the comments based on the file path, which is much faster than parsing the entire file
+ parsed_comments = Prism.parse_file_comments(@file_path)
+
+ # Group comments based on whether they belong to a single block of comments
+ grouped = parsed_comments.slice_when do |left, right|
+ left.location.start_line + 1 != right.location.start_line
+ end
+
+ # Find the group that is either immediately or two lines above the current entry
+ correct_group = grouped.find do |group|
+ comment_end_line = group.last.location.start_line
+ (comment_end_line - 1..comment_end_line).cover?(@location.start_line - 1)
+ end
+
+ # If we found something, we join the comments together. Otherwise, the entry has no documentation and we don't
+ # want to accidentally re-parse it, so we set it to an empty string. If an entry is updated, the entire entry
+ # object is dropped, so this will not prevent updates
+ if correct_group
+ correct_group.filter_map do |comment|
+ content = comment.slice
+ content if content.valid_encoding?
+ end.join("\n")
+ else
+ ""
+ end
+ end
+ end
+
class ModuleOperation
extend T::Sig
extend T::Helpers
abstract!
@@ -114,11 +142,11 @@
params(
nesting: T::Array[String],
file_path: String,
location: T.any(Prism::Location, RubyIndexer::Location),
name_location: T.any(Prism::Location, Location),
- comments: T::Array[String],
+ comments: T.nilable(String),
).void
end
def initialize(nesting, file_path, location, name_location, comments)
@name = T.let(nesting.join("::"), String)
# The original nesting where this namespace was discovered
@@ -175,11 +203,11 @@
params(
nesting: T::Array[String],
file_path: String,
location: T.any(Prism::Location, RubyIndexer::Location),
name_location: T.any(Prism::Location, Location),
- comments: T::Array[String],
+ comments: T.nilable(String),
parent_class: T.nilable(String),
).void
end
def initialize(nesting, file_path, location, name_location, comments, parent_class) # rubocop:disable Metrics/ParameterLists
super(nesting, file_path, location, name_location, comments)
@@ -193,11 +221,11 @@
end
class SingletonClass < Class
extend T::Sig
- sig { params(location: Prism::Location, name_location: Prism::Location, comments: T::Array[String]).void }
+ sig { params(location: Prism::Location, name_location: Prism::Location, comments: T.nilable(String)).void }
def update_singleton_information(location, name_location, comments)
# Create a new RubyIndexer::Location object from the Prism location
@location = Location.new(
location.start_line,
location.end_line,
@@ -208,11 +236,11 @@
name_location.start_line,
name_location.end_line,
name_location.start_column,
name_location.end_column,
)
- @comments.concat(comments)
+ (@comments ||= +"") << comments if comments
end
end
class Constant < Entry
end
@@ -300,30 +328,36 @@
def decorated_name
:"&#{@name}"
end
end
+ # A forwarding method parameter, e.g. `def foo(...)`
+ class ForwardingParameter < Parameter
+ extend T::Sig
+
+ sig { void }
+ def initialize
+ # You can't name a forwarding parameter, it's always called `...`
+ super(name: :"...")
+ end
+ end
+
class Member < Entry
extend T::Sig
extend T::Helpers
abstract!
sig { returns(T.nilable(Entry::Namespace)) }
attr_reader :owner
- sig { returns(T::Array[RubyIndexer::Entry::Parameter]) }
- def parameters
- T.must(signatures.first).parameters
- end
-
sig do
params(
name: String,
file_path: String,
location: T.any(Prism::Location, RubyIndexer::Location),
- comments: T::Array[String],
+ comments: T.nilable(String),
visibility: Visibility,
owner: T.nilable(Entry::Namespace),
).void
end
def initialize(name, file_path, location, comments, visibility, owner) # rubocop:disable Metrics/ParameterLists
@@ -387,11 +421,11 @@
params(
name: String,
file_path: String,
location: T.any(Prism::Location, RubyIndexer::Location),
name_location: T.any(Prism::Location, Location),
- comments: T::Array[String],
+ comments: T.nilable(String),
signatures: T::Array[Signature],
visibility: Visibility,
owner: T.nilable(Entry::Namespace),
).void
end
@@ -438,11 +472,11 @@
target: String,
nesting: T::Array[String],
name: String,
file_path: String,
location: T.any(Prism::Location, RubyIndexer::Location),
- comments: T::Array[String],
+ comments: T.nilable(String),
).void
end
def initialize(target, nesting, name, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
super(name, file_path, location, comments)
@@ -475,11 +509,11 @@
sig do
params(
name: String,
file_path: String,
location: T.any(Prism::Location, RubyIndexer::Location),
- comments: T::Array[String],
+ comments: T.nilable(String),
owner: T.nilable(Entry::Namespace),
).void
end
def initialize(name, file_path, location, comments, owner)
super(name, file_path, location, comments)
@@ -504,11 +538,11 @@
new_name: String,
old_name: String,
owner: T.nilable(Entry::Namespace),
file_path: String,
location: T.any(Prism::Location, RubyIndexer::Location),
- comments: T::Array[String],
+ comments: T.nilable(String),
).void
end
def initialize(new_name, old_name, owner, file_path, location, comments) # rubocop:disable Metrics/ParameterLists
super(new_name, file_path, location, comments)
@@ -528,14 +562,13 @@
sig { returns(T.nilable(Entry::Namespace)) }
attr_reader :owner
sig { params(target: T.any(Member, MethodAlias), unresolved_alias: UnresolvedMethodAlias).void }
def initialize(target, unresolved_alias)
- full_comments = ["Alias for #{target.name}\n"]
- full_comments.concat(unresolved_alias.comments)
- full_comments << "\n"
- full_comments.concat(target.comments)
+ full_comments = +"Alias for #{target.name}\n"
+ full_comments << "#{unresolved_alias.comments}\n"
+ full_comments << target.comments
super(
unresolved_alias.new_name,
unresolved_alias.file_path,
unresolved_alias.location,
@@ -544,15 +577,10 @@
@target = target
@owner = T.let(unresolved_alias.owner, T.nilable(Entry::Namespace))
end
- sig { returns(T::Array[Parameter]) }
- def parameters
- @target.parameters
- end
-
sig { returns(String) }
def decorated_parameters
@target.decorated_parameters
end
@@ -583,9 +611,111 @@
# Returns a string with the decorated names of the parameters of this member. E.g.: `(a, b = 1, c: 2)`
sig { returns(String) }
def format
@parameters.map(&:decorated_name).join(", ")
+ end
+
+ # Returns `true` if the given call node arguments array matches this method signature. This method will prefer
+ # returning `true` for situations that cannot be analyzed statically, like the presence of splats, keyword splats
+ # or forwarding arguments.
+ #
+ # Since this method is used to detect which overload should be displayed in signature help, it will also return
+ # `true` if there are missing arguments since the user may not be done typing yet. For example:
+ #
+ # ```ruby
+ # def foo(a, b); end
+ # # All of the following are considered matches because the user might be in the middle of typing and we have to
+ # # show them the signature
+ # foo
+ # foo(1)
+ # foo(1, 2)
+ # ```
+ sig { params(arguments: T::Array[Prism::Node]).returns(T::Boolean) }
+ def matches?(arguments)
+ min_pos = 0
+ max_pos = T.let(0, T.any(Integer, Float))
+ names = []
+ has_forward = T.let(false, T::Boolean)
+ has_keyword_rest = T.let(false, T::Boolean)
+
+ @parameters.each do |param|
+ case param
+ when RequiredParameter
+ min_pos += 1
+ max_pos += 1
+ when OptionalParameter
+ max_pos += 1
+ when RestParameter
+ max_pos = Float::INFINITY
+ when ForwardingParameter
+ max_pos = Float::INFINITY
+ has_forward = true
+ when KeywordParameter, OptionalKeywordParameter
+ names << param.name
+ when KeywordRestParameter
+ has_keyword_rest = true
+ end
+ end
+
+ keyword_hash_nodes, positional_args = arguments.partition { |arg| arg.is_a?(Prism::KeywordHashNode) }
+ keyword_args = T.cast(keyword_hash_nodes.first, T.nilable(Prism::KeywordHashNode))&.elements
+ forwarding_arguments, positionals = positional_args.partition do |arg|
+ arg.is_a?(Prism::ForwardingArgumentsNode)
+ end
+
+ return true if has_forward && min_pos == 0
+
+ # If the only argument passed is a forwarding argument, then anything will match
+ (positionals.empty? && forwarding_arguments.any?) ||
+ (
+ # Check if positional arguments match. This includes required, optional, rest arguments. We also need to
+ # verify if there's a trailing forwading argument, like `def foo(a, ...); end`
+ positional_arguments_match?(positionals, forwarding_arguments, keyword_args, min_pos, max_pos) &&
+ # If the positional arguments match, we move on to checking keyword, optional keyword and keyword rest
+ # arguments. If there's a forward argument, then it will always match. If the method accepts a keyword rest
+ # (**kwargs), then we can't analyze statically because the user could be passing a hash and we don't know
+ # what the runtime values inside the hash are.
+ #
+ # If none of those match, then we verify if the user is passing the expect names for the keyword arguments
+ (has_forward || has_keyword_rest || keyword_arguments_match?(keyword_args, names))
+ )
+ end
+
+ sig do
+ params(
+ positional_args: T::Array[Prism::Node],
+ forwarding_arguments: T::Array[Prism::Node],
+ keyword_args: T.nilable(T::Array[Prism::Node]),
+ min_pos: Integer,
+ max_pos: T.any(Integer, Float),
+ ).returns(T::Boolean)
+ end
+ def positional_arguments_match?(positional_args, forwarding_arguments, keyword_args, min_pos, max_pos)
+ # If the method accepts at least one positional argument and a splat has been passed
+ (min_pos > 0 && positional_args.any? { |arg| arg.is_a?(Prism::SplatNode) }) ||
+ # If there's at least one positional argument unaccounted for and a keyword splat has been passed
+ (min_pos - positional_args.length > 0 && keyword_args&.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }) ||
+ # If there's at least one positional argument unaccounted for and a forwarding argument has been passed
+ (min_pos - positional_args.length > 0 && forwarding_arguments.any?) ||
+ # If the number of positional arguments is within the expected range
+ (min_pos > 0 && positional_args.length <= max_pos) ||
+ (min_pos == 0 && positional_args.empty?)
+ end
+
+ sig { params(args: T.nilable(T::Array[Prism::Node]), names: T::Array[Symbol]).returns(T::Boolean) }
+ def keyword_arguments_match?(args, names)
+ return true unless args
+ return true if args.any? { |arg| arg.is_a?(Prism::AssocSplatNode) }
+
+ arg_names = args.filter_map do |arg|
+ next unless arg.is_a?(Prism::AssocNode)
+
+ key = arg.key
+ key.value&.to_sym if key.is_a?(Prism::SymbolNode)
+ end
+
+ (arg_names - names).empty?
end
end
end
end