blob: dd5c804e363f2e37f9fb9e540bb7a129b55a7eb5 [file] [log] [blame]
// Copyright (c) 2017, 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:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';
import '../analyzer.dart';
const _desc = r'Adhere to Effective Dart Guide directives sorting conventions.';
const _docImportKeyword = '@docImport';
const _exportKeyword = 'export';
const _importKeyword = 'import';
/// Compares directives by package name, then file name in the package.
///
/// The package name is everything until the first '/'.
int compareDirectives(String a, String b) {
if (!a.startsWith('package:') || !b.startsWith('package:')) {
if (!a.startsWith('/') && !b.startsWith('/')) {
return a.compareTo(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));
}
bool _isAbsoluteDirective(NamespaceDirective node) {
var uriContent = node.uri.stringValue;
return uriContent != null && uriContent.contains(':');
}
bool _isDartDirective(NamespaceDirective node) {
var uriContent = node.uri.stringValue;
return uriContent != null && uriContent.startsWith('dart:');
}
bool _isExportDirective(Directive node) => node is ExportDirective;
bool _isNotDartDirective(NamespaceDirective node) => !_isDartDirective(node);
bool _isPackageDirective(NamespaceDirective node) {
var uriContent = node.uri.stringValue;
return uriContent != null && uriContent.startsWith('package:');
}
bool _isPartDirective(Directive node) => node is PartDirective;
bool _isRelativeDirective(NamespaceDirective node) =>
!_isAbsoluteDirective(node);
class DirectivesOrdering extends MultiAnalysisRule {
static const List<LintCode> allCodes = [
LinterLintCode.directivesOrderingAlphabetical,
LinterLintCode.directivesOrderingDart,
LinterLintCode.directivesOrderingExports,
LinterLintCode.directivesOrderingPackageBeforeRelative,
];
DirectivesOrdering()
: super(name: LintNames.directives_ordering, description: _desc);
@override
List<DiagnosticCode> get diagnosticCodes => allCodes;
@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
var visitor = _Visitor(this);
registry.addCompilationUnit(this, visitor);
}
void _reportLintWithDartDirectiveGoFirstMessage(AstNode node, String type) {
reportAtNode(
node,
diagnosticCode: LinterLintCode.directivesOrderingDart,
arguments: ['${type}s'],
);
}
void _reportLintWithDirectiveSectionOrderedAlphabeticallyMessage(
AstNode node,
) {
reportAtNode(
node,
diagnosticCode: LinterLintCode.directivesOrderingAlphabetical,
);
}
void _reportLintWithExportDirectiveAfterImportDirectiveMessage(AstNode node) {
reportAtNode(
node,
diagnosticCode: LinterLintCode.directivesOrderingExports,
);
}
void _reportLintWithPackageDirectiveBeforeRelativeMessage(
AstNode node,
String type,
) {
reportAtNode(
node,
diagnosticCode: LinterLintCode.directivesOrderingPackageBeforeRelative,
arguments: ['${type}s'],
);
}
}
class _Visitor extends SimpleAstVisitor<void> {
final DirectivesOrdering rule;
_Visitor(this.rule);
@override
void visitCompilationUnit(CompilationUnit node) {
var lintedNodes = <AstNode>{};
_checkDartDirectiveGoFirst(lintedNodes, node);
_checkPackageDirectiveBeforeRelative(lintedNodes, node);
_checkExportDirectiveAfterImportDirective(lintedNodes, node);
_checkDirectiveSectionOrderedAlphabetically(lintedNodes, node);
}
void _checkDartDirectiveGoFirst(
Set<AstNode> lintedNodes,
CompilationUnit node,
) {
for (var import in node.importDirectives.withDartUrisSkippingTheFirstSet) {
if (lintedNodes.add(import)) {
rule._reportLintWithDartDirectiveGoFirstMessage(import, _importKeyword);
}
}
for (var export in node.exportDirectives.withDartUrisSkippingTheFirstSet) {
if (lintedNodes.add(export)) {
rule._reportLintWithDartDirectiveGoFirstMessage(export, _exportKeyword);
}
}
for (var import
in node.docImportDirectives.withDartUrisSkippingTheFirstSet) {
if (lintedNodes.add(import)) {
rule._reportLintWithDartDirectiveGoFirstMessage(
import,
_docImportKeyword,
);
}
}
}
void _checkDirectiveSectionOrderedAlphabetically(
Set<AstNode> lintedNodes,
CompilationUnit node,
) {
var dartImports = node.importDirectives.where(_isDartDirective);
var dartExports = node.exportDirectives.where(_isDartDirective);
var dartDocImports = node.docImportDirectives.where(_isDartDirective);
_checkSectionInOrder(lintedNodes, dartImports);
_checkSectionInOrder(lintedNodes, dartExports);
_checkSectionInOrder(lintedNodes, dartDocImports);
var relativeImports = node.importDirectives.where(_isRelativeDirective);
var relativeExports = node.exportDirectives.where(_isRelativeDirective);
var relativeDocImports = node.docImportDirectives.where(
_isRelativeDirective,
);
_checkSectionInOrder(lintedNodes, relativeImports);
_checkSectionInOrder(lintedNodes, relativeExports);
_checkSectionInOrder(lintedNodes, relativeDocImports);
// See: https://github.com/dart-lang/linter/issues/3395
// (`DartProject` removal)
// The rub is that *all* projects are being treated as "not pub"
// packages. We'll want to be careful when fixing this since it
// will have ecosystem impact.
// Not a pub package. Package directives should be sorted in one block.
var packageImports = node.importDirectives.where(_isPackageDirective);
var packageExports = node.exportDirectives.where(_isPackageDirective);
var packageDocImports = node.docImportDirectives.where(_isPackageDirective);
_checkSectionInOrder(lintedNodes, packageImports);
_checkSectionInOrder(lintedNodes, packageExports);
_checkSectionInOrder(lintedNodes, packageDocImports);
// The following is relying on projectName which is meant to come from
// a `DartProject` instance (but was not since the project was always null)
// else {
// var packageBox = _PackageBox(projectName);
//
// var thirdPartyPackageImports =
// importDirectives.where(packageBox._isNotOwnPackageDirective);
// var thirdPartyPackageExports =
// exportDirectives.where(packageBox._isNotOwnPackageDirective);
//
// var ownPackageImports =
// importDirectives.where(packageBox._isOwnPackageDirective);
// var ownPackageExports =
// exportDirectives.where(packageBox._isOwnPackageDirective);
//
// _checkSectionInOrder(lintedNodes, thirdPartyPackageImports);
// _checkSectionInOrder(lintedNodes, thirdPartyPackageExports);
//
// _checkSectionInOrder(lintedNodes, ownPackageImports);
// _checkSectionInOrder(lintedNodes, ownPackageExports);
// }
}
void _checkExportDirectiveAfterImportDirective(
Set<AstNode> lintedNodes,
CompilationUnit node,
) {
for (var directive
in node.directives.reversed
.skipWhile(_isPartDirective)
.skipWhile(_isExportDirective)
.where(_isExportDirective)) {
if (lintedNodes.add(directive)) {
rule._reportLintWithExportDirectiveAfterImportDirectiveMessage(
directive,
);
}
}
}
void _checkPackageDirectiveBeforeRelative(
Set<AstNode> lintedNodes,
CompilationUnit node,
) {
for (var import
in node.importDirectives.withPackageUrisSkippingAbsoluteUris) {
if (lintedNodes.add(import)) {
rule._reportLintWithPackageDirectiveBeforeRelativeMessage(
import,
_importKeyword,
);
}
}
for (var export
in node.exportDirectives.withPackageUrisSkippingAbsoluteUris) {
if (lintedNodes.add(export)) {
rule._reportLintWithPackageDirectiveBeforeRelativeMessage(
export,
_exportKeyword,
);
}
}
for (var import
in node.docImportDirectives.withPackageUrisSkippingAbsoluteUris) {
if (lintedNodes.add(import)) {
rule._reportLintWithPackageDirectiveBeforeRelativeMessage(
import,
_docImportKeyword,
);
}
}
}
void _checkSectionInOrder(
Set<AstNode> lintedNodes,
Iterable<UriBasedDirective> nodes,
) {
if (nodes.isEmpty) return;
var previousUri = nodes.first.uri.stringValue;
for (var directive in nodes.skip(1)) {
var directiveUri = directive.uri.stringValue;
if (previousUri != null &&
directiveUri != null &&
compareDirectives(previousUri, directiveUri) > 0) {
if (lintedNodes.add(directive)) {
rule._reportLintWithDirectiveSectionOrderedAlphabeticallyMessage(
directive,
);
}
}
previousUri = directive.uri.stringValue;
}
}
}
extension on CompilationUnit {
Iterable<ImportDirective> get docImportDirectives {
var libraryDirective = directives.whereType<LibraryDirective>().firstOrNull;
if (libraryDirective == null) return const [];
var docComment = libraryDirective.documentationComment;
if (docComment == null) return const [];
return docComment.docImports.map((e) => e.import);
}
Iterable<ExportDirective> get exportDirectives =>
directives.whereType<ExportDirective>();
Iterable<ImportDirective> get importDirectives =>
directives.whereType<ImportDirective>();
}
extension on Iterable<NamespaceDirective> {
/// The directives with 'dart:' URIs, skipping the first such set of
/// directives.
Iterable<NamespaceDirective> get withDartUrisSkippingTheFirstSet =>
skipWhile(_isDartDirective).where(_isDartDirective);
/// The directives with 'package:' URIs, after the first set of directives
/// with absolute URIs.
Iterable<NamespaceDirective> get withPackageUrisSkippingAbsoluteUris => where(
_isNotDartDirective,
).skipWhile(_isAbsoluteDirective).where(_isPackageDirective);
}