From 5f140ed91b61307eb06f3359b9c4a3dfb5f7a5ab Mon Sep 17 00:00:00 2001 From: Akos Hajdu Date: Tue, 3 Aug 2021 09:13:14 -0700 Subject: [PATCH] [erl-frontend] Support records Summary: Add support for Erlang records, including: - Parsing record definitions, storing info in environment - Record index expressions and patterns - Record creation expression, including initializers - Record update expressions and patterns - Record field access expressions - Pulse models The key idea is to translate records to tagged tuples (just as Erlang does under the hood), but this requires some bookkeeping to figure out which field maps to which tuple element. Reviewed By: rgrig Differential Revision: D29991819 fbshipit-source-id: a1c713b41 --- infer/src/IR/ErlangTypeName.ml | 5 +- infer/src/erlang/ErlangAst.ml | 5 +- infer/src/erlang/ErlangJsonParser.ml | 16 +- infer/src/erlang/ErlangTranslator.ml | 235 +++++++++++++-- .../codetoanalyze/erlang/features/issues.exp | 19 ++ .../erlang/features/src/records.erl | 272 ++++++++++++++++++ .../codetoanalyze/erlang/nonmatch/issues.exp | 15 + .../erlang/nonmatch/src/records.erl | 140 +++++++++ 8 files changed, 686 insertions(+), 21 deletions(-) create mode 100644 infer/tests/codetoanalyze/erlang/features/src/records.erl create mode 100644 infer/tests/codetoanalyze/erlang/nonmatch/src/records.erl diff --git a/infer/src/IR/ErlangTypeName.ml b/infer/src/IR/ErlangTypeName.ml index 4f0bef0bb..7c477dffa 100644 --- a/infer/src/IR/ErlangTypeName.ml +++ b/infer/src/IR/ErlangTypeName.ml @@ -27,4 +27,7 @@ let cons_head = "head" let cons_tail = "tail" -let tuple_field_names size = List.init size ~f:(Printf.sprintf "elem%d") +let tuple_elem i = Printf.sprintf "elem%d" i + +(* Tuple element indexing is one based *) +let tuple_field_names size = List.init size ~f:(fun i -> tuple_elem (i + 1)) diff --git a/infer/src/erlang/ErlangAst.ml b/infer/src/erlang/ErlangAst.ml index c374cd545..dfe27a98d 100644 --- a/infer/src/erlang/ErlangAst.ml +++ b/infer/src/erlang/ErlangAst.ml @@ -136,13 +136,16 @@ and catch_pattern = {exception_: exception_; pattern: pattern; variable: string} (** {2 S8.1: Module declarations and forms} *) -(* TODO: Add records, types, and specs. *) +(* TODO: Add types, and specs. *) +type record_field = {field_name: string; initializer_: expression option} [@@deriving sexp_of] + type simple_form = | Export of function_ list | Import of {module_name: string; functions: function_ list} | Module of string | File of {path: string} | Function of {function_: function_; clauses: case_clause list} + | Record of {name: string; fields: record_field list} [@@deriving sexp_of] type form = {line: line; simple_form: simple_form} [@@deriving sexp_of] diff --git a/infer/src/erlang/ErlangJsonParser.ml b/infer/src/erlang/ErlangJsonParser.ml index 47ba7b5bc..d0a01c68e 100644 --- a/infer/src/erlang/ErlangJsonParser.ml +++ b/infer/src/erlang/ErlangJsonParser.ml @@ -479,6 +479,16 @@ let to_function json : Ast.function_ option = unknown "function" json +let to_record_field json : Ast.record_field option = + match json with + | `List [`String "record_field"; _; `List [`String "atom"; _; `String field_name]; expr] -> + Some {Ast.field_name; initializer_= to_expression expr} + | `List [`String "record_field"; _; `List [`String "atom"; _; `String field_name]] -> + Some {Ast.field_name; initializer_= None} + | _ -> + unknown "record_field" json + + let to_line_form json : Ast.form option = let form line simple_form : Ast.form option = Some {line; simple_form} in match json with @@ -502,8 +512,12 @@ let to_line_form json : Ast.form option = let function_ : Ast.function_reference = FunctionName function_ in let function_ : Ast.function_ = {module_= ModuleMissing; function_; arity} in form line (Function {function_; clauses}) + | `List [`String "attribute"; anno; `String "record"; `List [`String name; fields]] -> + let* line = to_line anno in + let* field_list = to_list ~f:to_record_field fields in + form line (Record {name; fields= field_list}) | `List [`String "attribute"; _anno; `String _unknown_attribute; _] -> - (* TODO: handle types (spec, record, ...) *) + (* TODO: handle types (spec, ...) *) None | `List [`String "eof"; _] -> None diff --git a/infer/src/erlang/ErlangTranslator.ml b/infer/src/erlang/ErlangTranslator.ml index 24b845f7d..4bb029bee 100644 --- a/infer/src/erlang/ErlangTranslator.ml +++ b/infer/src/erlang/ErlangTranslator.ml @@ -37,10 +37,16 @@ type absent = Absent type 'a present = Present of 'a +type record_field_info = {index: int; initializer_: Ast.expression option} [@@deriving sexp_of] + +type record_info = {field_names: string list; field_info: record_field_info String.Map.t} +[@@deriving sexp_of] + type ('procdesc, 'result) environment = { current_module: module_name (** used to qualify function names *) ; exports: UnqualifiedFunction.Set.t (** used to determine public/private access *) ; imports: module_name UnqualifiedFunction.Map.t (** used to resolve function names *) + ; records: record_info String.Map.t (** used to get fields, indexes and initializers *) ; location: Location.t (** used to tag nodes and instructions being created *) ; procdesc: ('procdesc[@sexp.opaque]) ; result: ('result[@sexp.opaque]) } @@ -51,6 +57,7 @@ let get_environment module_ = { current_module= Printf.sprintf "%s:unknown_module" __FILE__ ; exports= UnqualifiedFunction.Set.empty ; imports= UnqualifiedFunction.Map.empty (* TODO: auto-import from module "erlang" *) + ; records= String.Map.empty ; location= Location.dummy ; procdesc= Absent ; result= Absent } @@ -72,6 +79,26 @@ let get_environment module_ = in let imports = List.fold ~init:env.imports ~f functions in {env with imports} + | Record {name; fields} -> ( + let process_one_field one_index map (one_field : Ast.record_field) = + (* Tuples are indexed from 1 and the first one is the name, hence start from 2 *) + match + Map.add ~key:one_field.field_name + ~data:{index= one_index + 2; initializer_= one_field.initializer_} + map + with + | `Ok map -> + map + | `Duplicate -> + L.die InternalError "repeated field in record: %s" one_field.field_name + in + let field_info = List.foldi ~init:String.Map.empty ~f:process_one_field fields in + let field_names = List.map ~f:(fun (rf : Ast.record_field) -> rf.field_name) fields in + match Map.add ~key:name ~data:{field_names; field_info} env.records with + | `Ok records -> + {env with records} + | `Duplicate -> + L.die InternalError "repeated record: %s" name ) | Module current_module -> {env with current_module} | File {path} -> @@ -216,6 +243,15 @@ module Block = struct let exit_success = Node.make_load env id e typ in let exit_failure = Node.make_nop env in {start= exit_success; exit_success; exit_failure} + + + (** Make a branch based on the condition: go to success if true, go to failure if false *) + let make_branch env condition = + let start = Node.make_nop env in + let exit_success = Node.make_if env true condition in + let exit_failure = Node.make_if env false condition in + start |~~> [exit_success; exit_failure] ; + {start; exit_success; exit_failure} end let has_type env ~result ~value (name : ErlangTypeName.t) : Sil.instr = @@ -233,6 +269,13 @@ let has_type env ~result ~value (name : ErlangTypeName.t) : Sil.instr = Call ((result, Typ.mk (Tint IBool)), fun_exp, args, env.location, CallFlags.default) +let translate_atom_literal (atom : string) : Exp.t = + (* With this hack, an atom may accidentaly be considered equal to an unrelated integer. + The [lsl] below makes this less likely. Proper fix is TODO (T93513105). *) + let hash = String.hash atom lsl 16 in + Exp.Const (Cint (IntLit.of_int hash)) + + (** If the pattern-match succeeds, then the [exit_success] node is reached and the pattern variables are storing the corresponding values; otherwise, the [exit_failure] node is reached. *) let rec translate_pattern env (value : Ident.t) {Ast.line; simple_expression} : Block.t = @@ -267,14 +310,12 @@ let rec translate_pattern env (value : Ident.t) {Ast.line; simple_expression} : wrong_type_node |~~> [exit_failure] ; submatcher.exit_failure |~~> [exit_failure] ; {start; exit_success= submatcher.exit_success; exit_failure} + | Literal (Atom atom) -> + let e = translate_atom_literal atom in + Block.make_branch env (Exp.BinOp (Eq, Var value, e)) | Literal (Int i) -> let e = Exp.Const (Cint (IntLit.of_string i)) in - let cond = Exp.BinOp (Eq, Var value, e) in - let start = Node.make_nop env in - let exit_success = Node.make_if env true cond in - let exit_failure = Node.make_if env false cond in - start |~~> [exit_success; exit_failure] ; - {start; exit_success; exit_failure} + Block.make_branch env (Exp.BinOp (Eq, Var value, e)) | Nil -> let id = Ident.create_fresh Ident.knormal in let start = Node.make_stmt env [has_type env ~result:id ~value Nil] in @@ -282,6 +323,60 @@ let rec translate_pattern env (value : Ident.t) {Ast.line; simple_expression} : let exit_failure = Node.make_if env false (Var id) in start |~~> [exit_success; exit_failure] ; {start; exit_success; exit_failure} + | RecordIndex {name; field} -> ( + match String.Map.find env.records name with + | None -> + L.debug Capture Verbose "@[Unknown record %s@." name ; + Block.make_failure env + | Some record_info -> + let field_info = String.Map.find_exn record_info.field_info field in + let index_expr = Exp.Const (Cint (IntLit.of_int field_info.index)) in + Block.make_branch env (Exp.BinOp (Eq, Var value, index_expr)) ) + | RecordUpdate {name; updates; _} -> ( + (* Match the type and the record name *) + match String.Map.find env.records name with + | None -> + L.debug Capture Verbose "@[Unknown record %s@." name ; + Block.make_failure env + | Some record_info -> + let tuple_size = 1 + List.length record_info.field_names in + let tuple_typ : ErlangTypeName.t = Tuple tuple_size in + let is_right_type_id = Ident.create_fresh Ident.knormal in + let start = Node.make_stmt env [has_type env ~result:is_right_type_id ~value tuple_typ] in + let right_type_node = Node.make_if env true (Var is_right_type_id) in + let wrong_type_node = Node.make_if env false (Var is_right_type_id) in + let name_id = Ident.create_fresh Ident.knormal in + let name_load = load_field name_id (ErlangTypeName.tuple_elem 1) tuple_typ in + let unpack_node = Node.make_stmt env [name_load] in + let name_cond = Exp.BinOp (Eq, Var name_id, translate_atom_literal name) in + let right_name_node = Node.make_if env true name_cond in + let wrong_name_node = Node.make_if env false name_cond in + let exit_failure = Node.make_nop env in + start |~~> [right_type_node; wrong_type_node] ; + right_type_node |~~> [unpack_node] ; + unpack_node |~~> [right_name_node; wrong_name_node] ; + wrong_type_node |~~> [exit_failure] ; + wrong_name_node |~~> [exit_failure] ; + let record_name_matcher : Block.t = {start; exit_success= right_name_node; exit_failure} in + (* Match each specified field *) + let make_one_field_matcher (one_update : Ast.record_update) = + match one_update.field with + | Some name -> + let field_info = String.Map.find_exn record_info.field_info name in + let value_id = Ident.create_fresh Ident.knormal in + let tuple_elem = ErlangTypeName.tuple_elem field_info.index in + let load_instr = load_field value_id tuple_elem tuple_typ in + let unpack_node = Node.make_stmt env [load_instr] in + let submatcher = translate_pattern env value_id one_update.expression in + unpack_node |~~> [submatcher.start] ; + { Block.start= unpack_node + ; exit_success= submatcher.exit_success + ; exit_failure= submatcher.exit_failure } + | None -> + Block.make_success env + in + let record_field_matchers = List.map ~f:make_one_field_matcher updates in + Block.all env (record_name_matcher :: record_field_matchers) ) | Tuple exprs -> let is_right_type_id = Ident.create_fresh Ident.knormal in let tuple_typ : ErlangTypeName.t = Tuple (List.length exprs) in @@ -315,12 +410,8 @@ let rec translate_pattern env (value : Ident.t) {Ast.line; simple_expression} : let expr_block = translate_expression {env with result= Present (Exp.Var id)} {Ast.line; simple_expression} in - let cond = Exp.BinOp (Eq, Var value, Var id) in - let start = Node.make_nop env in - let exit_success = Node.make_if env true cond in - let exit_failure = Node.make_if env false cond in - start |~~> [exit_success; exit_failure] ; - Block.all env [expr_block; {start; exit_success; exit_failure}] + let branch_block = Block.make_branch env (Exp.BinOp (Eq, Var value, Var id)) in + Block.all env [expr_block; branch_block] | Variable vname when String.equal vname "_" -> Block.make_success env | Variable vname -> @@ -380,6 +471,11 @@ and translate_expression env {Ast.line; simple_expression} = let ret_var = match result with Exp.Var ret_var -> ret_var | _ -> Ident.create_fresh Ident.knormal in + let load_field id field expr typ : Sil.instr = + (* x=value.field *) + let field = Fieldname.make (ErlangType typ) field in + Load {id; e= Lfield (expr, field, typ_of_name typ); root_typ= any; typ= any; loc= env.location} + in let expression_block : Block.t = match simple_expression with | BinaryOperator (e1, op, e2) -> ( @@ -526,12 +622,7 @@ and translate_expression env {Ast.line; simple_expression} = let blocks = {blocks with exit_failure= crash_node} in blocks | Literal (Atom atom) -> - let hash = - (* With this hack, an atom may accidentaly be considered equal to an unrelated integer. - The [lsl] below makes this less likely. Proper fix is TODO (T93513105). *) - String.hash atom lsl 16 - in - let e = Exp.Const (Cint (IntLit.of_int hash)) in + let e = translate_atom_literal atom in Block.make_load env ret_var e any | Literal (Int i) -> let e = Exp.Const (Cint (IntLit.of_string i)) in @@ -550,6 +641,114 @@ and translate_expression env {Ast.line; simple_expression} = let fun_exp = Exp.Const (Cfun BuiltinDecl.__erlang_make_nil) in let instruction = Sil.Call ((ret_var, any), fun_exp, [], env.location, CallFlags.default) in Block.make_instruction env [instruction] + | RecordAccess {record; name; field} -> ( + (* TODO: check for badrecord T97040801 *) + let record_id = Ident.create_fresh Ident.knormal in + let record_block = + let result = Present (Exp.Var record_id) in + translate_expression {env with result} record + in + (* Under the hood, a record is a tagged tuple, the first element is the name, + and then the fields follow in the order as in the record definition. *) + match String.Map.find env.records name with + | None -> + L.debug Capture Verbose "@[Unknown record %s@." name ; + Block.make_success env + | Some record_info -> + let field_info = String.Map.find_exn record_info.field_info field in + let field_no = field_info.index in + let tuple_typ : ErlangTypeName.t = Tuple (1 + List.length record_info.field_names) in + let field_load = + load_field ret_var (ErlangTypeName.tuple_elem field_no) (Var record_id) tuple_typ + in + let load_block = Block.make_instruction env [field_load] in + Block.all env [record_block; load_block] ) + | RecordIndex {name; field} -> ( + match String.Map.find env.records name with + | None -> + L.debug Capture Verbose "@[Unknown record %s@." name ; + Block.make_success env + | Some record_info -> + let field_info = String.Map.find_exn record_info.field_info field in + let expr = Exp.Const (Cint (IntLit.of_int field_info.index)) in + Block.make_load env ret_var expr any ) + | RecordUpdate {record; name; updates} -> ( + (* Under the hood, a record is a tagged tuple, the first element is the name, + and then the fields follow in the order as in the record definition. *) + match String.Map.find env.records name with + | None -> + L.debug Capture Verbose "@[Unknown record %s@." name ; + Block.make_success env + | Some record_info -> + let tuple_typ : ErlangTypeName.t = Tuple (1 + List.length record_info.field_names) in + (* First collect all the fields that are updated *) + let collect_updates map (one_update : Ast.record_update) = + match one_update.field with + | Some name -> + Map.add_exn ~key:name ~data:one_update.expression map + | None -> + (* '_' stands for 'everything else' *) + Map.add_exn ~key:"_" ~data:one_update.expression map + in + let updates_map = List.fold ~init:String.Map.empty ~f:collect_updates updates in + (* Translate record expression if it is an update *) + (* TODO: check for badrecord T97040801 *) + let record_id = Ident.create_fresh Ident.knormal in + let record_block = + match record with + | Some expr -> + let result = Present (Exp.Var record_id) in + [translate_expression {env with result} expr] + | None -> + [] + in + (* Translate each field: the value can come from 5 different sources *) + let translate_one_field ((one_field_name, one_id) : string * Ident.t) = + (* (1) Check if field is explicitly set *) + match String.Map.find updates_map one_field_name with + | Some expr -> + let result = Present (Exp.Var one_id) in + translate_expression {env with result} expr + | None -> ( + (* (2) Check if field is set using 'everything else' *) + match String.Map.find updates_map "_" with + | Some expr -> + let result = Present (Exp.Var one_id) in + translate_expression {env with result} expr + | None -> ( + let field_info = String.Map.find_exn record_info.field_info one_field_name in + (* (3) Check if we have to copy over from record that is being updated *) + match record with + | Some _ -> + let field_load = + load_field one_id + (ErlangTypeName.tuple_elem field_info.index) + (Var record_id) tuple_typ + in + Block.make_instruction env [field_load] + | None -> ( + (* (4) Check if there is an initializer *) + match field_info.initializer_ with + | Some expr -> + let result = Present (Exp.Var one_id) in + translate_expression {env with result} expr + | None -> + (* (5) Finally, it's undefined *) + Block.make_load env one_id (translate_atom_literal "undefined") any ) ) ) + in + let field_names = record_info.field_names in + let field_ids = + List.map ~f:(function _ -> Ident.create_fresh Ident.knormal) field_names + in + let field_blocks = List.map ~f:translate_one_field (List.zip_exn field_names field_ids) in + let field_ids_and_types = List.map ~f:(fun id -> (Exp.Var id, any)) field_ids in + let args_and_types = (translate_atom_literal name, any) :: field_ids_and_types in + let fun_exp = Exp.Const (Cfun BuiltinDecl.__erlang_make_tuple) in + let call_instruction = + Sil.Call ((ret_var, any), fun_exp, args_and_types, env.location, CallFlags.default) + in + let call_block = Block.make_instruction env [call_instruction] in + Block.all env (record_block @ field_blocks @ [call_block]) ) | Tuple exprs -> let exprs_with_ids = List.map ~f:(fun e -> (e, Ident.create_fresh Ident.knormal)) exprs in let expr_blocks = diff --git a/infer/tests/codetoanalyze/erlang/features/issues.exp b/infer/tests/codetoanalyze/erlang/features/issues.exp index 55886c773..8d2d3ad84 100644 --- a/infer/tests/codetoanalyze/erlang/features/issues.exp +++ b/infer/tests/codetoanalyze/erlang/features/issues.exp @@ -58,6 +58,25 @@ codetoanalyze/erlang/features/src/logic.erl, test_orelse00_Bad/0, 1, NO_TRUE_BRA codetoanalyze/erlang/features/src/logic.erl, test_unot_Bad/0, 1, NO_TRUE_BRANCH_IN_IF, no_bucket, ERROR, [no true branch in if expression here] codetoanalyze/erlang/features/src/logic.erl, test_xor00_Bad/0, 1, NO_TRUE_BRANCH_IN_IF, no_bucket, ERROR, [no true branch in if expression here] codetoanalyze/erlang/features/src/logic.erl, test_xor11_Bad/0, 1, NO_TRUE_BRANCH_IN_IF, no_bucket, ERROR, [no true branch in if expression here] +codetoanalyze/erlang/features/src/records.erl, test_field2_Bad/0, -38, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field3_Bad/0, -50, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field4_Bad/0, -62, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field_all_other1_Bad/0, -88, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field_all_other2_Bad/0, -100, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field_all_other3_Bad/0, -112, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field_rearranged_Bad/0, -75, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field_update1_Bad/0, -125, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field_update2_Bad/0, -139, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_field_update3_Bad/0, -153, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_index2_Bad/0, -7, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_index3_Bad/0, -17, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_index4_Bad/0, -27, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_initializer1_Bad/0, -168, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_initializer2_Bad/0, -180, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_initializer_explicit_override_Bad/0, -192, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_initializer_update_override_Bad/0, -205, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, test_undefined_Bad/0, -218, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `warn/1`,no matching function clause here] +codetoanalyze/erlang/features/src/records.erl, warn/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] codetoanalyze/erlang/features/src/short_circuit.erl, accepts_one/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] codetoanalyze/erlang/features/src/short_circuit.erl, test_and_Bad/0, -7, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `accepts_one/1`,no matching function clause here] codetoanalyze/erlang/features/src/short_circuit.erl, test_andalso_Bad/0, -15, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `accepts_one/1`,no matching function clause here] diff --git a/infer/tests/codetoanalyze/erlang/features/src/records.erl b/infer/tests/codetoanalyze/erlang/features/src/records.erl new file mode 100644 index 000000000..557640445 --- /dev/null +++ b/infer/tests/codetoanalyze/erlang/features/src/records.erl @@ -0,0 +1,272 @@ +% Copyright (c) Facebook, Inc. and its affiliates. +% +% This source code is licensed under the MIT license found in the +% LICENSE file in the root directory of this source tree. + +-module(records). + +-record(person, {name, phone, address}). + +-export([ + test_index2_Ok/0, + test_index2_Bad/0, + test_index3_Ok/0, + test_index3_Bad/0, + test_index4_Ok/0, + test_index4_Bad/0, + test_field2_Ok/0, + test_field2_Bad/0, + test_field3_Ok/0, + test_field3_Bad/0, + test_field4_Ok/0, + test_field4_Bad/0, + test_field_rearranged_Ok/0, + test_field_rearranged_Bad/0, + test_field_all_other1_Ok/0, + test_field_all_other1_Bad/0, + test_field_all_other2_Ok/0, + test_field_all_other2_Bad/0, + test_field_all_other3_Ok/0, + test_field_all_other3_Bad/0, + test_field_update1_Ok/0, + test_field_update1_Bad/0, + test_field_update2_Ok/0, + test_field_update2_Bad/0, + test_field_update3_Ok/0, + test_field_update3_Bad/0, + test_initializer1_Ok/0, + test_initializer1_Bad/0, + test_initializer2_Ok/0, + test_initializer2_Bad/0, + test_initializer_explicit_override_Ok/0, + test_initializer_explicit_override_Bad/0, + test_initializer_update_override_Ok/0, + test_initializer_update_override_Bad/0, + test_undefined_Ok/0, + test_undefined_Bad/0 +]). + +% Call this method with warn(1) to trigger a warning to expect +warn(0) -> ok. + +test_index2_Ok() -> + case #person.name of + 2 -> ok + end. + +test_index2_Bad() -> + case #person.name of + 2 -> warn(1) + end. + +test_index3_Ok() -> + case #person.phone of + 3 -> ok + end. + +test_index3_Bad() -> + case #person.phone of + 3 -> warn(1) + end. + +test_index4_Ok() -> + case #person.address of + 4 -> ok + end. + +test_index4_Bad() -> + case #person.address of + 4 -> warn(1) + end. + +test_field2_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P#person.name of + 123 -> ok + end. + +test_field2_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P#person.name of + 123 -> warn(1) + end. + +test_field3_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P#person.phone of + 45 -> ok + end. + +test_field3_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P#person.phone of + 45 -> warn(1) + end. + +test_field4_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P#person.address of + 6789 -> ok + end. + +test_field4_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P#person.address of + 6789 -> warn(1) + end. + +test_field_rearranged_Ok() -> + % Fields are set in different order + P = #person{phone = 45, address = 6789, name = 123}, + case P#person.name of + 123 -> ok + end. + +test_field_rearranged_Bad() -> + % Fields are set in different order + P = #person{phone = 45, address = 6789, name = 123}, + case P#person.name of + 123 -> warn(1) + end. + +test_field_all_other1_Ok() -> + P = #person{phone = 45, _ = 123}, + case P#person.name of + 123 -> ok + end. + +test_field_all_other1_Bad() -> + P = #person{phone = 45, _ = 123}, + case P#person.name of + 123 -> warn(1) + end. + +test_field_all_other2_Ok() -> + P = #person{phone = 45, _ = 123}, + case P#person.address of + 123 -> ok + end. + +test_field_all_other2_Bad() -> + P = #person{phone = 45, _ = 123}, + case P#person.address of + 123 -> warn(1) + end. + +test_field_all_other3_Ok() -> + P = #person{phone = 45, _ = 123}, + case P#person.phone of + 45 -> ok + end. + +test_field_all_other3_Bad() -> + P = #person{phone = 45, _ = 123}, + case P#person.phone of + 45 -> warn(1) + end. + +test_field_update1_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + Q = P#person{phone = 0}, + case Q#person.phone of + 0 -> ok + end. + +test_field_update1_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + Q = P#person{phone = 0}, + case Q#person.phone of + 0 -> warn(1) + end. + +test_field_update2_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + Q = P#person{phone = 0}, + case Q#person.name of + 123 -> ok + end. + +test_field_update2_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + Q = P#person{phone = 0}, + case Q#person.name of + 123 -> warn(1) + end. + +test_field_update3_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + Q = P#person{phone = 0}, + case Q#person.address of + 6789 -> ok + end. + +test_field_update3_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + Q = P#person{phone = 0}, + case Q#person.address of + 6789 -> warn(1) + end. + +-record(rabbit, {name = 123, color = 45}). + +test_initializer1_Ok() -> + R = #rabbit{}, + case R#rabbit.name of + 123 -> ok + end. + +test_initializer1_Bad() -> + R = #rabbit{}, + case R#rabbit.name of + 123 -> warn(1) + end. + +test_initializer2_Ok() -> + R = #rabbit{}, + case R#rabbit.color of + 45 -> ok + end. + +test_initializer2_Bad() -> + R = #rabbit{}, + case R#rabbit.color of + 45 -> warn(1) + end. + +test_initializer_explicit_override_Ok() -> + R = #rabbit{name = 6789}, + case R#rabbit.name of + 6789 -> ok + end. + +test_initializer_explicit_override_Bad() -> + R = #rabbit{name = 6789}, + case R#rabbit.name of + 6789 -> warn(1) + end. + +test_initializer_update_override_Ok() -> + R = #rabbit{name = 987, color = 65}, + Q = R#rabbit{name = 4321}, + case Q#rabbit.name of + 4321 -> ok + end. + +test_initializer_update_override_Bad() -> + R = #rabbit{name = 987, color = 65}, + Q = R#rabbit{name = 4321}, + case Q#rabbit.name of + 4321 -> warn(1) + end. + +test_undefined_Ok() -> + P = #person{}, + case P#person.name of + undefined -> ok + end. + +test_undefined_Bad() -> + P = #person{}, + case P#person.name of + undefined -> warn(1) + end. diff --git a/infer/tests/codetoanalyze/erlang/nonmatch/issues.exp b/infer/tests/codetoanalyze/erlang/nonmatch/issues.exp index 288e345f6..c65bbe6bd 100644 --- a/infer/tests/codetoanalyze/erlang/nonmatch/issues.exp +++ b/infer/tests/codetoanalyze/erlang/nonmatch/issues.exp @@ -28,6 +28,21 @@ codetoanalyze/erlang/nonmatch/src/match.erl, match_test_e_Bad/0, -15, NO_MATCHIN codetoanalyze/erlang/nonmatch/src/match.erl, match_test_g_Bad/0, 7, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `only_accepts_one/1`,no matching function clause here] codetoanalyze/erlang/nonmatch/src/match.erl, only_accepts_one/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] codetoanalyze/erlang/nonmatch/src/match.erl, tail/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, accepts_four_using_person/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, accepts_rabbits/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, accepts_three_using_rabbit/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_index3_Bad/0, -10, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `accepts_three_using_rabbit/1`,no matching function clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_index4_Bad/0, -11, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `accepts_four_using_person/1`,no matching function clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_as_tuple2_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_as_tuple3_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_as_tuple4_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_as_tuple5_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_field2_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_field3_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_field4_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_field_multiple2_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_match_field_multiple3_Bad/0, 2, NO_MATCHING_CASE_CLAUSE, no_bucket, ERROR, [no matching case clause here] +codetoanalyze/erlang/nonmatch/src/records.erl, test_type_Bad/0, -5, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [calling context starts here,in call to `accepts_rabbits/1`,no matching function clause here] codetoanalyze/erlang/nonmatch/src/tuples.erl, accepts_empty/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] codetoanalyze/erlang/nonmatch/src/tuples.erl, accepts_tuple_of_two/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] codetoanalyze/erlang/nonmatch/src/tuples.erl, first_from_at_most_three/1, 0, NO_MATCHING_FUNCTION_CLAUSE, no_bucket, ERROR, [*** LATENT ***,no matching function clause here] diff --git a/infer/tests/codetoanalyze/erlang/nonmatch/src/records.erl b/infer/tests/codetoanalyze/erlang/nonmatch/src/records.erl new file mode 100644 index 000000000..9560016f1 --- /dev/null +++ b/infer/tests/codetoanalyze/erlang/nonmatch/src/records.erl @@ -0,0 +1,140 @@ +% Copyright (c) Facebook, Inc. and its affiliates. +% +% This source code is licensed under the MIT license found in the +% LICENSE file in the root directory of this source tree. + +-module(records). + +-record(person, {name, phone, address}). +-record(rabbit, {name, color}). + +-export([ + test_type_Ok/0, + test_type_Bad/0, + test_index1_Ok/0, + test_index2_Ok/0, + test_index3_Bad/0, + test_index4_Bad/0, + test_match_field2_Ok/0, + test_match_field2_Bad/0, + test_match_field3_Ok/0, + test_match_field3_Bad/0, + test_match_field4_Ok/0, + test_match_field4_Bad/0, + test_match_field_multiple1_Ok/0, + test_match_field_multiple2_Bad/0, + test_match_field_multiple3_Bad/0, + test_match_as_tuple1_Ok/0, + test_match_as_tuple2_Bad/0, + test_match_as_tuple3_Bad/0, + test_match_as_tuple4_Bad/0, + test_match_as_tuple5_Bad/0 +]). + +accepts_rabbits(#rabbit{}) -> ok. + +test_type_Ok() -> + accepts_rabbits(#rabbit{name = "bunny", color = "brown"}). + +test_type_Bad() -> + accepts_rabbits(#person{name = "alice", phone = 123, address = "LON"}). + +accepts_three_using_rabbit(#rabbit.color) -> ok. + +accepts_four_using_person(#person.address) -> ok. + +test_index1_Ok() -> + accepts_three_using_rabbit(3). + +test_index2_Ok() -> + accepts_four_using_person(4). + +test_index3_Bad() -> + accepts_three_using_rabbit(2). + +test_index4_Bad() -> + accepts_four_using_person(5). + +test_match_field2_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{name = 123} -> ok + end. + +test_match_field2_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{name = 9999999} -> ok + end. + +test_match_field3_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{phone = 45} -> ok + end. + +test_match_field3_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{phone = 9999999} -> ok + end. + +test_match_field4_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{address = 6789} -> ok + end. + +test_match_field4_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{address = 9999999} -> ok + end. + +test_match_field_multiple1_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{address = 6789, name = 123} -> ok + end. + +test_match_field_multiple2_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{address = 999999, name = 123} -> ok + end. + +test_match_field_multiple3_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + #person{address = 6789, name = 99999} -> ok + end. + +test_match_as_tuple1_Ok() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + {person, 123, 45, 6789} -> ok + end. + +test_match_as_tuple2_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + {rabbit, 123, 45, 6789} -> ok + end. + +test_match_as_tuple3_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + {person, 999999, 45, 6789} -> ok + end. + +test_match_as_tuple4_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + {person, 123, 999999, 6789} -> ok + end. + +test_match_as_tuple5_Bad() -> + P = #person{name = 123, phone = 45, address = 6789}, + case P of + {person, 123, 45, 999999} -> ok + end.