Skip to content

Commit 22758a0

Browse files
committed
Add class_method attribute to scopes and enhance method call handling
1 parent 67f4a0c commit 22758a0

4 files changed

Lines changed: 39 additions & 7 deletions

File tree

‎lib/db/schema.rb‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def write(*args)
1919
t.string :path
2020
t.string :class_type, null: false
2121
t.text :parameters # JSON string of method parameters
22+
t.boolean :class_method, default: false # true for class methods (def self.method)
2223
end
2324

2425
add_index :scopes, :name

‎lib/ruby_language_server/project_manager.rb‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,26 @@ def possible_definitions(uri, position)
162162
name = word_at_location(uri, position)
163163
return {} if name.blank?
164164

165+
# Special case: "new" should find "initialize" (instance method, not class method)
165166
name = 'initialize' if name == 'new'
167+
166168
context = context_at_location(uri, position)
167169

168-
# If context has more than one element and the receiver is not a constant (class),
169-
# it's an instance method call (e.g., foo.bar, not Foo.bar)
170-
# In that case, skip scope chain search and go straight to project definitions
171-
if context && context.length > 1 && context.first && context.first[0] =~ /[a-z_@]/ # rubocop:disable Style/IfUnlessModifier
172-
return project_definitions_for(name)
170+
# If context has more than one element it's a method call on a receiver
171+
if context.length > 1
172+
receiver = context.first
173+
# Determine if it's a class method call (Foo.method) or instance method call (foo.method)
174+
if likely_class_name?(receiver)
175+
# Class method call or MyClass.new (which finds initialize as instance method)
176+
# initialize is weird because it's defined as an instance method but called on the class.
177+
return project_definitions_for(name, name == 'initialize' ? false : true)
178+
else
179+
# Instance method call (e.g., foo.bar, @foo.bar, FOO.bar)
180+
return project_definitions_for(name, false)
181+
end
173182
end
174183

184+
# No receiver - search in scope chain first, then project-wide
175185
scope = scopes_at(uri, position).first
176186
results = scope_definitions_for(name, scope, uri)
177187
return results unless results.empty?
@@ -194,8 +204,14 @@ def scope_definitions_for(name, scope, uri)
194204
return_array.uniq
195205
end
196206

197-
def project_definitions_for(name)
207+
def project_definitions_for(name, class_method_filter = nil)
198208
scopes = RubyLanguageServer::ScopeData::Scope.where(name:)
209+
210+
# Filter by class_method attribute if specified
211+
unless class_method_filter.nil?
212+
scopes = scopes.where(class_method: class_method_filter)
213+
end
214+
199215
variables = RubyLanguageServer::ScopeData::Variable.constant_variables.where(name:)
200216
(scopes + variables).reject { |scope| scope.code_file.nil? }.map do |scope|
201217
Location.hash(scope.code_file.uri, scope.top_line, 1)
@@ -204,6 +220,18 @@ def project_definitions_for(name)
204220

205221
private
206222

223+
# Guess if a receiver name is likely a class name based on idiomatic Ruby conventions.
224+
# This is a heuristic and not 100% accurate (e.g., FOO could be a constant holding an instance).
225+
# Returns true for names like "Foo" or "FooBar" (starts with uppercase, has lowercase letters).
226+
# Returns false for "FOO" (all uppercase constant) or "foo" (variable/method).
227+
def likely_class_name?(name)
228+
# Must start with uppercase letter
229+
return false unless name[0] =~ /[A-Z]/
230+
231+
# Must contain at least one lowercase letter to distinguish from constants like FOO
232+
name =~ /[a-z]/
233+
end
234+
207235
def code_file_for_uri(uri)
208236
CodeFile.find_by_uri(uri) || CodeFile.build(uri, nil)
209237
end

‎lib/ruby_language_server/scope_parser.rb‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ def visit_def_node(node)
7171
line = node.location.start_line
7272
end_line = node.location.end_line
7373
column = node.location.start_column
74+
is_class_method = node.receiver.is_a?(Prism::SelfNode)
7475

7576
scope = push_scope(ScopeData::Scope::TYPE_METHOD, name, line, column, end_line, false)
77+
scope.class_method = is_class_method
78+
scope.save!
7679

7780
# Extract and store parameters
7881
if node.parameters

‎spec/lib/ruby_language_server/project_manager_spec.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ def initialize
194194
CODE_FILE
195195

196196
project_manager.update_document_content('new_uri', file_with_new)
197+
project_manager.tags_for_uri('new_uri') # Force load of tags
197198

198199
# Position on "new"
199200
position = OpenStruct.new(line: 6, character: 17)
@@ -303,7 +304,6 @@ def some_method(meaningful) # line 17: parameter declaration
303304
end
304305

305306
it 'finds only class method when called on class name' do
306-
skip "Class method vs instance method distinction not yet implemented"
307307

308308
# Position on "meaningful" method call on Foo (line 20, character 12)
309309
position = OpenStruct.new(line: 20, character: 12)

0 commit comments

Comments
 (0)