blob: 5706f35ea8d26f8d5198b2c6bea474b33e6a6250 [file] [log] [blame]
// 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);
}