flutter_flutter/dev/devicelab/bin/tasks/technical_debt__cost.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

193 lines
6.4 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;
// the numbers below are prime, so that the totals don't seem round. :-)
const double todoCost = 1009.0; // about two average SWE days, in dollars
const double ignoreCost = 2003.0; // four average SWE days, in dollars
const double pythonCost = 3001.0; // six average SWE days, in dollars
const double skipCost =
2473.0; // 20 hours: 5 to fix the issue we're ignoring, 15 to fix the bugs we missed because the test was off
const double ignoreForFileCost = 2477.0; // similar thinking as skipCost
const double asDynamicCost = 2011.0; // a few days to refactor the code.
const double deprecationCost = 233.0; // a few hours to remove the old code.
const double legacyDeprecationCost = 9973.0; // a couple of weeks.
final RegExp todoPattern = RegExp(r'(?://|#) *TODO');
final RegExp ignorePattern = RegExp(r'// *ignore:');
final RegExp ignoreForFilePattern = RegExp(r'// *ignore_for_file:');
final RegExp asDynamicPattern = RegExp(r'\bas dynamic\b');
final RegExp deprecationPattern = RegExp(r'^ *@[dD]eprecated');
const Pattern globalsPattern = 'globals.';
const String legacyDeprecationPattern = '// flutter_ignore: deprecation_syntax, https';
Future<double> findCostsForFile(File file) async {
if (path.extension(file.path) == '.py') {
return pythonCost;
}
if (path.extension(file.path) != '.dart' &&
path.extension(file.path) != '.yaml' &&
path.extension(file.path) != '.sh') {
return 0.0;
}
final bool isTest = file.path.endsWith('_test.dart');
var total = 0.0;
for (final String line in await file.readAsLines()) {
if (line.contains(todoPattern)) {
total += todoCost;
}
if (line.contains(ignorePattern)) {
total += ignoreCost;
}
if (line.contains(ignoreForFilePattern)) {
total += ignoreForFileCost;
}
if (!isTest && line.contains(asDynamicPattern)) {
total += asDynamicCost;
}
if (line.contains(deprecationPattern)) {
total += deprecationCost;
}
if (line.contains(legacyDeprecationPattern)) {
total += legacyDeprecationCost;
}
if (isTest && line.contains('skip:') && !line.contains('[intended]')) {
total += skipCost;
}
}
return total;
}
Future<int> findGlobalsForFile(File file) async {
if (path.extension(file.path) != '.dart') {
return 0;
}
var total = 0;
for (final String line in await file.readAsLines()) {
if (line.contains(globalsPattern)) {
total += 1;
}
}
return total;
}
Future<double> findCostsForRepo() async {
final Process git = await startProcess('git', <String>[
'ls-files',
'--exclude',
'engine',
'--full-name',
flutterDirectory.path,
], workingDirectory: flutterDirectory.path);
var total = 0.0;
await for (final String entry
in git.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter())) {
total += await findCostsForFile(File(path.join(flutterDirectory.path, entry)));
}
final int gitExitCode = await git.exitCode;
if (gitExitCode != 0) {
throw Exception('git exit with unexpected error code $gitExitCode');
}
return total;
}
Future<int> findGlobalsForTool() async {
final Process git = await startProcess('git', <String>[
'ls-files',
'--full-name',
path.join(flutterDirectory.path, 'packages', 'flutter_tools'),
], workingDirectory: flutterDirectory.path);
var total = 0;
await for (final String entry
in git.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter())) {
total += await findGlobalsForFile(File(path.join(flutterDirectory.path, entry)));
}
final int gitExitCode = await git.exitCode;
if (gitExitCode != 0) {
throw Exception('git exit with unexpected error code $gitExitCode');
}
return total;
}
Future<int> countDependencies() async => _getCount(<String>{
...(await dependenciesAt(packageNames: const <String>['_flutter_packages'])),
...(await dependenciesAt(
packageNames: const <String>['flutter_tools'],
workingDirectory: path.join(flutterDirectory.path, 'packages', 'flutter_tools'),
)),
});
Future<Set<String>> dependenciesAt({
required List<String> packageNames,
String? workingDirectory,
}) async {
final String jsonOutput = await evalFlutter(
'pub',
options: <String>[
'deps',
'--json',
if (workingDirectory != null) ...<String>['-C', workingDirectory],
],
);
final json = jsonDecode(jsonOutput) as Map<String, dynamic>;
final packages = json['packages'] as List<dynamic>;
final Iterable<String> count = packages
.map((dynamic e) => e as Map<String, dynamic>)
.where((Map<String, dynamic> package) => packageNames.contains(package['name']))
.expand((Map<String, dynamic> element) => element['dependencies'] as List<dynamic>)
.map((dynamic e) => e as String);
return count.toSet();
}
Future<int> countConsumerDependencies() async => _getCount(
await dependenciesAt(
packageNames: <String>[
'flutter',
'flutter_test',
'flutter_driver',
'flutter_localizations',
'integration_test',
],
),
);
int _getCount(Set<String> deps) {
final int count = deps.length;
if (count < 2) {
throw Exception('"flutter pub deps --json" returned bogus output.');
}
return count;
}
const String _kCostBenchmarkKey = 'technical_debt_in_dollars';
const String _kNumberOfDependenciesKey = 'dependencies_count';
const String _kNumberOfConsumerDependenciesKey = 'consumer_dependencies_count';
const String _kNumberOfFlutterToolGlobals = 'flutter_tool_globals_count';
Future<void> main() async {
await task(() async {
return TaskResult.success(
<String, dynamic>{
_kCostBenchmarkKey: await findCostsForRepo(),
_kNumberOfDependenciesKey: await countDependencies(),
_kNumberOfConsumerDependenciesKey: await countConsumerDependencies(),
_kNumberOfFlutterToolGlobals: await findGlobalsForTool(),
},
benchmarkScoreKeys: <String>[
_kCostBenchmarkKey,
_kNumberOfDependenciesKey,
_kNumberOfConsumerDependenciesKey,
_kNumberOfFlutterToolGlobals,
],
);
});
}