From cfefd203f828ce488360a6fbc3fa336a3e083c34 Mon Sep 17 00:00:00 2001 From: Justin Ngai Date: Wed, 26 Jul 2023 14:56:28 -0700 Subject: [PATCH] Implement attribute core logic given file and position Summary: # Overview Refer to D47564289 This stack of diff will be split into the following steps: 1. Setup `CompletionItem` and helper signatures 2. **Implement attribute completion core logic given file and position and test** 3. Implement prefix filtering and test 4. Implement localBasedLookup completion public endpoint 5. Extend Pyre `requestHandler.ml` with handler for completion request --- # Diff Specific Notes - This diff implements and tests the core location based lookup logic for processing completion requests, leveraging `resolution_from_cfg_data` and `GlobalResolution.ClassSummary` to retrieve attribute information Reviewed By: kinto0 Differential Revision: D47636303 fbshipit-source-id: 2c24a42649bb1df53df8b91f25a577c73d77a3a6 --- source/analysis/locationBasedLookup.ml | 45 +++++- source/analysis/locationBasedLookup.mli | 5 + .../analysis/test/locationBasedLookupTest.ml | 135 ++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) diff --git a/source/analysis/locationBasedLookup.ml b/source/analysis/locationBasedLookup.ml index fc41c088b88..23ae3525ae0 100644 --- a/source/analysis/locationBasedLookup.ml +++ b/source/analysis/locationBasedLookup.ml @@ -773,7 +773,7 @@ let resolve_definition_for_name ~resolution ~module_reference ~define_name ~stat let find_definition = find_definition ~resolution ~module_reference ~define_name ~statement_key in match Node.value expression with | Expression.Name (Name.Identifier identifier) -> find_definition (Reference.create identifier) - | Name (Name.Attribute { base; attribute; _ } as name) -> ( + | Expression.Name (Name.Attribute { base; attribute; _ } as name) -> ( let definition = name_to_reference name >>= find_definition in match definition with | Some definition -> Some definition @@ -812,6 +812,29 @@ let resolve_definition_for_name ~resolution ~module_reference ~define_name ~stat | _ -> None +let resolve_attributes_for_name ~resolution expression = + match Node.value expression with + | Expression.Name (Name.Attribute { base; _ }) -> ( + (* Resolve prefix to check if this is a method. *) + let base_type = + match resolve ~resolution base with + | Some annotation when Type.is_meta annotation -> + (* If it is a call to a class method or static method, `Foo.my_class_method()`, the + resolved base type will be `Type[Foo]`. Extract the class type `Foo`. *) + Some (Type.single_parameter annotation) + | annotation -> annotation + in + let parent_class_summary = + base_type + >>= GlobalResolution.class_summary (Resolution.global_resolution resolution) + >>| Node.value + in + match parent_class_summary with + | Some base_class_summary -> base_class_summary |> ClassSummary.attributes |> Option.some + | None -> None) + | _ -> None + + let resolution_from_cfg_data ~type_environment ~use_postcondition_info @@ -880,6 +903,26 @@ let location_of_definition ~type_environment ~module_reference position = location +let resolve_completions_for_symbol + ~type_environment + { symbol_with_definition; cfg_data; use_postcondition_info } + = + let timer = Timer.start () in + let completions = + match symbol_with_definition with + | Expression expression + | TypeAnnotation expression -> + resolve_attributes_for_name + ~resolution:(resolution_from_cfg_data ~type_environment ~use_postcondition_info cfg_data) + expression + in + Log.log + ~section:`Performance + "locationBasedLookup: Resolve completion for symbol: %d ms" + (Timer.stop_in_ms timer); + completions + + let classify_coverage_data { expression; type_ } = let make_coverage_gap reason = Some { coverage_data = { expression; type_ }; reason } in match type_ with diff --git a/source/analysis/locationBasedLookup.mli b/source/analysis/locationBasedLookup.mli index 5d376aca737..e667564874b 100644 --- a/source/analysis/locationBasedLookup.mli +++ b/source/analysis/locationBasedLookup.mli @@ -106,6 +106,11 @@ val location_of_definition Location.position -> Location.WithModule.t option +val resolve_completions_for_symbol + : type_environment:TypeEnvironment.ReadOnly.t -> + symbol_and_cfg_data -> + ClassSummary.Attribute.t Identifier.SerializableMap.t option + val resolve_type_for_symbol : type_environment:TypeEnvironment.ReadOnly.t -> symbol_and_cfg_data -> diff --git a/source/analysis/test/locationBasedLookupTest.ml b/source/analysis/test/locationBasedLookupTest.ml index 79362195d7b..5d7cd4a9dfb 100644 --- a/source/analysis/test/locationBasedLookupTest.ml +++ b/source/analysis/test/locationBasedLookupTest.ml @@ -1686,6 +1686,140 @@ let test_resolve_definition_for_symbol context = () +let test_resolve_completions_for_symbol context = + let default_external_sources = + [ + ( "library.py", + {| + class Base: ... + + def return_str() -> str: + return "hello" + def contains_kw_args(foo: str, **kwargs) -> str: + return "hello" + |} + ); + ] + in + let module_reference = !&"test" in + let assert_resolved_completion_items + ?(external_sources = default_external_sources) + ~source + expected + = + let type_environment = + let { ScratchProject.BuiltTypeEnvironment.type_environment; _ } = + ScratchProject.setup ~context ["test.py", source] ~external_sources + |> ScratchProject.build_type_environment + in + type_environment + in + let symbol_data = + LocationBasedLookup.find_narrowest_spanning_symbol + ~type_environment + ~module_reference + (find_indicator_position ~source "cursor") + in + let attributes = + match + symbol_data >>= LocationBasedLookup.resolve_completions_for_symbol ~type_environment + with + | Some attributes_map -> attributes_map + | None -> Identifier.SerializableMap.empty + in + let list_diff format list = Format.fprintf format "%s\n" (String.concat ~sep:"\n" list) in + assert_equal + ~cmp:(fun left right -> + let sort_str_list = List.sort ~compare:String.compare in + List.equal String.equal (sort_str_list left) (sort_str_list right)) + ~printer:(String.concat ~sep:", ") + ~pp_diff:(diff ~print:list_diff) + expected + (attributes |> Identifier.SerializableMap.bindings |> List.map ~f:(fun (attr, _) -> attr)) + in + assert_resolved_completion_items + ~source: + {| + def getint() -> int: + # ^- cursor + return 42 + |} + (* TODO(T158922360) not an attribute, modify this testcase when we support local autocomplete *) + []; + assert_resolved_completion_items + ~source: + {| + class Foo: ... + + class Bar: + attribute: Foo = Foo() + attribute2: Foo = Foo() + attribute3: Foo = Foo() + + + Bar().attribute + # ^- cursor + |} + (* Single layer class attribute completion *) + ["attribute3"; "attribute2"; "attribute"]; + assert_resolved_completion_items + ~source: + {| + class Foo: + foo_attribute: int = 1 + foo_attribute2: int = 2 + foo_attribute3: int = 3 + + + class Bar: + attribute: Foo = Foo() + attribute2: Foo = Foo() + attribute3: Foo = Foo() + + + Bar().attribute.foo_attribute + # ^- cursor + |} + (* Multi layer class attribute completion *) + ["foo_attribute"; "foo_attribute2"; "foo_attribute3"]; + assert_resolved_completion_items + ~source: + {| + class Foo: ... + + class Bar: + attribute: Foo = Foo() + attribute2: Foo = Foo() + attribute3: Foo = Foo() + + + Bar().attr + # ^- cursor + |} + (* Incomplete attribute string (attr is not a valid attribute), attribute completion *) + ["attribute"; "attribute2"; "attribute3"]; + assert_resolved_completion_items + ~source: + {| + class Foo: ... + + class Bar: + attribute: Foo = Foo() + attribute2: Foo = Foo() + attribute3: Foo = Foo() + + + Bar().attr + # ^- cursor + |} + (* Incomplete attribute string + cursor at end of line, attribute completion *) + ["attribute"; "attribute2"; "attribute3"]; + () + + +(* TODO(T159483467) Add trailing period attribute autocomplete testcase after ERRPY update + released *) + (* Annotations *) let test_lookup_attributes context = @@ -3685,6 +3819,7 @@ let () = "narrowest_match" >:: test_narrowest_match; "find_narrowest_spanning_symbol" >:: test_find_narrowest_spanning_symbol; "resolve_definition_for_symbol" >:: test_resolve_definition_for_symbol; + "resolve_completions_for_symbol" >:: test_resolve_completions_for_symbol; "lookup_attributes" >:: test_lookup_attributes; "lookup_assign" >:: test_lookup_assign; "lookup_call_arguments" >:: test_lookup_call_arguments;