├── README.md ├── do-git └── testlint ├── sources ├── .DS_Store ├── Linter.h ├── Linter.m ├── NSArray+Frankenstein.h ├── NSArray+Frankenstein.m ├── RegexHelper.h ├── RegexHelper.m ├── Utility.h └── Utility.m ├── testlint.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── testlint.xccheckout │ └── xcuserdata │ │ └── ericasadun.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ ├── testlint-personal.xcscheme │ │ └── testlint.xcscheme └── xcuserdata │ └── ericasadun.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── testlint.xcscheme │ └── xcschememanagement.plist └── testlint └── main.m /README.md: -------------------------------------------------------------------------------- 1 | # testlint 2 | 3 | Sometimes little thrown-together solutions create greater value than their aesthetics would promise. 4 | 5 | ###Setup 6 | 7 | * Compile 8 | * Add to Swift project build phases (Target > Build Phases > or use directly at the command line. 9 | 10 | ![Setup](http://i.imgur.com/EIApOcy.jpg) 11 | 12 | When you do not add arguments, the utility looks for an .xcodeproj folder in the same directory. Run it this way at the top level of a project or as a build script. 13 | 14 | No dependencies. 15 | 16 | ###Options: 17 | 18 | Usage: /Users/ericasadun/bin/testlint options file... 19 | help: -help 20 | Use 'NOTE: ', 'ERROR: ', 'WARNING: ', HACK, and FIXME to force emit 21 | Use 'nwm' to skip individual line processing: // nwm 22 | Use ##SkipAccessChecksForFile somewhere to skip file processing 23 | 24 | 25 | ###Future directions: 26 | 27 | For any of the nuanced features (specifically constructors and public access checks), this tool is either leaky like a sieve or creating false positives all over the place. I'm exploring possibilities but even with these issues, I'm using the utility regularly and am happy with the help it offers. 28 | 29 | 30 | -------------------------------------------------------------------------------- /do-git: -------------------------------------------------------------------------------- 1 | #! /bin/csh -f 2 | 3 | if ($#argv != 1) then 4 | echo "Usage $0 commit-message" 5 | exit 1 6 | endif 7 | 8 | rm -rf build 9 | rm .DS_Store 10 | rm */.DS_Store 11 | rm -rf *.xcodeproj/ericasadun.* 12 | git add -A 13 | git commit -m "$1" 14 | git push 15 | 16 | 17 | -------------------------------------------------------------------------------- /testlint/sources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erica/testlint/a22392e90e047de681b77979635b5b5dd5d893b5/testlint/sources/.DS_Store -------------------------------------------------------------------------------- /testlint/sources/Linter.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import 8 | 9 | @interface Linter : NSObject 10 | @property (nonatomic) BOOL encounteredErrors; // = NO 11 | - (void) lint: (NSString *) path; 12 | @end 13 | -------------------------------------------------------------------------------- /testlint/sources/Linter.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import "Linter.h" 8 | #import "Utility.h" 9 | #import "NSArray+Frankenstein.h" 10 | #import "RegexHelper.h" 11 | 12 | /* 13 | 14 | To explore: 15 | - verbosity for anonymous types $0 vs something in 16 | - title case for enum variants 17 | - title case for global scope and types 18 | - overly long names? 19 | - avoiding i, j, k, tmp 20 | - excessive nesting 21 | - giant statements 22 | - protocol beautification 23 | - protocol single-letter abuse vs meaningful names 24 | - missing access modifiers? access modifier scan? 25 | - De Morgan's law check? (probably not) 26 | 27 | Possible: 28 | - Eliminate get in read-only computed properties and subscripts 29 | - omit return keyword in single-expression closure 30 | - construct names capped 31 | - enum cases capped 32 | - switch's should have at least n clauses 33 | - field names shouldn't dupe construct names 34 | - backticks and reserved words -- not for symbol names 35 | - avoid implicit unwrapped? 36 | - 37 | 38 | - See various in book chapters (there's a lot) 39 | 40 | */ 41 | 42 | @implementation Linter 43 | 44 | #pragma mark - Init 45 | 46 | - (instancetype) init 47 | { 48 | if (!(self = [super init])) return self; 49 | _encounteredErrors = NO; 50 | return self; 51 | } 52 | 53 | #pragma mark - Delintage 54 | 55 | - (void) lint: (NSString *) path 56 | { 57 | NSString *string = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; 58 | if (!string) return; 59 | 60 | // BOOL topLevel = [string containsString:@"@UIApplicationMain"]; // reserved for later use 61 | 62 | // I have some files that are purely generic type stuff and access checks kills 'em 63 | BOOL skipAccessCheckForFile = [string containsString:@"##SkipAccessChecksForFile"]; 64 | if (skipAccessCheckForFile) 65 | { 66 | Log(@"Developer-directed file skip"); 67 | return; 68 | } 69 | 70 | // Splinter into lines 71 | NSArray *lines = [string componentsSeparatedByString:@"\n"]; 72 | 73 | int count = 0; 74 | int cautions = 0; 75 | int warnings = 0; 76 | 77 | // In shell build phases you can write to stderr using the following format: 78 | // :: error | warn | note : \n 79 | 80 | for (NSString *eachLine in lines) 81 | { 82 | ++count; 83 | NSString *line = eachLine; 84 | 85 | // META PROCESSING 86 | // This material is always active and should never be overridden by command-line parameters 87 | { 88 | 89 | #pragma mark - Keyword Processing 90 | 91 | // No worries, mate! Skip any line with nwm 92 | if ([RegexHelper testString:@"nwm" inString:line]) continue; 93 | 94 | // Convert all FIXMEs to warnings 95 | if ([RegexHelper testString:@"FIXME" inString:line]) 96 | { 97 | ++warnings; 98 | Log(@"%@:%zd: warning: Line %zd is broken", path, count, count); 99 | Log(@"%@", line); 100 | } 101 | else if ([RegexHelper testString:@"TODO" inString:line]) 102 | { 103 | ++warnings; 104 | Log(@"%@:%zd: warning: Line %zd needs addressing", path, count, count); 105 | Log(@"%@", line); 106 | } 107 | else if ([RegexHelper testString:@"HACK" inString:line]) 108 | { 109 | ++warnings; 110 | Log(@"%@:%zd: warning: Line %zd uses inelegant problem solving", path, count, count); 111 | Log(@"%@", line); 112 | } 113 | else if ([RegexHelper testString:@"NOTE: " inString:line]) 114 | { 115 | NSRange range = [line rangeOfString:@"NOTE: "]; // should always be found 116 | NSString *remaining = [line substringFromIndex:range.location]; 117 | Log(@"%@:%zd: note: Line %zd : %@", path, count, count, remaining); 118 | Log(@"%@", line); 119 | } 120 | else if ([RegexHelper testString:@"WARNING: " inString:line]) 121 | { 122 | NSRange range = [line rangeOfString:@"WARNING: "]; // should always be found 123 | NSString *remaining = [line substringFromIndex:range.location]; 124 | Log(@"%@:%zd: warning: Line %zd : %@", path, count, count, remaining); 125 | Log(@"%@", line); 126 | } 127 | else if ([RegexHelper testString:@"ERROR: " inString:line]) 128 | { 129 | NSRange range = [line rangeOfString:@"ERROR: "]; // should always be found 130 | NSString *remaining = [line substringFromIndex:range.location]; 131 | Log(@"%@:%zd: error: Line %zd : %@", path, count, count, remaining); 132 | Log(@"%@", line); 133 | _encounteredErrors = YES; 134 | } 135 | 136 | #pragma mark - Single-line Comment Processing 137 | 138 | // Avoid false pings on lines by clipping trailing comments 139 | NSRange range = [line rangeOfString:@"// "]; 140 | if (range.location != NSNotFound) 141 | { 142 | line = [line substringToIndex:range.location]; 143 | 144 | // Also trims start of line, but that shouldn't be an issue for the following checks 145 | line = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 146 | } 147 | 148 | // Skip commented lines. This is not particularly reliable. 149 | if ([RegexHelper testPattern:@"^\\s*//" inString:line]) 150 | continue; 151 | } 152 | 153 | #pragma mark - Swift 3.0 Warnings 154 | { 155 | /* 156 | 157 | Extra catches for me 158 | 159 | */ 160 | 161 | if ([RegexHelper testPattern:@"XCPSetExecutionShouldContinueIndefinitely" inString:line] || 162 | [RegexHelper testPattern:@"XCPSharedDataDirectoryPath" inString:line] || 163 | [RegexHelper testPattern:@"XCPCaptureValue" inString:line] || 164 | [RegexHelper testPattern:@"XCPShowView" inString:line] 165 | ) 166 | { 167 | ++warnings; 168 | Log(@"%@:%zd: warning old style XCPlayground use", path, count, count); 169 | } 170 | 171 | /* 172 | 173 | https://github.com/apple/swift-evolution/blob/master/proposals/0001-keywords-as-argument-labels.md 174 | 175 | Reliability/Stability: probably medium-high 176 | 177 | */ 178 | 179 | if ([RegexHelper testPattern:@"[^+]\\+\\+[^+]" inString:line] 180 | ) 181 | { 182 | ++warnings; 183 | Log(@"%@:%zd: warning: ++ and -- operators to be removed in Swift 3.0", path, count, count); 184 | } 185 | 186 | /* 187 | 188 | https://github.com/apple/swift-evolution/blob/master/proposals/0004-remove-pre-post-inc-decrement.md 189 | 190 | Reliability/Stability: probably medium 191 | 192 | */ 193 | 194 | if ([RegexHelper testPattern:@"`\\w+`\\s*:" inString:line] 195 | ) 196 | { 197 | ++warnings; 198 | Log(@"%@:%zd: warning: keyword escapes will not be needed in Swift 3.0", path, count, count); 199 | } 200 | 201 | /* 202 | 203 | https://github.com/apple/swift-evolution/blob/master/proposals/0002-remove-currying.md 204 | 205 | Reliability/Stability: probably medium 206 | 207 | */ 208 | 209 | if ([RegexHelper testPattern:@"func.*\\(.*\\)\\(.*\\)" inString:line] 210 | ) 211 | { 212 | ++warnings; 213 | Log(@"%@:%zd: warning: direct currying to be removed Swift 3.0", path, count, count); 214 | } 215 | 216 | /* 217 | 218 | https://github.com/apple/swift-evolution/blob/master/proposals/0007-remove-c-style-for-loops.md 219 | 220 | Reliability/Stability: probably medium 221 | 222 | Note: May double-catch with for var x in array due to use with, e.g. 223 | 224 | for var i = 0 ; i < 10 ; i++ { 225 | print(i) 226 | } 227 | 228 | Note: May also double-catch with ++ and -- tests 229 | 230 | */ 231 | 232 | if ([RegexHelper testPattern:@"for.*;.*;" inString:line] 233 | ) 234 | { 235 | ++warnings; 236 | Log(@"%@:%zd: warning: C-style for-loops to be removed Swift 3.0", path, count, count); 237 | } 238 | 239 | 240 | /* 241 | 242 | https://github.com/apple/swift-evolution/blob/master/proposals/0003-remove-var-parameters-patterns.md 243 | 244 | Reliability/Stability: probably medium 245 | 246 | */ 247 | 248 | if ([RegexHelper testPattern:@"func.*\\Wvar " inString:line] || 249 | [RegexHelper testPattern:@"guard\\s+var " inString:line] || 250 | [RegexHelper testPattern:@"while\\s+var " inString:line] || 251 | [RegexHelper testPattern:@"case\\s+var" inString:line] || 252 | [RegexHelper testPattern:@"case\\s+\\.\\w+\\(var" inString:line] || 253 | [RegexHelper testPattern:@"case\\s+\\.\\w+\\(.*, var" inString:line] || 254 | [RegexHelper testPattern:@"for\\s+var" inString:line] 255 | ) 256 | { 257 | ++warnings; 258 | Log(@"%@:%zd: warning: var parameters to be removed Swift 3.0", path, count, count); 259 | } 260 | 261 | if ( 262 | [RegexHelper testPattern:@"if\\s+var " inString:line] 263 | ) 264 | { 265 | ++warnings; 266 | Log(@"%@:%zd: warning: var parameters to be removed Swift 3.0. Use if let and move shadowed var assignment into the following scope", path, count, count); 267 | } 268 | 269 | } 270 | 271 | #pragma mark - STYLE ISSUES 272 | 273 | // STYLE ISSUES 274 | { 275 | 276 | #pragma mark - Check for Colon style issues 277 | 278 | /* 279 | 280 | This is another one that goes against personal taste. 281 | I much prefer spaces before and after colons for the most part (except 282 | in parameter lists, where I left associate with the label) but again 283 | consistency and peer pressure wins. 284 | 285 | Reliability/Stability: Medium 286 | 287 | */ 288 | // Spaces before colons 289 | if ([RegexHelper testPattern:@"\\?\\s+.*:" inString:line]) 290 | { 291 | // ignore "? x : y" in ternary statements 292 | } 293 | else if ([RegexHelper testPattern:@"\\?\\s+:" inString:line]) 294 | { 295 | // ignore "? :" in ternary statements 296 | } 297 | else if ([RegexHelper testPattern:@"\".*:.*\"" inString:line]) 298 | { 299 | // ignore between quotes (will miss a bunch here) 300 | } 301 | else if ([RegexHelper testPattern:@"\\?\\s+:" inString:line]) 302 | { 303 | // ignore "? :" in ternary statements 304 | } 305 | else if ([RegexHelper testPattern:@"\\s+\\:" inString:line]) 306 | { 307 | ++warnings; 308 | Log(@"%@:%zd: warning: Line %zd uses a space before a colon", path, count, count); 309 | } 310 | 311 | // Spaces missing after colons 312 | if ([RegexHelper testPattern:@"http\\:\\S+" inString:line]) { 313 | // skip http: 314 | } 315 | else if ([RegexHelper testPattern:@"\"H\\:" inString:line] || 316 | [RegexHelper testPattern:@"\"V\\:" inString:line]) 317 | { 318 | // skip for "H: and "V: which are quite common for auto layout VPL 319 | } 320 | else if ([RegexHelper testPattern:@"[:]" inString:line]) 321 | { 322 | // allow [:] 323 | } 324 | else if ([RegexHelper testPattern:@"\\:\\S+" inString:line]) 325 | { 326 | ++warnings; 327 | Log(@"%@:%zd: warning: Line %zd lacks a space after a colon", path, count, count); 328 | } 329 | 330 | #pragma mark - Kevin Tests 331 | 332 | /* 333 | 334 | The Rule of Kevin: “When a trailing closure argument is functional, 335 | use parentheses. When it is procedural, use braces.” 336 | The consistency of style communicates whether closures return values. 337 | There’s an ongoing dispute as to whether a space should be left before 338 | trailing braces. 339 | 340 | Reliability/Stability: Quite Low. Needs a *lot* of love. 341 | 342 | NOT: withUnsafeBufferPointer, withUnsafeMutableBufferPointer, withUTF8Buffer, withCString, 343 | withExtendedLifetime, withUnsafePointer, withUnsafePointers, withUnsafeMutablePointer, withUnsafeMutablePointers, 344 | withVaList 345 | 346 | s: `@"\b(map|flatMap|filter|indexOf|minElement|...)\s*{"` 347 | 348 | */ 349 | 350 | if ([RegexHelper testPattern:@"map\\s*\\{" inString:line] || 351 | [RegexHelper testPattern:@"filter\\s*\\{" inString:line] || 352 | [RegexHelper testPattern:@"flatMap\\s*\\{" inString:line] || 353 | [RegexHelper testPattern:@"withCString\\s*\\{" inString:line] || 354 | [RegexHelper testPattern:@"minElement\\s*\\{" inString:line] || 355 | [RegexHelper testPattern:@"maxElement\\s*\\{" inString:line] || 356 | [RegexHelper testPattern:@"sort\\s*\\{" inString:line] || 357 | [RegexHelper testPattern:@"indexOf\\s*{" inString:line] || 358 | [RegexHelper testPattern:@"startsWith\\s*{" inString:line] || 359 | [RegexHelper testPattern:@"elementsEqual\\s*{" inString:line] || 360 | [RegexHelper testPattern:@"lexicographicalCompare\\s*{" inString:line] || 361 | // [RegexHelper testPattern:@"contains\\s*{" inString:line] || 362 | // [RegexHelper testPattern:@"startsWith\\s*{" inString:line] || 363 | [RegexHelper testPattern:@"split\\s*{" inString:line] || 364 | [RegexHelper testPattern:@"reduce\\s*\\{" inString:line] 365 | ) 366 | { 367 | ++warnings; 368 | Log(@"%@:%zd: warning: Line %zd fails the Rule of Kevin. Treat functional (non-procedural) trailing closures as arguments by enclosing within parens", path, count, count); 369 | } 370 | else if ([RegexHelper testPattern:@"sortInPlace\\s*\\({" inString:line] || 371 | [RegexHelper testPattern:@"startsWith\\s*\\({" inString:line] || 372 | [RegexHelper testPattern:@"elementsEqual\\s*\\({" inString:line] || 373 | [RegexHelper testPattern:@"contains\\s*\\({" inString:line] || 374 | [RegexHelper testPattern:@"element\\s*\\({" inString:line] || 375 | [RegexHelper testPattern:@"flatten\\s*\\({" inString:line] || 376 | [RegexHelper testPattern:@"forEach\\s*\\({" inString:line] || 377 | [RegexHelper testPattern:@"lexicographicalCompare\\s*\\({" inString:line] 378 | ) 379 | { 380 | ++warnings; 381 | Log(@"%@:%zd: warning: Line %zd fails the Rule of Kevin. Skip parens around procedural calls", path, count, count); 382 | } 383 | 384 | #pragma mark - Core Geometry tests 385 | 386 | /* 387 | 388 | Prefer modern constructors, accessors, constants to old-style ones 389 | 390 | Reliability/Stability: Medium 391 | 392 | */ 393 | 394 | if ([RegexHelper testString:@"CGRectMake" inString:line] || 395 | [RegexHelper testString:@"CGPointMake" inString:line] || 396 | [RegexHelper testString:@"CGSizeMake" inString:line] || 397 | [RegexHelper testString:@"CGVectorMake" inString:line] 398 | ) 399 | { 400 | ++warnings; 401 | Log(@"%@:%zd: warning: Line %zd: Prefer native constructors over old-style convenience functions", path, count, count); 402 | } 403 | else if ((![RegexHelper testString:@"CGAffineTransformMake" inString:line]) && 404 | ([RegexHelper testPattern:@"CG[:word:]*Make" inString:line] || 405 | [RegexHelper testString:@"NSMake" inString:line])) 406 | { 407 | ++warnings; 408 | Log(@"%@:%zd: warning: Line %zd: Prefer native constructors over old-style convenience functions", path, count, count); 409 | } 410 | 411 | 412 | if ([RegexHelper testString:@"CGPointGetMinX" inString:line] || 413 | [RegexHelper testString:@"CGPointGetMinY" inString:line] || 414 | [RegexHelper testString:@"CGPointGetMidX" inString:line] || 415 | [RegexHelper testString:@"CGPointGetMidY" inString:line] || 416 | [RegexHelper testString:@"CGPointGetMaxX" inString:line] || 417 | [RegexHelper testString:@"CGPointGetMaxY" inString:line] 418 | ) 419 | { 420 | ++warnings; 421 | Log(@"%@:%zd: warning: Line %zd uses non-Swift Core Geometry value accessors. Use .min, .mid, .max properties instead", path, count, count); 422 | } 423 | 424 | 425 | if ([RegexHelper testString:@"CGRectGetHeight" inString:line] || 426 | [RegexHelper testString:@"CGRectGetWidth" inString:line] 427 | ) 428 | { 429 | ++warnings; 430 | Log(@"%@:%zd: warning: Line %zd uses non-Swift Core Geometry value accessors. Use .width, .height properties instead", path, count, count); 431 | } 432 | 433 | 434 | if ([RegexHelper testString:@"CGRectZero" inString:line] || 435 | [RegexHelper testString:@"CGPointZero" inString:line] || 436 | [RegexHelper testString:@"CGSizeZero" inString:line] || 437 | [RegexHelper testString:@"CGRectInfinite" inString:line] || 438 | [RegexHelper testString:@"CGRectNull" inString:line] 439 | ) 440 | { 441 | ++warnings; 442 | Log(@"%@:%zd: warning: Line %zd uses non-Swift Core Geometry constants. Use .zero, .infinite, .null static properties instead", path, count, count); 443 | } 444 | 445 | if ([RegexHelper testString:@"CGRectMinXEdge" inString:line] || 446 | [RegexHelper testString:@"CGRectMinYEdge" inString:line] || 447 | [RegexHelper testString:@"CGRectMaxXEdge" inString:line] || 448 | [RegexHelper testString:@"CGRectMaxYEdge" inString:line] 449 | ) 450 | { 451 | ++warnings; 452 | Log(@"%@:%zd: warning: Line %zd uses non-Swift Core Geometry edge types. Use min(XY)/mid(XY)/max(XY) properties instead", path, count, count); 453 | } 454 | 455 | if ([RegexHelper testString:@"CGFLOAT_MIN" inString:line] || 456 | [RegexHelper testString:@"CGFLOAT_MAX" inString:line] 457 | ) 458 | { 459 | ++warnings; 460 | Log(@"%@:%zd: warning: Line %zd uses non-Swift Core Geometry constants. Use CGFloat.min and CGFloat.max instead", path, count, count); 461 | } 462 | 463 | 464 | #pragma mark - Use of yorn (always bad, not swift specific) 465 | /* 466 | 467 | yorn is a bad habit I need to break. 468 | Reliability/Stability: High 469 | 470 | */ 471 | 472 | if ([RegexHelper testString:@"yorn" inString:line]) 473 | { 474 | ++warnings; 475 | Log(@"%@:%zd: warning: Line %zd uses yorn. yorn is wrong. Prefer parameter-specific boolean names over yes-or-no", path, count, count); 476 | } 477 | 478 | #pragma mark - Single-line Brace Check 479 | 480 | /* 481 | 482 | Reliability/Stability: High but I'm not sure I really care about this test 483 | 484 | */ 485 | 486 | { 487 | if ([RegexHelper testPattern:@"\\{.*;.*\\}" inString:line]) 488 | { 489 | ++warnings; 490 | Log(@"%@:%zd: warning: Line %zd Excessive content in single-line scope", path, count, count); 491 | } 492 | 493 | /* 494 | 495 | Excessive single-line brace length 496 | I've disabled this for now, but I'm wavering back and forth on its value. 497 | This would apply a maximum size check instead of existence check 498 | 499 | */ 500 | 501 | // if ([RegexHelper testPattern:@"\\{.{80,}\\}" inString:line]) 502 | // { 503 | // ++warnings; 504 | // Log(@"%@:%zd: Line %zd warning: Excessive content in single-line scope", path, count, count); 505 | // } 506 | } 507 | 508 | #pragma mark - Allman Check 509 | 510 | /* 511 | 512 | True fact. I love 💗💗 Allman. It's the best for teaching and reading, not to mention the 513 | better cognitive load for general dev. However, I'm operating under external pressures. 514 | I have caved here. 515 | 516 | Reliability/Stability: High, but :( 517 | 518 | */ 519 | 520 | // Allman 521 | if ([RegexHelper testPattern:@"^\\s*\\{" inString:line]) 522 | { 523 | ++warnings; 524 | Log(@"%@:%zd: warning: Line %zd uses egregious Allman pattern. Welcome to Swift.", path, count, count); 525 | } 526 | 527 | if (![RegexHelper testString:@"#else" inString:line] && 528 | ([RegexHelper testPattern:@"^\\s*else" inString:line] || 529 | [RegexHelper testPattern:@"else\\s*$" inString:line])) 530 | { 531 | ++warnings; 532 | Log(@"%@:%zd: warning: Line %zd Else case does not follow colinear 1TBS standard.", path, count, count); 533 | } 534 | 535 | // // This is the anti-Allman version. I don't know why I have it here. 536 | // if ([RegexHelper testPattern:@"\\S+\\s*\\{" inString:line]) 537 | // { 538 | // ++warnings; 539 | // Log(@"%@:%zd: warning: Line %zd uses non-Allman pattern. Kittens cry.", path, count, count); 540 | // } 541 | // 542 | // if ([RegexHelper testPattern:@"\\}\\s+else" inString:line] || 543 | // [RegexHelper testPattern:@"else\\s+{" inString:line]) 544 | // { 545 | // ++warnings; 546 | // Log(@"%@:%zd: warning: Line %zd Else case does not follow Allman standard.", path, count, count); 547 | // } 548 | 549 | #pragma mark - Use of temp, tmp 550 | /* 551 | 552 | Avoid meaningless names 553 | stability/reliability: high 554 | exhaustive definition: low 555 | 556 | */ 557 | if ([RegexHelper testString:@"temp" inString:line] || 558 | [RegexHelper testString:@"tmp" inString:line] || 559 | [RegexHelper testString:@"results" inString:line] || 560 | [RegexHelper testString:@"returnValue" inString:line]) 561 | { 562 | ++warnings; 563 | Log(@"%@:%zd: warning: Line %zd: Avoid semantically meaningless names like temp, tmp, returnValue", path, count, count); 564 | } 565 | 566 | /* 567 | 568 | Prefer meaningful index names 569 | stability/reliability: medium-ish 570 | Maybe replace with for var rule (prefer in over C-style loops) 571 | or forEach. 572 | 573 | */ 574 | if ([RegexHelper testPattern:@"var i[\\s=]" inString:line] || 575 | [RegexHelper testPattern:@"var j[\\s=]" inString:line] || 576 | [RegexHelper testPattern:@"for.*\\s+i\\s+in" inString:line]) 577 | { 578 | ++warnings; 579 | Log(@"%@:%zd: warning: Line %zd: Avoid semantically meaningless iterator names (i, j)", path, count, count); 580 | } 581 | } 582 | 583 | // FILE HYGIENE 584 | 585 | #pragma mark - Trailing Whitespace 586 | /* 587 | 588 | Trailing whitespace 589 | Reliability/Stability: Very high 590 | 591 | */ 592 | 593 | // Trailing whitespace on lines 594 | if ([RegexHelper testPattern:@"\\s+//.*\\S\\s+$" inString:line]) 595 | { 596 | // ignore extra spaces on comment lines 597 | } 598 | else if ([RegexHelper testPattern:@"\\S\\s+$" inString:line]) 599 | { 600 | ++warnings; 601 | Log(@"%@:%zd: warning: Line %zd includes trailing whitespace characters", path, count, count); 602 | } 603 | 604 | #pragma mark - LANGUAGE ISSUES 605 | 606 | #pragma mark - Collection Constructor Checks 607 | /* 608 | 609 | Collection Constructors 610 | Reliability/Stability: Medium-High 611 | 612 | */ 613 | 614 | if ([RegexHelper testPattern:@"=.*>\\(\\)" inString:line]|| 615 | [RegexHelper testPattern:@"=.*]\\(\\)" inString:line] 616 | ) { 617 | ++warnings; 618 | Log(@"%@:%zd: warning: Line %zd: avoid type-based constructors and prefer [] or [:] initialization instead", path, count, count); 619 | } 620 | 621 | 622 | 623 | // LANGUAGE ISSUES 624 | /* 625 | 626 | This is terrible with embedded functions (no access modifiers) and properties but it will generally start highlighting 627 | files that you haven't done a full access modifier audit on, so you know which items to buckle down on and fix. 628 | Wouldn't it be great if Swift did this bit for you, basically when you say: "Make everything in this construct 629 | public that can be public" and then you can tweak down what you want to be internal and private. 630 | 631 | There are some implementation details such as no public modifiers for many generic details, so be aware of this going in. 632 | 633 | */ 634 | 635 | 636 | #pragma mark - Space check after init 637 | /* 638 | 639 | init checks - skip space between init or init? and ( 640 | stability/reliability: High 641 | 642 | */ 643 | if ([RegexHelper testPattern:@"init\\?*\\s+\\(" inString:line]){ 644 | ++warnings; 645 | Log(@"%@:%zd: warning: Line %zd: Extraneous space in init( or init?( declaration", path, count, count); 646 | } 647 | 648 | #pragma mark - Prefer for-in or forEach over C-style loops 649 | /* 650 | 651 | Mostly avoid "for var" in favor of for name in 652 | stability/reliability: med 653 | Not sure this is a great rule or not 654 | 655 | */ 656 | if ([RegexHelper testString:@"for var" inString:line]) 657 | { 658 | ++warnings; 659 | Log(@"%@:%zd: warning: Line %zd: Prefer for-in or forEach over C-style loops", path, count, count); 660 | } 661 | 662 | #pragma mark - (...) -> Void check 663 | 664 | /* 665 | 666 | Void return type check 667 | stability/reliability high 668 | 669 | */ 670 | 671 | // Check that -> return tokens point to Void and not () and that they are surrounded by spaces 672 | if ([RegexHelper testString:@"->=" inString:line]) { 673 | // Skip ->= 674 | } else if ([RegexHelper testPattern:@"->\\s*\\(\\)" inString:line]) { 675 | ++warnings; 676 | Log(@"%@:%zd: warning: Line %zd: Prefer Void as a return type over ()", path, count, count); 677 | } else if ([RegexHelper testPattern:@"->\\S" inString:line] || 678 | [RegexHelper testPattern:@"\\S->" inString:line]) { 679 | ++warnings; 680 | Log(@"%@:%zd: warning: Line %zd: leave spaces around the -> return token", path, count, count); 681 | } 682 | 683 | #pragma mark - Terminal semicolons 684 | 685 | /* 686 | 687 | Terminal semicolons check 688 | stability/reliability high 689 | 690 | */ 691 | 692 | 693 | // Eliminate line-terminating semicolons 694 | if ([RegexHelper testPattern:@";\\s*$" inString:line]) 695 | { 696 | ++warnings; 697 | Log(@"%@:%zd: warning: Line %zd: Swift does not require terminal semicolons except for statement separation. They are bad. You should feel bad.", path, count, count); 698 | } 699 | 700 | #pragma mark - Check for parens around if conditions 701 | 702 | /* 703 | 704 | Eliminate parentheses around if conditions 705 | stability/reliability medium 706 | can fail with if (x == y) && (z == w) for example 707 | 708 | */ 709 | 710 | if ([RegexHelper testPattern:@"if[\\s]*\\(" inString:line]) 711 | { 712 | ++warnings; 713 | Log(@"%@:%zd: warning: Line %zd: Swift if statements do not require parentheses", path, count, count); 714 | } 715 | 716 | #pragma mark - Check for parens around switch conditions 717 | 718 | /* 719 | 720 | Eliminate parentheses around switch conditions that aren't tuples 721 | stability/reliability medium 722 | 723 | */ 724 | if ([RegexHelper testPattern:@"switch\\s*\\(.*,.*\\)\\s*\\{" inString:line]) 725 | { 726 | // skip likely tuples 727 | } 728 | else if ([RegexHelper testPattern:@"switch\\s*\\(" inString:line]) 729 | { 730 | ++warnings; 731 | Log(@"%@:%zd: warning: Line %zd: Swift switch statements do not require parentheses", path, count, count); 732 | } 733 | 734 | #pragma mark - Check for Pattern Matching issues 735 | /* 736 | 737 | Just starting on this, which is for testing for less 738 | desirable pattern matching habits. 739 | 740 | Reliability/Stability: Quite Low 741 | 742 | */ 743 | 744 | if ([RegexHelper testPattern:@"case\\s*\\(let" inString:line] || 745 | [RegexHelper testPattern:@"case\\s*\\(var" inString:line]) 746 | { 747 | ++warnings; 748 | Log(@"%@:%zd: warning: Line %zd embeds let/var. Consider moving the binding keyword outs of the parens", path, count, count); 749 | } 750 | 751 | #pragma mark - Extraneous lets 752 | 753 | /* 754 | 755 | Extraneous lets. For multi-line in-context scan, would test for ,\s*\n\s*let 756 | stability/reliability medium 757 | 758 | */ 759 | 760 | if ([RegexHelper testString:@"case" inString:line]) 761 | { 762 | // do not test with case statements 763 | } 764 | else if ([RegexHelper testString:@"(.*let" inString:line]) 765 | { 766 | // Skip lines that are likely tuple assignments in switch statements 767 | } 768 | else if ([RegexHelper testString:@", let" inString:line]) 769 | { 770 | ++warnings; 771 | Log(@"%@:%zd: warning: Line %zd: Check for extraneous let usage in cascaded let", path, count, count); 772 | } 773 | 774 | #pragma mark - Forced unwraps and casts 775 | /* 776 | 777 | Forced unwraps and casts 778 | Limit use of forced unwrap and casts, however there are some cromulent reasons to 779 | use these. Currently issued as warning rather than caution, but may downgrade or 780 | offer forceChecksAreCautions option. 781 | 782 | stability/reliability medium 783 | 784 | */ 785 | if ([RegexHelper testString:@" as!" inString:line]) 786 | { 787 | ++warnings; 788 | Log(@"%@:%zd: warning: Line %zd: Forced casts are generally unsafe", path, count, count); 789 | } 790 | else if ([RegexHelper testPattern:@":\\s*\\w+! " inString:line]) 791 | { 792 | // Also a do-nothing. This is likely an implicitly unwrapped declaration 793 | } 794 | else if ([RegexHelper testString:@"! " inString:line]) 795 | { 796 | ++warnings; 797 | Log(@"%@:%zd: warning: Line %zd: Forced unwrapping is unsafe. Use let to conditionally unwrap where possible.", path, count, count); 798 | } 799 | 800 | #pragma mark - Extraneous break statements 801 | 802 | /* 803 | 804 | Eliminate extraneous breaks in switch patterns other than in default statements 805 | For multi-line in-context, should test for control-flow use of break 806 | 807 | stability/reliability medium 808 | 809 | */ 810 | 811 | if ([RegexHelper testPattern:@":\\s+break" inString:line]) 812 | { 813 | // skip "case ...: break" cases 814 | } 815 | else if ([RegexHelper testString:@"{" inString:line] || 816 | [RegexHelper testString:@"}" inString:line]) 817 | { 818 | // whitelist any break on a line with } or { 819 | } 820 | else if ((lines.count > (count + 1)) && 821 | [RegexHelper testString:@"}" inString:lines[count + 1]]) 822 | { 823 | // whitelist any break is followed by a line with a } 824 | } 825 | else if ([RegexHelper testString:@"break" inString:line]) 826 | { 827 | // was the previous line "default"? 828 | // this line is count - 1. previous line is count - 2 829 | if ((count - 2) > 0) 830 | { 831 | NSString *previousLine = lines[count - 2]; 832 | if (![RegexHelper testString:@"default:" inString:previousLine]) 833 | { 834 | ++warnings; 835 | Log(@"%@:%zd: warning: Line %zd: Swift cases do not implicitly fall through.", path, count, count); 836 | } 837 | } 838 | } 839 | 840 | #pragma mark - Empty count checks 841 | 842 | /* 843 | 844 | Test for .count == 0 and count() == 0 that might be better as isEmpty 845 | Should this be a warning or a caution? May also catch mirror.count 846 | 847 | stability/reliability medium to high 848 | 849 | */ 850 | 851 | if ([RegexHelper testPattern:@"mirror" inString:line]) 852 | { 853 | // Ignore any line that references mirrors. 854 | } 855 | else if ([RegexHelper testPattern:@"\\.count\\s*==\\s*0" inString:line] || 856 | [RegexHelper testPattern:@"count\\(.*\\)\\s*==\\s*0" inString:line]) 857 | { 858 | ++warnings; 859 | Log(@"%@:%zd: warning: Line %zd: Consider replacing zero count check with isEmpty.", path, count, count); 860 | } 861 | 862 | #pragma mark - NSNotFound checks 863 | 864 | /* 865 | 866 | Test for any use of NSNotFound, use contains instead? 867 | 868 | stability/reliability high 869 | 870 | */ 871 | 872 | if ([RegexHelper testPattern:@"!=\\s*NSNotFound" inString:line]) 873 | { 874 | ++warnings; 875 | Log(@"%@:%zd: warning: Line %zd: Consider replacing NSNotFound pattern with contains", path, count, count); 876 | } 877 | 878 | #pragma mark - Ref checks 879 | 880 | /* 881 | 882 | Try to find CG/CF constructors with Ref at the end 883 | 884 | stability/reliability medium 885 | 886 | */ 887 | 888 | if ([RegexHelper testCaseSensitivePattern:@"[:upper:]{3}\\w*Ref\\W" inString:line]) 889 | { 890 | ++warnings; 891 | Log(@"%@:%zd: warning: Line %zd CFReference types need not end with Ref in Swift", path, count, count); 892 | } 893 | 894 | #pragma mark - MAX and MIN usage 895 | /* 896 | 897 | Any use of _MAX / MIN or some name like that is likely to be replaced in Swift by .max/.min 898 | 899 | */ 900 | 901 | if ([RegexHelper testString:@"_MAX" inString:line] || 902 | [RegexHelper testString:@"_MIN" inString:line]) 903 | { 904 | ++warnings; 905 | Log(@"%@:%zd: warning: Line %zd Prefer Swift .max and .min over old constants", path, count, count); 906 | } 907 | 908 | 909 | 910 | #pragma mark - Antiquated -> ( tests 911 | /* 912 | 913 | Check for -> ( patterns 914 | 915 | stability/reliability medium 916 | 917 | */ 918 | 919 | if ([RegexHelper testPattern:@"->\\s*\\([^,]*\\)" inString:line]) 920 | { 921 | ++warnings; 922 | Log(@"%@:%zd: warning: Line %zd parens after return token are not required except for tuples", path, count, count); 923 | } 924 | 925 | 926 | #pragma mark - Option Set checks 927 | /* 928 | 929 | Convert option sets with raw values to [] 930 | 931 | stability/reliability low-medium (will improve as I figure this out) 932 | 933 | */ 934 | if ([RegexHelper testString:@"Options" inString:line] && 935 | [RegexHelper testPattern:@"rawValue:\\s*0\\)" inString:line]) 936 | { 937 | ++warnings; 938 | Log(@"%@:%zd: warning: Line %zd Many zero options can now be replaced with [] instead", path, count, count); 939 | } 940 | else if ([RegexHelper testPattern:@"rawValue:" inString:line]) 941 | { 942 | ++warnings; 943 | Log(@"%@:%zd: warning: Line %zd Many rawValue initializers can now be replaced with option set initialization", path, count, count); 944 | } 945 | 946 | 947 | #pragma mark - Enumeration prefix checks 948 | 949 | /* 950 | 951 | Highlight enumeration prefixes for elimnation 952 | 953 | stability/reliability high 954 | note the list needs to be upgraded for iOS 9/OS X 10.11 955 | 956 | */ 957 | 958 | for (NSString *prefix in prefixes) 959 | { 960 | // Has enumeration prefix with dot after but no rawValue 961 | if ([line rangeOfString:[prefix stringByAppendingString:@"."]].location != NSNotFound && 962 | [line rangeOfString:@"rawValue"].location == NSNotFound) 963 | { 964 | ++warnings; 965 | Log(@"%@:%zd: warning: Line %zd: Swift type inference may not require enumeration prefix on this line", path, count, count); 966 | } 967 | } 968 | 969 | #pragma mark - Constructor checks 970 | 971 | /* 972 | 973 | Find constructors, which in context may use inferrable class types 974 | 975 | stability/reliability low - medium 976 | 977 | */ 978 | 979 | { 980 | // Matches most no-arg class methods as likely constructors 981 | if ([RegexHelper testCaseSensitivePattern:@"^\\s*[:upper:]\\w+\\.\\w+\\(\\)" inString:line]) 982 | { 983 | // first thing on line? skip. 984 | } 985 | else if ([RegexHelper testCaseSensitivePattern:@"let\\s*\\w+\\s*[=]\\s*[:upper:]\\w+\\.\\w+\\(\\)" inString:line] || 986 | [RegexHelper testCaseSensitivePattern:@"var\\s*\\w+\\s*[=]\\s*[:upper:]\\w+\\.\\w+\\(\\)" inString:line]) 987 | { 988 | // after let or var assignment, skip. There won't be any context for type inference 989 | } 990 | else if ([RegexHelper testCaseSensitivePattern:@"[=]\\s*[:upper:]\\w+\\.\\w+\\(\\)" inString:line]) 991 | { 992 | // after assignment, e.g. view.backgroundColor = UIColor.blueColor() 993 | ++cautions; 994 | Log(@"%@:%zd: note: Line %zd: CAUTION: Possible constructor assignment pattern may not require inferred prefix", path, count, count); 995 | Log(@"%@", line); 996 | } 997 | else if ([RegexHelper testCaseSensitivePattern:@"[ :,\{\(][:upper:]\\w+\\.\\w+\\(\\)" inString:line]) 998 | { 999 | ++cautions; 1000 | Log(@"%@:%zd: note: Line %zd: CAUTION: Possible constructor pattern may not require inferred prefix", path, count, count); 1001 | Log(@"%@", line); 1002 | } 1003 | } 1004 | 1005 | #pragma mark - Self. reference checks 1006 | 1007 | /* 1008 | 1009 | self references that may not be needed 1010 | stability/reliability medium - high 1011 | 1012 | */ 1013 | 1014 | if ([RegexHelper testString:@"self.init" inString:line]) 1015 | { 1016 | // Skip self.init pattern 1017 | } 1018 | else if ([RegexHelper testPattern:@"self\\.(\\w+)\\s*=\\s*\\1" inString:line]) 1019 | { 1020 | // Skip likely self-initialization self.x = x 1021 | } 1022 | else if ([RegexHelper testPattern:@"\\\\\\(self" inString:line]) 1023 | { 1024 | // Skip likely in-string reference \(self 1025 | } 1026 | else if ([RegexHelper testPattern:@"\\{.*self" inString:line]) 1027 | { 1028 | // Skip single line likely closure reference {....self...} 1029 | } 1030 | else if ([RegexHelper testString:@"self[" inString:line]) 1031 | { 1032 | // Skip array access 1033 | } 1034 | else if ([RegexHelper testPattern:@"self\\.(\\S)+\\s*=\\s*\\S" inString:line]) 1035 | { 1036 | ++warnings; 1037 | Log(@"%@:%zd: warning: Line %zd: Swift does not usually require 'self' references for assignments", path, count, count); 1038 | } 1039 | else if ([RegexHelper testString:@"self." inString:line]) 1040 | { 1041 | ++warnings; 1042 | Log(@"%@:%zd: warning: Line %zd: Swift does not usually require 'self' references outside of closures", path, count, count); 1043 | } 1044 | } 1045 | 1046 | #pragma mark - FILE ISSUES 1047 | 1048 | #pragma mark - File ending check 1049 | 1050 | /* 1051 | 1052 | Check for trailing or missing newlines 1053 | stability/reliability high 1054 | 1055 | */ 1056 | 1057 | // File-level line hygiene 1058 | if ([string hasSuffix:@"\n\n"] || ![string hasSuffix:@"\n"]) 1059 | { 1060 | ++warnings; 1061 | Log(@"%@:0: warning: File %@ should have a single trailing newline", path, path.lastPathComponent); 1062 | } 1063 | 1064 | Log(@"%zd warnings, %zd cautions for %@", warnings, cautions, path.lastPathComponent); 1065 | } 1066 | @end 1067 | 1068 | -------------------------------------------------------------------------------- /testlint/sources/NSArray+Frankenstein.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import 8 | 9 | typedef id (^MapBlock)(id object); 10 | typedef BOOL (^TestingBlock)(id object); 11 | typedef void (^DoBlock)(id object); 12 | 13 | #pragma mark - Utility 14 | @interface NSArray (Frankenstein) 15 | @property (nonatomic, readonly) id car; 16 | @property (nonatomic, readonly) NSArray *cdr; 17 | @property (nonatomic, readonly) NSArray *reversed; 18 | - (NSArray *) map: (MapBlock) aBlock; 19 | - (NSArray *) collect: (TestingBlock) aBlock; 20 | - (NSArray *) reject: (TestingBlock) aBlock; 21 | - (void) performBlock: (DoBlock) aBlock; 22 | @end 23 | -------------------------------------------------------------------------------- /testlint/sources/NSArray+Frankenstein.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import "NSArray+Frankenstein.h" 8 | 9 | @implementation NSArray (Frankenstein) 10 | 11 | - (NSArray *) reversed 12 | { 13 | return [[self reverseObjectEnumerator] allObjects]; 14 | } 15 | 16 | - (id) car 17 | { 18 | if (self.count == 0) return nil; 19 | return self[0]; 20 | } 21 | 22 | - (NSArray *) cdr 23 | { 24 | if (self.count < 2) return nil; 25 | return [self subarrayWithRange:NSMakeRange(1, self.count - 1)]; 26 | } 27 | 28 | - (NSArray *) map: (MapBlock) aBlock 29 | { 30 | if (!aBlock) return self; 31 | 32 | NSMutableArray *resultArray = [NSMutableArray array]; 33 | for (id object in self) 34 | { 35 | id result = aBlock(object); 36 | [resultArray addObject: result ? : [NSNull null]]; 37 | } 38 | return [resultArray copy]; 39 | } 40 | 41 | - (NSArray *) collect: (TestingBlock) aBlock 42 | { 43 | if (!aBlock) return self; 44 | 45 | NSMutableArray *resultArray = [NSMutableArray array]; 46 | for (id object in self) 47 | { 48 | BOOL result = aBlock(object); 49 | if (result) 50 | [resultArray addObject:object]; 51 | } 52 | return [resultArray copy]; 53 | } 54 | 55 | - (NSArray *) reject: (TestingBlock) aBlock 56 | { 57 | if (!aBlock) return self; 58 | 59 | NSMutableArray *resultArray = [NSMutableArray array]; 60 | for (id object in self) 61 | { 62 | BOOL result = aBlock(object); 63 | if (!result) 64 | [resultArray addObject:object]; 65 | } 66 | return [resultArray copy]; 67 | } 68 | 69 | - (void) performBlock: (DoBlock) aBlock 70 | { 71 | if (!aBlock) return; 72 | for (id object in self) 73 | { 74 | aBlock(object); 75 | } 76 | } 77 | @end 78 | -------------------------------------------------------------------------------- /testlint/sources/RegexHelper.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import 8 | 9 | /* 10 | . (dot) Any single character 11 | [abc] [^abc] A single character of: a, b, or c | a single character except a, b, or c 12 | [a-z] [a-zA-Z] Any single character in the range a-z | in the range a-z or A-Z 13 | ^ or \A Start of line/string 14 | $ or \z End of line/string 15 | \< or \> Start of word or end of word 16 | \c Control character 17 | \x \O Hex/Octal digit 18 | \s \S Any whitespace character / any non-whitespace character 19 | \d \D Any digit / any non-digit 20 | \w \W Any word character (letter, number, underscore) / any non-word character 21 | \b Any word boundary 22 | \Q \E Begin / end literal sequence 23 | (...) Capture everything enclosed 24 | (a|b) a or b 25 | a? a* a+ Zero or one of a | Zero or more of a | One or more of a 26 | a{3} a{3,} a{3,5} Exactly 3 of a | 3 or more of a | 3, 4, or 5 of a 27 | [:upper:] Upper case letters 28 | [:lower:] Lower case letters 29 | [:alpha:] All letters 30 | [:alnum:] Digits and letters 31 | [:digit:] Digits 32 | [:xdigit:] Hexade­cimal digits 33 | [:punct:] Punctu­ation 34 | [:blank:] Space and tab 35 | [:space:] Blank characters 36 | [:cntrl:] Control characters 37 | [:graph:] Printed characters 38 | [:print:] Printed characters and spaces 39 | [:word:] Digits, letters and underscore 40 | */ 41 | 42 | @interface RegexHelper : NSObject 43 | + (BOOL) testPattern: (NSString *) searchPattern inString: (NSString *) string; 44 | + (BOOL) testCaseSensitivePattern: (NSString *) searchPattern inString: (NSString *) string; 45 | + (BOOL) testString: (NSString *) test inString: (NSString *) string; 46 | @end 47 | 48 | -------------------------------------------------------------------------------- /testlint/sources/RegexHelper.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import "RegexHelper.h" 8 | 9 | @implementation RegexHelper 10 | + (BOOL) testCaseSensitivePattern: (NSString *) searchPattern inString: (NSString *) string 11 | { 12 | return [string rangeOfString:searchPattern options:NSRegularExpressionCaseInsensitive | NSRegularExpressionSearch].location != NSNotFound; 13 | } 14 | 15 | + (BOOL) testPattern: (NSString *) searchPattern inString: (NSString *) string 16 | { 17 | return [string rangeOfString:searchPattern options:NSRegularExpressionSearch].location != NSNotFound; 18 | } 19 | 20 | // Here for convenience. 21 | + (BOOL) testString: (NSString *) test inString: (NSString *) string 22 | { 23 | return [string rangeOfString:test].location != NSNotFound; 24 | } 25 | @end 26 | -------------------------------------------------------------------------------- /testlint/sources/Utility.h: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import 8 | 9 | // Update for iOS9 -- OS X and iOS 10 | #define prefixes @[@"ACAccountCredentialRenewResult", @"ADAdType", @"ADError", @"ADInterstitialPresentationPolicy", @"ALAssetOrientation", @"ALAuthorizationStatus", @"AMLogLevel", @"AVAssetExportSessionStatus", @"AVAssetImageGeneratorResult", @"AVAssetReaderStatus", @"AVAssetReferenceRestrictions", @"AVAssetWriterStatus", @"AVAudio3DMixingRenderingAlgorithm", @"AVAudioCommonFormat", @"AVAudioEnvironmentDistanceAttenuationModel", @"AVAudioPlayerNodeBufferOptions", @"AVAudioQuality", @"AVAudioSessionCategoryOptions", @"AVAudioSessionErrorCode", @"AVAudioSessionInterruptionOptions", @"AVAudioSessionInterruptionType", @"AVAudioSessionPortOverride", @"AVAudioSessionRecordPermission", @"AVAudioSessionRouteChangeReason", @"AVAudioSessionSetActiveOptions", @"AVAudioSessionSilenceSecondaryAudioHintType", @"AVAudioUnitDistortionPreset", @"AVAudioUnitEQFilterType", @"AVAudioUnitReverbPreset", @"AVAuthorizationStatus", @"AVB17221EntityPropertyChanged", @"AVCaptureAutoFocusRangeRestriction", @"AVCaptureAutoFocusSystem", @"AVCaptureDevicePosition", @"AVCaptureDeviceTransportControlsPlaybackMode", @"AVCaptureExposureMode", @"AVCaptureFlashMode", @"AVCaptureFocusMode", @"AVCaptureTorchMode", @"AVCaptureVideoOrientation", @"AVCaptureVideoStabilizationMode", @"AVCaptureViewControlsStyle", @"AVCaptureWhiteBalanceMode", @"AVContentAuthorizationStatus", @"AVError", @"AVKeyValueStatus", @"AVPlayerActionAtItemEnd", @"AVPlayerItemStatus", @"AVPlayerStatus", @"AVPlayerViewControlsStyle", @"AVPlayerViewTrimResult", @"AVQueuedSampleBufferRenderingStatus", @"AVSampleBufferRequestDirection", @"AVSampleBufferRequestMode", @"AVSpeechBoundary", @"AVVideoFieldMode", @"CBATTError", @"CBAttributePermissions", @"CBCentralManagerState", @"CBCharacteristicProperties", @"CBCharacteristicWriteType", @"CBError", @"CBPeripheralAuthorizationStatus", @"CBPeripheralManagerAuthorizationStatus", @"CBPeripheralManagerConnectionLatency", @"CBPeripheralManagerState", @"CBPeripheralState", @"CKAccountStatus", @"CKApplicationPermissionStatus", @"CKApplicationPermissions", @"CKErrorCode", @"CKNotificationType", @"CKQueryNotificationReason", @"CKRecordSavePolicy", @"CKRecordZoneCapabilities", @"CKReferenceAction", @"CKSubscriptionOptions", @"CKSubscriptionType", @"CLActivityType", @"CLError", @"CLProximity", @"CLRegionState", @"CMAttitudeReferenceFrame", @"CMMotionActivityConfidence", @"CWChannelBand", @"CWChannelWidth", @"CWCipherKeyFlags", @"CWErr", @"CWEventType", @"CWIBSSModeSecurity", @"CWInterfaceMode", @"CWKeychainDomain", @"CWPHYMode", @"CWSecurity", @"EABluetoothAccessoryPickerErrorCode", @"EAGLRenderingAPI", @"EAWiFiUnconfiguredAccessoryBrowserState", @"EAWiFiUnconfiguredAccessoryConfigurationStatus", @"EAWiFiUnconfiguredAccessoryProperties", @"EKAuthorizationStatus", @"FIMenuKind", @"GKChallengeState", @"GKErrorCode", @"GKGameCenterViewControllerState", @"GKInviteRecipientResponse", @"GKLeaderboardPlayerScope", @"GKLeaderboardTimeScope", @"GKMatchSendDataMode", @"GKMatchType", @"GKPlayerConnectionState", @"GKTurnBasedMatchOutcome", @"GKTurnBasedMatchStatus", @"GKTurnBasedParticipantStatus", @"GKVoiceChatPlayerState", @"HKAuthorizationStatus", @"HKBiologicalSex", @"HKBloodType", @"HKBodyTemperatureSensorLocation", @"HKCategoryValueSleepAnalysis", @"HKErrorCode", @"HKHeartRateSensorLocation", @"HKMetricPrefix", @"HKQuantityAggregationStyle", @"HKQueryOptions", @"HKStatisticsOptions", @"HKUpdateFrequency", @"HKWorkoutActivityType", @"HKWorkoutEventType", @"HMCharacteristicValueDoorState", @"HMCharacteristicValueHeatingCooling", @"HMCharacteristicValueLockMechanismLastKnownAction", @"HMCharacteristicValueLockMechanismState", @"HMCharacteristicValueRotationDirection", @"HMCharacteristicValueTemperatureUnit", @"HMErrorCode", @"IMGroupListPermissions", @"IMHandleAuthorizationStatus", @"IMHandleAvailability", @"IMSessionAvailability", @"InstallerSectionDirection", @"LAError", @"LAPolicy", @"MCEncryptionPreference", @"MCErrorCode", @"MCSessionSendDataMode", @"MCSessionState", @"MKAnnotationViewDragState", @"MKDirectionsTransportType", @"MKDistanceFormatterUnitStyle", @"MKDistanceFormatterUnits", @"MKErrorCode", @"MKMapType", @"MKOverlayLevel", @"MKPinAnnotationColor", @"MKUserTrackingMode", @"MLMediaSourceType", @"MLMediaType", @"MPMediaGrouping", @"MPMediaPlaylistAttribute", @"MPMediaPredicateComparison", @"MPMediaType", @"MPMovieControlStyle", @"MPMovieFinishReason", @"MPMovieLoadState", @"MPMovieMediaTypeMask", @"MPMoviePlaybackState", @"MPMovieRepeatMode", @"MPMovieScalingMode", @"MPMovieSourceType", @"MPMovieTimeOption", @"MPMusicPlaybackState", @"MPMusicRepeatMode", @"MPMusicShuffleMode", @"MPRemoteCommandHandlerStatus", @"MPSeekCommandEventType", @"MTLArgumentAccess", @"MTLArgumentType", @"MTLBlendFactor", @"MTLBlendOperation", @"MTLCPUCacheMode", @"MTLColorWriteMask", @"MTLCommandBufferError", @"MTLCommandBufferStatus", @"MTLCompareFunction", @"MTLCullMode", @"MTLDataType", @"MTLFeatureSet", @"MTLFunctionType", @"MTLIndexType", @"MTLLibraryError", @"MTLLoadAction", @"MTLPipelineOption", @"MTLPixelFormat", @"MTLPrimitiveType", @"MTLPurgeableState", @"MTLRenderPipelineError", @"MTLResourceOptions", @"MTLSamplerAddressMode", @"MTLSamplerMinMagFilter", @"MTLSamplerMipFilter", @"MTLStencilOperation", @"MTLStoreAction", @"MTLTextureType", @"MTLTriangleFillMode", @"MTLVertexFormat", @"MTLVertexStepFunction", @"MTLVisibilityResultMode", @"MTLWinding", @"NCUpdateResult", @"NEEvaluateConnectionRuleAction", @"NEOnDemandRuleAction", @"NEOnDemandRuleInterfaceType", @"NEVPNError", @"NEVPNIKEAuthenticationMethod", @"NEVPNIKEv2CertificateType", @"NEVPNIKEv2DeadPeerDetectionRate", @"NEVPNIKEv2DiffieHellmanGroup", @"NEVPNIKEv2EncryptionAlgorithm", @"NEVPNIKEv2IntegrityAlgorithm", @"NEVPNStatus", @"NKIssueContentStatus", @"NSAccessibilityOrientation", @"NSAccessibilityPriorityLevel", @"NSAccessibilityRulerMarkerType", @"NSAccessibilitySortDirection", @"NSAccessibilityUnits", @"NSAlertStyle", @"NSAnimationBlockingMode", @"NSAnimationCurve", @"NSAnimationEffect", @"NSApplicationActivationOptions", @"NSApplicationActivationPolicy", @"NSApplicationDelegateReply", @"NSApplicationOcclusionState", @"NSApplicationPresentationOptions", @"NSApplicationPrintReply", @"NSApplicationTerminateReply", @"NSAttributeType", @"NSAttributedStringEnumerationOptions", @"NSAutoresizingMaskOptions", @"NSBackgroundActivityResult", @"NSBackgroundStyle", @"NSBackingStoreType", @"NSBatchUpdateRequestResultType", @"NSBezelStyle", @"NSBezierPathElement", @"NSBinarySearchingOptions", @"NSBitmapFormat", @"NSBitmapImageFileType", @"NSBorderType", @"NSBoxType", @"NSBrowserColumnResizingType", @"NSBrowserDropOperation", @"NSButtonType", @"NSByteCountFormatterCountStyle", @"NSByteCountFormatterUnits", @"NSCalculationError", @"NSCalendarOptions", @"NSCalendarUnit", @"NSCellAttribute", @"NSCellHitResult", @"NSCellImagePosition", @"NSCellStyleMask", @"NSCellType", @"NSCharacterCollection", @"NSCollectionViewDropOperation", @"NSColorPanelMode", @"NSColorPanelOptions", @"NSColorRenderingIntent", @"NSColorSpaceModel", @"NSComparisonPredicateModifier", @"NSComparisonPredicateOptions", @"NSComparisonResult", @"NSCompositingOperation", @"NSCompoundPredicateType", @"NSControlCharacterAction", @"NSControlSize", @"NSControlTint", @"NSCorrectionIndicatorType", @"NSCorrectionResponse", @"NSDataBase64DecodingOptions", @"NSDataBase64EncodingOptions", @"NSDataReadingOptions", @"NSDataSearchOptions", @"NSDataWritingOptions", @"NSDateComponentsFormatterUnitsStyle", @"NSDateComponentsFormatterZeroFormattingBehavior", @"NSDateFormatterBehavior", @"NSDateFormatterStyle", @"NSDateIntervalFormatterStyle", @"NSDatePickerElementFlags", @"NSDatePickerMode", @"NSDatePickerStyle", @"NSDeleteRule", @"NSDirectoryEnumerationOptions", @"NSDocumentChangeType", @"NSDragOperation", @"NSDraggingContext", @"NSDraggingFormation", @"NSDraggingItemEnumerationOptions", @"NSDrawerState", @"NSEnergyFormatterUnit", @"NSEntityMappingType", @"NSEnumerationOptions", @"NSEventButtonMask", @"NSEventGestureAxis", @"NSEventModifierFlags", @"NSEventPhase", @"NSEventSwipeTrackingOptions", @"NSEventType", @"NSExpressionType", @"NSFetchRequestResultType", @"NSFetchedResultsChangeType", @"NSFileCoordinatorReadingOptions", @"NSFileCoordinatorWritingOptions", @"NSFileManagerItemReplacementOptions", @"NSFileVersionAddingOptions", @"NSFileVersionReplacingOptions", @"NSFileWrapperReadingOptions", @"NSFileWrapperWritingOptions", @"NSFindPanelAction", @"NSFindPanelSubstringMatchType", @"NSFocusRingPlacement", @"NSFocusRingType", @"NSFontAction", @"NSFontCollectionOptions", @"NSFontCollectionVisibility", @"NSFontRenderingMode", @"NSFontTraitMask", @"NSFormattingContext", @"NSFormattingUnitStyle", @"NSGestureRecognizerState", @"NSGlyphInscription", @"NSGlyphProperty", @"NSGradientType", @"NSHTTPCookieAcceptPolicy", @"NSImageAlignment", @"NSImageCacheMode", @"NSImageFrameStyle", @"NSImageInterpolation", @"NSImageLoadStatus", @"NSImageRepLoadStatus", @"NSImageResizingMode", @"NSImageScaling", @"NSInsertionPosition", @"NSItemProviderErrorCode", @"NSJSONReadingOptions", @"NSJSONWritingOptions", @"NSKeyValueChange", @"NSKeyValueObservingOptions", @"NSKeyValueSetMutationKind", @"NSLayoutAttribute", @"NSLayoutConstraintOrientation", @"NSLayoutFormatOptions", @"NSLayoutRelation", @"NSLengthFormatterUnit", @"NSLevelIndicatorStyle", @"NSLineBreakMode", @"NSLineCapStyle", @"NSLineJoinStyle", @"NSLineMovementDirection", @"NSLineSweepDirection", @"NSLinguisticTaggerOptions", @"NSLocaleLanguageDirection", @"NSManagedObjectContextConcurrencyType", @"NSMassFormatterUnit", @"NSMatchingFlags", @"NSMatchingOptions", @"NSMatrixMode", @"NSMediaLibrary", @"NSMenuProperties", @"NSMergePolicyType", @"NSMultibyteGlyphPacking", @"NSNetServiceOptions", @"NSNetServicesError", @"NSNotificationCoalescing", @"NSNotificationSuspensionBehavior", @"NSNumberFormatterBehavior", @"NSNumberFormatterPadPosition", @"NSNumberFormatterRoundingMode", @"NSNumberFormatterStyle", @"NSOpenGLContextParameter", @"NSOperationQueuePriority", @"NSPDFPanelOptions", @"NSPageControllerTransitionStyle", @"NSPaperOrientation", @"NSPasteboardReadingOptions", @"NSPasteboardWritingOptions", @"NSPathStyle", @"NSPersistentStoreRequestType", @"NSPersistentStoreUbiquitousTransitionType", @"NSPointingDeviceType", @"NSPopUpArrowPosition", @"NSPopoverAppearance", @"NSPopoverBehavior", @"NSPostingStyle", @"NSPredicateOperatorType", @"NSPrintPanelOptions", @"NSPrintRenderingQuality", @"NSPrinterTableStatus", @"NSPrintingOrientation", @"NSPrintingPageOrder", @"NSPrintingPaginationMode", @"NSProcessInfoThermalState", @"NSProgressIndicatorStyle", @"NSProgressIndicatorThickness", @"NSPropertyListFormat", @"NSPropertyListMutabilityOptions", @"NSQualityOfService", @"NSRegularExpressionOptions", @"NSRelativePosition", @"NSRemoteNotificationType", @"NSRequestUserAttentionType", @"NSRoundingMode", @"NSRuleEditorNestingMode", @"NSRuleEditorRowType", @"NSRulerOrientation", @"NSSaveOperationType", @"NSSaveOptions", @"NSScrollArrowPosition", @"NSScrollElasticity", @"NSScrollViewFindBarPosition", @"NSScrollerArrow", @"NSScrollerKnobStyle", @"NSScrollerPart", @"NSScrollerStyle", @"NSSearchPathDirectory", @"NSSearchPathDomainMask", @"NSSegmentStyle", @"NSSegmentSwitchTracking", @"NSSelectionAffinity", @"NSSelectionDirection", @"NSSelectionGranularity", @"NSSharingContentScope", @"NSSliderType", @"NSSnapshotEventType", @"NSSortOptions", @"NSSpeechBoundary", @"NSSplitViewDividerStyle", @"NSStackViewGravity", @"NSStreamEvent", @"NSStreamStatus", @"NSStringCompareOptions", @"NSStringDrawingOptions", @"NSStringEncodingConversionOptions", @"NSStringEnumerationOptions", @"NSTIFFCompression", @"NSTabState", @"NSTabViewControllerTabStyle", @"NSTabViewType", @"NSTableColumnResizingOptions", @"NSTableViewAnimationOptions", @"NSTableViewColumnAutoresizingStyle", @"NSTableViewDraggingDestinationFeedbackStyle", @"NSTableViewDropOperation", @"NSTableViewGridLineStyle", @"NSTableViewRowSizeStyle", @"NSTableViewSelectionHighlightStyle", @"NSTaskTerminationReason", @"NSTestComparisonOperation", @"NSTextAlignment", @"NSTextBlockDimension", @"NSTextBlockLayer", @"NSTextBlockValueType", @"NSTextBlockVerticalAlignment", @"NSTextFieldBezelStyle", @"NSTextFinderAction", @"NSTextFinderMatchingType", @"NSTextLayoutOrientation", @"NSTextListOptions", @"NSTextStorageEditActions", @"NSTextStorageEditedOptions", @"NSTextTabType", @"NSTextTableLayoutAlgorithm", @"NSTextWritingDirection", @"NSTickMarkPosition", @"NSTimeZoneNameStyle", @"NSTitlePosition", @"NSTokenStyle", @"NSToolbarDisplayMode", @"NSToolbarSizeMode", @"NSTouchPhase", @"NSTrackingAreaOptions", @"NSTypesetterBehavior", @"NSTypesetterControlCharacterAction", @"NSURLBookmarkCreationOptions", @"NSURLBookmarkResolutionOptions", @"NSURLCacheStoragePolicy", @"NSURLCredentialPersistence", @"NSURLHandleStatus", @"NSURLRelationship", @"NSURLRequestCachePolicy", @"NSURLRequestNetworkServiceType", @"NSURLSessionAuthChallengeDisposition", @"NSURLSessionResponseDisposition", @"NSURLSessionTaskState", @"NSUnderlineStyle", @"NSUsableScrollerParts", @"NSUserInterfaceLayoutDirection", @"NSUserInterfaceLayoutOrientation", @"NSUserNotificationActivationType", @"NSViewControllerTransitionOptions", @"NSViewLayerContentsPlacement", @"NSViewLayerContentsRedrawPolicy", @"NSVisualEffectBlendingMode", @"NSVisualEffectMaterial", @"NSVisualEffectState", @"NSVolumeEnumerationOptions", @"NSWhoseSubelementIdentifier", @"NSWindingRule", @"NSWindowAnimationBehavior", @"NSWindowBackingLocation", @"NSWindowButton", @"NSWindowCollectionBehavior", @"NSWindowOcclusionState", @"NSWindowOrderingMode", @"NSWindowSharingType", @"NSWindowTitleVisibility", @"NSWorkspaceIconCreationOptions", @"NSWorkspaceLaunchOptions", @"NSWritingDirection", @"NSXMLDTDNodeKind", @"NSXMLDocumentContentKind", @"NSXMLNodeKind", @"NSXMLParserError", @"NSXMLParserExternalEntityResolvingPolicy", @"NSXPCConnectionOptions", @"OSALanguageFeatures", @"OSAScriptState", @"OSAStorageOptions", @"PHAssetBurstSelectionType", @"PHAssetCollectionSubtype", @"PHAssetCollectionType", @"PHAssetEditOperation", @"PHAssetMediaSubtype", @"PHAssetMediaType", @"PHAuthorizationStatus", @"PHCollectionEditOperation", @"PHCollectionListSubtype", @"PHCollectionListType", @"PHImageContentMode", @"PHImageRequestOptionsDeliveryMode", @"PHImageRequestOptionsResizeMode", @"PHImageRequestOptionsVersion", @"PHVideoRequestOptionsDeliveryMode", @"PHVideoRequestOptionsVersion", @"PKAddressField", @"PKMerchantCapability", @"PKPassKitErrorCode", @"PKPassLibraryAddPassesStatus", @"PKPassType", @"PKPaymentAuthorizationStatus", @"PKPaymentButtonStyle", @"PKPaymentButtonType", @"PKPaymentPassActivationState", @"PKShippingType", @"QTMovieModernizerStatus", @"SCNActionTimingMode", @"SCNAntialiasingMode", @"SCNChamferMode", @"SCNCullMode", @"SCNFilterMode", @"SCNGeometryPrimitiveType", @"SCNMorpherCalculationMode", @"SCNParticleBirthDirection", @"SCNParticleBirthLocation", @"SCNParticleBlendMode", @"SCNParticleEvent", @"SCNParticleImageSequenceAnimationMode", @"SCNParticleInputMode", @"SCNParticleModifierStage", @"SCNParticleOrientationMode", @"SCNParticleSortingMode", @"SCNPhysicsBodyType", @"SCNPhysicsCollisionCategory", @"SCNPhysicsFieldScope", @"SCNSceneSourceStatus", @"SCNShadowMode", @"SCNTransparencyMode", @"SCNWrapMode", @"SKActionTimingMode", @"SKBlendMode", @"SKDownloadState", @"SKInterpolationMode", @"SKLabelHorizontalAlignmentMode", @"SKLabelVerticalAlignmentMode", @"SKPaymentTransactionState", @"SKRepeatMode", @"SKSceneScaleMode", @"SKTextureFilteringMode", @"SKTransitionDirection", @"SKUniformType", @"SLComposeViewControllerResult", @"SLRequestMethod", @"SSReadingListErrorCode", @"TKErrorCode", @"TKSmartCardProtocol", @"TKSmartCardSlotState", @"UIAccessibilityNavigationStyle", @"UIAccessibilityScrollDirection", @"UIAccessibilityZoomType", @"UIActionSheetStyle", @"UIActivityCategory", @"UIActivityIndicatorViewStyle", @"UIAlertActionStyle", @"UIAlertControllerStyle", @"UIAlertViewStyle", @"UIApplicationState", @"UIAttachmentBehaviorType", @"UIBackgroundFetchResult", @"UIBackgroundRefreshStatus", @"UIBarButtonItemStyle", @"UIBarButtonSystemItem", @"UIBarMetrics", @"UIBarPosition", @"UIBarStyle", @"UIBaselineAdjustment", @"UIBlurEffectStyle", @"UIButtonType", @"UICollectionElementCategory", @"UICollectionUpdateAction", @"UICollectionViewScrollDirection", @"UICollectionViewScrollPosition", @"UICollisionBehaviorMode", @"UIControlContentHorizontalAlignment", @"UIControlContentVerticalAlignment", @"UIControlEvents", @"UIControlState", @"UIDataDetectorTypes", @"UIDatePickerMode", @"UIDeviceBatteryState", @"UIDeviceOrientation", @"UIDocumentChangeKind", @"UIDocumentMenuOrder", @"UIDocumentPickerMode", @"UIDocumentSaveOperation", @"UIDocumentState", @"UIEventSubtype", @"UIEventType", @"UIGestureRecognizerState", @"UIGuidedAccessRestrictionState", @"UIImageOrientation", @"UIImagePickerControllerCameraCaptureMode", @"UIImagePickerControllerCameraDevice", @"UIImagePickerControllerCameraFlashMode", @"UIImagePickerControllerQualityType", @"UIImagePickerControllerSourceType", @"UIImageRenderingMode", @"UIImageResizingMode", @"UIInputViewStyle", @"UIInterfaceOrientation", @"UIInterfaceOrientationMask", @"UIInterpolatingMotionEffectType", @"UIKeyModifierFlags", @"UIKeyboardAppearance", @"UIKeyboardType", @"UILayoutConstraintAxis", @"UILineBreakMode", @"UIMenuControllerArrowDirection", @"UIModalPresentationStyle", @"UIModalTransitionStyle", @"UINavigationControllerOperation", @"UIPageViewControllerNavigationDirection", @"UIPageViewControllerNavigationOrientation", @"UIPageViewControllerSpineLocation", @"UIPageViewControllerTransitionStyle", @"UIPopoverArrowDirection", @"UIPrintInfoDuplex", @"UIPrintInfoOrientation", @"UIPrintInfoOutputType", @"UIPrinterJobTypes", @"UIProgressViewStyle", @"UIPushBehaviorMode", @"UIRectCorner", @"UIRectEdge", @"UIRemoteNotificationType", @"UIReturnKeyType", @"UIScreenOverscanCompensation", @"UIScrollViewIndicatorStyle", @"UIScrollViewKeyboardDismissMode", @"UISearchBarIcon", @"UISearchBarStyle", @"UISegmentedControlSegment", @"UISegmentedControlStyle", @"UISplitViewControllerDisplayMode", @"UIStatusBarAnimation", @"UIStatusBarStyle", @"UISwipeGestureRecognizerDirection", @"UISystemAnimation", @"UITabBarItemPositioning", @"UITabBarSystemItem", @"UITableViewCellAccessoryType", @"UITableViewCellEditingStyle", @"UITableViewCellSelectionStyle", @"UITableViewCellSeparatorStyle", @"UITableViewCellStateMask", @"UITableViewCellStyle", @"UITableViewRowActionStyle", @"UITableViewRowAnimation", @"UITableViewScrollPosition", @"UITableViewStyle", @"UITextAlignment", @"UITextAutocapitalizationType", @"UITextAutocorrectionType", @"UITextBorderStyle", @"UITextFieldViewMode", @"UITextGranularity", @"UITextLayoutDirection", @"UITextSpellCheckingType", @"UITextStorageDirection", @"UITextWritingDirection", @"UITouchPhase", @"UIUserInterfaceIdiom", @"UIUserInterfaceLayoutDirection", @"UIUserInterfaceSizeClass", @"UIUserNotificationActionContext", @"UIUserNotificationActivationMode", @"UIUserNotificationType", @"UIViewAnimationCurve", @"UIViewAnimationOptions", @"UIViewAnimationTransition", @"UIViewAutoresizing", @"UIViewContentMode", @"UIViewKeyframeAnimationOptions", @"UIViewTintAdjustmentMode", @"UIWebPaginationBreakingMode", @"UIWebPaginationMode", @"UIWebViewNavigationType", @"WKErrorCode", @"WKInterfaceMapPinColor", @"WKMenuItemIcon", @"WKNavigationActionPolicy", @"WKNavigationResponsePolicy", @"WKNavigationType", @"WKSelectionGranularity", @"WKTextInputMode", @"WKUserNotificationInterfaceType", @"WKUserScriptInjectionTime", @"WatchKitErrorCode", @"WebCacheModel", @"WebDragDestinationAction", @"WebDragSourceAction", @"WebNavigationType", @"WebViewInsertAction"] 11 | 12 | void Log(NSString *formatString,...); 13 | -------------------------------------------------------------------------------- /testlint/sources/Utility.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import "Utility.h" 8 | 9 | void Log(NSString *formatString,...) 10 | { 11 | va_list arglist; 12 | if (formatString) 13 | { 14 | va_start(arglist, formatString); 15 | NSString *outstring = [[NSString alloc] initWithFormat:formatString arguments:arglist]; 16 | fprintf(stderr, "%s\n", [outstring UTF8String]); 17 | va_end(arglist); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8E0A29ED1B10F24800E625ED /* Linter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E0A29EC1B10F24800E625ED /* Linter.m */; }; 11 | 8ECAFF131B0A76FD00A97C1C /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ECAFF121B0A76FD00A97C1C /* main.m */; }; 12 | 8ECAFF201B0A786000A97C1C /* NSArray+Frankenstein.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ECAFF1D1B0A785300A97C1C /* NSArray+Frankenstein.m */; }; 13 | 8ECAFF251B0A7FA300A97C1C /* Utility.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ECAFF241B0A7FA300A97C1C /* Utility.m */; }; 14 | 8ECAFF2C1B0A895900A97C1C /* RegexHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ECAFF2B1B0A895900A97C1C /* RegexHelper.m */; }; 15 | 9CE9D6381B1230BB0081EE78 /* RegexHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ECAFF2B1B0A895900A97C1C /* RegexHelper.m */; }; 16 | 9CE9D6391B1230BB0081EE78 /* Utility.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ECAFF241B0A7FA300A97C1C /* Utility.m */; }; 17 | 9CE9D63A1B1230BB0081EE78 /* NSArray+Frankenstein.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ECAFF1D1B0A785300A97C1C /* NSArray+Frankenstein.m */; }; 18 | 9CE9D63B1B1230BB0081EE78 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ECAFF121B0A76FD00A97C1C /* main.m */; }; 19 | 9CE9D63C1B1230BB0081EE78 /* Linter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E0A29EC1B10F24800E625ED /* Linter.m */; }; 20 | 9CE9D63F1B1230BB0081EE78 /* testlint in Copy to personal bin folder */ = {isa = PBXBuildFile; fileRef = 8ECAFF0F1B0A76FD00A97C1C /* testlint */; }; 21 | 9CE9D6411B1230BB0081EE78 /* testlint in Copy to personal drop box */ = {isa = PBXBuildFile; fileRef = 8ECAFF0F1B0A76FD00A97C1C /* testlint */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXCopyFilesBuildPhase section */ 25 | 9CE9D63E1B1230BB0081EE78 /* Copy to personal bin folder */ = { 26 | isa = PBXCopyFilesBuildPhase; 27 | buildActionMask = 2147483647; 28 | dstPath = /Users/ericasadun/bin; 29 | dstSubfolderSpec = 0; 30 | files = ( 31 | 9CE9D63F1B1230BB0081EE78 /* testlint in Copy to personal bin folder */, 32 | ); 33 | name = "Copy to personal bin folder"; 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | 9CE9D6401B1230BB0081EE78 /* Copy to personal drop box */ = { 37 | isa = PBXCopyFilesBuildPhase; 38 | buildActionMask = 2147483647; 39 | dstPath = "/Users/ericasadun/Public/Drop Box"; 40 | dstSubfolderSpec = 0; 41 | files = ( 42 | 9CE9D6411B1230BB0081EE78 /* testlint in Copy to personal drop box */, 43 | ); 44 | name = "Copy to personal drop box"; 45 | runOnlyForDeploymentPostprocessing = 0; 46 | }; 47 | /* End PBXCopyFilesBuildPhase section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 8E0A29EB1B10F24800E625ED /* Linter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Linter.h; sourceTree = ""; }; 51 | 8E0A29EC1B10F24800E625ED /* Linter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Linter.m; sourceTree = ""; }; 52 | 8ECAFF0F1B0A76FD00A97C1C /* testlint */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = testlint; sourceTree = BUILT_PRODUCTS_DIR; }; 53 | 8ECAFF121B0A76FD00A97C1C /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ../testlint/main.m; sourceTree = ""; }; 54 | 8ECAFF1C1B0A785300A97C1C /* NSArray+Frankenstein.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSArray+Frankenstein.h"; sourceTree = ""; }; 55 | 8ECAFF1D1B0A785300A97C1C /* NSArray+Frankenstein.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Frankenstein.m"; sourceTree = ""; }; 56 | 8ECAFF231B0A7FA300A97C1C /* Utility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Utility.h; sourceTree = ""; }; 57 | 8ECAFF241B0A7FA300A97C1C /* Utility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Utility.m; sourceTree = ""; }; 58 | 8ECAFF2A1B0A895900A97C1C /* RegexHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RegexHelper.h; sourceTree = ""; }; 59 | 8ECAFF2B1B0A895900A97C1C /* RegexHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegexHelper.m; sourceTree = ""; }; 60 | 9CE9D6451B1230BB0081EE78 /* testlint-personal */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "testlint-personal"; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXFrameworksBuildPhase section */ 64 | 8ECAFF0C1B0A76FD00A97C1C /* Frameworks */ = { 65 | isa = PBXFrameworksBuildPhase; 66 | buildActionMask = 2147483647; 67 | files = ( 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | 9CE9D63D1B1230BB0081EE78 /* Frameworks */ = { 72 | isa = PBXFrameworksBuildPhase; 73 | buildActionMask = 2147483647; 74 | files = ( 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | /* End PBXFrameworksBuildPhase section */ 79 | 80 | /* Begin PBXGroup section */ 81 | 8ECAFF061B0A76FD00A97C1C = { 82 | isa = PBXGroup; 83 | children = ( 84 | 8ECAFF1B1B0A785300A97C1C /* Source */, 85 | 8ECAFF101B0A76FD00A97C1C /* Products */, 86 | ); 87 | sourceTree = ""; 88 | }; 89 | 8ECAFF101B0A76FD00A97C1C /* Products */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 8ECAFF0F1B0A76FD00A97C1C /* testlint */, 93 | 9CE9D6451B1230BB0081EE78 /* testlint-personal */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 8ECAFF1B1B0A785300A97C1C /* Source */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 8ECAFF121B0A76FD00A97C1C /* main.m */, 102 | 8E0A29EB1B10F24800E625ED /* Linter.h */, 103 | 8E0A29EC1B10F24800E625ED /* Linter.m */, 104 | 8ECAFF231B0A7FA300A97C1C /* Utility.h */, 105 | 8ECAFF241B0A7FA300A97C1C /* Utility.m */, 106 | 8ECAFF1C1B0A785300A97C1C /* NSArray+Frankenstein.h */, 107 | 8ECAFF1D1B0A785300A97C1C /* NSArray+Frankenstein.m */, 108 | 8ECAFF2A1B0A895900A97C1C /* RegexHelper.h */, 109 | 8ECAFF2B1B0A895900A97C1C /* RegexHelper.m */, 110 | ); 111 | name = Source; 112 | path = sources; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXNativeTarget section */ 118 | 8ECAFF0E1B0A76FD00A97C1C /* testlint */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = 8ECAFF161B0A76FD00A97C1C /* Build configuration list for PBXNativeTarget "testlint" */; 121 | buildPhases = ( 122 | 8ECAFF0B1B0A76FD00A97C1C /* Sources */, 123 | 8ECAFF0C1B0A76FD00A97C1C /* Frameworks */, 124 | ); 125 | buildRules = ( 126 | ); 127 | dependencies = ( 128 | ); 129 | name = testlint; 130 | productName = testlint; 131 | productReference = 8ECAFF0F1B0A76FD00A97C1C /* testlint */; 132 | productType = "com.apple.product-type.tool"; 133 | }; 134 | 9CE9D6361B1230BB0081EE78 /* testlint - erica's personal */ = { 135 | isa = PBXNativeTarget; 136 | buildConfigurationList = 9CE9D6421B1230BB0081EE78 /* Build configuration list for PBXNativeTarget "testlint - erica's personal" */; 137 | buildPhases = ( 138 | 9CE9D6371B1230BB0081EE78 /* Sources */, 139 | 9CE9D63D1B1230BB0081EE78 /* Frameworks */, 140 | 9CE9D63E1B1230BB0081EE78 /* Copy to personal bin folder */, 141 | 9CE9D6401B1230BB0081EE78 /* Copy to personal drop box */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = "testlint - erica's personal"; 148 | productName = testlint; 149 | productReference = 9CE9D6451B1230BB0081EE78 /* testlint-personal */; 150 | productType = "com.apple.product-type.tool"; 151 | }; 152 | /* End PBXNativeTarget section */ 153 | 154 | /* Begin PBXProject section */ 155 | 8ECAFF071B0A76FD00A97C1C /* Project object */ = { 156 | isa = PBXProject; 157 | attributes = { 158 | LastUpgradeCheck = 0700; 159 | ORGANIZATIONNAME = "Erica Sadun"; 160 | TargetAttributes = { 161 | 8ECAFF0E1B0A76FD00A97C1C = { 162 | CreatedOnToolsVersion = 6.3.1; 163 | }; 164 | }; 165 | }; 166 | buildConfigurationList = 8ECAFF0A1B0A76FD00A97C1C /* Build configuration list for PBXProject "testlint" */; 167 | compatibilityVersion = "Xcode 3.2"; 168 | developmentRegion = English; 169 | hasScannedForEncodings = 0; 170 | knownRegions = ( 171 | en, 172 | ); 173 | mainGroup = 8ECAFF061B0A76FD00A97C1C; 174 | productRefGroup = 8ECAFF101B0A76FD00A97C1C /* Products */; 175 | projectDirPath = ""; 176 | projectRoot = ""; 177 | targets = ( 178 | 8ECAFF0E1B0A76FD00A97C1C /* testlint */, 179 | 9CE9D6361B1230BB0081EE78 /* testlint - erica's personal */, 180 | ); 181 | }; 182 | /* End PBXProject section */ 183 | 184 | /* Begin PBXSourcesBuildPhase section */ 185 | 8ECAFF0B1B0A76FD00A97C1C /* Sources */ = { 186 | isa = PBXSourcesBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | 8ECAFF2C1B0A895900A97C1C /* RegexHelper.m in Sources */, 190 | 8ECAFF251B0A7FA300A97C1C /* Utility.m in Sources */, 191 | 8ECAFF201B0A786000A97C1C /* NSArray+Frankenstein.m in Sources */, 192 | 8ECAFF131B0A76FD00A97C1C /* main.m in Sources */, 193 | 8E0A29ED1B10F24800E625ED /* Linter.m in Sources */, 194 | ); 195 | runOnlyForDeploymentPostprocessing = 0; 196 | }; 197 | 9CE9D6371B1230BB0081EE78 /* Sources */ = { 198 | isa = PBXSourcesBuildPhase; 199 | buildActionMask = 2147483647; 200 | files = ( 201 | 9CE9D6381B1230BB0081EE78 /* RegexHelper.m in Sources */, 202 | 9CE9D6391B1230BB0081EE78 /* Utility.m in Sources */, 203 | 9CE9D63A1B1230BB0081EE78 /* NSArray+Frankenstein.m in Sources */, 204 | 9CE9D63B1B1230BB0081EE78 /* main.m in Sources */, 205 | 9CE9D63C1B1230BB0081EE78 /* Linter.m in Sources */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXSourcesBuildPhase section */ 210 | 211 | /* Begin XCBuildConfiguration section */ 212 | 8ECAFF141B0A76FD00A97C1C /* Debug */ = { 213 | isa = XCBuildConfiguration; 214 | buildSettings = { 215 | ALWAYS_SEARCH_USER_PATHS = NO; 216 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 217 | CLANG_CXX_LIBRARY = "libc++"; 218 | CLANG_ENABLE_MODULES = YES; 219 | CLANG_ENABLE_OBJC_ARC = YES; 220 | CLANG_WARN_BOOL_CONVERSION = YES; 221 | CLANG_WARN_CONSTANT_CONVERSION = YES; 222 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 223 | CLANG_WARN_EMPTY_BODY = YES; 224 | CLANG_WARN_ENUM_CONVERSION = YES; 225 | CLANG_WARN_INT_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_UNREACHABLE_CODE = YES; 228 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 229 | COPY_PHASE_STRIP = NO; 230 | DEBUG_INFORMATION_FORMAT = dwarf; 231 | ENABLE_STRICT_OBJC_MSGSEND = YES; 232 | ENABLE_TESTABILITY = YES; 233 | GCC_C_LANGUAGE_STANDARD = gnu99; 234 | GCC_DYNAMIC_NO_PIC = NO; 235 | GCC_NO_COMMON_BLOCKS = YES; 236 | GCC_OPTIMIZATION_LEVEL = 0; 237 | GCC_PREPROCESSOR_DEFINITIONS = ( 238 | "DEBUG=1", 239 | "$(inherited)", 240 | ); 241 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 242 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 243 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 244 | GCC_WARN_UNDECLARED_SELECTOR = YES; 245 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 246 | GCC_WARN_UNUSED_FUNCTION = YES; 247 | GCC_WARN_UNUSED_VARIABLE = YES; 248 | MACOSX_DEPLOYMENT_TARGET = 10.10; 249 | MTL_ENABLE_DEBUG_INFO = YES; 250 | ONLY_ACTIVE_ARCH = YES; 251 | SDKROOT = macosx; 252 | }; 253 | name = Debug; 254 | }; 255 | 8ECAFF151B0A76FD00A97C1C /* Release */ = { 256 | isa = XCBuildConfiguration; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 260 | CLANG_CXX_LIBRARY = "libc++"; 261 | CLANG_ENABLE_MODULES = YES; 262 | CLANG_ENABLE_OBJC_ARC = YES; 263 | CLANG_WARN_BOOL_CONVERSION = YES; 264 | CLANG_WARN_CONSTANT_CONVERSION = YES; 265 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 266 | CLANG_WARN_EMPTY_BODY = YES; 267 | CLANG_WARN_ENUM_CONVERSION = YES; 268 | CLANG_WARN_INT_CONVERSION = YES; 269 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 270 | CLANG_WARN_UNREACHABLE_CODE = YES; 271 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 272 | COPY_PHASE_STRIP = NO; 273 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 274 | ENABLE_NS_ASSERTIONS = NO; 275 | ENABLE_STRICT_OBJC_MSGSEND = YES; 276 | GCC_C_LANGUAGE_STANDARD = gnu99; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 279 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 280 | GCC_WARN_UNDECLARED_SELECTOR = YES; 281 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 282 | GCC_WARN_UNUSED_FUNCTION = YES; 283 | GCC_WARN_UNUSED_VARIABLE = YES; 284 | MACOSX_DEPLOYMENT_TARGET = 10.10; 285 | MTL_ENABLE_DEBUG_INFO = NO; 286 | SDKROOT = macosx; 287 | }; 288 | name = Release; 289 | }; 290 | 8ECAFF171B0A76FD00A97C1C /* Debug */ = { 291 | isa = XCBuildConfiguration; 292 | buildSettings = { 293 | PRODUCT_NAME = "$(TARGET_NAME)"; 294 | }; 295 | name = Debug; 296 | }; 297 | 8ECAFF181B0A76FD00A97C1C /* Release */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | PRODUCT_NAME = "$(TARGET_NAME)"; 301 | }; 302 | name = Release; 303 | }; 304 | 9CE9D6431B1230BB0081EE78 /* Debug */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | PRODUCT_NAME = "testlint-personal"; 308 | }; 309 | name = Debug; 310 | }; 311 | 9CE9D6441B1230BB0081EE78 /* Release */ = { 312 | isa = XCBuildConfiguration; 313 | buildSettings = { 314 | PRODUCT_NAME = "testlint-personal"; 315 | }; 316 | name = Release; 317 | }; 318 | /* End XCBuildConfiguration section */ 319 | 320 | /* Begin XCConfigurationList section */ 321 | 8ECAFF0A1B0A76FD00A97C1C /* Build configuration list for PBXProject "testlint" */ = { 322 | isa = XCConfigurationList; 323 | buildConfigurations = ( 324 | 8ECAFF141B0A76FD00A97C1C /* Debug */, 325 | 8ECAFF151B0A76FD00A97C1C /* Release */, 326 | ); 327 | defaultConfigurationIsVisible = 0; 328 | defaultConfigurationName = Release; 329 | }; 330 | 8ECAFF161B0A76FD00A97C1C /* Build configuration list for PBXNativeTarget "testlint" */ = { 331 | isa = XCConfigurationList; 332 | buildConfigurations = ( 333 | 8ECAFF171B0A76FD00A97C1C /* Debug */, 334 | 8ECAFF181B0A76FD00A97C1C /* Release */, 335 | ); 336 | defaultConfigurationIsVisible = 0; 337 | defaultConfigurationName = Release; 338 | }; 339 | 9CE9D6421B1230BB0081EE78 /* Build configuration list for PBXNativeTarget "testlint - erica's personal" */ = { 340 | isa = XCConfigurationList; 341 | buildConfigurations = ( 342 | 9CE9D6431B1230BB0081EE78 /* Debug */, 343 | 9CE9D6441B1230BB0081EE78 /* Release */, 344 | ); 345 | defaultConfigurationIsVisible = 0; 346 | defaultConfigurationName = Release; 347 | }; 348 | /* End XCConfigurationList section */ 349 | }; 350 | rootObject = 8ECAFF071B0A76FD00A97C1C /* Project object */; 351 | } 352 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/project.xcworkspace/xcshareddata/testlint.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 06BE7A5A-B6FF-401D-B520-5A2C45CB1D60 9 | IDESourceControlProjectName 10 | testlint 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 9558C5F0D07A4B34D744527944151FBD368A61B1 14 | https://github.com/lifely/testlint.git 15 | 16 | IDESourceControlProjectPath 17 | testlint/testlint.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 9558C5F0D07A4B34D744527944151FBD368A61B1 21 | ../../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/lifely/testlint.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 9558C5F0D07A4B34D744527944151FBD368A61B1 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 9558C5F0D07A4B34D744527944151FBD368A61B1 36 | IDESourceControlWCCName 37 | testlint 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/project.xcworkspace/xcuserdata/ericasadun.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erica/testlint/a22392e90e047de681b77979635b5b5dd5d893b5/testlint/testlint.xcodeproj/project.xcworkspace/xcuserdata/ericasadun.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/project.xcworkspace/xcuserdata/ericasadun.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HasAskedToTakeAutomaticSnapshotBeforeSignificantChanges 6 | 7 | SnapshotAutomaticallyBeforeSignificantChanges 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/xcshareddata/xcschemes/testlint-personal.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/xcshareddata/xcschemes/testlint.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/xcuserdata/ericasadun.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/xcuserdata/ericasadun.xcuserdatad/xcschemes/testlint.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /testlint/testlint.xcodeproj/xcuserdata/ericasadun.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | testlint.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 8ECAFF0E1B0A76FD00A97C1C 16 | 17 | primary 18 | 19 | 20 | 9CE9D6361B1230BB0081EE78 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /testlint/testlint/main.m: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Erica Sadun, http://ericasadun.com 4 | 5 | */ 6 | 7 | #import 8 | #import "Linter.h" 9 | #import "Utility.h" 10 | #import "NSArray+Frankenstein.h" 11 | 12 | @interface Processor : NSObject 13 | @property (nonatomic, strong) Linter *linter; 14 | @end 15 | 16 | @implementation Processor 17 | 18 | #pragma mark - Linter 19 | 20 | - (Linter *) linter 21 | { 22 | if (!_linter) _linter = [Linter new]; 23 | return _linter; 24 | } 25 | 26 | - (instancetype) init 27 | { 28 | if (!(self = [super init])) return self; 29 | return self; 30 | } 31 | 32 | - (void) dealloc 33 | { 34 | } 35 | 36 | 37 | #pragma mark - Paths 38 | 39 | - (NSString *) projectName 40 | { 41 | NSFileManager *manager = [NSFileManager defaultManager]; 42 | NSString *wd = manager.currentDirectoryPath; 43 | NSArray *contents = [manager contentsOfDirectoryAtPath:wd error:nil]; 44 | NSArray *filtered = [contents filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self endswith 'xcodeproj'"]]; 45 | if (!filtered.count) return nil; 46 | return [filtered.lastObject stringByDeletingPathExtension]; 47 | } 48 | 49 | - (NSString *) projectPath 50 | { 51 | NSFileManager *manager = [NSFileManager defaultManager]; 52 | NSString *wd = manager.currentDirectoryPath; 53 | NSArray *contents = [manager contentsOfDirectoryAtPath:wd error:nil]; 54 | NSArray *filtered = [contents filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self endswith 'xcodeproj'"]]; 55 | if (!filtered.count) return nil; 56 | return [wd stringByAppendingPathComponent:filtered.lastObject]; 57 | } 58 | 59 | - (NSArray *) fetchPathsForDict: (NSDictionary *) dict 60 | { 61 | NSMutableArray *results = @[].mutableCopy; 62 | NSMutableDictionary *objects = [dict[@"objects"] mutableCopy]; 63 | 64 | NSArray *groups = [objects.allKeys collect:^BOOL(id object) { 65 | NSDictionary *d = objects[object]; 66 | BOOL group = [d[@"isa"] isEqualToString:@"PBXGroup"]; 67 | return group; 68 | }]; 69 | 70 | NSMutableArray *files = [objects.allKeys collect:^BOOL(id object) { 71 | NSDictionary *d = objects[object]; 72 | BOOL isSwift = [d[@"lastKnownFileType"] isEqualToString:@"sourcecode.swift"]; 73 | return isSwift; 74 | }].mutableCopy; 75 | 76 | for (NSString *key in groups) 77 | { 78 | NSDictionary *group = objects[key]; 79 | NSString *groupPath = group[@"path"]; 80 | 81 | for (NSString *childKey in group[@"children"]) 82 | { 83 | NSDictionary *child = objects[childKey]; 84 | [files removeObject:childKey]; 85 | BOOL isSwiftFile = [child[@"lastKnownFileType"] isEqualToString:@"sourcecode.swift"]; 86 | if (!isSwiftFile) continue; 87 | 88 | NSString *childPath = child[@"path"]; 89 | if (!childPath) continue; 90 | 91 | NSString *wd = [[NSFileManager defaultManager] currentDirectoryPath]; 92 | if (!groupPath) groupPath = [wd stringByAppendingPathComponent:[self projectName]]; 93 | NSString *path = [groupPath stringByAppendingPathComponent:childPath]; 94 | if ([[NSFileManager defaultManager] fileExistsAtPath:path]) { 95 | [results addObject:path]; 96 | continue; 97 | } 98 | 99 | const char *cpath = [path cStringUsingEncoding:NSUTF8StringEncoding]; 100 | char *resolved = NULL; 101 | char *returnValue = realpath(cpath, resolved); 102 | if (returnValue == NULL || resolved == NULL) continue; 103 | [results addObject:[NSString stringWithCString:returnValue encoding:NSUTF8StringEncoding]]; 104 | } 105 | } 106 | 107 | // Files not in groups 108 | for (NSString *fileKey in files) 109 | { 110 | NSDictionary *dict = objects[fileKey]; 111 | NSString *path = dict[@"path"]; 112 | NSLog(@"NOTGROUP: %@", dict); 113 | const char *cpath = [path cStringUsingEncoding:NSUTF8StringEncoding]; 114 | char *resolved = NULL; 115 | char *returnValue = realpath(cpath, resolved); 116 | NSLog(@"path: %@, cpath: %s, resolved %s return value %s", path, cpath, resolved, returnValue); 117 | if (returnValue == NULL || resolved == NULL) continue; 118 | [results addObject:[NSString stringWithCString:returnValue encoding:NSUTF8StringEncoding]]; 119 | } 120 | 121 | return results; 122 | } 123 | 124 | #pragma mark - Action 125 | 126 | - (void) processIndividualItems: (NSArray *) args 127 | { 128 | NSFileManager *manager = [NSFileManager defaultManager]; 129 | for (NSString *arg in args) { 130 | 131 | // Establish path 132 | BOOL isDir = false; NSString *path = arg; 133 | if ([arg isEqualToString:@"."]) path = manager.currentDirectoryPath; 134 | if (![manager fileExistsAtPath:path isDirectory:&isDir]) 135 | path = [manager.currentDirectoryPath stringByAppendingPathComponent:arg]; 136 | if (![manager fileExistsAtPath:path isDirectory:&isDir]) continue; 137 | 138 | // Process each Swift file 139 | if ([path hasSuffix:@".swift"]) 140 | { 141 | Log(@"Checking '%@'", path.lastPathComponent); 142 | [self.linter lint:path]; 143 | continue; 144 | } 145 | 146 | // Recursively descend through directories 147 | if (isDir) 148 | { 149 | NSMutableArray *files = @[].mutableCopy; 150 | for (NSString *file in [manager contentsOfDirectoryAtPath:path error:nil]) 151 | [files addObject:[arg stringByAppendingPathComponent:file]]; 152 | [self processIndividualItems:files]; 153 | continue; 154 | } 155 | } 156 | } 157 | 158 | // Entry point for project builds 159 | - (void) go: (NSArray *) args 160 | { 161 | if (args.count > 0) 162 | { 163 | [self processIndividualItems:args]; 164 | return; 165 | } 166 | 167 | // Find embedded project.pbxproj 168 | NSString *xcpath = [self projectPath]; if (!xcpath) return; 169 | NSFileWrapper *fileWrapper = [[NSFileWrapper alloc] initWithURL:[NSURL fileURLWithPath:xcpath] options:NSFileWrapperReadingImmediate error:nil]; 170 | if (!fileWrapper.isDirectory) return; 171 | NSFileWrapper *pbx = fileWrapper.fileWrappers[@"project.pbxproj"]; if (!pbx) return; 172 | 173 | // Read XML 174 | CFDataRef xmlData = (__bridge CFDataRef)pbx.regularFileContents; 175 | CFPropertyListFormat format = kCFPropertyListXMLFormat_v1_0; 176 | CFPropertyListRef plist = CFPropertyListCreateWithData(kCFAllocatorDefault, xmlData, kCFPropertyListMutableContainers, &format, nil); 177 | if (!plist) return; 178 | 179 | // Fetch project file paths 180 | NSDictionary *dict = (__bridge_transfer NSDictionary *) plist; 181 | NSArray *paths = [self fetchPathsForDict:dict]; 182 | int count = 0; 183 | for (NSString *path in paths) 184 | { 185 | Log(@"Checking '%@' (%zd of %zd):", path.lastPathComponent, ++count, paths.count); 186 | [self.linter lint:path]; 187 | } 188 | } 189 | @end 190 | 191 | #pragma mark - Usage 192 | 193 | // accessModifierChecks 194 | void usage(NSString *appname) 195 | { 196 | Log(@"Usage: %@ options file...", appname); 197 | Log(@" help: -help"); 198 | Log(@""); 199 | Log(@"Use 'NOTE: ', 'ERROR: ', 'WARNING: ', HACK, and FIXME to force emit"); 200 | Log(@"Use 'nwm' to skip individual line processing: // nwm"); 201 | Log(@"Use ##SkipAccessChecksForFile somewhere to skip file processing"); 202 | exit(-1); 203 | } 204 | 205 | #pragma mark - Command-line entry point 206 | 207 | #define DEFAULT_OBJ(_KEY_) [[NSUserDefaults standardUserDefaults] objectForKey:_KEY_] 208 | typedef void (^UtilityBlock)(void); 209 | 210 | int main(int argc, const char * argv[]) { 211 | @autoreleasepool { 212 | Processor *processor = [Processor new]; 213 | 214 | // Fetch app name and arguments 215 | NSString *appName = [[NSProcessInfo processInfo] arguments].firstObject; 216 | NSMutableArray *arguments = [[NSProcessInfo processInfo] arguments].mutableCopy; 217 | [arguments removeObjectAtIndex:0]; // remove app name 218 | 219 | // Fetched dashed arguments 220 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF beginswith '-'"]; 221 | NSArray *dashedArguments = [arguments filteredArrayUsingPredicate:predicate]; 222 | 223 | // Equivalence list for argument conversion to standard form 224 | NSDictionary *equivalences = @{ 225 | @"--help" : @"-help", 226 | @"--h" : @"-help", 227 | @"-h" : @"-help", 228 | }; 229 | 230 | // Should continue execution after processing arguments 231 | BOOL __block shouldContinue = YES; 232 | 233 | for (NSString *actualArgument in dashedArguments) 234 | { 235 | NSString *argument = actualArgument; 236 | [arguments removeObject:argument]; // trim down to just items listed at end 237 | 238 | if (equivalences[actualArgument]) argument = equivalences[actualArgument]; 239 | // NSString *argumentValue = DEFAULT_OBJ([actualArgument substringFromIndex:1]); 240 | 241 | UtilityBlock block = @{ 242 | @"-help" : ^{ 243 | // Quits after help 244 | usage(appName); 245 | }, 246 | }[argument]; 247 | if (block) block(); 248 | } 249 | if (!shouldContinue) exit(0); 250 | [processor go:arguments]; 251 | return processor.linter.encounteredErrors ? -1 : 0; 252 | } 253 | return 0; 254 | } 255 | --------------------------------------------------------------------------------