Photo of a forest
These would be non-abstract trees.

UPDATE: A new and improved version can be found here.

Did you know that Xcode includes a command line interface to the Swift compiler, swiftc? And beyond just compiling sources for you, it can do some other neat tricks. Chiefly of interest to me is that it will parse source files and generate an abstract syntax tree for you.

So what is an abstract syntax tree? In short, it’s a logical representation of the code structure that the compiler generates as an intermediate stage of of compilation. The reason it’s interesting to us is that it’s an easily parseable representation of the code. For instance, this code:

import Foundation
import Restless

protocol GitHubService: DRWebService {

    func listRepos(user: String, completionHandler: DRCallback<Array>) -> NSURLSessionDataTask
}

Gets translated to this:

(source_file
  (import_decl 'Foundation')
  (import_decl 'Restless')
  (protocol "GitHubService" type='GitHubService.Protocol' access=internal inherits: DRWebService
    (func_decl "listRepos(_:completionHandler:)" type='<`Self` : GitHubService> Self -> (String, completionHandler: DRCallback<Array>) -> NSURLSessionDataTask' interface type='<τ_0_0 where τ_0_0 : GitHubService> τ_0_0 -> (String, completionHandler: DRCallback<Array>) -> NSURLSessionDataTask' access=internal
      (body_params
        (pattern_typed implicit type='Self'
          (pattern_named implicit type='Self' 'self'))
        (pattern_tuple type='(String, completionHandler: DRCallback<Array>)' names='',completionHandler
          (pattern_typed type='String'
            (pattern_named type='String' 'user')
            (type_ident
              (component id='String' bind=type)))
          (pattern_typed type='DRCallback<Array>'
            (pattern_named type='DRCallback<Array>' 'completionHandler')
            (type_ident
              (component id='DRCallback' bind=type)
                (type_ident
                  (component id='Array' bind=type)
                    (type_ident
                      (component id='GitHubRepo' bind=type)))))))
      (result
        (type_ident
          (component id='NSURLSessionDataTask' bind=type))))))

Now you maybe thinking, “hey that doesn’t look too easy to read”. Well that’s true for us humans, but it’s actually much easier to parse than raw source.

So how do we generate this AST? If you’re just talking about a code snippet the lives in isolation, it’s not too hard to do. Just run this command:

swiftc -dump-ast MyFile.swift

However, it becomes more complicated if you actually want to dump the AST for an iOS project. If you don’t specify settings, swiftc makes all sorts of assumptions about what you want it to do. And they are generally wrong for iOS projects. Also, your project may have dependencies on third-party frameworks (probably through CocoaPods).

So here’s the final script that will give you the AST for your whole project, which you can execute in a Run Script Build Phase for your project:

eval CUR_TARGET_NAME='/pre>$DEPLOYMENT_TARGET_SETTING_NAME

find "${SRCROOT}/${PROJECT_NAME}" -type f -name *.swift | xargs xcrun -sdk ${SDK_NAME} swiftc -dump-ast -F ${FRAMEWORK_SEARCH_PATHS} -target ${arch}-apple-${SWIFT_PLATFORM_TARGET_PREFIX}${CUR_TARGET_NAME}

Let’s unpack that a bit.

First off, the find command:

find "${SRCROOT}/${PROJECT_NAME}" -type f -name *.swift

This is going to find all the Swift files in your project. I have made a mild assumption here, which is that all your source code will be in a sub-directory of your main project directory which has the same name as your project. If you use the standard project template, this should be true. Otherwise you may need to tweak it. We pipe the result of this to the main command (via xargs).

Next, we will actually launch the swiftc command via xcrun:

xcrun -sdk ${SDK_NAME} swiftc -dump-ast

Using xcrun will locate the correct copy of swift for us. That SDK_NAME will be iphoneos, iphonesimulator, or an OS X equivalent (if this isn’t an iOS project).

Next, we need to pull in our custom frameworks:

-F ${FRAMEWORK_SEARCH_PATHS}

Because swiftc is actually going to be parsing and validating our code, our dependencies need to be available. This setting tells it where to find our custom frameworks (such as our CocoaPods).

Finally we need to construct a ‘target’ setting. This ended up being the most daunting piece in many ways. What we want is a value like “x86_64-apple-ios9.1”. But there is no Xcode environment variable that gives us anything like that. Instead, I ended up constructing it from multiple variables (after much trial and error).

eval CUR_TARGET_NAME='/pre>$DEPLOYMENT_TARGET_SETTING_NAME

This actually reads the name of a different environment variable out of DEPLOYMENT_TARGET_SETTING_NAME and then gets that variable’s value.

Then we combine that with some other variables:

${arch}-apple-${SWIFT_PLATFORM_TARGET_PREFIX}${CUR_TARGET_NAME}

Whew! I’ll admit, this construction is a bit of a guess on my part. It works for me so far, but I’m not yet certain if it works in every case. For instance, for OS X projects. I’ve asked for clarification on the Swift Users mailing list, and will update this post when I hear back.

Conclusion

Ok, after all that work, what are we going to do with the AST now that we have it? Personally, I’m planning to write a parser for it so that I can do some code generation. Stay tuned for updates on that.