| Index: pkg/analyzer/lib/src/lint/linter.dart
|
| diff --git a/pkg/analyzer/lib/src/lint/linter.dart b/pkg/analyzer/lib/src/lint/linter.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..5706f35ea8d26f8d5198b2c6bea474b33e6a6250
|
| --- /dev/null
|
| +++ b/pkg/analyzer/lib/src/lint/linter.dart
|
| @@ -0,0 +1,429 @@
|
| +// Copyright (c) 2015, 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:io';
|
| +
|
| +import 'package:analyzer/analyzer.dart';
|
| +import 'package:analyzer/dart/ast/token.dart';
|
| +import 'package:analyzer/src/generated/engine.dart'
|
| + show AnalysisErrorInfo, AnalysisErrorInfoImpl, Logger;
|
| +import 'package:analyzer/src/generated/java_engine.dart' show CaughtException;
|
| +import 'package:analyzer/src/generated/source.dart' show LineInfo;
|
| +import 'package:analyzer/src/generated/source_io.dart';
|
| +import 'package:analyzer/src/lint/analysis.dart';
|
| +import 'package:analyzer/src/lint/config.dart';
|
| +import 'package:analyzer/src/lint/io.dart';
|
| +import 'package:analyzer/src/lint/project.dart';
|
| +import 'package:analyzer/src/lint/pub.dart';
|
| +import 'package:analyzer/src/lint/registry.dart';
|
| +import 'package:analyzer/src/services/lint.dart' show Linter;
|
| +import 'package:glob/glob.dart';
|
| +import 'package:path/path.dart' as p;
|
| +
|
| +typedef Printer(String msg);
|
| +
|
| +/// Describes a String in valid camel case format.
|
| +class CamelCaseString {
|
| + static final _camelCaseMatcher = new RegExp(r'[A-Z][a-z]*');
|
| + static final _camelCaseTester = new RegExp(r'^([_$]*)([A-Z?$]+[a-z0-9]*)+$');
|
| +
|
| + final String value;
|
| + CamelCaseString(this.value) {
|
| + if (!isCamelCase(value)) {
|
| + throw new ArgumentError('$value is not CamelCase');
|
| + }
|
| + }
|
| +
|
| + String get humanized => _humanize(value);
|
| +
|
| + @override
|
| + String toString() => value;
|
| +
|
| + static bool isCamelCase(String name) => _camelCaseTester.hasMatch(name);
|
| +
|
| + static String _humanize(String camelCase) =>
|
| + _camelCaseMatcher.allMatches(camelCase).map((m) => m.group(0)).join(' ');
|
| +}
|
| +
|
| +/// Dart source linter.
|
| +class DartLinter implements AnalysisErrorListener {
|
| + final errors = <AnalysisError>[];
|
| +
|
| + final LinterOptions options;
|
| + final Reporter reporter;
|
| +
|
| + /// The total number of sources that were analyzed. Only valid after
|
| + /// [lintFiles] has been called.
|
| + int numSourcesAnalyzed;
|
| +
|
| + /// Creates a new linter.
|
| + DartLinter(this.options, {this.reporter: const PrintingReporter()});
|
| +
|
| + Iterable<AnalysisErrorInfo> lintFiles(List<File> files) {
|
| + List<AnalysisErrorInfo> errors = [];
|
| + var analysisDriver = new AnalysisDriver(options);
|
| + errors.addAll(analysisDriver.analyze(files.where((f) => isDartFile(f))));
|
| + numSourcesAnalyzed = analysisDriver.numSourcesAnalyzed;
|
| + files.where((f) => isPubspecFile(f)).forEach((p) {
|
| + numSourcesAnalyzed++;
|
| + return errors.addAll(_lintPubspecFile(p));
|
| + });
|
| + return errors;
|
| + }
|
| +
|
| + Iterable<AnalysisErrorInfo> lintPubspecSource(
|
| + {String contents, String sourcePath}) {
|
| + var results = <AnalysisErrorInfo>[];
|
| +
|
| + Uri sourceUrl = sourcePath == null ? null : p.toUri(sourcePath);
|
| +
|
| + var spec = new Pubspec.parse(contents, sourceUrl: sourceUrl);
|
| +
|
| + for (Linter lint in options.enabledLints) {
|
| + if (lint is LintRule) {
|
| + LintRule rule = lint;
|
| + var visitor = rule.getPubspecVisitor();
|
| + if (visitor != null) {
|
| + // Analyzer sets reporters; if this file is not being analyzed,
|
| + // we need to set one ourselves. (Needless to say, when pubspec
|
| + // processing gets pushed down, this hack can go away.)
|
| + if (rule.reporter == null && sourceUrl != null) {
|
| + var source = createSource(sourceUrl);
|
| + rule.reporter = new ErrorReporter(this, source);
|
| + }
|
| + try {
|
| + spec.accept(visitor);
|
| + } on Exception catch (e) {
|
| + reporter.exception(new LinterException(e.toString()));
|
| + }
|
| + if (rule._locationInfo != null && rule._locationInfo.isNotEmpty) {
|
| + results.addAll(rule._locationInfo);
|
| + rule._locationInfo.clear();
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + return results;
|
| + }
|
| +
|
| + @override
|
| + onError(AnalysisError error) => errors.add(error);
|
| +
|
| + Iterable<AnalysisErrorInfo> _lintPubspecFile(File sourceFile) =>
|
| + lintPubspecSource(
|
| + contents: sourceFile.readAsStringSync(), sourcePath: sourceFile.path);
|
| +}
|
| +
|
| +class FileGlobFilter extends LintFilter {
|
| + Iterable<Glob> includes;
|
| + Iterable<Glob> excludes;
|
| +
|
| + FileGlobFilter([Iterable<String> includeGlobs, Iterable<String> excludeGlobs])
|
| + : includes = includeGlobs.map((glob) => new Glob(glob)),
|
| + excludes = excludeGlobs.map((glob) => new Glob(glob));
|
| +
|
| + @override
|
| + bool filter(AnalysisError lint) {
|
| + // TODO specify order
|
| + return excludes.any((glob) => glob.matches(lint.source.fullName)) &&
|
| + !includes.any((glob) => glob.matches(lint.source.fullName));
|
| + }
|
| +}
|
| +
|
| +class Group implements Comparable<Group> {
|
| + /// Defined rule groups.
|
| + static const Group errors =
|
| + const Group._('errors', description: 'Possible coding errors.');
|
| + static const Group pub = const Group._('pub',
|
| + description: 'Pub-related rules.',
|
| + link: const Hyperlink('See the <strong>Pubspec Format</strong>',
|
| + 'https://www.dartlang.org/tools/pub/pubspec.html'));
|
| + static const Group style = const Group._('style',
|
| + description:
|
| + 'Matters of style, largely derived from the official Dart Style Guide.',
|
| + link: const Hyperlink('See the <strong>Style Guide</strong>',
|
| + 'https://www.dartlang.org/articles/style-guide/'));
|
| +
|
| + /// List of builtin groups in presentation order.
|
| + static const Iterable<Group> builtin = const [errors, style, pub];
|
| +
|
| + final String name;
|
| + final bool custom;
|
| + final String description;
|
| + final Hyperlink link;
|
| +
|
| + factory Group(String name, {String description: '', Hyperlink link}) {
|
| + var n = name.toLowerCase();
|
| + return builtin.firstWhere((g) => g.name == n,
|
| + orElse: () => new Group._(name,
|
| + custom: true, description: description, link: link));
|
| + }
|
| +
|
| + const Group._(this.name, {this.custom: false, this.description, this.link});
|
| +
|
| + @override
|
| + int compareTo(Group other) => name.compareTo(other.name);
|
| +}
|
| +
|
| +class Hyperlink {
|
| + final String label;
|
| + final String href;
|
| + final bool bold;
|
| + const Hyperlink(this.label, this.href, {this.bold: false});
|
| + String get html => '<a href="$href">${_emph(label)}</a>';
|
| + String _emph(msg) => bold ? '<strong>$msg</strong>' : msg;
|
| +}
|
| +
|
| +/// Thrown when an error occurs in linting.
|
| +class LinterException implements Exception {
|
| + /// A message describing the error.
|
| + final String message;
|
| +
|
| + /// Creates a new LinterException with an optional error [message].
|
| + const LinterException([this.message]);
|
| +
|
| + @override
|
| + String toString() =>
|
| + message == null ? "LinterException" : "LinterException: $message";
|
| +}
|
| +
|
| +/// Linter options.
|
| +class LinterOptions extends DriverOptions {
|
| + Iterable<LintRule> enabledLints;
|
| + LintFilter filter;
|
| + LinterOptions([this.enabledLints]) {
|
| + enabledLints ??= Registry.ruleRegistry;
|
| + }
|
| + void configure(LintConfig config) {
|
| + // TODO(pquitslund): revisit these default-to-on semantics.
|
| + enabledLints = Registry.ruleRegistry.where((LintRule rule) =>
|
| + !config.ruleConfigs.any((rc) => rc.disables(rule.name)));
|
| + filter = new FileGlobFilter(config.fileIncludes, config.fileExcludes);
|
| + }
|
| +}
|
| +
|
| +/// Filtered lints are ommitted from linter output.
|
| +abstract class LintFilter {
|
| + bool filter(AnalysisError lint);
|
| +}
|
| +
|
| +/// Describes a lint rule.
|
| +abstract class LintRule extends Linter implements Comparable<LintRule> {
|
| + /// Description (in markdown format) suitable for display in a detailed lint
|
| + /// description.
|
| + final String details;
|
| +
|
| + /// Short description suitable for display in console output.
|
| + final String description;
|
| +
|
| + /// Lint group (for example, 'style').
|
| + final Group group;
|
| +
|
| + /// Lint maturity (stable|experimental).
|
| + final Maturity maturity;
|
| +
|
| + /// Lint name.
|
| + @override
|
| + final String name;
|
| +
|
| + /// Until pubspec analysis is pushed into the analyzer proper, we need to
|
| + /// do some extra book-keeping to keep track of details that will help us
|
| + /// constitute AnalysisErrorInfos.
|
| + final List<AnalysisErrorInfo> _locationInfo = <AnalysisErrorInfo>[];
|
| +
|
| + LintRule(
|
| + {this.name,
|
| + this.group,
|
| + this.description,
|
| + this.details,
|
| + this.maturity: Maturity.stable});
|
| +
|
| + LintCode get lintCode => new _LintCode(name, description);
|
| +
|
| + @override
|
| + int compareTo(LintRule other) {
|
| + var g = group.compareTo(other.group);
|
| + if (g != 0) {
|
| + return g;
|
| + }
|
| + return name.compareTo(other.name);
|
| + }
|
| +
|
| + /// Return a visitor to be passed to provide access to Dart project context
|
| + /// and to perform project-level analyses.
|
| + ProjectVisitor getProjectVisitor() => null;
|
| +
|
| + /// Return a visitor to be passed to pubspecs to perform lint
|
| + /// analysis.
|
| + /// Lint errors are reported via this [Linter]'s error [reporter].
|
| + PubspecVisitor getPubspecVisitor() => null;
|
| +
|
| + @override
|
| + AstVisitor getVisitor() => null;
|
| +
|
| + void reportLint(AstNode node, {bool ignoreSyntheticNodes: true}) {
|
| + if (node != null && (!node.isSynthetic || !ignoreSyntheticNodes)) {
|
| + reporter.reportErrorForNode(lintCode, node, []);
|
| + }
|
| + }
|
| +
|
| + void reportLintForToken(Token token, {bool ignoreSyntheticTokens: true}) {
|
| + if (token != null && (!token.isSynthetic || !ignoreSyntheticTokens)) {
|
| + reporter.reportErrorForToken(lintCode, token, []);
|
| + }
|
| + }
|
| +
|
| + void reportPubLint(PSNode node) {
|
| + Source source = createSource(node.span.sourceUrl);
|
| +
|
| + // Cache error and location info for creating AnalysisErrorInfos
|
| + // Note that error columns are 1-based
|
| + AnalysisError error = new AnalysisError(
|
| + source, node.span.start.column + 1, node.span.length, lintCode);
|
| + LineInfo lineInfo = new LineInfo.fromContent(source.contents.data);
|
| +
|
| + _locationInfo.add(new AnalysisErrorInfoImpl([error], lineInfo));
|
| +
|
| + // Then do the reporting
|
| + reporter?.reportError(error);
|
| + }
|
| +}
|
| +
|
| +class Maturity implements Comparable<Maturity> {
|
| + static const Maturity stable = const Maturity._('stable', ordinal: 0);
|
| + static const Maturity experimental = const Maturity._('stable', ordinal: 1);
|
| +
|
| + final String name;
|
| + final int ordinal;
|
| +
|
| + factory Maturity(String name, {int ordinal}) {
|
| + switch (name.toLowerCase()) {
|
| + case 'stable':
|
| + return stable;
|
| + case 'experimental':
|
| + return experimental;
|
| + default:
|
| + return new Maturity._(name, ordinal: ordinal);
|
| + }
|
| + }
|
| +
|
| + const Maturity._(this.name, {this.ordinal});
|
| +
|
| + @override
|
| + int compareTo(Maturity other) => this.ordinal - other.ordinal;
|
| +}
|
| +
|
| +class PrintingReporter implements Reporter, Logger {
|
| + final Printer _print;
|
| +
|
| + const PrintingReporter([this._print = print]);
|
| +
|
| + @override
|
| + void exception(LinterException exception) {
|
| + _print('EXCEPTION: $exception');
|
| + }
|
| +
|
| + @override
|
| + void logError(String message, [CaughtException exception]) {
|
| + _print('ERROR: $message');
|
| + }
|
| +
|
| + @override
|
| + void logInformation(String message, [CaughtException exception]) {
|
| + _print('INFO: $message');
|
| + }
|
| +
|
| + @override
|
| + void warn(String message) {
|
| + _print('WARN: $message');
|
| + }
|
| +}
|
| +
|
| +abstract class Reporter {
|
| + void exception(LinterException exception);
|
| + void warn(String message);
|
| +}
|
| +
|
| +/// Linter implementation.
|
| +class SourceLinter implements DartLinter, AnalysisErrorListener {
|
| + @override
|
| + final errors = <AnalysisError>[];
|
| + @override
|
| + final LinterOptions options;
|
| + @override
|
| + final Reporter reporter;
|
| +
|
| + @override
|
| + int numSourcesAnalyzed;
|
| +
|
| + SourceLinter(this.options, {this.reporter: const PrintingReporter()});
|
| +
|
| + @override
|
| + Iterable<AnalysisErrorInfo> lintFiles(List<File> files) {
|
| + List<AnalysisErrorInfo> errors = [];
|
| + var analysisDriver = new AnalysisDriver(options);
|
| + errors.addAll(analysisDriver.analyze(files.where((f) => isDartFile(f))));
|
| + numSourcesAnalyzed = analysisDriver.numSourcesAnalyzed;
|
| + files.where((f) => isPubspecFile(f)).forEach((p) {
|
| + numSourcesAnalyzed++;
|
| + return errors.addAll(_lintPubspecFile(p));
|
| + });
|
| + return errors;
|
| + }
|
| +
|
| + @override
|
| + Iterable<AnalysisErrorInfo> lintPubspecSource(
|
| + {String contents, String sourcePath}) {
|
| + var results = <AnalysisErrorInfo>[];
|
| +
|
| + Uri sourceUrl = sourcePath == null ? null : p.toUri(sourcePath);
|
| +
|
| + var spec = new Pubspec.parse(contents, sourceUrl: sourceUrl);
|
| +
|
| + for (Linter lint in options.enabledLints) {
|
| + if (lint is LintRule) {
|
| + LintRule rule = lint;
|
| + var visitor = rule.getPubspecVisitor();
|
| + if (visitor != null) {
|
| + // Analyzer sets reporters; if this file is not being analyzed,
|
| + // we need to set one ourselves. (Needless to say, when pubspec
|
| + // processing gets pushed down, this hack can go away.)
|
| + if (rule.reporter == null && sourceUrl != null) {
|
| + var source = createSource(sourceUrl);
|
| + rule.reporter = new ErrorReporter(this, source);
|
| + }
|
| + try {
|
| + spec.accept(visitor);
|
| + } on Exception catch (e) {
|
| + reporter.exception(new LinterException(e.toString()));
|
| + }
|
| + if (rule._locationInfo != null && rule._locationInfo.isNotEmpty) {
|
| + results.addAll(rule._locationInfo);
|
| + rule._locationInfo.clear();
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + return results;
|
| + }
|
| +
|
| + @override
|
| + onError(AnalysisError error) => errors.add(error);
|
| +
|
| + @override
|
| + Iterable<AnalysisErrorInfo> _lintPubspecFile(File sourceFile) =>
|
| + lintPubspecSource(
|
| + contents: sourceFile.readAsStringSync(), sourcePath: sourceFile.path);
|
| +}
|
| +
|
| +class _LintCode extends LintCode {
|
| + static final registry = <String, LintCode>{};
|
| +
|
| + factory _LintCode(String name, String message) => registry.putIfAbsent(
|
| + name + message, () => new _LintCode._(name, message));
|
| +
|
| + _LintCode._(String name, String message) : super(name, message);
|
| +}
|
|
|