// 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 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 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 findCostsForRepo() async { final Process git = await startProcess('git', [ 'ls-files', '--exclude', 'engine', '--full-name', flutterDirectory.path, ], workingDirectory: flutterDirectory.path); var total = 0.0; await for (final String entry in git.stdout.transform(utf8.decoder).transform(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 findGlobalsForTool() async { final Process git = await startProcess('git', [ '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(utf8.decoder).transform(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 countDependencies() async => _getCount({ ...(await dependenciesAt(packageNames: const ['_flutter_packages'])), ...(await dependenciesAt( packageNames: const ['flutter_tools'], workingDirectory: path.join(flutterDirectory.path, 'packages', 'flutter_tools'), )), }); Future> dependenciesAt({ required List packageNames, String? workingDirectory, }) async { final String jsonOutput = await evalFlutter( 'pub', options: [ 'deps', '--json', if (workingDirectory != null) ...['-C', workingDirectory], ], ); final json = jsonDecode(jsonOutput) as Map; final packages = json['packages'] as List; final Iterable count = packages .map((dynamic e) => e as Map) .where((Map package) => packageNames.contains(package['name'])) .expand((Map element) => element['dependencies'] as List) .map((dynamic e) => e as String); return count.toSet(); } Future countConsumerDependencies() async => _getCount( await dependenciesAt( packageNames: [ 'flutter', 'flutter_test', 'flutter_driver', 'flutter_localizations', 'integration_test', ], ), ); int _getCount(Set 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 main() async { await task(() async { return TaskResult.success( { _kCostBenchmarkKey: await findCostsForRepo(), _kNumberOfDependenciesKey: await countDependencies(), _kNumberOfConsumerDependenciesKey: await countConsumerDependencies(), _kNumberOfFlutterToolGlobals: await findGlobalsForTool(), }, benchmarkScoreKeys: [ _kCostBenchmarkKey, _kNumberOfDependenciesKey, _kNumberOfConsumerDependenciesKey, _kNumberOfFlutterToolGlobals, ], ); }); }