Summary: This is a simple checker that identifies inefficient uses of `keySet` iterator where (not only the key but also) the value is accessed via `get(key)`. It is more efficient to use `entrySet` iterator which already returns both key-value pairs. This optimization would get rid of many extra lookups which can be expensive. We simply traverse the CFG starting from the loop head upwards and pick up the map that is iterated over. Then, we check in the loop nodes if there is a call to `get(...)` over this map. If, so we report. Reviewed By: ngorogiannis Differential Revision: D15737779 fbshipit-source-id: 702465b4emaster
parent
8e31b136d0
commit
d2eb3c8cc6
@ -0,0 +1,95 @@
|
||||
(*
|
||||
* Copyright (c) 2019-present, Facebook, Inc.
|
||||
*
|
||||
* 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 CFG = ProcCfg.Normal
|
||||
module LoopNodes = AbstractDomain.FiniteSet (Procdesc.Node)
|
||||
|
||||
let find_loaded_pvar id = function
|
||||
| Sil.Load (lhs_id, Exp.Lvar rhs_pvar, _, _) when Ident.equal lhs_id id ->
|
||||
Some rhs_pvar
|
||||
| _ ->
|
||||
None
|
||||
|
||||
|
||||
let find_first_arg_id ~fun_name ~lhs_f = function
|
||||
| Sil.Call ((lhs_id, _), Exp.Const (Const.Cfun pname), (Exp.Var rhs_id, _) :: _, _, _)
|
||||
when lhs_f lhs_id && String.equal fun_name (Typ.Procname.get_method pname) ->
|
||||
Some rhs_id
|
||||
| _ ->
|
||||
None
|
||||
|
||||
|
||||
(** If given a node that has 4 instructions and calls fun_name,
|
||||
pickup bcvarY, i.e. variable for the first argument
|
||||
n$X = *&$bcvarY
|
||||
_ = *n$X
|
||||
n$X+1 = fun_name(n$X,....)
|
||||
*&$irvarZ = n$X+1
|
||||
*)
|
||||
let find_first_arg_pvar node ~fun_name =
|
||||
let instrs = Procdesc.Node.get_instrs node in
|
||||
if Int.equal (Instrs.count instrs) 4 then
|
||||
let instr_arr = Instrs.get_underlying_not_reversed instrs in
|
||||
match instr_arr.(3) with
|
||||
| Sil.Store (Exp.Lvar _, _, Exp.Var rhs_id, _) ->
|
||||
find_first_arg_id ~fun_name ~lhs_f:(Ident.equal rhs_id) instr_arr.(2)
|
||||
|> Option.bind ~f:(fun arg_id -> find_loaded_pvar arg_id instr_arr.(0))
|
||||
| _ ->
|
||||
None
|
||||
else None
|
||||
|
||||
|
||||
let report_matching_get summary pvar loop_nodes : unit =
|
||||
LoopNodes.iter
|
||||
(fun node ->
|
||||
let instrs = Procdesc.Node.get_instrs node in
|
||||
if Int.equal (Instrs.count instrs) 5 then
|
||||
let instr_arr = Instrs.get_underlying_not_reversed instrs in
|
||||
find_first_arg_id ~fun_name:"get" ~lhs_f:(fun _ -> true) instr_arr.(3)
|
||||
|> Option.iter ~f:(fun arg_id ->
|
||||
find_loaded_pvar arg_id instr_arr.(0)
|
||||
|> Option.iter ~f:(fun arg_pvar ->
|
||||
if Pvar.equal arg_pvar pvar then
|
||||
let pp_m = MarkupFormatter.pp_monospaced in
|
||||
let exp_desc =
|
||||
F.asprintf
|
||||
"Accessing a value using a key that was retrieved from a %a iterator. \
|
||||
It is more efficient to use an iterator on the %a of the map, \
|
||||
avoiding the extra %a lookup."
|
||||
pp_m "keySet" pp_m "entrySet" pp_m "HashMap.get(key)"
|
||||
in
|
||||
let loc = Procdesc.Node.get_loc node in
|
||||
let ltr = [Errlog.make_trace_element 0 loc exp_desc []] in
|
||||
Reporting.log_error summary ~loc ~ltr IssueType.inefficient_keyset_iterator
|
||||
exp_desc ) ) )
|
||||
loop_nodes
|
||||
|
||||
|
||||
let when_dominating_pred_satisfies idom my_node ~f =
|
||||
let preds =
|
||||
Procdesc.Node.get_preds my_node
|
||||
|> List.filter ~f:(fun node -> Dominators.dominates idom node my_node)
|
||||
in
|
||||
match preds with [pred_node] -> f pred_node | _ -> ()
|
||||
|
||||
|
||||
let checker Callbacks.{summary; proc_desc} : Summary.t =
|
||||
let cfg = CFG.from_pdesc proc_desc in
|
||||
let _, loop_head_to_loop_nodes = Loop_control.get_loop_control_maps cfg in
|
||||
let idom = Dominators.get_idoms proc_desc in
|
||||
Procdesc.NodeMap.iter
|
||||
(fun loop_head loop_nodes ->
|
||||
if find_first_arg_pvar loop_head ~fun_name:"hasNext" |> Option.is_some then
|
||||
when_dominating_pred_satisfies idom loop_head ~f:(fun itr_node ->
|
||||
if Option.is_some (find_first_arg_pvar itr_node ~fun_name:"iterator") then
|
||||
when_dominating_pred_satisfies idom itr_node ~f:(fun keySet_node ->
|
||||
find_first_arg_pvar keySet_node ~fun_name:"keySet"
|
||||
|> Option.iter ~f:(fun get_pvar ->
|
||||
report_matching_get summary get_pvar loop_nodes ) ) ) )
|
||||
loop_head_to_loop_nodes ;
|
||||
summary
|
@ -0,0 +1,12 @@
|
||||
# Copyright (c) 2019-present, Facebook, Inc.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
TESTS_DIR = ../../..
|
||||
|
||||
INFER_OPTIONS = --inefficient-keyset-iterator-only --debug-exceptions
|
||||
INFERPRINT_OPTIONS = --issues-tests
|
||||
SOURCES = $(wildcard *.java)
|
||||
|
||||
include $(TESTS_DIR)/javac.make
|
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2019-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
class Test {
|
||||
|
||||
void inefficient_loop_bad(HashMap<String, Integer> testMap) {
|
||||
for (String key : testMap.keySet()) {
|
||||
testMap.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
void inefficient_loop_itr_bad(HashMap<String, Integer> testMap) {
|
||||
|
||||
Iterator itr2 = testMap.keySet().iterator();
|
||||
while (itr2.hasNext()) {
|
||||
String key = (String) itr2.next();
|
||||
testMap.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
void inefficient_loop_itr_bad_FN(HashMap<String, Integer> testMap) {
|
||||
|
||||
Iterator itr2 = testMap.keySet().iterator();
|
||||
int i = 0;
|
||||
while (itr2.hasNext()) {
|
||||
String key = (String) itr2.next();
|
||||
testMap.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
void efficient_loop_itr_ok(HashMap<String, Integer> testMap) {
|
||||
|
||||
Iterator<Map.Entry<String, Integer>> itr1 = testMap.entrySet().iterator();
|
||||
while (itr1.hasNext()) {
|
||||
Map.Entry<String, Integer> entry = itr1.next();
|
||||
entry.getKey();
|
||||
entry.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
void efficient_loop_ok(HashMap<String, Integer> testMap) {
|
||||
for (Map.Entry<String, Integer> entry : testMap.entrySet()) {
|
||||
entry.getKey();
|
||||
entry.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
void negative_loop_ok(HashMap<String, Integer> testMap1, HashMap<String, Integer> testMap2) {
|
||||
for (String key : testMap1.keySet()) {
|
||||
testMap2.get(key);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
codetoanalyze/java/inefficientKeysetIterator/Test.java, Test.inefficient_loop_bad(java.util.HashMap):void, 2, INEFFICIENT_KEYSET_ITERATOR, no_bucket, ERROR, [Accessing a value using a key that was retrieved from a `keySet` iterator. It is more efficient to use an iterator on the `entrySet` of the map, avoiding the extra `HashMap.get(key)` lookup.]
|
||||
codetoanalyze/java/inefficientKeysetIterator/Test.java, Test.inefficient_loop_itr_bad(java.util.HashMap):void, 5, INEFFICIENT_KEYSET_ITERATOR, no_bucket, ERROR, [Accessing a value using a key that was retrieved from a `keySet` iterator. It is more efficient to use an iterator on the `entrySet` of the map, avoiding the extra `HashMap.get(key)` lookup.]
|
Loading…
Reference in new issue