April 10, 2014

Compile-Time Error for Incorrectly Cased #import

OS X uses a case-insensitive filesystem by default. That means the following code that purports to load AFNetworking.h both compiles and runs, nary a peep:

#import "AfNeTwOrKiNg.H"

Gawsh, I can't put my finger on it, but that kinda rubs me the wrong way.

The frequency of my typing YWaction.h when I meant YWAction.h would frighten children. One of my favorite things about working in a language like Objective-C (relative to my mother tongue Perl, anyway) is that I am immediately notified of most typos. But clang does not warn about this one. It's just plain sloppy. That I cannot abide. Practically, it would also lead to lots of tiny problems should anyone try to build this app on a case-sensitive filesystem.

Carefully reviewing all the #import statements in my project makes my eyes glaze over. So, let's put this high-falutin' typin' teevee machine to work. Here is how you can get Xcode to text you more when you screw up your imports.

  1. Navigate to your app target. It's probably the first entry in your file navigator, then under Targets on the left pane.
  2. Select the Build Phases tab.
  3. Click the little + button at the top left.
  4. Select "New Run Script Build Phase".
  5. This adds a "Run Script" entry to the bottom of this list. Pop it open by clicking its disclosure triangle.
  6. Set the value of Shell to /usr/bin/perl
  7. You heard that right. Perl.
  8. In the text field below Shell, paste in the following Perl script:
    my @files = glob("*/*.[hm]");
    my %is_file = map { s{.*/}{}r => 1 } @files;
    my %lc_file = map { lc($_) => $_ } keys %is_file;
    
    my $errors = 0;
    
    for my $file (@files) {
        open my $handle, "<", $file;
        while (<$handle>) {
            next unless my ($import) = /#import\s*"(.*)"/;
            next if $is_file{$import};
    
            print qq{$file:$.: warning "$import"};
    
            if (my $fixed_case = $lc_file{lc $import}) {
                print qq{ (should be "$fixed_case")};
            }
    
            print qq{\n};
    
            ++$errors;
        }
    }
    
    exit 1 if $errors;
  9. Rename the build phase by clicking its name twice. I called mine Check #import Casing.
  10. Drag and drop to reorder your build phases however you like. Mine's near the top, because I think it's better to fail fast.

When it's all said and done, your build phase should resemble mine. Unless you've got a newer version of Xcode than me, in which case I'm knowing about my own future, that's cool!

Now when you ⌘B, Xcode will tell you about all your miscased #import statements just like any other builtin error. One less thing to be vigilant about! Happily, Xcode even shows these errors right in context.

You might notice that this is actually an error. That's because in my projects, all warnings are errors. Ain't nobody got time for anything less.