Skip to content

Commit

Permalink
Implement attribute core logic given file and position
Browse files Browse the repository at this point in the history
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
  • Loading branch information
NgaiJustin authored and facebook-github-bot committed Jul 26, 2023
1 parent ef8d3da commit cfefd20
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 1 deletion.
45 changes: 44 additions & 1 deletion source/analysis/locationBasedLookup.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions source/analysis/locationBasedLookup.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
135 changes: 135 additions & 0 deletions source/analysis/test/locationBasedLookupTest.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit cfefd20

Please sign in to comment.