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