[required-props] Add call chain to trace

Summary:
Improve the trace by incorporating the callees and their locations in the call chain (i.e. chain of methods starting from `build()` call)

- extend the domain to contain the callee location
- replace the test results with the new traces

This makes our job much easier to debug FPs in a big codebase.

Reviewed By: skcho

Differential Revision: D17788996

fbshipit-source-id: 31938b5fe
master
Ezgi Çiçek 5 years ago committed by Facebook Github Bot
parent 856dfc5b74
commit 2e129a5abe

@ -43,7 +43,10 @@ module LithoContext = struct
let report astate _tenv summary = let report astate _tenv summary =
let report_graphql_getter access_path call_chain = let report_graphql_getter access_path call_chain =
let call_strings = let call_strings =
List.map ~f:(Typ.Procname.to_simplified_string ~withclass:false) call_chain List.map
~f:(fun Domain.MethodCall.{procname} ->
Typ.Procname.to_simplified_string ~withclass:false procname )
call_chain
in in
let call_string = String.concat ~sep:"." call_strings in let call_string = String.concat ~sep:"." call_strings in
let message = F.asprintf "%a.%s" AccessPath.pp access_path call_string in let message = F.asprintf "%a.%s" AccessPath.pp access_path call_string in

@ -27,9 +27,10 @@ module LocalAccessPath = struct
end end
module MethodCall = struct module MethodCall = struct
type t = {receiver: LocalAccessPath.t; procname: Typ.Procname.t} [@@deriving compare] type t = {receiver: LocalAccessPath.t; procname: Typ.Procname.t; location: Location.t}
[@@deriving compare]
let make receiver procname = {receiver; procname} let make receiver procname location = {receiver; procname; location}
let pp fmt {receiver; procname} = let pp fmt {receiver; procname} =
F.fprintf fmt "%a.%a" LocalAccessPath.pp receiver Typ.Procname.pp procname F.fprintf fmt "%a.%a" LocalAccessPath.pp receiver Typ.Procname.pp procname
@ -50,11 +51,11 @@ let substitute ~(f_sub : LocalAccessPath.t -> LocalAccessPath.t option) astate =
in in
let call_set' = let call_set' =
CallSet.fold CallSet.fold
(fun ({procname} as call) call_set_acc -> (fun ({procname; location} as call) call_set_acc ->
let receiver = let receiver =
match f_sub call.receiver with Some receiver' -> receiver' | None -> call.receiver match f_sub call.receiver with Some receiver' -> receiver' | None -> call.receiver
in in
CallSet.add {receiver; procname} call_set_acc ) CallSet.add {receiver; procname; location} call_set_acc )
call_set CallSet.empty call_set CallSet.empty
in in
add access_path' call_set' acc ) add access_path' call_set' acc )
@ -65,12 +66,12 @@ let substitute ~(f_sub : LocalAccessPath.t -> LocalAccessPath.t option) astate =
maximal chain. For example, if the domain encodes the chains foo().bar().goo() and foo().baz(), maximal chain. For example, if the domain encodes the chains foo().bar().goo() and foo().baz(),
[f] will be called once on foo().bar().goo() and once on foo().baz() *) [f] will be called once on foo().bar().goo() and once on foo().baz() *)
let iter_call_chains_with_suffix ~f call_suffix astate = let iter_call_chains_with_suffix ~f call_suffix astate =
let rec unroll_call_ ({receiver; procname} : MethodCall.t) (acc, visited) = let rec unroll_call_ ({receiver; procname} as call : MethodCall.t) (acc, visited) =
let is_cycle (call : MethodCall.t) = let is_cycle (call : MethodCall.t) =
(* detect direct cycles and cycles due to mutual recursion *) (* detect direct cycles and cycles due to mutual recursion *)
LocalAccessPath.equal call.receiver receiver || Typ.Procname.Set.mem call.procname visited LocalAccessPath.equal call.receiver receiver || Typ.Procname.Set.mem call.procname visited
in in
let acc' = procname :: acc in let acc' = call :: acc in
let visited' = Typ.Procname.Set.add procname visited in let visited' = Typ.Procname.Set.add procname visited in
try try
let calls' = find receiver astate in let calls' = find receiver astate in

@ -19,11 +19,12 @@ module LocalAccessPath : sig
val pp : F.formatter -> t -> unit val pp : F.formatter -> t -> unit
end end
(** Called procedure + its receiver *) (** Called procedure & location + its receiver *)
module MethodCall : sig module MethodCall : sig
type t = private {receiver: LocalAccessPath.t; procname: Typ.Procname.t} [@@deriving compare] type t = private {receiver: LocalAccessPath.t; procname: Typ.Procname.t; location: Location.t}
[@@deriving compare]
val make : LocalAccessPath.t -> Typ.Procname.t -> t val make : LocalAccessPath.t -> Typ.Procname.t -> Location.t -> t
val pp : F.formatter -> t -> unit val pp : F.formatter -> t -> unit
end end
@ -36,5 +37,5 @@ val substitute : f_sub:(LocalAccessPath.t -> LocalAccessPath.t option) -> t -> t
(** Substitute each access path in the domain using [f_sub]. If [f_sub] returns None, the (** Substitute each access path in the domain using [f_sub]. If [f_sub] returns None, the
original access path is retained; otherwise, the new one is used *) original access path is retained; otherwise, the new one is used *)
val iter_call_chains : f:(AccessPath.t -> Typ.Procname.t list -> unit) -> t -> unit val iter_call_chains : f:(AccessPath.t -> MethodCall.t list -> unit) -> t -> unit
(** Apply [f] to each maximal call chain encoded in [t] *) (** Apply [f] to each maximal call chain encoded in [t] *)

