blob: 805408963d402ad026e1e2d2fc733e26dab11e9a [file] [log] [blame]
// 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/file_system/physical_file_system.dart';
import 'package:analyzer/src/dart/analysis/session.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/postmortem_file.dart';
import 'package:nnbd_migration/src/variables.dart';
/// Implementation of the [NullabilityMigration] public API.
class NullabilityMigrationImpl implements NullabilityMigration {
/// Set this constant to a pathname to cause nullability migration to output
/// a post-mortem file that can be later examined by tool/postmortem.dart.
static const String _postmortemPath = null;
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();
/// If not `null`, the object that will be used to write out post-mortem
/// information once migration is complete.
final PostmortemFileWriter _postmortemFileWriter =
_makePostmortemFileWriter();
final LineInfo Function(String) _getLineInfo;
/// 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,
LineInfo Function(String) getLineInfo,
{bool permissive = false,
NullabilityMigrationInstrumentation instrumentation,
bool removeViaComments = false,
bool warnOnWeakCode = true})
: this._(
listener,
NullabilityGraph(instrumentation: instrumentation),
permissive,
instrumentation,
removeViaComments,
warnOnWeakCode,
getLineInfo);
NullabilityMigrationImpl._(
this.listener,
this._graph,
this._permissive,
this._instrumentation,
this.removeViaComments,
this.warnOnWeakCode,
this._getLineInfo) {
_instrumentation?.immutableNodes(_graph.never, _graph.always);
_postmortemFileWriter?.graph = _graph;
}
@override
bool get isPermissive => _permissive;
@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(_postmortemFileWriter);
}
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,
_permissive ? listener : null,
unit,
warnOnWeakCode,
_graph);
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);
}
}
void finish() {
if (!_propagated) {
// [finalizeInput] sets this field to `true`, so if it's still false, that
// means it was never called; this probably means that all the code fed
// to the migration tool was already migrated.
throw ExperimentStatusException.migratedAlready();
}
_postmortemFileWriter?.write();
_instrumentation?.finished();
}
void prepareInput(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 (_variables == null) {
_variables = Variables(_graph, result.typeProvider, _getLineInfo,
instrumentation: _instrumentation,
postmortemFileWriter: _postmortemFileWriter);
_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,
_getLineInfo,
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,
instrumentation: _instrumentation));
} finally {
DecoratedTypeParameterBounds.current = null;
}
}
@override
void update() {
_graph.update(_postmortemFileWriter);
}
static Location _computeLocation(
LineInfo lineInfo, SourceEdit edit, Source source) {
final locationInfo = lineInfo.getLocation(edit.offset);
var location = Location(
source.fullName,
edit.offset,
edit.length,
locationInfo.lineNumber,
locationInfo.columnNumber,
);
return location;
}
static PostmortemFileWriter _makePostmortemFileWriter() {
if (_postmortemPath == null) return null;
return PostmortemFileWriter(
PhysicalResourceProvider.INSTANCE.getFile(_postmortemPath));
}
}