diff --git a/Makefile b/Makefile index 42e1a83af..558a1c2d1 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,7 @@ DIRECT_TESTS += \ java_crashcontext java_harness endif ifneq ($(XCODE_SELECT),no) -DIRECT_TESTS += objc_frontend objc_errors objc_linters objcpp_frontend objcpp_linters +DIRECT_TESTS += objc_frontend objc_errors objc_linters objc_ioslinters objcpp_frontend objcpp_linters endif .PHONY: all diff --git a/infer/src/base/Config.ml b/infer/src/base/Config.ml index 47da63825..f5f9f07ea 100644 --- a/infer/src/base/Config.ml +++ b/infer/src/base/Config.ml @@ -846,6 +846,10 @@ and infer_cache = CLOpt.mk_path_opt ~deprecated:["infer_cache"; "-infer_cache"] ~long:"infer-cache" ~meta:"dir" "Select a directory to contain the infer cache (Buck and Java only)" +and iphoneos_target_sdk_version = + CLOpt.mk_string_opt ~long:"iphoneos-target-sdk-version" ~exes:CLOpt.[Toplevel;Clang] + "Specify the target SDK version to use for iphoneos" + and iterations = CLOpt.mk_int ~deprecated:["iterations"] ~long:"iterations" ~default:1 ~meta:"int" @@ -1433,6 +1437,7 @@ and frontend_stats = !frontend_stats and headers = !headers and icfg_dotty_outfile = !icfg_dotty_outfile and infer_cache = !infer_cache +and iphoneos_target_sdk_version = !iphoneos_target_sdk_version and iterations = !iterations and java_jar_compiler = !java_jar_compiler and javac_verbose_out = !verbose_out diff --git a/infer/src/base/Config.mli b/infer/src/base/Config.mli index 9a02800e1..0cfd1d3d1 100644 --- a/infer/src/base/Config.mli +++ b/infer/src/base/Config.mli @@ -196,6 +196,7 @@ val generated_classes : string option val headers : bool val icfg_dotty_outfile : string option val infer_cache : string option +val iphoneos_target_sdk_version : string option val is_originator : bool val iterations : int val java_jar_compiler : string option diff --git a/infer/src/base/Utils.ml b/infer/src/base/Utils.ml index 6a81a976b..85953eea1 100644 --- a/infer/src/base/Utils.ml +++ b/infer/src/base/Utils.ml @@ -342,3 +342,14 @@ let suppress_stderr2 f2 x1 x2 = let f () = f2 x1 x2 in let finally () = restore_stderr orig_stderr in protect ~f ~finally + +let compare_versions v1 v2 = + let int_list_of_version v = + let lv = String.split ~on:'.' v in + let int_of_string_or_zero v = + try int_of_string v + with Failure _ -> 0 in + List.map ~f:int_of_string_or_zero lv in + let lv1 = int_list_of_version v1 in + let lv2 = int_list_of_version v2 in + [%compare : int list] lv1 lv2 diff --git a/infer/src/base/Utils.mli b/infer/src/base/Utils.mli index f4e7cbe05..2d13fec94 100644 --- a/infer/src/base/Utils.mli +++ b/infer/src/base/Utils.mli @@ -79,3 +79,8 @@ val realpath : string -> string (** wraps a function expecting 2 arguments in another that temporarily redirects stderr to /dev/null for the duration of the function call *) val suppress_stderr2 : ('a -> 'b -> 'c) -> 'a -> 'b -> 'c + +(** [compare_versions v1 v2] returns 1 if v1 is newer than v2, + -1 if v1 is older than v2 and 0 if they are the same version. + The versions are strings of the shape "n.m.t", the order is lexicographic. *) +val compare_versions : string -> string -> int diff --git a/infer/src/clang/cFrontend_checkers.ml b/infer/src/clang/cFrontend_checkers.ml index 391e90a64..38c60602d 100644 --- a/infer/src/clang/cFrontend_checkers.ml +++ b/infer/src/clang/cFrontend_checkers.ml @@ -41,7 +41,6 @@ let location_from_an lcxt an = | CTL.Stmt st -> location_from_stmt lcxt st | CTL.Decl d -> location_from_decl lcxt d - let decl_name an = match an with | CTL.Decl dec -> @@ -50,6 +49,34 @@ let decl_name an = | None -> "") | _ -> "" +let tag_name_of_node an = + match an with + | CTL.Stmt stmt -> Clang_ast_proj.get_stmt_kind_string stmt + | CTL.Decl decl -> Clang_ast_proj.get_decl_kind_string decl + +let decl_ref_or_selector_name an = + match CTL.next_state_via_transition an (Some CTL.PointerToDecl) with + | Some (CTL.Decl ObjCMethodDecl _ as decl_an) -> + "The selector " ^ (decl_name decl_an) + | Some (CTL.Decl _ as decl_an) -> + "The reference " ^ (decl_name decl_an) + | _ -> failwith("decl_ref_or_selector_name must be called with a DeclRefExpr \ + or an ObjCMessageExpr, but got " ^ (tag_name_of_node an)) + +let iphoneos_target_sdk_version _ = + match Config.iphoneos_target_sdk_version with + | Some f -> f + | None -> "0" + +let available_ios_sdk an = + match CTL.next_state_via_transition an (Some CTL.PointerToDecl) with + | Some CTL.Decl decl -> + (match Predicates.get_available_attr_ios_sdk decl with + | Some version -> version + | None -> "") + | _ -> failwith("available_ios_sdk must be called with a DeclRefExpr \ + or an ObjCMessageExpr, but got " ^ (tag_name_of_node an)) + let ivar_name an = let open Clang_ast_t in match an with @@ -270,3 +297,22 @@ let ctl_captured_cxx_ref_in_objc_block_warning lctx an = | _ -> location_from_an lctx an; } in condition, Some issue_desc + +(** If the declaration has avilability attributes, check that it's compatible with + the iphoneos_target_sdk_version *) +let ctl_unavailable_api_in_supported_ios_sdk_error lctx an = + let open CTL in + let condition = + InNode(["DeclRefExpr"; "ObjCMessageExpr"], + EX (Some PointerToDecl, (Atomic ("decl_unavailable_in_supported_ios_sdk", [])))) in + let issue_desc = + { CIssue.name = "UNAVAILABLE_API_IN_SUPPORTED_IOS_SDK"; + severity = Exceptions.Kerror; + mode = CIssue.On; + description = + "%decl_ref_or_selector_name% is not available in the required iOS SDK version \ + %iphoneos_target_sdk_version% (only available from version %available_ios_sdk%)"; + suggestion = Some "This could cause a crash."; + loc = location_from_an lctx an + } in + condition, Some issue_desc diff --git a/infer/src/clang/cFrontend_checkers.mli b/infer/src/clang/cFrontend_checkers.mli index 54b65d62f..ef39a4cb1 100644 --- a/infer/src/clang/cFrontend_checkers.mli +++ b/infer/src/clang/cFrontend_checkers.mli @@ -29,6 +29,12 @@ val ctl_direct_atomic_property_access_warning : val ctl_captured_cxx_ref_in_objc_block_warning : CLintersContext.context -> CTL.ast_node -> CTL.t * CIssue.issue_desc option +(** Unavailable_api_in_supported_os_error : + If the declaration has avilability attributes, check that it's compatible with + the iphoneos_target_sdk_version *) +val ctl_unavailable_api_in_supported_ios_sdk_error : + CLintersContext.context -> CTL.ast_node -> CTL.t * CIssue.issue_desc option + val ctl_bad_pointer_comparison_warning : CLintersContext.context -> CTL.ast_node -> CTL.t * CIssue.issue_desc option @@ -57,3 +63,9 @@ val decl_name : CTL.ast_node -> string val ivar_name : CTL.ast_node -> string val var_name : CTL.ast_node -> string + +val decl_ref_or_selector_name : CTL.ast_node -> string + +val iphoneos_target_sdk_version : CTL.ast_node -> string + +val available_ios_sdk : CTL.ast_node -> string diff --git a/infer/src/clang/cFrontend_errors.ml b/infer/src/clang/cFrontend_errors.ml index c416712d9..063ccdb89 100644 --- a/infer/src/clang/cFrontend_errors.ml +++ b/infer/src/clang/cFrontend_errors.ml @@ -11,7 +11,7 @@ open! IStd open CFrontend_utils -(* List of checkers on properties *) +(* List of checkers on decls *) let decl_checkers_list = [CFrontend_checkers.ctl_strong_delegate_warning; CFrontend_checkers.ctl_assign_pointer_warning; CFrontend_checkers.ctl_ns_notification_warning; @@ -22,12 +22,13 @@ let decl_checkers_list = [CFrontend_checkers.ctl_strong_delegate_warning; ComponentKit.component_file_cyclomatic_complexity_info; ComponentKit.component_with_multiple_factory_methods_advice;] -(* List of checkers on ivar access *) +(* List of checkers on stmts *) let stmt_checkers_list = [CFrontend_checkers.ctl_direct_atomic_property_access_warning; CFrontend_checkers.ctl_captured_cxx_ref_in_objc_block_warning; CFrontend_checkers.ctl_bad_pointer_comparison_warning; ComponentKit.component_file_cyclomatic_complexity_info; - ComponentKit.component_initializer_with_side_effects_advice;] + ComponentKit.component_initializer_with_side_effects_advice; + CFrontend_checkers.ctl_unavailable_api_in_supported_ios_sdk_error;] (* List of checkers on translation unit that potentially output multiple issues *) let translation_unit_checkers_list = [ComponentKit.component_file_line_count_info;] @@ -37,6 +38,11 @@ let evaluate_place_holder ph an = | "%ivar_name%" -> CFrontend_checkers.ivar_name an | "%decl_name%" -> CFrontend_checkers.decl_name an | "%var_name%" -> CFrontend_checkers.var_name an + | "%decl_ref_or_selector_name%" -> + CFrontend_checkers.decl_ref_or_selector_name an + | "%iphoneos_target_sdk_version%" -> + CFrontend_checkers.iphoneos_target_sdk_version an + | "%available_ios_sdk%" -> CFrontend_checkers.available_ios_sdk an | _ -> (Logging.err "ERROR: helper function %s is unknown. Stop.\n" ph; assert false) diff --git a/infer/src/clang/cTL.ml b/infer/src/clang/cTL.ml index bec2f061c..e8a817123 100644 --- a/infer/src/clang/cTL.ml +++ b/infer/src/clang/cTL.ml @@ -19,10 +19,11 @@ open CFrontend_utils (* Transition labels used for example to switch from decl to stmt *) type transitions = - | Body (* decl to stmt *) - | InitExpr (* decl to stmt *) - | Super (* decl to decl *) + | Body (** decl to stmt *) + | InitExpr (** decl to stmt *) + | Super (** decl to decl *) | Cond + | PointerToDecl (** stmt to decl *) (* In formulas below prefix "E" means "exists a path" @@ -60,7 +61,8 @@ module Debug = struct | Body -> Format.pp_print_string fmt "Body" | InitExpr -> Format.pp_print_string fmt "InitExpr" | Super -> Format.pp_print_string fmt "Super" - | Cond -> Format.pp_print_string fmt "Cond" in + | Cond -> Format.pp_print_string fmt "Cond" + | PointerToDecl -> Format.pp_print_string fmt "PointerToDecl" in match trans_opt with | Some trans -> pp_aux fmt trans | None -> Format.pp_print_string fmt "_" @@ -337,6 +339,19 @@ let transition_stmt_to_stmt_via_condition st = | WhileStmt (_, [_; cond; _]) -> Some (Stmt cond) | _ -> None +let transition_stmt_to_decl_via_pointer stmt = + let open Clang_ast_t in + match stmt with + | ObjCMessageExpr (_, _, _, obj_c_message_expr_info) -> + (match Ast_utils.get_decl_opt obj_c_message_expr_info.Clang_ast_t.omei_decl_pointer with + | Some decl -> Some (Decl decl) + | None -> None) + | DeclRefExpr (_, _, _, decl_ref_expr_info) -> + (match Ast_utils.get_decl_opt_with_decl_ref decl_ref_expr_info.Clang_ast_t.drti_decl_ref with + | Some decl -> Some (Decl decl) + | None -> None) + | _ -> None + (* given a node an returns the node an' such that an transition to an' via label trans *) let next_state_via_transition an trans = match an, trans with @@ -344,6 +359,8 @@ let next_state_via_transition an trans = | Decl d, Some InitExpr | Decl d, Some Body -> transition_decl_to_stmt d trans | Stmt st, Some Cond -> transition_stmt_to_stmt_via_condition st + | Stmt st, Some PointerToDecl -> + transition_stmt_to_decl_via_pointer st | _, _ -> None (* Evaluation of formulas *) @@ -373,6 +390,8 @@ let eval_Atomic pred_name args an lcxt = | "in_node", [nodename], Stmt st -> Predicates.is_stmt nodename st | "in_node", [nodename], Decl d -> Predicates.is_decl nodename d | "isa", [classname], Stmt st -> Predicates.isa classname st + | "decl_unavailable_in_supported_ios_sdk", [], Decl decl -> + Predicates.decl_unavailable_in_supported_ios_sdk decl | _ -> failwith ("ERROR: Undefined Predicate or wrong set of arguments: " ^ pred_name) (* st, lcxt |= EF phi <=> diff --git a/infer/src/clang/cTL.mli b/infer/src/clang/cTL.mli index a6288eb7a..ef75252ae 100644 --- a/infer/src/clang/cTL.mli +++ b/infer/src/clang/cTL.mli @@ -20,6 +20,7 @@ type transitions = | InitExpr (* decl to stmt *) | Super (* decl to decl *) | Cond + | PointerToDecl (* stmt to decl *) (* In formulas below prefix "E" means "exists a path" @@ -65,6 +66,7 @@ val eval_formula : t -> ast_node -> CLintersContext.context -> bool val save_dotty_when_in_debug_mode : SourceFile.t -> unit +val next_state_via_transition : ast_node -> transitions option -> ast_node option module Debug : sig val pp_formula : Format.formatter -> t -> unit diff --git a/infer/src/clang/linter_rules/linters.al b/infer/src/clang/linter_rules/linters.al index 533bc5272..7c8fe471f 100644 --- a/infer/src/clang/linter_rules/linters.al +++ b/infer/src/clang/linter_rules/linters.al @@ -191,3 +191,18 @@ DEFINE-CHECKER ctl_captured_cxx_ref_in_objc_block_warning = { SET suggestion = "C++ References are unmanaged and may be invalid by the time the block executes."; }; + + DEFINE-CHECKER ctl_unavailable_api_in_supported_ios_sdk_error = { + SET report_when = + WHEN + WITH-TRANSITION PointerToDecl decl_unavailable_in_supported_ios_sdk + HOLDS-IN-NODE DeclRefExpr, ObjCMessageExpr; + + SET message = + "%decl_ref_or_selector_name% is available only starting \ + from ios sdk %available_ios_sdk% but we support earlier versions from \ + ios sdk %iphoneos_target_sdk_version%; + + SET suggestion = "This could cause a crash."; + + }; diff --git a/infer/src/clang/predicates.ml b/infer/src/clang/predicates.ml index 70c879092..9487d660a 100644 --- a/infer/src/clang/predicates.ml +++ b/infer/src/clang/predicates.ml @@ -11,6 +11,20 @@ open! IStd open CFrontend_utils +let get_available_attr_ios_sdk decl = + let open Clang_ast_t in + let decl_info = Clang_ast_proj.get_decl_tuple decl in + let rec get_available_attr attrs = + match attrs with + | [] -> None + | AvailabilityAttr attr_info :: _ -> + (match attr_info.Clang_ast_t.ai_parameters with + | "ios" :: version :: _ -> + Some (String.Search_pattern.replace_all + (String.Search_pattern.create "_") ~in_:version ~with_:".") + | _ -> None) + | _ :: rest -> get_available_attr rest in + get_available_attr decl_info.Clang_ast_t.di_attributes let get_ivar_attributes ivar_decl = let open Clang_ast_t in @@ -203,3 +217,10 @@ let isa classname stmt = let typ = CFrontend_utils.Ast_utils.get_desugared_type expr_info.ei_type_ptr in CFrontend_utils.Ast_utils.is_ptr_to_objc_class typ classname | _ -> false + +let decl_unavailable_in_supported_ios_sdk decl = + let available_attr_ios_sdk = get_available_attr_ios_sdk decl in + match available_attr_ios_sdk, Config.iphoneos_target_sdk_version with + | Some available_attr_ios_sdk, Some iphoneos_target_sdk_version -> + Utils.compare_versions available_attr_ios_sdk iphoneos_target_sdk_version = 1 + | _ -> false diff --git a/infer/src/clang/predicates.mli b/infer/src/clang/predicates.mli index 798a9098c..11913496f 100644 --- a/infer/src/clang/predicates.mli +++ b/infer/src/clang/predicates.mli @@ -54,3 +54,7 @@ val is_stmt : string -> Clang_ast_t.stmt -> bool val is_decl : string -> Clang_ast_t.decl -> bool val pp_predicate : Format.formatter -> t -> unit + +val decl_unavailable_in_supported_ios_sdk : Clang_ast_t.decl -> bool + +val get_available_attr_ios_sdk : Clang_ast_t.decl -> string option diff --git a/infer/tests/codetoanalyze/objc/ioslinters/Makefile b/infer/tests/codetoanalyze/objc/ioslinters/Makefile new file mode 100644 index 000000000..021421ae2 --- /dev/null +++ b/infer/tests/codetoanalyze/objc/ioslinters/Makefile @@ -0,0 +1,27 @@ +# 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. + +TESTS_DIR = ../../.. + +IPHONESIMULATOR_ISYSROOT_SUFFIX = /Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk + +XCODEROOT = $(shell xcode-select -p) + +CLANG_OPTIONS = -x objective-c \ + -isysroot $(XCODEROOT)$(IPHONESIMULATOR_ISYSROOT_SUFFIX) \ + -mios-simulator-version-min=8.2 --target=x86_64-apple-darwin14 -fobjc-arc -c \ + +ANALYZER = linters +INFER_OPTIONS = --no-filtering --debug-exceptions --project-root $(TESTS_DIR) \ +--iphoneos-target-sdk-version 8.0 +INFERPRINT_OPTIONS = --issues-tests + +SOURCES = \ + $(wildcard *.m) \ + $(wildcard */*.m) \ + +include $(TESTS_DIR)/clang.make diff --git a/infer/tests/codetoanalyze/objc/ioslinters/issues.exp b/infer/tests/codetoanalyze/objc/ioslinters/issues.exp new file mode 100644 index 000000000..778b18db4 --- /dev/null +++ b/infer/tests/codetoanalyze/objc/ioslinters/issues.exp @@ -0,0 +1,2 @@ +codetoanalyze/objc/ioslinters/unavailable_api_in_supported_ios_sdk.m, OpenURLOptionsFromSourceApplication, 18, UNAVAILABLE_API_IN_SUPPORTED_IOS_SDK, [] +codetoanalyze/objc/ioslinters/unavailable_api_in_supported_ios_sdk.m, Unavailable_api_in_supported_ios_sdk_test:and:, 11, UNAVAILABLE_API_IN_SUPPORTED_IOS_SDK, [] diff --git a/infer/tests/codetoanalyze/objc/ioslinters/unavailable_api_in_supported_ios_sdk.m b/infer/tests/codetoanalyze/objc/ioslinters/unavailable_api_in_supported_ios_sdk.m new file mode 100644 index 000000000..e6e3ce675 --- /dev/null +++ b/infer/tests/codetoanalyze/objc/ioslinters/unavailable_api_in_supported_ios_sdk.m @@ -0,0 +1,20 @@ +#import + +@interface Unavailable_api_in_supported_ios_sdk : NSObject + +@end + +@implementation Unavailable_api_in_supported_ios_sdk + +- (void)test:(int)n and:(NSData*)data { + NSDictionary* cacheData = + [NSKeyedUnarchiver unarchiveTopLevelObjectWithData:data error:nil]; +} +@end + +static NSDictionary* OpenURLOptionsFromSourceApplication( + NSString* sourceApplication) { + NSDictionary* options = + @{UIApplicationOpenURLOptionsSourceApplicationKey : sourceApplication}; + return options; +}