| // Copyright (c) 2014, 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 'dart:math' as math; |
| |
| import 'package:analysis_server/src/utilities/strings.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/source/line_info.dart'; |
| import 'package:analyzer/src/dart/element/extensions.dart'; |
| import 'package:analyzer/src/error/codes.dart'; |
| import 'package:analyzer/src/ignore_comments/ignore_info.dart'; |
| import 'package:analyzer_plugin/protocol/protocol_common.dart' |
| hide AnalysisError, Element; |
| import 'package:collection/collection.dart'; |
| import 'package:meta/meta_meta.dart'; |
| |
| /// Organizer of imports (and other directives) in the [unit]. |
| class ImportOrganizer { |
| final String initialCode; |
| |
| final CompilationUnit unit; |
| |
| final List<AnalysisError> errors; |
| |
| final bool removeUnused; |
| |
| String code; |
| |
| String endOfLine = '\n'; |
| |
| bool hasUnresolvedIdentifierError = false; |
| |
| ImportOrganizer(this.initialCode, this.unit, this.errors, |
| {this.removeUnused = true}) |
| : code = initialCode { |
| endOfLine = getEOL(code); |
| hasUnresolvedIdentifierError = errors.any((error) { |
| return error.errorCode.isUnresolvedIdentifier; |
| }); |
| } |
| |
| /// Return the [SourceEdit]s that organize imports in the [unit]. |
| List<SourceEdit> organize() { |
| _organizeDirectives(); |
| // prepare edits |
| var edits = <SourceEdit>[]; |
| if (code != initialCode) { |
| var suffixLength = findCommonSuffix(initialCode, code); |
| var edit = SourceEdit(0, initialCode.length - suffixLength, |
| code.substring(0, code.length - suffixLength)); |
| edits.add(edit); |
| } |
| return edits; |
| } |
| |
| bool _isUnusedImport(UriBasedDirective directive) { |
| for (var error in errors) { |
| if ((error.errorCode == HintCode.DUPLICATE_IMPORT || |
| error.errorCode == HintCode.UNUSED_IMPORT) && |
| directive.uri.offset == error.offset) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /// Organize all [Directive]s. |
| void _organizeDirectives() { |
| var lineInfo = unit.lineInfo; |
| var hasLibraryDirective = false; |
| var directives = <_DirectiveInfo>[]; |
| // Track the end offset of any library-level comment/annotations that should |
| // remain at the top of the file regardless of whether it was attached to a |
| // directive that's moved/removed. |
| // Code up to this offset will be excluded from the comment/docs/annotation |
| // text for the computed DirectiveInfo and also its range for replacement |
| // in the document. |
| int? libraryDocsAndAnnotationsEndOffset; |
| for (var directive in unit.directives) { |
| if (directive is LibraryDirective) { |
| hasLibraryDirective = true; |
| } |
| if (directive is UriBasedDirective) { |
| var priority = _getDirectivePriority(directive); |
| if (priority != null) { |
| var offset = directive.offset; |
| var end = directive.end; |
| |
| final isPseudoLibraryDirective = directive == unit.directives.first; |
| Annotation? lastLibraryAnnotation; |
| if (isPseudoLibraryDirective) { |
| // Find the last library-level annotation that does not come |
| // after any non-library annotation. If there are already |
| // non-library annotations before library annotations, we will not |
| // try to correct those. |
| lastLibraryAnnotation = directive.metadata |
| .takeWhile(_isLibraryTargetAnnotation) |
| .lastOrNull; |
| if (lastLibraryAnnotation != null) { |
| libraryDocsAndAnnotationsEndOffset = |
| lineInfo.getOffsetOfLineAfter(lastLibraryAnnotation.end); |
| // In the case of a blank line after the last library annotation |
| // we should include that in the library part. Otherwise it will |
| // be included in the top of the following directive and may |
| // result in an extra blank line in the annotation block if it |
| // is moved. |
| final nextLineOffset = lineInfo |
| .getOffsetOfLineAfter(libraryDocsAndAnnotationsEndOffset); |
| if (code |
| .substring(libraryDocsAndAnnotationsEndOffset, nextLineOffset) |
| .trim() |
| .isEmpty) { |
| libraryDocsAndAnnotationsEndOffset = nextLineOffset; |
| } |
| } |
| } |
| |
| // Usually we look for leading comments on the directive. However if |
| // some library annotations were trimmed off, those comments are part |
| // of that and should not also be included here. |
| final leadingToken = |
| lastLibraryAnnotation == null ? directive.beginToken : null; |
| final leadingComment = leadingToken != null |
| ? getLeadingComment(unit, leadingToken, lineInfo) |
| : null; |
| final trailingComment = getTrailingComment(unit, directive, lineInfo); |
| |
| /// Computes the offset to use for the start of directive-specific |
| /// code below taking into account code already included by |
| /// [libraryDocsAndAnnotationsEndOffset]. |
| final clampedOffset = libraryDocsAndAnnotationsEndOffset == null |
| ? (int offset) => offset |
| : (int offset) => |
| math.max(libraryDocsAndAnnotationsEndOffset!, offset); |
| |
| String? leadingCommentText; |
| if (leadingComment != null && leadingToken != null) { |
| offset = clampedOffset(leadingComment.offset); |
| leadingCommentText = code.substring( |
| offset, |
| clampedOffset(leadingToken.offset), |
| ); |
| } |
| String? trailingCommentText; |
| if (trailingComment != null) { |
| trailingCommentText = |
| code.substring(directive.end, trailingComment.end); |
| end = trailingComment.end; |
| } |
| String? documentationText; |
| var documentationComment = directive.documentationComment; |
| if (documentationComment != null) { |
| documentationText = code.substring( |
| clampedOffset(documentationComment.offset), |
| clampedOffset(documentationComment.end), |
| ); |
| } |
| String? annotationText; |
| String? postAnnotationCommentText; |
| var beginToken = directive.metadata.beginToken; |
| var endToken = directive.metadata.endToken; |
| if (beginToken != null && endToken != null) { |
| var annotationOffset = clampedOffset(beginToken.offset); |
| var annotationEnd = clampedOffset(endToken.end); |
| if (annotationOffset != annotationEnd) { |
| annotationText = code.substring( |
| annotationOffset, |
| annotationEnd, |
| ); |
| } |
| // Capture text between the end of the annotation and the directive |
| // text as there may be end-of line or line comments between. |
| // If not, this will capture the newline between the two, as it |
| // cannot be assumed there is a newline after annotationText because |
| // of the possibility of comments. |
| if (annotationEnd < |
| directive.firstTokenAfterCommentAndMetadata.offset) { |
| postAnnotationCommentText = code.substring(annotationEnd, |
| directive.firstTokenAfterCommentAndMetadata.offset); |
| } |
| } |
| var text = code.substring( |
| directive.firstTokenAfterCommentAndMetadata.offset, |
| directive.end); |
| var uriContent = directive.uri.stringValue ?? ''; |
| directives.add( |
| _DirectiveInfo( |
| directive, |
| priority, |
| leadingCommentText, |
| documentationText, |
| annotationText, |
| postAnnotationCommentText, |
| uriContent, |
| trailingCommentText, |
| isPseudoLibraryDirective |
| ? (libraryDocsAndAnnotationsEndOffset ?? offset) |
| : offset, |
| end, |
| text, |
| ), |
| ); |
| } |
| } |
| } |
| // nothing to do |
| if (directives.isEmpty) { |
| return; |
| } |
| var firstDirectiveOffset = directives.first.offset; |
| var lastDirectiveEnd = directives.last.end; |
| |
| // Without a library directive, the library comment is the comment of the |
| // first directive. |
| _DirectiveInfo? libraryDocumentationDirective; |
| if (!hasLibraryDirective && directives.isNotEmpty) { |
| libraryDocumentationDirective = directives.first; |
| } |
| |
| // sort |
| directives.sort(); |
| // append directives with grouping |
| String directivesCode; |
| { |
| var sb = StringBuffer(); |
| if (libraryDocumentationDirective != null && |
| libraryDocumentationDirective.documentationText != null) { |
| sb.write(libraryDocumentationDirective.documentationText); |
| sb.write(endOfLine); |
| } |
| _DirectivePriority? currentPriority; |
| for (var directiveInfo in directives) { |
| if (!hasUnresolvedIdentifierError) { |
| var directive = directiveInfo.directive; |
| if (removeUnused && _isUnusedImport(directive)) { |
| continue; |
| } |
| } |
| if (currentPriority != directiveInfo.priority) { |
| if (currentPriority != null) { |
| sb.write(endOfLine); |
| } |
| currentPriority = directiveInfo.priority; |
| } |
| if (directiveInfo.leadingCommentText != null) { |
| sb.write(directiveInfo.leadingCommentText); |
| } |
| if (directiveInfo != libraryDocumentationDirective && |
| directiveInfo.documentationText != null) { |
| sb.write(directiveInfo.documentationText); |
| sb.write(endOfLine); |
| } |
| if (directiveInfo.annotationText != null) { |
| sb.write(directiveInfo.annotationText); |
| } |
| if (directiveInfo.postAnnotationCommentText != null) { |
| sb.write(directiveInfo.postAnnotationCommentText); |
| } |
| sb.write(directiveInfo.text); |
| if (directiveInfo.trailingCommentText != null) { |
| sb.write(directiveInfo.trailingCommentText); |
| } |
| sb.write(endOfLine); |
| } |
| directivesCode = sb.toString(); |
| directivesCode = directivesCode.trimRight(); |
| } |
| // prepare code |
| var beforeDirectives = code.substring(0, firstDirectiveOffset); |
| var afterDirectives = code.substring(lastDirectiveEnd); |
| code = beforeDirectives + directivesCode + afterDirectives; |
| } |
| |
| /// Return the EOL to use for [code]. |
| static String getEOL(String code) { |
| if (code.contains('\r\n')) { |
| return '\r\n'; |
| } else { |
| return '\n'; |
| } |
| } |
| |
| /// Gets the first comment token considered to be the leading comment for this |
| /// token. |
| /// |
| /// Leading comments for the first directive in a file are considered library |
| /// comments and not returned unless they contain blank lines, in which case |
| /// only the last part of the comment will be returned (unless it is a |
| /// language directive comment, in which case it will also be skipped) or an |
| /// '// ignore:' comment which should always be treated as attached to the |
| /// import. |
| static Token? getLeadingComment( |
| CompilationUnit unit, Token beginToken, LineInfo lineInfo) { |
| if (beginToken.precedingComments == null) { |
| return null; |
| } |
| |
| Token? firstComment = beginToken.precedingComments; |
| var comment = firstComment; |
| var nextComment = comment?.next; |
| // Don't connect comments that have a blank line between them |
| while (comment != null && nextComment != null) { |
| var currentLine = lineInfo.getLocation(comment.offset).lineNumber; |
| var nextLine = lineInfo.getLocation(nextComment.offset).lineNumber; |
| if (nextLine - currentLine > 1) { |
| firstComment = nextComment; |
| } |
| comment = nextComment; |
| nextComment = comment.next; |
| } |
| |
| // Language version tokens should never be attached so skip over. |
| if (firstComment is LanguageVersionToken) { |
| firstComment = firstComment.next; |
| } |
| |
| // If the comment is the first comment in the document then whether we |
| // consider it the leading comment depends on whether it's an ignore comment |
| // or not. |
| if (firstComment != null && |
| firstComment == unit.beginToken.precedingComments) { |
| return _isIgnoreComment(firstComment) ? firstComment : null; |
| } |
| |
| // Skip over any comments on the same line as the previous directive |
| // as they will be attached to the end of it. |
| var previousDirectiveLine = |
| lineInfo.getLocation(beginToken.previous!.end).lineNumber; |
| comment = firstComment; |
| while (comment != null && |
| previousDirectiveLine == |
| lineInfo.getLocation(comment.offset).lineNumber) { |
| comment = comment.next; |
| } |
| return comment; |
| } |
| |
| /// Gets the last comment token considered to be the trailing comment for this |
| /// directive. |
| /// |
| /// To be considered a trailing comment, the comment must be on the same line |
| /// as the directive. |
| static Token? getTrailingComment( |
| CompilationUnit unit, UriBasedDirective directive, LineInfo lineInfo) { |
| var line = lineInfo.getLocation(directive.end).lineNumber; |
| Token? comment = directive.endToken.next!.precedingComments; |
| while (comment != null) { |
| if (lineInfo.getLocation(comment.offset).lineNumber == line) { |
| return comment; |
| } |
| comment = comment.next; |
| } |
| return null; |
| } |
| |
| static _DirectivePriority? _getDirectivePriority( |
| UriBasedDirective directive) { |
| var uriContent = directive.uri.stringValue ?? ''; |
| if (directive is ImportDirective) { |
| if (uriContent.startsWith('dart:')) { |
| return _DirectivePriority.IMPORT_SDK; |
| } else if (uriContent.startsWith('package:')) { |
| return _DirectivePriority.IMPORT_PKG; |
| } else if (uriContent.contains('://')) { |
| return _DirectivePriority.IMPORT_OTHER; |
| } else { |
| return _DirectivePriority.IMPORT_REL; |
| } |
| } |
| if (directive is ExportDirective) { |
| if (uriContent.startsWith('dart:')) { |
| return _DirectivePriority.EXPORT_SDK; |
| } else if (uriContent.startsWith('package:')) { |
| return _DirectivePriority.EXPORT_PKG; |
| } else if (uriContent.contains('://')) { |
| return _DirectivePriority.EXPORT_OTHER; |
| } else { |
| return _DirectivePriority.EXPORT_REL; |
| } |
| } |
| if (directive is PartDirective) { |
| return _DirectivePriority.PART; |
| } |
| return null; |
| } |
| |
| /// Returns whether this token is a '// ignore:' comment (but not an |
| /// '// ignore_for_file:' comment). |
| static bool _isIgnoreComment(Token token) => |
| IgnoreInfo.IGNORE_MATCHER.matchAsPrefix(token.lexeme) != null; |
| |
| static bool _isLibraryTargetAnnotation(Annotation annotation) => |
| annotation.elementAnnotation?.targetKinds.contains(TargetKind.library) ?? |
| false; |
| } |
| |
| class _DirectiveInfo implements Comparable<_DirectiveInfo> { |
| final UriBasedDirective directive; |
| final _DirectivePriority priority; |
| final String? leadingCommentText; |
| final String? documentationText; |
| final String? annotationText; |
| final String? postAnnotationCommentText; |
| final String uri; |
| final String? trailingCommentText; |
| |
| /// The offset of the first token, usually the keyword but may include leading comments. |
| final int offset; |
| |
| /// The offset after the last token, including the end-of-line comment. |
| final int end; |
| |
| /// The text excluding comments, documentation and annotations. |
| final String text; |
| |
| _DirectiveInfo( |
| this.directive, |
| this.priority, |
| this.leadingCommentText, |
| this.documentationText, |
| this.annotationText, |
| this.postAnnotationCommentText, |
| this.uri, |
| this.trailingCommentText, |
| this.offset, |
| this.end, |
| this.text, |
| ); |
| |
| @override |
| int compareTo(_DirectiveInfo other) { |
| if (priority == other.priority) { |
| return _compareUri(uri, other.uri); |
| } |
| return priority.ordinal - other.priority.ordinal; |
| } |
| |
| @override |
| String toString() => '(priority=$priority; text=$text)'; |
| |
| /// Should keep these in sync! Copied from |
| /// https://github.com/dart-lang/linter/blob/658f497eef/lib/src/rules/directives_ordering.dart#L380-L387 |
| /// Consider finding a way to share this code! |
| static int _compareUri(String a, String b) { |
| var indexA = a.indexOf('/'); |
| var indexB = b.indexOf('/'); |
| if (indexA == -1 || indexB == -1) return a.compareTo(b); |
| var result = a.substring(0, indexA).compareTo(b.substring(0, indexB)); |
| if (result != 0) return result; |
| return a.substring(indexA + 1).compareTo(b.substring(indexB + 1)); |
| } |
| } |
| |
| class _DirectivePriority { |
| static const IMPORT_SDK = _DirectivePriority('IMPORT_SDK', 0); |
| static const IMPORT_PKG = _DirectivePriority('IMPORT_PKG', 1); |
| static const IMPORT_OTHER = _DirectivePriority('IMPORT_OTHER', 2); |
| static const IMPORT_REL = _DirectivePriority('IMPORT_REL', 3); |
| static const EXPORT_SDK = _DirectivePriority('EXPORT_SDK', 4); |
| static const EXPORT_PKG = _DirectivePriority('EXPORT_PKG', 5); |
| static const EXPORT_OTHER = _DirectivePriority('EXPORT_OTHER', 6); |
| static const EXPORT_REL = _DirectivePriority('EXPORT_REL', 7); |
| static const PART = _DirectivePriority('PART', 8); |
| |
| final String name; |
| final int ordinal; |
| |
| const _DirectivePriority(this.name, this.ordinal); |
| |
| @override |
| String toString() => name; |
| } |