diff --git a/infer/src/python/PyEnv.ml b/infer/src/python/PyEnv.ml index dabd817acd3..4759ec1a862 100644 --- a/infer/src/python/PyEnv.ml +++ b/infer/src/python/PyEnv.ml @@ -216,7 +216,7 @@ end (* TODO: try to merge this in a [globals]/[Symbol]. However be careful not to override the original parent info since it might not be locally available when we executer [register_symbol] after [register_class] *) -type class_info = {parent: Ident.t option} +type class_info = {parents: Ident.t list} type label_info = {label_name: string; ssa_parameters: T.Typ.t list; prelude: prelude option; processed: bool} @@ -613,15 +613,15 @@ let lookup_method {shared= {signatures}} ~enclosing_class name = let lookup_fields {shared= {fields}} class_name = T.TypeName.Map.find_opt class_name fields -let register_class ({shared} as env) class_name parent = +let register_class ({shared} as env) class_name parents = PyDebug.p "[register_class] %s\n" class_name ; - ( match parent with - | None -> + ( match parents with + | [] -> PyDebug.p "\n" - | Some parent -> - PyDebug.p " extending %a\n" Ident.pp parent ) ; + | _ :: _ -> + PyDebug.p " extending %a\n" (Pp.seq ~sep:", " Ident.pp) parents ) ; let {classes} = shared in - let classes = SMap.add class_name {parent} classes in + let classes = SMap.add class_name {parents} classes in let shared = {shared with classes} in {env with shared} diff --git a/infer/src/python/PyEnv.mli b/infer/src/python/PyEnv.mli index 04f7c357853..83cf2619b6f 100644 --- a/infer/src/python/PyEnv.mli +++ b/infer/src/python/PyEnv.mli @@ -127,9 +127,10 @@ module Label : sig (** Process a label [info] and turn it into Textual information *) end -(** Class Level info. For now, only the parent info (if present) is tracked. We may track more - information, like being an abstract class, a dataclass, ... *) -type class_info = {parent: Ident.t option} +(** Class Level info. For now, only the parent info (if present) is tracked, supporting multiple + inheritance. We may track more information in the future, like being an abstract class, a + dataclass, ... *) +type class_info = {parents: Ident.t list} val empty : Ident.t -> t @@ -238,7 +239,7 @@ val lookup_fields : t -> T.TypeName.t -> PyCommon.signature option (** Lookup the signature of a function / method *) -val register_class : t -> string -> Ident.t option -> t +val register_class : t -> string -> Ident.t list -> t (** Register a class declaration (based on [LOAD_BUILD_CLASS]) *) val get_declared_classes : t -> class_info PyCommon.SMap.t diff --git a/infer/src/python/PyTrans.ml b/infer/src/python/PyTrans.ml index 75091237aa0..2f1d461602d 100644 --- a/infer/src/python/PyTrans.ml +++ b/infer/src/python/PyTrans.ml @@ -24,8 +24,8 @@ module Error = struct | LoadMapCells | FunctionFlags of string * MakeFunctionFlags.t | StaticCallImport of Ident.t - | LoadMultipleClasses of int | SuperNotInFile of Ident.t * Ident.t + | SuperMultipleInheritance of Ident.t | Closure of string | CompareOp of Builtin.Compare.t | Exception of string * DataStack.cell @@ -42,12 +42,12 @@ module Error = struct flags | StaticCallImport id -> F.fprintf fmt "Unsupported Import %a in static call" Ident.pp id - | LoadMultipleClasses n -> - F.fprintf fmt "Unsupported LOAD_BUILD_CLASS with arg = %d" n | SuperNotInFile (id, parent) -> F.fprintf fmt "Unsupported `super` in class %a because its parent %a is not in the same file" Ident.pp id Ident.pp parent + | SuperMultipleInheritance id -> + F.fprintf fmt "Unsupported call to `super` with multiple inheritance in %a" Ident.pp id | Closure opname -> F.fprintf fmt "[%s] Unsupported statement with closure" opname | CompareOp op -> @@ -76,6 +76,8 @@ module Error = struct | EmptyStack of string * string | BuiltinClassName of string * string * DataStack.cell | BuiltinClassBody of string * DataStack.cell + | BuiltinClassInvalidParents of string + | BuiltinClassInvalid | CallInvalid of string * DataStack.cell | MakeFunctionInvalidCode of DataStack.cell | MakeFunctionInvalidName of DataStack.cell @@ -132,6 +134,10 @@ module Error = struct DataStack.pp_cell cell | BuiltinClassBody (opname, cell) -> F.fprintf fmt "[%s] Expected code object but got %a" opname DataStack.pp_cell cell + | BuiltinClassInvalidParents name -> + F.fprintf fmt "Failure to load parent classes of '%s" name + | BuiltinClassInvalid -> + F.pp_print_string fmt "Failure to load class: not enough information can be found" | CallInvalid (opname, cell) -> F.fprintf fmt "[%s] invalid object to call: %a" opname DataStack.pp_cell cell | MakeFunctionInvalidCode cell -> @@ -723,7 +729,7 @@ module FUNCTION = struct Ok (env, None) - let builtin_build_class env code opname name_cell code_cell parent_cell = + let builtin_build_class env code opname name_cell code_cell parent_cells = let open IResult.Let_syntax in let as_name kind cell = match DataStack.as_name code cell with @@ -733,7 +739,11 @@ module FUNCTION = struct Error (L.ExternalError, Error.BuiltinClassName (opname, kind, cell)) in let* code_name = as_name "base" name_cell in - let parent = Option.bind ~f:(DataStack.as_id code) parent_cell in + let* parents = + let ids = List.map ~f:(DataStack.as_id code) parent_cells in + let ids = Option.all ids in + Result.of_option ids ~error:(L.InternalError, Error.BuiltinClassInvalidParents code_name) + in let* code = match DataStack.as_code code code_cell with | Some code -> @@ -741,7 +751,7 @@ module FUNCTION = struct | None -> Error (L.ExternalError, Error.BuiltinClassBody (opname, code_cell)) in - let env = Env.register_class env code_name parent in + let env = Env.register_class env code_name parents in let env = Env.push env (DataStack.Code {fun_or_class= false; code_name; code}) in Ok env @@ -774,13 +784,13 @@ module FUNCTION = struct | BuiltinBuildClass -> ( match cells with | [code_cell; name_cell] -> - let* env = builtin_build_class env code opname name_cell code_cell None in + let* env = builtin_build_class env code opname name_cell code_cell [] in Ok (env, None) - | [code_cell; name_cell; parent_cell] -> - let* env = builtin_build_class env code opname name_cell code_cell (Some parent_cell) in + | code_cell :: name_cell :: parent_cells -> + let* env = builtin_build_class env code opname name_cell code_cell parent_cells in Ok (env, None) | _ -> - Error (L.InternalError, Error.TODO (LoadMultipleClasses arg)) ) + Error (L.InternalError, Error.BuiltinClassInvalid) ) | Path id -> static_call env code id cells | ImportCall {id; loc} -> @@ -929,24 +939,26 @@ module METHOD = struct let class_info = SMap.find_opt class_name classes in let params = Env.get_params env in let is_static = Env.is_static env in - let* parent_info = + let* parent = match class_info with | None -> Error (L.ExternalError, Error.SuperNotInClass current_module) - | Some {parent} -> - Ok parent + | Some {parents} -> ( + match parents with + | [] -> + Error (L.ExternalError, Error.SuperNoParent current_module) + | [parent] -> + Ok parent + | _ -> + Error (L.InternalError, Error.TODO (SuperMultipleInheritance current_module)) ) in let* parent_name = - match parent_info with + let key = Symbol.Global parent in + match Env.lookup_symbol env key with | None -> - Error (L.ExternalError, Error.SuperNoParent current_module) - | Some parent -> ( - let key = Symbol.Global parent in - match Env.lookup_symbol env key with - | None -> - Error (L.InternalError, Error.TODO (SuperNotInFile (current_module, parent))) - | Some {Symbol.id} -> - Ok id ) + Error (L.InternalError, Error.TODO (SuperNotInFile (current_module, parent))) + | Some {Symbol.id} -> + Ok id in let* self = if is_static then Error (L.ExternalError, Error.SuperInStatic current_module) @@ -2630,9 +2642,9 @@ let constructor_stubs ~kind loc class_name = (** Process the special function generated by the Python compiler to create a class instances. This is where we can see some of the type annotations for fields, and method definitions. *) -let rec class_declaration env module_name ({FFI.Code.instructions; co_name} as code) loc parent = +let rec class_declaration env module_name ({FFI.Code.instructions; co_name} as code) loc parents = let open IResult.Let_syntax in - Debug.p "[class declaration] %s (%a)\n" co_name (Pp.option Ident.pp) parent ; + Debug.p "[class declaration] %s (%a)\n" co_name (Pp.seq ~sep:", " Ident.pp) parents ; (* TODO: - use annotations to declare class member types - pass in [env] to PyClassDecl when we'll support decorators @@ -2681,10 +2693,8 @@ let rec class_declaration env module_name ({FFI.Code.instructions; co_name} as c in let* env, decls = to_proc_descs env class_name (Array.of_list codes) in let supers, static_supers = - match parent with - | None -> - ([], []) - | Some parent -> + (* TODO: check if/how ordering of parent class matters. Atm I keep the same order. *) + List.fold_right ~init:([], []) parents ~f:(fun parent (supers, static_supers) -> let parent_id = let key = Symbol.Global parent in match Env.lookup_symbol env key with Some {Symbol.id} -> Some id | _ -> None @@ -2699,9 +2709,9 @@ let rec class_declaration env module_name ({FFI.Code.instructions; co_name} as c since compilation was happy *) Option.value parent_id ~default:parent in - let supers = [Ident.to_type_name ~static:false parent] in - let static_supers = [Ident.to_type_name ~static:true parent] in - (supers, static_supers) + let supers = Ident.to_type_name ~static:false parent :: supers in + let static_supers = Ident.to_type_name ~static:true parent :: static_supers in + (supers, static_supers) ) in let t = T.Module.Struct {name= class_type_name; supers; fields; attributes= []} in let companion = @@ -2749,8 +2759,8 @@ and to_proc_descs env enclosing_class_id codes = (* TODO: supported nested classes *) let classes = Env.get_declared_classes env in match SMap.find_opt co_name classes with - | Some {parent} -> - let* env, new_decls = class_declaration env enclosing_class_id code loc parent in + | Some {parents} -> + let* env, new_decls = class_declaration env enclosing_class_id code loc parents in Ok (env, new_decls @ decls) | None -> let* env, decl = to_proc_desc env loc enclosing_class_id (Some co_name) code in diff --git a/infer/src/python/unit/PyTransTest.ml b/infer/src/python/unit/PyTransTest.ml index b81a092ffff..8ba4a0fa748 100644 --- a/infer/src/python/unit/PyTransTest.ml +++ b/infer/src/python/unit/PyTransTest.ml @@ -1673,6 +1673,83 @@ def g(c: C) -> None: declare $builtins.python_float(float) : *PyFloat declare $builtins.python_int(int) : *PyInt |}] + + + let%expect_test _ = + let source = + {| +class A: + pass + +class B: + pass + +class C(A, B): + pass + |} + in + test source ; + [%expect + {| + .source_language = "python" + + define dummy.$toplevel() : *PyNone { + #b0: + n0 = $builtins.python_class("dummy::A") + n1 = $builtins.python_class("dummy::B") + n2 = $builtins.python_class("dummy::C") + ret null + + } + + declare dummy::A(...) : *dummy::A + + declare dummy::A.__init__(...) : *PyNone + + global dummy::A$static: *PyObject + + type .static dummy::A$static = {} + + type dummy::A = {} + + declare dummy::B(...) : *dummy::B + + declare dummy::B.__init__(...) : *PyNone + + global dummy::B$static: *PyObject + + type .static dummy::B$static = {} + + type dummy::B = {} + + declare dummy::C(...) : *dummy::C + + declare dummy::C.__init__(...) : *PyNone + + global dummy::C$static: *PyObject + + type .static dummy::C$static extends dummy::A$static, dummy::B$static = { + } + + type dummy::C extends dummy::A, dummy::B = {} + + global $python_implicit_names::__name__: *PyString + + global $python_implicit_names::__file__: *PyString + + declare $builtins.python_class(*String) : *PyClass + + declare $builtins.python_tuple(...) : *PyObject + + declare $builtins.python_bytes(*Bytes) : *PyBytes + + declare $builtins.python_string(*String) : *PyString + + declare $builtins.python_bool(int) : *PyBool + + declare $builtins.python_float(float) : *PyFloat + + declare $builtins.python_int(int) : *PyInt |}] end )