diff --git a/ext/rubydex/graph.c b/ext/rubydex/graph.c index 5434990b..6c4b1e9f 100644 --- a/ext/rubydex/graph.c +++ b/ext/rubydex/graph.c @@ -16,17 +16,20 @@ static VALUE cKeywordParameter; // Interned once in `rdxi_initialize_graph` to avoid repeated symbol-table lookups on hot completion paths. static ID id_self_receiver; -// Extracts the optional `self_receiver:` kwarg from `opts`. Returns NULL when the kwarg is -// absent or nil; raises ArgumentError when the value is the wrong type or empty. +// Extracts the required `self_receiver:` kwarg from `opts`. Returns NULL when the value is `nil`, +// which means "no self-type to walk" (e.g., empty class body where the singleton class hasn't +// been created). Raises ArgumentError if the kwarg is absent, of the wrong type, or an empty +// string. The kwarg is required so that callers commit to a self type — there is no implicit +// default. static const char *extract_self_receiver(VALUE opts) { if (NIL_P(opts)) { - return NULL; + rb_raise(rb_eArgError, "missing keyword: self_receiver"); } VALUE kwarg_val; - rb_get_kwargs(opts, &id_self_receiver, 0, 1, &kwarg_val); + rb_get_kwargs(opts, &id_self_receiver, 1, 0, &kwarg_val); - if (kwarg_val == Qundef || NIL_P(kwarg_val)) { + if (NIL_P(kwarg_val)) { return NULL; } @@ -608,11 +611,10 @@ static VALUE completion_result_to_ruby_array(struct CompletionResult result, VAL return ruby_array; } -// Graph#complete_expression: (Array[String] nesting, self_receiver: nil) -> Array[Declaration | Keyword] +// Graph#complete_expression: (Array[String] nesting, self_receiver:) -> Array[Declaration | Keyword] // Returns completion candidates for an expression context. -// The nesting array represents the lexical scope stack. The optional self_receiver keyword argument -// overrides the self-type (e.g., "Foo::" for `def Foo.bar`); when nil, self is derived from -// the innermost nesting element. +// The nesting array represents the lexical scope stack. The required self_receiver keyword argument overrides the +// self-type (e.g., "Foo::" for `def Foo.bar`); when nil, self is derived from the innermost nesting element. static VALUE rdxr_graph_complete_expression(int argc, VALUE *argv, VALUE self) { VALUE nesting, opts; rb_scan_args(argc, argv, "1:", &nesting, &opts); @@ -633,10 +635,11 @@ static VALUE rdxr_graph_complete_expression(int argc, VALUE *argv, VALUE self) { return completion_result_to_ruby_array(result, self); } -// Graph#complete_namespace_access: (String name, self_receiver: nil) -> Array[Declaration] +// Graph#complete_namespace_access: (String name, self_receiver:) -> Array[Declaration] // Returns completion candidates after a namespace access operator (e.g., `Foo::`). -// The optional self_receiver kwarg is the caller's runtime self type, used to filter -// visibility-restricted singleton methods (e.g., `private_class_method`). +// The required self_receiver kwarg is the caller's runtime self type, used to filter +// visibility-restricted singleton methods (e.g., `private_class_method`). Pass `nil` when there +// is no caller context. static VALUE rdxr_graph_complete_namespace_access(int argc, VALUE *argv, VALUE self) { VALUE name, opts; rb_scan_args(argc, argv, "1:", &name, &opts); @@ -652,10 +655,10 @@ static VALUE rdxr_graph_complete_namespace_access(int argc, VALUE *argv, VALUE s return completion_result_to_ruby_array(result, self); } -// Graph#complete_method_call: (String name, self_receiver: nil) -> Array[Declaration] +// Graph#complete_method_call: (String name, self_receiver:) -> Array[Declaration] // Returns completion candidates after a method call operator (e.g., `foo.`). -// The optional self_receiver kwarg is the caller's runtime self type, used for MRI-style -// visibility checks (private/protected). +// The required self_receiver kwarg is the caller's runtime self type, used for visibility checks (private/protected). +// Pass `nil` when there is no caller context. static VALUE rdxr_graph_complete_method_call(int argc, VALUE *argv, VALUE self) { VALUE name, opts; rb_scan_args(argc, argv, "1:", &name, &opts); @@ -671,9 +674,9 @@ static VALUE rdxr_graph_complete_method_call(int argc, VALUE *argv, VALUE self) return completion_result_to_ruby_array(result, self); } -// Graph#complete_method_argument: (String name, Array[String] nesting, self_receiver: nil) -> Array[Declaration | Keyword | KeywordParameter] +// Graph#complete_method_argument: (String name, Array[String] nesting, self_receiver:) -> Array[Declaration | Keyword | KeywordParameter] // Returns completion candidates inside a method call's argument list (e.g., `foo.bar(|)`). -// See complete_expression for semantics of self_receiver. +// See complete_expression for semantics of self_receiver (required, may be nil). static VALUE rdxr_graph_complete_method_argument(int argc, VALUE *argv, VALUE self) { VALUE name, nesting, opts; rb_scan_args(argc, argv, "2:", &name, &nesting, &opts); diff --git a/rbi/rubydex.rbi b/rbi/rubydex.rbi index b0ac1ced..8ad9900c 100644 --- a/rbi/rubydex.rbi +++ b/rbi/rubydex.rbi @@ -344,48 +344,48 @@ class Rubydex::Graph # Returns completion candidates for an expression context. This includes all keywords, constants, methods, instance # variables, class variables and global variables reachable from the current lexical scope and self type. # - # The nesting array represents the lexical scope stack. The optional `self_receiver` keyword argument overrides the + # The nesting array represents the lexical scope stack. The required `self_receiver` keyword argument overrides the # self type independently of the lexical scope (e.g., `"Foo::"` for `def Foo.bar`). This distinction is important # because constants and class variables are always attached to the lexical scope. Meanwhile, methods and instance - # variables are attached to the type of `self` and those don't always match. + # variables are attached to the type of `self` and those don't always match. Pass `nil` when the self type is unknown sig do params( nesting: T::Array[String], self_receiver: T.nilable(String), ).returns(T::Array[T.any(Rubydex::Declaration, Rubydex::Keyword)]) end - def complete_expression(nesting, self_receiver: nil); end + def complete_expression(nesting, self_receiver:); end # Returns completion candidates after a namespace access operator (e.g., `Foo::`). This includes all constants and # singleton methods for the namespace and its ancestors. # - # The optional `self_receiver` kwarg is the caller's runtime self type. It's used to filter visibility-restricted - # singleton methods (e.g., `private_class_method`). Pass `nil` (the default) for top-level/script scope. + # The required `self_receiver` kwarg is the caller's runtime self type. It's used to filter visibility-restricted + # singleton methods (e.g., `private_class_method`). Pass `nil` for top-level/script scope. sig do params( name: String, self_receiver: T.nilable(String), ).returns(T::Array[Rubydex::Declaration]) end - def complete_namespace_access(name, self_receiver: nil); end + def complete_namespace_access(name, self_receiver:); end # Returns completion candidates after a method call operator (e.g., `foo.`). This includes all methods that exist on # the type of the receiver and its ancestors. # - # The optional `self_receiver` kwarg is the caller's runtime self type. It's used for visibility checks for `private` - # and `protected` methods. Pass `nil` (the default) for top-level/script scope. + # The required `self_receiver` kwarg is the caller's runtime self type. It's used for visibility checks for `private` + # and `protected` methods. Pass `nil` for top-level/script scope. sig do params( name: String, self_receiver: T.nilable(String), ).returns(T::Array[Rubydex::Method]) end - def complete_method_call(name, self_receiver: nil); end + def complete_method_call(name, self_receiver:); end # Returns completion candidates inside a method call's argument list (e.g., `foo.bar(|)`). This includes everything # that expression completion provides plus keyword argument names of the method being called. # - # See `complete_expression` for the semantics of `nesting` and `self_receiver`. + # See `complete_expression` for the semantics of `nesting` and `self_receiver` (required, may be `nil`). sig do params( name: String, @@ -393,7 +393,7 @@ class Rubydex::Graph self_receiver: T.nilable(String), ).returns(T::Array[T.any(Rubydex::Declaration, Rubydex::Keyword, Rubydex::KeywordParameter)]) end - def complete_method_argument(name, nesting, self_receiver: nil); end + def complete_method_argument(name, nesting, self_receiver:); end private diff --git a/rust/rubydex-sys/src/graph_api.rs b/rust/rubydex-sys/src/graph_api.rs index 13a0cd68..b40f0212 100644 --- a/rust/rubydex-sys/src/graph_api.rs +++ b/rust/rubydex-sys/src/graph_api.rs @@ -801,9 +801,7 @@ fn run_and_finalize_completion( /// /// - `pointer` must be a valid `GraphPointer` previously returned by this crate. /// - `nesting` must point to `nesting_count` valid, null-terminated UTF-8 strings. -/// - `self_receiver` must be null or a valid, null-terminated UTF-8 string. When non-null, it -/// overrides the self-type (e.g., `"Foo::"` for completion inside `def Foo.bar`), while -/// the lexical nesting still comes from `nesting`. +/// - `self_receiver` is the fully qualified name of the **type of `self`** #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_graph_complete_expression( pointer: GraphPointer, diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index b66d1130..85f6f4ec 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -454,20 +454,7 @@ fn expression_completion<'a>( let NameRef::Resolved(name_ref) = name_ref else { return Err(format!("Expected name {nesting_name_id} to be resolved").into()); }; - // When no explicit self is given, self is the innermost lexical scope (the nesting's own declaration). - // When explicit, follow constant aliases so callers can pass whatever the expression that set self - // resolves to without having to unwrap aliases themselves. Missing or non-namespace decls are graph - // inconsistencies and surfaced as errors. - let resolved_self_decl_id = match self_decl_id { - Some(id) => resolve_self_namespace(graph, id)?, - None => *name_ref.declaration_id(), - }; - let self_decl = graph - .declarations() - .get(&resolved_self_decl_id) - .unwrap() - .as_namespace() - .ok_or("Expected associated declaration to be a namespace")?; + let innermost_lexical_decl = graph .declarations() .get(name_ref.declaration_id()) @@ -496,7 +483,16 @@ fn expression_completion<'a>( // Collect methods and instance variables, which are based on the inheritance chain of the `self` type (which may // not match the immediate lexical scope) - collect_methods_and_ivars_from_self(graph, self_decl, &mut context, &mut candidates); + if let Some(self_decl_id) = self_decl_id.map(|id| resolve_self_namespace(graph, id)).transpose()? { + let self_decl = graph + .declarations() + .get(&self_decl_id) + .unwrap() + .as_namespace() + .ok_or("Expected associated declaration to be a namespace")?; + + collect_methods_and_ivars_from_self(graph, self_decl, &mut context, &mut candidates); + } // Keywords are always available in expression contexts candidates.extend(keywords::KEYWORDS.iter().map(CompletionCandidate::Keyword)); @@ -1120,7 +1116,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Child")), nesting_name_id: name_id, }, [ @@ -1169,7 +1165,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Child")), nesting_name_id: name_id, }, [ @@ -1218,7 +1214,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo")), nesting_name_id: name_id, }, [ @@ -1268,7 +1264,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo::")), nesting_name_id: name_id, }, [ @@ -1288,7 +1284,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Bar")), nesting_name_id: name_id, }, [ @@ -1305,6 +1301,48 @@ mod tests { ); } + #[test] + fn completion_candidates_for_instance_variables_inside_singleton_class_body() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + " + class Foo + @class_level_ivar = 1 + + def initialize + @instance_level_ivar = 1 + end + + class << self + @singleton_level_ivar = 1 + end + end + ", + ); + context.resolve(); + + let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id(); + let name_id = Name::new(StringId::from(""), ParentScope::Attached(foo_id), Some(foo_id)).id(); + + assert_declaration_completion_eq!( + context, + CompletionReceiver::Expression { + self_decl_id: Some(DeclarationId::from("Foo::::<>")), + nesting_name_id: name_id, + }, + [ + "Module", + "Class", + "Object", + "BasicObject", + "Kernel", + "Foo", + "Foo::::<>#@singleton_level_ivar" + ] + ); + } + #[test] fn completion_candidates_includes_constants_accessible_within_lexical_scope() { let mut context = GraphTest::new(); @@ -1337,10 +1375,11 @@ mod tests { Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()), ) .id(); + assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Bar")), nesting_name_id: name_id, }, [ @@ -1361,7 +1400,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Bar")), nesting_name_id: name_id, }, [ @@ -1405,7 +1444,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo::Bar")), nesting_name_id: name_id, }, [ @@ -1452,7 +1491,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo::Bar")), nesting_name_id: name_id, }, ["Foo::Bar", "$var2", "$var", "Foo::Bar#bar_m()"] @@ -1982,7 +2021,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::MethodArgument { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo")), nesting_name_id: name_id, method_decl_id: DeclarationId::from("Foo#greet()"), }, @@ -2000,6 +2039,48 @@ mod tests { ); } + #[test] + fn method_argument_in_body_completion_uses_singleton_self() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + " + class Foo + @class_level_ivar = 1 + + def instance_method; end + + def self.configure(name:, label: 'default'); end + + # `configure(...)` is invoked at class body level — cursor inside the args. + end + ", + ); + context.resolve(); + + let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id(); + assert_declaration_completion_eq!( + context, + CompletionReceiver::MethodArgument { + self_decl_id: Some(DeclarationId::from("Foo::")), + nesting_name_id: name_id, + method_decl_id: DeclarationId::from("Foo::#configure()"), + }, + [ + "Module", + "Class", + "Object", + "BasicObject", + "Kernel", + "Foo", + "Foo::#configure()", + "Foo::#@class_level_ivar", + "name:", + "label:" + ] + ); + } + #[test] fn method_argument_completion_no_keyword_params() { let mut context = GraphTest::new(); @@ -2018,7 +2099,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::MethodArgument { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo")), nesting_name_id: name_id, method_decl_id: DeclarationId::from("Foo#bar()"), }, @@ -2044,7 +2125,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::MethodArgument { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo")), nesting_name_id: name_id, method_decl_id: DeclarationId::from("Foo#search()"), }, @@ -2088,7 +2169,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::MethodArgument { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo")), nesting_name_id: name_id, method_decl_id: DeclarationId::from("Foo#bar()"), }, @@ -2181,7 +2262,7 @@ mod tests { assert_completion_eq!( context, CompletionReceiver::MethodArgument { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo")), nesting_name_id: name_id, method_decl_id: DeclarationId::from("Foo#bar()"), }, @@ -2612,7 +2693,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Object")), nesting_name_id: name_id, }, [ @@ -2950,7 +3031,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo")), nesting_name_id: foo_name_id, }, [ @@ -2970,7 +3051,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Bar")), nesting_name_id: bar_name_id, }, [ @@ -3286,7 +3367,7 @@ mod tests { assert_declaration_completion_eq!( context, CompletionReceiver::Expression { - self_decl_id: None, + self_decl_id: Some(DeclarationId::from("Foo")), nesting_name_id: foo_name_id, }, [ diff --git a/test/graph_test.rb b/test/graph_test.rb index f727463a..f1106031 100644 --- a/test/graph_test.rb +++ b/test/graph_test.rb @@ -752,7 +752,7 @@ def test_complete_expression graph.index_source("file:///foo.rb", "class Foo\n CONST = 1\n def bar; end\nend", "ruby") graph.resolve - candidates = graph.complete_expression(["Foo"]) + candidates = graph.complete_expression(["Foo"], self_receiver: "Foo") # Declaration candidates constants = candidates.select { |c| c.is_a?(Rubydex::Constant) } @@ -774,7 +774,7 @@ def test_complete_expression refute_empty(if_keyword.documentation) end - def test_complete_expression_inside_singleton_class_block + def test_complete_expression_inside_singleton_method_body graph = Rubydex::Graph.new graph.index_source("file:///foo.rb", <<~RUBY, "ruby") $global_var = 1 @@ -790,7 +790,7 @@ def bar RUBY graph.resolve - candidates = graph.complete_expression(["Foo", ""]) + candidates = graph.complete_expression(["Foo", ""], self_receiver: "Foo::") # Singleton methods defined in the singleton class block methods = candidates.select { |c| c.is_a?(Rubydex::Method) } @@ -983,18 +983,28 @@ def test_complete_method_argument_raises_on_empty_self_receiver end end - def test_complete_expression_with_nil_self_receiver_matches_no_kwarg + def test_complete_expression_raises_when_self_receiver_kwarg_missing + graph = Rubydex::Graph.new + assert_raises(ArgumentError) { graph.complete_expression(["Foo"]) } + end + + def test_complete_expression_with_nil_self_receiver_skips_self_members graph = Rubydex::Graph.new graph.index_source("file:///foo.rb", <<~RUBY, "ruby") class Foo + CONST = 1 + @ivar = 2 def instance_m; end end RUBY graph.resolve - without_kwarg = graph.complete_expression(["Foo"]).map(&:name) - with_nil = graph.complete_expression(["Foo"], self_receiver: nil).map(&:name) - assert_equal(without_kwarg.sort, with_nil.sort) + candidates = graph.complete_expression(["Foo"], self_receiver: nil) + names = candidates.map(&:name) + + assert_includes(names, "Foo::CONST") + refute_includes(names, "Foo#instance_m()") + refute_includes(names, "Foo::\#@ivar") end def test_complete_expression_raises_on_nonexistent_self_receiver @@ -1041,7 +1051,7 @@ def test_complete_expression_with_empty_nesting graph.index_source("file:///foo.rb", "class Object; end\nclass Foo; end", "ruby") graph.resolve - candidates = graph.complete_expression([]) + candidates = graph.complete_expression([], self_receiver: "Object") # Top-level constants should be reachable (Object context) constants = candidates.select { |c| c.is_a?(Rubydex::Declaration) } @@ -1063,7 +1073,7 @@ def bar graph.resolve assert_raises(ArgumentError) do - graph.complete_expression(["Foo#bar()"]) + graph.complete_expression(["Foo#bar()"], self_receiver: nil) end end @@ -1080,7 +1090,7 @@ def bar; end RUBY graph.resolve - candidates = graph.complete_namespace_access("Foo") + candidates = graph.complete_namespace_access("Foo", self_receiver: nil) # All candidates should be Declaration subclasses (no keywords) candidates.each { |c| assert_kind_of(Rubydex::Declaration, c) } @@ -1100,7 +1110,7 @@ def bar graph.resolve assert_raises(ArgumentError) do - graph.complete_namespace_access("Foo#bar()") + graph.complete_namespace_access("Foo#bar()", self_receiver: nil) end end @@ -1109,7 +1119,7 @@ def test_complete_method_call graph.index_source("file:///foo.rb", "class Foo\n def bar; end\n def baz; end\nend", "ruby") graph.resolve - candidates = graph.complete_method_call("Foo") + candidates = graph.complete_method_call("Foo", self_receiver: nil) # All candidates should be Method instances candidates.each { |c| assert_kind_of(Rubydex::Method, c) } @@ -1130,7 +1140,7 @@ def bar graph.resolve assert_raises(ArgumentError) do - graph.complete_method_call("Foo#bar()") + graph.complete_method_call("Foo#bar()", self_receiver: nil) end end @@ -1147,7 +1157,7 @@ def secret; end RUBY graph.resolve - external = graph.complete_method_call("Foo").map(&:name) + external = graph.complete_method_call("Foo", self_receiver: nil).map(&:name) assert_includes(external, "Foo#public_one()") refute_includes(external, "Foo#secret()") @@ -1186,7 +1196,7 @@ class Other refute_includes(unrelated, "Foo#shielded()") # External context: protected access denied. - external = graph.complete_method_call("Foo").map(&:name) + external = graph.complete_method_call("Foo", self_receiver: nil).map(&:name) refute_includes(external, "Foo#shielded()") end @@ -1201,7 +1211,7 @@ class Foo RUBY graph.resolve - candidates = graph.complete_namespace_access("Foo").map(&:name) + candidates = graph.complete_namespace_access("Foo", self_receiver: nil).map(&:name) assert_includes(candidates, "Foo::PUBLIC_CONST") refute_includes(candidates, "Foo::SECRET") end @@ -1211,7 +1221,7 @@ def test_complete_method_argument graph.index_source("file:///foo.rb", "class Foo\n def bar(name:); end\nend", "ruby") graph.resolve - candidates = graph.complete_method_argument("Foo#bar()", ["Foo"]) + candidates = graph.complete_method_argument("Foo#bar()", ["Foo"], self_receiver: "Foo") # Method candidates methods = candidates.select { |c| c.is_a?(Rubydex::Method) } @@ -1237,14 +1247,99 @@ def bar(name:) graph.resolve assert_raises(ArgumentError) do - graph.complete_method_argument("Foo#bar()", ["Foo#bar()"]) + graph.complete_method_argument("Foo#bar()", ["Foo#bar()"], self_receiver: nil) end end + def test_complete_expression_inside_class_body + graph = Rubydex::Graph.new + graph.index_source("file:///foo.rb", <<~RUBY, "ruby") + class Bar + def self.baz; end + end + + class Foo < Bar + @class_level_ivar = 1 + @@class_var = 2 + + def instance_method + @instance_level_ivar = 3 + end + end + RUBY + graph.resolve + + # Lexical scope is Foo, but the self type is Foo:: because it's within the body + candidates = graph.complete_expression(["Foo"], self_receiver: "Foo::").map(&:name) + + assert_includes(candidates, "Foo::\#@class_level_ivar") + assert_includes(candidates, "Foo\#@@class_var") + assert_includes(candidates, "Bar::#baz()") + refute_includes(candidates, "Foo\#@instance_level_ivar") + end + + def test_complete_expression_inside_singleton_class_block + graph = Rubydex::Graph.new + graph.index_source("file:///foo.rb", <<~RUBY, "ruby") + class Foo + @class_level_ivar = 1 + + class << self + @singleton_level_ivar = 2 + end + end + RUBY + graph.resolve + + candidates = graph.complete_expression(["Foo", ""], self_receiver: "Foo::::<>").map(&:name) + + assert_includes(candidates, "Foo::::<>\#@singleton_level_ivar") + refute_includes(candidates, "Foo::\#@class_level_ivar") + end + + def test_complete_method_call_for_singleton_method_from_class_body + graph = Rubydex::Graph.new + graph.index_source("file:///foo.rb", <<~RUBY, "ruby") + class Foo + def self.helper + end + end + RUBY + graph.resolve + + # Calling `Foo.helper` from anywhere — we're querying methods on the singleton class . + candidates = graph.complete_method_call("Foo::", self_receiver: nil) + + methods = candidates.select { |c| c.is_a?(Rubydex::Method) } + assert(methods.any? { |c| c.name == "Foo::#helper()" }) + end + + def test_complete_method_argument_inside_class_body + graph = Rubydex::Graph.new + graph.index_source("file:///foo.rb", <<~RUBY, "ruby") + class Foo + @class_level_ivar = 1 + + def self.helper(name:) + end + + helper() + end + RUBY + graph.resolve + + candidates = graph.complete_method_argument("Foo::#helper()", ["Foo"], self_receiver: "Foo::").map(&:name) + + assert_includes(candidates, "Foo::\#@class_level_ivar") + assert_includes(candidates, "Foo::#helper()") + assert_includes(candidates, "name") + assert_includes(candidates, "if") + end + def test_complete_expression_raises_with_wrong_types graph = Rubydex::Graph.new - assert_raises(TypeError) { graph.complete_expression("not an array") } - assert_raises(TypeError) { graph.complete_expression([123]) } + assert_raises(TypeError) { graph.complete_expression("not an array", self_receiver: nil) } + assert_raises(TypeError) { graph.complete_expression([123], self_receiver: nil) } end def test_complete_namespace_access_raises_with_wrong_types @@ -1259,17 +1354,17 @@ def test_complete_method_call_raises_with_wrong_types def test_complete_method_argument_raises_with_wrong_types graph = Rubydex::Graph.new - assert_raises(TypeError) { graph.complete_method_argument(123, []) } - assert_raises(TypeError) { graph.complete_method_argument("Foo#bar()", "not an array") } - assert_raises(TypeError) { graph.complete_method_argument("Foo#bar()", [123]) } + assert_raises(TypeError) { graph.complete_method_argument(123, [], self_receiver: nil) } + assert_raises(TypeError) { graph.complete_method_argument("Foo#bar()", "not an array", self_receiver: nil) } + assert_raises(TypeError) { graph.complete_method_argument("Foo#bar()", [123], self_receiver: nil) } end def test_completion_returns_empty_for_non_existent_declarations graph = Rubydex::Graph.new graph.resolve - assert_equal([], graph.complete_namespace_access("DoesNotExist")) - assert_equal([], graph.complete_method_call("DoesNotExist")) + assert_equal([], graph.complete_namespace_access("DoesNotExist", self_receiver: nil)) + assert_equal([], graph.complete_method_call("DoesNotExist", self_receiver: nil)) end def test_complete_expression_for_non_existent_nesting @@ -1277,7 +1372,7 @@ def test_complete_expression_for_non_existent_nesting graph.resolve assert_raises(ArgumentError) do - graph.complete_expression(["NonExistent"]) + graph.complete_expression(["NonExistent"], self_receiver: nil) end end @@ -1287,7 +1382,7 @@ def test_complete_expression_on_unresolved_graph # Nesting with a name that exists but hasn't been resolved assert_raises(ArgumentError) do - graph.complete_expression(["Foo"]) + graph.complete_expression(["Foo"], self_receiver: nil) end end