| // 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 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 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 is! ResolvedUnitResult) { | |
| 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; | |
| } | |
| 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})'); | |
| } | |
| } | |
| /// 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; | |
| } | |
| } |