source/analysis/uninitializedLocalCheck.ml (265 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 Ast
open Pyre
open Statement
open Expression
module Error = AnalysisError
let name = "UninitializedLocal"
module NameAccessSet = Set.Make (Define.NameAccess)
module AccessCollector = struct
let rec from_expression collected { Node.value; location = expression_location } =
let open Expression in
let from_entry collected { Dictionary.Entry.key; value } =
let collected = from_expression collected key in
from_expression collected value
in
match value with
(* Lambdas are speical -- they bind their own names, which we want to exclude *)
| Lambda { Lambda.parameters; body } ->
let collected =
let from_parameter collected { Node.value = { Parameter.value; _ }; _ } =
Option.value_map value ~f:(from_expression collected) ~default:collected
in
List.fold parameters ~init:collected ~f:from_parameter
in
let bound_names =
List.map parameters ~f:(fun { Node.value = { Parameter.name; _ }; _ } ->
Identifier.split_star name |> snd)
|> Identifier.Set.of_list
in
let names_in_body = from_expression NameAccessSet.empty body in
let unbound_names_in_body =
Set.filter names_in_body ~f:(fun { Define.NameAccess.name; _ } ->
not (Identifier.Set.mem bound_names name))
in
Set.union unbound_names_in_body collected
| Name (Name.Identifier identifier) ->
(* For simple names, add them to the result *)
Set.add collected { Define.NameAccess.name = identifier; location = expression_location }
| Name (Name.Attribute _) ->
(* For attribute access, we currently skip *)
collected
(* The rest is boilerplates to make sure that expressions are visited recursively *)
| Await await -> from_expression collected await
| BooleanOperator { BooleanOperator.left; right; _ }
| ComparisonOperator { ComparisonOperator.left; right; _ } ->
let collected = from_expression collected left in
from_expression collected right
| Call { Call.callee; arguments } ->
let collected = from_expression collected callee in
List.fold arguments ~init:collected ~f:(fun collected { Call.Argument.value; _ } ->
from_expression collected value)
| Dictionary { Dictionary.entries; keywords } ->
let collected = List.fold entries ~init:collected ~f:from_entry in
List.fold keywords ~init:collected ~f:from_expression
| DictionaryComprehension comprehension -> from_comprehension from_entry collected comprehension
| Generator comprehension
| ListComprehension comprehension
| SetComprehension comprehension ->
from_comprehension from_expression collected comprehension
| List expressions
| Set expressions
| Tuple expressions ->
List.fold expressions ~init:collected ~f:from_expression
| FormatString substrings ->
let from_substring sofar = function
| Substring.Literal _ -> sofar
| Substring.Format expression -> from_expression sofar expression
in
List.fold substrings ~init:collected ~f:from_substring
| Starred (Starred.Once expression)
| Starred (Starred.Twice expression) ->
from_expression collected expression
| Ternary { Ternary.target; test; alternative } ->
let collected = from_expression collected target in
let collected = from_expression collected test in
from_expression collected alternative
| UnaryOperator { UnaryOperator.operand; _ } -> from_expression collected operand
| WalrusOperator { WalrusOperator.value; _ } -> from_expression collected value
| Yield yield -> Option.value_map yield ~default:collected ~f:(from_expression collected)
| YieldFrom yield -> from_expression collected yield
| Constant _ -> collected
(* Generators are as special as lambdas -- they bind their own names, which we want to exclude *)
and from_comprehension :
'a.
(NameAccessSet.t -> 'a -> NameAccessSet.t) ->
NameAccessSet.t ->
'a Comprehension.t ->
NameAccessSet.t
=
fun from_element collected { Comprehension.element; generators } ->
let remove_bound_names ~bound_names =
Set.filter ~f:(fun { Define.NameAccess.name; _ } -> not (Identifier.Set.mem bound_names name))
in
let bound_names, collected =
let from_generator
(bound_names, accesses_sofar)
{ Comprehension.Generator.target; iterator; conditions; _ }
=
let iterator_accesses =
from_expression NameAccessSet.empty iterator |> remove_bound_names ~bound_names
in
let bound_names =
let add_bound_name bound_names { Define.NameAccess.name; _ } = Set.add bound_names name in
from_expression NameAccessSet.empty target
|> NameAccessSet.fold ~init:bound_names ~f:add_bound_name
in
let condition_accesses =
List.fold conditions ~init:NameAccessSet.empty ~f:from_expression
|> remove_bound_names ~bound_names
in
( bound_names,
NameAccessSet.union_list [accesses_sofar; iterator_accesses; condition_accesses] )
in
List.fold generators ~init:(Identifier.Set.empty, collected) ~f:from_generator
in
let element_accesses =
from_element NameAccessSet.empty element |> remove_bound_names ~bound_names
in
NameAccessSet.union collected element_accesses
end
let extract_reads_expression expression =
let name_access_to_identifier_node { Define.NameAccess.name; location } =
{ Node.value = name; location }
in
AccessCollector.from_expression NameAccessSet.empty expression
|> NameAccessSet.to_list
|> List.map ~f:name_access_to_identifier_node
let extract_reads_statement { Node.value; _ } =
let expressions =
match value with
| Statement.Assign { Assign.value = expression; _ }
| Expression expression
| If { If.test = expression; _ }
| While { While.test = expression; _ } ->
[expression]
| Delete expressions -> expressions
| Assert { Assert.test; message; _ } -> [test] @ Option.to_list message
| For { For.target; iterator; _ } -> [target; iterator]
| Raise { Raise.expression; from } -> Option.to_list expression @ Option.to_list from
| Return { Return.expression; _ } -> Option.to_list expression
| With { With.items; _ } -> items |> List.map ~f:(fun (value, _) -> value)
| Break
| Class _
| Continue
| Define _
| Global _
| Import _
(* TODO(T107105911): Handle access for match statement. *)
| Match _
| Nonlocal _
| Pass
| Try _ ->
[]
in
expressions |> List.concat_map ~f:extract_reads_expression
module InitializedVariables = Identifier.Set
module type Context = sig
val fixpoint_post_statement : (Statement.t * InitializedVariables.t) Int.Table.t
end
module State (Context : Context) = struct
type t =
| Bottom
| Value of InitializedVariables.t
let show = function
| Bottom -> "[]"
| Value state ->
state |> InitializedVariables.elements |> String.concat ~sep:", " |> Format.sprintf "[%s]"
let bottom = Bottom
let pp format state = Format.fprintf format "%s" (show state)
let initial ~define:{ Node.value = { Define.signature; _ }; _ } =
signature.parameters
|> Scope.Binding.of_parameters []
|> List.map ~f:Scope.Binding.name
|> List.map ~f:Identifier.sanitized
|> InitializedVariables.of_list
|> fun value -> Value value
let errors ~qualifier ~define _ =
let emit_error { Node.value; location } =
Error.create
~location:(Location.with_module ~module_reference:qualifier location)
~kind:(Error.UninitializedLocal value)
~define
in
let all_locals =
let { Scope.Scope.bindings; globals; nonlocals; _ } =
Scope.Scope.of_define_exn define.value
in
(* Santitization is needed to remove (some) scope information that is (sometimes, but not
consistently) added into the identifiers themselves (e.g. $local_test?f$y). *)
let locals =
Identifier.Map.keys bindings |> List.map ~f:Identifier.sanitized |> Identifier.Set.of_list
in
(* This operation needs to be repeated as Scope doesn't know about qualification, and hence
doesn't remove all globals and nonlocals from bindings *)
let globals = Identifier.Set.map ~f:Identifier.sanitized globals in
let nonlocals = Identifier.Set.map ~f:Identifier.sanitized nonlocals in
Identifier.Set.diff (Identifier.Set.diff locals globals) nonlocals
in
let in_local_scope { Node.value = identifier; _ } =
identifier |> Identifier.sanitized |> Identifier.Set.mem all_locals
in
let uninitialized_usage (statement, initialized) =
let is_uninitialized { Node.value = identifier; _ } =
not (InitializedVariables.mem initialized (Identifier.sanitized identifier))
in
extract_reads_statement statement |> List.filter ~f:is_uninitialized
in
Int.Table.data Context.fixpoint_post_statement
|> List.map ~f:uninitialized_usage
|> List.concat
|> List.filter ~f:in_local_scope
|> List.map ~f:emit_error
let less_or_equal ~left ~right =
match left, right with
| Value left, Value right -> InitializedVariables.is_subset right ~of_:left
| Value _, Bottom -> false
| Bottom, Value _ -> true
| Bottom, Bottom -> true
let join left right =
match left, right with
| Value left, Value right -> Value (InitializedVariables.inter left right)
| Value left, Bottom -> Value left
| Bottom, Value right -> Value right
| Bottom, Bottom -> Bottom
let widen ~previous ~next ~iteration:_ = join previous next
let forward ~statement_key state ~statement =
match state with
| Bottom -> Bottom
| Value state ->
let new_state =
Scope.Binding.of_statement [] statement
|> List.map ~f:Scope.Binding.name
|> List.map ~f:Identifier.sanitized
|> InitializedVariables.of_list
|> InitializedVariables.union state
in
Hashtbl.set Context.fixpoint_post_statement ~key:statement_key ~data:(statement, new_state);
Value new_state
let backward ~statement_key:_ _ ~statement:_ = failwith "Not implemented"
end
let run_on_define ~qualifier define =
let module Context = struct
let fixpoint_post_statement = Int.Table.create ()
end
in
let module State = State (Context) in
let module Fixpoint = Fixpoint.Make (State) in
let cfg = Cfg.create (Node.value define) in
let fixpoint = Fixpoint.forward ~cfg ~initial:(State.initial ~define) in
Fixpoint.exit fixpoint >>| State.errors ~qualifier ~define |> Option.value ~default:[]
let run
~configuration:_
~environment:_
~source:({ Source.source_path = { SourcePath.qualifier; _ }; _ } as source)
=
source
|> Preprocessing.defines ~include_toplevels:false
|> List.map ~f:(run_on_define ~qualifier)
|> List.concat