| // Copyright (c) 2018, 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/protocol_server.dart' hide Element; |
| import 'package:analysis_server/src/services/correction/status.dart'; |
| import 'package:analysis_server/src/services/refactoring/refactoring.dart'; |
| import 'package:analysis_server/src/services/refactoring/refactoring_internal.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/file_system/file_system.dart'; |
| import 'package:analyzer/src/dart/analysis/driver.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; |
| import 'package:analyzer_plugin/utilities/range_factory.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:path/path.dart' as pathos; |
| |
| /// [MoveFileRefactoring] implementation. |
| class MoveFileRefactoringImpl extends RefactoringImpl |
| implements MoveFileRefactoring { |
| final ResourceProvider resourceProvider; |
| final pathos.Context pathContext; |
| final RefactoringWorkspace refactoringWorkspace; |
| late AnalysisDriver driver; |
| late AnalysisSession _session; |
| |
| /// The path provided by the client to be renamed from. |
| /// |
| /// May be a file or folder path. |
| late String oldFile; |
| |
| /// The path provided by the client to be renamed to. |
| /// |
| /// May be a file or folder path. |
| late String newFile; |
| |
| final packagePrefixedStringPattern = RegExp(r'''^r?['"]+package:'''); |
| |
| MoveFileRefactoringImpl( |
| this.resourceProvider, this.refactoringWorkspace, this.oldFile) |
| : pathContext = resourceProvider.pathContext; |
| |
| @override |
| String get refactoringName => 'Move File'; |
| |
| @override |
| Future<RefactoringStatus> checkFinalConditions() async { |
| for (var driver in refactoringWorkspace.drivers) { |
| var rootPath = driver.analysisContext!.contextRoot.root.path; |
| if (pathContext.equals(rootPath, oldFile)) { |
| return RefactoringStatus.fatal( |
| 'Renaming an analysis root is not supported ($oldFile)'); |
| } |
| } |
| |
| final drivers = refactoringWorkspace.driversContaining(oldFile); |
| if (drivers.length != 1) { |
| return RefactoringStatus.fatal( |
| '$oldFile does not belong to an analysis root.'); |
| } |
| |
| driver = drivers.first; |
| await driver.applyPendingFileChanges(); |
| _session = driver.currentSession; |
| if (!resourceProvider.getResource(oldFile).exists) { |
| return RefactoringStatus.fatal('$oldFile does not exist.'); |
| } |
| |
| return RefactoringStatus(); |
| } |
| |
| @override |
| Future<RefactoringStatus> checkInitialConditions() async { |
| return RefactoringStatus(); |
| } |
| |
| @override |
| Future<SourceChange> createChange() async { |
| var changeBuilder = ChangeBuilder(session: _session); |
| |
| final resource = resourceProvider.getResource(oldFile); |
| |
| try { |
| await _appendChangesForResource(changeBuilder, resource, newFile); |
| } on InconsistentAnalysisException { |
| // If an InconsistentAnalysisException occurs, it's likely the user |
| // modified the source and is no longer interested in the results. |
| return SourceChange('Refactor cancelled by file modifications'); |
| } |
| |
| // If cancellation was requested the results may be incomplete so return |
| // a new empty change instead of a partial one with a descriptive name |
| // so it's clear from any logs that cancellation was processed. |
| if (isCancellationRequested) { |
| return SourceChange('Refactor cancelled'); |
| } |
| |
| return changeBuilder.sourceChange; |
| } |
| |
| Future<void> _appendChangeForFile( |
| ChangeBuilder changeBuilder, File file, String newPath) async { |
| var oldPath = file.path; |
| var oldDir = pathContext.dirname(oldPath); |
| var newDir = pathContext.dirname(newPath); |
| |
| final resolvedUnit = await _session.getResolvedUnit(file.path); |
| if (resolvedUnit is! ResolvedUnitResult) { |
| return; |
| } |
| |
| var element = resolvedUnit.unit.declaredElement; |
| if (element == null) { |
| return; |
| } |
| |
| var libraryElement = element.library; |
| |
| // If this element is a library, update outgoing references inside the file. |
| if (element == libraryElement.definingCompilationUnit) { |
| // Handle part-of directives in this library |
| var libraryResult = |
| await _session.getResolvedLibraryByElement(libraryElement); |
| if (libraryResult is! ResolvedLibraryResult) { |
| return; |
| } |
| var definingUnitResult = libraryResult.units.first; |
| for (var result in libraryResult.units) { |
| if (result.isPart) { |
| var partOfs = result.unit.directives |
| .whereType<PartOfDirective>() |
| .map((e) => e.uri) |
| .whereNotNull() |
| .where((uri) => _isRelativeUri(uri.stringValue)); |
| if (partOfs.isNotEmpty) { |
| await changeBuilder.addDartFileEdit( |
| result.unit.declaredElement!.source.fullName, (builder) { |
| for (var uri in partOfs) { |
| var newUri = _getRelativeUri(newPath, oldDir); |
| builder.addSimpleReplacement( |
| SourceRange(uri.offset, uri.length), "'$newUri'"); |
| } |
| }); |
| } |
| } |
| } |
| |
| if (newDir != oldDir) { |
| await changeBuilder.addDartFileEdit(definingUnitResult.path, (builder) { |
| for (var directive in definingUnitResult.unit.directives) { |
| if (directive is UriBasedDirective) { |
| // If the import is relative and the referenced file is also in |
| // the moved folder, no update is necessary. |
| var uriContent = directive.uriContent; |
| var uriFullPath = directive.uriSource?.fullName; |
| if (uriContent != null && |
| uriFullPath != null && |
| pathContext.isRelative(uriContent) && |
| // `oldFile` is used here and not `oldDir` because we care |
| // about whether this is within the folder being renamed, not |
| // the folder for this specific resource. |
| pathContext.isWithin(oldFile, uriFullPath)) { |
| continue; |
| } |
| _updateUriReference(builder, directive, oldDir, newDir); |
| } |
| } |
| }); |
| } |
| } else if (newDir != oldDir) { |
| // Otherwise, we need to update any relative part-of references. |
| var partOfs = resolvedUnit.unit.directives |
| .whereType<PartOfDirective>() |
| .map((e) => e.uri) |
| .whereNotNull() |
| .where((uri) => _isRelativeUri(uri.stringValue)); |
| |
| if (partOfs.isNotEmpty) { |
| await changeBuilder.addDartFileEdit(element.source.fullName, (builder) { |
| for (var uri in partOfs) { |
| var oldLocation = pathContext.join(oldDir, uri.stringValue); |
| var newUri = _getRelativeUri(oldLocation, newDir); |
| builder.addSimpleReplacement( |
| SourceRange(uri.offset, uri.length), "'$newUri'"); |
| } |
| }); |
| } |
| } |
| |
| // Update incoming references to this file |
| var matches = |
| await refactoringWorkspace.searchEngine.searchReferences(element); |
| var references = getSourceReferences(matches); |
| for (var reference in references) { |
| // If the import is relative and the referencing file is also in |
| // the moved folder, no update is necessary. |
| var uriFullPath = reference.file; |
| if (!_isPackageReference(reference) && |
| // `oldFile` is used here and not `oldDir` because we care |
| // about whether this is within the folder being renamed, not |
| // the folder for this specific resource. |
| pathContext.isWithin(oldFile, uriFullPath)) { |
| continue; |
| } |
| await changeBuilder.addDartFileEdit(reference.file, (builder) { |
| var newUri = _computeNewUri(reference, newPath); |
| builder.addSimpleReplacement(reference.range, "'$newUri'"); |
| }); |
| } |
| } |
| |
| Future<void> _appendChangesForResource( |
| ChangeBuilder changeBuilder, Resource resource, String newPath) async { |
| if (isCancellationRequested) { |
| return; |
| } |
| |
| if (resource is File) { |
| await _appendChangeForFile(changeBuilder, resource, newPath); |
| } else if (resource is Folder) { |
| for (final child in resource.getChildren()) { |
| await _appendChangesForResource(changeBuilder, child, |
| pathContext.join(newPath, pathContext.basename(child.path))); |
| } |
| } |
| } |
| |
| /// Computes the URI to use to reference [newPath] from [reference]. |
| String _computeNewUri(SourceReference reference, String newPath) { |
| var refDir = pathContext.dirname(reference.file); |
| // Try to keep package: URI |
| if (_isPackageReference(reference)) { |
| var restoredUri = driver.sourceFactory.pathToUri(newPath); |
| // If the new URI is not a package: URI, fall back to computing a relative |
| // URI below. |
| if (restoredUri?.isScheme('package') ?? false) { |
| return restoredUri.toString(); |
| } |
| } |
| return _getRelativeUri(newPath, refDir); |
| } |
| |
| String _getRelativeUri(String path, String from) { |
| var uri = pathContext.relative(path, from: from); |
| var parts = pathContext.split(uri); |
| return pathos.posix.joinAll(parts); |
| } |
| |
| bool _isPackageReference(SourceReference reference) { |
| var source = reference.element.source!; |
| var quotedImportUri = source.contents.data.substring(reference.range.offset, |
| reference.range.offset + reference.range.length); |
| return packagePrefixedStringPattern.hasMatch(quotedImportUri); |
| } |
| |
| /// Checks if the given [path] represents a relative URI. |
| /// |
| /// The following URI's are not relative: |
| /// `/absolute/path/file.dart` |
| /// `dart:math` |
| bool _isRelativeUri(String? path) { |
| if (path == null) { |
| return false; |
| } |
| // absolute URI |
| if (Uri.parse(path).isAbsolute) { |
| return false; |
| } |
| // absolute path |
| if (pathContext.isAbsolute(path)) { |
| return false; |
| } |
| // OK |
| return true; |
| } |
| |
| void _updateUriReference(FileEditBuilder builder, UriBasedDirective directive, |
| String oldDir, String newDir) { |
| var uriNode = directive.uri; |
| var uriValue = uriNode.stringValue; |
| if (_isRelativeUri(uriValue)) { |
| var elementPath = pathContext.join(oldDir, uriValue); |
| var newUri = _getRelativeUri(elementPath, newDir); |
| builder.addSimpleReplacement(range.node(uriNode), "'$newUri'"); |
| } |
| } |
| } |