当我们搜索 Flutter & Dart 静态代码分析(static code analysis)时,得到的结果有 flutter analyze, dartanalyzer 等命令行工具。他们是什么?怎么用?有何关系?是否有业界最佳实践?如何自定义静态代码分析?最终应用于企业级 Flutter & Dart 研发和架构中。
Dart analyzer
Dart 具有静态代码分析工具 analyzer | Dart Package,在代码执行前发现问题,提高复杂软件代码质量。其中许多 linter 规则用于确保 Dart 代码符合最佳实践 Effective Dart | Dart。
例如:一个简单的空语句问题
10. void increment() {
11. if (count < 10) ;
12. count++;
13. }
lint • Avoid empty statements • example.dart:11 • empty_statements
Dart SDK 包括 VM,dart2js,核心库等,其中 analyzer 相关 package 位于:
GitHub: sdk/pkg at master · dart-lang/sdk
- sdk/pkg/
- analysis_server
- analysis_server_client
- analysis_tool
- analyzer
- analyzer_cli
- analyzer_plugin
通常将 analyzer 工具集成到命令行工具中(analyzer_cli)供终端用户使用,即 dartanalyzer 命令。
另外 analysis_server 作为持续运行进程,可以将 analyzer 静态代码分析结果不断提供给其他工具。
analysis_server 通常不会单独使用,而是与客户端(analysis_server_client)通信,例如:命令行工具(analyzer_cli),IDE(Android Studio)等。遵守 JSON 数据交换格式协议 Analysis Server API Specification。
dartanalyzer 使用示例:
Usage: dartanalyzer [options...]
dartanalyzer lib test web
静态代码分析 lib, test, web 目录
dartanalyzer --help
更多参数参考 --help
Flutter analyze
Flutter SDK 包含众多开发者工具 flutter_tools, 例如:命令行工具 commands。其中 analyze 相关命令位于:
GitHub: flutter/packages/flutter_tools/lib/src/commands at master · flutter/flutter
- flutter/packages/flutter_tools/lib/src/commands/
- analyze.dart [1]
- analyze_base.dart [2]
- analyze_continuously.dart [3]
- analyze_once.dart [4]
- flutter/packages/flutter_tools/lib/src/dart
- analysis.dart [5]
AnalyzeCommand[1] 类创建命令即 flutter analyze 并传递选项及参数,通过 AnalyzeBase[2] 的子类 AnalyzeContinuously[3] 或 AnalyzeOnce[4] 异步执行 AnalyzeBase.analyze() 开始静态代码分析。
analyze.dart 文件代码如下:
class AnalyzeCommand extends FlutterCommand {
...
@override
Future<FlutterCommandResult> runCommand() async {
if (boolArg('watch')) {
await AnalyzeContinuously(
argResults,
runner.getRepoRoots(),
runner.getRepoPackages(),
fileSystem: _fileSystem,
logger: _logger,
platform: _platform,
processManager: _processManager,
terminal: _terminal,
experiments: stringsArg('enable-experiment'),
artifacts: _artifacts,
).analyze();
} else {
await AnalyzeOnce(
argResults,
runner.getRepoRoots(),
runner.getRepoPackages(),
workingDirectory: workingDirectory,
fileSystem: _fileSystem,
logger: _logger,
platform: _platform,
processManager: _processManager,
terminal: _terminal,
experiments: stringsArg('enable-experiment'),
artifacts: _artifacts,
).analyze();
}
return FlutterCommandResult.success();
}
}
AnalyzeBase 子类异步执行 analyze() 方法,开启 Dart analysis server[5] AnalysisServer.start(),监听静态代码分析结果的数据流 AnalysisServer.onAnalyzing.listen()。
P.S. analyze_once.dart 逻辑类似,区别在于 flutter analyze --watch 执行 analyze_continuously.dart 逻辑,随着文件内容改变,持续静态代码分析。
analyze_continuously.dart 文件代码如下:
class AnalyzeContinuously extends AnalyzeBase {
...
@override
Future<void> analyze() async {
List<String> directories;
if (argResults['flutter-repo'] as bool) {
final PackageDependencyTracker dependencies = PackageDependencyTracker();
dependencies.checkForConflictingDependencies(repoPackages, dependencies);
directories = repoRoots;
analysisTarget = 'Flutter repository';
logger.printTrace('Analyzing Flutter repository:');
for (final String projectPath in repoRoots) {
logger.printTrace(' ${fileSystem.path.relative(projectPath)}');
}
} else {
directories = <String>[fileSystem.currentDirectory.path];
analysisTarget = fileSystem.currentDirectory.path;
}
final String sdkPath = argResults['dart-sdk'] as String ??
artifacts.getArtifactPath(Artifact.engineDartSdkPath);
final AnalysisServer server = AnalysisServer(sdkPath, directories,
fileSystem: fileSystem,
logger: logger,
platform: platform,
processManager: processManager,
terminal: terminal,
experiments: experiments,
);
server.onAnalyzing.listen((bool isAnalyzing) => _handleAnalysisStatus(server, isAnalyzing));
server.onErrors.listen(_handleAnalysisErrors);
await server.start();
final int exitCode = await server.onExit;
final String message = 'Analysis server exited with code $exitCode.';
if (exitCode != 0) {
throwToolExit(message, exitCode: exitCode);
}
logger.printStatus(message);
if (server.didServerErrorOccur) {
throwToolExit('Server error(s) occurred.');
}
}
...
}
AnalysisServer.start() 最终执行 sdkPath 路径下的 analysis_server.dart.snapshot 文件。
analysis.dart 文件代码如下:
class AnalysisServer {
...
Future<void> start() async {
final String snapshot = _fileSystem.path.join(
sdkPath,
'bin',
'snapshots',
'analysis_server.dart.snapshot',
);
final List<String> command = <String>[
_fileSystem.path.join(sdkPath, 'bin', 'dart'),
'--disable-dart-dev',
snapshot,
for (String experiment in _experiments)
...<String>[
'--enable-experiment',
experiment,
],
'--disable-server-feature-completion',
'--disable-server-feature-search',
'--sdk',
sdkPath,
];
_logger.printTrace('dart ${command.skip(1).join(' ')}');
_process = await _processManager.start(command);
// This callback hookup can't throw.
unawaited(_process.exitCode.whenComplete(() => _process = null));
final Stream<String> errorStream = _process.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter());
errorStream.listen(_logger.printError);
final Stream<String> inStream = _process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter());
inStream.listen(_handleServerResponse);
_sendCommand('server.setSubscriptions', <String, dynamic>{
'subscriptions': <String>['STATUS'],
});
_sendCommand('analysis.setAnalysisRoots',
<String, dynamic>{'included': directories, 'excluded': <String>[]});
}
...
}
总结:flutter analyze wrap dart analyzer.
flutter analyze 使用示例:
flutter analyze --write=analyzer-output.txt
将当前目录静态代码分析结果写入文本文件中。
文本格式:
// [error] Target of URI doesn't exist: 'package:test/test.dart' (/user/flutter/dev/tools/test/common.dart:8:8)
[${severity.toLowerCase()}] $messageSentenceFragment ($file:$startLine:$startColumn)
[严重程度] 错误信息 (文件路径:起始行:列) 由 AnalysisError[5] 类定义。
flutter analyze --help
更多参数参考 --help
Pedantic package
pedantic | Dart Package 是根据 Google 内部 Dart 代码最佳实践,总结的静态代码分析规则。如果说 Effective Dart 是设计文档,那么 pedantic 可以视为一种具体实现。
用户可以直接使用 pedantic 静态代码分析规则。
pedantic package 使用示例:
pubspec.yaml 文件添加如下内容:
dev_dependencies:
pedantic: ^1.9.0
引入 pedantic 作为 dev_dependencies 依赖,而非 dependencies,用于获取 analysis_options.yaml 静态代码分析规则,无需使用 package:pedantic/pedantic.dart 代码文件。
analysis_options.yaml 文件添加如下内容:
include: package:pedantic/analysis_options.yaml
pedantic 包中存在不同版本的静态代码分析规则,可以指定特定版本,以此避免 pedantic 升级导致的静态代码分析失败。例如:当 Flutter(Dart) SDK 中 errors, warnings, info 更新, 我们不得不消除这类新增的代码问题,才能保证静态代码分析通过。
GitHub: pedantic/lib at master · dart-lang/pedantic
- pedantic/lib/
- analysis_options.1.0.0.yaml
- analysis_options.1.1.0.yaml
- analysis_options.1.2.0.yaml
- ...
- analysis_options.1.9.0.yaml
include: package:pedantic/analysis_options.1.9.0.yaml
总结:pedantic is Google‘s static code analysis best practices.
深入阅读:
自定义静态代码分析|Customizing static code analysis
分析选项文件|analysis options file
通过配置 analysis_options.yaml 文件,自定义静态代码分析,将配置文件放置在包的根目录中,与 pubspec.yaml 文件位于同一层级。
analysis_options.yaml 文件配置示例:
include: package:pedantic/analysis_options.1.8.0.yaml
analyzer:
exclude: [build/**]
strong-mode:
implicit-casts: false
linter:
rules:
- camel_case_types
analysis_options.yaml 配置文件包含若干外层对象:
include: 引入其他静态代码分析配置文件,例如:pedantic package, effective_dart package。analyzer: 自定义静态代码分析选项,例如:强类型检查, 排除文件, 忽略规则, 改变规则严重程度...linter: 配置 linter 规则,例如:camel_case_types...
静态代码分析执行过程
静态代码分析从包的根目录遍历,发现 analysis_options.yaml 配置文件,则执行自定义静态代码分析。若没有自定义配置文件,根据 Flutter SDK 默认配置文件 执行标准静态代码分析。

- 根据 #1 analysis_options.yaml 配置文件,自定义静态代码分析 my_other_package 和 my_other_other_package 目录。
- 根据 #2 analysis_options.yaml 配置文件,自定义静态代码分析 my_package 目录。
强类型检查|stricter type checks
Dart 类型推断引擎默认非强类型检查(implicit-casts, implicit-dynamic default true)。
强类型检查需要手动开启:
analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
implicit-casts 值为 false 可以确保 Dart 类型推断引擎不会隐式转换具体类型。
例如:隐式转换失效
Object o = ...
String s = o; // Implicit downcast
String s2 = s.substring(1);
error • A value of type 'Object' can't be assigned to a variable of type 'String' • invalid_assignment
implicit-dynamic 值为 false 可以确保 Dart 类型推断引擎在无法确定静态类型时不会选择动态类型。
代码分析规则|linter rules
linter 规则众多 Linter for Dart,并非每条都适用于不同项目,某些规则更适用于库,而另一些则是针对 Flutter 应用设计的。
类似 pedantic package 用户可以直接使用 effective_dart 静态代码分析规则。
effective_dart package 使用示例:
pubspec.yaml 文件添加如下内容:
dev_dependencies:
effective_dart: ^1.0.0
analysis_options.yaml 文件添加如下内容:
include: package:effective_dart/analysis_options.yaml
- 批量开启 linter 规则
linter:
rules:
- annotate_overrides
- await_only_futures
- camel_case_types
- cancel_subscriptions
- close_sinks
- comment_references
- constant_identifier_names
- control_flow_in_finally
- empty_statements
- 若要自定义 linter 规则生效与否,可以修改键-值(规则名称-布尔变量)。
例如:引入 pedantic 规则集合,但禁止 avoid_shadowing_type_parameters 规则,开启 await_only_futures 规则。
include: package:pedantic/analysis_options.yaml
linter:
rules:
avoid_shadowing_type_parameters: false
await_only_futures: true
注意:受限于 YAML 格式,上述使用列表批量开启 linter 规则,和使用键-值对自定义 linter 规则生效与否,不能混合使用在同一 rules: 对象下
不需要分析的代码|Excluding code from analysis
静态代码分析结果并非金科玉律,有时某些代码无法通过静态代码分析,却仍然可以正常工作。例如:自动生成的代码。
- 不分析整个文件和文件夹或某一类文件
analyzer:
exclude:
- lib/client.dart
- lib/server/*.g.dart
- test/_data/**
- 相关规则对单一文件不生效
// ignore_for_file: unused_import, unused_local_variable
添加到需要的 .dart 文件,无论位于何处,整个文件忽略相关规则。
- 相关规则仅对 .dart 文件内单行代码不生效
// ignore: invalid_assignment
int x = '';
注释后的单行语句忽略相关规则。
规则严重程度|AnalysisSeverity
每个 analyzer error codes 和 linter rules 都有默认的严重程度,可以全局改变单个规则的严重程度,或始终忽略某些规则。
规则严重程度分级(由低到高):
- info
- warning
- error
error.dart 文件
class AnalysisError implements Diagnostic {
...
@override
Severity get severity {
switch (errorCode.errorSeverity) {
case ErrorSeverity.ERROR:
return Severity.error;
case ErrorSeverity.WARNING:
return Severity.warning;
case ErrorSeverity.INFO:
return Severity.info;
default:
throw StateError('Invalid error severity: ${errorCode.errorSeverity}');
}
}
...
}
- 全局忽略规则
analyzer:
errors:
todo: ignore
- 全局改变规则严重程度
analyzer:
errors:
invalid_assignment: warning
missing_return: error
dead_code: info
参考:
Customizing static analysis | Dart
企业持续集成应用|CI/CD static code analysis
自定义静态代码分析并应用于企业级 Flutter & Dart 研发和架构中,通常需要考虑:
如何通过用户界面形式自定义静态代码分析执行过程并查看结果?
- 可视化传递参数给
flutter analyze命令。 - 根据用户操作生成 analysis_options.yaml,例如:选择 linter 规则,更改规则严重程度...
- 静态代码分析完成后向用户展示结果,例如:成功/失败,失败原因:[严重程度] 错误信息 (文件路径:起始行:列)...
- ...
Codemagic Flutter CI/CD 静态代码分析结果示例:
参考:
Static code analysis - Codemagic Docs
如何控制 CI/CD 流程执行静态代码分析?
例如:
- 默认关闭静态代码分析,需要手动开启。
- 允许前置/后置静态代码分析到其他 CI/CD 流程,例如:编译构建,自动化测试,代码合入...
- 根据静态代码分析的问题严重程度,返回执行结果控制 CI/CD 流程,例如:全局配置优先级。
- ...
深入阅读:
Flutter 如何根据静态代码分析的问题严重程度,全局配置优先级返回执行结果控制 CI/CD 流程
参考
sdk/pkg at master · dart-lang/sdk
Analysis Server API Specification
flutter/packages/flutter_tools/lib/src/commands at master · flutter/flutter
pedantic/lib at master · dart-lang/pedantic
Customizing static analysis | Dart
Static code analysis - Codemagic Docs