blob: 7f51a0de8f207b8bbde5a8ec9512f09b4bbed219 [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:io';
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/token.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:path/src/context.dart';
import '../../test/utils/package_root.dart' as package_root;
/// Generate the file `diagnostics.md` based on the documentation associated
/// with the declarations of the error codes.
void main() async {
IOSink sink = File(computeOutputPath()).openWrite();
DocumentationGenerator generator = DocumentationGenerator(computeCodePaths());
generator.writeDocumentation(sink);
await sink.flush();
await sink.close();
}
/// Compute a list of the code paths for the files containing diagnostics that
/// have been documented.
List<CodePath> computeCodePaths() {
Context pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
String packageRoot = pathContext.normalize(package_root.packageRoot);
String analyzerPath = pathContext.join(packageRoot, 'analyzer');
return CodePath.from([
[analyzerPath, 'lib', 'src', 'dart', 'error', 'hint_codes.dart'],
[analyzerPath, 'lib', 'src', 'dart', 'error', 'syntactic_errors.dart'],
[analyzerPath, 'lib', 'src', 'error', 'codes.dart'],
], [
null,
[analyzerPath, 'lib', 'src', 'dart', 'error', 'syntactic_errors.g.dart'],
null,
]);
}
/// Compute the path to the file into which documentation is being generated.
String computeOutputPath() {
Context pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
String packageRoot = pathContext.normalize(package_root.packageRoot);
String analyzerPath = pathContext.join(packageRoot, 'analyzer');
return pathContext.join(
analyzerPath, 'tool', 'diagnostics', 'diagnostics.md');
}
/// A representation of the paths to the documentation and declaration of a set
/// of diagnostic codes.
class CodePath {
/// The path to the file containing the declarations of the diagnostic codes
/// that might have documentation associated with them.
final String documentationPath;
/// The path to the file containing the generated definition of the diagnostic
/// codes that include the message, or `null` if the
final String declarationPath;
/// Initialize a newly created code path from the [documentationPath] and
/// [declarationPath].
CodePath(this.documentationPath, this.declarationPath);
/// Return a list of code paths computed by joining the path segments in the
/// corresponding lists from [documentationPaths] and [declarationPaths].
static List<CodePath> from(List<List<String>> documentationPaths,
List<List<String>> declarationPaths) {
Context pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
List<CodePath> paths = [];
for (int i = 0; i < documentationPaths.length; i++) {
String docPath = pathContext.joinAll(documentationPaths[i]);
String declPath = null;
if (declarationPaths[i] != null) {
declPath = pathContext.joinAll(declarationPaths[i]);
}
paths.add(CodePath(docPath, declPath));
}
return paths;
}
}
/// A class used to generate diagnostic documentation.
class DocumentationGenerator {
/// The absolute paths of the files containing the declarations of the error
/// codes.
final List<CodePath> codePaths;
/// A map from the name of a diagnostic code to the lines of the documentation
/// for that code.
Map<String, List<String>> docsByCode = {};
/// Initialize a newly created documentation generator.
DocumentationGenerator(this.codePaths) {
_extractAllDocs();
}
/// Write the documentation to the file at the given [outputPath].
void writeDocumentation(StringSink sink) {
_writeHeader(sink);
_writeGlossary(sink);
_writeDiagnostics(sink);
}
/// Return a version of the [text] in which characters that have special
/// meaning in markdown have been escaped.
String _escape(String text) {
return text.replaceAll('_', '\\_');
}
/// Extract documentation from all of the files containing the definitions of
/// diagnostics.
void _extractAllDocs() {
List<String> includedPaths = [];
for (CodePath codePath in codePaths) {
includedPaths.add(codePath.documentationPath);
if (codePath.declarationPath != null) {
includedPaths.add(codePath.declarationPath);
}
}
AnalysisContextCollection collection = new AnalysisContextCollection(
includedPaths: includedPaths,
resourceProvider: PhysicalResourceProvider.INSTANCE);
for (CodePath codePath in codePaths) {
String docPath = codePath.documentationPath;
String declPath = codePath.declarationPath;
if (declPath == null) {
_extractDocs(_parse(collection, docPath), null);
} else {
File file = File(declPath);
if (file.existsSync()) {
_extractDocs(
_parse(collection, docPath), _parse(collection, declPath));
} else {
_extractDocs(_parse(collection, docPath), null);
}
}
}
}
/// Extract documentation from the given [field] declaration.
List<String> _extractDoc(FieldDeclaration field) {
Token comments = field.firstTokenAfterCommentAndMetadata.precedingComments;
if (comments == null) {
return null;
}
List<String> docs = [];
bool inDartCodeBlock = false;
while (comments != null) {
String lexeme = comments.lexeme;
if (lexeme.startsWith('// TODO')) {
break;
} else if (lexeme.startsWith('// %')) {
// Ignore lines containing directives for testing support.
} else if (lexeme.startsWith('// ')) {
String trimmedLine = lexeme.substring(3);
if (trimmedLine == '```dart') {
inDartCodeBlock = true;
docs.add('{% prettify dart %}');
} else if (trimmedLine == '```') {
if (inDartCodeBlock) {
docs.add('{% endprettify %}');
inDartCodeBlock = false;
} else {
docs.add(trimmedLine);
}
} else {
docs.add(trimmedLine);
}
} else if (lexeme == '//') {
docs.add('');
}
comments = comments.next;
}
if (docs.isEmpty) {
return null;
}
return docs;
}
/// Extract documentation from the file that was parsed to produce the given
/// [result]. If a [generatedResult] is provided, then the messages might be
/// in the file parsed to produce the result.
void _extractDocs(ParsedUnitResult result, ParsedUnitResult generatedResult) {
CompilationUnit unit = result.unit;
for (CompilationUnitMember declaration in unit.declarations) {
if (declaration is ClassDeclaration) {
for (ClassMember member in declaration.members) {
if (member is FieldDeclaration) {
List<String> docs = _extractDoc(member);
if (docs != null) {
VariableDeclaration variable = member.fields.variables[0];
String variableName = variable.name.name;
if (docsByCode.containsKey(variableName)) {
throw StateError('Duplicate diagnostic code');
}
String message =
_extractMessage(variable.initializer, generatedResult);
docs = [
'### ${variableName.toLowerCase()}',
'',
..._split('_${_escape(message)}_'),
'',
...docs,
];
docsByCode[variableName] = docs;
}
}
}
}
}
}
/// Extract the message from the [expression]. If the expression is the name
/// of a generated code, then the [generatedResult] should have the unit in
/// which the message can be found.
String _extractMessage(
Expression expression, ParsedUnitResult generatedResult) {
if (expression is InstanceCreationExpression) {
return (expression.argumentList.arguments[1] as StringLiteral)
.stringValue;
} else if (expression is SimpleIdentifier && generatedResult != null) {
VariableDeclaration variable =
_findVariable(expression.name, generatedResult.unit);
if (variable != null) {
return _extractMessage(variable.initializer, null);
}
}
throw StateError(
'Cannot extract a message from a ${expression.runtimeType}');
}
/// Return the declaration of the top-level variable with the [name] in the
/// compilation unit, or `null` if there is no such variable.
VariableDeclaration _findVariable(String name, CompilationUnit unit) {
for (CompilationUnitMember member in unit.declarations) {
if (member is TopLevelVariableDeclaration) {
for (VariableDeclaration variable in member.variables.variables) {
if (variable.name.name == name) {
return variable;
}
}
}
}
return null;
}
/// Use the analysis context [collection] to parse the file at the given
/// [path] and return the result.
ParsedUnitResult _parse(AnalysisContextCollection collection, String path) {
AnalysisSession session = collection.contextFor(path).currentSession;
if (session == null) {
throw new StateError('No session for "$path"');
}
ParsedUnitResult result = session.getParsedUnit(path);
if (result.state != ResultState.VALID) {
throw new StateError('Unable to parse "$path"');
}
return result;
}
/// Split the [message] into multiple lines, each of which is less than 80
/// characters long.
List<String> _split(String message) {
// This uses a brute force approach because we don't expect to have messages
// that need to be split more than once.
int length = message.length;
if (length <= 80) {
return [message];
}
int endIndex = message.lastIndexOf(' ', 80);
if (endIndex < 0) {
return [message];
}
return [message.substring(0, endIndex), message.substring(endIndex + 1)];
}
/// Write the documentation for all of the diagnostics.
void _writeDiagnostics(StringSink sink) {
sink.write('''
## Diagnostics
The analyzer produces the following diagnostics for code that
doesn't conform to the language specification or
that might work in unexpected ways.
''');
List<String> errorCodes = docsByCode.keys.toList();
errorCodes.sort();
for (String errorCode in errorCodes) {
List<String> docs = docsByCode[errorCode];
sink.writeln();
for (String line in docs) {
sink.writeln(line);
}
}
}
/// Write the glossary.
void _writeGlossary(StringSink sink) {
sink.write('''
## Glossary
This page uses the following terms.
### Constant context
A _constant context_ is a region of code in which it isn't necessary to include
the `const` keyword because it's implied by the fact that everything in that
region is required to be a constant. The following locations are constant
contexts:
* Everything inside a list, map or set literal that's prefixed by the
`const` keyword. Example:
```dart
var l = const [/*constant context*/];
```
* The arguments inside an invocation of a constant constructor. Example:
```dart
var p = const Point(/*constant context*/);
```
* The initializer for a variable that's prefixed by the `const` keyword.
Example:
```dart
const v = /*constant context*/;
```
* Annotations
* The expression in a case clause. Example:
```dart
void f(int e) {
switch (e) {
case /*constant context*/:
break;
}
}
```
''');
//### Potentially non-nullable
//
//A type is _potentially non-nullable_ if it's either explicitly non-nullable or
//if it's a type parameter. The latter case is included because the actual runtime
//type might be non-nullable.
}
/// Write the header of the file.
void _writeHeader(StringSink sink) {
sink.write('''
---
title: Diagnostics
description: Details for diagnostics produced by the Dart analyzer.
---
{%- comment %}
WARNING: Do NOT EDIT this file directly. It is autogenerated by the script in
`pkg/analyzer/tool/diagnostics/generate.dart` in the sdk repository.
{% endcomment -%}
This page lists diagnostic messages produced by the Dart analyzer,
with details about what those messages mean and how you can fix your code.
For more information about the analyzer, see
[Customizing static analysis](/guides/language/analysis-options).
''');
}
}