| // 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/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'; |
| |
| main(List<String> args) async { |
| ArgResults parsedArgs = parseArguments(args); |
| |
| Sdk sdk = Sdk(parsedArgs['sdk'] as String); |
| |
| warnOnNoAssertions(); |
| |
| Playground playground = |
| Playground(defaultPlaygroundPath, parsedArgs['clean'] as bool); |
| |
| List<Package> packages = [ |
| for (String package in parsedArgs['packages'] as Iterable<String>) |
| SdkPackage(package), |
| for (String package in parsedArgs['manual_packages'] as Iterable<String>) |
| ManualPackage(package), |
| ]; |
| |
| var packageNames = parsedArgs['git_packages'] as Iterable<String>; |
| 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 = <String>{}; |
| 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.getFile(path).lineInfo; |
| var migration = |
| NullabilityMigration(listener, getLineInfo, permissive: true); |
| for (var file in localFiles) { |
| var resolvedUnit = await session.getResolvedUnit(file); |
| 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.getResolvedUnit(file); |
| if (!resolvedUnit.errors.any((e) => e.severity == Severity.error)) { |
| migration.processInput(resolvedUnit); |
| } |
| } |
| for (var file in localFiles) { |
| var resolvedUnit = await session.getResolvedUnit(file); |
| 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<String> 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<MapEntry<String, int>> exceptionCountPerPackage; |
| |
| ExceptionCategory(this.topOfStack, Map<String, int> exceptions) |
| : this.exceptionCountPerPackage = exceptions.entries.toList() |
| ..sort((e1, e2) => e2.value.compareTo(e1.value)); |
| |
| int get count => exceptionCountPerPackage.length; |
| |
| List<String> get packageNames => |
| [for (var entry in exceptionCountPerPackage) entry.key]; |
| |
| Iterable<String> 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 = <String, Map<String, int>>{}; |
| |
| 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] ??= <String, int>{}) |
| .update(currentPackage, (value) => ++value, ifAbsent: () => 1); |
| ++numExceptions; |
| } |
| |
| String _classifyStackTrace(List<String> 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 '???'; |
| } |
| } |