Skip to content

Commit 57f9dd1

Browse files
committed
Fix incorrect analysis report for optional function args
1 parent d680426 commit 57f9dd1

File tree

6 files changed

+299
-45
lines changed

6 files changed

+299
-45
lines changed

analysis/reanalyze/src/DeadCommon.ml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,12 @@ let addDeclaration_ ~config ~decls ~(file : FileContext.t) ?posEnd ?posStart
123123
Declarations.add decls pos decl)
124124

125125
let addValueDeclaration ~config ~decls ~file ?(isToplevel = true)
126-
~(loc : Location.t) ~moduleLoc ?(optionalArgs = OptionalArgs.empty) ~path
127-
~sideEffects name =
126+
?(ownsOptionalArgs = false) ~(loc : Location.t) ~moduleLoc
127+
?(optionalArgs = OptionalArgs.empty) ~path ~sideEffects name =
128128
name
129129
|> addDeclaration_ ~config ~decls ~file
130-
~declKind:(Value {isToplevel; optionalArgs; sideEffects})
130+
~declKind:
131+
(Value {isToplevel; ownsOptionalArgs; optionalArgs; sideEffects})
131132
~loc ~moduleLoc ~path
132133

133134
(** Create a dead code issue. Pure - no side effects. *)

analysis/reanalyze/src/DeadOptionalArgs.ml

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,34 @@ let addFunctionReference ~config ~decls ~cross_file ~(locFrom : Location.t)
77
if active () then
88
let posTo = locTo.loc_start in
99
let posFrom = locFrom.loc_start in
10-
(* Check if target has optional args - for filtering and debug logging *)
10+
(* Only declarations that own optional args should participate in
11+
optional-arg state merging. A function-valued alias like
12+
[let f = useNotification()] can have an optional-arg type, but it is not
13+
the declaration site that should receive warnings. *)
1114
let shouldAdd =
12-
match Declarations.find_opt_builder decls posTo with
13-
| Some {declKind = Value {optionalArgs}} ->
14-
not (OptionalArgs.isEmpty optionalArgs)
15-
| _ -> false
15+
if posTo.pos_fname <> posFrom.pos_fname then
16+
match Declarations.find_opt_builder decls posTo with
17+
| Some {declKind = Value {ownsOptionalArgs; optionalArgs}} ->
18+
ownsOptionalArgs && not (OptionalArgs.isEmpty optionalArgs)
19+
| _ -> false
20+
else
21+
match
22+
( Declarations.find_opt_builder decls posFrom,
23+
Declarations.find_opt_builder decls posTo )
24+
with
25+
| ( Some
26+
{
27+
declKind =
28+
Value {ownsOptionalArgs = true; optionalArgs = sourceArgs};
29+
},
30+
Some
31+
{
32+
declKind =
33+
Value {ownsOptionalArgs = true; optionalArgs = targetArgs};
34+
} ) ->
35+
(not (OptionalArgs.isEmpty sourceArgs))
36+
&& not (OptionalArgs.isEmpty targetArgs)
37+
| _ -> false
1638
in
1739
if shouldAdd then (
1840
if config.DceConfig.cli.debug then
@@ -39,23 +61,44 @@ let rec fromTypeExpr (texpr : Types.type_expr) =
3961
| Tsubst t -> fromTypeExpr t
4062
| _ -> []
4163

42-
let addReferences ~config ~cross_file ~(locFrom : Location.t)
64+
let rec fromTypeExprWithArity (texpr : Types.type_expr) arity =
65+
if arity <= 0 then []
66+
else
67+
match texpr.desc with
68+
| _ when not (active ()) -> []
69+
| Tarrow ({lbl = Optional {txt = s}}, tTo, _, _) ->
70+
s :: fromTypeExprWithArity tTo (arity - 1)
71+
| Tarrow (_, tTo, _, _) -> fromTypeExprWithArity tTo (arity - 1)
72+
| Tlink t -> fromTypeExprWithArity t arity
73+
| Tsubst t -> fromTypeExprWithArity t arity
74+
| _ -> []
75+
76+
let addReferences ~config ~decls ~cross_file ~(locFrom : Location.t)
4377
~(locTo : Location.t) ~(binding : Location.t) ~path (argNames, argNamesMaybe)
4478
=
45-
if active () then (
79+
if active () then
4680
let posTo = locTo.loc_start in
4781
let posFrom = binding.loc_start in
48-
CrossFileItems.add_optional_arg_call cross_file ~pos_from:posFrom
49-
~pos_to:posTo ~arg_names:argNames ~arg_names_maybe:argNamesMaybe;
50-
if config.DceConfig.cli.debug then
51-
let callPos = locFrom.loc_start in
52-
Log_.item
53-
"DeadOptionalArgs.addReferences %s called with optional argNames:%s \
54-
argNamesMaybe:%s %s@."
55-
(path |> DcePath.fromPathT |> DcePath.toString)
56-
(argNames |> String.concat ", ")
57-
(argNamesMaybe |> String.concat ", ")
58-
(callPos |> Pos.toString))
82+
let callPos = locFrom.loc_start in
83+
let shouldAdd =
84+
if posTo.pos_fname <> callPos.pos_fname then true
85+
else
86+
match Declarations.find_opt_builder decls posTo with
87+
| Some {declKind = Value {ownsOptionalArgs; optionalArgs}} ->
88+
ownsOptionalArgs && not (OptionalArgs.isEmpty optionalArgs)
89+
| _ -> false
90+
in
91+
if shouldAdd then (
92+
CrossFileItems.add_optional_arg_call cross_file ~pos_from:posFrom
93+
~pos_to:posTo ~arg_names:argNames ~arg_names_maybe:argNamesMaybe;
94+
if config.DceConfig.cli.debug then
95+
Log_.item
96+
"DeadOptionalArgs.addReferences %s called with optional argNames:%s \
97+
argNamesMaybe:%s %s@."
98+
(path |> DcePath.fromPathT |> DcePath.toString)
99+
(argNames |> String.concat ", ")
100+
(argNamesMaybe |> String.concat ", ")
101+
(callPos |> Pos.toString))
59102

60103
(** Check for optional args issues. Returns issues instead of logging.
61104
Uses optional_args_state map for final computed state. *)

analysis/reanalyze/src/DeadValue.ml

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,28 @@ let collectValueBinding ~config ~decls ~file ~(current_binding : Location.t)
2727
when (not loc_ghost) && not vb.vb_loc.loc_ghost ->
2828
let name = Ident.name id |> Name.create ~isInterface:false in
2929
let optionalArgs =
30-
vb.vb_expr.exp_type |> DeadOptionalArgs.fromTypeExpr
31-
|> OptionalArgs.fromList
30+
match vb.vb_expr.exp_desc with
31+
| Texp_function {arity = Some arity; _} ->
32+
vb.vb_expr.exp_type
33+
|> (fun texpr -> DeadOptionalArgs.fromTypeExprWithArity texpr arity)
34+
|> OptionalArgs.fromList
35+
| Texp_function _ ->
36+
vb.vb_expr.exp_type |> DeadOptionalArgs.fromTypeExpr
37+
|> OptionalArgs.fromList
38+
| _ -> OptionalArgs.empty
39+
in
40+
(* Only actual function declarations own optional-arg diagnostics.
41+
Aliases to function values can expose the same optional-arg type, but
42+
warnings should stay attached to the declaration site. *)
43+
let ownsOptionalArgs =
44+
match vb.vb_expr.exp_desc with
45+
| Texp_function _ -> true
46+
| _ -> false
3247
in
3348
let exists =
3449
match Declarations.find_opt_builder decls loc_start with
3550
| Some {declKind = Value r} ->
51+
r.ownsOptionalArgs <- ownsOptionalArgs;
3652
r.optionalArgs <- optionalArgs;
3753
true
3854
| _ -> false
@@ -48,8 +64,9 @@ let collectValueBinding ~config ~decls ~file ~(current_binding : Location.t)
4864
let isToplevel = oldLastBinding = Location.none in
4965
let sideEffects = SideEffects.checkExpr vb.vb_expr in
5066
name
51-
|> addValueDeclaration ~config ~decls ~file ~isToplevel ~loc
52-
~moduleLoc:modulePath.loc ~optionalArgs ~path ~sideEffects);
67+
|> addValueDeclaration ~config ~decls ~file ~isToplevel
68+
~ownsOptionalArgs ~loc ~moduleLoc:modulePath.loc ~optionalArgs
69+
~path ~sideEffects);
5370
(match Declarations.find_opt_builder decls loc_start with
5471
| None -> ()
5572
| Some decl ->
@@ -74,8 +91,8 @@ let collectValueBinding ~config ~decls ~file ~(current_binding : Location.t)
7491
in
7592
loc
7693

77-
let processOptionalArgs ~config ~cross_file ~expType ~(locFrom : Location.t)
78-
~(binding : Location.t) ~locTo ~path args =
94+
let processOptionalArgs ~config ~decls ~cross_file ~expType
95+
~(locFrom : Location.t) ~(binding : Location.t) ~locTo ~path args =
7996
if expType |> DeadOptionalArgs.hasOptionalArgs then (
8097
let supplied = ref [] in
8198
let suppliedMaybe = ref [] in
@@ -104,13 +121,36 @@ let processOptionalArgs ~config ~cross_file ~expType ~(locFrom : Location.t)
104121
if argIsSupplied = None then suppliedMaybe := s :: !suppliedMaybe
105122
| _ -> ());
106123
(!supplied, !suppliedMaybe)
107-
|> DeadOptionalArgs.addReferences ~config ~cross_file ~locFrom ~locTo
124+
|> DeadOptionalArgs.addReferences ~config ~decls ~cross_file ~locFrom ~locTo
108125
~binding ~path)
109126

