| // 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)); |
| } |
| } |