| // 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. |
| |
| library linter.test.lint_test; |
| |
| import 'dart:io'; |
| |
| import 'package:analyzer/src/generated/ast.dart'; |
| import 'package:analyzer/src/generated/engine.dart'; |
| import 'package:analyzer/src/generated/error.dart'; |
| import 'package:analyzer/src/generated/source.dart'; |
| import 'package:analyzer/src/string_source.dart'; |
| import 'package:cli_util/cli_util.dart'; |
| import 'package:linter/src/analysis.dart'; |
| import 'package:linter/src/ast.dart'; |
| import 'package:linter/src/io.dart'; |
| import 'package:linter/src/linter.dart'; |
| import 'package:linter/src/plugin/linter_plugin.dart'; |
| import 'package:linter/src/pub.dart'; |
| import 'package:linter/src/rules.dart'; |
| import 'package:linter/src/rules/camel_case_types.dart'; |
| import 'package:linter/src/rules/package_prefixed_library_names.dart'; |
| import 'package:linter/src/util.dart'; |
| import 'package:mockito/mockito.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:plugin/manager.dart'; |
| import 'package:unittest/unittest.dart'; |
| |
| import '../bin/linter.dart' as dartlint; |
| import 'mocks.dart'; |
| |
| main() { |
| groupSep = ' | '; |
| |
| defineSanityTests(); |
| defineLinterEngineTests(); |
| definePluginTests(); |
| defineRuleTests(); |
| defineRuleUnitTests(); |
| } |
| |
| const String ruleDir = 'test/rules'; |
| |
| /// Linter engine tests |
| void defineLinterEngineTests() { |
| group('engine', () { |
| group('reporter', () { |
| _test(String label, String expected, report(PrintingReporter r)) { |
| test(label, () { |
| String msg; |
| PrintingReporter reporter = new PrintingReporter((m) => msg = m); |
| report(reporter); |
| expect(msg, expected); |
| }); |
| } |
| _test('exception', 'EXCEPTION: LinterException: foo', |
| (r) => r.exception(new LinterException('foo'))); |
| _test('logError', 'ERROR: foo', (r) => r.logError('foo')); |
| _test('logError2', 'ERROR: foo', |
| (r) => r.logError2('foo', new Exception())); |
| _test('logInformation', 'INFO: foo', (r) => r.logInformation('foo')); |
| _test('logInformation2', 'INFO: foo', |
| (r) => r.logInformation2('foo', new Exception())); |
| _test('warn', 'WARN: foo', (r) => r.warn('foo')); |
| }); |
| |
| group('exceptions', () { |
| test('message', () { |
| expect(const LinterException('foo').message, equals('foo')); |
| }); |
| test('toString', () { |
| expect(const LinterException().toString(), equals('LinterException')); |
| expect(const LinterException('foo').toString(), |
| equals('LinterException: foo')); |
| }); |
| }); |
| |
| group('analysis logger', () { |
| var currentErr = errorSink; |
| var currentOut = outSink; |
| var errCollector = new CollectingSink(); |
| var outCollector = new CollectingSink(); |
| var logger = new StdLogger(); |
| setUp(() { |
| errorSink = errCollector; |
| outSink = outCollector; |
| }); |
| tearDown(() { |
| errorSink = currentErr; |
| outSink = currentOut; |
| errCollector.buffer.clear(); |
| outCollector.buffer.clear(); |
| }); |
| test('logError', () { |
| logger.logError('logError'); |
| expect(errCollector.trim(), equals('logError')); |
| }); |
| test('logError2', () { |
| logger.logError2('logError2', null); |
| expect(errCollector.trim(), equals('logError2')); |
| }); |
| test('logInformation', () { |
| logger.logInformation('logInformation'); |
| expect(outCollector.trim(), equals('logInformation')); |
| }); |
| test('logInformation2', () { |
| logger.logInformation2('logInformation2', null); |
| expect(outCollector.trim(), equals('logInformation2')); |
| }); |
| }); |
| |
| group('camel case', () { |
| test('humanize', () { |
| expect(new CamelCaseString('FooBar').humanized, equals('Foo Bar')); |
| expect(new CamelCaseString('Foo').humanized, equals('Foo')); |
| }); |
| test('validation', () { |
| expect(() => new CamelCaseString('foo'), |
| throwsA(new isInstanceOf<ArgumentError>())); |
| }); |
| test('toString', () { |
| expect(new CamelCaseString('FooBar').toString(), equals('FooBar')); |
| }); |
| }); |
| |
| group('groups', () { |
| test('factory', () { |
| expect(new Group('style').custom, isFalse); |
| expect(new Group('pub').custom, isFalse); |
| expect(new Group('errors').custom, isFalse); |
| expect(new Group('Kustom').custom, isTrue); |
| }); |
| test('builtins', () { |
| expect(Group.builtin.contains(Group.style), isTrue); |
| expect(Group.builtin.contains(Group.errors), isTrue); |
| expect(Group.builtin.contains(Group.pub), isTrue); |
| }); |
| }); |
| |
| group('lint driver', () { |
| test('pubspec', () { |
| bool visited; |
| var options = |
| new LinterOptions([new MockLinter((n) => visited = true)]); |
| new SourceLinter(options).lintPubspecSource(contents: 'name: foo_bar'); |
| expect(visited, isTrue); |
| }); |
| test('error collecting', () { |
| var error = new AnalysisError.con1(new StringSource('foo', ''), |
| new LintCode('MockLint', 'This is a test...')); |
| var linter = new SourceLinter(new LinterOptions([])); |
| linter.onError(error); |
| expect(linter.errors.contains(error), isTrue); |
| }); |
| test('pubspec visitor error handling', () { |
| var rule = new MockRule(); |
| var visitor = new MockPubVisitor(); |
| when(visitor.visitPackageAuthor(any)) |
| .thenAnswer((_) => throw new Exception()); |
| when(rule.getPubspecVisitor()).thenReturn(visitor); |
| |
| var reporter = new MockReporter(); |
| var linter = |
| new SourceLinter(new LinterOptions([rule]), reporter: reporter); |
| linter.lintPubspecSource(contents: 'author: foo'); |
| verify(reporter.exception(any)).called(1); |
| }); |
| }); |
| |
| group('main', () { |
| setUp(() { |
| exitCode = 0; |
| errorSink = new MockIOSink(); |
| }); |
| tearDown(() { |
| exitCode = 0; |
| errorSink = stderr; |
| }); |
| test('smoke', () { |
| FileSystemEntity firstRuleTest = |
| new Directory(ruleDir).listSync().firstWhere((f) => isDartFile(f)); |
| dartlint.main([firstRuleTest.path]); |
| expect(dartlint.isLinterErrorCode(exitCode), isFalse); |
| }); |
| test('no args', () { |
| dartlint.main([]); |
| expect(exitCode, equals(dartlint.unableToProcessExitCode)); |
| }); |
| test('help', () { |
| dartlint.main(['-h']); |
| // Help shouldn't generate an error code |
| expect(dartlint.isLinterErrorCode(exitCode), isFalse); |
| }); |
| test('unknown arg', () { |
| dartlint.main(['-XXXXX']); |
| expect(exitCode, equals(dartlint.unableToProcessExitCode)); |
| }); |
| //TODO: revisit error handling |
| skip_test('bad path', () { |
| var badPath = new Directory(ruleDir).path + '/___NonExistent.dart'; |
| dartlint.main([badPath]); |
| expect(exitCode, equals(dartlint.unableToProcessExitCode)); |
| }); |
| test('custom sdk path', () { |
| // Smoke test to ensure a custom sdk path doesn't sink the ship |
| FileSystemEntity firstRuleTest = |
| new Directory(ruleDir).listSync().firstWhere((f) => isDartFile(f)); |
| var sdk = getSdkDir(); |
| dartlint.main(['--dart-sdk', sdk.path, firstRuleTest.path]); |
| expect(dartlint.isLinterErrorCode(exitCode), isFalse); |
| }); |
| test('custom package root', () { |
| // Smoke test to ensure a custom package root doesn't sink the ship |
| FileSystemEntity firstRuleTest = |
| new Directory(ruleDir).listSync().firstWhere((f) => isDartFile(f)); |
| var packageDir = new Directory('.').path; |
| dartlint.main(['--package-root', packageDir, firstRuleTest.path]); |
| expect(dartlint.isLinterErrorCode(exitCode), isFalse); |
| }); |
| }); |
| |
| group('dtos', () { |
| group('hyperlink', () { |
| test('html', () { |
| Hyperlink link = new Hyperlink('dart', 'http://dartlang.org'); |
| expect(link.html, equals('<a href="http://dartlang.org">dart</a>')); |
| }); |
| test('html - strong', () { |
| Hyperlink link = |
| new Hyperlink('dart', 'http://dartlang.org', bold: true); |
| expect(link.html, equals( |
| '<a href="http://dartlang.org"><strong>dart</strong></a>')); |
| }); |
| }); |
| |
| group('rule', () { |
| test('comparing', () { |
| LintRule r1 = new MockLintRule('Bar', new Group('acme')); |
| LintRule r2 = new MockLintRule('Foo', new Group('acme')); |
| expect(r1.compareTo(r2), equals(-1)); |
| LintRule r3 = new MockLintRule('Bar', new Group('acme')); |
| LintRule r4 = new MockLintRule('Bar', new Group('woody')); |
| expect(r3.compareTo(r4), equals(-1)); |
| }); |
| }); |
| group('maturity', () { |
| test('comparing', () { |
| // Custom |
| Maturity m1 = new Maturity('foo', ordinal: 0); |
| Maturity m2 = new Maturity('bar', ordinal: 1); |
| expect(m1.compareTo(m2), equals(-1)); |
| // Builtin |
| expect(Maturity.stable.compareTo(Maturity.experimental), equals(-1)); |
| }); |
| }); |
| }); |
| }); |
| } |
| |
| /// Default contributed lint rules. |
| var builtinRules = [ |
| 'camel_case_types', |
| 'constant_identifier_names', |
| 'empty_constructor_bodies', |
| 'library_names', |
| 'library_prefixes', |
| 'non_constant_identifier_names', |
| 'one_member_abstracts', |
| 'slash_for_doc_comments', |
| 'super_goes_last', |
| 'type_init_formals', |
| 'unnecessary_brace_in_string_interp' |
| ]; |
| |
| /// Plugin tests |
| definePluginTests() { |
| group('plugin', () { |
| test('contributed rules', () { |
| LinterPlugin linterPlugin = new LinterPlugin(); |
| ExtensionManager manager = new ExtensionManager(); |
| manager.processPlugins([linterPlugin]); |
| var contributedRules = linterPlugin.lintRules.map((rule) => rule.name); |
| expect(contributedRules, unorderedEquals(builtinRules)); |
| }); |
| }); |
| } |
| |
| /// Rule tests |
| defineRuleTests() { |
| |
| //TODO: if ruleDir cannot be found print message to set CWD to project root |
| group('rule', () { |
| group('dart', () { |
| for (var entry in new Directory(ruleDir).listSync()) { |
| if (entry is! File || !isDartFile(entry)) continue; |
| var ruleName = p.basenameWithoutExtension(entry.path); |
| testRule(ruleName, entry); |
| } |
| }); |
| group('pub', () { |
| for (var entry in new Directory(ruleDir + '/pub').listSync()) { |
| if (entry is! Directory) continue; |
| Directory pubTestDir = entry; |
| for (var file in pubTestDir.listSync()) { |
| if (file is! File || !isPubspecFile(file)) continue; |
| var ruleName = p.basename(pubTestDir.path); |
| testRule(ruleName, file); |
| } |
| } |
| }); |
| }); |
| } |
| |
| defineRuleUnitTests() { |
| group('names', () { |
| group('keywords', () { |
| var good = ['class', 'if', 'assert', 'catch', 'import']; |
| testEach(good, isKeyWord, isTrue); |
| var bad = ['_class', 'iff', 'assert_', 'Catch']; |
| testEach(bad, isKeyWord, isFalse); |
| }); |
| group('identifiers', () { |
| var good = ['foo', '_if', '_', 'f2', 'fooBar', 'foo_bar']; |
| testEach(good, isValidDartIdentifier, isTrue); |
| var bad = ['if', '42', '3', '2f']; |
| testEach(bad, isValidDartIdentifier, isFalse); |
| }); |
| group('pubspec', () { |
| testEach(['pubspec.yaml', '_pubspec.yaml'], isPubspecFileName, isTrue); |
| testEach(['__pubspec.yaml', 'foo.yaml'], isPubspecFileName, isFalse); |
| }); |
| |
| group('camel case', () { |
| group('upper', () { |
| var good = [ |
| '_FooBar', |
| 'FooBar', |
| '_Foo', |
| 'Foo', |
| 'F', |
| 'FB', |
| 'F1', |
| 'FooBar1' |
| ]; |
| testEach(good, isUpperCamelCase, isTrue); |
| var bad = ['fooBar', 'foo', 'f', '_f', 'F_B']; |
| testEach(bad, isUpperCamelCase, isFalse); |
| }); |
| }); |
| group('lower_case_underscores', () { |
| var good = ['foo_bar', 'foo', 'foo_bar_baz', 'p', 'p1', 'p21', 'p1ll0']; |
| testEach(good, isLowerCaseUnderScore, isTrue); |
| |
| var bad = [ |
| 'Foo', |
| 'fooBar', |
| 'foo_Bar', |
| 'foo_', |
| '_f', |
| 'F_B', |
| 'JS', |
| 'JSON', |
| '1', |
| '1b' |
| ]; |
| testEach(bad, isLowerCaseUnderScore, isFalse); |
| }); |
| group('qualified lower_case_underscores', () { |
| var good = [ |
| 'bwu_server.shared.datastore.some_file', |
| 'foo_bar.baz', |
| 'foo_bar', |
| 'foo.bar', |
| 'foo_bar_baz', |
| 'foo', |
| 'foo.bar_baz.bang', |
| 'a.b', |
| 'a.b.c', |
| 'p2.src.acme' |
| ]; |
| testEach(good, isLowerCaseUnderScoreWithDots, isTrue); |
| |
| var bad = [ |
| 'Foo', |
| 'fooBar.', |
| '.foo_Bar', |
| 'foo_', |
| '_f', |
| 'F_B', |
| 'JS', |
| 'JSON' |
| ]; |
| testEach(bad, isLowerCaseUnderScoreWithDots, isFalse); |
| }); |
| group('lowerCamelCase', () { |
| var good = ['fooBar', 'foo', 'f', 'f1', '_f', '_foo', '_']; |
| testEach(good, isLowerCamelCase, isTrue); |
| |
| var bad = ['Foo', 'foo_', 'foo_bar']; |
| testEach(bad, isLowerCamelCase, isFalse); |
| }); |
| group('libary_name_prefixes', () { |
| testEach( |
| Iterable<List<String>> values, dynamic f(List<String> s), Matcher m) { |
| values.forEach((s) => test('${s[3]}', () => expect(f(s), m))); |
| } |
| |
| bool isGoodPrefx(List<String> v) => matchesOrIsPrefixedBy(v[3], |
| createLibraryNamePrefix( |
| libraryPath: v[0], projectRoot: v[1], packageName: v[2])); |
| |
| var good = [ |
| ['/u/b/c/lib/src/a.dart', '/u/b/c', 'acme', 'acme.src.a'], |
| ['/u/b/c/lib/a.dart', '/u/b/c', 'acme', 'acme.a'], |
| ['/u/b/c/test/a.dart', '/u/b/c', 'acme', 'acme.test.a'], |
| ['/u/b/c/test/data/a.dart', '/u/b/c', 'acme', 'acme.test.data.a'], |
| ['/u/b/c/lib/acme.dart', '/u/b/c', 'acme', 'acme'] |
| ]; |
| testEach(good, isGoodPrefx, isTrue); |
| |
| var bad = [ |
| ['/u/b/c/lib/src/a.dart', '/u/b/c', 'acme', 'acme.a'], |
| ['/u/b/c/lib/a.dart', '/u/b/c', 'acme', 'wrk.acme.a'], |
| ['/u/b/c/test/a.dart', '/u/b/c', 'acme', 'acme.a'], |
| ['/u/b/c/test/data/a.dart', '/u/b/c', 'acme', 'acme.test.a'] |
| ]; |
| testEach(bad, isGoodPrefx, isFalse); |
| }); |
| }); |
| } |
| |
| /// Test framework sanity |
| defineSanityTests() { |
| group('test framework', () { |
| group('annotation', () { |
| test('extraction', () { |
| expect(extractAnnotation('int x; // LINT [1:3]'), isNotNull); |
| expect(extractAnnotation('int x; //LINT'), isNotNull); |
| expect(extractAnnotation('int x; // OK'), isNull); |
| expect(extractAnnotation('int x;'), isNull); |
| expect(extractAnnotation('dynamic x; // LINT dynamic is bad').message, |
| equals('dynamic is bad')); |
| expect(extractAnnotation( |
| 'dynamic x; // LINT [1:3] dynamic is bad').message, |
| equals('dynamic is bad')); |
| expect( |
| extractAnnotation('dynamic x; // LINT [1:3] dynamic is bad').column, |
| equals(1)); |
| expect( |
| extractAnnotation('dynamic x; // LINT [1:3] dynamic is bad').length, |
| equals(3)); |
| expect(extractAnnotation('dynamic x; //LINT').message, isNull); |
| expect(extractAnnotation('dynamic x; //LINT ').message, isNull); |
| }); |
| }); |
| test('equality', () { |
| expect( |
| new Annotation('Actual message (to be ignored)', ErrorType.LINT, 1), |
| matchesAnnotation(null, ErrorType.LINT, 1)); |
| expect(new Annotation('Message', ErrorType.LINT, 1), |
| matchesAnnotation('Message', ErrorType.LINT, 1)); |
| }); |
| test('inequality', () { |
| expect(() => expect(new Annotation('Message', ErrorType.LINT, 1), |
| matchesAnnotation('Message', ErrorType.HINT, 1)), |
| throwsA(new isInstanceOf<TestFailure>())); |
| expect(() => expect(new Annotation('Message', ErrorType.LINT, 1), |
| matchesAnnotation('Message2', ErrorType.LINT, 1)), |
| throwsA(new isInstanceOf<TestFailure>())); |
| expect(() => expect(new Annotation('Message', ErrorType.LINT, 1), |
| matchesAnnotation('Message', ErrorType.LINT, 2)), |
| throwsA(new isInstanceOf<TestFailure>())); |
| }); |
| }); |
| } |
| |
| Annotation extractAnnotation(String line) { |
| int index = line.indexOf(new RegExp(r'(//|#)[ ]?LINT')); |
| if (index > -1) { |
| int column; |
| int length; |
| var annotation = line.substring(index); |
| var leftBrace = annotation.indexOf('['); |
| if (leftBrace != -1) { |
| var sep = annotation.indexOf(':'); |
| column = int.parse(annotation.substring(leftBrace + 1, sep)); |
| var rightBrace = annotation.indexOf(']'); |
| length = int.parse(annotation.substring(sep + 1, rightBrace)); |
| } |
| |
| int msgIndex = annotation.indexOf(']') + 1; |
| if (msgIndex < 1) { |
| msgIndex = annotation.indexOf('T') + 1; |
| } |
| String msg = null; |
| if (msgIndex < line.length) { |
| msg = line.substring(index + msgIndex).trim(); |
| if (msg.length == 0) { |
| msg = null; |
| } |
| } |
| return new Annotation.forLint(msg, column, length); |
| } |
| return null; |
| } |
| |
| AnnotationMatcher matchesAnnotation( |
| String message, ErrorType type, int lineNumber) => |
| new AnnotationMatcher(new Annotation(message, type, lineNumber)); |
| |
| testEach(Iterable<String> values, dynamic f(String s), Matcher m) { |
| values.forEach((s) => test('"$s"', () => expect(f(s), m))); |
| } |
| |
| testRule(String ruleName, File file) { |
| test('$ruleName', () { |
| var expected = <AnnotationMatcher>[]; |
| |
| int lineNumber = 1; |
| for (var line in file.readAsLinesSync()) { |
| var annotation = extractAnnotation(line); |
| if (annotation != null) { |
| annotation.lineNumber = lineNumber; |
| expected.add(new AnnotationMatcher(annotation)); |
| } |
| ++lineNumber; |
| } |
| |
| DartLinter driver = new DartLinter.forRules( |
| [ruleRegistry[ruleName]].where((rule) => rule != null)); |
| |
| Iterable<AnalysisErrorInfo> lints = driver.lintFiles([file]); |
| |
| List<Annotation> actual = []; |
| lints.forEach((AnalysisErrorInfo info) { |
| info.errors.forEach((AnalysisError error) { |
| if (error.errorCode.type == ErrorType.LINT) { |
| actual.add(new Annotation.forError(error, info.lineInfo)); |
| } |
| }); |
| }); |
| expect(actual, unorderedMatches(expected)); |
| }); |
| } |
| |
| typedef nodeVisitor(node); |
| |
| typedef dynamic /* AstVisitor, PubSpecVisitor*/ VisitorCallback(); |
| |
| class Annotation { |
| final int column; |
| final int length; |
| final String message; |
| final ErrorType type; |
| int lineNumber; |
| |
| Annotation(this.message, this.type, this.lineNumber, |
| {this.column, this.length}); |
| |
| Annotation.forError(AnalysisError error, LineInfo lineInfo) : this( |
| error.message, error.errorCode.type, |
| lineInfo.getLocation(error.offset).lineNumber, |
| column: lineInfo.getLocation(error.offset).columnNumber, |
| length: error.length); |
| |
| Annotation.forLint([String message, int column, int length]) |
| : this(message, ErrorType.LINT, null, column: column, length: length); |
| |
| String toString() => |
| '[$type]: "$message" (line: $lineNumber) - [$column:$length]'; |
| |
| static Iterable<Annotation> fromErrors(AnalysisErrorInfo error) { |
| List<Annotation> annotations = []; |
| error.errors.forEach( |
| (e) => annotations.add(new Annotation.forError(e, error.lineInfo))); |
| return annotations; |
| } |
| } |
| |
| class AnnotationMatcher extends Matcher { |
| final Annotation _expected; |
| AnnotationMatcher(this._expected); |
| |
| Description describe(Description description) => |
| description.addDescriptionOf(_expected); |
| |
| bool matches(item, Map matchState) => |
| item is Annotation && _matches(item as Annotation); |
| |
| bool _matches(Annotation other) { |
| // Only test messages if they're specified in the expectation |
| if (_expected.message != null) { |
| if (_expected.message != other.message) { |
| return false; |
| } |
| } |
| // Similarly for highlighting |
| if (_expected.column != null) { |
| if (_expected.column != other.column || |
| _expected.length != other.length) { |
| return false; |
| } |
| } |
| return _expected.type == other.type && |
| _expected.lineNumber == other.lineNumber; |
| } |
| } |
| |
| class MockLinter extends LintRule { |
| VisitorCallback visitorCallback; |
| |
| MockLinter([nodeVisitor v]) : super( |
| name: 'MockLint', |
| group: Group.style, |
| description: 'Desc', |
| details: 'And so on...') { |
| visitorCallback = () => new MockVisitor(v); |
| } |
| |
| @override |
| PubspecVisitor getPubspecVisitor() => visitorCallback(); |
| |
| @override |
| AstVisitor getVisitor() => visitorCallback(); |
| } |
| |
| class MockLintRule extends LintRule { |
| MockLintRule(String name, Group group) : super(name: name, group: group); |
| |
| @override |
| AstVisitor getVisitor() => new MockVisitor(null); |
| } |
| |
| class MockVisitor extends GeneralizingAstVisitor with PubspecVisitor { |
| final nodeVisitor; |
| |
| MockVisitor(this.nodeVisitor); |
| |
| visitNode(AstNode node) { |
| if (nodeVisitor != null) { |
| nodeVisitor(node); |
| } |
| } |
| |
| visitPackageName(PSEntry node) { |
| if (nodeVisitor != null) { |
| nodeVisitor(node); |
| } |
| } |
| } |