[litho] Split into two checkers: litho-required-props and litho-graphql-field-access

Summary: `litho` checker contained two checkers:  required-props and graphQL field accesses. Although they use the same domain, their reporting conditions and analysis details are different. However, they were bundled into the same analysis by adding disjunctions to `exec_instr` to handle both cases. Let's separate them into two different checkers, keeping a modular transfer function and analyzer that is reused by these two checkers.

Reviewed By: skcho

Differential Revision: D17788834

fbshipit-source-id: 47d77063b
master
Ezgi Çiçek 5 years ago committed by Facebook Github Bot
parent 615100e55e
commit 856dfc5b74

@ -176,13 +176,21 @@ OPTIONS
Activates: Enable --linters and disable all other checkers Activates: Enable --linters and disable all other checkers
(Conversely: --no-linters-only) (Conversely: --no-linters-only)
--litho --litho-graphql-field-access
Activates: Experimental checkers supporting the Litho framework Activates: [EXPERIMENTAL] GraphQL field access check for Litho
(Conversely: --no-litho) (Conversely: --no-litho-graphql-field-access)
--litho-only --litho-graphql-field-access-only
Activates: Enable --litho and disable all other checkers Activates: Enable --litho-graphql-field-access and disable all
(Conversely: --no-litho-only) other checkers (Conversely: --no-litho-graphql-field-access-only)
--litho-required-props
Activates: [EXPERIMENTAL] Required Prop check for Litho
(Conversely: --no-litho-required-props)
--litho-required-props-only
Activates: Enable --litho-required-props and disable all other
checkers (Conversely: --no-litho-required-props-only)
--no-liveness --no-liveness
Deactivates: the detection of dead stores and unused variables Deactivates: the detection of dead stores and unused variables

@ -710,13 +710,23 @@ OPTIONS
in JSON format to stdout (Conversely: in JSON format to stdout (Conversely:
--no-linters-validate-syntax-only) See also infer-capture(1). --no-linters-validate-syntax-only) See also infer-capture(1).
--litho --litho-graphql-field-access
Activates: Experimental checkers supporting the Litho framework Activates: [EXPERIMENTAL] GraphQL field access check for Litho
(Conversely: --no-litho) See also infer-analyze(1). (Conversely: --no-litho-graphql-field-access) See also infer-analyze(1).
--litho-only --litho-graphql-field-access-only
Activates: Enable --litho and disable all other checkers Activates: Enable --litho-graphql-field-access and disable all
(Conversely: --no-litho-only) See also infer-analyze(1). other checkers (Conversely: --no-litho-graphql-field-access-only)
See also infer-analyze(1).
--litho-required-props
Activates: [EXPERIMENTAL] Required Prop check for Litho
(Conversely: --no-litho-required-props) See also infer-analyze(1).
--litho-required-props-only
Activates: Enable --litho-required-props and disable all other
checkers (Conversely: --no-litho-required-props-only)
See also infer-analyze(1).
--no-liveness --no-liveness
Deactivates: the detection of dead stores and unused variables Deactivates: the detection of dead stores and unused variables

@ -710,13 +710,23 @@ OPTIONS
in JSON format to stdout (Conversely: in JSON format to stdout (Conversely:
--no-linters-validate-syntax-only) See also infer-capture(1). --no-linters-validate-syntax-only) See also infer-capture(1).
--litho --litho-graphql-field-access
Activates: Experimental checkers supporting the Litho framework Activates: [EXPERIMENTAL] GraphQL field access check for Litho
(Conversely: --no-litho) See also infer-analyze(1). (Conversely: --no-litho-graphql-field-access) See also infer-analyze(1).
--litho-only --litho-graphql-field-access-only
Activates: Enable --litho and disable all other checkers Activates: Enable --litho-graphql-field-access and disable all
(Conversely: --no-litho-only) See also infer-analyze(1). other checkers (Conversely: --no-litho-graphql-field-access-only)
See also infer-analyze(1).
--litho-required-props
Activates: [EXPERIMENTAL] Required Prop check for Litho
(Conversely: --no-litho-required-props) See also infer-analyze(1).
--litho-required-props-only
Activates: Enable --litho-required-props and disable all other
checkers (Conversely: --no-litho-required-props-only)
See also infer-analyze(1).
--no-liveness --no-liveness
Deactivates: the detection of dead stores and unused variables Deactivates: the detection of dead stores and unused variables

