source/analysis/locationBasedLookup.ml (602 lines of code) (raw):
(*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open Core
open Pyre
open Ast
open Expression
open Statement
type resolved_type_lookup = Type.t Location.Table.t
(** This visitor stores the resolved type formation for an expression on the key of its location.
It special-case names such as named arguments or the names in comprehensions and generators.
The result state of this visitor is ignored. We need two read-only pieces of information to
build the location table: the types resolved for this statement, and a reference to the
(mutable) location table to update. *)
module CreateDefinitionAndAnnotationLookupVisitor = struct
type t = {
pre_resolution: Resolution.t;
post_resolution: Resolution.t;
resolved_types_lookup: resolved_type_lookup;
}
let node_base
~postcondition
({ pre_resolution; post_resolution; resolved_types_lookup; _ } as state)
node
=
let resolve ~resolution ~expression =
try
let annotation = Resolution.resolve_expression_to_annotation resolution expression in
let original = Annotation.original annotation in
if Type.is_top original || Type.is_unbound original then
let annotation = Annotation.annotation annotation in
if Type.is_top annotation || Type.is_unbound annotation then
None
else
Some annotation
else
Some original
with
| ClassHierarchy.Untracked _ -> None
in
let store_resolved_type ({ Node.location; value } as expression) =
let store_lookup ~table ~location data =
if not (Location.equal location Location.any) then
Hashtbl.set table ~key:location ~data |> ignore
in
let store_resolved_type = store_lookup ~table:resolved_types_lookup in
let store_generator_and_compute_resolution
resolution
{ Comprehension.Generator.target; iterator; conditions; _ }
=
(* The basic idea here is to simulate element for x in generator if cond as the following: x
= generator.__iter__().__next__() assert cond element *)
let annotate_expression resolution ({ Node.location; _ } as expression) =
resolve ~resolution ~expression >>| store_resolved_type ~location |> ignore
in
annotate_expression resolution iterator;
let resolution =
let target_assignment =
let iterator_element_call =
let to_call function_name base =
Expression.Call
{
callee =
Node.create_with_default_location
(Expression.Name
(Name.Attribute { base; attribute = function_name; special = false }));
arguments = [];
}
|> Node.create_with_default_location
in
iterator |> to_call "__iter__" |> to_call "__next__"
in
{ Assign.target; value = iterator_element_call; annotation = None }
in
Resolution.resolve_assignment resolution target_assignment
in
let store_condition_and_refine resolution condition =
annotate_expression resolution condition;
Resolution.resolve_assertion resolution ~asserted_expression:condition
|> Option.value ~default:resolution
in
let resolution = List.fold conditions ~f:store_condition_and_refine ~init:resolution in
annotate_expression resolution target;
resolution
in
let resolution = if postcondition then post_resolution else pre_resolution in
resolve ~resolution ~expression >>| store_resolved_type ~location |> ignore;
match value with
| Call { arguments; _ } ->
let annotate_argument_name { Call.Argument.name; value } =
match name, resolve ~resolution ~expression:value with
| Some { Node.location; _ }, Some annotation -> store_resolved_type ~location annotation
| _ -> ()
in
List.iter ~f:annotate_argument_name arguments
| DictionaryComprehension { element = { key; value }; generators; _ } ->
let resolution =
List.fold generators ~f:store_generator_and_compute_resolution ~init:resolution
in
let annotate_expression ({ Node.location; _ } as expression) =
store_resolved_type
~location
(Resolution.resolve_expression_to_type resolution expression)
in
annotate_expression key;
annotate_expression value
| ListComprehension { element; generators; _ }
| SetComprehension { element; generators; _ } ->
let annotate resolution ({ Node.location; _ } as expression) =
resolve ~resolution ~expression >>| store_resolved_type ~location |> ignore
in
let resolution =
List.fold generators ~f:store_generator_and_compute_resolution ~init:resolution
in
annotate resolution element
| _ -> ()
in
match node with
| Visit.Expression expression ->
store_resolved_type expression;
state
| Visit.Reference { Node.value = reference; location } ->
store_resolved_type (Ast.Expression.from_reference ~location reference);
state
| _ -> state
let node = node_base ~postcondition:false
let node_postcondition = node_base ~postcondition:true
let visit_statement_children _ _ = true
let visit_format_string_children _ _ = false
end
(** This is a simple wrapper around [CreateDefinitionAndAnnotationLookupVisitor]. It ensures that
the lookup for type annotations, such as `x: Foo`, points to the definition of the type `Foo`,
not `Type[Foo]`. *)
module CreateLookupsIncludingTypeAnnotationsVisitor = struct
include Visit.MakeNodeVisitor (CreateDefinitionAndAnnotationLookupVisitor)
let visit state source =
let state = ref state in
let visit_statement_override ~state statement =
(* Special-casing for statements that require lookup using the postcondition. *)
let precondition_visit =
visit_expression ~state ~visitor_override:CreateDefinitionAndAnnotationLookupVisitor.node
in
let postcondition_visit =
visit_expression
~state
~visitor_override:CreateDefinitionAndAnnotationLookupVisitor.node_postcondition
in
let store_type_annotation annotation =
let { CreateDefinitionAndAnnotationLookupVisitor.pre_resolution; resolved_types_lookup; _ } =
!state
in
let resolved =
GlobalResolution.parse_annotation (Resolution.global_resolution pre_resolution) annotation
|> Type.meta
in
let location = Node.location annotation in
if not (Location.equal location Location.any) then
Hashtbl.add resolved_types_lookup ~key:location ~data:resolved |> ignore
in
match Node.value statement with
| Statement.Assign { Assign.target; annotation; value; _ } ->
postcondition_visit target;
annotation >>| store_type_annotation |> ignore;
precondition_visit value
| Define
({ Define.signature = { name; parameters; decorators; return_annotation; _ }; _ } as
define) ->
let visit_parameter { Node.value = { Parameter.annotation; value; name }; location } =
(* Location in the AST includes both the parameter name and the annotation. For our
purpose, we just need the location of the name. *)
let location =
let { Location.start = { Location.line = start_line; column = start_column }; _ } =
location
in
{
Location.start = { Location.line = start_line; column = start_column };
stop =
{
Location.line = start_line;
column = start_column + String.length (Identifier.sanitized name);
};
}
in
Expression.Name (Name.Identifier name) |> Node.create ~location |> postcondition_visit;
Option.iter ~f:postcondition_visit value;
annotation >>| store_type_annotation |> ignore
in
precondition_visit
(Ast.Expression.from_reference
~location:(Define.name_location ~body_location:statement.location define)
name);
List.iter parameters ~f:visit_parameter;
List.iter decorators ~f:postcondition_visit;
Option.iter ~f:postcondition_visit return_annotation
| Import { Import.from; imports } ->
let visit_import { Node.value = { Import.name; _ }; location = import_location } =
let qualifier =
match from with
| Some from -> from
| None -> Reference.empty
in
let create_qualified_expression ~location =
Reference.combine qualifier name |> Ast.Expression.from_reference ~location
in
precondition_visit (create_qualified_expression ~location:import_location)
in
List.iter imports ~f:visit_import
| _ -> visit_statement ~state statement
in
List.iter ~f:(visit_statement_override ~state) source.Source.statements;
!state
end
let create_of_module type_environment qualifier =
let resolved_types_lookup = Location.Table.create () in
let global_resolution = TypeEnvironment.ReadOnly.global_resolution type_environment in
let walk_define
({ Node.value = { Define.signature = { name; _ }; _ } as define; _ } as define_node)
=
let resolved_type_lookup =
TypeCheck.get_or_recompute_local_annotations ~environment:type_environment name
|> function
| Some resolved_type_lookup -> resolved_type_lookup
| None -> LocalAnnotationMap.empty () |> LocalAnnotationMap.read_only
in
let cfg = Cfg.create define in
let walk_statement node_id statement_index statement =
let pre_annotations, post_annotations =
let statement_key = [%hash: int * int] (node_id, statement_index) in
( LocalAnnotationMap.ReadOnly.get_precondition resolved_type_lookup ~statement_key
|> Option.value ~default:Refinement.Store.empty,
LocalAnnotationMap.ReadOnly.get_postcondition resolved_type_lookup ~statement_key
|> Option.value ~default:Refinement.Store.empty )
in
let pre_resolution =
(* TODO(T65923817): Eliminate the need of creating a dummy context here *)
TypeCheck.resolution
global_resolution
~annotation_store:pre_annotations
(module TypeCheck.DummyContext)
in
let post_resolution =
(* TODO(T65923817): Eliminate the need of creating a dummy context here *)
TypeCheck.resolution
global_resolution
~annotation_store:post_annotations
(module TypeCheck.DummyContext)
in
CreateLookupsIncludingTypeAnnotationsVisitor.visit
{
CreateDefinitionAndAnnotationLookupVisitor.pre_resolution;
post_resolution;
resolved_types_lookup;
}
(Source.create [statement])
|> ignore
in
let walk_cfg_node ~key:node_id ~data:cfg_node =
let statements = Cfg.Node.statements cfg_node in
List.iteri statements ~f:(walk_statement node_id)
in
Hashtbl.iteri cfg ~f:walk_cfg_node;
(* Special-case define signature processing, since this is not included in the define's cfg. *)
let define_signature =
{ define_node with value = Statement.Define { define with Define.body = [] } }
in
walk_statement Cfg.entry_index 0 define_signature
in
let all_defines =
let unannotated_global_environment =
GlobalResolution.unannotated_global_environment global_resolution
in
UnannotatedGlobalEnvironment.ReadOnly.all_defines_in_module
unannotated_global_environment
qualifier
|> List.filter_map
~f:(UnannotatedGlobalEnvironment.ReadOnly.get_define_body unannotated_global_environment)
in
List.iter all_defines ~f:walk_define;
resolved_types_lookup
let get_best_location lookup_table ~position =
let location_contains_position
{
Location.start = { Location.column = start_column; line = start_line };
stop = { Location.column = stop_column; line = stop_line };
_;
}
{ Location.column; line }
=
let start_ok = start_line < line || (start_line = line && start_column <= column) in
let stop_ok = stop_line > line || (stop_line = line && stop_column > column) in
start_ok && stop_ok
in
let weight
{
Location.start = { Location.column = start_column; line = start_line };
stop = { Location.column = stop_column; line = stop_line };
_;
}
=
((stop_line - start_line) * 1000) + stop_column - start_column
in
Hashtbl.filter_keys lookup_table ~f:(fun key -> location_contains_position key position)
|> Hashtbl.to_alist
|> List.min_elt ~compare:(fun (location_left, _) (location_right, _) ->
weight location_left - weight location_right)
let get_resolved_type = get_best_location
let get_all_resolved_types resolved_types_lookup = Hashtbl.to_alist resolved_types_lookup
type symbol_with_definition =
| Expression of Expression.t
| TypeAnnotation of Expression.t
[@@deriving compare, show]
type cfg_data = {
define_name: Reference.t;
node_id: int;
statement_index: int;
}
[@@deriving compare, show]
type symbol_and_cfg_data = {
symbol_with_definition: symbol_with_definition;
cfg_data: cfg_data;
(* This indicates whether the expression needs to be processed using information after checking
the current statement.
For example, in `x = f(x)`, we want the type of the target `x` after typechecking the statement
but we want the type of the argument `x` before typechecking the statement. *)
use_postcondition_info: bool;
}
[@@deriving compare, show]
let location_insensitive_compare_symbol_and_cfg_data
({ symbol_with_definition = left_symbol_with_definition; _ } as left)
({ symbol_with_definition = right_symbol_with_definition; _ } as right)
=
let first_result =
match left_symbol_with_definition, right_symbol_with_definition with
| Expression left_expression, Expression right_expression
| TypeAnnotation left_expression, TypeAnnotation right_expression ->
Expression.location_insensitive_compare left_expression right_expression
| Expression _, TypeAnnotation _ -> -1
| TypeAnnotation _, Expression _ -> 1
in
if first_result = 0 then
[%compare: symbol_and_cfg_data]
left
{ right with symbol_with_definition = left_symbol_with_definition }
else
first_result
module type PositionData = sig
val position : Location.position
val cfg_data : cfg_data
end
module FindNarrowestSpanningExpression (PositionData : PositionData) = struct
type t = symbol_and_cfg_data list
let node_common ~use_postcondition_info state = function
| Visit.Expression ({ Node.location; _ } as expression)
when Location.contains ~location PositionData.position ->
{
symbol_with_definition = Expression expression;
cfg_data = PositionData.cfg_data;
use_postcondition_info;
}
:: state
| _ -> state
let node = node_common ~use_postcondition_info:false
let node_using_postcondition = node_common ~use_postcondition_info:true
let visit_statement_children _ { Node.location; _ } =
Location.contains ~location PositionData.position
let visit_format_string_children _ _ = false
end
(** This is a simple wrapper around [FindNarrowestSpanningExpression]. It visits imported symbols
and type annotations, and ensures that we use postcondition information when dealing with
function parameters or target variables in assignment statements. . *)
module FindNarrowestSpanningExpressionOrTypeAnnotation (PositionData : PositionData) = struct
include Visit.MakeNodeVisitor (FindNarrowestSpanningExpression (PositionData))
let visit state source =
let visit_statement_for_type_annotations_and_parameters
~state
({ Node.location; _ } as statement)
=
let module Visitor = FindNarrowestSpanningExpression (PositionData) in
let visit_using_precondition_info = visit_expression ~state ~visitor_override:Visitor.node in
let visit_using_postcondition_info =
visit_expression ~state ~visitor_override:Visitor.node_using_postcondition
in
let store_type_annotation ({ Node.location; _ } as annotation) =
if Location.contains ~location PositionData.position then
state :=
{
symbol_with_definition = TypeAnnotation annotation;
cfg_data = PositionData.cfg_data;
use_postcondition_info = false;
}
:: !state
in
if Location.contains ~location PositionData.position then
match Node.value statement with
| Statement.Assign { Assign.target; annotation; value; _ } ->
visit_using_postcondition_info target;
Option.iter annotation ~f:store_type_annotation;
visit_using_precondition_info value
| Define
({ Define.signature = { name; parameters; decorators; return_annotation; _ }; _ } as
define) ->
let visit_parameter { Node.value = { Parameter.annotation; value; name }; location } =
(* Location in the AST includes both the parameter name and the annotation. For our
purpose, we just need the location of the name. *)
let location =
let { Location.start = { Location.line = start_line; column = start_column }; _ } =
location
in
{
Location.start = { Location.line = start_line; column = start_column };
stop =
{
Location.line = start_line;
column = start_column + String.length (Identifier.sanitized name);
};
}
in
Expression.Name (Name.Identifier name)
|> Node.create ~location
|> visit_using_postcondition_info;
Option.iter value ~f:visit_using_postcondition_info;
Option.iter annotation ~f:store_type_annotation
in
let define_name =
Ast.Expression.from_reference
~location:(Define.name_location ~body_location:statement.location define)
name
in
visit_using_precondition_info define_name;
List.iter parameters ~f:visit_parameter;
List.iter decorators ~f:visit_using_postcondition_info;
Option.iter return_annotation ~f:store_type_annotation
(* Note that we do not recurse on the body of the define. That is done by the caller
when walking the CFG. *)
| Import { Import.from; imports } ->
let visit_import { Node.value = { Import.name; _ }; location = import_location } =
let qualifier =
match from with
| Some from -> from
| None -> Reference.empty
in
let create_qualified_expression ~location =
Reference.combine qualifier name |> Ast.Expression.from_reference ~location
in
create_qualified_expression ~location:import_location |> visit_using_precondition_info
in
List.iter imports ~f:visit_import
| _ -> visit_statement ~state statement
in
let state = ref state in
List.iter
~f:(visit_statement_for_type_annotations_and_parameters ~state)
source.Source.statements;
!state
end
let narrowest_match symbol_data_list =
let compare_by_length
{ symbol_with_definition = Expression left | TypeAnnotation left; _ }
{ symbol_with_definition = Expression right | TypeAnnotation right; _ }
=
let open Location in
let { start = left_start; stop = left_stop } = Node.location left in
let { start = right_start; stop = right_stop } = Node.location right in
(* We assume that if expression A overlaps with expression B, then A contains B (or vice versa).
That is, there are no partially-overlapping expressions. *)
if compare_position left_start right_start = -1 || compare_position left_stop right_stop = 1
then
1
else if
compare_position right_start left_start = -1 || compare_position right_stop left_stop = 1
then
-1
else
0
in
List.min_elt ~compare:compare_by_length symbol_data_list
let find_narrowest_spanning_symbol ~type_environment ~module_reference position =
let global_resolution = TypeEnvironment.ReadOnly.global_resolution type_environment in
let walk_define
names_so_far
({ Node.value = { Define.signature = { name; _ }; _ } as define; _ } as define_node)
=
let walk_statement ~node_id statement_index symbols_so_far statement =
let module FindNarrowestSpanningExpressionOrTypeAnnotation =
FindNarrowestSpanningExpressionOrTypeAnnotation (struct
let position = position
let cfg_data = { define_name = name; node_id; statement_index }
end)
in
FindNarrowestSpanningExpressionOrTypeAnnotation.visit [] (Source.create [statement])
@ symbols_so_far
in
let walk_cfg_node ~key:node_id ~data:cfg_node names_so_far =
let statements = Cfg.Node.statements cfg_node in
List.foldi statements ~init:names_so_far ~f:(walk_statement ~node_id)
in
let cfg = Cfg.create define in
let names_so_far = Hashtbl.fold cfg ~init:names_so_far ~f:walk_cfg_node in
(* Special-case define signature processing, since this is not included in the define's cfg. *)
let define_signature =
{ define_node with value = Statement.Define { define with Define.body = [] } }
in
walk_statement ~node_id:Cfg.entry_index 0 names_so_far define_signature
in
let all_defines =
let unannotated_global_environment =
GlobalResolution.unannotated_global_environment global_resolution
in
UnannotatedGlobalEnvironment.ReadOnly.all_defines_in_module
unannotated_global_environment
module_reference
|> List.filter_map
~f:(UnannotatedGlobalEnvironment.ReadOnly.get_define_body unannotated_global_environment)
in
let timer = Timer.start () in
let symbols_covering_position = List.fold all_defines ~init:[] ~f:walk_define in
Log.log
~section:`Performance
"locationBasedLookup: Find narrowest symbol spanning position: %d"
(Timer.stop_in_ms timer);
narrowest_match symbols_covering_position
let resolve ~resolution expression =
try
let annotation = Resolution.resolve_expression_to_annotation resolution expression in
let original = Annotation.original annotation in
if Type.is_top original || Type.is_unbound original then
let annotation = Annotation.annotation annotation in
if Type.is_top annotation || Type.is_unbound annotation then
None
else
Some annotation
else
Some original
with
| ClassHierarchy.Untracked _ -> None
let resolve_definition_for_name ~resolution expression =
let find_definition reference =
GlobalResolution.global_location (Resolution.global_resolution resolution) reference
>>= fun location ->
Option.some_if
(not ([%compare.equal: Location.WithModule.t] location Location.WithModule.any))
location
in
match Node.value expression with
| Expression.Name (Name.Identifier identifier) -> find_definition (Reference.create identifier)
| Name (Name.Attribute { base; attribute; _ } as name) -> (
let definition = name_to_reference name >>= find_definition in
match definition with
| Some definition -> Some definition
| None ->
(* Resolve prefix to check if this is a method. *)
resolve ~resolution base
>>| Type.class_name
>>| (fun prefix -> Reference.create ~prefix attribute)
>>= find_definition)
| _ -> None
let resolve_definition_for_symbol
~type_environment
{
symbol_with_definition;
cfg_data = { define_name; node_id; statement_index };
use_postcondition_info;
}
=
let timer = Timer.start () in
let resolution =
let global_resolution = TypeEnvironment.ReadOnly.global_resolution type_environment in
let resolved_type_lookup =
TypeCheck.get_or_recompute_local_annotations ~environment:type_environment define_name
|> function
| Some resolved_type_lookup -> resolved_type_lookup
| None -> LocalAnnotationMap.empty () |> LocalAnnotationMap.read_only
in
let annotation_store =
let statement_key = [%hash: int * int] (node_id, statement_index) in
if use_postcondition_info then
LocalAnnotationMap.ReadOnly.get_postcondition resolved_type_lookup ~statement_key
|> Option.value ~default:Refinement.Store.empty
else
LocalAnnotationMap.ReadOnly.get_precondition resolved_type_lookup ~statement_key
|> Option.value ~default:Refinement.Store.empty
in
(* TODO(T65923817): Eliminate the need of creating a dummy context here *)
TypeCheck.resolution global_resolution ~annotation_store (module TypeCheck.DummyContext)
in
let definition_location =
match symbol_with_definition with
| Expression expression
| TypeAnnotation expression ->
resolve_definition_for_name ~resolution expression
in
Log.log
~section:`Performance
"locationBasedLookup: Resolve definition for symbol: %d"
(Timer.stop_in_ms timer);
definition_location
let location_of_definition ~type_environment ~module_reference position =
let result = find_narrowest_spanning_symbol ~type_environment ~module_reference position in
result >>= resolve_definition_for_symbol ~type_environment