blob: 22e440bef98af184ca86ce2768f5873c6151014f [file] [log] [blame]
// 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 '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/error/codes.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart'
hide AnalysisError, Element;
/// 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;
bool hasUnresolvedIdentifierError;
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>[];
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 leadingComment = getLeadingComment(unit, directive, lineInfo);
final trailingComment =
getTrailingComment(unit, directive, lineInfo, end);
String leadingCommentText;
if (leadingComment != null) {
leadingCommentText =
code.substring(leadingComment.offset, directive.offset);
offset = leadingComment.offset;
}
String trailingCommentText;
if (trailingComment != null) {
trailingCommentText =
code.substring(directive.end, trailingComment.end);
end = trailingComment.end;
}
String documentationText;
if (directive.documentationComment != null) {
documentationText = code.substring(
directive.documentationComment.offset,
directive.documentationComment.end);
}
String annotationText;
if (directive.metadata.beginToken != null) {
annotationText = code.substring(
directive.metadata.beginToken.offset,
directive.metadata.endToken.end);
}
var text = code.substring(
directive.firstTokenAfterCommentAndMetadata.offset,
directive.end);
var uriContent = directive.uri.stringValue;
directives.add(
_DirectiveInfo(
directive,
priority,
leadingCommentText,
documentationText,
annotationText,
uriContent,
trailingCommentText,
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?.documentationText != null) {
sb.write(libraryDocumentationDirective.documentationText);
sb.write(endOfLine);
}
var currentPriority = directives.first.priority;
for (var directiveInfo in directives) {
if (!hasUnresolvedIdentifierError) {
var directive = directiveInfo.directive;
if (removeUnused && _isUnusedImport(directive)) {
continue;
}
}
if (currentPriority != directiveInfo.priority) {
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);
sb.write(endOfLine);
}
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;
}
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;
}
/// 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
/// directive.
///
/// 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.
static Token getLeadingComment(
CompilationUnit unit, UriBasedDirective directive, LineInfo lineInfo) {
if (directive.beginToken.precedingComments == null) {
return null;
}
var firstComment = directive.beginToken.precedingComments;
var comment = firstComment;
// Don't connect comments that have a blank line between them
while (comment.next != null) {
var currentLine = lineInfo.getLocation(comment.offset).lineNumber;
var nextLine = lineInfo.getLocation(comment.next.offset).lineNumber;
if (nextLine - currentLine > 1) {
firstComment = comment.next;
}
comment = comment.next;
}
// Check if the comment is the first comment in the document
if (firstComment != unit.beginToken.precedingComments) {
var previousDirectiveLine =
lineInfo.getLocation(directive.beginToken.previous.end).lineNumber;
// Skip over any comments on the same line as the previous directive
// as they will be attached to the end of it.
var comment = firstComment;
while (comment != null &&
previousDirectiveLine ==
lineInfo.getLocation(comment.offset).lineNumber) {
comment = comment.next;
}
return comment;
}
return null;
}
/// 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, int end) {
var line = lineInfo.getLocation(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;
}
}
class _DirectiveInfo implements Comparable<_DirectiveInfo> {
final UriBasedDirective directive;
final _DirectivePriority priority;
final String leadingCommentText;
final String documentationText;
final String annotationText;
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.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)';
static int _compareUri(String a, String b) {
var aList = _splitUri(a);
var bList = _splitUri(b);
int result;
if ((result = aList[0].compareTo(bList[0])) != 0) return result;
if ((result = aList[1].compareTo(bList[1])) != 0) return result;
return 0;
}
/// Split the given [uri] like `package:some.name/and/path.dart` into a list
/// like `[package:some.name, and/path.dart]`.
static List<String> _splitUri(String uri) {
var index = uri.indexOf('/');
if (index == -1) {
return <String>[uri, ''];
}
return <String>[uri.substring(0, index), uri.substring(index + 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;
}