blob: 950e06f8a8ba1c82e35fd1f2531d3a35705f58ab [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:analysis_server/src/analysis_server.dart';
import 'package:analysis_server/src/domains/analysis/navigation_dart.dart';
import 'package:analysis_server/src/edit/fix/dartfix_listener.dart';
import 'package:analysis_server/src/edit/nnbd_migration/instrumentation_information.dart';
import 'package:analysis_server/src/edit/nnbd_migration/migration_info.dart';
import 'package:analysis_server/src/edit/nnbd_migration/offset_mapper.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
show Location, SourceEdit, SourceFileEdit;
import 'package:analyzer_plugin/protocol/protocol_common.dart' as protocol;
import 'package:analyzer_plugin/src/utilities/navigation/navigation.dart';
import 'package:nnbd_migration/instrumentation.dart';
import 'package:nnbd_migration/nnbd_migration.dart';
class FixInfo {
/// The fix being described.
SingleNullabilityFix fix;
/// The reasons why the fix was made.
List<FixReasonInfo> reasons;
/// Initialize information about a fix from the given map [entry].
FixInfo(this.fix, this.reasons);
}
/// A builder used to build the migration information for a library.
class InfoBuilder {
/// The instrumentation information gathered while the migration engine was
/// running.
final InstrumentationInformation info;
/// The listener used to gather the changes to be applied.
final DartFixListener listener;
/// A map from the path of a compilation unit to the information about that
/// unit.
final Map<String, UnitInfo> unitMap = {};
/// Initialize a newly created builder.
InfoBuilder(this.info, this.listener);
/// The analysis server used to get information about libraries.
AnalysisServer get server => listener.server;
/// Return the migration information for all of the libraries that were
/// migrated.
Future<List<UnitInfo>> explainMigration() async {
Map<Source, SourceInformation> sourceInfoMap = info.sourceInformation;
List<UnitInfo> units = [];
for (Source source in sourceInfoMap.keys) {
String filePath = source.fullName;
AnalysisSession session =
server.getAnalysisDriver(filePath).currentSession;
if (!session.getFile(filePath).isPart) {
ResolvedLibraryResult result =
await session.getResolvedLibrary(filePath);
SourceInformation sourceInfo = sourceInfoMap[source];
for (ResolvedUnitResult unitResult in result.units) {
SourceFileEdit edit =
listener.sourceChange.getFileEdit(unitResult.path);
units.add(_explainUnit(sourceInfo, unitResult, edit));
}
}
}
return units;
}
/// Return details for a fix built from the given [edge], or `null` if the
/// edge does not have an origin.
String _buildDescriptionForDestination(AstNode node) {
// Other found types:
// - ConstructorDeclaration
if (node.parent is FormalParameterList) {
return "A nullable value can't be passed as an argument";
} else {
return "A nullable value can't be used here";
}
}
/// Return details for a fix built from the given [edge], or `null` if the
/// edge does not have an origin.
String _buildDescriptionForOrigin(AstNode node) {
String /*!*/ description;
if (node.parent is ArgumentList) {
if (node is NullLiteral) {
description = "An explicit 'null' is passed as an argument";
} else {
description = "A nullable value is explicitly passed as an argument";
}
} else {
if (node is NullLiteral) {
description = "An explicit 'null' is assigned";
} else {
description = "A nullable value is assigned";
}
}
if (_inTestCode(node)) {
description += " in test code";
}
return description;
}
/// Compute the details for the fix with the given [fixInfo].
List<RegionDetail> _computeDetails(FixInfo fixInfo) {
List<RegionDetail> details = [];
for (FixReasonInfo reason in fixInfo.reasons) {
if (reason is NullabilityNodeInfo) {
for (EdgeInfo edge in reason.upstreamEdges) {
if (edge.isTriggered) {
EdgeOriginInfo origin = info.edgeOrigin[edge];
if (origin != null) {
// TODO(brianwilkerson) If the origin is an InheritanceOrigin then
// the node is the method declaration in the subclass and we want
// to link to the corresponding parameter in the declaration in
// the superclass.
details.add(RegionDetail(_buildDescriptionForOrigin(origin.node),
_targetForNode(origin.source.fullName, origin.node)));
}
}
}
} else if (reason is EdgeInfo) {
NullabilityNodeInfo destination = reason.destinationNode;
var nodeInfo = info.nodeInfoFor(destination);
if (nodeInfo != null) {
details.add(RegionDetail(
_buildDescriptionForDestination(nodeInfo.astNode),
_targetForNode(nodeInfo.filePath, nodeInfo.astNode)));
}
} else {
throw UnimplementedError(
'Unexpected class of reason: ${reason.runtimeType}');
}
}
return details;
}
/// Return the navigation sources for the unit associated with the [result].
List<NavigationSource> _computeNavigationSources(ResolvedUnitResult result) {
NavigationCollectorImpl collector = new NavigationCollectorImpl();
computeDartNavigation(
result.session.resourceProvider, collector, result.unit, null, null);
collector.createRegions();
List<String> files = collector.files;
List<protocol.NavigationRegion> regions = collector.regions;
List<protocol.NavigationTarget> rawTargets = collector.targets;
List<NavigationTarget> convertedTargets =
List<NavigationTarget>(rawTargets.length);
return regions.map((region) {
List<int> targets = region.targets;
if (targets.isEmpty) {
throw StateError('Targets is empty');
}
NavigationTarget target = convertedTargets[targets[0]];
if (target == null) {
protocol.NavigationTarget rawTarget = rawTargets[targets[0]];
target = _targetFor(
files[rawTarget.fileIndex], rawTarget.offset, rawTarget.length);
convertedTargets[targets[0]] = target;
}
return NavigationSource(region.offset, region.length, target);
}).toList();
}
/// Return the migration information for the unit associated with the
/// [result].
UnitInfo _explainUnit(SourceInformation sourceInfo, ResolvedUnitResult result,
SourceFileEdit fileEdit) {
UnitInfo unitInfo = _unitForPath(result.path);
if (unitInfo.sources == null) {
unitInfo.sources = _computeNavigationSources(result);
}
String content = result.content;
// [fileEdit] is null when a file has no edits.
if (fileEdit != null) {
List<RegionInfo> regions = unitInfo.regions;
List<SourceEdit> edits = fileEdit.edits;
edits.sort((first, second) => first.offset.compareTo(second.offset));
OffsetMapper mapper = OffsetMapper.forEdits(edits);
// Apply edits in reverse order and build the regions.
for (SourceEdit edit in edits.reversed) {
int offset = edit.offset;
int length = edit.length;
String replacement = edit.replacement;
int end = offset + length;
// Insert the replacement text without deleting the replaced text.
content = content.replaceRange(end, end, replacement);
FixInfo fixInfo = _findFixInfo(sourceInfo, offset);
if (fixInfo != null) {
String explanation = '${fixInfo.fix.description.appliedMessage}.';
List<RegionDetail> details = _computeDetails(fixInfo);
if (length > 0) {
regions.add(
RegionInfo(mapper.map(offset), length, explanation, details));
}
regions.add(RegionInfo(
mapper.map(end), replacement.length, explanation, details));
}
}
regions.sort((first, second) => first.offset.compareTo(second.offset));
unitInfo.offsetMapper = mapper;
}
unitInfo.content = content;
return unitInfo;
}
/// Return information about the fix that was applied at the given [offset],
/// or `null` if the information could not be found. The information is
/// extracted from the [sourceInfo].
FixInfo _findFixInfo(SourceInformation sourceInfo, int offset) {
for (MapEntry<SingleNullabilityFix, List<FixReasonInfo>> entry
in sourceInfo.fixes.entries) {
Location location = entry.key.location;
if (location.offset == offset) {
return FixInfo(entry.key, entry.value);
}
}
return null;
}
/// Return `true` if the given [node] is from a compilation unit within the
/// 'test' directory of the package.
bool _inTestCode(AstNode node) {
// TODO(brianwilkerson) Generalize this.
CompilationUnit unit = node.thisOrAncestorOfType<CompilationUnit>();
CompilationUnitElement unitElement = unit?.declaredElement;
if (unitElement == null) {
return false;
}
String filePath = unitElement.source.fullName;
var resourceProvider = unitElement.session.resourceProvider;
return resourceProvider.pathContext.split(filePath).contains('test');
}
/// Return the navigation target in the file with the given [filePath] at the
/// given [offset] ans with the given [length].
NavigationTarget _targetFor(String filePath, int offset, int length) {
UnitInfo unitInfo = _unitForPath(filePath);
NavigationTarget target = NavigationTarget(filePath, offset, length);
unitInfo.targets.add(target);
return target;
}
/// Return the navigation target corresponding to the given [node] in the file
/// with the given [filePath].
NavigationTarget _targetForNode(String filePath, AstNode node) {
return _targetFor(filePath, node.offset, node.length);
}
/// Return the unit info for the file at the given [path].
UnitInfo _unitForPath(String path) {
return unitMap.putIfAbsent(path, () => UnitInfo(path));
}
}