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.
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 oclintInstall xcpretty to format xcodebuild output:
gem install xcpretty3.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.jsonClang 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 100000If 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.xmlPerformance 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
NetEase Cloud Music Tech Team
Official account of NetEase Cloud Music Tech Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.