| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 import 'dart:io'; | |
| 6 | |
| 7 import 'package:analyzer/analyzer.dart'; | |
| 8 import 'package:analyzer/dart/ast/token.dart'; | |
| 9 import 'package:analyzer/src/generated/engine.dart' | |
| 10 show AnalysisErrorInfo, AnalysisErrorInfoImpl, Logger; | |
| 11 import 'package:analyzer/src/generated/java_engine.dart' show CaughtException; | |
| 12 import 'package:analyzer/src/generated/source.dart' show LineInfo; | |
| 13 import 'package:analyzer/src/generated/source_io.dart'; | |
| 14 import 'package:analyzer/src/lint/analysis.dart'; | |
| 15 import 'package:analyzer/src/lint/config.dart'; | |
| 16 import 'package:analyzer/src/lint/io.dart'; | |
| 17 import 'package:analyzer/src/lint/project.dart'; | |
| 18 import 'package:analyzer/src/lint/pub.dart'; | |
| 19 import 'package:analyzer/src/lint/registry.dart'; | |
| 20 import 'package:analyzer/src/services/lint.dart' show Linter; | |
| 21 import 'package:glob/glob.dart'; | |
| 22 import 'package:path/path.dart' as p; | |
| 23 | |
| 24 typedef Printer(String msg); | |
| 25 | |
| 26 /// Describes a String in valid camel case format. | |
| 27 class CamelCaseString { | |
| 28 static final _camelCaseMatcher = new RegExp(r'[A-Z][a-z]*'); | |
| 29 static final _camelCaseTester = new RegExp(r'^([_$]*)([A-Z?$]+[a-z0-9]*)+$'); | |
| 30 | |
| 31 final String value; | |
| 32 CamelCaseString(this.value) { | |
| 33 if (!isCamelCase(value)) { | |
| 34 throw new ArgumentError('$value is not CamelCase'); | |
| 35 } | |
| 36 } | |
| 37 | |
| 38 String get humanized => _humanize(value); | |
| 39 | |
| 40 @override | |
| 41 String toString() => value; | |
| 42 | |
| 43 static bool isCamelCase(String name) => _camelCaseTester.hasMatch(name); | |
| 44 | |
| 45 static String _humanize(String camelCase) => | |
| 46 _camelCaseMatcher.allMatches(camelCase).map((m) => m.group(0)).join(' '); | |
| 47 } | |
| 48 | |
| 49 /// Dart source linter. | |
| 50 class DartLinter implements AnalysisErrorListener { | |
| 51 final errors = <AnalysisError>[]; | |
| 52 | |
| 53 final LinterOptions options; | |
| 54 final Reporter reporter; | |
| 55 | |
| 56 /// The total number of sources that were analyzed. Only valid after | |
| 57 /// [lintFiles] has been called. | |
| 58 int numSourcesAnalyzed; | |
| 59 | |
| 60 /// Creates a new linter. | |
| 61 DartLinter(this.options, {this.reporter: const PrintingReporter()}); | |
| 62 | |
| 63 Iterable<AnalysisErrorInfo> lintFiles(List<File> files) { | |
| 64 List<AnalysisErrorInfo> errors = []; | |
| 65 var analysisDriver = new AnalysisDriver(options); | |
| 66 errors.addAll(analysisDriver.analyze(files.where((f) => isDartFile(f)))); | |
| 67 numSourcesAnalyzed = analysisDriver.numSourcesAnalyzed; | |
| 68 files.where((f) => isPubspecFile(f)).forEach((p) { | |
| 69 numSourcesAnalyzed++; | |
| 70 return errors.addAll(_lintPubspecFile(p)); | |
| 71 }); | |
| 72 return errors; | |
| 73 } | |
| 74 | |
| 75 Iterable<AnalysisErrorInfo> lintPubspecSource( | |
| 76 {String contents, String sourcePath}) { | |
| 77 var results = <AnalysisErrorInfo>[]; | |
| 78 | |
| 79 Uri sourceUrl = sourcePath == null ? null : p.toUri(sourcePath); | |
| 80 | |
| 81 var spec = new Pubspec.parse(contents, sourceUrl: sourceUrl); | |
| 82 | |
| 83 for (Linter lint in options.enabledLints) { | |
| 84 if (lint is LintRule) { | |
| 85 LintRule rule = lint; | |
| 86 var visitor = rule.getPubspecVisitor(); | |
| 87 if (visitor != null) { | |
| 88 // Analyzer sets reporters; if this file is not being analyzed, | |
| 89 // we need to set one ourselves. (Needless to say, when pubspec | |
| 90 // processing gets pushed down, this hack can go away.) | |
| 91 if (rule.reporter == null && sourceUrl != null) { | |
| 92 var source = createSource(sourceUrl); | |
| 93 rule.reporter = new ErrorReporter(this, source); | |
| 94 } | |
| 95 try { | |
| 96 spec.accept(visitor); | |
| 97 } on Exception catch (e) { | |
| 98 reporter.exception(new LinterException(e.toString())); | |
| 99 } | |
| 100 if (rule._locationInfo != null && rule._locationInfo.isNotEmpty) { | |
| 101 results.addAll(rule._locationInfo); | |
| 102 rule._locationInfo.clear(); | |
| 103 } | |
| 104 } | |
| 105 } | |
| 106 } | |
| 107 | |
| 108 return results; | |
| 109 } | |
| 110 | |
| 111 @override | |
| 112 onError(AnalysisError error) => errors.add(error); | |
| 113 | |
| 114 Iterable<AnalysisErrorInfo> _lintPubspecFile(File sourceFile) => | |
| 115 lintPubspecSource( | |
| 116 contents: sourceFile.readAsStringSync(), sourcePath: sourceFile.path); | |
| 117 } | |
| 118 | |
| 119 class FileGlobFilter extends LintFilter { | |
| 120 Iterable<Glob> includes; | |
| 121 Iterable<Glob> excludes; | |
| 122 | |
| 123 FileGlobFilter([Iterable<String> includeGlobs, Iterable<String> excludeGlobs]) | |
| 124 : includes = includeGlobs.map((glob) => new Glob(glob)), | |
| 125 excludes = excludeGlobs.map((glob) => new Glob(glob)); | |
| 126 | |
| 127 @override | |
| 128 bool filter(AnalysisError lint) { | |
| 129 // TODO specify order | |
| 130 return excludes.any((glob) => glob.matches(lint.source.fullName)) && | |
| 131 !includes.any((glob) => glob.matches(lint.source.fullName)); | |
| 132 } | |
| 133 } | |
| 134 | |
| 135 class Group implements Comparable<Group> { | |
| 136 /// Defined rule groups. | |
| 137 static const Group errors = | |
| 138 const Group._('errors', description: 'Possible coding errors.'); | |
| 139 static const Group pub = const Group._('pub', | |
| 140 description: 'Pub-related rules.', | |
| 141 link: const Hyperlink('See the <strong>Pubspec Format</strong>', | |
| 142 'https://www.dartlang.org/tools/pub/pubspec.html')); | |
| 143 static const Group style = const Group._('style', | |
| 144 description: | |
| 145 'Matters of style, largely derived from the official Dart Style Guide.
', | |
| 146 link: const Hyperlink('See the <strong>Style Guide</strong>', | |
| 147 'https://www.dartlang.org/articles/style-guide/')); | |
| 148 | |
| 149 /// List of builtin groups in presentation order. | |
| 150 static const Iterable<Group> builtin = const [errors, style, pub]; | |
| 151 | |
| 152 final String name; | |
| 153 final bool custom; | |
| 154 final String description; | |
| 155 final Hyperlink link; | |
| 156 | |
| 157 factory Group(String name, {String description: '', Hyperlink link}) { | |
| 158 var n = name.toLowerCase(); | |
| 159 return builtin.firstWhere((g) => g.name == n, | |
| 160 orElse: () => new Group._(name, | |
| 161 custom: true, description: description, link: link)); | |
| 162 } | |
| 163 | |
| 164 const Group._(this.name, {this.custom: false, this.description, this.link}); | |
| 165 | |
| 166 @override | |
| 167 int compareTo(Group other) => name.compareTo(other.name); | |
| 168 } | |
| 169 | |
| 170 class Hyperlink { | |
| 171 final String label; | |
| 172 final String href; | |
| 173 final bool bold; | |
| 174 const Hyperlink(this.label, this.href, {this.bold: false}); | |
| 175 String get html => '<a href="$href">${_emph(label)}</a>'; | |
| 176 String _emph(msg) => bold ? '<strong>$msg</strong>' : msg; | |
| 177 } | |
| 178 | |
| 179 /// Thrown when an error occurs in linting. | |
| 180 class LinterException implements Exception { | |
| 181 /// A message describing the error. | |
| 182 final String message; | |
| 183 | |
| 184 /// Creates a new LinterException with an optional error [message]. | |
| 185 const LinterException([this.message]); | |
| 186 | |
| 187 @override | |
| 188 String toString() => | |
| 189 message == null ? "LinterException" : "LinterException: $message"; | |
| 190 } | |
| 191 | |
| 192 /// Linter options. | |
| 193 class LinterOptions extends DriverOptions { | |
| 194 Iterable<LintRule> enabledLints; | |
| 195 LintFilter filter; | |
| 196 LinterOptions([this.enabledLints]) { | |
| 197 enabledLints ??= Registry.ruleRegistry; | |
| 198 } | |
| 199 void configure(LintConfig config) { | |
| 200 // TODO(pquitslund): revisit these default-to-on semantics. | |
| 201 enabledLints = Registry.ruleRegistry.where((LintRule rule) => | |
| 202 !config.ruleConfigs.any((rc) => rc.disables(rule.name))); | |
| 203 filter = new FileGlobFilter(config.fileIncludes, config.fileExcludes); | |
| 204 } | |
| 205 } | |
| 206 | |
| 207 /// Filtered lints are ommitted from linter output. | |
| 208 abstract class LintFilter { | |
| 209 bool filter(AnalysisError lint); | |
| 210 } | |
| 211 | |
| 212 /// Describes a lint rule. | |
| 213 abstract class LintRule extends Linter implements Comparable<LintRule> { | |
| 214 /// Description (in markdown format) suitable for display in a detailed lint | |
| 215 /// description. | |
| 216 final String details; | |
| 217 | |
| 218 /// Short description suitable for display in console output. | |
| 219 final String description; | |
| 220 | |
| 221 /// Lint group (for example, 'style'). | |
| 222 final Group group; | |
| 223 | |
| 224 /// Lint maturity (stable|experimental). | |
| 225 final Maturity maturity; | |
| 226 | |
| 227 /// Lint name. | |
| 228 @override | |
| 229 final String name; | |
| 230 | |
| 231 /// Until pubspec analysis is pushed into the analyzer proper, we need to | |
| 232 /// do some extra book-keeping to keep track of details that will help us | |
| 233 /// constitute AnalysisErrorInfos. | |
| 234 final List<AnalysisErrorInfo> _locationInfo = <AnalysisErrorInfo>[]; | |
| 235 | |
| 236 LintRule( | |
| 237 {this.name, | |
| 238 this.group, | |
| 239 this.description, | |
| 240 this.details, | |
| 241 this.maturity: Maturity.stable}); | |
| 242 | |
| 243 LintCode get lintCode => new _LintCode(name, description); | |
| 244 | |
| 245 @override | |
| 246 int compareTo(LintRule other) { | |
| 247 var g = group.compareTo(other.group); | |
| 248 if (g != 0) { | |
| 249 return g; | |
| 250 } | |
| 251 return name.compareTo(other.name); | |
| 252 } | |
| 253 | |
| 254 /// Return a visitor to be passed to provide access to Dart project context | |
| 255 /// and to perform project-level analyses. | |
| 256 ProjectVisitor getProjectVisitor() => null; | |
| 257 | |
| 258 /// Return a visitor to be passed to pubspecs to perform lint | |
| 259 /// analysis. | |
| 260 /// Lint errors are reported via this [Linter]'s error [reporter]. | |
| 261 PubspecVisitor getPubspecVisitor() => null; | |
| 262 | |
| 263 @override | |
| 264 AstVisitor getVisitor() => null; | |
| 265 | |
| 266 void reportLint(AstNode node, {bool ignoreSyntheticNodes: true}) { | |
| 267 if (node != null && (!node.isSynthetic || !ignoreSyntheticNodes)) { | |
| 268 reporter.reportErrorForNode(lintCode, node, []); | |
| 269 } | |
| 270 } | |
| 271 | |
| 272 void reportLintForToken(Token token, {bool ignoreSyntheticTokens: true}) { | |
| 273 if (token != null && (!token.isSynthetic || !ignoreSyntheticTokens)) { | |
| 274 reporter.reportErrorForToken(lintCode, token, []); | |
| 275 } | |
| 276 } | |
| 277 | |
| 278 void reportPubLint(PSNode node) { | |
| 279 Source source = createSource(node.span.sourceUrl); | |
| 280 | |
| 281 // Cache error and location info for creating AnalysisErrorInfos | |
| 282 // Note that error columns are 1-based | |
| 283 AnalysisError error = new AnalysisError( | |
| 284 source, node.span.start.column + 1, node.span.length, lintCode); | |
| 285 LineInfo lineInfo = new LineInfo.fromContent(source.contents.data); | |
| 286 | |
| 287 _locationInfo.add(new AnalysisErrorInfoImpl([error], lineInfo)); | |
| 288 | |
| 289 // Then do the reporting | |
| 290 reporter?.reportError(error); | |
| 291 } | |
| 292 } | |
| 293 | |
| 294 class Maturity implements Comparable<Maturity> { | |
| 295 static const Maturity stable = const Maturity._('stable', ordinal: 0); | |
| 296 static const Maturity experimental = const Maturity._('stable', ordinal: 1); | |
| 297 | |
| 298 final String name; | |
| 299 final int ordinal; | |
| 300 | |
| 301 factory Maturity(String name, {int ordinal}) { | |
| 302 switch (name.toLowerCase()) { | |
| 303 case 'stable': | |
| 304 return stable; | |
| 305 case 'experimental': | |
| 306 return experimental; | |
| 307 default: | |
| 308 return new Maturity._(name, ordinal: ordinal); | |
| 309 } | |
| 310 } | |
| 311 | |
| 312 const Maturity._(this.name, {this.ordinal}); | |
| 313 | |
| 314 @override | |
| 315 int compareTo(Maturity other) => this.ordinal - other.ordinal; | |
| 316 } | |
| 317 | |
| 318 class PrintingReporter implements Reporter, Logger { | |
| 319 final Printer _print; | |
| 320 | |
| 321 const PrintingReporter([this._print = print]); | |
| 322 | |
| 323 @override | |
| 324 void exception(LinterException exception) { | |
| 325 _print('EXCEPTION: $exception'); | |
| 326 } | |
| 327 | |
| 328 @override | |
| 329 void logError(String message, [CaughtException exception]) { | |
| 330 _print('ERROR: $message'); | |
| 331 } | |
| 332 | |
| 333 @override | |
| 334 void logInformation(String message, [CaughtException exception]) { | |
| 335 _print('INFO: $message'); | |
| 336 } | |
| 337 | |
| 338 @override | |
| 339 void warn(String message) { | |
| 340 _print('WARN: $message'); | |
| 341 } | |
| 342 } | |
| 343 | |
| 344 abstract class Reporter { | |
| 345 void exception(LinterException exception); | |
| 346 void warn(String message); | |
| 347 } | |
| 348 | |
| 349 /// Linter implementation. | |
| 350 class SourceLinter implements DartLinter, AnalysisErrorListener { | |
| 351 @override | |
| 352 final errors = <AnalysisError>[]; | |
| 353 @override | |
| 354 final LinterOptions options; | |
| 355 @override | |
| 356 final Reporter reporter; | |
| 357 | |
| 358 @override | |
| 359 int numSourcesAnalyzed; | |
| 360 | |
| 361 SourceLinter(this.options, {this.reporter: const PrintingReporter()}); | |
| 362 | |
| 363 @override | |
| 364 Iterable<AnalysisErrorInfo> lintFiles(List<File> files) { | |
| 365 List<AnalysisErrorInfo> errors = []; | |
| 366 var analysisDriver = new AnalysisDriver(options); | |
| 367 errors.addAll(analysisDriver.analyze(files.where((f) => isDartFile(f)))); | |
| 368 numSourcesAnalyzed = analysisDriver.numSourcesAnalyzed; | |
| 369 files.where((f) => isPubspecFile(f)).forEach((p) { | |
| 370 numSourcesAnalyzed++; | |
| 371 return errors.addAll(_lintPubspecFile(p)); | |
| 372 }); | |
| 373 return errors; | |
| 374 } | |
| 375 | |
| 376 @override | |
| 377 Iterable<AnalysisErrorInfo> lintPubspecSource( | |
| 378 {String contents, String sourcePath}) { | |
| 379 var results = <AnalysisErrorInfo>[]; | |
| 380 | |
| 381 Uri sourceUrl = sourcePath == null ? null : p.toUri(sourcePath); | |
| 382 | |
| 383 var spec = new Pubspec.parse(contents, sourceUrl: sourceUrl); | |
| 384 | |
| 385 for (Linter lint in options.enabledLints) { | |
| 386 if (lint is LintRule) { | |
| 387 LintRule rule = lint; | |
| 388 var visitor = rule.getPubspecVisitor(); | |
| 389 if (visitor != null) { | |
| 390 // Analyzer sets reporters; if this file is not being analyzed, | |
| 391 // we need to set one ourselves. (Needless to say, when pubspec | |
| 392 // processing gets pushed down, this hack can go away.) | |
| 393 if (rule.reporter == null && sourceUrl != null) { | |
| 394 var source = createSource(sourceUrl); | |
| 395 rule.reporter = new ErrorReporter(this, source); | |
| 396 } | |
| 397 try { | |
| 398 spec.accept(visitor); | |
| 399 } on Exception catch (e) { | |
| 400 reporter.exception(new LinterException(e.toString())); | |
| 401 } | |
| 402 if (rule._locationInfo != null && rule._locationInfo.isNotEmpty) { | |
| 403 results.addAll(rule._locationInfo); | |
| 404 rule._locationInfo.clear(); | |
| 405 } | |
| 406 } | |
| 407 } | |
| 408 } | |
| 409 | |
| 410 return results; | |
| 411 } | |
| 412 | |
| 413 @override | |
| 414 onError(AnalysisError error) => errors.add(error); | |
| 415 | |
| 416 @override | |
| 417 Iterable<AnalysisErrorInfo> _lintPubspecFile(File sourceFile) => | |
| 418 lintPubspecSource( | |
| 419 contents: sourceFile.readAsStringSync(), sourcePath: sourceFile.path); | |
| 420 } | |
| 421 | |
| 422 class _LintCode extends LintCode { | |
| 423 static final registry = <String, LintCode>{}; | |
| 424 | |
| 425 factory _LintCode(String name, String message) => registry.putIfAbsent( | |
| 426 name + message, () => new _LintCode._(name, message)); | |
| 427 | |
| 428 _LintCode._(String name, String message) : super(name, message); | |
| 429 } | |
| OLD | NEW |