|
|
|
(*
|
|
|
|
* Copyright (c) 2017-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 L = Logging
|
|
|
|
module MF = MarkupFormatter
|
|
|
|
|
|
|
|
type lock = Lock | Unlock | LockedIfTrue | NoEffect
|
|
|
|
|
|
|
|
type thread = BackgroundThread | MainThread | MainThreadIfTrue | UnknownThread
|
|
|
|
|
|
|
|
let is_thread_utils_type java_pname =
|
|
|
|
let pn = Typ.Procname.Java.get_class_name java_pname in
|
|
|
|
String.is_suffix ~suffix:"ThreadUtils" pn || String.is_suffix ~suffix:"ThreadUtil" pn
|
|
|
|
|
|
|
|
|
|
|
|
let is_thread_utils_method method_name_str = function
|
|
|
|
| Typ.Procname.Java java_pname ->
|
|
|
|
is_thread_utils_type java_pname
|
|
|
|
&& String.equal (Typ.Procname.Java.get_method java_pname) method_name_str
|
|
|
|
| _ ->
|
|
|
|
false
|
|
|
|
|
|
|
|
|
|
|
|
let get_thread = function
|
|
|
|
| Typ.Procname.Java java_pname when is_thread_utils_type java_pname -> (
|
|
|
|
match Typ.Procname.Java.get_method java_pname with
|
|
|
|
| "assertMainThread" | "assertOnUiThread" | "checkOnMainThread" ->
|
|
|
|
MainThread
|
|
|
|
| "isMainThread" | "isUiThread" ->
|
|
|
|
MainThreadIfTrue
|
|
|
|
| "assertOnBackgroundThread" | "assertOnNonUiThread" | "checkOnNonUiThread" ->
|
|
|
|
BackgroundThread
|
|
|
|
| _ ->
|
|
|
|
UnknownThread )
|
|
|
|
| _ ->
|
|
|
|
UnknownThread
|
|
|
|
|
|
|
|
|
|
|
|
let get_lock =
|
|
|
|
let is_cpp_lock =
|
|
|
|
let matcher_lock =
|
|
|
|
QualifiedCppName.Match.of_fuzzy_qual_names
|
|
|
|
[ "apache::thrift::concurrency::ReadWriteMutex::acquireRead"
|
|
|
|
; "apache::thrift::concurrency::ReadWriteMutex::acquireWrite"
|
|
|
|
; "folly::MicroSpinLock::lock"
|
|
|
|
; "folly::RWSpinLock::lock"
|
|
|
|
; "folly::RWSpinLock::lock_shared"
|
|
|
|
; "folly::SharedMutexImpl::lockExclusiveImpl"
|
|
|
|
; "folly::SharedMutexImpl::lockSharedImpl"
|
|
|
|
; "std::mutex::lock"
|
|
|
|
; "std::unique_lock::lock"
|
|
|
|
; "std::lock" ]
|
|
|
|
in
|
|
|
|
let matcher_lock_constructor =
|
|
|
|
QualifiedCppName.Match.of_fuzzy_qual_names
|
|
|
|
[ "std::lock_guard::lock_guard"
|
|
|
|
; "std::unique_lock::unique_lock"
|
|
|
|
; "folly::LockedPtr::LockedPtr" ]
|
|
|
|
in
|
|
|
|
fun pname actuals ->
|
|
|
|
QualifiedCppName.Match.match_qualifiers matcher_lock (Typ.Procname.get_qualifiers pname)
|
|
|
|
|| QualifiedCppName.Match.match_qualifiers matcher_lock_constructor
|
|
|
|
(Typ.Procname.get_qualifiers pname)
|
|
|
|
(* Passing additional parameter allows to defer the lock *)
|
|
|
|
&& Int.equal 2 (List.length actuals)
|
|
|
|
and is_cpp_unlock =
|
|
|
|
let matcher =
|
|
|
|
QualifiedCppName.Match.of_fuzzy_qual_names
|
|
|
|
[ "apache::thrift::concurrency::ReadWriteMutex::release"
|
|
|
|
; "folly::MicroSpinLock::unlock"
|
|
|
|
; "folly::RWSpinLock::unlock"
|
|
|
|
; "folly::RWSpinLock::unlock_shared"
|
|
|
|
; "folly::SharedMutexImpl::unlock"
|
|
|
|
; "folly::SharedMutexImpl::unlock_shared"
|
|
|
|
; "std::lock_guard::~lock_guard"
|
|
|
|
; "std::mutex::unlock"
|
|
|
|
; "std::unique_lock::unlock"
|
|
|
|
; "std::unique_lock::~unique_lock"
|
|
|
|
; "folly::LockedPtr::~LockedPtr" ]
|
|
|
|
in
|
|
|
|
fun pname ->
|
|
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Procname.get_qualifiers pname)
|
|
|
|
and is_cpp_trylock =
|
|
|
|
let matcher =
|
|
|
|
QualifiedCppName.Match.of_fuzzy_qual_names
|
|
|
|
["std::unique_lock::owns_lock"; "std::unique_lock::try_lock"]
|
|
|
|
in
|
|
|
|
fun pname ->
|
|
|
|
QualifiedCppName.Match.match_qualifiers matcher (Typ.Procname.get_qualifiers pname)
|
|
|
|
in
|
|
|
|
fun pname actuals ->
|
|
|
|
match pname with
|
|
|
|
| Typ.Procname.Java java_pname -> (
|
|
|
|
if is_thread_utils_method "assertHoldsLock" (Typ.Procname.Java java_pname) then Lock
|
|
|
|
else
|
|
|
|
match
|
|
|
|
(Typ.Procname.Java.get_class_name java_pname, Typ.Procname.Java.get_method java_pname)
|
|
|
|
with
|
|
|
|
| ( ( "java.util.concurrent.locks.Lock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantLock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock" )
|
|
|
|
, ("lock" | "lockInterruptibly") ) ->
|
|
|
|
Lock
|
|
|
|
| ( ( "java.util.concurrent.locks.Lock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantLock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock" )
|
|
|
|
, "unlock" ) ->
|
|
|
|
Unlock
|
|
|
|
| ( ( "java.util.concurrent.locks.Lock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantLock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock"
|
|
|
|
| "java.util.concurrent.locks.ReentrantReadWriteLock$WriteLock" )
|
|
|
|
, "tryLock" ) ->
|
|
|
|
LockedIfTrue
|
|
|
|
| ( "com.facebook.buck.util.concurrent.AutoCloseableReadWriteUpdateLock"
|
|
|
|
, ("readLock" | "updateLock" | "writeLock") ) ->
|
|
|
|
Lock
|
|
|
|
| _ ->
|
|
|
|
NoEffect )
|
|
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) as pname when is_cpp_lock pname actuals ->
|
|
|
|
Lock
|
|
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) as pname when is_cpp_unlock pname ->
|
|
|
|
Unlock
|
|
|
|
| (Typ.Procname.ObjC_Cpp _ | C _) as pname when is_cpp_trylock pname ->
|
|
|
|
LockedIfTrue
|
|
|
|
| pname when Typ.Procname.equal pname BuiltinDecl.__set_locked_attribute ->
|
|
|
|
Lock
|
|
|
|
| pname when Typ.Procname.equal pname BuiltinDecl.__delete_locked_attribute ->
|
|
|
|
Unlock
|
|
|
|
| _ ->
|
|
|
|
NoEffect
|
|
|
|
|
|
|
|
|
|
|
|
let get_current_class_and_annotated_superclasses is_annot tenv pname =
|
|
|
|
match pname with
|
|
|
|
| Typ.Procname.Java java_pname ->
|
|
|
|
let current_class = Typ.Procname.Java.get_class_type_name java_pname in
|
|
|
|
let annotated_classes =
|
|
|
|
PatternMatch.find_superclasses_with_attributes is_annot tenv current_class
|
|
|
|
in
|
|
|
|
Some (current_class, annotated_classes)
|
|
|
|
| _ ->
|
|
|
|
None
|
|
|
|
|
|
|
|
|
|
|
|
let find_annotated_or_overriden_annotated_method is_annot pname tenv =
|
|
|
|
PatternMatch.override_find
|
|
|
|
(fun pn ->
|
|
|
|
Annotations.pname_has_return_annot pn ~attrs_of_pname:Summary.proc_resolve_attributes
|
|
|
|
is_annot )
|
|
|
|
tenv pname
|
|
|
|
|
|
|
|
|
|
|
|
let is_call_of_class ?(search_superclasses = true) ?(method_prefix = false)
|
|
|
|
?(actuals_pred = fun _ -> true) clazz methods =
|
|
|
|
let method_matcher =
|
|
|
|
if method_prefix then fun current_method target_method ->
|
|
|
|
String.is_prefix current_method ~prefix:target_method
|
|
|
|
else fun current_method target_method -> String.equal current_method target_method
|
|
|
|
in
|
|
|
|
let class_matcher =
|
|
|
|
let is_target_class =
|
|
|
|
let target = Typ.Name.Java.from_string clazz in
|
|
|
|
fun tname -> Typ.Name.equal tname target
|
|
|
|
in
|
|
|
|
if search_superclasses then fun tenv classname ->
|
|
|
|
let is_target_struct tname _ = is_target_class tname in
|
|
|
|
PatternMatch.supertype_exists tenv is_target_struct classname
|
|
|
|
else fun _ classname -> is_target_class classname
|
|
|
|
in
|
|
|
|
(fun tenv pn actuals ->
|
|
|
|
actuals_pred actuals
|
|
|
|
&&
|
|
|
|
match pn with
|
|
|
|
| Typ.Procname.Java java_pname ->
|
|
|
|
let mthd = Typ.Procname.Java.get_method java_pname in
|
|
|
|
List.exists methods ~f:(method_matcher mthd)
|
|
|
|
&&
|
|
|
|
let classname = Typ.Procname.Java.get_class_type_name java_pname in
|
|
|
|
class_matcher tenv classname
|
|
|
|
| _ ->
|
|
|
|
false )
|
|
|
|
|> Staged.stage
|
|
|
|
|
|
|
|
|
|
|
|
type matcher = Tenv.t -> Typ.Procname.t -> HilExp.t list -> bool
|
|
|
|
|
|
|
|
type java_matcher_record =
|
|
|
|
{ search_superclasses: bool option
|
|
|
|
; method_prefix: bool option
|
|
|
|
; actuals_pred: (HilExp.t list -> bool) option
|
|
|
|
; classname: string
|
|
|
|
; methods: string list }
|
|
|
|
|
|
|
|
let make_matcher_from_record {search_superclasses; method_prefix; actuals_pred; classname; methods}
|
|
|
|
=
|
|
|
|
is_call_of_class ?search_superclasses ?method_prefix ?actuals_pred classname methods
|
|
|
|
|> Staged.unstage
|
|
|
|
|
|
|
|
|
|
|
|
let empty_matcher =
|
|
|
|
{search_superclasses= None; method_prefix= None; actuals_pred= None; classname= ""; methods= []}
|
|
|
|
|
|
|
|
|
|
|
|
let matcher_of_json top_json =
|
|
|
|
let error json =
|
|
|
|
L.(die UserError "Could not parse json matcher(s): %s" (Yojson.Basic.to_string json))
|
|
|
|
in
|
|
|
|
let make_matcher_from_json json =
|
|
|
|
let parse_method_name = function `String methodname -> methodname | _ -> error json in
|
|
|
|
let rec parse_fields assoclist acc =
|
|
|
|
match assoclist with
|
|
|
|
| ("search_superclasses", `Bool b) :: rest ->
|
|
|
|
{acc with search_superclasses= Some b} |> parse_fields rest
|
|
|
|
| ("method_prefix", `Bool b) :: rest ->
|
|
|
|
{acc with method_prefix= Some b} |> parse_fields rest
|
|
|
|
| ("classname", `String classname) :: rest ->
|
|
|
|
{acc with classname} |> parse_fields rest
|
|
|
|
| ("methods", `List methodnames) :: rest ->
|
|
|
|
let methods = List.map methodnames ~f:parse_method_name in
|
|
|
|
{acc with methods} |> parse_fields rest
|
|
|
|
| [] ->
|
|
|
|
if String.equal acc.classname "" || List.is_empty acc.methods then error json else acc
|
|
|
|
| _ ->
|
|
|
|
error json
|
|
|
|
in
|
|
|
|
(match json with `Assoc fields -> parse_fields fields empty_matcher | _ -> error json)
|
|
|
|
|> make_matcher_from_record
|
|
|
|
in
|
|
|
|
match top_json with
|
|
|
|
| `List matchers_json ->
|
|
|
|
let matchers = List.map matchers_json ~f:make_matcher_from_json in
|
|
|
|
fun tenv pn actuals -> List.exists matchers ~f:(fun m -> m tenv pn actuals)
|
|
|
|
| _ ->
|
|
|
|
error top_json
|
|
|
|
|
|
|
|
|
|
|
|
let ui_matcher_records =
|
|
|
|
let superclasses = {empty_matcher with search_superclasses= Some true} in
|
|
|
|
let fragment_methods =
|
|
|
|
(* sort police: this is in lifecycle order *)
|
|
|
|
[ "onAttach"
|
|
|
|
; "onCreate"
|
|
|
|
; "onCreateView"
|
|
|
|
; "onActivityCreated"
|
|
|
|
; "onStart"
|
|
|
|
; "onResume"
|
|
|
|
; "onPause"
|
|
|
|
; "onStop"
|
|
|
|
; "onDestroyView"
|
|
|
|
; "onDestroy"
|
|
|
|
; "onDetach" ]
|
|
|
|
in
|
|
|
|
[ {superclasses with classname= "android.support.v4.app.Fragment"; methods= fragment_methods}
|
|
|
|
; {superclasses with classname= "android.app.Fragment"; methods= fragment_methods}
|
|
|
|
; {superclasses with classname= "android.content.ContentProvider"; methods= ["onCreate"]}
|
|
|
|
; {superclasses with classname= "android.content.BroadcastReceiver"; methods= ["onReceive"]}
|
|
|
|
; { superclasses with
|
|
|
|
classname= "android.app.Service"
|
|
|
|
; methods= ["onBind"; "onCreate"; "onDestroy"; "onStartCommand"] }
|
|
|
|
; { superclasses with
|
|
|
|
classname= "android.app.Activity"
|
|
|
|
; methods= ["onCreate"; "onStart"; "onRestart"; "onResume"; "onPause"; "onStop"; "onDestroy"]
|
|
|
|
}
|
|
|
|
; { superclasses with
|
|
|
|
(* according to Android documentation, *all* methods of the View class run on UI thread, but
|
|
|
|
let's be a bit conservative and catch all methods that start with "on".
|
|
|
|
https://developer.android.com/reference/android/view/View.html *)
|
|
|
|
method_prefix= Some true
|
|
|
|
; classname= "android.view.View"
|
|
|
|
; methods= ["on"] } ]
|
|
|
|
|
|
|
|
|
|
|
|
let is_ui_method =
|
|
|
|
let matchers = List.map ui_matcher_records ~f:make_matcher_from_record in
|
|
|
|
(* we pass an empty actuals list because all matching is done on class and method name here *)
|
|
|
|
fun tenv pname -> List.exists matchers ~f:(fun m -> m tenv pname [])
|
|
|
|
|
|
|
|
|
|
|
|
let runs_on_ui_thread =
|
|
|
|
(* assume that methods annotated with @UiThread, @OnEvent, @OnBind, @OnMount, @OnUnbind,
|
|
|
|
@OnUnmount always run on the UI thread *)
|
|
|
|
let annotation_matchers =
|
|
|
|
[ Annotations.ia_is_mainthread
|
|
|
|
; Annotations.ia_is_ui_thread
|
|
|
|
; Annotations.ia_is_on_bind
|
|
|
|
; Annotations.ia_is_on_event
|
|
|
|
; Annotations.ia_is_on_mount
|
|
|
|
; Annotations.ia_is_on_unbind
|
|
|
|
; Annotations.ia_is_on_unmount ]
|
|
|
|
in
|
|
|
|
let is_annot annot = List.exists annotation_matchers ~f:(fun m -> m annot) in
|
|
|
|
let mono_pname = MF.wrap_monospaced Typ.Procname.pp in
|
|
|
|
fun tenv proc_desc ->
|
|
|
|
let pname = Procdesc.get_proc_name proc_desc in
|
|
|
|
if
|
|
|
|
Annotations.pdesc_has_return_annot proc_desc Annotations.ia_is_worker_thread
|
|
|
|
|| find_annotated_or_overriden_annotated_method Annotations.ia_is_worker_thread pname tenv
|
|
|
|
|> Option.is_some
|
|
|
|
|| get_current_class_and_annotated_superclasses Annotations.ia_is_worker_thread tenv pname
|
|
|
|
|> Option.value_map ~default:false ~f:(function _, [] -> false | _ -> true)
|
|
|
|
then None
|
|
|
|
else if is_ui_method tenv pname then
|
|
|
|
Some (F.asprintf "%a is a standard UI-thread method" mono_pname pname)
|
|
|
|
else if Annotations.pdesc_has_return_annot proc_desc is_annot then
|
|
|
|
Some
|
|
|
|
(F.asprintf "%a is annotated %s" mono_pname pname
|
|
|
|
(MF.monospaced_to_string Annotations.ui_thread))
|
|
|
|
else
|
|
|
|
match find_annotated_or_overriden_annotated_method is_annot pname tenv with
|
|
|
|
| Some override_pname ->
|
|
|
|
Some
|
|
|
|
(F.asprintf "class %a overrides %a, which is annotated %s" mono_pname pname mono_pname
|
|
|
|
override_pname
|
|
|
|
(MF.monospaced_to_string Annotations.ui_thread))
|
|
|
|
| None -> (
|
|
|
|
match get_current_class_and_annotated_superclasses is_annot tenv pname with
|
|
|
|
| Some (current_class, (super_class :: _ as super_classes)) ->
|
|
|
|
let middle =
|
|
|
|
if List.exists super_classes ~f:(Typ.Name.equal current_class) then ""
|
|
|
|
else F.asprintf " extends %a, which" (MF.wrap_monospaced Typ.Name.pp) super_class
|
|
|
|
in
|
|
|
|
Some
|
|
|
|
(F.asprintf "class %s%s is annotated %s"
|
|
|
|
(MF.monospaced_to_string (Typ.Name.name current_class))
|
|
|
|
middle
|
|
|
|
(MF.monospaced_to_string Annotations.ui_thread))
|
|
|
|
| _ ->
|
|
|
|
None )
|