diff --git a/infer/src/clang/ALVar.ml b/infer/src/clang/ALVar.ml index d9df9a82f..94f1ea6a4 100644 --- a/infer/src/clang/ALVar.ml +++ b/infer/src/clang/ALVar.ml @@ -107,3 +107,8 @@ module FormulaIdMap = Caml.Map.Make ( struct type t = formula_id[@@deriving compare] end) + +module VarMap = Caml.Map.Make ( + struct + type t = string[@@deriving compare] + end) diff --git a/infer/src/clang/ALVar.mli b/infer/src/clang/ALVar.mli index 75f08cad7..953ba1f46 100644 --- a/infer/src/clang/ALVar.mli +++ b/infer/src/clang/ALVar.mli @@ -51,3 +51,5 @@ val str_match_regex : string -> string -> bool val compare_str_with_alexp : string -> alexp -> bool module FormulaIdMap : Caml.Map.S with type key = formula_id + +module VarMap : Caml.Map.S with type key = string diff --git a/infer/src/clang/cFrontend_checkers_main.ml b/infer/src/clang/cFrontend_checkers_main.ml index 0b79e7db5..99f0c44fa 100644 --- a/infer/src/clang/cFrontend_checkers_main.ml +++ b/infer/src/clang/cFrontend_checkers_main.ml @@ -34,40 +34,55 @@ let parse_al_file fname channel : CTL.al_file option = let already_imported_files = ref [] -let rec parse_import_file import_file channel : CTL.clause list = +let rec parse_import_file import_file channel = if List.mem !already_imported_files import_file then failwith ("Cyclic imports: file '" ^ import_file ^ "' was already imported.") else ( match parse_al_file import_file channel with - | Some {import_files = imports; global_macros = curr_file_macros; checkers = _} -> + | Some { + import_files = imports; + global_macros = curr_file_macros; + global_paths = curr_file_paths; + checkers = _ + } -> already_imported_files := import_file :: !already_imported_files; - collect_all_macros imports curr_file_macros - | None -> L.(debug Linters Medium) "No macros found.@\n";[]) + collect_all_macros_and_paths imports curr_file_macros curr_file_paths + | None -> L.(debug Linters Medium) "No macros or paths found.@\n";[], []) -and collect_all_macros imports curr_file_macros = +and collect_all_macros_and_paths imports curr_file_macros curr_file_paths = L.(debug Linters Medium) "#### Start parsing import macros #####@\n"; - let import_macros = parse_imports imports in + let import_macros, import_paths = parse_imports imports in L.(debug Linters Medium) "#### Add global macros to import macros #####@\n"; - List.append import_macros curr_file_macros + let macros = List.append import_macros curr_file_macros in + let paths = List.append import_paths curr_file_paths in + macros, paths (* Parse import files with macro definitions, and it returns a list of LET clauses *) -and parse_imports imports_files : CTL.clause list = - let parse_one_import_file fimport macros = +and parse_imports imports_files = + let parse_one_import_file fimport (macros, paths) = L.(debug Linters Medium) " Loading import macros from file %s@\n" fimport; let in_channel = open_in fimport in - let parsed_macros = parse_import_file fimport in_channel in + let parsed_macros, parsed_paths = parse_import_file fimport in_channel in In_channel.close in_channel; - List.append parsed_macros macros in - List.fold_right ~f:parse_one_import_file ~init:[] imports_files + let macros = List.append parsed_macros macros in + let paths = List.append parsed_paths paths in + macros, paths in + List.fold_right ~f:parse_one_import_file ~init:([], []) imports_files let parse_ctl_file linters_def_file channel : CFrontend_errors.linter list = match parse_al_file linters_def_file channel with - | Some {import_files = imports; global_macros = curr_file_macros; checkers = parsed_checkers} -> + | Some { + import_files = imports; + global_macros = curr_file_macros; + global_paths = curr_file_paths; + checkers = parsed_checkers + } -> already_imported_files := [linters_def_file]; - let macros = collect_all_macros imports curr_file_macros in + let macros, paths = collect_all_macros_and_paths imports curr_file_macros curr_file_paths in let macros_map = CFrontend_errors.build_macros_map macros in + let paths_map = CFrontend_errors.build_paths_map paths in L.(debug Linters Medium) "#### Start Expanding checkers #####@\n"; - let exp_checkers = CFrontend_errors.expand_checkers macros_map parsed_checkers in + let exp_checkers = CFrontend_errors.expand_checkers macros_map paths_map parsed_checkers in L.(debug Linters Medium) "#### Checkers Expanded #####@\n"; if Config.debug_mode then List.iter ~f:CTL.print_checker exp_checkers; CFrontend_errors.create_parsed_linters linters_def_file exp_checkers diff --git a/infer/src/clang/cFrontend_errors.ml b/infer/src/clang/cFrontend_errors.ml index 9148bb266..4309f833b 100644 --- a/infer/src/clang/cFrontend_errors.ml +++ b/infer/src/clang/cFrontend_errors.ml @@ -64,6 +64,9 @@ let pp_linters fmt linters = formula was already expanded and, if yes we have a cyclic definifion *) type macros_map = (bool * ALVar.t list * CTL.t) ALVar.FormulaIdMap.t +(* Map a path name to a list of paths. *) +type paths_map = (ALVar.t list) ALVar.VarMap.t + let single_to_multi checker = fun ctx an -> let issue_desc_opt = checker ctx an in @@ -285,6 +288,18 @@ let expand_formula phi _map _error_msg = | ETX (tl, sw, f1) -> ETX (tl, sw, expand f1 map error_msg) in expand phi _map _error_msg +let rec expand_path paths path_map = + match paths with + | [] -> [] + | ALVar.Var path_var :: rest -> + (try + let paths = ALVar.VarMap.find path_var path_map in + List.append paths (expand_path rest path_map) + with Not_found -> failwithf "Path variable %s not found. " path_var) + | path :: rest -> + path :: (expand_path rest path_map) + + let _build_macros_map macros init_map = let macros_map = List.fold ~f:(fun map' data -> match data with | CTL.CLet (key, params, formula) -> @@ -299,8 +314,18 @@ let build_macros_map macros = let init_map : macros_map = ALVar.FormulaIdMap.empty in _build_macros_map macros init_map +let build_paths_map paths = + let build_paths_map_aux paths init_map = + let paths_map = List.fold ~f:(fun map' data -> match data with + | (path_name, paths) -> + if ALVar.VarMap.mem path_name map' then + failwith ("Path '" ^ path_name ^ "' has more than one definition.") + else ALVar.VarMap.add path_name paths map') ~init:init_map paths in + paths_map in + build_paths_map_aux paths ALVar.VarMap.empty + (* expands use of let defined formula id in checkers with their definition *) -let expand_checkers macro_map checkers = +let expand_checkers macro_map path_map checkers = let open CTL in let expand_one_checker c = L.(debug Linters Medium) " +Start expanding %s@\n" c.name; @@ -310,6 +335,9 @@ let expand_checkers macro_map checkers = | CSet (report_when_const, phi) -> L.(debug Linters Medium) " -Expanding report_when@\n"; CSet (report_when_const, expand_formula phi map "") :: defs + | CPath (black_or_white_list, paths) -> + L.(debug Linters Medium) " -Expanding path@\n"; + CPath (black_or_white_list, expand_path paths path_map) :: defs | cl -> cl :: defs) ~init:[] c.definitions in { c with definitions = exp_defs} in List.map ~f:expand_one_checker checkers diff --git a/infer/src/clang/cFrontend_errors.mli b/infer/src/clang/cFrontend_errors.mli index 63f7016c3..f31fcf422 100644 --- a/infer/src/clang/cFrontend_errors.mli +++ b/infer/src/clang/cFrontend_errors.mli @@ -27,6 +27,9 @@ val pp_linters : Format.formatter -> linter list -> unit formula was already expanded and, if yes we have a cyclic definifion *) type macros_map = (bool * ALVar.t list * CTL.t) ALVar.FormulaIdMap.t +(* Map a path name to a list of paths. *) +type paths_map = (ALVar.t list) ALVar.VarMap.t + (* List of checkers that will be filled after parsing them from a file *) val parsed_linters : linter list ref @@ -37,7 +40,9 @@ val invoke_set_of_checkers_on_node : CLintersContext.context -> Ctl_parser_types val build_macros_map : CTL.clause list -> macros_map -val expand_checkers : macros_map -> CTL.ctl_checker list -> CTL.ctl_checker list +val build_paths_map : (string * ALVar.alexp list) list -> paths_map + +val expand_checkers : macros_map -> paths_map -> CTL.ctl_checker list -> CTL.ctl_checker list val create_parsed_linters : string -> CTL.ctl_checker list -> linter list diff --git a/infer/src/clang/cTL.ml b/infer/src/clang/cTL.ml index dd32d6d50..791d9a391 100644 --- a/infer/src/clang/cTL.ml +++ b/infer/src/clang/cTL.ml @@ -92,6 +92,7 @@ type ctl_checker = { type al_file = { import_files : string list; global_macros : clause list; + global_paths : (string * ALVar.alexp list) list; checkers : ctl_checker list } diff --git a/infer/src/clang/cTL.mli b/infer/src/clang/cTL.mli index c5c550f69..7dc773124 100644 --- a/infer/src/clang/cTL.mli +++ b/infer/src/clang/cTL.mli @@ -90,6 +90,7 @@ type ctl_checker = { type al_file = { import_files : string list; global_macros : clause list; + global_paths : (string * ALVar.alexp list) list; checkers : ctl_checker list } diff --git a/infer/src/clang/ctl_lexer.mll b/infer/src/clang/ctl_lexer.mll index 306b1f473..3ea2d5945 100644 --- a/infer/src/clang/ctl_lexer.mll +++ b/infer/src/clang/ctl_lexer.mll @@ -49,6 +49,7 @@ rule token = parse | "WITH-TRANSITION" {WITH_TRANSITION} | "DEFINE-CHECKER" { DEFINE_CHECKER } | "GLOBAL-MACROS" { GLOBAL_MACROS } + | "GLOBAL-PATHS" { GLOBAL_PATHS } | "#IMPORT" { HASHIMPORT } | "SET" { SET } | "LET" { LET } diff --git a/infer/src/clang/ctl_parser.mly b/infer/src/clang/ctl_parser.mly index a27dbc380..7e196828f 100644 --- a/infer/src/clang/ctl_parser.mly +++ b/infer/src/clang/ctl_parser.mly @@ -21,12 +21,13 @@ ^ "which cannot be used in identifiers:")) else () - let is_defined_identifier id = - if (List.mem ~equal:ALVar.equal !formal_params (ALVar.Var id)) then - L.(debug Linters Verbose) "\tParsed exp '%s' as variable" id - else - raise (Ctl_parser_types.ALParsingException - ("ERROR: Variable '" ^ id ^ "' is undefined")) + let is_defined_identifier id = + if (List.mem ~equal:ALVar.equal !formal_params (ALVar.Var id)) then + L.(debug Linters Verbose) "\tParsed exp '%s' as variable" id + else + raise (Ctl_parser_types.ALParsingException + ("ERROR: Variable '" ^ id ^ "' is undefined")) + %} %token EU @@ -40,6 +41,7 @@ %token EH %token DEFINE_CHECKER %token GLOBAL_MACROS +%token GLOBAL_PATHS %token HASHIMPORT %token LESS_THAN %token GREATER_THAN @@ -95,8 +97,8 @@ formal_params: | var_list { formal_params := $1; $1} al_file: - | import_files global_macros checkers_list { - { CTL.import_files = $1; CTL.global_macros = $2; CTL.checkers = $3 } + | import_files global_macros global_paths checkers_list { + { CTL.import_files = $1; CTL.global_macros = $2; CTL.global_paths = $3; CTL.checkers = $4 } } ; @@ -112,6 +114,21 @@ global_macros: { L.(debug Linters Verbose) "Parsed global macro definitions...@\n@\n"; $3 } ; +global_path_declaration: + | LET identifier ASSIGNMENT LEFT_BRACE path_list RIGHT_BRACE SEMICOLON { ($2, $5) } +; + +global_paths_list: + | global_path_declaration { [$1] } + | global_path_declaration SEMICOLON global_paths_list { $1 :: $3 } +; + +global_paths: + | { [] } + | GLOBAL_PATHS LEFT_BRACE global_paths_list RIGHT_BRACE SEMICOLON + { L.(debug Linters Verbose) "Parsed global path definitions...@\n"; $3 } + ; + checkers_list: | EOF { [] } | checker SEMICOLON checkers_list { $1::$3 } @@ -128,8 +145,8 @@ checker: ; path_list: - | alexp { [$1] } - | alexp COMMA path_list { $1 :: $3 } + | alexp_path { [$1] } + | alexp_path COMMA path_list { $1 :: $3 } ; clause_list: @@ -254,16 +271,29 @@ formula: ; -alexp: - | STRING { is_not_infer_reserved_id $1; - L.(debug Linters Verbose) "\tParsed string constant '%s'@\n" $1; - ALVar.Const $1 } - | REGEXP LEFT_PAREN STRING RIGHT_PAREN +alexp_const: STRING + { is_not_infer_reserved_id $1; + L.(debug Linters Verbose) "\tParsed string constant '%s'@\n" $1; + ALVar.Const $1 } + +alexp_regex: REGEXP LEFT_PAREN STRING RIGHT_PAREN { L.(debug Linters Verbose) "\tParsed regular expression '%s'@\n" $3; ALVar.Regexp $3 } - | identifier { is_defined_identifier $1; ALVar.Var $1 } + +alexp_var: identifier { is_defined_identifier $1; ALVar.Var $1 } + +alexp: + | alexp_const {$1} + | alexp_regex {$1} + | alexp_var { $1} ; + alexp_path: + | alexp_const {$1} + | alexp_regex {$1} + | identifier { ALVar.Var $1 } + ; + identifier: | IDENTIFIER { is_not_infer_reserved_id $1; L.(debug Linters Verbose) "\tParsed identifier '%s'@\n" $1; $1 } diff --git a/infer/tests/codetoanalyze/objc/linters-for-test-only/library.al b/infer/tests/codetoanalyze/objc/linters-for-test-only/library.al index f6ba13996..ddd261220 100644 --- a/infer/tests/codetoanalyze/objc/linters-for-test-only/library.al +++ b/infer/tests/codetoanalyze/objc/linters-for-test-only/library.al @@ -5,3 +5,8 @@ GLOBAL-MACROS { is_class(x) HOLDS-IN-SOME-SUPERCLASS-OF ObjCInterfaceDecl; }; + + +GLOBAL-PATHS { + LET all_files = {REGEXP(".*") }; +}; diff --git a/infer/tests/codetoanalyze/objc/linters-for-test-only/linters_example.al b/infer/tests/codetoanalyze/objc/linters-for-test-only/linters_example.al index 2f99ba059..4a9bf6cde 100644 --- a/infer/tests/codetoanalyze/objc/linters-for-test-only/linters_example.al +++ b/infer/tests/codetoanalyze/objc/linters-for-test-only/linters_example.al @@ -30,6 +30,10 @@ GLOBAL-MACROS { }; + GLOBAL-PATHS { + LET filtered_files = {REGEXP("codetoanalyze/objc/linters-for-test-only/filter_by_path/.*") }; + }; + //Check that class A is not subclassed. DEFINE-CHECKER SUBCLASSING_TEST_EXAMPLE = { @@ -242,7 +246,7 @@ DEFINE-CHECKER FILTER_BY_PATH_EXAMPLE = { WHEN declaration_has_name("main") HOLDS-IN-NODE FunctionDecl; SET message = "Found main method"; - SET whitelist_path = { REGEXP("codetoanalyze/objc/linters-for-test-only/filter_by_path/.*"), "A.m" }; + SET whitelist_path = { filtered_files, "A.m" }; }; DEFINE-CHECKER ALL_PATH_NO_FILTER_EXAMPLE = { @@ -273,6 +277,6 @@ DEFINE-CHECKER WHITE_BLACKLIST_PATH_EXAMPLE = { WHEN declaration_has_name("main") HOLDS-IN-NODE FunctionDecl; SET message = "Found main method"; - SET whitelist_path = { REGEXP(".*") }; - SET blacklist_path = { REGEXP("codetoanalyze/objc/linters-for-test-only/filter_by_path/.*") }; + SET whitelist_path = { all_files }; + SET blacklist_path = { filtered_files }; };