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 |