110-
let rec collectExpr ~config ~refs ~file_deps ~cross_file
127+
let rec collectExpr ~config ~decls ~refs ~file_deps ~cross_file ~callee_locs
111128
~(last_binding : Location.t) super self (e : Typedtree.expression) =
112129
let locFrom = e.exp_loc in
113130
let binding = last_binding in
131+
let suppressOptionalArgs pos =
132+
match Declarations.find_opt_builder decls pos with
133+
| Some ({declKind = Value ({optionalArgs; _} as value_kind)} as decl)
134+
when not (OptionalArgs.isEmpty optionalArgs) ->
135+
Declarations.replace_builder decls pos
136+
{
137+
decl with
138+
declKind = Value {value_kind with optionalArgs = OptionalArgs.empty};
139+
}
140+
| _ -> ()
141+
in
142+
let rec remove_first target = function
143+
| [] -> []
144+
| x :: xs when x = target -> xs
145+
| x :: xs -> x :: remove_first target xs
146+
in
147+
let callee_loc_opt =
148+
match e.exp_desc with
149+
| Texp_apply {funct = {exp_desc = Texp_ident (_, _, _); exp_loc}; _} ->
150+
Some exp_loc
151+
| _ -> None
152+
in
153+
Option.iter (fun loc -> callee_locs := loc :: !callee_locs) callee_loc_opt;
114154
(match e.exp_desc with
115155
| Texp_ident (_path, _, {Types.val_loc = {loc_ghost = false; _} as locTo}) ->
116156
(* if Path.name _path = "rc" then assert false; *)
@@ -123,9 +163,11 @@ let rec collectExpr ~config ~refs ~file_deps ~cross_file
123163
(locTo.loc_start |> Pos.toString);
124164
References.add_value_ref refs ~posTo:locTo.loc_start
125165
~posFrom:Location.none.loc_start)
126-
else
166+
else (
127167
addValueReference ~config ~refs ~file_deps ~binding ~addFileReference:true
128-
~locFrom ~locTo
168+
~locFrom ~locTo;
169+
if not (List.mem locFrom !callee_locs) then
170+
suppressOptionalArgs locTo.loc_start)
129171
| Texp_apply
130172
{
131173
funct =
@@ -138,7 +180,7 @@ let rec collectExpr ~config ~refs ~file_deps ~cross_file
138180
args;
139181
} ->
140182
args
141-
|> processOptionalArgs ~config ~cross_file ~expType:exp_type
183+
|> processOptionalArgs ~config ~decls ~cross_file ~expType:exp_type
142184
~locFrom:(locFrom : Location.t)
143185
~binding:last_binding ~locTo ~path
144186
| Texp_let
@@ -179,7 +221,7 @@ let rec collectExpr ~config ~refs ~file_deps ~cross_file
179221
&& Ident.name etaArg = "eta"
180222
&& Path.name idArg2 = "arg" ->
181223
args
182-
|> processOptionalArgs ~config ~cross_file ~expType:exp_type
224+
|> processOptionalArgs ~config ~decls ~cross_file ~expType:exp_type
183225
~locFrom:(locFrom : Location.t)
184226
~binding:last_binding ~locTo ~path
185227
| Texp_field
@@ -206,12 +248,16 @@ let rec collectExpr ~config ~refs ~file_deps ~cross_file
206248
->
207249
(* Punned field in OCaml projects has ghost location in expression *)
208250
let e = {e with exp_loc = {exp_loc with loc_ghost = false}} in
209-
collectExpr ~config ~refs ~file_deps ~cross_file ~last_binding
210-
super self e
251+
collectExpr ~config ~decls ~refs ~file_deps ~cross_file
252+
~callee_locs ~last_binding super self e
211253
|> ignore
212254
| _ -> ())
213255
| _ -> ());
214-
super.Tast_mapper.expr self e
256+
let result = super.Tast_mapper.expr self e in
257+
Option.iter
258+
(fun loc -> callee_locs := remove_first loc !callee_locs)
259+
callee_loc_opt;
260+
result
215261

