Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
14be833
Add cyclomatic complexity linter configuration for camera_android_cam…
reidbaker Jun 25, 2026
f206376
Configure CI target and script to run cyclomatic complexity linter
reidbaker Jun 25, 2026
0299ea1
Integrate cyclomatic complexity check directly into flutter_plugin_to…
reidbaker Jun 25, 2026
30757fa
Remove commented-out plugin lines from analysis_options.yaml
reidbaker Jun 25, 2026
fc676e2
Refactor analyze_command linter check to a separate helper method and…
reidbaker Jun 25, 2026
e82d111
Refactor custom linter checks into a pipeline runner list for easier …
reidbaker Jun 25, 2026
5e798fe
Clean up custom linter API boundaries by extracting dependency checki…
reidbaker Jun 25, 2026
8b336a2
Clean up static analysis warnings in analyze_command and analyze_comm…
reidbaker Jun 25, 2026
2f96c6b
Restrict custom linter checks to dev_dependencies only
reidbaker Jun 25, 2026
84abbf0
Use expectedInitialArgs to explicitly document mocks in linter tests
reidbaker Jun 25, 2026
eb76af7
Improve tests by explicitly mocking process queues and verifying cons…
reidbaker Jun 25, 2026
d93d538
De-duplicate pubspec writing and mock process logic in linter tests u…
reidbaker Jun 25, 2026
dd58a0d
Ensure linter skip tests are robust against print string changes by a…
reidbaker Jun 25, 2026
214f051
Add descriptive doc comments and dynamic Dart/Flutter SDK resolution …
reidbaker Jun 25, 2026
71559a9
Update custom_analysis.yaml comment to say cyclomatic complexity
reidbaker Jun 25, 2026
24d9e8a
Simplify custom linter checks to always execute via dart run
reidbaker Jun 25, 2026
ea9770a
Add defensive check and tests to ensure linter does not run on packag…
reidbaker Jun 25, 2026
e9bdaeb
Address review feedback: rename mock helper and report cyclomatic com…
reidbaker Jun 30, 2026
342eab8
Fix formatting and linter issues in analyze command changes
reidbaker Jun 30, 2026
64a8a90
Refactor custom linter checking to aggregate errors instead of failin…
reidbaker Jul 1, 2026
db7e807
Merge branch 'main' into add-cyclomatic-complexity-linter
reidbaker Jul 1, 2026
f745d82
Fix early return on standard analysis failure and add combined aggreg…
reidbaker Jul 1, 2026
d14f6e1
Rename combined failures integration test for clarity
reidbaker Jul 1, 2026
002182d
Fix dart_code_linter compile error during pub downgrade by pinning fi…
reidbaker Jul 1, 2026
c1247d4
Revert "Fix dart_code_linter compile error during pub downgrade by pi…
reidbaker Jul 1, 2026
b934bf8
Skip custom linter checks during downgraded analyze runs
reidbaker Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .repo_tool_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions packages/camera/camera_android_camerax/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
include: ../../../analysis_options.yaml

analyzer:
exclude:
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**

dart_code_linter:
metrics:
cyclomatic-complexity: 15

1 change: 1 addition & 0 deletions packages/camera/camera_android_camerax/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies:

dev_dependencies:
build_runner: ^2.2.0
dart_code_linter: 4.1.5

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've audited this as safe to run in our CI and locally?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHA1 (dart_code_linter-4.1.5.tar.gz) = 86fa1bd240a88c98a4c45f0beb43b4e7adb32735
For auditability this this is the artifact I inspected.

Looking at the code I didn't see anything suspicious.

Some things I found but I do not think are blocking:
It uses https://pub.dev/packages/pub_updater to see if there is a newer version. It works without internet access.
There is a python script to make cursor skills. It is for contributors but I found that interesting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As concrete examples of suspicious things I looked for. Code that tried to spawn other process, code that tried to install other things, network calls, code that read env variables that looked like secrets.

dart_skills_lint:
git:
url: https://github.com/flutter/skills.git
Expand Down
3 changes: 3 additions & 0 deletions script/configs/custom_analysis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
reidbaker marked this conversation as resolved.
Outdated
- camera/camera_android_camerax

64 changes: 64 additions & 0 deletions script/tool/lib/src/analyze_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,53 @@ class AnalyzeCommand extends PackageLoopingCommand {
if (exitCode != 0) {
return PackageResult.fail();
}

final customCheckRunners = <_CustomLinter>[

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think we need this since we're only adding one linter at the moment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am expecting we will expand this list. I would like to keep the functionality for now.

_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) {
final PackageResult result = await runner.run(package);
if (result.state == RunState.failed) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strongly preferred pattern in the repo tooling is for the helper functions to return List<String> representing any errors (such than an empty array is success), and for the loop to aggregate the errors into a local errors array, then at the end to return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors);

That way, we get all the results in every run, rather than only up to the first failing thing.

@reidbaker reidbaker Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sorry, I authored this pr at the same time as the other one you gave this feedback on and I didnt go back to check if the pattern was violated anywhere else.

I also filed flutter/flutter#188867 which tracks changing the gradle behavior to follow this pattern.

I will personally try to remember this pattern and I have taken steps on my local machine to try to ensure this pattern does not pop up again.

return result;
}
}
}

return PackageResult.success();
}

