How to improve the quality of iOS code through static analysis

 

With the expansion of the project, it is more and more unrealistic to rely on human codereview to ensure the quality of the project. At this time, it is necessary to use an automatic code review tool: program static analysis.

 

Program Static Analysis is a kind of code analysis technology that can scan the program code through lexical analysis, syntax analysis, control flow, data flow analysis and other technologies without running the code to verify whether the code meets the specification, safety, reliability, maintainability and other indicators. (from Baidu Encyclopedia)

Lexical analysis, syntax analysis and other work are carried out by the compiler, so in order to complete the static analysis of iOS projects, we need to rely on the compiler. The static analysis of OC language can be completely through Clang For Swift's static analysis, in addition to Clange, we need to use SourceKit.

SwiftLint is the static analysis tool corresponding to Swift language, and Infer and OCLitn are the static analysis tools corresponding to OC language. The following is an introduction to the installation and use of various static analysis tools.

SwiftLint

 

For static analysis of Swift projects, you can use the SwiftLint . SwiftLint is a tool used to enforce the style and specification of Swift code. Its implementation is Hook with Clang and SourceKit so that it can use AST To represent more precise results of the source code file. Clange, we got it. What's the use of SourceKit?

SourceKit included in Swift The main warehouse of the project, which is a set of toolsets, supports most of Swift's source code operation features: source code parsing, syntax highlighting, typesetting, automatic completion, cross language header generation, etc.

install

There are two ways to install, either way: through Homebrew

$ brew install swiftlint

This is a global installation, which can be used by all applications. Mode 2: through CocoaPods

pod 'SwiftLint', :configurations => ['Debug']

This approach is equivalent to integrating SwiftLint into the project as a three-party library, because it is only a debugging tool, so we should specify it to only take effect in the Debug environment.

Integrated into Xcode

We need to add a Run Script Phase in Build Phases of the project. If it's installed through homebrew, your script should look like this.

if which swiftlint >/dev/null; then
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

If it is installed through cocoapods, your script should be as follows:

"${PODS_ROOT}/SwiftLint/swiftlint"

 

Run SwiftLint

Type CMD + B to compile the project. After compiling, the script we just added will be run. After that, we will see a large number of warning messages in the project. Sometimes the build information can't be filled in the project code. We can view it in the compiled log.

 

 

customized

There are too many SwiftLint rules. If we don't want to execute a rule, or want to filter out the analysis of Pods library, we can configure SwfitLint.

Create a new one in the project root directory swiftlint.yml File, then fill in the following:

disabled_rules: # rule identifiers to exclude from running
  - colon
  - trailing_whitespace
  - vertical_whitespace
  - function_body_length
opt_in_rules: # some rules are only opt-in
  - empty_count
  # Find all the available rules by running:
  # swiftlint rules
included: # paths to include during linting. `--path` is ignored if present.
  - Source