@ -16,7 +16,8 @@ type t =
; class_loads: ClassLoadsDomain.summary option ; class_loads: ClassLoadsDomain.summary option
; cost: CostDomain.summary option ; cost: CostDomain.summary option
; lab_resource_leaks: ResourceLeakDomain.summary option ; lab_resource_leaks: ResourceLeakDomain.summary option
; litho: LithoDomain.t option ; litho_graphql_field_access: LithoDomain.t option
; litho_required_props: LithoDomain.t option
; pulse: PulseSummary.t option ; pulse: PulseSummary.t option
; purity: PurityDomain.summary option ; purity: PurityDomain.summary option
; quandary: QuandarySummary.t option ; quandary: QuandarySummary.t option
@ -43,7 +44,8 @@ let fields =
~buffer_overrun_checker:(fun f -> mk f "BufferOverrunChecker" BufferOverrunCheckerSummary.pp) ~buffer_overrun_checker:(fun f -> mk f "BufferOverrunChecker" BufferOverrunCheckerSummary.pp)
~class_loads:(fun f -> mk f "ClassLoads" ClassLoadsDomain.pp_summary) ~class_loads:(fun f -> mk f "ClassLoads" ClassLoadsDomain.pp_summary)
~cost:(fun f -> mk f "Cost" CostDomain.pp_summary) ~cost:(fun f -> mk f "Cost" CostDomain.pp_summary)
~litho:(fun f -> mk f "Litho" LithoDomain.pp) ~litho_graphql_field_access:(fun f -> mk f "Litho GraphQL Field Access" LithoDomain.pp)
~litho_required_props:(fun f -> mk f "Litho Required Props" LithoDomain.pp)
~pulse:(fun f -> mk f "Pulse" PulseSummary.pp) ~pulse:(fun f -> mk f "Pulse" PulseSummary.pp)
~purity:(fun f -> mk f "Purity" PurityDomain.pp_summary) ~purity:(fun f -> mk f "Purity" PurityDomain.pp_summary)
~quandary:(fun f -> mk f "Quandary" QuandarySummary.pp) ~quandary:(fun f -> mk f "Quandary" QuandarySummary.pp)
@ -69,7 +71,8 @@ let empty =
; class_loads= None ; class_loads= None
; cost= None ; cost= None
; lab_resource_leaks= None ; lab_resource_leaks= None
; litho= None ; litho_graphql_field_access= None
; litho_required_props= None
; pulse= None ; pulse= None
; purity= None ; purity= None
; quandary= None ; quandary= None

@ -21,7 +21,8 @@ include
; class_loads: ClassLoadsDomain.summary option ; class_loads: ClassLoadsDomain.summary option
; cost: CostDomain.summary option ; cost: CostDomain.summary option
; lab_resource_leaks: ResourceLeakDomain.summary option ; lab_resource_leaks: ResourceLeakDomain.summary option
; litho: LithoDomain.t option ; litho_graphql_field_access: LithoDomain.t option
; litho_required_props: LithoDomain.t option
; pulse: PulseSummary.t option ; pulse: PulseSummary.t option
; purity: PurityDomain.summary option ; purity: PurityDomain.summary option
; quandary: QuandarySummary.t option ; quandary: QuandarySummary.t option

