| // 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:async'; |
| import 'dart:io' hide File; |
| |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/diagnostic/diagnostic.dart'; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/file_system/file_system.dart' |
| show File, ResourceProvider; |
| import 'package:analyzer/file_system/physical_file_system.dart'; |
| import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart'; |
| import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart'; |
| import 'package:analyzer/src/error/codes.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer/src/util/sdk.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' |
| hide AnalysisError; |
| import 'package:args/args.dart'; |
| import 'package:cli_util/cli_logging.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:nnbd_migration/src/edit_plan.dart'; |
| import 'package:nnbd_migration/src/exceptions.dart'; |
| import 'package:nnbd_migration/src/front_end/dartfix_listener.dart'; |
| import 'package:nnbd_migration/src/front_end/driver_provider_impl.dart'; |
| import 'package:nnbd_migration/src/front_end/migration_state.dart'; |
| import 'package:nnbd_migration/src/front_end/non_nullable_fix.dart'; |
| import 'package:nnbd_migration/src/messages.dart'; |
| import 'package:nnbd_migration/src/utilities/progress_bar.dart'; |
| import 'package:nnbd_migration/src/utilities/source_edit_diff_formatter.dart'; |
| import 'package:path/path.dart' show Context; |
| |
| String _pluralize(int count, String single, {String? multiple}) { |
| return count == 1 ? single : (multiple ?? '${single}s'); |
| } |
| |
| String _removePeriod(String value) { |
| return value.endsWith('.') ? value.substring(0, value.length - 1) : value; |
| } |
| |
| /// The result of a round of static analysis; primarily a list of |
| /// [AnalysisError]s. |
| class AnalysisResult { |
| final List<AnalysisError> errors; |
| final Map<String?, LineInfo> lineInfo; |
| final Context pathContext; |
| final String rootDirectory; |
| final bool allSourcesAlreadyMigrated; |
| |
| AnalysisResult(this.errors, this.lineInfo, this.pathContext, |
| this.rootDirectory, this.allSourcesAlreadyMigrated) { |
| errors.sort((AnalysisError one, AnalysisError two) { |
| if (one.source != two.source) { |
| return one.source.fullName.compareTo(two.source.fullName); |
| } |
| return one.offset - two.offset; |
| }); |
| } |
| |
| bool get hasErrors => errors.isNotEmpty; |
| |
| /// Whether the errors include any which may be the result of not yet having |
| /// run "pub get". |
| bool get hasImportErrors => errors.any( |
| (error) => error.errorCode == CompileTimeErrorCode.URI_DOES_NOT_EXIST); |
| |
| /// Converts the list of errors into JSON, for displaying in the web preview. |
| List<Map<String, dynamic>> toJson() { |
| var result = <Map<String, dynamic>>[]; |
| // severity • Message ... at foo/bar.dart:6:1 • (error_code) |
| for (var error in errors) { |
| var lineInfoForThisFile = lineInfo[error.source.fullName]!; |
| var location = lineInfoForThisFile.getLocation(error.offset); |
| var path = |
| pathContext.relative(error.source.fullName, from: rootDirectory); |
| result.add({ |
| 'severity': error.severity.name, |
| 'message': _removePeriod(error.message), |
| 'location': '$path:${location.lineNumber}:${location.columnNumber}', |
| 'code': error.errorCode.name.toLowerCase(), |
| }); |
| } |
| return result; |
| } |
| } |
| |
| /// Data structure recording command-line options for the migration tool that |
| /// have been passed in by the client. |
| class CommandLineOptions { |
| static const applyChangesFlag = 'apply-changes'; |
| static const helpFlag = 'help'; |
| static const ignoreErrorsFlag = 'ignore-errors'; |
| static const ignoreExceptionsFlag = 'ignore-exceptions'; |
| static const previewHostnameOption = 'preview-hostname'; |
| static const previewPortOption = 'preview-port'; |
| static const sdkPathOption = 'sdk-path'; |
| static const skipImportCheckFlag = 'skip-import-check'; |
| static const summaryOption = 'summary'; |
| static const verboseFlag = 'verbose'; |
| static const webPreviewFlag = 'web-preview'; |
| |
| final bool applyChanges; |
| |
| final String directory; |
| |
| final bool? ignoreErrors; |
| |
| final bool? ignoreExceptions; |
| |
| final String? previewHostname; |
| |
| final int? previewPort; |
| |
| final String sdkPath; |
| |
| final bool? skipImportCheck; |
| |
| final String? summary; |
| |
| final bool? webPreview; |
| |
| CommandLineOptions( |
| {required this.applyChanges, |
| required this.directory, |
| required this.ignoreErrors, |
| required this.ignoreExceptions, |
| required this.previewHostname, |
| required this.previewPort, |
| required this.sdkPath, |
| required this.skipImportCheck, |
| required this.summary, |
| required this.webPreview}); |
| } |
| |
| /// Command-line API for the migration tool, with additional parameters exposed |
| /// for testing. |
| /// |
| /// Recommended usage: create an instance of this object and call |
| /// [decodeCommandLineArgs]. If it returns non-null, call |
| /// [MigrationCliRunner.run] on the result. If either method throws a |
| /// [MigrationExit], exit with the error code contained therein. |
| class MigrationCli { |
| /// A list of all the command-line options supported by the tool. |
| /// |
| /// This may be used by clients that wish to run migration but provide their |
| /// own command-line interface. |
| static final List<MigrationCliOption> options = [ |
| MigrationCliOption( |
| CommandLineOptions.verboseFlag, |
| (parser, hide) => parser.addFlag( |
| CommandLineOptions.verboseFlag, |
| abbr: 'v', |
| defaultsTo: false, |
| help: 'Show additional command output.', |
| negatable: false, |
| )), |
| MigrationCliOption( |
| CommandLineOptions.applyChangesFlag, |
| (parser, hide) => parser.addFlag(CommandLineOptions.applyChangesFlag, |
| defaultsTo: false, |
| negatable: false, |
| help: |
| 'Apply the proposed null safety changes to the files on disk.')), |
| MigrationCliOption( |
| CommandLineOptions.ignoreErrorsFlag, |
| (parser, hide) => parser.addFlag( |
| CommandLineOptions.ignoreErrorsFlag, |
| defaultsTo: false, |
| negatable: false, |
| help: |
| 'Attempt to perform null safety analysis even if the project has ' |
| 'analysis errors.', |
| )), |
| MigrationCliOption( |
| CommandLineOptions.skipImportCheckFlag, |
| (parser, hide) => parser.addFlag( |
| CommandLineOptions.skipImportCheckFlag, |
| defaultsTo: false, |
| negatable: false, |
| help: 'Go ahead with migration even if some imported files have ' |
| 'not yet been migrated.', |
| )), |
| MigrationCliOption.separator('Web interface options:'), |
| MigrationCliOption( |
| CommandLineOptions.webPreviewFlag, |
| (parser, hide) => parser.addFlag( |
| CommandLineOptions.webPreviewFlag, |
| defaultsTo: true, |
| negatable: true, |
| help: |
| 'Show an interactive preview of the proposed null safety changes ' |
| 'in a browser window. Use --no-web-preview to print proposed changes ' |
| 'to the console.', |
| )), |
| MigrationCliOption( |
| CommandLineOptions.previewHostnameOption, |
| (parser, hide) => parser.addOption( |
| CommandLineOptions.previewHostnameOption, |
| defaultsTo: 'localhost', |
| valueHelp: 'host', |
| help: 'Run the preview server on the specified hostname. If not ' |
| 'specified, "localhost" is used. Use "any" to specify IPv6.any or ' |
| 'IPv4.any.', |
| )), |
| MigrationCliOption( |
| CommandLineOptions.previewPortOption, |
| (parser, hide) => parser.addOption( |
| CommandLineOptions.previewPortOption, |
| valueHelp: 'port', |
| help: |
| 'Run the preview server on the specified port. If not specified, ' |
| 'dynamically allocate a port.', |
| )), |
| MigrationCliOption.separator('Additional options:'), |
| MigrationCliOption( |
| CommandLineOptions.summaryOption, |
| (parser, hide) => parser.addOption( |
| CommandLineOptions.summaryOption, |
| help: 'Output a machine-readable summary of migration changes.', |
| valueHelp: 'path', |
| )), |
| // hidden options |
| MigrationCliOption( |
| CommandLineOptions.ignoreExceptionsFlag, |
| (parser, hide) => parser.addFlag( |
| CommandLineOptions.ignoreExceptionsFlag, |
| defaultsTo: false, |
| negatable: false, |
| help: |
| 'Attempt to perform null safety analysis even if exceptions occur.', |
| hide: hide, |
| )), |
| MigrationCliOption( |
| CommandLineOptions.sdkPathOption, |
| (parser, hide) => parser.addOption( |
| CommandLineOptions.sdkPathOption, |
| valueHelp: 'sdk-path', |
| help: 'The path to the Dart SDK.', |
| hide: hide, |
| )), |
| ]; |
| |
| static const String migrationGuideLink = |
| 'See https://dart.dev/go/null-safety-migration for a migration guide.'; |
| |
| /// The name of the executable, for reporting in help messages. |
| final String binaryName; |
| |
| /// The SDK path that should be used if none is provided by the user. Used in |
| /// testing to install a mock SDK. |
| final String? defaultSdkPathOverride; |
| |
| /// Factory to create an appropriate Logger instance to give feedback to the |
| /// user. Used in testing to allow user feedback messages to be tested. |
| final Logger Function(bool isVerbose) loggerFactory; |
| |
| /// Resource provider that should be used to access the filesystem. Used in |
| /// testing to redirect to an in-memory filesystem. |
| final ResourceProvider resourceProvider; |
| |
| /// Logger instance we use to give feedback to the user. |
| final Logger logger; |
| |
| /// The environment variables, tracked to help users debug if SDK_PATH was |
| /// specified and that resulted in any [ExperimentStatusException]s. |
| final Map<String, String> _environmentVariables; |
| |
| MigrationCli({ |
| required this.binaryName, |
| @visibleForTesting this.loggerFactory = _defaultLoggerFactory, |
| @visibleForTesting this.defaultSdkPathOverride, |
| @visibleForTesting ResourceProvider? resourceProvider, |
| @visibleForTesting Map<String, String>? environmentVariables, |
| }) : logger = loggerFactory(false), |
| resourceProvider = |
| resourceProvider ?? PhysicalResourceProvider.INSTANCE, |
| _environmentVariables = environmentVariables ?? Platform.environment; |
| |
| Context get pathContext => resourceProvider.pathContext; |
| |
| /// Parses and validates command-line arguments, and creates a |
| /// [MigrationCliRunner] that is prepared to perform migration. |
| /// |
| /// If the user asked for help, it is printed using the logger configured in |
| /// the constructor, and `null` is returned. |
| /// |
| /// If the user supplied a bad option, a message is printed using the logger |
| /// configured in the constructor, and [MigrationExit] is thrown. |
| MigrationCliRunner? decodeCommandLineArgs(ArgResults argResults, |
| {bool? isVerbose}) { |
| try { |
| isVerbose ??= argResults[CommandLineOptions.verboseFlag] as bool?; |
| if (argResults[CommandLineOptions.helpFlag] as bool) { |
| _showUsage(isVerbose!); |
| return null; |
| } |
| var rest = argResults.rest; |
| String migratePath; |
| if (rest.isEmpty) { |
| migratePath = pathContext.current; |
| } else if (rest.length > 1) { |
| throw _BadArgException('No more than one path may be specified.'); |
| } else { |
| migratePath = pathContext |
| .normalize(pathContext.join(pathContext.current, rest[0])); |
| } |
| var migrateResource = resourceProvider.getResource(migratePath); |
| if (migrateResource is File) { |
| if (migrateResource.exists) { |
| throw _BadArgException('$migratePath is a file.'); |
| } else { |
| throw _BadArgException('$migratePath does not exist.'); |
| } |
| } |
| var applyChanges = |
| argResults[CommandLineOptions.applyChangesFlag] as bool; |
| var previewPortRaw = |
| argResults[CommandLineOptions.previewPortOption] as String?; |
| int? previewPort; |
| try { |
| previewPort = previewPortRaw == null ? null : int.parse(previewPortRaw); |
| } on FormatException catch (_) { |
| throw _BadArgException( |
| 'Invalid value for --${CommandLineOptions.previewPortOption}'); |
| } |
| bool? webPreview; |
| if (argResults.wasParsed(CommandLineOptions.webPreviewFlag)) { |
| webPreview = argResults[CommandLineOptions.webPreviewFlag] as bool?; |
| } else { |
| // If the `webPreviewFlag` wasn't explicitly passed, then the value of |
| // this option is based on the value of the [applyChanges] option. |
| webPreview = !applyChanges; |
| } |
| if (applyChanges && webPreview!) { |
| throw _BadArgException('--apply-changes requires --no-web-preview'); |
| } |
| var options = CommandLineOptions( |
| applyChanges: applyChanges, |
| directory: migratePath, |
| ignoreErrors: |
| argResults[CommandLineOptions.ignoreErrorsFlag] as bool?, |
| ignoreExceptions: |
| argResults[CommandLineOptions.ignoreExceptionsFlag] as bool?, |
| previewHostname: |
| argResults[CommandLineOptions.previewHostnameOption] as String?, |
| previewPort: previewPort, |
| sdkPath: argResults[CommandLineOptions.sdkPathOption] as String? ?? |
| defaultSdkPathOverride ?? |
| getSdkPath(), |
| skipImportCheck: |
| argResults[CommandLineOptions.skipImportCheckFlag] as bool?, |
| summary: argResults[CommandLineOptions.summaryOption] as String?, |
| webPreview: webPreview); |
| return MigrationCliRunner(this, options, |
| logger: isVerbose! ? loggerFactory(true) : null); |
| } on Object catch (exception) { |
| handleArgParsingException(exception); |
| return null; |
| } |
| } |
| |
| @alwaysThrows |
| void handleArgParsingException(Object exception) { |
| String message; |
| if (exception is FormatException) { |
| message = exception.message; |
| } else if (exception is _BadArgException) { |
| message = exception.message; |
| } else { |
| message = |
| 'Exception occurred while parsing command-line options: $exception'; |
| } |
| logger.stderr(message); |
| _showUsage(false); |
| throw MigrationExit(1); |
| } |
| |
| void _showUsage(bool isVerbose) { |
| logger.stderr('Usage: $binaryName [options...] [<project directory>]'); |
| |
| logger.stderr(''); |
| logger.stderr(createParser(hide: !isVerbose).usage); |
| if (!isVerbose) { |
| logger.stderr(''); |
| logger |
| .stderr('Run "$binaryName -h -v" for verbose help output, including ' |
| 'less commonly used options.'); |
| } |
| } |
| |
| static ArgParser createParser({bool hide = true}) { |
| var parser = ArgParser(); |
| parser.addFlag(CommandLineOptions.helpFlag, |
| abbr: 'h', |
| help: |
| 'Display this help message. Add --verbose to show hidden options.', |
| defaultsTo: false, |
| negatable: false); |
| defineOptions(parser, hide); |
| return parser; |
| } |
| |
| static void defineOptions(ArgParser parser, bool hide) { |
| for (var option in options) { |
| option.addToParser(parser, hide); |
| } |
| } |
| |
| static Logger _defaultLoggerFactory(bool isVerbose) { |
| var ansi = Ansi(Ansi.terminalSupportsAnsi); |
| if (isVerbose) { |
| return Logger.verbose(ansi: ansi); |
| } else { |
| return Logger.standard(ansi: ansi); |
| } |
| } |
| } |
| |
| /// Data structure representing a single command-line option to the migration |
| /// tool, or a separator in the list of command-line options. |
| class MigrationCliOption { |
| /// The name of the option, without the leading `--`. |
| final String name; |
| |
| /// Callback function that can be used to add the option or separator to the |
| /// given [parser]. If [hide] is `true`, and the option is rarely used, it |
| /// is added as a hidden option. |
| final void Function(ArgParser parser, bool hide) addToParser; |
| |
| /// If `true`, this is a separator between command line options; if `false`, |
| /// it's an option. |
| final bool isSeparator; |
| |
| MigrationCliOption(this.name, this.addToParser) : isSeparator = false; |
| |
| MigrationCliOption.separator(this.name) |
| : addToParser = ((parser, hide) => parser.addSeparator(name)), |
| isSeparator = true; |
| } |
| |
| /// Internals of the command-line API for the migration tool, with additional |
| /// methods exposed for testing. |
| /// |
| /// This class may be used directly by clients that with to run migration but |
| /// provide their own command-line interface. |
| class MigrationCliRunner implements DartFixListenerClient { |
| final MigrationCli cli; |
| |
| /// Logger instance we use to give feedback to the user. |
| final Logger logger; |
| |
| /// The result of parsing command-line options. |
| final CommandLineOptions options; |
| |
| final Map<String?, LineInfo> lineInfo = {}; |
| |
| DartFixListener? _dartFixListener; |
| |
| _FixCodeProcessor? _fixCodeProcessor; |
| |
| AnalysisContextCollectionImpl? _contextCollection; |
| |
| bool _hasExceptions = false; |
| |
| bool _hasAnalysisErrors = false; |
| |
| /// Subscription of interrupt signals (control-C). |
| StreamSubscription<ProcessSignal>? _sigIntSubscription; |
| |
| /// Completes when an interrupt signal (control-C) is received. |
| late Completer<void> sigIntSignalled; |
| |
| MigrationCliRunner(this.cli, this.options, {Logger? logger}) |
| : logger = logger ?? cli.logger; |
| |
| @visibleForTesting |
| DriverBasedAnalysisContext get analysisContext { |
| // Handle the case of more than one analysis context being found (typically, |
| // the current directory and one or more sub-directories). |
| if (hasMultipleAnalysisContext) { |
| return contextCollection!.contextFor(options.directory); |
| } else { |
| return contextCollection!.contexts.single; |
| } |
| } |
| |
| Ansi get ansi => logger.ansi; |
| |
| AnalysisContextCollectionImpl? get contextCollection { |
| _contextCollection ??= AnalysisContextCollectionImpl( |
| includedPaths: [options.directory], |
| resourceProvider: resourceProvider, |
| sdkPath: pathContext.normalize(options.sdkPath)); |
| return _contextCollection; |
| } |
| |
| @visibleForTesting |
| bool get hasMultipleAnalysisContext { |
| return contextCollection!.contexts.length > 1; |
| } |
| |
| @visibleForTesting |
| bool get isPreviewServerRunning => |
| _fixCodeProcessor?.isPreviewServerRunning ?? false; |
| |
| Context get pathContext => resourceProvider.pathContext; |
| |
| ResourceProvider get resourceProvider => cli.resourceProvider; |
| |
| /// Called after changes have been applied on disk. Maybe overridden by a |
| /// derived class. |
| void applyHook() {} |
| |
| /// Computes the internet address that should be passed to `HttpServer.bind` |
| /// when starting the preview server. May be overridden in derived classes. |
| Object? computeBindAddress() { |
| var hostname = options.previewHostname; |
| if (hostname == 'localhost') { |
| return InternetAddress.loopbackIPv4; |
| } else if (hostname == 'any') { |
| return InternetAddress.anyIPv6; |
| } else { |
| return hostname; |
| } |
| } |
| |
| /// Computes the set of file paths that should be analyzed by the migration |
| /// engine. May be overridden by a derived class. |
| /// |
| /// All files to be migrated must be included in the returned set. It is |
| /// permissible for the set to contain additional files that could help the |
| /// migration tool build up a more complete nullability graph (for example |
| /// generated files, or usages of the code-to-be-migrated by one one of its |
| /// clients). |
| /// |
| /// By default returns the set of all `.dart` files contained in the context. |
| Set<String> computePathsToProcess(DriverBasedAnalysisContext context) => |
| context.contextRoot |
| .analyzedFiles() |
| .where((s) => |
| s.endsWith('.dart') && |
| // Any file may have been deleted since its initial analysis. |
| resourceProvider.getFile(s).exists) |
| .toSet(); |
| |
| NonNullableFix createNonNullableFix(DartFixListener listener, |
| ResourceProvider resourceProvider, Object? bindAddress, |
| {List<String> included = const <String>[], |
| int? preferredPort, |
| String? summaryPath, |
| required String sdkPath}) { |
| return NonNullableFix(listener, resourceProvider, bindAddress, logger, |
| (String? path) => shouldBeMigrated(path!), |
| included: included, |
| preferredPort: preferredPort, |
| summaryPath: summaryPath, |
| sdkPath: sdkPath); |
| } |
| |
| /// Subscribes to the interrupt signal (control-C). |
| @visibleForTesting |
| void listenForSignalInterrupt() { |
| var stream = ProcessSignal.sigint.watch(); |
| sigIntSignalled = Completer(); |
| _sigIntSubscription = stream.listen((_) { |
| if (!sigIntSignalled.isCompleted) { |
| sigIntSignalled.complete(); |
| } |
| }); |
| } |
| |
| @override |
| void onException(String detail) { |
| if (_hasExceptions) { |
| if (!options.ignoreExceptions!) { |
| // Our intention is to exit immediately when an exception occurred. We |
| // tried, but failed (probably due to permissive mode logic in the |
| // migration tool itself catching the MigrationExit exception). The |
| // stack has now been unwound further, so throw again. |
| throw MigrationExit(1); |
| } |
| // We're not exiting immediately when an exception occurs. We've already |
| // reported that an exception happened. So do nothing further. |
| return; |
| } |
| _hasExceptions = true; |
| if (options.ignoreExceptions!) { |
| logger.stdout(''' |
| Exception(s) occurred during migration. Attempting to perform |
| migration anyway due to the use of --${CommandLineOptions.ignoreExceptionsFlag}. |
| |
| To see exception details, re-run without --${CommandLineOptions.ignoreExceptionsFlag}. |
| '''); |
| } else { |
| if (_hasAnalysisErrors) { |
| logger.stderr(''' |
| Aborting migration due to an exception. This may be due to a bug in |
| the migration tool, or it may be due to errors in the source code |
| being migrated. If possible, try to fix errors in the source code and |
| re-try migrating. If that doesn't work, consider filing a bug report |
| at: |
| '''); |
| } else { |
| logger.stderr(''' |
| Aborting migration due to an exception. This most likely is due to a |
| bug in the migration tool. Please consider filing a bug report at: |
| '''); |
| } |
| logger.stderr('https://github.com/dart-lang/sdk/issues/new'); |
| var sdkVersion = Platform.version.split(' ')[0]; |
| logger.stderr(''' |
| Please include the SDK version ($sdkVersion) in your bug report. |
| |
| To attempt to perform migration anyway, you may re-run with |
| --${CommandLineOptions.ignoreExceptionsFlag}. |
| |
| Exception details: |
| '''); |
| logger.stderr(detail); |
| throw MigrationExit(1); |
| } |
| } |
| |
| @override |
| void onFatalError(String detail) { |
| logger.stderr(detail); |
| throw MigrationExit(1); |
| } |
| |
| @override |
| void onMessage(String detail) { |
| logger.stdout(detail); |
| } |
| |
| /// Runs the full migration process. |
| /// |
| /// If something goes wrong, a message is printed using the logger configured |
| /// in the constructor, and [MigrationExit] is thrown. |
| Future<void> run() async { |
| logger.stdout('Migrating ${options.directory}'); |
| logger.stdout(''); |
| |
| logger.stdout(MigrationCli.migrationGuideLink); |
| logger.stdout(''); |
| |
| if (hasMultipleAnalysisContext) { |
| logger.stdout('Note: more than one project found; migrating the ' |
| 'top-level project.'); |
| logger.stdout(''); |
| } |
| |
| NonNullableFix nonNullableFix; |
| |
| logger.stdout(ansi.emphasized('Analyzing project...')); |
| _fixCodeProcessor = _FixCodeProcessor(analysisContext, this); |
| _dartFixListener = DartFixListener( |
| DriverProviderImpl(resourceProvider, analysisContext), this); |
| nonNullableFix = createNonNullableFix( |
| _dartFixListener!, resourceProvider, computeBindAddress(), |
| included: [options.directory], |
| preferredPort: options.previewPort, |
| summaryPath: options.summary, |
| sdkPath: options.sdkPath); |
| nonNullableFix.rerunFunction = _rerunFunction; |
| _fixCodeProcessor!.registerCodeTask(nonNullableFix); |
| |
| try { |
| var analysisResult = await _fixCodeProcessor!.runFirstPhase(); |
| |
| if (analysisResult.hasErrors) { |
| _logErrors(analysisResult); |
| if (!options.ignoreErrors!) { |
| throw MigrationExit(1); |
| } |
| } else if (analysisResult.allSourcesAlreadyMigrated) { |
| _logAlreadyMigrated(); |
| throw MigrationExit(0); |
| } else { |
| logger.stdout('No analysis issues found.'); |
| } |
| } on ExperimentStatusException catch (e) { |
| logger.stdout(e.toString()); |
| final sdkPathVar = cli._environmentVariables['SDK_PATH']; |
| if (sdkPathVar != null) { |
| logger.stdout('$sdkPathEnvironmentVariableSet: $sdkPathVar'); |
| } |
| throw MigrationExit(1); |
| } |
| |
| logger.stdout(''); |
| logger.stdout(ansi.emphasized('Generating migration suggestions...')); |
| var previewUrls = (await _fixCodeProcessor!.runLaterPhases()).previewUrls; |
| |
| if (options.applyChanges) { |
| logger.stdout(ansi.emphasized('Applying changes:')); |
| |
| var allEdits = _dartFixListener!.sourceChange.edits; |
| _applyMigrationSuggestions(allEdits); |
| |
| logger.stdout(''); |
| logger.stdout( |
| 'Applied ${allEdits.length} ${_pluralize(allEdits.length, 'edit')}.'); |
| |
| // Note: do not open the web preview if apply-changes is specified, as we |
| // currently cannot tell the web preview to disable the "apply migration" |
| // button. |
| return; |
| } |
| |
| if (options.webPreview!) { |
| assert(previewUrls!.length == 1, |
| 'Got unexpected extra preview URLs from server'); |
| |
| var url = previewUrls!.single; |
| // TODO(#41809): Open a browser automatically. |
| logger.stdout(''' |
| View the migration suggestions by visiting: |
| |
| ${ansi.emphasized(url)} |
| |
| Use this interactive web view to review, improve, or apply the results. |
| When finished with the preview, hit ctrl-c to terminate this process. |
| |
| If you make edits outside of the web view (in your IDE), use the 'Rerun from |
| sources' action. |
| |
| '''); |
| |
| listenForSignalInterrupt(); |
| await Future.any([ |
| sigIntSignalled.future, |
| nonNullableFix.serverIsShutdown.future, |
| ]); |
| // Either the interrupt signal was caught, or the server was shutdown. |
| // Either way, cancel the interrupt signal subscription, and shutdown the |
| // server. |
| _sigIntSubscription?.cancel(); |
| nonNullableFix.shutdownServer(); |
| } else { |
| logger.stdout(ansi.emphasized('Diff of changes:')); |
| |
| _displayChangeDiff(_dartFixListener!); |
| |
| logger.stdout(''); |
| logger.stdout('To apply these changes, re-run the tool with ' |
| '--${CommandLineOptions.applyChangesFlag}.'); |
| } |
| } |
| |
| /// Determines whether a migrated version of the file at [path] should be |
| /// output by the migration too. May be overridden by a derived class. |
| /// |
| /// This method should return `false` for files that are being considered by |
| /// the migration tool for information only (for example generated files, or |
| /// usages of the code-to-be-migrated by one one of its clients). |
| /// |
| /// By default returns `true` if the file is contained within the context |
| /// root. This means that if a client overrides [computePathsToProcess] to |
| /// return additional paths that aren't inside the user's project, but doesn't |
| /// override this method, then those additional paths will be analyzed but not |
| /// migrated. |
| bool shouldBeMigrated(String path) { |
| return analysisContext.contextRoot.isAnalyzed(path); |
| } |
| |
| /// Perform the indicated source edits to the given source, returning the |
| /// resulting transformed text. |
| String _applyEdits(SourceFileEdit sourceFileEdit, String source) { |
| List<SourceEdit> edits = _sortEdits(sourceFileEdit); |
| return SourceEdit.applySequence(source, edits); |
| } |
| |
| void _applyMigrationSuggestions(List<SourceFileEdit> edits) { |
| // Apply the changes to disk. |
| for (SourceFileEdit sourceFileEdit in edits) { |
| String relPath = |
| pathContext.relative(sourceFileEdit.file, from: options.directory); |
| int count = sourceFileEdit.edits.length; |
| logger.stdout(' $relPath ($count ${_pluralize(count, 'change')})'); |
| |
| String? source; |
| var file = resourceProvider.getFile(sourceFileEdit.file); |
| try { |
| source = file.readAsStringSync(); |
| } catch (_) {} |
| |
| if (source == null) { |
| logger.stdout(' Unable to retrieve source for file.'); |
| } else { |
| source = _applyEdits(sourceFileEdit, source); |
| |
| try { |
| file.writeAsStringSync(source); |
| } catch (e) { |
| logger.stdout(' Unable to write source for file: $e'); |
| } |
| } |
| } |
| applyHook(); |
| } |
| |
| void _displayChangeDiff(DartFixListener migrationResults) { |
| Map<String, List<DartFixSuggestion>> fileSuggestions = {}; |
| for (DartFixSuggestion suggestion in migrationResults.suggestions) { |
| String file = suggestion.location.file; |
| fileSuggestions.putIfAbsent(file, () => <DartFixSuggestion>[]); |
| fileSuggestions[file]!.add(suggestion); |
| } |
| |
| // present a diff-like view |
| var diffStyle = DiffStyle(logger.ansi); |
| for (SourceFileEdit sourceFileEdit in migrationResults.sourceChange.edits) { |
| String file = sourceFileEdit.file; |
| String relPath = pathContext.relative(file, from: options.directory); |
| var edits = sourceFileEdit.edits; |
| int count = edits.length; |
| |
| logger.stdout(''); |
| logger.stdout('${ansi.emphasized(relPath)} ' |
| '($count ${_pluralize(count, 'change')}):'); |
| |
| String? source; |
| try { |
| source = resourceProvider.getFile(file).readAsStringSync(); |
| } catch (_) {} |
| |
| if (source == null) { |
| logger.stdout(' (unable to retrieve source for file)'); |
| } else { |
| for (var line |
| in diffStyle.formatDiff(source, _sourceEditsToAtomicEdits(edits))) { |
| logger.stdout(' $line'); |
| } |
| } |
| } |
| } |
| |
| void _logAlreadyMigrated() { |
| logger.stdout(migratedAlready); |
| } |
| |
| void _logErrors(AnalysisResult analysisResult) { |
| logger.stdout(''); |
| |
| var issueCount = analysisResult.errors.length; |
| logger.stdout( |
| '$issueCount analysis ${_pluralize(issueCount, 'issue')} found:'); |
| |
| _IssueRenderer renderer = |
| _IssueRenderer(logger, options.directory, pathContext, lineInfo); |
| for (AnalysisError error in analysisResult.errors) { |
| renderer.render(error); |
| } |
| logger.stdout(''); |
| _hasAnalysisErrors = true; |
| |
| if (options.ignoreErrors!) { |
| logger.stdout('Note: analysis errors will result in erroneous migration ' |
| 'suggestions.'); |
| logger.stdout('Continuing with migration suggestions due to the use of ' |
| '--${CommandLineOptions.ignoreErrorsFlag}.'); |
| } else { |
| // Fail with how to continue. |
| logger.stdout("The migration tool didn't start, due to analysis errors."); |
| logger.stdout(''); |
| if (analysisResult.hasImportErrors) { |
| logger.stdout(''' |
| The following steps might fix your problem: |
| 1. Run `dart pub get`. |
| 2. Try running `dart migrate` again. |
| '''); |
| } else if (analysisResult.allSourcesAlreadyMigrated) { |
| logger.stdout(''' |
| The following steps might fix your problem: |
| 1. Set the lower SDK constraint (in pubspec.yaml) to a version before 2.12. |
| 2. Run `dart pub get`. |
| 3. Try running `dart migrate` again. |
| '''); |
| } else { |
| const ignoreErrors = CommandLineOptions.ignoreErrorsFlag; |
| logger.stdout(''' |
| We recommend fixing the analysis issues before running `dart migrate`. |
| Alternatively, you can run `dart migrate --$ignoreErrors`, but you might |
| get erroneous migration suggestions. |
| '''); |
| } |
| logger.stdout( |
| 'More information: https://dart.dev/go/null-safety-migration'); |
| } |
| } |
| |
| Future<MigrationState> _rerunFunction() async { |
| logger.stdout(ansi.emphasized('Re-analyzing project...')); |
| |
| _dartFixListener!.reset(); |
| await _fixCodeProcessor!.prepareToRerun(); |
| var analysisResult = await _fixCodeProcessor!.runFirstPhase(); |
| if (analysisResult.hasErrors && !options.ignoreErrors!) { |
| _logErrors(analysisResult); |
| return MigrationState( |
| _fixCodeProcessor!._task!.migration, |
| _fixCodeProcessor!._task!.includedRoot, |
| _dartFixListener, |
| _fixCodeProcessor!._task!.instrumentationListener, |
| {}, |
| _fixCodeProcessor!._task!.shouldBeMigratedFunction, |
| analysisResult); |
| } else if (analysisResult.allSourcesAlreadyMigrated) { |
| _logAlreadyMigrated(); |
| return MigrationState( |
| _fixCodeProcessor!._task!.migration, |
| _fixCodeProcessor!._task!.includedRoot, |
| _dartFixListener, |
| _fixCodeProcessor!._task!.instrumentationListener, |
| {}, |
| _fixCodeProcessor!._task!.shouldBeMigratedFunction, |
| analysisResult); |
| } else { |
| logger.stdout(ansi.emphasized('Re-generating migration suggestions...')); |
| return await _fixCodeProcessor!.runLaterPhases(); |
| } |
| } |
| |
| List<SourceEdit> _sortEdits(SourceFileEdit sourceFileEdit) { |
| // Sort edits in reverse offset order. |
| List<SourceEdit> edits = sourceFileEdit.edits.toList(); |
| edits.sort((a, b) { |
| return b.offset - a.offset; |
| }); |
| return edits; |
| } |
| |
| static Map<int, List<AtomicEdit>> _sourceEditsToAtomicEdits( |
| List<SourceEdit> edits) { |
| return { |
| for (var edit in edits) |
| edit.offset: [AtomicEdit.replace(edit.length, edit.replacement)] |
| }; |
| } |
| } |
| |
| /// Exception thrown by [MigrationCli] if the client should exit. |
| class MigrationExit { |
| /// The exit code that the client should set. |
| final int exitCode; |
| |
| MigrationExit(this.exitCode); |
| } |
| |
| /// An abstraction over the static methods on [Process]. |
| /// |
| /// Used in tests to run mock processes. |
| abstract class ProcessManager { |
| const factory ProcessManager.system() = SystemProcessManager; |
| |
| /// Run a process synchronously, as in [Process.runSync]. |
| ProcessResult runSync(String executable, List<String> arguments, |
| {String? workingDirectory}); |
| } |
| |
| /// A [ProcessManager] that directs all method calls to static methods of |
| /// [Process], in order to run real processes. |
| class SystemProcessManager implements ProcessManager { |
| const SystemProcessManager(); |
| |
| ProcessResult runSync(String executable, List<String> arguments, |
| {String? workingDirectory}) => |
| Process.runSync(executable, arguments, |
| workingDirectory: workingDirectory ?? Directory.current.path); |
| } |
| |
| class _BadArgException implements Exception { |
| final String message; |
| |
| _BadArgException(this.message); |
| } |
| |
| class _FixCodeProcessor extends Object { |
| static const numPhases = 3; |
| |
| final DriverBasedAnalysisContext context; |
| |
| /// The task used to migrate to NNBD. |
| NonNullableFix? _task; |
| |
| Set<String> pathsToProcess; |
| |
| late ProgressBar _progressBar; |
| |
| final MigrationCliRunner _migrationCli; |
| |
| _FixCodeProcessor(this.context, this._migrationCli) |
| : pathsToProcess = _migrationCli.computePathsToProcess(context); |
| |
| bool get isPreviewServerRunning => _task?.isPreviewServerRunning ?? false; |
| |
| Future<void> prepareToRerun() async { |
| var driver = context.driver; |
| pathsToProcess = _migrationCli.computePathsToProcess(context); |
| pathsToProcess.forEach(driver.changeFile); |
| await driver.applyPendingFileChanges(); |
| } |
| |
| /// Call the supplied [process] function to process each compilation unit. |
| Future<void> processResources( |
| Future<void> Function(ResolvedUnitResult result) process) async { |
| var driver = context.currentSession; |
| var pathsProcessed = <String?>{}; |
| for (var path in pathsToProcess) { |
| if (pathsProcessed.contains(path)) continue; |
| var result = await driver.getResolvedLibrary(path); |
| // Parts will either be found in a library, below, or if the library |
| // isn't [isIncluded], will be picked up in the final loop. |
| if (result is ResolvedLibraryResult) { |
| for (var unit in result.units) { |
| if (!pathsProcessed.contains(unit.path)) { |
| await process(unit); |
| pathsProcessed.add(unit.path); |
| } |
| } |
| } |
| } |
| |
| for (var path in pathsToProcess.difference(pathsProcessed)) { |
| var result = await driver.getResolvedUnit(path); |
| if (result is ResolvedUnitResult) { |
| await process(result); |
| } |
| } |
| } |
| |
| void registerCodeTask(NonNullableFix task) { |
| _task = task; |
| } |
| |
| Future<AnalysisResult> runFirstPhase() async { |
| var analysisErrors = <AnalysisError>[]; |
| |
| // All tasks should be registered; [numPhases] should be finalized. |
| _progressBar = ProgressBar(_migrationCli.logger, pathsToProcess.length); |
| |
| // Process each source file. |
| bool allSourcesAlreadyMigrated = true; |
| await processResources((ResolvedUnitResult result) async { |
| if (!result.unit.featureSet.isEnabled(Feature.non_nullable)) { |
| allSourcesAlreadyMigrated = false; |
| } |
| _progressBar.tick(); |
| List<AnalysisError> errors = result.errors |
| .where((error) => error.severity == Severity.error) |
| .toList(); |
| if (errors.isNotEmpty) { |
| analysisErrors.addAll(errors); |
| _migrationCli.lineInfo[result.path] = result.lineInfo; |
| } |
| if (_migrationCli.options.ignoreErrors! || analysisErrors.isEmpty) { |
| await _task!.prepareUnit(result); |
| } |
| }); |
| |
| var unmigratedDependencies = _task!.migration!.unmigratedDependencies; |
| if (unmigratedDependencies.isNotEmpty) { |
| if (_migrationCli.options.skipImportCheck!) { |
| _migrationCli.logger.stdout(unmigratedDependenciesWarning); |
| } else { |
| throw ExperimentStatusException.unmigratedDependencies( |
| unmigratedDependencies); |
| } |
| } |
| |
| return AnalysisResult( |
| analysisErrors, |
| _migrationCli.lineInfo, |
| _migrationCli.pathContext, |
| _migrationCli.options.directory, |
| allSourcesAlreadyMigrated); |
| } |
| |
| Future<MigrationState> runLaterPhases() async { |
| _progressBar = ProgressBar( |
| _migrationCli.logger, pathsToProcess.length * (numPhases - 1)); |
| |
| await processResources((ResolvedUnitResult result) async { |
| _progressBar.tick(); |
| await _task!.processUnit(result); |
| }); |
| await processResources((ResolvedUnitResult result) async { |
| _progressBar.tick(); |
| if (_migrationCli.shouldBeMigrated(result.path)) { |
| await _task!.finalizeUnit(result); |
| } |
| }); |
| _progressBar.complete(); |
| _migrationCli.logger.stdout(_migrationCli.ansi |
| .emphasized('Compiling instrumentation information...')); |
| // Update the tasks paths-to-process, in case of new or deleted files. |
| _task!.pathsToProcess = pathsToProcess; |
| var state = await _task!.finish(); |
| _task!.processPackage(context.contextRoot.root, state.neededPackages); |
| if (_migrationCli.options.webPreview!) { |
| await _task!.startPreviewServer(state, _migrationCli.applyHook); |
| } |
| state.previewUrls = _task!.previewUrls; |
| |
| return state; |
| } |
| } |
| |
| /// Given a Logger and an analysis issue, render the issue to the logger. |
| class _IssueRenderer { |
| final Logger logger; |
| final String rootDirectory; |
| final Context pathContext; |
| final Map<String?, LineInfo> lineInfo; |
| |
| _IssueRenderer( |
| this.logger, this.rootDirectory, this.pathContext, this.lineInfo); |
| |
| void render(AnalysisError issue) { |
| // severity • Message ... at foo/bar.dart:6:1 • (error_code) |
| var lineInfoForThisFile = lineInfo[issue.source.fullName]!; |
| var location = lineInfoForThisFile.getLocation(issue.offset); |
| |
| final Ansi ansi = logger.ansi; |
| |
| logger.stdout( |
| ' ${ansi.error(issue.severity.name)} • ' |
| '${ansi.emphasized(_removePeriod(issue.message))} ' |
| 'at ${pathContext.relative(issue.source.fullName, from: rootDirectory)}' |
| ':${location.lineNumber}:${location.columnNumber} ' |
| '• (${issue.errorCode.name.toLowerCase()})', |
| ); |
| } |
| } |
| |
| extension on Severity { |
| /// Returns the simple name of the Severity, as a String. |
| String get name { |
| switch (this) { |
| case Severity.error: |
| return 'error'; |
| case Severity.warning: |
| return 'warning'; |
| case Severity.info: |
| return 'info'; |
| } |
| } |
| } |