blob: 6b0b3c15c491ad86bc81be103bdbf5f4957d7bb3 [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/status/pages.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/dart/element/type.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 metrics to determine which untyped variable declarations can be used
/// to imply an expected context type, i.e. the RHS of 'var string = ^' could be
/// assumed to be a [String].
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 =[0];
out.writeln('Analyzing root: "$rootPath"');
var computer = ImpliedTypeComputer();
var stopwatch = Stopwatch();
await computer.compute(rootPath, verbose: result['verbose']);
var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
out.writeln('Metrics computed in $duration');
await out.flush();
/// Create a parser that can be used to parse the command-line arguments.
ArgParser createArgParser() {
var parser = ArgParser();
abbr: 'h',
help: 'Print this help message.',
abbr: 'v',
help: 'Print additional information about the analysis',
negatable: false,
return parser;
/// Print usage information for this tool.
void printUsage(ArgParser parser, {String? error}) {
if (error != null) {
print('usage: dart implicit_type_declarations.dart [options] packagePath');
print('Compute implicit types in field declaration locations without a '
'specified type.');
/// 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')) {
return false;
} else if ( != 1) {
printUsage(parser, error: 'No package path specified.');
return false;
var rootPath =[0];
if (!io.Directory(rootPath).existsSync()) {
printUsage(parser, error: 'The directory "$rootPath" does not exist.');
return false;
return true;
/// An object that visits a compilation unit in order to record the data used to
/// compute the metrics.
class ImpliedTypeCollector extends RecursiveAstVisitor<void> {
/// The implied type data being collected.
ImpliedTypeData data;
/// Initialize a newly created collector to add data points to the given
/// [data].
void handleVariableDeclaration(VariableDeclaration node, DartType? dartType) {
// If some untyped variable declaration
if (node.equals != null && dartType == null ||
(dartType != null && (dartType.isDynamic || dartType.isVoid))) {
// And if we can determine the type on the RHS of the variable declaration
var rhsType = node.initializer?.staticType;
if (rhsType != null && !rhsType.isDynamic) {
// Record the name with the type.
rhsType.getDisplayString(withNullability: false),
void visitVariableDeclarationList(VariableDeclarationList node) {
for (var varDecl in node.variables) {
handleVariableDeclaration(varDecl, node.type?.type);
return null;
/// An object used to compute metrics for a single file or directory.
class ImpliedTypeComputer {
/// The metrics data that was computed.
final ImpliedTypeData data = ImpliedTypeData();
/// Initialize a newly created metrics computer that can compute the metrics
/// in one or more files and directories.
/// Compute the metrics for the file(s) in the [rootPath].
/// If [corpus] is true, treat rootPath as a container of packages, creating
/// a new context collection for each subdirectory.
Future<void> compute(String rootPath, {required bool verbose}) async {
final collection = AnalysisContextCollection(
includedPaths: [rootPath],
resourceProvider: PhysicalResourceProvider.INSTANCE,
final collector = ImpliedTypeCollector(data);
for (var context in collection.contexts) {
await _computeInContext(context.contextRoot, collector, verbose: verbose);
/// Write a report of the metrics that were computed to the [sink].
void writeMetrics(StringSink sink) {
data.impliedTypesMap.forEach((String name, Map<String, int> displayStrMap) {
var sum = 0;
displayStrMap.forEach((String displayStr, int count) {
sum += count;
if (sum >= 5) {
sink.writeln('$name $sum:');
displayStrMap.forEach((String displayStr, int count) {
sink.writeln(' $displayStr $count ${printPercentage(count / sum)}');
/// 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]. Include additional details in the
/// output if [verbose] is `true`.
Future<void> _computeInContext(
ContextRoot root, ImpliedTypeCollector collector,
{required bool verbose}) 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.getResolvedUnit2(filePath);
// Check for errors that cause the file to be skipped.
if (resolvedUnitResult is! ResolvedUnitResult) {
print('File $filePath skipped because it could not be analyzed.');
if (verbose) {
} else if (hasError(resolvedUnitResult)) {
if (verbose) {
print('File $filePath skipped due to errors:');
for (var error in resolvedUnitResult.errors
.where((e) => e.severity == Severity.error)) {
print(' ${error.toString()}');
} else {
print('File $filePath skipped due to analysis errors.');
} catch (exception, stacktrace) {
print('Exception caught analyzing: "$filePath"');
/// 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;
class ImpliedTypeData {
Map<String, Map<String, int>> impliedTypesMap = {};
/// Record the variable name with the type.
void recordImpliedType(String name, String displayString) {
var nameMap = impliedTypesMap.putIfAbsent(name, () => {});
nameMap[displayString] = (nameMap[displayString] ?? 0) + 1;