@ -30,7 +30,8 @@ type checkers =
; impurity: bool ref ; impurity: bool ref
; inefficient_keyset_iterator: bool ref ; inefficient_keyset_iterator: bool ref
; linters: bool ref ; linters: bool ref
; litho: bool ref ; litho_graphql_field_access: bool ref
; litho_required_props: bool ref
; liveness: bool ref ; liveness: bool ref
; loop_hoisting: bool ref ; loop_hoisting: bool ref
; nullsafe: bool ref ; nullsafe: bool ref
@ -669,7 +670,8 @@ and { annotation_reachability
; impurity ; impurity
; inefficient_keyset_iterator ; inefficient_keyset_iterator
; linters ; linters
; litho ; litho_graphql_field_access
; litho_required_props
; liveness ; liveness
; loop_hoisting ; loop_hoisting
; nullsafe ; nullsafe
@ -718,7 +720,11 @@ and { annotation_reachability
mk_checker ~long:"inefficient-keyset-iterator" ~default:true mk_checker ~long:"inefficient-keyset-iterator" ~default:true
"Check for inefficient uses of keySet iterator that access both the key and the value." "Check for inefficient uses of keySet iterator that access both the key and the value."
and linters = mk_checker ~long:"linters" ~default:true "syntactic linters" and linters = mk_checker ~long:"linters" ~default:true "syntactic linters"
and litho = mk_checker ~long:"litho" "Experimental checkers supporting the Litho framework" and litho_graphql_field_access =
mk_checker ~long:"litho-graphql-field-access"
"[EXPERIMENTAL] GraphQL field access check for Litho"
and litho_required_props =
mk_checker ~long:"litho-required-props" "[EXPERIMENTAL] Required Prop check for Litho"
and liveness = and liveness =
mk_checker ~long:"liveness" ~default:true "the detection of dead stores and unused variables" mk_checker ~long:"liveness" ~default:true "the detection of dead stores and unused variables"
and loop_hoisting = mk_checker ~long:"loop-hoisting" ~default:false "checker for loop-hoisting" and loop_hoisting = mk_checker ~long:"loop-hoisting" ~default:false "checker for loop-hoisting"
@ -793,7 +799,8 @@ and { annotation_reachability
; impurity ; impurity
; inefficient_keyset_iterator ; inefficient_keyset_iterator
; linters ; linters
; litho ; litho_graphql_field_access
; litho_required_props
; liveness ; liveness
; loop_hoisting ; loop_hoisting
; nullsafe ; nullsafe
@ -2961,7 +2968,9 @@ and linters_ignore_clang_failures = !linters_ignore_clang_failures
and linters_validate_syntax_only = !linters_validate_syntax_only and linters_validate_syntax_only = !linters_validate_syntax_only
and litho = !litho and litho_graphql_field_access = !litho_graphql_field_access
and litho_required_props = !litho_required_props
and liveness = !liveness and liveness = !liveness

@ -463,7 +463,9 @@ val linters_ignore_clang_failures : bool
val linters_validate_syntax_only : bool val linters_validate_syntax_only : bool
val litho : bool val litho_graphql_field_access : bool
val litho_required_props : bool
val liveness : bool val liveness : bool

@ -0,0 +1,62 @@
(*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open! IStd
module F = Format
module Domain = LithoDomain
(* return true if this is a graphql getter *)
let is_graphql_function procname summary =
Option.is_none summary
(* we skip analysis of all GraphQL procs *)
&&
match procname with
| Typ.Procname.Java java_procname -> (
PatternMatch.is_getter java_procname
&&
match Typ.Procname.Java.get_package java_procname with
| Some package ->
String.is_prefix ~prefix:"com.facebook.graphql.model" package
| None ->
false )
| _ ->
false
module LithoContext = struct
type t = Domain.t
let check_callee ~callee_pname ~tenv:_ = is_graphql_function callee_pname
let satisfies_heuristic ~callee_pname:_ ~caller_pname:_ = true
let field = Payloads.Fields.litho_graphql_field_access
let should_report proc_desc _tenv =
LithoFramework.is_on_create_layout (Procdesc.get_proc_name proc_desc)
let report astate _tenv summary =
let report_graphql_getter access_path call_chain =
let call_strings =
List.map ~f:(Typ.Procname.to_simplified_string ~withclass:false) call_chain
in
let call_string = String.concat ~sep:"." call_strings in
let message = F.asprintf "%a.%s" AccessPath.pp access_path call_string in
let loc = Summary.get_loc summary in
let ltr = [Errlog.make_trace_element 0 loc message []] in
Reporting.log_error summary ~loc ~ltr IssueType.graphql_field_access message
in
Domain.iter_call_chains ~f:report_graphql_getter astate
let session_name = "litho graphql field access"
end
module Analyzer = LithoFramework.MakeAnalyzer (LithoContext)
let checker callback = Analyzer.checker callback

@ -1,341 +0,0 @@
(*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open! IStd
module F = Format
module Domain = LithoDomain
module Payload = SummaryPayload.Make (struct
type t = Domain.t
let field = Payloads.Fields.litho
end)
module LithoFramework = struct
(** return true if this function is part of the Litho framework code rather than client code *)
let is_function = function
| Typ.Procname.Java java_procname -> (
match Typ.Procname.Java.get_package java_procname with
| Some "com.facebook.litho" ->
true
| _ ->
false )
| _ ->
false
let is_component_builder procname tenv =
match procname with
| Typ.Procname.Java java_procname ->
PatternMatch.is_subtype_of_str tenv
(Typ.Procname.Java.get_class_type_name java_procname)
"com.facebook.litho.Component$Builder"
| _ ->
false
let is_component_build_method procname tenv =
match Typ.Procname.get_method procname with
| "build" ->
is_component_builder procname tenv
| _ ->
false
let is_on_create_layout = function
| Typ.Procname.Java java_pname -> (
match Typ.Procname.Java.get_method java_pname with "onCreateLayout" -> true | _ -> false )
| _ ->
false
end
module GraphQLGetters = struct
(* return true if this is a graphql getter *)
let is_function procname summary =
Option.is_none summary
(* we skip analysis of all GraphQL procs *)
&&
match procname with
| Typ.Procname.Java java_procname -> (
PatternMatch.is_getter java_procname
&&
match Typ.Procname.Java.get_package java_procname with
| Some package ->
String.is_prefix ~prefix:"com.facebook.graphql.model" package
| None ->
false )
| _ ->
false
let should_report proc_desc =
LithoFramework.is_on_create_layout (Procdesc.get_proc_name proc_desc)
let report astate summary =
let report_graphql_getter access_path call_chain =
let call_strings =
List.map ~f:(Typ.Procname.to_simplified_string ~withclass:false) call_chain
in
let call_string = String.concat ~sep:"." call_strings in
let message = F.asprintf "%a.%s" AccessPath.pp access_path call_string in
let loc = Summary.get_loc summary in
let ltr = [Errlog.make_trace_element 0 loc message []] in
Reporting.log_error summary ~loc ~ltr IssueType.graphql_field_access message
in
Domain.iter_call_chains ~f:report_graphql_getter astate
end
module RequiredProps = struct
(* VarProp is only for props that have a varArg parameter like
@Prop(varArg = "var_prop") whereas Prop is for everything except. *)
type required_prop = Prop of string | VarProp of {prop: string; var_prop: string}
let get_required_props typename tenv =
let is_required annot_list =
List.exists
~f:(fun (({Annot.parameters} as annot), _) ->
Annotations.annot_ends_with annot Annotations.prop
&& (* Don't count as required if it's @Prop(optional = true) *)
not
(List.exists
~f:(fun Annot.{name; value} ->
Option.value_map name ~default:false ~f:(fun name ->
String.equal "optional" name )
&& String.equal value "true" )
parameters) )
annot_list
in
let get_var_args annot_list =
List.fold ~init:None
~f:(fun acc (({Annot.parameters} as annot), _) ->
if Annotations.annot_ends_with annot Annotations.prop then
(* Pick up the parameter for varArg if it has the form
@Prop(varArg = myProp). *)
List.fold ~init:acc
~f:(fun acc Annot.{name; value} ->
if Option.value_map name ~default:false ~f:(fun name -> String.equal "varArg" name)
then Some value
else acc )
parameters
else acc )
annot_list
in
match Tenv.lookup tenv typename with
| Some {fields} ->
List.filter_map
~f:(fun (fieldname, _, annot) ->
if is_required annot then
let prop = Typ.Fieldname.Java.get_field fieldname in
let var_prop_opt = get_var_args annot in
Some
(Option.value_map var_prop_opt ~default:(Prop prop) ~f:(fun var_prop ->
VarProp {var_prop; prop} ))
else None )
fields
| None ->
[]
let report_missing_required_prop summary prop parent_typename loc =
let message =
let prop_string =
match prop with
| Prop prop ->
F.asprintf "@Prop %s" prop
| VarProp {var_prop; prop} ->
F.asprintf "Either @Prop %s or @Prop(varArg = %s)" prop var_prop
in
F.asprintf "%s is required for component %s, but is not set before the call to build()"
prop_string (Typ.Name.name parent_typename)
in
let ltr = [Errlog.make_trace_element 0 loc message []] in
Reporting.log_error summary ~loc ~ltr IssueType.missing_required_prop message
(* walk backward through [call_chain] and return the first type T <: Component that is not part of
the Litho framework (i.e., is client code) *)
let find_client_component_type call_chain =
List.find_map
~f:(fun pname ->
match pname with
| Typ.Procname.Java java_pname ->
Typ.Name.Java.get_outer_class (Typ.Procname.Java.get_class_type_name java_pname)
| _ ->
None )
call_chain
let should_report proc_desc tenv =
let pname = Procdesc.get_proc_name proc_desc in
(not (LithoFramework.is_function pname))
&& (not (LithoFramework.is_component_build_method pname tenv))
&& Procdesc.get_access proc_desc <> PredSymb.Private
let suffixes = String.Set.of_list ["Attr"; "Dip"; "Px"; "Res"; "Sp"]
let has_prop prop_set prop =
let check prop =
String.Set.mem prop_set prop
|| (* @Prop(resType = ...) myProp can also be set via myProp(), myPropAttr(), myPropDip(), myPropPx(), myPropRes() or myPropSp().
Our annotation parameter parsing is too primitive to identify resType, so just assume
that all @Prop's can be set any of these 6 ways. *)
String.Set.exists prop_set ~f:(fun el ->
String.chop_prefix el ~prefix:prop
|> Option.exists ~f:(fun suffix -> String.Set.mem suffixes suffix) )
in
match prop with
| Prop prop ->
check prop
| VarProp {var_prop; prop} ->
(* @Prop(varArg = myProp) List <?> myPropList can also be set
via myPropList() or myProp().*)
check var_prop || check prop
let report astate tenv summary =
let check_required_prop_chain _ call_chain =
let rev_chain = List.rev call_chain in
match rev_chain with
| pname :: _ when LithoFramework.is_component_build_method pname tenv -> (
(* Here, we'll have a type name like MyComponent$Builder in hand. Truncate the $Builder
part from the typename, then look at the fields of MyComponent to figure out which
ones are annotated with @Prop *)
match find_client_component_type call_chain with
| Some parent_typename ->
let required_props = get_required_props parent_typename tenv in
let prop_set = List.map ~f:Typ.Procname.get_method call_chain |> String.Set.of_list in
List.iter
~f:(fun required_prop ->
if not (has_prop prop_set required_prop) then
report_missing_required_prop summary required_prop parent_typename
(Summary.get_loc summary) )
required_props
| _ ->
() )
| _ ->
()
in
Domain.iter_call_chains ~f:check_required_prop_chain astate
end
module TransferFunctions (CFG : ProcCfg.S) = struct
module CFG = CFG
module Domain = Domain
type extras = ProcData.no_extras
let apply_callee_summary summary_opt caller_pname ret_id_typ actuals astate =
match summary_opt with
| Some summary ->
(* TODO: append paths if the footprint access path is an actual path instead of a var *)
let f_sub {Domain.LocalAccessPath.access_path= (var, _), _} =
match Var.get_footprint_index var with
| Some footprint_index -> (
match List.nth actuals footprint_index with
| Some (HilExp.AccessExpression actual_access_expr) ->
Some
(Domain.LocalAccessPath.make
(HilExp.AccessExpression.to_access_path actual_access_expr)
caller_pname)
| _ ->
None )
| None ->
if Var.is_return var then
Some (Domain.LocalAccessPath.make (ret_id_typ, []) caller_pname)
else None
in
Domain.substitute ~f_sub summary |> Domain.join astate
| None ->
astate
let exec_instr astate (proc_data : extras ProcData.t) _ (instr : HilInstr.t) : Domain.t =
let caller_pname = Summary.get_proc_name proc_data.summary in
match instr with
| Call
( return_base
, Direct (Typ.Procname.Java java_callee_procname as callee_procname)
, (HilExp.AccessExpression receiver_ae :: _ as actuals)
, _
, _ ) ->
let domain_summary =
Payload.read ~caller_summary:proc_data.summary ~callee_pname:callee_procname
in
let receiver =
Domain.LocalAccessPath.make
(HilExp.AccessExpression.to_access_path receiver_ae)
caller_pname
in
if
( LithoFramework.is_component_builder callee_procname proc_data.tenv
(* track Builder's in order to check required prop's *)
|| GraphQLGetters.is_function callee_procname domain_summary
|| (* track GraphQL getters in order to report graphql field accesses *)
Domain.mem receiver astate
(* track anything called on a receiver we're already tracking *) )
&& (not (Typ.Procname.Java.is_static java_callee_procname))
&& not
( LithoFramework.is_function callee_procname
&& not (LithoFramework.is_function caller_pname) )
(* don't track Litho client -> Litho framework calls; we want to use the summaries *)
then
let return_access_path = Domain.LocalAccessPath.make (return_base, []) caller_pname in
let return_calls =
( try Domain.find return_access_path astate
with Caml.Not_found -> Domain.CallSet.empty )
|> Domain.CallSet.add (Domain.MethodCall.make receiver callee_procname)
in
Domain.add return_access_path return_calls astate
else
(* treat it like a normal call *)
apply_callee_summary domain_summary caller_pname return_base actuals astate
| Call (ret_id_typ, Direct callee_procname, actuals, _, _) ->
let summary =
Payload.read ~caller_summary:proc_data.summary ~callee_pname:callee_procname
in
apply_callee_summary summary caller_pname ret_id_typ actuals astate
| Assign (lhs_ae, HilExp.AccessExpression rhs_ae, _) -> (
(* creating an alias for the rhs binding; assume all reads will now occur through the
alias. this helps us keep track of chains in cases like tmp = getFoo(); x = tmp;
tmp.getBar() *)
let lhs_access_path =
Domain.LocalAccessPath.make (HilExp.AccessExpression.to_access_path lhs_ae) caller_pname
in
let rhs_access_path =
Domain.LocalAccessPath.make (HilExp.AccessExpression.to_access_path rhs_ae) caller_pname
in
try
let call_set = Domain.find rhs_access_path astate in
Domain.remove rhs_access_path astate |> Domain.add lhs_access_path call_set
with Caml.Not_found -> astate )
| _ ->
astate
let pp_session_name _node fmt = F.pp_print_string fmt "litho"
end
module Analyzer = LowerHil.MakeAbstractInterpreter (TransferFunctions (ProcCfg.Exceptional))
let checker {Callbacks.summary; exe_env} =
let proc_desc = Summary.get_proc_desc summary in
let tenv = Exe_env.get_tenv exe_env (Summary.get_proc_name summary) in
let proc_data = ProcData.make_default summary tenv in
match Analyzer.compute_post proc_data ~initial:Domain.empty with
| Some post ->
if RequiredProps.should_report proc_desc tenv then RequiredProps.report post tenv summary ;
if GraphQLGetters.should_report proc_desc then GraphQLGetters.report post summary ;
let postprocess astate formal_map : Domain.t =
let f_sub access_path = Domain.LocalAccessPath.to_formal_option access_path formal_map in
Domain.substitute ~f_sub astate
in
let payload = postprocess post (FormalMap.make proc_desc) in
Payload.update_summary payload summary
| None ->
summary

@ -0,0 +1,174 @@
(*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open! IStd
module F = Format
module Domain = LithoDomain
(** return true if this function is part of the Litho framework code rather than client code *)
let is_function = function
| Typ.Procname.Java java_procname -> (
match Typ.Procname.Java.get_package java_procname with
| Some "com.facebook.litho" ->
true
| _ ->
false )
| _ ->
false
let is_component_builder procname tenv =
match procname with
| Typ.Procname.Java java_procname ->
PatternMatch.is_subtype_of_str tenv
(Typ.Procname.Java.get_class_type_name java_procname)
"com.facebook.litho.Component$Builder"
| _ ->
false
let is_component_build_method procname tenv =
match Typ.Procname.get_method procname with
| "build" ->
is_component_builder procname tenv
| _ ->
false
let is_on_create_layout = function
| Typ.Procname.Java java_pname -> (
match Typ.Procname.Java.get_method java_pname with "onCreateLayout" -> true | _ -> false )
| _ ->
false
module type LithoContext = sig
type t
val field : (Payloads.t, t option) Field.t
val check_callee : callee_pname:Typ.Procname.t -> tenv:Tenv.t -> t option -> bool
val satisfies_heuristic : callee_pname:Typ.Procname.t -> caller_pname:Typ.Procname.t -> bool
val should_report : Procdesc.t -> Tenv.t -> bool
val report : t -> Tenv.t -> Summary.t -> unit
val session_name : string
end
module TransferFunctions (CFG : ProcCfg.S) (LithoContext : LithoContext with type t = Domain.t) =
struct
module CFG = CFG
module Domain = Domain
module Payload = SummaryPayload.Make (LithoContext)
type extras = ProcData.no_extras
let apply_callee_summary summary_opt caller_pname ret_id_typ actuals astate =
match summary_opt with
| Some summary ->
(* TODO: append paths if the footprint access path is an actual path instead of a var *)
let f_sub {Domain.LocalAccessPath.access_path= (var, _), _} =
match Var.get_footprint_index var with
| Some footprint_index -> (
match List.nth actuals footprint_index with
| Some (HilExp.AccessExpression actual_access_expr) ->
Some
(Domain.LocalAccessPath.make
(HilExp.AccessExpression.to_access_path actual_access_expr)
caller_pname)
| _ ->
None )
| None ->
if Var.is_return var then
Some (Domain.LocalAccessPath.make (ret_id_typ, []) caller_pname)
else None
in
Domain.substitute ~f_sub summary |> Domain.join astate
| None ->
astate
let exec_instr astate ProcData.{summary; tenv} _ (instr : HilInstr.t) : Domain.t =
let caller_pname = Summary.get_proc_name summary in
match instr with
| Call
( return_base
, Direct (Typ.Procname.Java java_callee_procname as callee_pname)
, (HilExp.AccessExpression receiver_ae :: _ as actuals)
, _
, _ ) ->
let domain_summary = Payload.read ~caller_summary:summary ~callee_pname in
let receiver =
Domain.LocalAccessPath.make
(HilExp.AccessExpression.to_access_path receiver_ae)
caller_pname
in
if
( LithoContext.check_callee ~callee_pname ~tenv domain_summary
|| (* track callee in order to report respective errors *)
Domain.mem receiver astate
(* track anything called on a receiver we're already tracking *) )
&& (not (Typ.Procname.Java.is_static java_callee_procname))
&& LithoContext.satisfies_heuristic ~callee_pname ~caller_pname
then
let return_access_path = Domain.LocalAccessPath.make (return_base, []) caller_pname in
let return_calls =
( try Domain.find return_access_path astate
with Caml.Not_found -> Domain.CallSet.empty )
|> Domain.CallSet.add (Domain.MethodCall.make receiver callee_pname)
in
Domain.add return_access_path return_calls astate
else
(* treat it like a normal call *)
apply_callee_summary domain_summary caller_pname return_base actuals astate
| Call (ret_id_typ, Direct callee_procname, actuals, _, _) ->
let callee_summary = Payload.read ~caller_summary:summary ~callee_pname:callee_procname in
apply_callee_summary callee_summary caller_pname ret_id_typ actuals astate
| Assign (lhs_ae, HilExp.AccessExpression rhs_ae, _) -> (
(* creating an alias for the rhs binding; assume all reads will now occur through the
alias. this helps us keep track of chains in cases like tmp = getFoo(); x = tmp;
tmp.getBar() *)
let lhs_access_path =
Domain.LocalAccessPath.make (HilExp.AccessExpression.to_access_path lhs_ae) caller_pname
in
let rhs_access_path =
Domain.LocalAccessPath.make (HilExp.AccessExpression.to_access_path rhs_ae) caller_pname
in
try
let call_set = Domain.find rhs_access_path astate in
Domain.remove rhs_access_path astate |> Domain.add lhs_access_path call_set
with Caml.Not_found -> astate )
| _ ->
astate
let pp_session_name _node fmt = F.pp_print_string fmt LithoContext.session_name
end
module MakeAnalyzer (LithoContext : LithoContext with type t = Domain.t) = struct
module TF = TransferFunctions (ProcCfg.Exceptional) (LithoContext)
module A = LowerHil.MakeAbstractInterpreter (TF)
let checker {Callbacks.summary; exe_env} =
let proc_desc = Summary.get_proc_desc summary in
let tenv = Exe_env.get_tenv exe_env (Summary.get_proc_name summary) in
let proc_data = ProcData.make_default summary tenv in
match A.compute_post proc_data ~initial:Domain.empty with
| Some post ->
if LithoContext.should_report proc_desc tenv then LithoContext.report post tenv summary ;
let postprocess astate formal_map : Domain.t =
let f_sub access_path = Domain.LocalAccessPath.to_formal_option access_path formal_map in
Domain.substitute ~f_sub astate
in
let payload = postprocess post (FormalMap.make proc_desc) in
TF.Payload.update_summary payload summary
| None ->
summary
end

@ -0,0 +1,161 @@
(*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open! IStd
module F = Format
module Domain = LithoDomain
(* VarProp is only for props that have a varArg parameter like
@Prop(varArg = "var_prop") whereas Prop is for everything except. *)
type required_prop = Prop of string | VarProp of {prop: string; var_prop: string}
let get_required_props typename tenv =
let is_required annot_list =
List.exists
~f:(fun (({Annot.parameters} as annot), _) ->
Annotations.annot_ends_with annot Annotations.prop
&& (* Don't count as required if it's @Prop(optional = true) *)
not
(List.exists
~f:(fun Annot.{name; value} ->
Option.value_map name ~default:false ~f:(fun name -> String.equal "optional" name)
&& String.equal value "true" )
parameters) )
annot_list
in
let get_var_args annot_list =
List.fold ~init:None
~f:(fun acc (({Annot.parameters} as annot), _) ->
if Annotations.annot_ends_with annot Annotations.prop then
(* Pick up the parameter for varArg if it has the form
@Prop(varArg = myProp). *)
List.fold ~init:acc
~f:(fun acc Annot.{name; value} ->
if Option.value_map name ~default:false ~f:(fun name -> String.equal "varArg" name)
then Some value
else acc )
parameters
else acc )
annot_list
in
match Tenv.lookup tenv typename with
| Some {fields} ->
List.filter_map
~f:(fun (fieldname, _, annot) ->
if is_required annot then
let prop = Typ.Fieldname.Java.get_field fieldname in
let var_prop_opt = get_var_args annot in
Some
(Option.value_map var_prop_opt ~default:(Prop prop) ~f:(fun var_prop ->
VarProp {var_prop; prop} ))
else None )
fields
| None ->
[]
let report_missing_required_prop summary prop parent_typename loc =
let message =
let prop_string =
match prop with
| Prop prop ->
F.asprintf "@Prop %s" prop
| VarProp {var_prop; prop} ->
F.asprintf "Either @Prop %s or @Prop(varArg = %s)" prop var_prop
in
F.asprintf "%s is required for component %s, but is not set before the call to build()"
prop_string (Typ.Name.name parent_typename)
in
let ltr = [Errlog.make_trace_element 0 loc message []] in
Reporting.log_error summary ~loc ~ltr IssueType.missing_required_prop message
(* walk backward through [call_chain] and return the first type T <: Component that is not part of
the Litho framework (i.e., is client code) *)
let find_client_component_type call_chain =
List.find_map
~f:(fun pname ->
match pname with
| Typ.Procname.Java java_pname ->
Typ.Name.Java.get_outer_class (Typ.Procname.Java.get_class_type_name java_pname)
| _ ->
None )
call_chain
let suffixes = String.Set.of_list ["Attr"; "Dip"; "Px"; "Res"; "Sp"]
let has_prop prop_set prop =
let check prop =
String.Set.mem prop_set prop
|| (* @Prop(resType = ...) myProp can also be set via myProp(), myPropAttr(), myPropDip(), myPropPx(), myPropRes() or myPropSp().
Our annotation parameter parsing is too primitive to identify resType, so just assume
that all @Prop's can be set any of these 6 ways. *)
String.Set.exists prop_set ~f:(fun el ->
String.chop_prefix el ~prefix:prop
|> Option.exists ~f:(fun suffix -> String.Set.mem suffixes suffix) )
in
match prop with
| Prop prop ->
check prop
| VarProp {var_prop; prop} ->
(* @Prop(varArg = myProp) List <?> myPropList can also be set
via myPropList() or myProp().*)
check var_prop || check prop
module LithoContext = struct
type t = Domain.t
let check_callee ~callee_pname ~tenv _ = LithoFramework.is_component_builder callee_pname tenv
let satisfies_heuristic ~callee_pname ~caller_pname =
(* don't track Litho client -> Litho framework calls; we want to use the summaries *)
not (LithoFramework.is_function callee_pname && not (LithoFramework.is_function caller_pname))
let field = Payloads.Fields.litho_required_props
let should_report proc_desc tenv =
let pname = Procdesc.get_proc_name proc_desc in
(not (LithoFramework.is_function pname))
&& (not (LithoFramework.is_component_build_method pname tenv))
&& Procdesc.get_access proc_desc <> PredSymb.Private
let report astate tenv summary =
let check_required_prop_chain _ call_chain =
let rev_chain = List.rev call_chain in
match rev_chain with
| pname :: _ when LithoFramework.is_component_build_method pname tenv -> (
(* Here, we'll have a type name like MyComponent$Builder in hand. Truncate the $Builder
part from the typename, then look at the fields of MyComponent to figure out which
ones are annotated with @Prop *)
match find_client_component_type call_chain with
| Some parent_typename ->
let required_props = get_required_props parent_typename tenv in
let prop_set = List.map ~f:Typ.Procname.get_method call_chain |> String.Set.of_list in
List.iter
~f:(fun required_prop ->
if not (has_prop prop_set required_prop) then
report_missing_required_prop summary required_prop parent_typename
(Summary.get_loc summary) )
required_props
| _ ->
() )
| _ ->
()
in
Domain.iter_call_chains ~f:check_required_prop_chain astate
let session_name = "litho required props"
end
module Analyzer = LithoFramework.MakeAnalyzer (LithoContext)
let checker callback = Analyzer.checker callback

@ -102,7 +102,12 @@ let all_checkers =
interprocedural later on *) interprocedural later on *)
Procedure ResourceLeaks.checker Procedure ResourceLeaks.checker
, Language.Java ) ] } , Language.Java ) ] }
; {name= "litho"; active= Config.litho; callbacks= [(Procedure Litho.checker, Language.Java)]} ; { name= "litho-required-props"
; active= Config.litho_required_props
; callbacks= [(Procedure RequiredProps.checker, Language.Java)] }
; { name= "litho-graphql-field-access"
; active= Config.litho_graphql_field_access
; callbacks= [(Procedure GraphQLFieldAccess.checker, Language.Java)] }
; {name= "SIOF"; active= Config.siof; callbacks= [(Procedure Siof.checker, Language.Clang)]} ; {name= "SIOF"; active= Config.siof; callbacks= [(Procedure Siof.checker, Language.Clang)]}
; { name= "uninitialized variables" ; { name= "uninitialized variables"
; active= Config.uninit ; active= Config.uninit

@ -5,7 +5,7 @@
TESTS_DIR = ../../.. TESTS_DIR = ../../..
INFER_OPTIONS = --litho-only --debug-exceptions INFER_OPTIONS = --litho-graphql-field-access-only --litho-required-props --debug-exceptions
INFERPRINT_OPTIONS = --issues-tests INFERPRINT_OPTIONS = --issues-tests
SOURCES = $(wildcard *.java) SOURCES = $(wildcard *.java)

Loading…
Cancel
Save