Flutter & Dart 静态代码分析|Flutter & Dart static code analysis

5,176 阅读7分钟

当我们搜索 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.

深入阅读:

Pedantic Dart - Dart - Medium

自定义静态代码分析|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 codeslinter 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 流程

[flutter_tools] According to AnalysisSeverity return exit code detailed proposal by includecmath · Pull Request #61589 · flutter/flutter

参考

analyzer | Dart Package

Effective Dart | Dart

sdk/pkg at master · dart-lang/sdk

Analysis Server API Specification

flutter/packages/flutter_tools/lib/src/commands at master · flutter/flutter

pedantic | Dart Package

pedantic/lib at master · dart-lang/pedantic

Pedantic Dart - Dart - Medium

Flutter SDK 默认配置文件

Linter for Dart

analyzer error codes

linter rules

Customizing static analysis | Dart

Static code analysis - Codemagic Docs