216262
(*
217263
type k. is a locally abstract type
@@ -279,13 +325,17 @@ let rec processSignatureItem ~config ~decls ~file ~doTypes ~doValues ~moduleLoc
279325
let optionalArgs =
280326
val_type |> DeadOptionalArgs.fromTypeExpr |> OptionalArgs.fromList
281327
in
328+
(* Signature items only expose the function type, so we conservatively
329+
seed ownership from the presence of optional args. The implementation
330+
pass above refines this for aliases that should not own warnings. *)
331+
let ownsOptionalArgs = not (OptionalArgs.isEmpty optionalArgs) in
282332

283333
(* if Ident.name id = "someValue" then
284334
Printf.printf "XXX %s\n" (Ident.name id); *)
285335
Ident.name id
286336
|> Name.create ~isInterface:false
287337
|> addValueDeclaration ~config ~decls ~file ~loc ~moduleLoc
288-
~optionalArgs ~path ~sideEffects:false
338+
~ownsOptionalArgs ~optionalArgs ~path ~sideEffects:false
289339
| Sig_module (id, {Types.md_type = moduleType; md_loc = moduleLoc}, _)
290340
| Sig_modtype (id, {Types.mtd_type = Some moduleType; mtd_loc = moduleLoc}) ->
291341
let modulePath' =
@@ -309,6 +359,7 @@ let rec processSignatureItem ~config ~decls ~file ~doTypes ~doValues ~moduleLoc
309359
(* Traverse the AST *)
310360
let traverseStructure ~config ~decls ~refs ~file_deps ~cross_file ~file ~doTypes
311361
~doExternals (structure : Typedtree.structure) : unit =
362+
let callee_locs = ref [] in
312363
let rec create_mapper (last_binding : Location.t) (modulePath : ModulePath.t)
313364
=
314365
let super = Tast_mapper.default in
@@ -317,9 +368,8 @@ let traverseStructure ~config ~decls ~refs ~file_deps ~cross_file ~file ~doTypes
317368
super with
318369
expr =
319370
(fun _self e ->
320-
e
321-
|> collectExpr ~config ~refs ~file_deps ~cross_file ~last_binding
322-
super mapper);
371+
collectExpr ~config ~decls ~refs ~file_deps ~cross_file ~callee_locs
372+
~last_binding super mapper e);
323373
pat = (fun _self p -> p |> collectPattern ~config ~refs super mapper);
324374
structure_item =
325375
(fun _self (structureItem : Typedtree.structure_item) ->

analysis/reanalyze/src/Decl.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Kind = struct
77
| VariantCase
88
| Value of {
99
isToplevel: bool;
10+
mutable ownsOptionalArgs: bool;
1011
mutable optionalArgs: OptionalArgs.t;
1112
sideEffects: bool;
1213
}

0 commit comments

Comments
 (0)