@ -103,7 +103,7 @@ struct
, Direct (Typ.Procname.Java java_callee_procname as callee_pname) , Direct (Typ.Procname.Java java_callee_procname as callee_pname)
, (HilExp.AccessExpression receiver_ae :: _ as actuals) , (HilExp.AccessExpression receiver_ae :: _ as actuals)
, _ , _
, _ ) -> , location ) ->
let domain_summary = Payload.read ~caller_summary:summary ~callee_pname in let domain_summary = Payload.read ~caller_summary:summary ~callee_pname in
let receiver = let receiver =
Domain.LocalAccessPath.make Domain.LocalAccessPath.make
@ -122,7 +122,7 @@ struct
let return_calls = let return_calls =
( try Domain.find return_access_path astate ( try Domain.find return_access_path astate
with Caml.Not_found -> Domain.CallSet.empty ) with Caml.Not_found -> Domain.CallSet.empty )
|> Domain.CallSet.add (Domain.MethodCall.make receiver callee_pname) |> Domain.CallSet.add (Domain.MethodCall.make receiver callee_pname location)
in in
Domain.add return_access_path return_calls astate Domain.add return_access_path return_calls astate
else else

@ -58,7 +58,7 @@ let get_required_props typename tenv =
[] []
let report_missing_required_prop summary prop parent_typename loc = let report_missing_required_prop summary prop parent_typename loc call_chain =
let message = let message =
let prop_string = let prop_string =
match prop with match prop with
@ -70,7 +70,12 @@ let report_missing_required_prop summary prop parent_typename loc =
F.asprintf "%s is required for component %s, but is not set before the call to build()" F.asprintf "%s is required for component %s, but is not set before the call to build()"
prop_string (Typ.Name.name parent_typename) prop_string (Typ.Name.name parent_typename)
in in
let ltr = [Errlog.make_trace_element 0 loc message []] in let ltr =
Errlog.make_trace_element 0 loc message []
:: List.map call_chain ~f:(fun Domain.MethodCall.{procname; location} ->
let call_msg = F.asprintf "calls %a" Typ.Procname.pp procname in
Errlog.make_trace_element 0 location call_msg [] )
in
Reporting.log_error summary ~loc ~ltr IssueType.missing_required_prop message Reporting.log_error summary ~loc ~ltr IssueType.missing_required_prop message
@ -78,8 +83,8 @@ let report_missing_required_prop summary prop parent_typename loc =
the Litho framework (i.e., is client code) *) the Litho framework (i.e., is client code) *)
let find_client_component_type call_chain = let find_client_component_type call_chain =
List.find_map List.find_map
~f:(fun pname -> ~f:(fun Domain.MethodCall.{procname} ->
match pname with match procname with
| Typ.Procname.Java java_pname -> | Typ.Procname.Java java_pname ->
Typ.Name.Java.get_outer_class (Typ.Procname.Java.get_class_type_name java_pname) Typ.Name.Java.get_outer_class (Typ.Procname.Java.get_class_type_name java_pname)
| _ -> | _ ->
@ -131,19 +136,25 @@ module LithoContext = struct
let check_required_prop_chain _ call_chain = let check_required_prop_chain _ call_chain =
let rev_chain = List.rev call_chain in let rev_chain = List.rev call_chain in
match rev_chain with match rev_chain with
| pname :: _ when LithoFramework.is_component_build_method pname tenv -> ( | Domain.MethodCall.{procname} :: _
when LithoFramework.is_component_build_method procname tenv -> (
(* Here, we'll have a type name like MyComponent$Builder in hand. Truncate the $Builder (* 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 part from the typename, then look at the fields of MyComponent to figure out which
ones are annotated with @Prop *) ones are annotated with @Prop *)
match find_client_component_type call_chain with match find_client_component_type call_chain with
| Some parent_typename -> | Some parent_typename ->
let required_props = get_required_props parent_typename tenv in 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 let prop_set =
List.map
~f:(fun Domain.MethodCall.{procname} -> Typ.Procname.get_method procname)
call_chain
|> String.Set.of_list
in
List.iter List.iter
~f:(fun required_prop -> ~f:(fun required_prop ->
if not (has_prop prop_set required_prop) then if not (has_prop prop_set required_prop) then
report_missing_required_prop summary required_prop parent_typename report_missing_required_prop summary required_prop parent_typename
(Summary.get_loc summary) ) (Summary.get_loc summary) call_chain )
required_props required_props
| _ -> | _ ->
() ) () )

@ -1,13 +1,13 @@
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.FP_setRequiredOnBothBranchesOk(boolean):com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.FP_setRequiredOnBothBranchesOk(boolean):com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build(),calls MyComponent$Builder MyComponent$Builder.prop2(Object),calls MyComponent$Builder MyComponent$Builder.prop3(Object),calls MyComponent MyComponent$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildPropResMissingBad():void, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop is required for component codetoanalyze.java.litho.ResPropComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildPropResMissingBad():void, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop is required for component codetoanalyze.java.litho.ResPropComponent, but is not set before the call to build(),calls ResPropComponent ResPropComponent$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildPropVarArgMissingBad():void, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [Either @Prop props or @Prop(varArg = prop) is required for component codetoanalyze.java.litho.VarArgPropComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildPropVarArgMissingBad():void, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [Either @Prop props or @Prop(varArg = prop) is required for component codetoanalyze.java.litho.VarArgPropComponent, but is not set before the call to build(),calls VarArgPropComponent VarArgPropComponent$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildWithColumnChildBad():void, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop3 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildWithColumnChildBad():void, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop3 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build(),calls MyComponent$Builder MyComponent$Builder.prop1(Object),calls Component Component$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildWithout1Bad():com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildWithout1Bad():com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build(),calls MyComponent$Builder MyComponent$Builder.prop2(Object),calls MyComponent$Builder MyComponent$Builder.prop3(Object),calls MyComponent MyComponent$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildWithout3Bad():com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop3 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.buildWithout3Bad():com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop3 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build(),calls MyComponent$Builder MyComponent$Builder.prop1(Object),calls MyComponent$Builder MyComponent$Builder.prop2(Object),calls MyComponent MyComponent$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.callBuildSuffixWithoutRequiredBad():com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.callBuildSuffixWithoutRequiredBad():com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build(),calls MyComponent$Builder MyComponent$Builder.prop2(Object),calls MyComponent$Builder MyComponent$Builder.prop3(Object),calls MyComponent MyComponent$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.generalTypeForgot3Bad():java.lang.Object, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop3 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.generalTypeForgot3Bad():java.lang.Object, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop3 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build(),calls MyComponent$Builder MyComponent$Builder.prop1(Object),calls MyComponent MyComponent$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.setProp3InCalleeButForgetProp1Bad():com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.setProp3InCalleeButForgetProp1Bad():com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build(),calls MyComponent$Builder MyComponent$Builder.prop3(Object),calls MyComponent$Builder MyComponent$Builder.prop2(Object),calls MyComponent MyComponent$Builder.build()]
codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.setRequiredOnOneBranchBad(boolean):com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build()] codetoanalyze/java/litho/RequiredProps.java, codetoanalyze.java.litho.RequiredProps.setRequiredOnOneBranchBad(boolean):com.facebook.litho.Component, 0, MISSING_REQUIRED_PROP, no_bucket, ERROR, [@Prop prop1 is required for component codetoanalyze.java.litho.MyComponent, but is not set before the call to build(),calls MyComponent$Builder MyComponent$Builder.prop2(Object),calls MyComponent$Builder MyComponent$Builder.prop3(Object),calls MyComponent MyComponent$Builder.build()]
codetoanalyze/java/litho/ShouldUpdate.java, com.facebook.graphql.model.LithoTest.onCreateLayout():void, 0, GRAPHQL_FIELD_ACCESS, no_bucket, ERROR, [story.getActors()] codetoanalyze/java/litho/ShouldUpdate.java, com.facebook.graphql.model.LithoTest.onCreateLayout():void, 0, GRAPHQL_FIELD_ACCESS, no_bucket, ERROR, [story.getActors()]
codetoanalyze/java/litho/ShouldUpdate.java, com.facebook.graphql.model.LithoTest.onCreateLayout():void, 0, GRAPHQL_FIELD_ACCESS, no_bucket, ERROR, [story.getActors().get(...).toString()] codetoanalyze/java/litho/ShouldUpdate.java, com.facebook.graphql.model.LithoTest.onCreateLayout():void, 0, GRAPHQL_FIELD_ACCESS, no_bucket, ERROR, [story.getActors().get(...).toString()]
codetoanalyze/java/litho/ShouldUpdate.java, com.facebook.graphql.model.LithoTest.onCreateLayout():void, 0, GRAPHQL_FIELD_ACCESS, no_bucket, ERROR, [story.getActors().get(...)] codetoanalyze/java/litho/ShouldUpdate.java, com.facebook.graphql.model.LithoTest.onCreateLayout():void, 0, GRAPHQL_FIELD_ACCESS, no_bucket, ERROR, [story.getActors().get(...)]

Loading…
Cancel
Save