From 14be833d46f7791ed9055945233767c4a23d4a43 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:16:37 -0400 Subject: [PATCH 01/25] Add cyclomatic complexity linter configuration for camera_android_camerax --- .repo_tool_config.yaml | 3 +++ .../camera_android_camerax/analysis_options.yaml | 12 ++++++++++++ packages/camera/camera_android_camerax/pubspec.yaml | 1 + script/configs/custom_analysis.yaml | 3 +++ 4 files changed, 19 insertions(+) create mode 100644 packages/camera/camera_android_camerax/analysis_options.yaml diff --git a/.repo_tool_config.yaml b/.repo_tool_config.yaml index 7797e87a90fc..d522aa8e91a3 100644 --- a/.repo_tool_config.yaml +++ b/.repo_tool_config.yaml @@ -20,6 +20,9 @@ allowed_dependencies: # A pin can be either an exact version, or a range with an explicit, inclusive # max version, which must a version that already exists, not a future version. pinned: + # Cyclomatic complexity linter for camera_android_camerax + - dart_code_linter + # Test-only dependency, so does not impact package clients, and # has limited impact so could be easily removed if there are # ever maintenance issues in the future. diff --git a/packages/camera/camera_android_camerax/analysis_options.yaml b/packages/camera/camera_android_camerax/analysis_options.yaml new file mode 100644 index 000000000000..4d8a1f334b60 --- /dev/null +++ b/packages/camera/camera_android_camerax/analysis_options.yaml @@ -0,0 +1,12 @@ +include: ../../../analysis_options.yaml + +analyzer: + exclude: + - build/** + - android/** +# plugins: +# - dart_code_linter + +dart_code_linter: + metrics: + cyclomatic-complexity: 15 diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 5225408a8bde..07d36d18c44f 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: dev_dependencies: build_runner: ^2.2.0 + dart_code_linter: 4.0.5 dart_skills_lint: git: url: https://github.com/flutter/skills.git diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index c0c5b53c1a83..25704807071c 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -34,3 +34,6 @@ - rfw/example # Disables docs requirements, as it is test code. - web_benchmarks/testing/test_app +# Uses custom analysis to enable the cognitive complexity linter plugin. +- camera/camera_android_camerax + From f206376039479bd623972ce89602960bd57118c8 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:17:53 -0400 Subject: [PATCH 02/25] Configure CI target and script to run cyclomatic complexity linter --- .ci/scripts/analyze_camerax_complexity.sh | 11 +++++++++++ .ci/targets/analyze.yaml | 3 +++ .../camera_android_camerax/analysis_options.yaml | 5 +++++ 3 files changed, 19 insertions(+) create mode 100755 .ci/scripts/analyze_camerax_complexity.sh diff --git a/.ci/scripts/analyze_camerax_complexity.sh b/.ci/scripts/analyze_camerax_complexity.sh new file mode 100755 index 000000000000..77b5ce2ef3fa --- /dev/null +++ b/.ci/scripts/analyze_camerax_complexity.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +set -e + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +readonly REPO_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" + +cd "$REPO_DIR/packages/camera/camera_android_camerax" +flutter pub run dart_code_linter:metrics analyze lib --set-exit-on-violation-level=warning diff --git a/.ci/targets/analyze.yaml b/.ci/targets/analyze.yaml index 3d4f4716cff3..b25a69a25157 100644 --- a/.ci/targets/analyze.yaml +++ b/.ci/targets/analyze.yaml @@ -15,6 +15,9 @@ tasks: # DO NOT change the custom-analysis argument here without changing the Dart repo. # See the comment in script/configs/custom_analysis.yaml for details. args: ["analyze", "--custom-analysis=script/configs/custom_analysis.yaml"] + - name: camerax complexity check + script: .ci/scripts/analyze_camerax_complexity.sh + # Re-run analysis with path-based dependencies to ensure that publishing # the changes won't break analysis of other packages in the respository # that depend on it. diff --git a/packages/camera/camera_android_camerax/analysis_options.yaml b/packages/camera/camera_android_camerax/analysis_options.yaml index 4d8a1f334b60..b9762aeaacf7 100644 --- a/packages/camera/camera_android_camerax/analysis_options.yaml +++ b/packages/camera/camera_android_camerax/analysis_options.yaml @@ -4,6 +4,11 @@ analyzer: exclude: - build/** - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** # plugins: # - dart_code_linter From 0299ea1d0311328cef8ebb35dda2c4a9357e1a17 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:29:15 -0400 Subject: [PATCH 03/25] Integrate cyclomatic complexity check directly into flutter_plugin_tools analyze command --- .ci/scripts/analyze_camerax_complexity.sh | 11 --------- .ci/targets/analyze.yaml | 3 --- .../camera_android_camerax/pubspec.yaml | 2 +- script/tool/lib/src/analyze_command.dart | 23 +++++++++++++++++++ 4 files changed, 24 insertions(+), 15 deletions(-) delete mode 100755 .ci/scripts/analyze_camerax_complexity.sh diff --git a/.ci/scripts/analyze_camerax_complexity.sh b/.ci/scripts/analyze_camerax_complexity.sh deleted file mode 100755 index 77b5ce2ef3fa..000000000000 --- a/.ci/scripts/analyze_camerax_complexity.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Copyright 2013 The Flutter Authors -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. -set -e - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -readonly REPO_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")" - -cd "$REPO_DIR/packages/camera/camera_android_camerax" -flutter pub run dart_code_linter:metrics analyze lib --set-exit-on-violation-level=warning diff --git a/.ci/targets/analyze.yaml b/.ci/targets/analyze.yaml index b25a69a25157..3d4f4716cff3 100644 --- a/.ci/targets/analyze.yaml +++ b/.ci/targets/analyze.yaml @@ -15,9 +15,6 @@ tasks: # DO NOT change the custom-analysis argument here without changing the Dart repo. # See the comment in script/configs/custom_analysis.yaml for details. args: ["analyze", "--custom-analysis=script/configs/custom_analysis.yaml"] - - name: camerax complexity check - script: .ci/scripts/analyze_camerax_complexity.sh - # Re-run analysis with path-based dependencies to ensure that publishing # the changes won't break analysis of other packages in the respository # that depend on it. diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 07d36d18c44f..3d830c9fc84e 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: dev_dependencies: build_runner: ^2.2.0 - dart_code_linter: 4.0.5 + dart_code_linter: 4.1.5 dart_skills_lint: git: url: https://github.com/flutter/skills.git diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 5fe1ee9e0929..bcfc0362bc26 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -313,6 +313,29 @@ class AnalyzeCommand extends PackageLoopingCommand { if (exitCode != 0) { return PackageResult.fail(); } + + final Pubspec pubspec = package.parsePubspec(); + final bool hasLinter = pubspec.devDependencies.containsKey('dart_code_linter') || + pubspec.dependencies.containsKey('dart_code_linter'); + if (hasLinter) { + print('Running dart_code_linter:metrics analysis...'); + final int linterExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'pub', + 'run', + 'dart_code_linter:metrics', + 'analyze', + 'lib', + '--set-exit-on-violation-level=warning', + ], + workingDir: package.directory, + ); + if (linterExitCode != 0) { + return PackageResult.fail(['Metrics violations found (e.g. cyclomatic complexity).']); + } + } + return PackageResult.success(); } From 30757fa86b4052dcec4ff42c7b129dfc63fe8ffb Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:30:31 -0400 Subject: [PATCH 04/25] Remove commented-out plugin lines from analysis_options.yaml --- packages/camera/camera_android_camerax/analysis_options.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/camera/camera_android_camerax/analysis_options.yaml b/packages/camera/camera_android_camerax/analysis_options.yaml index b9762aeaacf7..fcd0618acc89 100644 --- a/packages/camera/camera_android_camerax/analysis_options.yaml +++ b/packages/camera/camera_android_camerax/analysis_options.yaml @@ -9,8 +9,7 @@ analyzer: - windows/** - macos/** - linux/** -# plugins: -# - dart_code_linter + dart_code_linter: metrics: From fc676e26f318319a26caa53d0fe401b1c79db411 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:41:45 -0400 Subject: [PATCH 05/25] Refactor analyze_command linter check to a separate helper method and add unit tests --- .../analysis_options.yaml | 2 +- script/tool/lib/src/analyze_command.dart | 4 + script/tool/test/analyze_command_test.dart | 133 ++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/analysis_options.yaml b/packages/camera/camera_android_camerax/analysis_options.yaml index fcd0618acc89..38619134b81a 100644 --- a/packages/camera/camera_android_camerax/analysis_options.yaml +++ b/packages/camera/camera_android_camerax/analysis_options.yaml @@ -10,7 +10,7 @@ analyzer: - macos/** - linux/** - dart_code_linter: metrics: cyclomatic-complexity: 15 + diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index bcfc0362bc26..3f1e93f256d0 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -314,6 +314,10 @@ class AnalyzeCommand extends PackageLoopingCommand { return PackageResult.fail(); } + return _runDartCodeLinterForPackage(package); + } + + Future _runDartCodeLinterForPackage(RepositoryPackage package) async { final Pubspec pubspec = package.parsePubspec(); final bool hasLinter = pubspec.devDependencies.containsKey('dart_code_linter') || pubspec.dependencies.containsKey('dart_code_linter'); diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 7a45616a8325..e00712bd8298 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -442,6 +442,139 @@ void main() { }); }); + group('dart_code_linter', () { + test('runs dart_code_linter if present in dev_dependencies', () async { + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + package.pubspecFile.writeAsStringSync(''' +name: a_package +version: 0.0.1 +environment: + sdk: ">=2.14.0 <4.0.0" + flutter: ">=2.5.0" +dependencies: + flutter: + sdk: flutter +dev_dependencies: + dart_code_linter: 4.1.5 +'''); + + await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), + ProcessCall( + 'flutter', + const [ + 'pub', + 'run', + 'dart_code_linter:metrics', + 'analyze', + 'lib', + '--set-exit-on-violation-level=warning', + ], + package.path, + ), + ]), + ); + }); + + test('runs dart_code_linter if present in dependencies', () async { + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + package.pubspecFile.writeAsStringSync(''' +name: a_package +version: 0.0.1 +environment: + sdk: ">=2.14.0 <4.0.0" + flutter: ">=2.5.0" +dependencies: + flutter: + sdk: flutter + dart_code_linter: 4.1.5 +'''); + + await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), + ProcessCall( + 'flutter', + const [ + 'pub', + 'run', + 'dart_code_linter:metrics', + 'analyze', + 'lib', + '--set-exit-on-violation-level=warning', + ], + package.path, + ), + ]), + ); + }); + + test('does not run dart_code_linter if not present', () async { + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + + await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), + ]), + ); + }); + + test('fails if dart_code_linter analysis fails', () async { + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + package.pubspecFile.writeAsStringSync(''' +name: a_package +version: 0.0.1 +environment: + sdk: ">=2.14.0 <4.0.0" + flutter: ">=2.5.0" +dependencies: + flutter: + sdk: flutter +dev_dependencies: + dart_code_linter: 4.1.5 +'''); + + // Simulate linter failure. + processRunner.mockProcessesForExecutable['flutter'] = [ + FakeProcessInfo(MockProcess(exitCode: 0)), // flutter pub get + FakeProcessInfo(MockProcess(exitCode: 1)), // flutter pub run dart_code_linter:metrics... + ]; + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo(MockProcess(exitCode: 0)), // dart analyze + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['analyze'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Metrics violations found (e.g. cyclomatic complexity).'), + ]), + ); + }); + }); + test('skips if requested if "pub get" fails in the resolver', () async { final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); From e82d1113bfa9882cac5b1a52a51df4af4c1b99cb Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:42:08 -0400 Subject: [PATCH 06/25] Refactor custom linter checks into a pipeline runner list for easier extensibility --- script/tool/lib/src/analyze_command.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 3f1e93f256d0..cda9b4766472 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -314,7 +314,18 @@ class AnalyzeCommand extends PackageLoopingCommand { return PackageResult.fail(); } - return _runDartCodeLinterForPackage(package); + final List Function(RepositoryPackage)> customCheckRunners = [ + _runDartCodeLinterForPackage, + ]; + + for (final runner in customCheckRunners) { + final PackageResult result = await runner(package); + if (result.state == RunState.failed) { + return result; + } + } + + return PackageResult.success(); } Future _runDartCodeLinterForPackage(RepositoryPackage package) async { From 5e798fe5c14d0b09cf82c076c606eb36ce5e5a44 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:43:35 -0400 Subject: [PATCH 07/25] Clean up custom linter API boundaries by extracting dependency checking to the runner loop --- script/tool/lib/src/analyze_command.dart | 63 ++++++++++++++---------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index cda9b4766472..40e0065e8c9c 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -314,14 +314,22 @@ class AnalyzeCommand extends PackageLoopingCommand { return PackageResult.fail(); } - final List Function(RepositoryPackage)> customCheckRunners = [ - _runDartCodeLinterForPackage, + final List<_CustomLinter> customCheckRunners = [ + _CustomLinter( + dependencyName: 'dart_code_linter', + run: _runDartCodeLinterForPackage, + ), ]; + final Pubspec pubspec = package.parsePubspec(); for (final runner in customCheckRunners) { - final PackageResult result = await runner(package); - if (result.state == RunState.failed) { - return result; + final bool hasDependency = pubspec.devDependencies.containsKey(runner.dependencyName) || + pubspec.dependencies.containsKey(runner.dependencyName); + if (hasDependency) { + final PackageResult result = await runner.run(package); + if (result.state == RunState.failed) { + return result; + } } } @@ -329,26 +337,21 @@ class AnalyzeCommand extends PackageLoopingCommand { } Future _runDartCodeLinterForPackage(RepositoryPackage package) async { - final Pubspec pubspec = package.parsePubspec(); - final bool hasLinter = pubspec.devDependencies.containsKey('dart_code_linter') || - pubspec.dependencies.containsKey('dart_code_linter'); - if (hasLinter) { - print('Running dart_code_linter:metrics analysis...'); - final int linterExitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'pub', - 'run', - 'dart_code_linter:metrics', - 'analyze', - 'lib', - '--set-exit-on-violation-level=warning', - ], - workingDir: package.directory, - ); - if (linterExitCode != 0) { - return PackageResult.fail(['Metrics violations found (e.g. cyclomatic complexity).']); - } + print('Running dart_code_linter:metrics analysis...'); + final int linterExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'pub', + 'run', + 'dart_code_linter:metrics', + 'analyze', + 'lib', + '--set-exit-on-violation-level=warning', + ], + workingDir: package.directory, + ); + if (linterExitCode != 0) { + return PackageResult.fail(['Metrics violations found (e.g. cyclomatic complexity).']); } return PackageResult.success(); @@ -475,3 +478,13 @@ class AnalyzeCommand extends PackageLoopingCommand { return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); } } + +class _CustomLinter { + const _CustomLinter({ + required this.dependencyName, + required this.run, + }); + + final String dependencyName; + final Future Function(RepositoryPackage) run; +} From 8b336a2df15cfc271436bc68ee983bb43fb7e245 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:46:06 -0400 Subject: [PATCH 08/25] Clean up static analysis warnings in analyze_command and analyze_command_test --- script/tool/lib/src/analyze_command.dart | 2 +- script/tool/test/analyze_command_test.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 40e0065e8c9c..c5a09d08ca55 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -314,7 +314,7 @@ class AnalyzeCommand extends PackageLoopingCommand { return PackageResult.fail(); } - final List<_CustomLinter> customCheckRunners = [ + final customCheckRunners = <_CustomLinter>[ _CustomLinter( dependencyName: 'dart_code_linter', run: _runDartCodeLinterForPackage, diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index e00712bd8298..a9b8b32f24d3 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -549,11 +549,11 @@ dev_dependencies: // Simulate linter failure. processRunner.mockProcessesForExecutable['flutter'] = [ - FakeProcessInfo(MockProcess(exitCode: 0)), // flutter pub get + FakeProcessInfo(MockProcess()), // flutter pub get FakeProcessInfo(MockProcess(exitCode: 1)), // flutter pub run dart_code_linter:metrics... ]; processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo(MockProcess(exitCode: 0)), // dart analyze + FakeProcessInfo(MockProcess()), // dart analyze ]; Error? commandError; From 2f96c6b0bde62f6035f28e190a73412d9d542249 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:48:11 -0400 Subject: [PATCH 09/25] Restrict custom linter checks to dev_dependencies only --- script/tool/lib/src/analyze_command.dart | 3 +-- script/tool/test/analyze_command_test.dart | 14 +------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index c5a09d08ca55..21079f0902e0 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -323,8 +323,7 @@ class AnalyzeCommand extends PackageLoopingCommand { final Pubspec pubspec = package.parsePubspec(); for (final runner in customCheckRunners) { - final bool hasDependency = pubspec.devDependencies.containsKey(runner.dependencyName) || - pubspec.dependencies.containsKey(runner.dependencyName); + final bool hasDependency = pubspec.devDependencies.containsKey(runner.dependencyName); if (hasDependency) { final PackageResult result = await runner.run(package); if (result.state == RunState.failed) { diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index a9b8b32f24d3..8985acdda605 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -481,7 +481,7 @@ dev_dependencies: ); }); - test('runs dart_code_linter if present in dependencies', () async { + test('does not run dart_code_linter if present in dependencies', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); package.pubspecFile.writeAsStringSync(''' name: a_package @@ -502,18 +502,6 @@ dependencies: orderedEquals([ ProcessCall('flutter', const ['pub', 'get'], package.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), - ProcessCall( - 'flutter', - const [ - 'pub', - 'run', - 'dart_code_linter:metrics', - 'analyze', - 'lib', - '--set-exit-on-violation-level=warning', - ], - package.path, - ), ]), ); }); From 84abbf005a8b1300f15c2b3fe5ad458d0525061e Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:52:18 -0400 Subject: [PATCH 10/25] Use expectedInitialArgs to explicitly document mocks in linter tests --- script/tool/test/analyze_command_test.dart | 40 +++++++--------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 8985acdda605..810cffc1b70f 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -481,31 +481,6 @@ dev_dependencies: ); }); - test('does not run dart_code_linter if present in dependencies', () async { - final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); - package.pubspecFile.writeAsStringSync(''' -name: a_package -version: 0.0.1 -environment: - sdk: ">=2.14.0 <4.0.0" - flutter: ">=2.5.0" -dependencies: - flutter: - sdk: flutter - dart_code_linter: 4.1.5 -'''); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], package.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), - ]), - ); - }); - test('does not run dart_code_linter if not present', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); @@ -537,11 +512,20 @@ dev_dependencies: // Simulate linter failure. processRunner.mockProcessesForExecutable['flutter'] = [ - FakeProcessInfo(MockProcess()), // flutter pub get - FakeProcessInfo(MockProcess(exitCode: 1)), // flutter pub run dart_code_linter:metrics... + FakeProcessInfo( + MockProcess(), + ['pub', 'get'], + ), + FakeProcessInfo( + MockProcess(exitCode: 1), + ['pub', 'run', 'dart_code_linter:metrics'], + ), ]; processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo(MockProcess()), // dart analyze + FakeProcessInfo( + MockProcess(), + ['analyze'], + ), ]; Error? commandError; From eb76af7e39d2e412142d96c92bcb4af1a32a8755 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:53:29 -0400 Subject: [PATCH 11/25] Improve tests by explicitly mocking process queues and verifying console output for all scenarios --- script/tool/test/analyze_command_test.dart | 78 +++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 810cffc1b70f..625ec9906fcc 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -458,7 +458,25 @@ dev_dependencies: dart_code_linter: 4.1.5 '''); - await runCapturingPrint(runner, ['analyze']); + // Explicitly mock all calls. + processRunner.mockProcessesForExecutable['flutter'] = [ + FakeProcessInfo( + MockProcess(), + ['pub', 'get'], + ), + FakeProcessInfo( + MockProcess(), + ['pub', 'run', 'dart_code_linter:metrics'], + ), + ]; + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo( + MockProcess(), + ['analyze'], + ), + ]; + + final List output = await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, @@ -479,12 +497,67 @@ dev_dependencies: ), ]), ); + expect(output, contains('Running dart_code_linter:metrics analysis...')); + }); + + test('does not run dart_code_linter if present in dependencies', () async { + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + package.pubspecFile.writeAsStringSync(''' +name: a_package +version: 0.0.1 +environment: + sdk: ">=2.14.0 <4.0.0" + flutter: ">=2.5.0" +dependencies: + flutter: + sdk: flutter + dart_code_linter: 4.1.5 +'''); + + // Explicitly mock all calls. + processRunner.mockProcessesForExecutable['flutter'] = [ + FakeProcessInfo( + MockProcess(), + ['pub', 'get'], + ), + ]; + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo( + MockProcess(), + ['analyze'], + ), + ]; + + final List output = await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), + ]), + ); + expect(output, isNot(contains('Running dart_code_linter:metrics analysis...'))); }); test('does not run dart_code_linter if not present', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); - await runCapturingPrint(runner, ['analyze']); + // Explicitly mock all calls. + processRunner.mockProcessesForExecutable['flutter'] = [ + FakeProcessInfo( + MockProcess(), + ['pub', 'get'], + ), + ]; + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo( + MockProcess(), + ['analyze'], + ), + ]; + + final List output = await runCapturingPrint(runner, ['analyze']); expect( processRunner.recordedCalls, @@ -493,6 +566,7 @@ dev_dependencies: ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), ]), ); + expect(output, isNot(contains('Running dart_code_linter:metrics analysis...'))); }); test('fails if dart_code_linter analysis fails', () async { From d93d538e94acaabebd176606ffbf8224060f8f98 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:54:13 -0400 Subject: [PATCH 12/25] De-duplicate pubspec writing and mock process logic in linter tests using helper functions --- script/tool/test/analyze_command_test.dart | 134 ++++++++------------- 1 file changed, 47 insertions(+), 87 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 625ec9906fcc..d4346930379e 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -445,36 +445,14 @@ void main() { group('dart_code_linter', () { test('runs dart_code_linter if present in dev_dependencies', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); - package.pubspecFile.writeAsStringSync(''' -name: a_package -version: 0.0.1 -environment: - sdk: ">=2.14.0 <4.0.0" - flutter: ">=2.5.0" -dependencies: - flutter: - sdk: flutter -dev_dependencies: - dart_code_linter: 4.1.5 -'''); + _writeFakePubspecWithLinter(package, inDevDependencies: true); - // Explicitly mock all calls. - processRunner.mockProcessesForExecutable['flutter'] = [ - FakeProcessInfo( - MockProcess(), - ['pub', 'get'], - ), + _mockStandardFlutterAndDartProcesses(processRunner, extraFlutterCalls: [ FakeProcessInfo( MockProcess(), ['pub', 'run', 'dart_code_linter:metrics'], ), - ]; - processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo( - MockProcess(), - ['analyze'], - ), - ]; + ]); final List output = await runCapturingPrint(runner, ['analyze']); @@ -502,31 +480,9 @@ dev_dependencies: test('does not run dart_code_linter if present in dependencies', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); - package.pubspecFile.writeAsStringSync(''' -name: a_package -version: 0.0.1 -environment: - sdk: ">=2.14.0 <4.0.0" - flutter: ">=2.5.0" -dependencies: - flutter: - sdk: flutter - dart_code_linter: 4.1.5 -'''); + _writeFakePubspecWithLinter(package, inDependencies: true); - // Explicitly mock all calls. - processRunner.mockProcessesForExecutable['flutter'] = [ - FakeProcessInfo( - MockProcess(), - ['pub', 'get'], - ), - ]; - processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo( - MockProcess(), - ['analyze'], - ), - ]; + _mockStandardFlutterAndDartProcesses(processRunner); final List output = await runCapturingPrint(runner, ['analyze']); @@ -543,19 +499,7 @@ dependencies: test('does not run dart_code_linter if not present', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); - // Explicitly mock all calls. - processRunner.mockProcessesForExecutable['flutter'] = [ - FakeProcessInfo( - MockProcess(), - ['pub', 'get'], - ), - ]; - processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo( - MockProcess(), - ['analyze'], - ), - ]; + _mockStandardFlutterAndDartProcesses(processRunner); final List output = await runCapturingPrint(runner, ['analyze']); @@ -571,36 +515,14 @@ dependencies: test('fails if dart_code_linter analysis fails', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); - package.pubspecFile.writeAsStringSync(''' -name: a_package -version: 0.0.1 -environment: - sdk: ">=2.14.0 <4.0.0" - flutter: ">=2.5.0" -dependencies: - flutter: - sdk: flutter -dev_dependencies: - dart_code_linter: 4.1.5 -'''); + _writeFakePubspecWithLinter(package, inDevDependencies: true); - // Simulate linter failure. - processRunner.mockProcessesForExecutable['flutter'] = [ - FakeProcessInfo( - MockProcess(), - ['pub', 'get'], - ), + _mockStandardFlutterAndDartProcesses(processRunner, extraFlutterCalls: [ FakeProcessInfo( MockProcess(exitCode: 1), ['pub', 'run', 'dart_code_linter:metrics'], ), - ]; - processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo( - MockProcess(), - ['analyze'], - ), - ]; + ]); Error? commandError; final List output = await runCapturingPrint( @@ -1705,3 +1627,41 @@ packages/package_a/lib/foo.dart }); }); } + +void _writeFakePubspecWithLinter( + RepositoryPackage package, { + bool inDevDependencies = false, + bool inDependencies = false, +}) { + package.pubspecFile.writeAsStringSync(''' +name: ${package.directory.basename} +version: 0.0.1 +environment: + sdk: ">=2.14.0 <4.0.0" + flutter: ">=2.5.0" +dependencies: + flutter: + sdk: flutter +${inDependencies ? ' dart_code_linter: 4.1.5' : ''} +${inDevDependencies ? 'dev_dependencies:\n dart_code_linter: 4.1.5' : ''} +'''); +} + +void _mockStandardFlutterAndDartProcesses( + RecordingProcessRunner processRunner, { + List extraFlutterCalls = const [], +}) { + processRunner.mockProcessesForExecutable['flutter'] = [ + FakeProcessInfo( + MockProcess(), + const ['pub', 'get'], + ), + ...extraFlutterCalls, + ]; + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo( + MockProcess(), + const ['analyze'], + ), + ]; +} From dd58a0d21965ab9ef8fb2c3fb5edfb753ce54b9b Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 14:57:34 -0400 Subject: [PATCH 13/25] Ensure linter skip tests are robust against print string changes by asserting absence of case-insensitive keywords --- script/tool/test/analyze_command_test.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index d4346930379e..654fdd0a4159 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -493,7 +493,9 @@ void main() { ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), ]), ); - expect(output, isNot(contains('Running dart_code_linter:metrics analysis...'))); + final String combinedOutput = output.join('\n').toLowerCase(); + expect(combinedOutput, isNot(contains('dart_code_linter'))); + expect(combinedOutput, isNot(contains('metrics'))); }); test('does not run dart_code_linter if not present', () async { @@ -510,7 +512,9 @@ void main() { ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), ]), ); - expect(output, isNot(contains('Running dart_code_linter:metrics analysis...'))); + final String combinedOutput = output.join('\n').toLowerCase(); + expect(combinedOutput, isNot(contains('dart_code_linter'))); + expect(combinedOutput, isNot(contains('metrics'))); }); test('fails if dart_code_linter analysis fails', () async { From 214f051e419a7c69cf0a2b674810b2378926980f Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 15:01:58 -0400 Subject: [PATCH 14/25] Add descriptive doc comments and dynamic Dart/Flutter SDK resolution to linter checks --- script/tool/lib/src/analyze_command.dart | 20 +++++++-- script/tool/test/analyze_command_test.dart | 50 ++++++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 21079f0902e0..e854fac94343 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -335,12 +335,17 @@ class AnalyzeCommand extends PackageLoopingCommand { return PackageResult.success(); } + /// Runs the `dart_code_linter` metrics analyzer on the package. + /// + /// Assumes `dart_code_linter` is present in `dev_dependencies`. Future _runDartCodeLinterForPackage(RepositoryPackage package) async { print('Running dart_code_linter:metrics analysis...'); + final bool isFlutter = package.requiresFlutter(); + final String sdkCommand = isFlutter ? flutterCommand : _dartBinaryPath; final int linterExitCode = await processRunner.runAndStream( - flutterCommand, + sdkCommand, [ - 'pub', + if (isFlutter) 'pub', 'run', 'dart_code_linter:metrics', 'analyze', @@ -350,7 +355,9 @@ class AnalyzeCommand extends PackageLoopingCommand { workingDir: package.directory, ); if (linterExitCode != 0) { - return PackageResult.fail(['Metrics violations found (e.g. cyclomatic complexity).']); + return PackageResult.fail([ + 'Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.' + ]); } return PackageResult.success(); @@ -478,12 +485,19 @@ class AnalyzeCommand extends PackageLoopingCommand { } } +/// Represents a custom linter check that is executed during package analysis. class _CustomLinter { const _CustomLinter({ required this.dependencyName, required this.run, }); + /// The name of the package dependency that triggers this custom check. + /// + /// The check is only executed if this dependency is listed in the package's + /// `dev_dependencies`. final String dependencyName; + + /// The runner function that executes the custom check. final Future Function(RepositoryPackage) run; } diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 654fdd0a4159..8a61c074773b 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -517,6 +517,48 @@ void main() { expect(combinedOutput, isNot(contains('metrics'))); }); + test('runs dart_code_linter using dart for pure Dart packages', () async { + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: false); + _writeFakePubspecWithLinter(package, inDevDependencies: true, includeFlutter: false); + + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo( + MockProcess(), + ['pub', 'get'], + ), + FakeProcessInfo( + MockProcess(), + ['analyze'], + ), + FakeProcessInfo( + MockProcess(), + ['run', 'dart_code_linter:metrics'], + ), + ]; + + final List output = await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('dart', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), + ProcessCall( + 'dart', + const [ + 'run', + 'dart_code_linter:metrics', + 'analyze', + 'lib', + '--set-exit-on-violation-level=warning', + ], + package.path, + ), + ]), + ); + expect(output, contains('Running dart_code_linter:metrics analysis...')); + }); + test('fails if dart_code_linter analysis fails', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); _writeFakePubspecWithLinter(package, inDevDependencies: true); @@ -541,7 +583,7 @@ void main() { expect( output, containsAllInOrder([ - contains('Metrics violations found (e.g. cyclomatic complexity).'), + contains('Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.'), ]), ); }); @@ -1636,16 +1678,16 @@ void _writeFakePubspecWithLinter( RepositoryPackage package, { bool inDevDependencies = false, bool inDependencies = false, + bool includeFlutter = true, }) { package.pubspecFile.writeAsStringSync(''' name: ${package.directory.basename} version: 0.0.1 environment: sdk: ">=2.14.0 <4.0.0" - flutter: ">=2.5.0" + ${includeFlutter ? 'flutter: ">=2.5.0"' : ''} dependencies: - flutter: - sdk: flutter + ${includeFlutter ? 'flutter:\n sdk: flutter' : ''} ${inDependencies ? ' dart_code_linter: 4.1.5' : ''} ${inDevDependencies ? 'dev_dependencies:\n dart_code_linter: 4.1.5' : ''} '''); From 71559a97e62ff6052462a7b213f1b3719ce875ea Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 15:14:33 -0400 Subject: [PATCH 15/25] Update custom_analysis.yaml comment to say cyclomatic complexity --- script/configs/custom_analysis.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml index 25704807071c..b4539c78a0e1 100644 --- a/script/configs/custom_analysis.yaml +++ b/script/configs/custom_analysis.yaml @@ -34,6 +34,6 @@ - rfw/example # Disables docs requirements, as it is test code. - web_benchmarks/testing/test_app -# Uses custom analysis to enable the cognitive complexity linter plugin. +# Uses custom analysis to enable the cyclomatic complexity linter plugin. - camera/camera_android_camerax From 24d9e8aefd3963a9d230e3b04446db6ca73c5c6b Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 15:28:06 -0400 Subject: [PATCH 16/25] Simplify custom linter checks to always execute via dart run --- script/tool/lib/src/analyze_command.dart | 5 +---- script/tool/test/analyze_command_test.dart | 13 +++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index e854fac94343..3001b2a30544 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -340,12 +340,9 @@ class AnalyzeCommand extends PackageLoopingCommand { /// Assumes `dart_code_linter` is present in `dev_dependencies`. Future _runDartCodeLinterForPackage(RepositoryPackage package) async { print('Running dart_code_linter:metrics analysis...'); - final bool isFlutter = package.requiresFlutter(); - final String sdkCommand = isFlutter ? flutterCommand : _dartBinaryPath; final int linterExitCode = await processRunner.runAndStream( - sdkCommand, + _dartBinaryPath, [ - if (isFlutter) 'pub', 'run', 'dart_code_linter:metrics', 'analyze', diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 8a61c074773b..7428dbd653d9 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -447,10 +447,10 @@ void main() { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); _writeFakePubspecWithLinter(package, inDevDependencies: true); - _mockStandardFlutterAndDartProcesses(processRunner, extraFlutterCalls: [ + _mockStandardFlutterAndDartProcesses(processRunner, extraDartCalls: [ FakeProcessInfo( MockProcess(), - ['pub', 'run', 'dart_code_linter:metrics'], + ['run', 'dart_code_linter:metrics'], ), ]); @@ -462,9 +462,8 @@ void main() { ProcessCall('flutter', const ['pub', 'get'], package.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), ProcessCall( - 'flutter', + 'dart', const [ - 'pub', 'run', 'dart_code_linter:metrics', 'analyze', @@ -563,10 +562,10 @@ void main() { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); _writeFakePubspecWithLinter(package, inDevDependencies: true); - _mockStandardFlutterAndDartProcesses(processRunner, extraFlutterCalls: [ + _mockStandardFlutterAndDartProcesses(processRunner, extraDartCalls: [ FakeProcessInfo( MockProcess(exitCode: 1), - ['pub', 'run', 'dart_code_linter:metrics'], + ['run', 'dart_code_linter:metrics'], ), ]); @@ -1696,6 +1695,7 @@ ${inDevDependencies ? 'dev_dependencies:\n dart_code_linter: 4.1.5' : ''} void _mockStandardFlutterAndDartProcesses( RecordingProcessRunner processRunner, { List extraFlutterCalls = const [], + List extraDartCalls = const [], }) { processRunner.mockProcessesForExecutable['flutter'] = [ FakeProcessInfo( @@ -1709,5 +1709,6 @@ void _mockStandardFlutterAndDartProcesses( MockProcess(), const ['analyze'], ), + ...extraDartCalls, ]; } From ea9770a2b3a52a5e8313c8db47bf14ebc1a164e9 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Thu, 25 Jun 2026 15:35:39 -0400 Subject: [PATCH 17/25] Add defensive check and tests to ensure linter does not run on packages lacking a lib/ directory --- script/tool/lib/src/analyze_command.dart | 3 +++ script/tool/test/analyze_command_test.dart | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 3001b2a30544..487df32f9e02 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -339,6 +339,9 @@ class AnalyzeCommand extends PackageLoopingCommand { /// /// Assumes `dart_code_linter` is present in `dev_dependencies`. Future _runDartCodeLinterForPackage(RepositoryPackage package) async { + if (!package.libDirectory.existsSync()) { + return PackageResult.success(); + } print('Running dart_code_linter:metrics analysis...'); final int linterExitCode = await processRunner.runAndStream( _dartBinaryPath, diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 7428dbd653d9..9895aada2fbe 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -557,6 +557,26 @@ void main() { ); expect(output, contains('Running dart_code_linter:metrics analysis...')); }); + test('skips dart_code_linter if lib/ does not exist', () async { + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + _writeFakePubspecWithLinter(package, inDevDependencies: true); + package.libDirectory.deleteSync(recursive: true); + + _mockStandardFlutterAndDartProcesses(processRunner); + + final List output = await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), + ]), + ); + final String combinedOutput = output.join('\n').toLowerCase(); + expect(combinedOutput, isNot(contains('dart_code_linter'))); + expect(combinedOutput, isNot(contains('metrics'))); + }); test('fails if dart_code_linter analysis fails', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); From e9bdaeb30104bdec74a0e6f7a48fa1107fbe8119 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Tue, 30 Jun 2026 17:38:55 -0400 Subject: [PATCH 18/25] Address review feedback: rename mock helper and report cyclomatic complexity threshold on failure --- script/tool/lib/src/analyze_command.dart | 34 +++++++++++++++- script/tool/test/analyze_command_test.dart | 46 +++++++++++++++++++--- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 487df32f9e02..d1b4fff29a9f 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -16,6 +16,7 @@ import 'common/plugin_utils.dart'; import 'common/pub_utils.dart'; import 'common/repository_package.dart'; import 'common/xcode.dart'; +import 'package:yaml/yaml.dart'; /// A command to run Dart analysis on packages. class AnalyzeCommand extends PackageLoopingCommand { @@ -335,6 +336,33 @@ class AnalyzeCommand extends PackageLoopingCommand { return PackageResult.success(); } + /// Retrieves the configured cyclomatic complexity threshold from the local + /// `analysis_options.yaml` if it exists and is configured. + int? _getLinterThreshold(RepositoryPackage package) { + final File optionsFile = package.directory.childFile('analysis_options.yaml'); + if (!optionsFile.existsSync()) { + return null; + } + try { + final Object? yaml = loadYaml(optionsFile.readAsStringSync()); + if (yaml is YamlMap) { + final Object? linter = yaml['dart_code_linter']; + if (linter is YamlMap) { + final Object? metrics = linter['metrics']; + if (metrics is YamlMap) { + final Object? complexity = metrics['cyclomatic-complexity']; + if (complexity is int) { + return complexity; + } + } + } + } + } catch (_) { + // Ignore errors parsing invalid/incomplete files. + } + return null; + } + /// Runs the `dart_code_linter` metrics analyzer on the package. /// /// Assumes `dart_code_linter` is present in `dev_dependencies`. @@ -355,8 +383,12 @@ class AnalyzeCommand extends PackageLoopingCommand { workingDir: package.directory, ); if (linterExitCode != 0) { + final int? threshold = _getLinterThreshold(package); + final String thresholdMessage = threshold != null + ? ' (configured threshold: $threshold)' + : ''; return PackageResult.fail([ - 'Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.' + 'Metrics violations found$thresholdMessage. See the package\'s local "analysis_options.yaml" for configured thresholds.' ]); } diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 9895aada2fbe..95024f129887 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -447,7 +447,7 @@ void main() { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); _writeFakePubspecWithLinter(package, inDevDependencies: true); - _mockStandardFlutterAndDartProcesses(processRunner, extraDartCalls: [ + _mockCallsForFlutterAnalyze(processRunner, extraDartCalls: [ FakeProcessInfo( MockProcess(), ['run', 'dart_code_linter:metrics'], @@ -481,7 +481,7 @@ void main() { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); _writeFakePubspecWithLinter(package, inDependencies: true); - _mockStandardFlutterAndDartProcesses(processRunner); + _mockCallsForFlutterAnalyze(processRunner); final List output = await runCapturingPrint(runner, ['analyze']); @@ -500,7 +500,7 @@ void main() { test('does not run dart_code_linter if not present', () async { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); - _mockStandardFlutterAndDartProcesses(processRunner); + _mockCallsForFlutterAnalyze(processRunner); final List output = await runCapturingPrint(runner, ['analyze']); @@ -562,7 +562,7 @@ void main() { _writeFakePubspecWithLinter(package, inDevDependencies: true); package.libDirectory.deleteSync(recursive: true); - _mockStandardFlutterAndDartProcesses(processRunner); + _mockCallsForFlutterAnalyze(processRunner); final List output = await runCapturingPrint(runner, ['analyze']); @@ -582,7 +582,7 @@ void main() { final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); _writeFakePubspecWithLinter(package, inDevDependencies: true); - _mockStandardFlutterAndDartProcesses(processRunner, extraDartCalls: [ + _mockCallsForFlutterAnalyze(processRunner, extraDartCalls: [ FakeProcessInfo( MockProcess(exitCode: 1), ['run', 'dart_code_linter:metrics'], @@ -606,6 +606,40 @@ void main() { ]), ); }); + + test('fails with threshold if analysis_options.yaml defines one', () async { + final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + _writeFakePubspecWithLinter(package, inDevDependencies: true); + package.directory.childFile('analysis_options.yaml').writeAsStringSync(''' +dart_code_linter: + metrics: + cyclomatic-complexity: 15 +'''); + + _mockCallsForFlutterAnalyze(processRunner, extraDartCalls: [ + FakeProcessInfo( + MockProcess(exitCode: 1), + ['run', 'dart_code_linter:metrics'], + ), + ]); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['analyze', '--custom-analysis', 'a_package'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Metrics violations found (configured threshold: 15). See the package\'s local "analysis_options.yaml" for configured thresholds.'), + ]), + ); + }); }); test('skips if requested if "pub get" fails in the resolver', () async { @@ -1712,7 +1746,7 @@ ${inDevDependencies ? 'dev_dependencies:\n dart_code_linter: 4.1.5' : ''} '''); } -void _mockStandardFlutterAndDartProcesses( +void _mockCallsForFlutterAnalyze( RecordingProcessRunner processRunner, { List extraFlutterCalls = const [], List extraDartCalls = const [], From 342eab824f863b9d46d1419972e4c31c6ab7d513 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Tue, 30 Jun 2026 17:40:06 -0400 Subject: [PATCH 19/25] Fix formatting and linter issues in analyze command changes --- script/tool/lib/src/analyze_command.dart | 36 ++---- script/tool/test/analyze_command_test.dart | 143 +++++++++++---------- 2 files changed, 86 insertions(+), 93 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index d1b4fff29a9f..90cd20d4605a 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -5,6 +5,7 @@ import 'dart:io' as io; import 'package:file/file.dart'; +import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/file_filters.dart'; @@ -16,7 +17,6 @@ import 'common/plugin_utils.dart'; import 'common/pub_utils.dart'; import 'common/repository_package.dart'; import 'common/xcode.dart'; -import 'package:yaml/yaml.dart'; /// A command to run Dart analysis on packages. class AnalyzeCommand extends PackageLoopingCommand { @@ -316,10 +316,7 @@ class AnalyzeCommand extends PackageLoopingCommand { } final customCheckRunners = <_CustomLinter>[ - _CustomLinter( - dependencyName: 'dart_code_linter', - run: _runDartCodeLinterForPackage, - ), + _CustomLinter(dependencyName: 'dart_code_linter', run: _runDartCodeLinterForPackage), ]; final Pubspec pubspec = package.parsePubspec(); @@ -371,24 +368,18 @@ class AnalyzeCommand extends PackageLoopingCommand { return PackageResult.success(); } print('Running dart_code_linter:metrics analysis...'); - final int linterExitCode = await processRunner.runAndStream( - _dartBinaryPath, - [ - 'run', - 'dart_code_linter:metrics', - 'analyze', - 'lib', - '--set-exit-on-violation-level=warning', - ], - workingDir: package.directory, - ); + final int linterExitCode = await processRunner.runAndStream(_dartBinaryPath, [ + 'run', + 'dart_code_linter:metrics', + 'analyze', + 'lib', + '--set-exit-on-violation-level=warning', + ], workingDir: package.directory); if (linterExitCode != 0) { final int? threshold = _getLinterThreshold(package); - final String thresholdMessage = threshold != null - ? ' (configured threshold: $threshold)' - : ''; + final thresholdMessage = threshold != null ? ' (configured threshold: $threshold)' : ''; return PackageResult.fail([ - 'Metrics violations found$thresholdMessage. See the package\'s local "analysis_options.yaml" for configured thresholds.' + 'Metrics violations found$thresholdMessage. See the package\'s local "analysis_options.yaml" for configured thresholds.', ]); } @@ -519,10 +510,7 @@ class AnalyzeCommand extends PackageLoopingCommand { /// Represents a custom linter check that is executed during package analysis. class _CustomLinter { - const _CustomLinter({ - required this.dependencyName, - required this.run, - }); + const _CustomLinter({required this.dependencyName, required this.run}); /// The name of the package dependency that triggers this custom check. /// diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 95024f129887..5b12887d1dfe 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -444,15 +444,19 @@ void main() { group('dart_code_linter', () { test('runs dart_code_linter if present in dev_dependencies', () async { - final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); _writeFakePubspecWithLinter(package, inDevDependencies: true); - _mockCallsForFlutterAnalyze(processRunner, extraDartCalls: [ - FakeProcessInfo( - MockProcess(), - ['run', 'dart_code_linter:metrics'], - ), - ]); + _mockCallsForFlutterAnalyze( + processRunner, + extraDartCalls: [ + FakeProcessInfo(MockProcess(), ['run', 'dart_code_linter:metrics']), + ], + ); final List output = await runCapturingPrint(runner, ['analyze']); @@ -461,24 +465,24 @@ void main() { orderedEquals([ ProcessCall('flutter', const ['pub', 'get'], package.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), - ProcessCall( - 'dart', - const [ - 'run', - 'dart_code_linter:metrics', - 'analyze', - 'lib', - '--set-exit-on-violation-level=warning', - ], - package.path, - ), + ProcessCall('dart', const [ + 'run', + 'dart_code_linter:metrics', + 'analyze', + 'lib', + '--set-exit-on-violation-level=warning', + ], package.path), ]), ); expect(output, contains('Running dart_code_linter:metrics analysis...')); }); test('does not run dart_code_linter if present in dependencies', () async { - final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); _writeFakePubspecWithLinter(package, inDependencies: true); _mockCallsForFlutterAnalyze(processRunner); @@ -498,7 +502,11 @@ void main() { }); test('does not run dart_code_linter if not present', () async { - final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); _mockCallsForFlutterAnalyze(processRunner); @@ -517,22 +525,13 @@ void main() { }); test('runs dart_code_linter using dart for pure Dart packages', () async { - final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: false); + final RepositoryPackage package = createFakePackage('a_package', packagesDir); _writeFakePubspecWithLinter(package, inDevDependencies: true, includeFlutter: false); processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo( - MockProcess(), - ['pub', 'get'], - ), - FakeProcessInfo( - MockProcess(), - ['analyze'], - ), - FakeProcessInfo( - MockProcess(), - ['run', 'dart_code_linter:metrics'], - ), + FakeProcessInfo(MockProcess(), ['pub', 'get']), + FakeProcessInfo(MockProcess(), ['analyze']), + FakeProcessInfo(MockProcess(), ['run', 'dart_code_linter:metrics']), ]; final List output = await runCapturingPrint(runner, ['analyze']); @@ -542,23 +541,23 @@ void main() { orderedEquals([ ProcessCall('dart', const ['pub', 'get'], package.path), ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), - ProcessCall( - 'dart', - const [ - 'run', - 'dart_code_linter:metrics', - 'analyze', - 'lib', - '--set-exit-on-violation-level=warning', - ], - package.path, - ), + ProcessCall('dart', const [ + 'run', + 'dart_code_linter:metrics', + 'analyze', + 'lib', + '--set-exit-on-violation-level=warning', + ], package.path), ]), ); expect(output, contains('Running dart_code_linter:metrics analysis...')); }); test('skips dart_code_linter if lib/ does not exist', () async { - final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); _writeFakePubspecWithLinter(package, inDevDependencies: true); package.libDirectory.deleteSync(recursive: true); @@ -579,15 +578,19 @@ void main() { }); test('fails if dart_code_linter analysis fails', () async { - final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); _writeFakePubspecWithLinter(package, inDevDependencies: true); - _mockCallsForFlutterAnalyze(processRunner, extraDartCalls: [ - FakeProcessInfo( - MockProcess(exitCode: 1), - ['run', 'dart_code_linter:metrics'], - ), - ]); + _mockCallsForFlutterAnalyze( + processRunner, + extraDartCalls: [ + FakeProcessInfo(MockProcess(exitCode: 1), ['run', 'dart_code_linter:metrics']), + ], + ); Error? commandError; final List output = await runCapturingPrint( @@ -602,13 +605,19 @@ void main() { expect( output, containsAllInOrder([ - contains('Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.'), + contains( + 'Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.', + ), ]), ); }); test('fails with threshold if analysis_options.yaml defines one', () async { - final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true); + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); _writeFakePubspecWithLinter(package, inDevDependencies: true); package.directory.childFile('analysis_options.yaml').writeAsStringSync(''' dart_code_linter: @@ -616,12 +625,12 @@ dart_code_linter: cyclomatic-complexity: 15 '''); - _mockCallsForFlutterAnalyze(processRunner, extraDartCalls: [ - FakeProcessInfo( - MockProcess(exitCode: 1), - ['run', 'dart_code_linter:metrics'], - ), - ]); + _mockCallsForFlutterAnalyze( + processRunner, + extraDartCalls: [ + FakeProcessInfo(MockProcess(exitCode: 1), ['run', 'dart_code_linter:metrics']), + ], + ); Error? commandError; final List output = await runCapturingPrint( @@ -636,7 +645,9 @@ dart_code_linter: expect( output, containsAllInOrder([ - contains('Metrics violations found (configured threshold: 15). See the package\'s local "analysis_options.yaml" for configured thresholds.'), + contains( + 'Metrics violations found (configured threshold: 15). See the package\'s local "analysis_options.yaml" for configured thresholds.', + ), ]), ); }); @@ -1752,17 +1763,11 @@ void _mockCallsForFlutterAnalyze( List extraDartCalls = const [], }) { processRunner.mockProcessesForExecutable['flutter'] = [ - FakeProcessInfo( - MockProcess(), - const ['pub', 'get'], - ), + FakeProcessInfo(MockProcess(), const ['pub', 'get']), ...extraFlutterCalls, ]; processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo( - MockProcess(), - const ['analyze'], - ), + FakeProcessInfo(MockProcess(), const ['analyze']), ...extraDartCalls, ]; } From 64a8a9035a92f1b87922e8dee082f5fa6fcee037 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 1 Jul 2026 12:01:47 -0400 Subject: [PATCH 20/25] Refactor custom linter checking to aggregate errors instead of failing early --- script/tool/lib/src/analyze_command.dart | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 90cd20d4605a..18a8a5ca58d6 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -320,17 +320,15 @@ class AnalyzeCommand extends PackageLoopingCommand { ]; final Pubspec pubspec = package.parsePubspec(); + final errors = []; for (final runner in customCheckRunners) { final bool hasDependency = pubspec.devDependencies.containsKey(runner.dependencyName); if (hasDependency) { - final PackageResult result = await runner.run(package); - if (result.state == RunState.failed) { - return result; - } + errors.addAll(await runner.run(package)); } } - return PackageResult.success(); + return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors); } /// Retrieves the configured cyclomatic complexity threshold from the local @@ -363,9 +361,9 @@ class AnalyzeCommand extends PackageLoopingCommand { /// Runs the `dart_code_linter` metrics analyzer on the package. /// /// Assumes `dart_code_linter` is present in `dev_dependencies`. - Future _runDartCodeLinterForPackage(RepositoryPackage package) async { + Future> _runDartCodeLinterForPackage(RepositoryPackage package) async { if (!package.libDirectory.existsSync()) { - return PackageResult.success(); + return []; } print('Running dart_code_linter:metrics analysis...'); final int linterExitCode = await processRunner.runAndStream(_dartBinaryPath, [ @@ -378,12 +376,12 @@ class AnalyzeCommand extends PackageLoopingCommand { if (linterExitCode != 0) { final int? threshold = _getLinterThreshold(package); final thresholdMessage = threshold != null ? ' (configured threshold: $threshold)' : ''; - return PackageResult.fail([ + return [ 'Metrics violations found$thresholdMessage. See the package\'s local "analysis_options.yaml" for configured thresholds.', - ]); + ]; } - return PackageResult.success(); + return []; } Future _runPubCommand(RepositoryPackage package, String command) async { @@ -519,5 +517,5 @@ class _CustomLinter { final String dependencyName; /// The runner function that executes the custom check. - final Future Function(RepositoryPackage) run; + final Future> Function(RepositoryPackage) run; } From f745d822015166fb926ec2c0e0801604840aad9f Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 1 Jul 2026 12:43:39 -0400 Subject: [PATCH 21/25] Fix early return on standard analysis failure and add combined aggregation test --- script/tool/lib/src/analyze_command.dart | 4 -- script/tool/test/analyze_command_test.dart | 50 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index cf2a8fddc3e5..7d1e310918ff 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -331,10 +331,6 @@ class AnalyzeCommand extends PackageLoopingCommand { ...skillsErrors, ]; - if (errors.isNotEmpty) { - return PackageResult.fail(errors); - } - final customCheckRunners = <_CustomLinter>[ _CustomLinter(dependencyName: 'dart_code_linter', run: _runDartCodeLinterForPackage), ]; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 9c7db890b151..c54cd9e888bd 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -763,6 +763,56 @@ dart_code_linter: contains(' foo:\n Main package analysis failed\n Skills analysis failed'), ); }); + + test('fails with main, skills, and linter failures listed if all fail', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); + _writeFakePubspecWithLinter(package, inDevDependencies: true); + package.ciConfigFile.writeAsStringSync('analyze_skills: true'); + + package.directory + .childDirectory('.agents') + .childDirectory('skills') + .childFile('test.dart') + .createSync(recursive: true); + + processRunner.mockProcessesForExecutable['flutter'] = [ + FakeProcessInfo(MockProcess(), ['pub', 'get']), + ]; + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo(MockProcess(exitCode: 1), ['analyze']), // main package + FakeProcessInfo(MockProcess(exitCode: 1), ['analyze']), // skills package + FakeProcessInfo(MockProcess(exitCode: 1), [ + 'run', + 'dart_code_linter:metrics', + ]), // custom linter + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['analyze', '--custom-analysis', 'a_package'], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + final String joinedOutput = output.join('\n'); + expect(joinedOutput, contains('The following packages had errors:')); + expect( + joinedOutput, + contains( + ' a_package:\n' + ' Main package analysis failed\n' + ' Skills analysis failed\n' + ' Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.', + ), + ); + }); }); test('skips if requested if "pub get" fails in the resolver', () async { From d14f6e1d35b556e73c9e711146337b751b6bc7de Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 1 Jul 2026 12:44:14 -0400 Subject: [PATCH 22/25] Rename combined failures integration test for clarity --- script/tool/test/analyze_command_test.dart | 93 +++++++++++----------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index c54cd9e888bd..bd8fdf5f3835 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -764,55 +764,58 @@ dart_code_linter: ); }); - test('fails with main, skills, and linter failures listed if all fail', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', - packagesDir, - isFlutter: true, - ); - _writeFakePubspecWithLinter(package, inDevDependencies: true); - package.ciConfigFile.writeAsStringSync('analyze_skills: true'); + test( + 'fails with package analysis, skills analysis, and custom linter violations listed if all fail', + () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); + _writeFakePubspecWithLinter(package, inDevDependencies: true); + package.ciConfigFile.writeAsStringSync('analyze_skills: true'); - package.directory - .childDirectory('.agents') - .childDirectory('skills') - .childFile('test.dart') - .createSync(recursive: true); + package.directory + .childDirectory('.agents') + .childDirectory('skills') + .childFile('test.dart') + .createSync(recursive: true); - processRunner.mockProcessesForExecutable['flutter'] = [ - FakeProcessInfo(MockProcess(), ['pub', 'get']), - ]; - processRunner.mockProcessesForExecutable['dart'] = [ - FakeProcessInfo(MockProcess(exitCode: 1), ['analyze']), // main package - FakeProcessInfo(MockProcess(exitCode: 1), ['analyze']), // skills package - FakeProcessInfo(MockProcess(exitCode: 1), [ - 'run', - 'dart_code_linter:metrics', - ]), // custom linter - ]; + processRunner.mockProcessesForExecutable['flutter'] = [ + FakeProcessInfo(MockProcess(), ['pub', 'get']), + ]; + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo(MockProcess(exitCode: 1), ['analyze']), // main package + FakeProcessInfo(MockProcess(exitCode: 1), ['analyze']), // skills package + FakeProcessInfo(MockProcess(exitCode: 1), [ + 'run', + 'dart_code_linter:metrics', + ]), // custom linter + ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, - ['analyze', '--custom-analysis', 'a_package'], - errorHandler: (Error e) { - commandError = e; - }, - ); + Error? commandError; + final List output = await runCapturingPrint( + runner, + ['analyze', '--custom-analysis', 'a_package'], + errorHandler: (Error e) { + commandError = e; + }, + ); - expect(commandError, isA()); - final String joinedOutput = output.join('\n'); - expect(joinedOutput, contains('The following packages had errors:')); - expect( - joinedOutput, - contains( - ' a_package:\n' - ' Main package analysis failed\n' - ' Skills analysis failed\n' - ' Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.', - ), - ); - }); + expect(commandError, isA()); + final String joinedOutput = output.join('\n'); + expect(joinedOutput, contains('The following packages had errors:')); + expect( + joinedOutput, + contains( + ' a_package:\n' + ' Main package analysis failed\n' + ' Skills analysis failed\n' + ' Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.', + ), + ); + }, + ); }); test('skips if requested if "pub get" fails in the resolver', () async { From 002182d701e8766a7eca041a4fba11fbaae14d92 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 1 Jul 2026 15:13:49 -0400 Subject: [PATCH 23/25] Fix dart_code_linter compile error during pub downgrade by pinning file dependency --- packages/camera/camera_android_camerax/CHANGELOG.md | 4 ++++ packages/camera/camera_android_camerax/pubspec.yaml | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 8debb8c4e53e..b3a642dc57f2 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.3+1 + +* Fix dart_code_linter build issue with package:file downgrade. + ## 0.7.3 * Fixes `videoBitrate` configuration being ignored during video recording. diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index f7496c077e00..f3d2d57bab2b 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.7.3 +version: 0.7.3+1 environment: sdk: ^3.12.0 @@ -33,6 +33,7 @@ dev_dependencies: url: https://github.com/flutter/skills.git path: tool/dart_skills_lint ref: 8f85e82be6da429980f7cfe84f2f214a06cbfee1 + file: ^7.0.0 flutter_test: sdk: flutter leak_tracker_flutter_testing: any From c1247d4ac3ec61da1329ff05d4daf83df4823096 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 1 Jul 2026 15:15:34 -0400 Subject: [PATCH 24/25] Revert "Fix dart_code_linter compile error during pub downgrade by pinning file dependency" This reverts commit 002182d701e8766a7eca041a4fba11fbaae14d92. --- packages/camera/camera_android_camerax/CHANGELOG.md | 4 ---- packages/camera/camera_android_camerax/pubspec.yaml | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index b3a642dc57f2..8debb8c4e53e 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,7 +1,3 @@ -## 0.7.3+1 - -* Fix dart_code_linter build issue with package:file downgrade. - ## 0.7.3 * Fixes `videoBitrate` configuration being ignored during video recording. diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index f3d2d57bab2b..f7496c077e00 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.7.3+1 +version: 0.7.3 environment: sdk: ^3.12.0 @@ -33,7 +33,6 @@ dev_dependencies: url: https://github.com/flutter/skills.git path: tool/dart_skills_lint ref: 8f85e82be6da429980f7cfe84f2f214a06cbfee1 - file: ^7.0.0 flutter_test: sdk: flutter leak_tracker_flutter_testing: any From b934bf813af75c82393c9578d92d72cd7136ac96 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 1 Jul 2026 15:21:04 -0400 Subject: [PATCH 25/25] Skip custom linter checks during downgraded analyze runs --- script/tool/lib/src/analyze_command.dart | 13 +++++---- script/tool/test/analyze_command_test.dart | 34 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 7d1e310918ff..9db7abd6fb5d 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -335,11 +335,14 @@ class AnalyzeCommand extends PackageLoopingCommand { _CustomLinter(dependencyName: 'dart_code_linter', run: _runDartCodeLinterForPackage), ]; - final Pubspec pubspec = package.parsePubspec(); - for (final runner in customCheckRunners) { - final bool hasDependency = pubspec.devDependencies.containsKey(runner.dependencyName); - if (hasDependency) { - errors.addAll(await runner.run(package)); + // Skip custom linters during downgrade as metrics are redundant and vulnerable to dependency issues. + if (!getBoolArg(_downgradeFlag)) { + final Pubspec pubspec = package.parsePubspec(); + for (final runner in customCheckRunners) { + final bool hasDependency = pubspec.devDependencies.containsKey(runner.dependencyName); + if (hasDependency) { + errors.addAll(await runner.run(package)); + } } } diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index bd8fdf5f3835..57124c2ebaa5 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -477,6 +477,40 @@ void main() { expect(output, contains('Running dart_code_linter:metrics analysis...')); }); + test('does not run dart_code_linter when --downgrade is specified', () async { + final RepositoryPackage package = createFakePackage( + 'a_package', + packagesDir, + isFlutter: true, + ); + _writeFakePubspecWithLinter(package, inDevDependencies: true); + + processRunner.mockProcessesForExecutable['flutter'] = [ + FakeProcessInfo(MockProcess(), ['pub', 'downgrade']), + FakeProcessInfo(MockProcess(), ['pub', 'get']), + ]; + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo(MockProcess(), ['analyze', '--fatal-infos']), + ]; + + final List output = await runCapturingPrint(runner, [ + 'analyze', + '--downgrade', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('flutter', const ['pub', 'downgrade'], package.path), + ProcessCall('flutter', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], package.path), + ]), + ); + final String combinedOutput = output.join('\n').toLowerCase(); + expect(combinedOutput, isNot(contains('dart_code_linter'))); + expect(combinedOutput, isNot(contains('metrics'))); + }); + test('does not run dart_code_linter if present in dependencies', () async { final RepositoryPackage package = createFakePackage( 'a_package',