Files
sdk/pkg/analysis_server/tool/code_completion/flutter_metrics.dart
T
Paul Berry afcfbbeba8 Migrate developer experience packages to new constructor decl syntax.
(Part of https://github.com/dart-lang/sdk/issues/63288)

This change migrates the packages owned by the developer experience
team to use the new constructor declaration syntax, described in
https://github.com/dart-lang/language/blob/main/accepted/future-releases/primary-constructors/feature-specification.md#abbreviations-of-in-body-constructor-declarations.

This change was performed in an automated fashion, by (a) bumping the
packages' SDK constraints to `3.13.0-0`, (b) enabling the lints
`unnecessary_type_name_in_constructor` and
`unnecessary_const_in_enum_constructor`, (c) fixing the resulting lint
failures using `dart fix`, and then (d) reformatting the affected
files.

To ease code review, I've reverted unrelated formatting changes.

Since this change requires bumping SDK constaints to `3.13.0-0`, it
was only performed on packages that are *not* published on
pub. (Packages that *are* published on pub should remain on lower
language versions until at least after the stable version of 3.13 is
released, so that we don't block users on the stable channel from
receiving updates to those packages.)

Change-Id: Ibb4daebafd239da58251e838ea6a3f336a6a6964
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/505046
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
SLSA-Policy-Verified: SLSA Policy Verification Service <devtools-gerritcodereview-exitgate@google.com>
2026-05-27 14:52:58 -07:00

293 lines
10 KiB
Dart

// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. 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:io' as io;
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/context_root.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/utilities/extensions/diagnostic.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
import 'package:args/args.dart';
/// Compute and print information about flutter packages.
Future<void> main(List<String> args) async {
var parser = createArgParser();
var result = parser.parse(args);
if (validArguments(parser, result)) {
var out = io.stdout;
var rootPath = result.rest[0];
out.writeln('Analyzing root: "$rootPath"');
var computer = FlutterMetricsComputer();
var stopwatch = Stopwatch()..start();
await computer.compute(rootPath);
stopwatch.stop();
var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
out.writeln();
out.writeln('Analysis performed in $duration');
computer.writeResults(out);
await out.flush();
}
io.exit(0);
}
/// Create a parser that can be used to parse the command-line arguments.
ArgParser createArgParser() {
var parser = ArgParser();
parser.addOption('help', abbr: 'h', help: 'Print this help message.');
return parser;
}
/// Print usage information for this tool.
void printUsage(ArgParser parser, {String? error}) {
if (error != null) {
print(error);
print('');
}
print('usage: dart flutter_metrics.dart [options] packagePath');
print('');
print('Compute and print information about flutter packages.');
print('');
print(parser.usage);
}
/// Return `true` if the command-line arguments (represented by the [result] and
/// parsed by the [parser]) are valid.
bool validArguments(ArgParser parser, ArgResults result) {
if (result.wasParsed('help')) {
printUsage(parser);
return false;
} else if (result.rest.length != 1) {
printUsage(parser, error: 'No directory path specified.');
return false;
}
var rootPath = result.rest[0];
if (!io.Directory(rootPath).existsSync()) {
printUsage(parser, error: 'The directory "$rootPath" does not exist.');
return false;
}
return true;
}
/// An object that records the data as it is being computed.
class FlutterData {
/// The total number of widget creation expressions found.
int totalWidgetCount = 0;
/// A table mapping the name of a widget class to the number of times in
/// which an instance of that class is created.
Map<String, int> widgetCounts = {};
/// A table mapping the name of a widget class and the name of the parent
/// widget to the number of times the widget was created as a child of the
/// parent.
Map<String, Map<String, int>> parentData = {};
/// A table mapping the name of the parent widget and the name of a widget
/// class to the number of times the parent had a widget of the given kind.
Map<String, Map<String, int>> childData = {};
/// Initialize a newly created set of data to be empty.
new();
/// Record that an instance of the [childWidget] was created. If the instance
/// creation expression is an argument in another widget constructor
/// invocation, then the [parentWidget] is the name of the enclosing class.
void recordWidgetCreation(String childWidget, String? parentWidget) {
totalWidgetCount++;
widgetCounts[childWidget] = (widgetCounts[childWidget] ?? 0) + 1;
if (parentWidget != null) {
var parentMap = parentData.putIfAbsent(childWidget, () => {});
parentMap[parentWidget] = (parentMap[parentWidget] ?? 0) + 1;
var childMap = childData.putIfAbsent(parentWidget, () => {});
childMap[childWidget] = (childMap[childWidget] ?? 0) + 1;
}
}
}
/// An object that visits a compilation unit in order to record the data being
/// collected.
class FlutterDataCollector extends RecursiveAstVisitor<void> {
/// The data being collected.
final FlutterData data;
/// The name of the most deeply widget class whose constructor invocation we
/// are within.
String? parentWidget;
/// Initialize a newly created collector to add data points to the given
/// [data].
new(this.data);
@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
var previousParentWidget = parentWidget;
if (node.isWidgetCreation) {
var element = node.constructorName.element;
if (element == null) {
throw StateError(
'Unresolved constructor name: ${node.constructorName}',
);
}
var childWidget = element.enclosingElement.name!;
var library = element.library;
if (!library.uri.toString().startsWith('package:flutter/')) {
childWidget = 'user-defined';
}
data.recordWidgetCreation(childWidget, parentWidget);
parentWidget = childWidget;
}
super.visitInstanceCreationExpression(node);
parentWidget = previousParentWidget;
}
}
/// An object used to compute metrics for a single file or directory.
class FlutterMetricsComputer {
/// The data that was computed.
final FlutterData data = FlutterData();
/// Initialize a newly created metrics computer that can compute the metrics
/// in one or more files and directories.
new();
/// Compute the metrics for the file(s) in the [rootPath].
Future<void> compute(String rootPath) async {
var collection = AnalysisContextCollection(
includedPaths: [rootPath],
resourceProvider: PhysicalResourceProvider.INSTANCE,
);
var collector = FlutterDataCollector(data);
for (var context in collection.contexts) {
await _computeInContext(context.contextRoot, collector);
}
}
/// Write a report of the metrics that were computed to the [sink].
void writeResults(StringSink sink) {
_writeWidgetCounts(sink);
_writeChildData(sink);
_writeParentData(sink);
}
/// Compute the metrics for the files in the context [root], creating a
/// separate context collection to prevent accumulating memory. The metrics
/// should be captured in the [collector].
Future<void> _computeInContext(
ContextRoot root,
FlutterDataCollector collector,
) async {
// Create a new collection to avoid consuming large quantities of memory.
var collection = AnalysisContextCollection(
includedPaths: root.includedPaths.toList(),
excludedPaths: root.excludedPaths.toList(),
resourceProvider: PhysicalResourceProvider.INSTANCE,
);
var context = collection.contexts[0];
var pathContext = context.contextRoot.resourceProvider.pathContext;
for (var filePath in context.contextRoot.analyzedFiles()) {
if (file_paths.isDart(pathContext, filePath)) {
try {
var resolvedUnitResult = await context.currentSession.getResolvedUnit(
filePath,
);
//
// Check for errors that cause the file to be skipped.
//
if (resolvedUnitResult is! ResolvedUnitResult) {
print('');
print('File $filePath skipped because it could not be analyzed.');
continue;
} else if (resolvedUnitResult.diagnostics.errors.isNotEmpty) {
print('');
print('File $filePath skipped due to errors:');
for (var diagnostic in resolvedUnitResult.diagnostics) {
print(' ${diagnostic.toString()}');
}
continue;
}
resolvedUnitResult.unit.accept(collector);
} catch (exception, stackTrace) {
print('');
print('Exception caught analyzing: "$filePath"');
print(exception);
print(stackTrace);
}
}
}
}
/// Compute and format a percentage for the fraction [value] / [total].
String _formatPercent(int value, int total) {
var percent = ((value / total) * 100).toStringAsFixed(1);
if (percent.length == 3) {
percent = ' $percent';
} else if (percent.length == 4) {
percent = ' $percent';
}
return percent;
}
/// Write the child data to the [sink].
void _writeChildData(StringSink sink) {
sink.writeln();
sink.writeln('The number of times a widget had a given child.');
_writeStructureData(sink, data.childData);
}
/// Write the parent data to the [sink].
void _writeParentData(StringSink sink) {
sink.writeln();
sink.writeln('The number of times a widget had a given parent.');
_writeStructureData(sink, data.parentData);
}
/// Write the structure data in the [structureMap] to the [sink].
void _writeStructureData(
StringSink sink,
Map<String, Map<String, int>> structureMap,
) {
var outerEntries = structureMap.entries.toList();
outerEntries.sort((first, second) => first.key.compareTo(second.key));
for (var outerEntry in outerEntries) {
var outerKey = outerEntry.key;
sink.writeln(outerKey);
var innerMap = outerEntry.value;
var entries = innerMap.entries.toList();
entries.sort((first, second) => second.value.compareTo(first.value));
var total = entries.fold<int>(
0,
(previousValue, entry) => previousValue + entry.value,
);
for (var entry in entries) {
var percent = _formatPercent(entry.value, total);
sink.writeln(' $percent%: ${entry.key} (${entry.value})');
}
}
}
/// Write the widget count data to the [sink].
void _writeWidgetCounts(StringSink sink) {
sink.writeln();
sink.writeln('Widget classes by frequency of instantiation');
var total = data.totalWidgetCount;
var entries = data.widgetCounts.entries.toList();
entries.sort((first, second) => second.value.compareTo(first.value));
for (var entry in entries) {
var percent = _formatPercent(entry.value, total);
sink.writeln(' $percent%: ${entry.key} (${entry.value})');
}
}
}