// 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/diagnostic/diagnostic.dart' as diag; 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.lowerCaseName == 'non_constant_identifier_names' && diagnostic.message.contains("'test_"); } Future verify() async { await verifyFolder(docFolder); } Future 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 lines = const LineSplitter().convert(content); List 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 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 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 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 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 diagnostics = results.diagnostics.where((error) { DiagnosticCode diagnosticCode = error.diagnosticCode; // TODO(brianwilkerson): . return diagnosticCode != diag.unusedImport && diagnosticCode != diag.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); } }