blob: 74cc1539ace6f04d108d7c903d839660dddb139f [file] [log] [blame]
// 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:analysis_server/src/utilities/flutter.dart';
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/diagnostic/diagnostic.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
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.
FlutterData();
/// 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 object used to determine Flutter-specific features.
Flutter flutter;
/// 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].
FlutterDataCollector(this.data);
@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
var previousParentWidget = parentWidget;
if (flutter.isWidgetCreation(node)) {
var element = node.constructorName.staticElement;
if (element == null) {
throw StateError(
'Unresolved constructor name: ${node.constructorName}');
}
var childWidget = element.enclosingElement.name;
if (!element.librarySource.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 resource provider used to access the files being analyzed.
final PhysicalResourceProvider resourceProvider =
PhysicalResourceProvider.INSTANCE;
/// 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.
FlutterMetricsComputer();
/// Compute the metrics for the file(s) in the [rootPath].
Future<void> compute(String rootPath) async {
final collection = AnalysisContextCollection(
includedPaths: [rootPath],
resourceProvider: PhysicalResourceProvider.INSTANCE,
);
final 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.
final 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 == null) {
print('');
print('File $filePath skipped because of an internal error.');
continue;
} else if (resolvedUnitResult.state != ResultState.VALID) {
print('');
print('File $filePath skipped because it could not be analyzed.');
continue;
} else if (hasError(resolvedUnitResult)) {
print('');
print('File $filePath skipped due to errors:');
for (var error in resolvedUnitResult.errors) {
print(' ${error.toString()}');
}
continue;
}
collector.flutter = Flutter.instance;
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 outerKeys = structureMap.keys.toList()..sort();
for (var outerKey in outerKeys) {
sink.writeln(outerKey);
var innerMap = structureMap[outerKey];
var entries = innerMap.entries.toList();
entries.sort((first, second) => second.value.compareTo(first.value));
var total = entries.fold(
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})');
}
}
/// Return `true` if the [result] contains an error.
static bool hasError(ResolvedUnitResult result) {
for (var error in result.errors) {
if (error.severity == Severity.error) {
return true;
}
}
return false;
}
}