blob: 49c9e76c8ebefdb4a522930d5e6397c888be28c7 [file] [log] [blame]
// Copyright (c) 2019, 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:convert';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer_testing/package_root.dart' as package_root;
import 'package:test/test.dart';
main() async {
SnippetTester tester = SnippetTester();
await tester.verify();
}
class SnippetTester {
final OverlayResourceProvider provider;
final Folder docFolder;
final String snippetDirPath;
final String snippetPath;
final AnalysisContextCollection collection;
final StringBuffer output = StringBuffer();
factory SnippetTester() {
var provider = OverlayResourceProvider(PhysicalResourceProvider.INSTANCE);
var packageRoot = provider.pathContext.normalize(package_root.packageRoot);
var analyzerPath = provider.pathContext.join(packageRoot, 'analyzer');
var docPath = provider.pathContext.join(analyzerPath, 'doc');
var docFolder = provider.getFolder(docPath);
var snippetDirPath = provider.pathContext.join(
analyzerPath,
'test',
'snippets',
);
var snippetPath = provider.pathContext.join(snippetDirPath, 'snippet.dart');
return SnippetTester._(provider, docFolder, snippetDirPath, snippetPath);
}
SnippetTester._(
this.provider,
this.docFolder,
this.snippetDirPath,
this.snippetPath,
) : collection = AnalysisContextCollection(
resourceProvider: provider,
includedPaths: [snippetPath],
);
/// Return `true` if the given error is a diagnostic produced by a lint that
/// is allowed to occur in documentation.
bool isAllowedLint(Diagnostic diagnostic) {
var errorCode = diagnostic.diagnosticCode;
return errorCode is LintCode &&
errorCode.name == 'non_constant_identifier_names' &&
diagnostic.message.contains("'test_");
}
Future<void> verify() async {
await verifyFolder(docFolder);
}
Future<void> verifyFile(File file) async {
if (file.path.endsWith(
'/pkg/analyzer/doc/element_model_migration_guide.md',
) ||
file.path.endsWith(
r'\pkg\analyzer\doc\element_model_migration_guide.md',
)) {
return;
}
String content = file.readAsStringSync();
List<String> lines = const LineSplitter().convert(content);
List<String> codeLines = [];
bool inCode = false;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (line == '```dart') {
if (inCode) {
// TODO(brianwilkerson): Report this.
}
inCode = true;
} else if (line == '```') {
if (!inCode) {
// TODO(brianwilkerson): Report this.
}
await verifySnippet(file, codeLines.join('\n'));
codeLines.clear();
inCode = false;
} else if (inCode) {
codeLines.add(line);
}
}
}
Future<void> verifyFolder(Folder folder) async {
for (Resource child in folder.getChildren()) {
if (child is File) {
if (child.shortName.endsWith('.md')) {
test('Verify docs in ${child.path}', () async {
output.clear();
await verifyFile(child);
if (output.isNotEmpty) {
fail(output.toString());
}
}, timeout: Timeout.factor(4));
}
} else if (child is Folder) {
await verifyFolder(child);
}
}
}
Future<void> verifySnippet(File file, String snippet) async {
// TODO(brianwilkerson): When the files outside of 'src' contain only public
// API, write code to compute the list of imports so that new public API
// will automatically be allowed.
const String imports = '''
import 'dart:math' as math;
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/session.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/visitor2.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
Future<void> assertNoErrorsInCode(String s) async {}
void test(String s, void Function() f) {}
void group(String s, void Function() f) {}
''';
provider.setOverlay(
snippetPath,
content:
'''
$imports
$snippet
''',
modificationStamp: 1,
);
try {
List<AnalysisContext> contexts = collection.contexts;
if (contexts.length != 1) {
fail('The snippets directory contains multiple analysis contexts.');
}
var context = contexts[0];
// Mark the snippet as changed since we reuse the same path
// for each snippet found.
context.changeFile(snippetPath);
await context.applyPendingFileChanges();
var results = await context.currentSession.getErrors(snippetPath);
if (results is ErrorsResult) {
Iterable<Diagnostic> diagnostics = results.diagnostics.where((error) {
DiagnosticCode diagnosticCode = error.diagnosticCode;
// TODO(brianwilkerson): .
return diagnosticCode != WarningCode.unusedImport &&
diagnosticCode != WarningCode.unusedLocalVariable &&
!isAllowedLint(error);
});
if (diagnostics.isNotEmpty) {
String filePath = provider.pathContext.relative(
file.path,
from: docFolder.path,
);
if (output.isNotEmpty) {
output.writeln();
}
output.writeln('Errors in snippet in "$filePath":');
output.writeln();
output.writeln(snippet);
output.writeln();
int importsLength = imports.length + 1; // account for the '\n'.
for (var error in diagnostics) {
writeError(error, importsLength);
}
}
}
} catch (exception, stackTrace) {
if (output.isNotEmpty) {
output.writeln();
}
output.writeln('Exception while analyzing "$snippet"');
output.writeln();
output.writeln(exception);
output.writeln(stackTrace);
} finally {
provider.removeOverlay(snippetPath);
}
}
void writeError(Diagnostic diagnostic, int prefixLength) {
output.write(diagnostic.diagnosticCode);
output.write(' (');
output.write(diagnostic.offset - prefixLength);
output.write(', ');
output.write(diagnostic.length);
output.write(') ');
output.writeln(diagnostic.message);
}
}