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.