excluded: # paths to ignore during linting. Takes precedence over `included`.
  - Carthage
  - Pods
  - Source/ExcludedFolder
  - Source/ExcludedFile.swift
  - Source/*/ExcludedFile.swift # Exclude files with a wildcard
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
  - explicit_self

# configurable rules can be customized from this configuration file
# binary rules can set their severity level
force_cast: warning # implicitly
force_try:
  severity: warning # explicitly
# rules that have both warning and error levels, can set just the warning level
# implicitly
line_length: 110
# they can set both implicitly with an array
type_body_length:
  - 300 # warning
  - 400 # error
# or they can set both explicitly
file_length:
  warning: 500
  error: 1200
# naming rules can set warnings/errors for min_length and max_length
# additionally they can set excluded names
type_name:
  min_length: 4 # only warning
  max_length: # warning and error
    warning: 40
    error: 50
  excluded: iPhone # excluded via string
  allowed_symbols: ["_"] # these are allowed in type names
identifier_name:
  min_length: # only min_length
    error: 4 # only error
  excluded: # excluded via string array
    - id
    - URL
    - GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)

A rule prompt is as follows. Its corresponding rule name is function_body_length.

! Function Body Length Violation: Function body should span 40 lines or less excluding comments and whitespace: currently spans 43 lines (function_body_length)

disabled_ Fill in the rules we don't want to follow under rules.

excluded sets the directories we want to skip checking. Carthage, Pod and SubModule can be filtered out.

Others are like file length_ Length), type name length (type_name), we can adjust it by setting specific values.

In addition, SwiftLint also supports custom rules. We can define our own rules according to our own needs.

Generate report

If we want to generate a report for this analysis, it is also possible (the command is swiftint installed through homebrew):

# reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
$ swiftlint lint --reporter html > swiftlint.html

 

xcodebuild

Xcodebuild is the built-in compilation command of xcode. We can use it to compile and package our iOS projects. The Infer and OCLint introduced next are analyzed based on the compilation products of xcodebuild, so it is necessary to briefly introduce it.

Generally, when compiling a project, we need to specify the project name, configuration, scheme, sdk and other information. Here are a few simple commands and instructions.

# For projects without pod, the target name is TargetName. Under Debug, specify the simulator sdk environment to compile
xcodebuild -target TargetName -configuration Debug -sdk iphonesimulator
# Project with pod, workspace named TargetName.xcworkspace , under Release, scheme is targetname, which specifies the real environment for compilation. Do not specify simulator environment to validate certificate
xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release
# Understand the compiled product of the project
xcodebuild -workspace WorkspaceName.xcworkspace -scheme SchemeName Release clean

Later, the xcodebuild command needs to replace these parameters with the parameters of your own project.

Infer

 

Infer It is a static analysis tool developed by Facebook for C, OC and Java languages. It supports the analysis of iOS and Android applications at the same time. For Facebook internal applications such as Messenger, Instagram and other applications, it is used for static analysis. It mainly detects hidden problems, including the following:

  • Resource leak, memory leak
  • Non null detection of variables and parameters
  • Circular reference
  • Early nil operation

Custom rules are not supported at this time.

Installation and use

$ brew install infer

Running infer

$ cd projectDir
# Skip Pods analysis
$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator

We will get an infer out folder, which contains various code analysis files, including txt, json and other file formats. When this is inconvenient to view, we can convert it to html format:

$ infer explore --html

 

Click trace and we'll see the context of the problem code.

Because Infer is incremental by default, it only analyzes the changed code. If we want to compile as a whole, we need to clean the following projects:

$ xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator clean

Run Infer again to compile.

$ infer run --skip-analysis-in-path Pods -- xcodebuild -workspace "Project.xcworkspace" -scheme "Scheme" -configuration Debug -sdk iphonesimulator

Infer's general principle

The static analysis of Infer is divided into two stages:

1. Capture phase

Infer captures the compile command and translates the file into an intermediate language within infer.

This kind of translation is similar to compilation. Infer obtains information from the compilation process and translates it. That's why we call infer with a compile command, such as: infer -- clang -c file.c, infer -- javac File.java . The result is that the file is compiled as usual and translated into an intermediate language by infer, which is reserved for the second stage of processing. In particular, if no files are compiled, no files will be analyzed.

Infer stores intermediate files in the result folder. Generally speaking, this folder will be created in the directory where infer is running. The name is infer out /.

2. Analysis stage

In the analysis phase, Infer analyzes all files under Infer out /. As you analyze, each method and function is analyzed separately.

When analyzing a function, if an error is found, the analysis will be stopped, but this does not affect the further analysis of other functions.

So when you check the problem, after fixing the output error, you need to continue to run Infer to check until all problems have been fixed.

In addition to the standard output, the error will be output to the file "infer out"/ bug.txt In, we filter these problems to show only the most likely ones.

In the results folder (Infer out), there is also a CSV file report.csv , which contains all Infer generated information, including: errors, warnings, and messages.

OCLint

OCLint Is based on Clange Tooling It supports extension, and the scope of detection is larger than Infer. Not only hide bug s, but also some code normalization problems, such as naming and function complexity.

Install OCLint

OCLint is generally installed through Homebrew

$ brew tap oclint/formulae   
$ brew install oclint

Version 0.13 is installed through humbrew.

$ oclint --version
LLVM (http://llvm.org/):
  LLVM version 5.0.0svn-r313528
  Optimized build.
  Default target: x86_64-apple-darwin19.0.0
  Host CPU: skylake

OCLint (http://oclint.org/):
  OCLint version 0.13.
  Built Sep 18 2017 (08:58:40).

I have run OCLint on two projects with Xcode11 respectively. One instance project can run normally, and the other complex project fails to run. The following error is reported:

1 error generated
1 error generated
...
oclint: error: cannot open report output file ..../onlintReport.html

I don't know why. If you want to see if 0.13 can be used, just skip to install xcpretty. If you also encounter this problem, you can come back and install oclint version 0.15.

OCLint0.15

I am here oclint issuse #547 Here we find the problem and the corresponding solution.

We need to update oclint to version 0.15. The latest version on brew is 0.13, and the latest version on github is 0.15. I download release 0.15 on github, but this package is not compiled. I'm not sure if the official has made a mistake. I can only compile it manually. Because the compilation needs to download llvm and clange, these two packages are large, so I sent the compiled package directly here CodeChecker.

If you don't care about the compilation process, you can download the compiled package and skip to the step of setting environment variables.

Compile OCLint

1. Installation CMake and Ninja These two compilation tools

$ brew install cmake ninja

2. Project clone OCLint

$ git clone https://github.com/oclint/oclint

3. Enter the oclint scripts directory and execute the make command

$ ./make

After success, the build folder will appear, in which an oclint release is the oclint tool that compiles successfully.

Setting environment variables for oclint tool

The purpose of setting environment variables is for us to have quick access. Then we need to configure the PATH environment variable, pay attention to OCLint_PATH path is the PATH where you store oclint release. Add it to. zshrc, or. Bash_ End of profile file:

OCLint_PATH=/Users/zhangferry/oclint/build/oclint-release
export PATH=$OCLint_PATH/bin:$PATH

Execute source. Zshrc, refresh the environment variables, and then verify that oclint is installed successfully:

$ oclint --version
OCLint (http://oclint.org/):
OCLint version 0.15.
Built May 19 2020 (11:48:49).

The introduction shows that we have completed the installation.

Install xcpretty

xcpretty is a script tool for formatting the output of xcodebuild, and the parsing of oclint depends on its output. Its installation mode is:

$ gem install xcpretty

Use of OCLint

Before using OCLint, you need to prepare the COMPILER_INDEX_STORE_ENABLE is set to NO.

  • Compare the compiler under Building Settings in Project and Targets_ INDEX_ STORE_ Enable set to NO
  • Add the following script before target 'target' do in podfile to change the compilation configuration of each pod to this option
post_install do |installer|
  installer.pods_project.targets.each do |target|
      target.build_configurations.each do |config|
          config.build_settings['COMPILER_INDEX_STORE_ENABLE'] = "NO"
      end
  end
end

How to use

1. Enter the root directory of the project and run the following script:

$ xcodebuild -workspace ProjectName.xcworkspace -scheme ProjectScheme -configuration Debug -sdk iphonesimulator | xcpretty -r json-compilation-database -o compile_commands.json

Some information during xcodebuild compilation will be recorded as a file compile_commands.json , if we see the file in the root directory of the project, and there is content in it, it proves that we have completed the first step.

2. We turn this json file into html for easy viewing and filter out the analysis of Pods files. To prevent the upper limit of the number of lines, we add the limit of the number of lines:

$ oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html -rc LONG_LINE=9999 -max-priority-1=9999 -max-priority-2=9999 -max-priority-3=9999

Eventually, a oclintReport.html Documents.

 

OCLint supports custom rules, because its own rules are very rich, and the demand for custom rules should be very small, so there is no attempt.

Encapsulation script

OCLint and Infer are executed by running several script languages. We can encapsulate these commands into a script file. For example, Infer is similar to OCLint:

#!/bin/bash
# mark sure you had install the oclint and xcpretty

# You need to replace these values with your own project configuration
workspace_name="WorkSpaceName.xcworkspace"
scheme_name="SchemeName"

# remove history
rm compile_commands.json
rm oclint_result.xml
# clean project
# -sdk iphonesimulator means run simulator
xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator clean || (echo "command failed"; exit 1);

# export compile_commands.json
xcodebuild -workspace $workspace_name -scheme $scheme_name -configuration Debug -sdk iphonesimulator \
| xcpretty -r json-compilation-database -o compile_commands.json \
|| (echo "command failed"; exit 1);

# export report html
# you can run `oclint -help` to see all USAGE
oclint-json-compilation-database -e Pods -- -report-type html -o oclintReport.html \
-disable-rule ShortVariableName \
-rc LONG_LINE=1000 \
|| (echo "command failed"; exit 1);

open -a "/Applications/Safari.app" oclintReport.html

Description of several parameters of oclint JSON compilation database command:

-e. files that need to be ignored for analysis and warnings for these files will not appear in the report

-rc is the threshold value of the rule to be overridden. Here, you can customize the threshold value of the project, Default threshold

-The rules supported by enable rule are all supported by oclint by default. You can combine disable rule to filter out some rules Rule list

-Disable rule rules to be ignored, set according to project requirements

Using OCLint in Xcode

Because OCLint provides an output style in Xcode format, we can put it in Xcode as a script.

1. Under TARGETS of the project, click "+" below and select Aggregate under cross platform. Enter a name, here it's OCLint

 

2. Select the Target, enter Build Phases, add Run Script, and write the following script:

# Type a script or drag a script file from your workspace to insert its path.
# Built in variables
cd ${SRCROOT}
xcodebuild clean 
xcodebuild | xcpretty -r json-compilation-database
oclint-json-compilation-database -e Pods -- -report-type xcode

It can be seen that the script is the same as the above script, except that the - report type of OCLint JSON compilation database command is changed from html to xcode. OCLint as a target itself runs in a specific environment, so xcodebuild can omit configuration parameters.

3. Through CMD + B, we compile the following project, execute the script task, and get the warning information that can locate the code:

 

summary

The following is a comparison of these static analysis schemes. We can choose a suitable static analysis scheme according to our needs.

  SwiftLint Infer OCLint
Language support Swift C,C++,OC,Java C,C++,OC
Ease of use simple Simpler Simpler
Can it be integrated into Xcode sure Cannot integrate into xcode sure
Rule richness More, including code specifications Relatively few, mainly detecting potential problems More, including code specifications
Rule extensibility sure may not sure

reference resources

OCLint implementation Code Review - improve the quality of your code

Using OCLint in Xcode

Working mechanism of Infer

Getting started with llvm & clang

recommend 👇 :

If you want to advance together, you can add a communication group 1012951431

Interview questions or related learning materials can be downloaded in group files!

 

 

 

Tags: iOS JSON xcode SDK Swift

Posted on Tue, 26 May 2020 04:01:28 -0400 by onekoolman