diff --git a/infer/src/python/PyIR2Textual.ml b/infer/src/python/PyIR2Textual.ml index 78c41f7d4d..7d83854f2a 100644 --- a/infer/src/python/PyIR2Textual.ml +++ b/infer/src/python/PyIR2Textual.ml @@ -370,6 +370,8 @@ let builtin_name builtin = let str_py_store_name = "py_store_name" +let str_py_call = "py_call" + let of_stmt loc stmt : Textual.Instr.t = match (stmt : Stmt.t) with | Let {lhs; rhs} -> @@ -408,7 +410,7 @@ let of_stmt loc stmt : Textual.Instr.t = ; loc } | Call {lhs; exp; args; arg_names} -> let args = of_exp exp :: of_exp arg_names :: List.map ~f:of_exp args in - Let {id= Some (mk_ident lhs); exp= call_builtin "py_call" args; loc} + Let {id= Some (mk_ident lhs); exp= call_builtin str_py_call args; loc} | CallMethod {lhs; name; self_if_needed; args; arg_names} -> Let { id= Some (mk_ident lhs) @@ -712,6 +714,8 @@ let py_import_from = builtin_qual_proc_name str_py_import_from let py_build_class = builtin_qual_proc_name str_py_build_class +let py_call = builtin_qual_proc_name str_py_call + let py_store_name = builtin_qual_proc_name str_py_store_name let py_make_function = builtin_qual_proc_name str_py_make_function @@ -757,6 +761,20 @@ let gen_type module_name ~allow_classes name node = let name = DefaultType.get_string_constant ident_str_const acc |> Option.value_exn in let acc = DefaultType.add_class_body ident name acc in find_next_declaration acc instrs + | Instr.Let {id= Some ident; exp= Call {proc; args= [Var _id_decorator; _; Var id_fun_ptr]}} + :: instrs + when QualifiedProcName.equal py_call proc && DefaultType.is_fun_ptr id_fun_ptr acc -> + (* note: we could filter the decorator in id_decorator if needed *) + let typ = DefaultType.get_fun_ptr id_fun_ptr acc |> Option.value_exn in + let acc = DefaultType.add_fun_ptr ident typ acc in + find_next_declaration acc instrs + | Instr.Let {id= Some ident; exp= Call {proc; args= [Var _id_decorator; _; Var id_class]}} + :: instrs + when QualifiedProcName.equal py_call proc && DefaultType.is_class_body id_class acc -> + (* note: we could filter the decorator in id_decorator if needed *) + let name = DefaultType.get_class_body id_class acc |> Option.value_exn in + let acc = DefaultType.add_class_body ident name acc in + find_next_declaration acc instrs | Instr.Let {exp= Call {proc; args= [Const (Str target); _; _; Var ident]}} :: instrs when QualifiedProcName.equal py_store_name proc && DefaultType.is_fun_ptr ident acc -> let typ = DefaultType.get_fun_ptr ident acc |> Option.value_exn in diff --git a/infer/tests/codetoanalyze/python/pulse/async_import_simple.py b/infer/tests/codetoanalyze/python/pulse/async_import_simple.py index bc751d0351..4f8c4f94d2 100644 --- a/infer/tests/codetoanalyze/python/pulse/async_import_simple.py +++ b/infer/tests/codetoanalyze/python/pulse/async_import_simple.py @@ -5,20 +5,32 @@ import asyncio import async_utils as utils -from async_utils import await_it as async_await, dont_await_it as async_dont_await - +from async_utils import ( + await_it as async_await, + dont_await_it as async_dont_await, + sleep, + C +) async def with_import_bad(): - await utils.dont_await_it(asyncio.sleep(1)) + await utils.dont_await_it(sleep()) async def with_import_ok(): - await utils.await_it(asyncio.sleep(1)) + await utils.await_it(sleep()) async def with_from_import_bad(): - await async_dont_await(asyncio.sleep(1)) + await async_dont_await(sleep()) async def with_from_import_ok(): - await async_await(asyncio.sleep(1)) + await async_await(sleep()) + + +async def with_imported_class_bad(): + await async_dont_await(C.sleep()) + + +async def with_imported_class_ok(): + await async_await(C.sleep()) diff --git a/infer/tests/codetoanalyze/python/pulse/async_import_with_package.py b/infer/tests/codetoanalyze/python/pulse/async_import_with_package.py index a747d6f478..b7619c3649 100644 --- a/infer/tests/codetoanalyze/python/pulse/async_import_with_package.py +++ b/infer/tests/codetoanalyze/python/pulse/async_import_with_package.py @@ -4,12 +4,31 @@ # LICENSE file in the root directory of this source tree. import asyncio -from dir1.testmod import await_it as await_it1, dont_await_it as dont_await_it1 -from dir1.dir3.testmod import await_it as await_it3, dont_await_it as dont_await_it3 -from dir1.dir4.testmod import await_it as await_it4, dont_await_it as dont_await_it4 -from dir2.testmod import await_it as await_it2, dont_await_it as dont_await_it2 -from dir2.dir5.testmod import await_it as await_it5, dont_await_it as dont_await_it5 -from dir2.dir6.testmod import await_it as await_it6, dont_await_it as dont_await_it6 +from dir1.testmod import ( + await_it as await_it1, + dont_await_it as dont_await_it1, +) +from dir1.dir3.testmod import ( + await_it as await_it3, + dont_await_it as dont_await_it3, + C +) +from dir1.dir4.testmod import ( + await_it as await_it4, + dont_await_it as dont_await_it4, +) +from dir2.testmod import ( + await_it as await_it2, + dont_await_it as dont_await_it2, +) +from dir2.dir5.testmod import ( + await_it as await_it5, + dont_await_it as dont_await_it5, +) +from dir2.dir6.testmod import ( + await_it as await_it6, + dont_await_it as dont_await_it6, +) async def bad1(): @@ -36,6 +55,14 @@ async def ok3(): await await_it3(asyncio.sleep(1)) +async def from_class_bad3(): + await dont_await_it3(C.async_fun()) + + +async def from_class_ok3(): + await await_it3(C.async_fun()) + + async def bad4(): await dont_await_it4(asyncio.sleep(1)) diff --git a/infer/tests/codetoanalyze/python/pulse/async_utils.py b/infer/tests/codetoanalyze/python/pulse/async_utils.py index b5083fb5ea..a407c60093 100644 --- a/infer/tests/codetoanalyze/python/pulse/async_utils.py +++ b/infer/tests/codetoanalyze/python/pulse/async_utils.py @@ -3,8 +3,21 @@ # This source code is licensed under the MIT license found in the # LICENSE file in the root directory of this source tree. + +async def sleep(): + pass + + async def await_it(arg): await arg + async def dont_await_it(arg): pass + +@final +class C: + + @staticmethod + async def sleep(): + pass diff --git a/infer/tests/codetoanalyze/python/pulse/dir1/dir3/testmod.py b/infer/tests/codetoanalyze/python/pulse/dir1/dir3/testmod.py index b5083fb5ea..4b27c9773e 100644 --- a/infer/tests/codetoanalyze/python/pulse/dir1/dir3/testmod.py +++ b/infer/tests/codetoanalyze/python/pulse/dir1/dir3/testmod.py @@ -8,3 +8,10 @@ async def await_it(arg): async def dont_await_it(arg): pass + +@final +class C: + + @staticmethod + async def async_fun(): + pass diff --git a/infer/tests/codetoanalyze/python/pulse/issues.exp b/infer/tests/codetoanalyze/python/pulse/issues.exp index 6522b74a59..8e25a8456b 100644 --- a/infer/tests/codetoanalyze/python/pulse/issues.exp +++ b/infer/tests/codetoanalyze/python/pulse/issues.exp @@ -1,9 +1,11 @@ async_global.py, async_global.__module_body__, 24, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,parameter `globals` of async_global.__module_body__,assigned,when calling `closure:async_global:0.call` here,parameter `__this` of closure:async_global:0.call,when calling `async_global.set_unwaited_global` here,allocated by async call here,awaitable becomes unreachable here] async_import_simple.py, async_import_simple.with_import_bad, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] async_import_simple.py, async_import_simple.with_from_import_bad, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] +async_import_simple.py, async_import_simple.with_imported_class_bad, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] async_import_with_package.py, async_import_with_package.bad1, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] async_import_with_package.py, async_import_with_package.bad2, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] async_import_with_package.py, async_import_with_package.bad3, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] +async_import_with_package.py, async_import_with_package.from_class_bad3, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] async_import_with_package.py, async_import_with_package.bad4, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] async_import_with_package.py, async_import_with_package.bad5, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here] async_import_with_package.py, async_import_with_package.bad6, 1, PULSE_UNAWAITED_AWAITABLE, no_bucket, ERROR, [allocation part of the trace starts here,allocated by async call here,awaitable becomes unreachable here]