| // 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. |
| |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/src/dart/analysis/session.dart'; |
| import 'package:analyzer/src/dart/element/element.dart'; |
| import 'package:analyzer/src/dart/element/type_system.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart'; |
| import 'package:nnbd_migration/instrumentation.dart'; |
| import 'package:nnbd_migration/nnbd_migration.dart'; |
| import 'package:nnbd_migration/src/decorated_class_hierarchy.dart'; |
| import 'package:nnbd_migration/src/decorated_type.dart'; |
| import 'package:nnbd_migration/src/edge_builder.dart'; |
| import 'package:nnbd_migration/src/edit_plan.dart'; |
| import 'package:nnbd_migration/src/exceptions.dart'; |
| import 'package:nnbd_migration/src/fix_aggregator.dart'; |
| import 'package:nnbd_migration/src/fix_builder.dart'; |
| import 'package:nnbd_migration/src/node_builder.dart'; |
| import 'package:nnbd_migration/src/nullability_node.dart'; |
| import 'package:nnbd_migration/src/variables.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| /// Implementation of the [NullabilityMigration] public API. |
| class NullabilityMigrationImpl implements NullabilityMigration { |
| final NullabilityMigrationListener? listener; |
| |
| Variables? _variables; |
| |
| final NullabilityGraph _graph; |
| |
| final bool? _permissive; |
| |
| final NullabilityMigrationInstrumentation? _instrumentation; |
| |
| DecoratedClassHierarchy? _decoratedClassHierarchy; |
| |
| bool _propagated = false; |
| |
| /// Indicates whether code removed by the migration engine should be removed |
| /// by commenting it out. A value of `false` means to actually delete the |
| /// code that is removed. |
| final bool? removeViaComments; |
| |
| final bool? warnOnWeakCode; |
| |
| final _decoratedTypeParameterBounds = DecoratedTypeParameterBounds(); |
| |
| /// Map from [Source] object to a boolean indicating whether the source is |
| /// opted in to null safety. |
| final Map<Source, bool> _libraryOptInStatus = {}; |
| |
| /// Indicates whether the client has used the [unmigratedDependencies] getter. |
| bool _queriedUnmigratedDependencies = false; |
| |
| /// Map of additional package dependencies that will be required by the |
| /// migrated code. Keys are package names; values indicate the minimum |
| /// required version of each package. |
| final Map<String, Version> _neededPackages = {}; |
| |
| /// Prepares to perform nullability migration. |
| /// |
| /// If [permissive] is `true`, exception handling logic will try to proceed |
| /// as far as possible even though the migration algorithm is not yet |
| /// complete. TODO(paulberry): remove this mode once the migration algorithm |
| /// is fully implemented. |
| /// |
| /// Optional parameter [removeViaComments] indicates whether code that the |
| /// migration tool wishes to remove should instead be commenting it out. |
| /// |
| /// Optional parameter [warnOnWeakCode] indicates whether weak-only code |
| /// should be warned about or removed (in the way specified by |
| /// [removeViaComments]). |
| NullabilityMigrationImpl(NullabilityMigrationListener? listener, |
| {bool? permissive = false, |
| NullabilityMigrationInstrumentation? instrumentation, |
| bool? removeViaComments = false, |
| bool? warnOnWeakCode = true}) |
| : this._( |
| listener, |
| NullabilityGraph(instrumentation: instrumentation), |
| permissive, |
| instrumentation, |
| removeViaComments, |
| warnOnWeakCode, |
| ); |
| |
| NullabilityMigrationImpl._( |
| this.listener, |
| this._graph, |
| this._permissive, |
| this._instrumentation, |
| this.removeViaComments, |
| this.warnOnWeakCode, |
| ) { |
| _instrumentation?.immutableNodes(_graph.never, _graph.always); |
| } |
| |
| @override |
| bool? get isPermissive => _permissive; |
| |
| @override |
| List<String> get unmigratedDependencies { |
| _queriedUnmigratedDependencies = true; |
| var unmigratedDependencies = <Source>[]; |
| for (var entry in _libraryOptInStatus.entries) { |
| if (_graph.isPathBeingMigrated(entry.key.fullName)) continue; |
| if (!entry.value) { |
| unmigratedDependencies.add(entry.key); |
| } |
| } |
| var badUris = { |
| for (var dependency in unmigratedDependencies) dependency.uri.toString() |
| }.toList(); |
| badUris.sort(); |
| return badUris; |
| } |
| |
| @override |
| void finalizeInput(ResolvedUnitResult result) { |
| if (result.unit.featureSet.isEnabled(Feature.non_nullable)) { |
| // This library has already been migrated; nothing more to do. |
| return; |
| } |
| ExperimentStatusException.sanityCheck(result); |
| if (!_propagated) { |
| _propagated = true; |
| _graph.propagate(); |
| } |
| var unit = result.unit; |
| var compilationUnit = unit.declaredElement!; |
| var library = compilationUnit.library; |
| var source = compilationUnit.source; |
| // Hierarchies were created assuming the libraries being migrated are opted |
| // out, but the FixBuilder will analyze assuming they're opted in. So we |
| // need to clear the hierarchies before we continue. |
| (result.session as AnalysisSessionImpl).clearHierarchies(); |
| var fixBuilder = FixBuilder( |
| source, |
| _decoratedClassHierarchy, |
| result.typeProvider, |
| library.typeSystem as TypeSystemImpl, |
| _variables, |
| library as LibraryElementImpl, |
| _permissive! ? listener : null, |
| unit, |
| warnOnWeakCode, |
| _graph, |
| _neededPackages); |
| try { |
| DecoratedTypeParameterBounds.current = _decoratedTypeParameterBounds; |
| fixBuilder.visitAll(); |
| } finally { |
| DecoratedTypeParameterBounds.current = null; |
| } |
| var changes = FixAggregator.run(unit, result.content, fixBuilder.changes, |
| removeViaComments: removeViaComments, warnOnWeakCode: warnOnWeakCode)!; |
| _instrumentation?.changes(source, changes); |
| final lineInfo = LineInfo.fromContent(source.contents.data); |
| var offsets = changes.keys.toList(); |
| offsets.sort(); |
| for (var offset in offsets) { |
| var edits = changes[offset]!; |
| var descriptions = edits |
| .map((edit) => edit.info) |
| .where((info) => info != null) |
| .map((info) => info!.description.appliedMessage) |
| .join(', '); |
| var sourceEdit = edits.toSourceEdit(offset!); |
| listener!.addSuggestion( |
| descriptions, _computeLocation(lineInfo, sourceEdit, source)); |
| listener!.addEdit(source, sourceEdit); |
| } |
| } |
| |
| Map<String, Version> finish() { |
| _instrumentation?.finished(); |
| return _neededPackages; |
| } |
| |
| void prepareInput(ResolvedUnitResult result) { |
| assert( |
| !_queriedUnmigratedDependencies, |
| 'Should only query unmigratedDependencies after all calls to ' |
| 'prepareInput'); |
| if (result.unit.featureSet.isEnabled(Feature.non_nullable)) { |
| // This library has already been migrated; nothing more to do. |
| return; |
| } |
| ExperimentStatusException.sanityCheck(result); |
| _recordTransitiveImportExportOptInStatus( |
| result.libraryElement.importedLibraries); |
| _recordTransitiveImportExportOptInStatus( |
| result.libraryElement.exportedLibraries); |
| if (_variables == null) { |
| _variables = Variables(_graph, result.typeProvider, |
| instrumentation: _instrumentation); |
| _decoratedClassHierarchy = DecoratedClassHierarchy(_variables, _graph); |
| } |
| var unit = result.unit; |
| try { |
| DecoratedTypeParameterBounds.current = _decoratedTypeParameterBounds; |
| unit.accept(NodeBuilder(_variables, unit.declaredElement!.source, |
| _permissive! ? listener : null, _graph, result.typeProvider, |
| instrumentation: _instrumentation)); |
| } finally { |
| DecoratedTypeParameterBounds.current = null; |
| } |
| } |
| |
| void processInput(ResolvedUnitResult result) { |
| if (result.unit.featureSet.isEnabled(Feature.non_nullable)) { |
| // This library has already been migrated; nothing more to do. |
| return; |
| } |
| ExperimentStatusException.sanityCheck(result); |
| var unit = result.unit; |
| try { |
| DecoratedTypeParameterBounds.current = _decoratedTypeParameterBounds; |
| unit.accept(EdgeBuilder( |
| result.typeProvider, |
| result.typeSystem, |
| _variables, |
| _graph, |
| unit.declaredElement!.source, |
| _permissive! ? listener : null, |
| _decoratedClassHierarchy, |
| result.libraryElement, |
| instrumentation: _instrumentation)); |
| } finally { |
| DecoratedTypeParameterBounds.current = null; |
| } |
| } |
| |
| @override |
| void update() { |
| _graph.update(); |
| } |
| |
| /// Records the opt in/out status of all libraries in [libraries], and any |
| /// libraries they transitively import or export, in [_libraryOptInStatus]. |
| void _recordTransitiveImportExportOptInStatus( |
| Iterable<LibraryElement> libraries) { |
| var librariesToCheck = libraries.toList(); |
| while (librariesToCheck.isNotEmpty) { |
| var library = librariesToCheck.removeLast(); |
| if (_libraryOptInStatus.containsKey(library.source)) continue; |
| _libraryOptInStatus[library.source] = library.isNonNullableByDefault; |
| librariesToCheck.addAll(library.importedLibraries); |
| librariesToCheck.addAll(library.exportedLibraries); |
| } |
| } |
| |
| static Location _computeLocation( |
| LineInfo lineInfo, SourceEdit edit, Source source) { |
| final startLocation = lineInfo.getLocation(edit.offset); |
| final endLocation = lineInfo.getLocation(edit.end); |
| var location = Location( |
| source.fullName, |
| edit.offset, |
| edit.length, |
| startLocation.lineNumber, |
| startLocation.columnNumber, |
| endLine: endLocation.lineNumber, |
| endColumn: endLocation.columnNumber, |
| ); |
| return location; |
| } |
| } |