source/analysis/weakenMutableLiterals.ml (580 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 type typed_dictionary_mismatch = | MissingRequiredField of { field_name: Identifier.t; class_name: Identifier.t; } | FieldTypeMismatch of { field_name: Identifier.t; expected_type: Type.t; actual_type: Type.t; class_name: Identifier.t; } | UndefinedField of { field_name: Identifier.t; class_name: Identifier.t; } [@@deriving compare, show, sexp] type weakened_type = { resolved: Type.t; typed_dictionary_errors: typed_dictionary_mismatch Node.t list; } [@@deriving compare, show] let typed_dictionary_errors { typed_dictionary_errors; _ } = typed_dictionary_errors let resolved_type { resolved; _ } = resolved let make_weakened_type ?(typed_dictionary_errors = []) resolved = { resolved; typed_dictionary_errors } let combine_weakened_types weakened_types = { resolved = Type.union (List.map weakened_types ~f:resolved_type); typed_dictionary_errors = List.concat_map weakened_types ~f:typed_dictionary_errors; } let undefined_field_mismatches ~location ~expected_typed_dictionary: { Type.Record.TypedDictionary.fields = expected_fields; name = class_name } ~resolved_typed_dictionary:{ Type.Record.TypedDictionary.fields = resolved_fields; _ } = let make_undefined_field_mismatch { Type.Record.TypedDictionary.name; annotation = _; _ } = UndefinedField { field_name = name; class_name } |> Node.create ~location in let is_undefined_field field = not (List.exists expected_fields ~f:(Type.TypedDictionary.same_name field)) in List.filter resolved_fields ~f:is_undefined_field |> List.map ~f:make_undefined_field_mismatch let distribute_union_over_parametric ~parametric_name ~number_of_parameters annotation = match annotation with | Type.Union parameters -> let extract_matching_parameters = function | Type.Parametric { name; parameters } when Identifier.equal name parametric_name && List.length parameters = number_of_parameters -> Type.Parameter.all_singles parameters | _ -> None in let combine_parameters parameters_list = match number_of_parameters with | 1 -> Some [Type.Parameter.Single (Type.union (List.concat parameters_list))] | 2 -> Some [ Type.Parameter.Single (Type.union (List.map ~f:List.hd_exn parameters_list)); Type.Parameter.Single (Type.union (List.map ~f:List.last_exn parameters_list)); ] | _ -> None in List.map parameters ~f:extract_matching_parameters |> Option.all >>= combine_parameters >>| fun parametric_types -> Type.parametric parametric_name parametric_types | _ -> None let rec weaken_mutable_literals resolve ~resolve_items_individually ~get_typed_dictionary ~expression ~resolved ~expected ~comparator = let comparator_without_override = comparator in let comparator = comparator ~get_typed_dictionary_override:(fun _ -> None) in let open Expression in match expression, resolved, expected with | _, _, Type.Union expected_types -> ( let weakened_types = List.map ~f:(fun expected_type -> weaken_mutable_literals ~get_typed_dictionary resolve ~expression ~resolved ~expected:expected_type ~resolve_items_individually ~comparator:comparator_without_override) expected_types in match List.exists2 ~f:(fun { resolved = left; _ } right -> comparator ~left ~right) weakened_types expected_types with | Ok true -> make_weakened_type expected | Ok false -> make_weakened_type ~typed_dictionary_errors:(List.concat_map weakened_types ~f:typed_dictionary_errors) resolved | Unequal_lengths -> make_weakened_type resolved) | ( Some { Node.value = Expression.List items; _ }, Type.Parametric { name = "list" as container_name; parameters = [Single actual_item_type] }, Type.Parametric { name = "list"; parameters = [Single expected_item_type] } ) | ( Some { Node.value = Expression.Set items; _ }, Type.Parametric { name = "set" as container_name; parameters = [Single actual_item_type] }, Type.Parametric { name = "set"; parameters = [Single expected_item_type] } ) -> let weakened_item_types = List.map ~f:(fun item -> let resolved_item_type = resolve item in let resolved = if resolve_items_individually && not (Type.is_top resolved_item_type) then resolved_item_type else actual_item_type in weaken_mutable_literals ~get_typed_dictionary resolve ~expression:(Some item) ~resolved ~expected:expected_item_type ~resolve_items_individually ~comparator:comparator_without_override) items in let { resolved = weakened_item_type; typed_dictionary_errors } = combine_weakened_types weakened_item_types in make_weakened_type ~typed_dictionary_errors (if comparator ~left:weakened_item_type ~right:expected_item_type then expected else Type.parametric container_name [Single weakened_item_type]) | ( Some { Node.value = Expression.ListComprehension _; _ }, Type.Parametric { name = "list"; parameters = [Single actual] }, Type.Parametric { name = "list"; parameters = [Single expected_parameter] } ) when comparator ~left:actual ~right:expected_parameter -> make_weakened_type expected | ( Some { Node.value = Expression.SetComprehension _; _ }, Type.Parametric { name = "set"; parameters = [Single actual] }, Type.Parametric { name = "set"; parameters = [Single expected_parameter] } ) when comparator ~left:actual ~right:expected_parameter -> make_weakened_type expected | ( Some { Node.value = Expression.Tuple items; _ }, Type.Tuple (Concrete actual_item_types), Type.Tuple (Concrete expected_item_types) ) when List.length actual_item_types = List.length expected_item_types -> ( let weakened_item_types = List.map3 ~f:(fun item actual_item_type expected_item_type -> weaken_mutable_literals ~get_typed_dictionary resolve ~expression:(Some item) ~resolved:actual_item_type ~expected:expected_item_type ~resolve_items_individually ~comparator:comparator_without_override) items actual_item_types expected_item_types in match weakened_item_types with | Ok weakened_item_types -> let resolved_types = List.map weakened_item_types ~f:resolved_type in let weakened_type = Type.Tuple (Concrete resolved_types) in make_weakened_type ~typed_dictionary_errors: (List.concat_map weakened_item_types ~f:typed_dictionary_errors) (if comparator ~left:weakened_type ~right:expected then expected else weakened_type) | Unequal_lengths -> make_weakened_type resolved) | ( Some { Node.value = Expression.Tuple items; _ }, Type.Tuple (Concrete actual_item_types), Type.Tuple (Concatenation concatenation) ) -> let weakened_tuple expected_item_type = let weakened_item_types = List.map2 ~f:(fun item actual_item_type -> weaken_mutable_literals ~get_typed_dictionary resolve ~expression:(Some item) ~resolved:actual_item_type ~expected:expected_item_type ~resolve_items_individually ~comparator:comparator_without_override) items actual_item_types in match weakened_item_types with | Ok weakened_item_types -> let { resolved = weakened_item_type; typed_dictionary_errors } = combine_weakened_types weakened_item_types in Some (make_weakened_type ~typed_dictionary_errors (if comparator ~left:weakened_item_type ~right:expected_item_type then expected else resolved)) | Unequal_lengths -> None in Type.OrderedTypes.Concatenation.extract_sole_unbounded_annotation concatenation >>= weakened_tuple |> Option.value ~default:(make_weakened_type resolved) | ( Some { Node.value = Expression.Dictionary { entries; keywords = [] }; location }, _, Type.Primitive _ ) -> ( let open Type.Record.TypedDictionary in match get_typed_dictionary expected with | Some ({ fields = expected_fields; name = expected_class_name } as expected_typed_dictionary) -> let find_matching_field ~name = let matching_name ({ name = expected_name; _ } : Type.t typed_dictionary_field) = String.equal name expected_name in List.find ~f:matching_name in let resolve_entry { Dictionary.Entry.key; value } = let key = resolve key in match key with | Type.Literal (Type.String (Type.LiteralValue name)) -> let annotation, required = let resolved = resolve value in let relax { annotation; _ } = if Type.is_dictionary resolved || Option.is_some (get_typed_dictionary resolved) then weaken_mutable_literals resolve ~expression:(Some value) ~resolved ~expected:annotation ~comparator:comparator_without_override ~resolve_items_individually ~get_typed_dictionary else if comparator ~left:resolved ~right:annotation then make_weakened_type annotation else make_weakened_type resolved in find_matching_field expected_fields ~name >>| (fun field -> relax field, field.required) |> Option.value ~default:(make_weakened_type resolved, true) in Some { name; annotation; required } | _ -> None in let add_missing_fields_if_all_non_required sofar = let is_missing ({ name; _ } : Type.t typed_dictionary_field) = Option.is_none (find_matching_field sofar ~name) in let missing_fields = List.filter expected_fields ~f:is_missing in if List.for_all missing_fields ~f:(fun { required; _ } -> not required) then sofar @ missing_fields else sofar in let fresh_class_name = "$fresh_class_name" in let get_typed_dictionary_override ~typed_dictionary annotation = match annotation with | Type.Primitive name when String.equal name fresh_class_name -> Some typed_dictionary | _ -> None in let weaken_valid_fields fields = let ({ fields = actual_fields; _ } as resolved_typed_dictionary) = add_missing_fields_if_all_non_required fields |> Type.TypedDictionary.anonymous in let less_than_expected = comparator_without_override ~get_typed_dictionary_override: (get_typed_dictionary_override ~typed_dictionary:resolved_typed_dictionary) ~left:(Type.Primitive fresh_class_name) ~right:expected in if less_than_expected then make_weakened_type ~typed_dictionary_errors: (undefined_field_mismatches ~location ~resolved_typed_dictionary ~expected_typed_dictionary) expected else let type_mismatches = let make_type_mismatch { Type.Record.TypedDictionary.name = expected_field_name; annotation = expected_type; _; } { Type.Record.TypedDictionary.annotation = actual_type; required = _; _ } = FieldTypeMismatch { field_name = expected_field_name; expected_type; actual_type; class_name = expected_class_name; } |> Node.create ~location in let find_type_mismatch expected_field = List.find actual_fields ~f:(Type.TypedDictionary.same_name_different_annotation expected_field) >>| make_type_mismatch expected_field in List.filter_map expected_fields ~f:find_type_mismatch in let missing_field_mismatches = let is_missing expected_field = not (List.exists actual_fields ~f:(Type.TypedDictionary.same_name expected_field)) in let make_missing_field_mismatch { Type.Record.TypedDictionary.name = field_name; required; _ } = MissingRequiredField { field_name; class_name = expected_class_name } |> Node.create ~location |> Option.some_if required in List.filter expected_fields ~f:is_missing |> List.filter_map ~f:make_missing_field_mismatch in make_weakened_type ~typed_dictionary_errors:(type_mismatches @ missing_field_mismatches) resolved in let valid_field_or_typed_dictionary_error { name; required; annotation = { resolved; typed_dictionary_errors } as weakened_type; } = match typed_dictionary_errors with | [] -> Ok { name; required; annotation = resolved } | _ -> Error weakened_type in List.map entries ~f:resolve_entry |> Option.all >>| List.map ~f:valid_field_or_typed_dictionary_error >>| Result.combine_errors >>| (function | Ok fields -> weaken_valid_fields fields | Error erroneous_weakened_types -> make_weakened_type ~typed_dictionary_errors: (List.concat_map erroneous_weakened_types ~f:typed_dictionary_errors) resolved) |> Option.value ~default:(make_weakened_type resolved) | None -> make_weakened_type resolved) | ( Some { Node.value = Expression.Dictionary _; _ }, _, Type.Parametric { name = "typing.Mapping" as generic_name; parameters } ) | ( Some { Node.value = Expression.List _; _ }, _, Type.Parametric { name = ("typing.Sequence" | "typing.Iterable") as generic_name; parameters } ) | ( Some { Node.value = Expression.Set _; _ }, _, Type.Parametric { name = "typing.AbstractSet" as generic_name; parameters } ) -> let mutable_generic_name = match generic_name with | "typing.Mapping" -> "dict" | "typing.Sequence" | "typing.Iterable" -> "list" | "typing.AbstractSet" -> "set" | _ -> failwith "Unexpected generic name" in let { resolved = weakened_fallback_type; typed_dictionary_errors } = weaken_mutable_literals ~get_typed_dictionary resolve ~resolved ~expected:(Type.parametric mutable_generic_name parameters) ~comparator:comparator_without_override ~expression ~resolve_items_individually in let resolved = match weakened_fallback_type with | Type.Parametric { name; parameters } when Identifier.equal name mutable_generic_name -> Type.parametric generic_name parameters | _ -> weakened_fallback_type in make_weakened_type ~typed_dictionary_errors resolved | ( Some { Node.value = Expression.Dictionary { entries; _ }; _ }, Type.Parametric { name = "dict"; parameters = [Single actual_key_type; Single actual_value_type] }, Type.Parametric { name = "dict"; parameters = [Single expected_key_type; Single expected_value_type] } ) -> weaken_dictionary_entries ~get_typed_dictionary resolve ~expected ~comparator:comparator_without_override ~entries ~actual_key_type ~actual_value_type ~expected_key_type ~expected_value_type ~resolve_items_individually | ( Some { Node.value = Expression.DictionaryComprehension _; _ }, Type.Parametric { name = "dict"; parameters = [Single actual_key; Single actual_value] }, Type.Parametric { name = "dict"; parameters = [Single expected_key; Single expected_value] } ) when comparator ~left:actual_key ~right:expected_key && comparator ~left:actual_value ~right:expected_value -> make_weakened_type expected | ( Some { Node.value = Expression.Constant (Constant.String { StringLiteral.kind = StringLiteral.String; value = _ }) as expression; _; }, Type.Primitive "str", Type.Literal (Type.String _) ) | ( Some { Node.value = Expression.Constant (Constant.Integer _) as expression; _ }, Type.Primitive "int", Type.Literal (Type.Integer _) ) | ( Some { Node.value = Expression.Constant Constant.(True | False) as expression; _ }, Type.Primitive "bool", Type.Literal (Type.Boolean _) ) | ( Some { Node.value = Expression.Name (Attribute _) as expression; _ }, Type.Primitive _, Type.Literal (Type.EnumerationMember _) ) -> ( match Type.create_literal expression with | Some (Type.Literal _ as actual_literal) -> if Type.equal actual_literal expected then make_weakened_type expected else make_weakened_type actual_literal | _ -> make_weakened_type resolved) | _, _, Type.RecursiveType recursive_type -> let ({ resolved = weakened_fallback_type; _ } as weakened_type) = weaken_mutable_literals ~get_typed_dictionary resolve ~expression ~resolved ~expected:(Type.RecursiveType.unfold_recursive_type recursive_type) ~comparator:comparator_without_override ~resolve_items_individually:true in let resolved = if comparator ~left:weakened_fallback_type ~right:expected then expected else weakened_fallback_type in { weakened_type with resolved } | _ -> weaken_by_distributing_union ~get_typed_dictionary resolve ~expected ~resolved ~comparator:comparator_without_override ~expression ~resolve_items_individually and weaken_dictionary_entries ~get_typed_dictionary resolve ~expected ~comparator ~entries ~actual_key_type ~actual_value_type ~expected_key_type ~expected_value_type ~resolve_items_individually = let comparator_without_override = comparator in let comparator = comparator ~get_typed_dictionary_override:(fun _ -> None) in let { resolved = weakened_key_type; typed_dictionary_errors = key_errors } = List.map ~f:(fun { key; _ } -> weaken_mutable_literals ~get_typed_dictionary resolve ~expression:(Some key) ~resolved:actual_key_type ~expected:expected_key_type ~resolve_items_individually ~comparator:comparator_without_override) entries |> combine_weakened_types in let { resolved = weakened_value_type; typed_dictionary_errors = value_errors } = List.map ~f:(fun { value; _ } -> weaken_mutable_literals ~get_typed_dictionary resolve ~expression:(Some value) ~resolved:(if resolve_items_individually then resolve value else actual_value_type) ~expected:expected_value_type ~resolve_items_individually ~comparator:comparator_without_override) entries |> combine_weakened_types in make_weakened_type ~typed_dictionary_errors:(key_errors @ value_errors) (if comparator ~left:weakened_key_type ~right:expected_key_type && comparator ~left:weakened_value_type ~right:expected_value_type then expected else Type.dictionary ~key:weakened_key_type ~value:weakened_value_type) and weaken_by_distributing_union ~get_typed_dictionary resolve ~expected ~resolved ~comparator ~expression ~resolve_items_individually = let open Expression in let comparator_without_override = comparator in match expression, resolved, expected with | ( Some { Node.value = Expression.List _ | Expression.Set _; _ }, Type.Union _, Type.Parametric { name = ("list" | "set") as parametric_name; parameters = [Single (Type.Union _)] as parameters; } ) | ( Some { Node.value = Expression.Dictionary _; _ }, Type.Union _, Type.Parametric { name = "dict" as parametric_name; parameters = ([Single (Type.Union _); _] | [_; Single (Type.Union _)]) as parameters; } ) -> ( match distribute_union_over_parametric ~parametric_name ~number_of_parameters:(List.length parameters) resolved with | Some resolved -> weaken_mutable_literals ~get_typed_dictionary resolve ~expression ~resolved ~expected ~resolve_items_individually ~comparator:comparator_without_override | None -> make_weakened_type resolved) | _ -> make_weakened_type resolved let weaken_mutable_literals = weaken_mutable_literals ~resolve_items_individually:false