blob: 694a2105ce59fab254097763e7fd8454dab5651f [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/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/src/generated/source.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
show Location, SourceEdit, SourceFileEdit;
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<LibraryInfo>> explainMigration() async {
Map<Source, SourceInformation> sourceInfo = info.sourceInformation;
List<LibraryInfo> libraries = [];
for (Source source in sourceInfo.keys) {
String filePath = source.fullName;
AnalysisSession session =
server.getAnalysisDriver(filePath).currentSession;
if (!session.getFile(filePath).isPart) {
ParsedLibraryResult result = await session.getParsedLibrary(filePath);
libraries
.add(_explainLibrary(result, info, sourceInfo[source], listener));
}
}
return libraries;
}
/// 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) {
EdgeOriginInfo origin = info.edgeOrigin[edge];
if (origin != null) {
AstNode node = origin.node;
if (node.parent is ArgumentList) {
if (node is NullLiteral) {
details.add(RegionDetail(
'null is explicitly passed as an argument.',
_targetFor(origin)));
} else {
details.add(RegionDetail(
'A nullable value is explicitly passed as an argument.',
_targetFor(origin)));
}
} else {
details.add(RegionDetail(
'A nullable value is assigned.', _targetFor(origin)));
}
}
}
} else if (reason is EdgeInfo) {
// TODO(brianwilkerson) Implement this after finding an example whose
// reason is an edge.
} else {
throw UnimplementedError(
'Unexpected class of reason: ${reason.runtimeType}');
}
}
return details;
}
/// Return the migration information for the given library.
LibraryInfo _explainLibrary(
ParsedLibraryResult result,
InstrumentationInformation info,
SourceInformation sourceInfo,
DartFixListener listener) {
List<UnitInfo> units = [];
for (ParsedUnitResult unit in result.units) {
SourceFileEdit edit = listener.sourceChange.getFileEdit(unit.path);
units.add(_explainUnit(sourceInfo, unit, edit));
}
return LibraryInfo(units);
}
/// Return the migration information for the given unit.
UnitInfo _explainUnit(SourceInformation sourceInfo, ParsedUnitResult result,
SourceFileEdit fileEdit) {
UnitInfo unitInfo = _unitForPath(result.path);
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 the navigation target corresponding to the given [origin].
NavigationTarget _targetFor(EdgeOriginInfo origin) {
AstNode node = origin.node;
String filePath = origin.source.fullName;
UnitInfo unitInfo = _unitForPath(filePath);
NavigationTarget target =
NavigationTarget(filePath, node.offset, node.length);
unitInfo.targets.add(target);
return target;
}
/// Return the unit info for the file at the given [path].
UnitInfo _unitForPath(String path) {
return unitMap.putIfAbsent(path, () => UnitInfo(path));
}
}