// Copyright (c) 2019, 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. // This is a hacked-together client of the NNBD migration API, intended for // early testing of the migration process. It runs a small hardcoded set of // packages through the migration engine and outputs statistics about the // result of migration, as well as categories (and counts) of exceptions that // occurred. import 'dart:io'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/diagnostic/diagnostic.dart'; import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/src/generated/source.dart'; import 'package:analyzer_plugin/protocol/protocol_common.dart'; import 'package:args/args.dart'; import 'package:nnbd_migration/nnbd_migration.dart'; import 'package:path/path.dart' as path; import 'src/package.dart'; void main(List args) async { ArgResults parsedArgs = parseArguments(args); Sdk sdk = Sdk(parsedArgs['sdk'] as String); warnOnNoAssertions(); Playground playground = Playground(defaultPlaygroundPath, parsedArgs['clean'] as bool); List packages = [ for (String package in parsedArgs['packages'] as Iterable) SdkPackage(package), for (String package in parsedArgs['manual_packages'] as Iterable) ManualPackage(package), ]; var packageNames = parsedArgs['git_packages'] as Iterable; await Future.wait(packageNames.map((n) async => packages.add( await GitPackage.gitPackageFactory( n, playground, parsedArgs['update'] as bool)))); String categoryOfInterest = parsedArgs.rest.isEmpty ? null : parsedArgs.rest.single; var listener = _Listener(categoryOfInterest, printExceptionNodeOnly: parsedArgs['exception_node_only'] as bool); assert(listener.numExceptions == 0); var overallStartTime = DateTime.now(); for (var package in packages) { print('Migrating $package'); var startTime = DateTime.now(); listener.currentPackage = package.name; var contextCollection = AnalysisContextCollectionImpl( includedPaths: package.migrationPaths, sdkPath: sdk.sdkPath); var files = {}; var previousExceptionCount = listener.numExceptions; for (var context in contextCollection.contexts) { var localFiles = context.contextRoot.analyzedFiles().where((s) => s.endsWith('.dart')); files.addAll(localFiles); var session = context.currentSession; LineInfo getLineInfo(String path) => (session.getFile2(path) as FileResult).lineInfo; var migration = NullabilityMigration(listener, getLineInfo, permissive: true); for (var file in localFiles) { var resolvedUnit = await session.getResolvedUnit2(file) as ResolvedUnitResult; if (!resolvedUnit.errors.any((e) => e.severity == Severity.error)) { migration.prepareInput(resolvedUnit); } else { print(' Skipping $file; it has errors.'); } } for (var file in localFiles) { var resolvedUnit = await session.getResolvedUnit2(file) as ResolvedUnitResult; if (!resolvedUnit.errors.any((e) => e.severity == Severity.error)) { migration.processInput(resolvedUnit); } } for (var file in localFiles) { var resolvedUnit = await session.getResolvedUnit2(file) as ResolvedUnitResult; if (!resolvedUnit.errors.any((e) => e.severity == Severity.error)) { migration.finalizeInput(resolvedUnit); } } migration.finish(); } var endTime = DateTime.now(); print(' Migrated $package in ${endTime.difference(startTime).inSeconds} ' 'seconds'); print(' ${files.length} files found'); var exceptionCount = listener.numExceptions - previousExceptionCount; print(' $exceptionCount exceptions in this package'); } var overallDuration = DateTime.now().difference(overallStartTime); print('${packages.length} packages migrated in ${overallDuration.inSeconds} ' 'seconds'); print('${listener.numTypesMadeNullable} types made nullable'); print('${listener.numNullChecksAdded} null checks added'); print('${listener.numVariablesMarkedLate} variables marked late'); print('${listener.numInsertedCasts} casts inserted'); print('${listener.numInsertedParenthesis} parenthesis groupings inserted'); print('${listener.numMetaImportsAdded} meta imports added'); print('${listener.numRequiredAnnotationsAdded} required annotations added'); print('${listener.numDeadCodeSegmentsFound} dead code segments found'); print('and ${listener.numOtherEdits} other edits not categorized'); print('${listener.numExceptions} exceptions in ' '${listener.groupedExceptions.length} categories'); var sortedExceptions = [ for (var entry in listener.groupedExceptions.entries) ExceptionCategory(entry.key, entry.value) ]..sort((category1, category2) => category2.count.compareTo(category1.count)); var exceptionalPackages = sortedExceptions.expand((category) => category.packageNames).toSet(); print('Packages with exceptions: $exceptionalPackages'); print('Exception categories:'); for (var category in sortedExceptions) { print(' $category'); } if (categoryOfInterest == null) { print('\n(Note: to show stack traces & nodes for a particular failure,' ' rerun with a search string as an argument.)'); } } ArgResults parseArguments(List args) { ArgParser argParser = ArgParser(); ArgResults parsedArgs; argParser.addFlag('clean', abbr: 'c', defaultsTo: false, help: 'Recursively delete the playground directory before beginning.'); argParser.addFlag('help', abbr: 'h', help: 'Display options'); argParser.addFlag('exception_node_only', defaultsTo: false, negatable: true, help: 'Only print the exception node instead of the full stack trace.'); argParser.addFlag('update', abbr: 'u', defaultsTo: false, negatable: true, help: 'Auto-update fetched packages in the playground.'); argParser.addOption('sdk', abbr: 's', defaultsTo: path.dirname(path.dirname(Platform.resolvedExecutable)), help: 'Select the root of the SDK to analyze against for this run ' '(compiled with --nnbd). For example: ../../xcodebuild/DebugX64NNBD/dart-sdk'); argParser.addMultiOption( 'git_packages', abbr: 'g', defaultsTo: [], help: 'Shallow-clone the given git repositories into a playground area,' ' run pub get on them, and migrate them.', ); argParser.addMultiOption( 'manual_packages', abbr: 'm', defaultsTo: [], help: 'Run migration against packages in these directories. Does not ' 'run pub get, any git commands, or any other preparation.', ); argParser.addMultiOption( 'packages', abbr: 'p', defaultsTo: [], help: 'The list of SDK packages to run the migration against.', ); try { parsedArgs = argParser.parse(args); } on ArgParserException { stderr.writeln(argParser.usage); exit(1); } if (parsedArgs['help'] as bool) { print(argParser.usage); exit(0); } if (parsedArgs.rest.length > 1) { throw 'invalid args. Specify *one* argument to get exceptions of interest.'; } return parsedArgs; } void printWarning(String warn) { stderr.writeln(''' !!! !!! Warning! $warn !!! '''); } void warnOnNoAssertions() { try { assert(false); } catch (e) { return; } printWarning("You didn't --enable-asserts!"); } class ExceptionCategory { final String topOfStack; final List> exceptionCountPerPackage; ExceptionCategory(this.topOfStack, Map exceptions) : exceptionCountPerPackage = exceptions.entries.toList() ..sort((e1, e2) => e2.value.compareTo(e1.value)); int get count => exceptionCountPerPackage.length; List get packageNames => [for (var entry in exceptionCountPerPackage) entry.key]; Iterable get packageNamesAndCounts => exceptionCountPerPackage.map((entry) => '${entry.key} x${entry.value}'); String toString() => '$topOfStack (${packageNamesAndCounts.join(', ')})'; } class _Listener implements NullabilityMigrationListener { /// Set this to `true` to cause just the exception nodes to be printed when /// `_Listener.categoryOfInterest` is non-null. Set this to `false` to cause /// the full stack trace to be printed. final bool printExceptionNodeOnly; /// Set this to a non-null value to cause any exception to be printed in full /// if its category contains the string. final String categoryOfInterest; /// Exception mapped to a map of packages & exception counts. final groupedExceptions = >{}; int numExceptions = 0; int numTypesMadeNullable = 0; int numVariablesMarkedLate = 0; int numInsertedCasts = 0; int numInsertedParenthesis = 0; int numNullChecksAdded = 0; int numMetaImportsAdded = 0; int numRequiredAnnotationsAdded = 0; int numDeadCodeSegmentsFound = 0; int numOtherEdits = 0; String currentPackage; _Listener(this.categoryOfInterest, {this.printExceptionNodeOnly = false}); @override void addEdit(Source source, SourceEdit edit) { if (edit.replacement == '') { return; } if (edit.replacement.contains('!')) { ++numNullChecksAdded; } if (edit.replacement.contains('(')) { ++numInsertedParenthesis; } if (edit.replacement == '?' && edit.length == 0) { ++numTypesMadeNullable; } else if (edit.replacement == "import 'package:meta/meta.dart';\n" && edit.length == 0) { ++numMetaImportsAdded; } else if (edit.replacement == 'required ' && edit.length == 0) { ++numRequiredAnnotationsAdded; } else if (edit.replacement == 'late ' && edit.length == 0) { ++numVariablesMarkedLate; } else if (edit.replacement.startsWith(' as ') && edit.length == 0) { ++numInsertedCasts; } else if ((edit.replacement == '/* ' || edit.replacement == ' /*' || edit.replacement == '; /*') && edit.length == 0) { ++numDeadCodeSegmentsFound; } else if ((edit.replacement == '*/ ' || edit.replacement == ' */' || edit.replacement == ')' || edit.replacement == '!' || edit.replacement == '(') && edit.length == 0) { } else { numOtherEdits++; } } @override void addSuggestion(String descriptions, Location location) {} @override void reportException( Source source, AstNode node, Object exception, StackTrace stackTrace) { var category = _classifyStackTrace(stackTrace.toString().split('\n')); String detail = ''' In file $source While processing $node Exception $exception $stackTrace '''; if (categoryOfInterest != null && category.contains(categoryOfInterest)) { if (printExceptionNodeOnly) { print('$node'); } else { print(detail); } } (groupedExceptions[category] ??= {}) .update(currentPackage, (value) => ++value, ifAbsent: () => 1); ++numExceptions; } String _classifyStackTrace(List stackTrace) { for (var entry in stackTrace) { if (entry.contains('EdgeBuilder._unimplemented')) continue; if (entry.contains('_AssertionError._doThrowNew')) continue; if (entry.contains('_AssertionError._throwNew')) continue; if (entry.contains('NodeBuilder._unimplemented')) continue; if (entry.contains('Object.noSuchMethod')) continue; if (entry.contains('List.[] (dart:core-patch/growable_array.dart')) { continue; } return entry; } return '???'; } }