Mobile Development 19 min read

Practical Guide to Using OCLint for Static Code Analysis in iOS Projects

This practical guide walks iOS developers through installing OCLint, generating a compilation database, creating custom Clang‑AST rules, optimizing analysis runtime with parallel processing, and interpreting results that uncovered hundreds of performance‑critical issues, demonstrating how static analysis can dramatically improve startup speed.

NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
NetEase Cloud Music Tech Team
Practical Guide to Using OCLint for Static Code Analysis in iOS Projects

As projects grow, relying solely on manual code review to ensure code quality becomes impractical. This article explains why static analysis tools are needed and introduces three popular open‑source static analysers for C/Objective‑C: Clang Static Analyzer, Infer, and OCLint. After comparing their features, OCLint was chosen for its strong customizability.

The article is organized into the following sections:

OCLint environment deployment, compilation, and analysis.

Implementation of custom rules.

Optimization of static analysis runtime.

Using static analysis to continuously monitor startup performance degradation.

OCLint Overview

OCLint consists of four main modules:

Core Module : orchestrates the analysis workflow and generates reports.

Metrics Module : a standalone library that can be reused in other projects.

Rules Module : loads rule implementations as dynamic libraries, following the open/closed principle.

Reporters Module : formats detected issues into readable reports.

Environment Setup

3.1 Install OCLint

brew tap oclint/formulae
brew install oclint
# Recommended to install the latest version
brew install --cask oclint

Install xcpretty to format xcodebuild output:

gem install xcpretty

3.2 Generate Compilation Database

Run the following command in the project directory to produce compile_commands.json :

xcodebuild -workspace "${project_name}.xcworkspace" -scheme ${scheme} -destination generic/platform=iOS -configuration Debug COMPILER_INDEX_STORE_ENABLE=NO | xcpretty -r json-compilation-database -o compile_commands.json

Clang AST Introduction

OCLint builds on Clang Tooling, which analyses the Clang AST. An AST node represents a declaration, statement, or type. The three core AST classes are Decl , Stmt , and Type . A simple AST example:

#include "test.hpp"

int f(int x) {
  int result = (x / 42);
  return result;
}

Running clang -Xclang -ast-dump -fsyntax-only test.cpp produces a hierarchical dump (truncated for brevity).

OCLint Workflow

The main driver is oclint/oclint-driver/main.cpp . The main() function parses command‑line options, prepares the analysis, selects enabled rules, runs the RulesetBasedAnalyzer , and finally reports results. The full source is shown below:

int main(int argc, const char **argv)
{
    llvm::cl::SetVersionPrinter(oclintVersionPrinter);
    // construct parser
    auto expectedParser = CommonOptionsParser::create(argc, argv, OCLintOptionCategory);
    if (!expectedParser) {
        llvm::errs() << expectedParser.takeError();
        return COMMON_OPTIONS_PARSER_ERRORS;
    }
    CommonOptionsParser &optionsParser = expectedParser.get();
    oclint::option::process(argv[0]);

    // preparation – check rules & reporters
    int prepareStatus = prepare();
    if (prepareStatus) {
        return prepareStatus;
    }

    // list enabled rules if requested
    if (oclint::option::showEnabledRules()) {
        listRules();
    }

    // construct analyzer & driver
    oclint::RulesetBasedAnalyzer analyzer(oclint::option::rulesetFilter().filteredRules());
    oclint::Driver driver;

    // run analysis
    try {
        driver.run(optionsParser.getCompilations(), optionsParser.getSourcePathList(), analyzer);
    } catch (const exception &e) {
        printErrorLine(e.what());
        return ERROR_WHILE_PROCESSING;
    }

    // obtain results & report
    std::unique_ptr
results(std::move(getResults()));
    try {
        ostream *out = outStream();
        reporter()->report(results.get(), *out);
        disposeOutStream(out);
    } catch (const exception &e) {
        printErrorLine(e.what());
        return ERROR_WHILE_REPORTING;
    }

    // exit
    return handleExit(results.get());
}

Running Default Rules

Execute the helper oclint-json-compilation-database with desired options:

oclint-json-compilation-database --verbose -report-type html -o oclint.html -max-priority-1 100000 -max-priority-2 100000 -max-priority-3 100000

If the command fails, check the error message. A common failure is caused by non‑ASCII characters in file paths.

Custom Rule Development

To add a rule, create a subclass of AbstractASTVisitorRule . Example for detecting +load methods:

class ObjCVerifyLoadCallRule : public AbstractASTVisitorRule
{
public:
    // rule priority
    virtual int priority() const override { return priority; }

    // visit Objective‑C method declarations
    bool VisitObjCMethodDecl(ObjCMethodDecl *node)
    {
        string selectorName = node->getSelector().getAsString();
        if (node->isClassMethod() && selectorName == "load") {
            string desc = "xxx(replace with description)";
            addViolation(node, this, desc);
            return false;
        }
        return true;
    }
};

Compile the rule into a dynamic library and place it under OCLint's rule directory (e.g., /usr/local/Caskroom/oclint/22.02/oclint-22.02/lib/oclint/rules ), then verify with oclint -list-enabled-rules .

Selective Rule Execution

Run OCLint with only the desired rules:

oclint-json-compilation-database -- -rule ObjCVerifyLoadCall -rule NEModuleHubLaunch -enable-global-analysis -max-priority-3 100000 -report-type pmd -o result.xml

Performance Optimization

The original analysis took about 6 hours. Two major bottlenecks were identified:

Generating the compilation database (~50 minutes).

Analyzing compile_commands.json (~5 hours).

Splitting the large JSON into smaller chunks and processing them in parallel reduced total runtime to ~2.5 hours (≈58 % reduction). Sample multiprocessing script:

def subProcessLint():
    manager = Manager()
    list = manager.list(lintpy_files)  # shared list
    sub_p = []
    for i in range(process_count):
        process_name = 'Process------%02d' % (i+1)
        p = Process(target=lint_subProcess, args=(process_name, list))
        sub_p.append(p)
        p.start()
    for p in sub_p:
        p.join()

def lint_subProcess(name, files):
    while len(files) > 0:
        print('process name is ', name)
        lint_command = files[0]
        files.remove(lint_command)
        start_time = time.time()
        print('before lint:', lint_command)
        os.system(r'python3 %s' % lint_command)
        print('lint time:', time.time() - start_time)

After processing, the individual XML reports are merged into a final report.

Result and Future Work

The optimized pipeline detected over 600 potential performance‑impacting code fragments across 120+ libraries, estimating a possible 250 ms startup improvement after remediation. The team plans to integrate SwiftLint for Swift projects and continue enhancing the static analysis platform.

References

Clang documentation

Clang Static Analyzer

Infer

OCLint Documentation

oclint_argument_list_too_long_solution

Python tutorial

SwiftLint

iOSautomationcode qualitystatic analysisclangOCLint
NetEase Cloud Music Tech Team
Written by

NetEase Cloud Music Tech Team

Official account of NetEase Cloud Music Tech Team

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.