(* * Copyright (c) 2016 - present Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. *) open! IStd module L = Logging let already_imported_files = ref [] let rec parse_import_file import_file channel = if List.mem ~equal:String.equal !already_imported_files import_file then L.(die ExternalError) "Cyclic imports: file '%s' was already imported." import_file else match CTLParserHelper.parse_al_file import_file channel with | 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_and_paths imports curr_file_macros curr_file_paths | None -> L.(debug Linters Medium) "No macros or paths found.@\n" ; ([], []) 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, import_paths = parse_imports imports in L.(debug Linters Medium) "#### Add global macros to import macros #####@\n" ; 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 = let parse_one_import_file fimport (macros, paths) = L.(debug Linters Medium) " Loading import macros from file %s@\n" fimport ; let in_channel = In_channel.create fimport in let parsed_macros, parsed_paths = parse_import_file fimport in_channel in In_channel.close in_channel ; 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 CTLParserHelper.parse_al_file linters_def_file channel with | 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, 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 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 | None -> L.(debug Linters Medium) "No linters found.@\n" ; [] (* Parse the files with linters definitions, and it returns a list of linters *) let parse_ctl_files linters_def_files : CFrontend_errors.linter list = let collect_parsed_linters linters_def_file linters = L.(debug Linters Medium) "Loading linters rules from %s@\n" linters_def_file ; let in_channel = In_channel.create linters_def_file in let parsed_linters = parse_ctl_file linters_def_file in_channel in In_channel.close in_channel ; List.append parsed_linters linters in List.fold_right ~f:collect_parsed_linters ~init:[] linters_def_files let rec get_responds_to_selector stmt = let open Clang_ast_t in let responToSelectorMethods = ["respondsToSelector:"; "instancesRespondToSelector:"] in match stmt with | ObjCMessageExpr (_, [_; (ObjCSelectorExpr (_, _, _, method_name))], _, mdi) | ObjCMessageExpr (_, [(ObjCSelectorExpr (_, _, _, method_name))], _, mdi) when List.mem ~equal:String.equal responToSelectorMethods mdi.Clang_ast_t.omei_selector -> [method_name] | BinaryOperator (_, [stmt1; stmt2], _, bo_info) when PVariant.( = ) bo_info.Clang_ast_t.boi_kind `LAnd -> List.append (get_responds_to_selector stmt1) (get_responds_to_selector stmt2) | ImplicitCastExpr (_, [stmt], _, _) | ParenExpr (_, [stmt], _) | ExprWithCleanups (_, [stmt], _, _) -> get_responds_to_selector stmt | _ -> [] let rec is_core_foundation_version_number stmt = let open Clang_ast_t in match stmt with | DeclRefExpr (_, _, _, decl_ref_info) -> ( match decl_ref_info.drti_decl_ref with | Some decl_ref_info -> let name_info, _, _ = CAst_utils.get_info_from_decl_ref decl_ref_info in String.equal name_info.ni_name "kCFCoreFoundationVersionNumber" | None -> false ) | ImplicitCastExpr (_, [stmt], _, _) -> is_core_foundation_version_number stmt | _ -> false let rec current_os_version_constant stmt = let open Clang_ast_t in match stmt with | FloatingLiteral (_, _, _, number) -> CiOSVersionNumbers.version_of number | IntegerLiteral (_, _, _, info) -> CiOSVersionNumbers.version_of info.ili_value | ImplicitCastExpr (_, [stmt], _, _) -> current_os_version_constant stmt | _ -> None let rec get_current_os_version stmt = let open Clang_ast_t in match stmt with | BinaryOperator (_, [stmt1; stmt2], _, bo_info) when PVariant.( = ) bo_info.Clang_ast_t.boi_kind `GE && is_core_foundation_version_number stmt1 -> Option.to_list (current_os_version_constant stmt2) | BinaryOperator (_, [stmt1; stmt2], _, bo_info) when PVariant.( = ) bo_info.Clang_ast_t.boi_kind `LE && is_core_foundation_version_number stmt2 -> Option.to_list (current_os_version_constant stmt1) | BinaryOperator (_, [stmt1; stmt2], _, bo_info) when PVariant.( = ) bo_info.Clang_ast_t.boi_kind `LAnd -> List.append (get_current_os_version stmt1) (get_current_os_version stmt2) | ImplicitCastExpr (_, [stmt], _, _) | ParenExpr (_, [stmt], _) | ExprWithCleanups (_, [stmt], _, _) -> get_current_os_version stmt | _ -> [] let rec get_ios_available_version stmt = let open Clang_ast_t in match stmt with | ObjCAvailabilityCheckExpr (_, _, _, oacei) -> oacei.oacei_version | ImplicitCastExpr (_, [stmt], _, _) | ParenExpr (_, [stmt], _) | ExprWithCleanups (_, [stmt], _, _) -> get_ios_available_version stmt | _ -> None let compute_if_context (context: CLintersContext.context) stmt = let selector = get_responds_to_selector stmt in let receiver_class_method_call = match (CPredicates.get_selector (Stmt stmt), CPredicates.receiver_class_method_call (Stmt stmt)) with | Some selector, Some receiver when String.equal selector "class" -> Option.to_list (CPredicates.declaration_name receiver) | _ -> [] in let os_version = get_current_os_version stmt in let ios_available_version_opt = Option.to_list (get_ios_available_version stmt) in let os_version = List.append ios_available_version_opt os_version in let within_responds_to_selector_block, within_available_class_block, ios_version_guard = match context.if_context with | Some if_context -> let within_responds_to_selector_block = List.append selector if_context.within_responds_to_selector_block in let within_available_class_block = List.append receiver_class_method_call if_context.within_available_class_block in let ios_version_guard = List.append os_version if_context.ios_version_guard in (within_responds_to_selector_block, within_available_class_block, ios_version_guard) | None -> (selector, receiver_class_method_call, os_version) in Some ( {within_responds_to_selector_block; within_available_class_block; ios_version_guard} : CLintersContext.if_context ) let get_method_body_opt decl = let open Clang_ast_t in match decl with | FunctionDecl (_, _, _, fdi) | CXXMethodDecl (_, _, _, fdi, _) | CXXConstructorDecl (_, _, _, fdi, _) | CXXConversionDecl (_, _, _, fdi, _) | CXXDestructorDecl (_, _, _, fdi, _) -> fdi.Clang_ast_t.fdi_body | ObjCMethodDecl (_, _, mdi) -> mdi.Clang_ast_t.omdi_body | BlockDecl (_, block_decl_info) -> block_decl_info.Clang_ast_t.bdi_body | _ -> Logging.die InternalError "Should only be called with method, but got %s" (Clang_ast_proj.get_decl_kind_string decl) let rec do_frontend_checks_stmt (context: CLintersContext.context) stmt = let open Clang_ast_t in let do_all_checks_on_stmts context stmt = ( match stmt with | DeclStmt (_, _, decl_list) -> List.iter ~f:(do_frontend_checks_decl context) decl_list | BlockExpr (_, _, _, decl) -> List.iter ~f:(do_frontend_checks_decl context) [decl] | _ -> () ) ; do_frontend_checks_stmt context stmt in CFrontend_errors.invoke_set_of_checkers_on_node context (Ctl_parser_types.Stmt stmt) ; match stmt with | ObjCAtSynchronizedStmt (_, stmt_list) -> let stmt_context = {context with CLintersContext.in_synchronized_block= true} in List.iter ~f:(do_all_checks_on_stmts stmt_context) stmt_list | IfStmt (_, [stmt1; stmt2; cond_stmt; inside_if_stmt; inside_else_stmt]) -> (* here we analyze the children of the if stmt with the standard context, except for inside_if_stmt... *) List.iter ~f:(do_all_checks_on_stmts context) [stmt1; stmt2; cond_stmt; inside_else_stmt] ; let inside_if_stmt_context = {context with CLintersContext.if_context= compute_if_context context cond_stmt} in (* ...and here we analyze the stmt inside the if with the context extended with the condition of the if *) do_all_checks_on_stmts inside_if_stmt_context inside_if_stmt | OpaqueValueExpr (_, lstmt, _, opaque_value_expr_info) -> let stmts = match opaque_value_expr_info.Clang_ast_t.ovei_source_expr with | Some stmt -> lstmt @ [stmt] | _ -> lstmt in List.iter ~f:(do_all_checks_on_stmts context) stmts (* given that this has not been translated, looking up for variables *) (* inside leads to inconsistencies *) | ObjCAtCatchStmt _ -> () | _ -> let stmts = snd (Clang_ast_proj.get_stmt_tuple stmt) in List.iter ~f:(do_all_checks_on_stmts context) stmts and do_frontend_checks_decl (context: CLintersContext.context) decl = let open Clang_ast_t in match decl with | FunctionDecl _ | CXXMethodDecl _ | CXXConstructorDecl _ | CXXConversionDecl _ | CXXDestructorDecl _ | ObjCMethodDecl _ | BlockDecl _ -> ( let context' = CLintersContext.update_current_method context decl in CFrontend_errors.invoke_set_of_checkers_on_node context' (Ctl_parser_types.Decl decl) ; match get_method_body_opt decl with | Some stmt -> do_frontend_checks_stmt context' stmt | None -> () ) | ObjCImplementationDecl (_, _, decls, _, _) | ObjCInterfaceDecl (_, _, decls, _, _) -> CFrontend_errors.invoke_set_of_checkers_on_node context (Ctl_parser_types.Decl decl) ; let context' = {context with current_objc_class= Some decl} in List.iter ~f:(do_frontend_checks_decl context') decls | _ -> CFrontend_errors.invoke_set_of_checkers_on_node context (Ctl_parser_types.Decl decl) ; match Clang_ast_proj.get_decl_context_tuple decl with | Some (decls, _) -> List.iter ~f:(do_frontend_checks_decl context) decls | None -> () let context_with_ck_set context decl_list = let is_ck = context.CLintersContext.is_ck_translation_unit || ComponentKit.contains_ck_impl decl_list in if is_ck then {context with CLintersContext.is_ck_translation_unit= true} else context let store_issues source_file = let abbrev_source_file = DB.source_file_encoding source_file in let lint_issues_dir = Config.results_dir ^/ Config.lint_issues_dir_name in Utils.create_dir lint_issues_dir ; let lint_issues_file = DB.filename_from_string (Filename.concat lint_issues_dir (abbrev_source_file ^ ".issue")) in LintIssues.store_issues lint_issues_file !LintIssues.errLogMap let find_linters_files () = let rec find_aux init dir_path = let aux base_path files rel_path = let full_path = Filename.concat base_path rel_path in match (Unix.stat full_path).Unix.st_kind with | Unix.S_REG when String.is_suffix ~suffix:".al" full_path -> full_path :: files | Unix.S_DIR -> find_aux files full_path | _ -> files in Sys.fold_dir ~init ~f:(aux dir_path) dir_path in List.concat (List.map ~f:(fun folder -> find_aux [] folder) Config.linters_def_folder) let linters_files = List.dedup ~compare:String.compare (find_linters_files () @ Config.linters_def_file) let do_frontend_checks (trans_unit_ctx: CFrontend_config.translation_unit_context) ast = L.(debug Capture Quiet) "Loading the following linters files: %a@\n" (Pp.comma_seq Format.pp_print_string) linters_files ; CTL.create_ctl_evaluation_tracker trans_unit_ctx.source_file ; let parsed_linters = parse_ctl_files linters_files in let filtered_parsed_linters = CFrontend_errors.filter_parsed_linters parsed_linters trans_unit_ctx.source_file in CFrontend_errors.parsed_linters := filtered_parsed_linters ; let source_file = trans_unit_ctx.CFrontend_config.source_file in L.(debug Linters Medium) "Start linting file %a with rules: @\n%a@\n" SourceFile.pp source_file CFrontend_errors.pp_linters filtered_parsed_linters ; if Config.print_active_checkers then L.progress "Linting file %a, active linters: @\n%a@\n" SourceFile.pp source_file CFrontend_errors.pp_linters filtered_parsed_linters ; match ast with | Clang_ast_t.TranslationUnitDecl (_, decl_list, _, _) -> let context = context_with_ck_set (CLintersContext.empty trans_unit_ctx) decl_list in let is_decl_allowed decl = let decl_info = Clang_ast_proj.get_decl_tuple decl in CLocation.should_do_frontend_check trans_unit_ctx decl_info.Clang_ast_t.di_source_range in let allowed_decls = List.filter ~f:is_decl_allowed decl_list in (* We analyze the top level and then all the allowed declarations *) CFrontend_errors.invoke_set_of_checkers_on_node context (Ctl_parser_types.Decl ast) ; List.iter ~f:(do_frontend_checks_decl context) allowed_decls ; if LintIssues.exists_issues () then store_issues source_file ; L.(debug Linters Medium) "End linting file %a@\n" SourceFile.pp source_file ; CTL.save_dotty_when_in_debug_mode trans_unit_ctx.CFrontend_config.source_file | _ -> (* NOTE: Assumes that an AST always starts with a TranslationUnitDecl *) assert false