/// Runs the `dart_code_linter` metrics analyzer on the package.
///
/// Assumes `dart_code_linter` is present in `dev_dependencies`.
Future<PackageResult> _runDartCodeLinterForPackage(RepositoryPackage package) async {
print('Running dart_code_linter:metrics analysis...');
Comment thread
camsim99 marked this conversation as resolved.
Outdated
final bool isFlutter = package.requiresFlutter();
final String sdkCommand = isFlutter ? flutterCommand : _dartBinaryPath;
final int linterExitCode = await processRunner.runAndStream(
sdkCommand,
<String>[
if (isFlutter) 'pub',
'run',
'dart_code_linter:metrics',
'analyze',
'lib',
'--set-exit-on-violation-level=warning',
],
Comment thread
reidbaker marked this conversation as resolved.
Outdated
workingDir: package.directory,
);
if (linterExitCode != 0) {
return PackageResult.fail(<String>[
'Metrics violations found. See the package\'s local "analysis_options.yaml" for configured thresholds.'
Comment thread
reidbaker marked this conversation as resolved.
Outdated
]);
}

return PackageResult.success();
}

Expand Down Expand Up @@ -437,3 +484,20 @@ class AnalyzeCommand extends PackageLoopingCommand {
return errors.isEmpty ? PackageResult.success() : PackageResult.fail(errors);
}
}

/// 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<PackageResult> Function(RepositoryPackage) run;
}
185 changes: 185 additions & 0 deletions script/tool/test/analyze_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,153 @@ 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);
_writeFakePubspecWithLinter(package, inDevDependencies: true);

_mockStandardFlutterAndDartProcesses(processRunner, extraFlutterCalls: [
FakeProcessInfo(
MockProcess(),
<String>['pub', 'run', 'dart_code_linter:metrics'],
),
]);

final List<String> output = await runCapturingPrint(runner, <String>['analyze']);

expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall('flutter', const <String>['pub', 'get'], package.path),
ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path),
ProcessCall(
'flutter',
const <String>[
'pub',
'run',
'dart_code_linter:metrics',
'analyze',
'lib',
'--set-exit-on-violation-level=warning',
],
package.path,
),
]),
Comment thread
reidbaker marked this conversation as resolved.
Outdated
);
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);
_writeFakePubspecWithLinter(package, inDependencies: true);

_mockStandardFlutterAndDartProcesses(processRunner);

final List<String> output = await runCapturingPrint(runner, <String>['analyze']);

expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall('flutter', const <String>['pub', 'get'], package.path),
ProcessCall('dart', const <String>['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 not present', () async {
final RepositoryPackage package = createFakePackage('a_package', packagesDir, isFlutter: true);

_mockStandardFlutterAndDartProcesses(processRunner);

final List<String> output = await runCapturingPrint(runner, <String>['analyze']);

expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall('flutter', const <String>['pub', 'get'], package.path),
ProcessCall('dart', const <String>['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('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>[
FakeProcessInfo(
MockProcess(),
<String>['pub', 'get'],
),
FakeProcessInfo(
MockProcess(),
<String>['analyze'],
),
FakeProcessInfo(
MockProcess(),
<String>['run', 'dart_code_linter:metrics'],
),
];

final List<String> output = await runCapturingPrint(runner, <String>['analyze']);

expect(
processRunner.recordedCalls,
orderedEquals(<ProcessCall>[
ProcessCall('dart', const <String>['pub', 'get'], package.path),
ProcessCall('dart', const <String>['analyze', '--fatal-infos'], package.path),
ProcessCall(
'dart',
const <String>[
'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);

_mockStandardFlutterAndDartProcesses(processRunner, extraFlutterCalls: [
FakeProcessInfo(
MockProcess(exitCode: 1),
<String>['pub', 'run', 'dart_code_linter:metrics'],
),
]);
Comment thread
reidbaker marked this conversation as resolved.
Outdated

Error? commandError;
final List<String> output = await runCapturingPrint(
runner,
<String>['analyze'],
errorHandler: (Error e) {
commandError = e;
},
);

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('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 {
final RepositoryPackage plugin = createFakePlugin('foo', packagesDir);

Expand Down Expand Up @@ -1526,3 +1673,41 @@ packages/package_a/lib/foo.dart
});
});
}

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"
${includeFlutter ? 'flutter: ">=2.5.0"' : ''}
dependencies:
${includeFlutter ? 'flutter:\n sdk: flutter' : ''}
${inDependencies ? ' dart_code_linter: 4.1.5' : ''}
${inDevDependencies ? 'dev_dependencies:\n dart_code_linter: 4.1.5' : ''}
''');
}

void _mockStandardFlutterAndDartProcesses(
Comment thread
reidbaker marked this conversation as resolved.
Outdated
RecordingProcessRunner processRunner, {
List<FakeProcessInfo> extraFlutterCalls = const <FakeProcessInfo>[],
}) {
processRunner.mockProcessesForExecutable['flutter'] = <FakeProcessInfo>[
FakeProcessInfo(
MockProcess(),
const <String>['pub', 'get'],
),
...extraFlutterCalls,
];
processRunner.mockProcessesForExecutable['dart'] = <FakeProcessInfo>[
FakeProcessInfo(
MockProcess(),
const <String>['analyze'],
),
];
}
Comment thread
reidbaker marked this conversation as resolved.
Outdated
Loading