c52a4261c0
Previously, `pkg/analyzer/test/verify_diagnostics_test.dart` and `pkg/analyzer/tool/diagnostics/generate.dart` extracted documentation from the code of the analyzer error classes. However, since this code is generated from `messages.yaml`, it's much cleaner to just extract the documentation directly from the YAML. Change-Id: I7dc3eb73abbf5d72f203abbf04e43856b60d2a89 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/215648 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com> Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
232 lines
7.2 KiB
Dart
232 lines
7.2 KiB
Dart
// Copyright (c) 2021, 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';
|
|
|
|
/// Converts the given [documentation] string into a list of
|
|
/// [ErrorCodeDocumentationPart] objects. These objects represent
|
|
/// user-publishable documentation about the given [errorCode], along with code
|
|
/// blocks illustrating when the error occurs and how to fix it.
|
|
List<ErrorCodeDocumentationPart>? parseErrorCodeDocumentation(
|
|
String errorCode, String? documentation) {
|
|
if (documentation == null) {
|
|
return null;
|
|
}
|
|
var documentationLines = documentation.split('\n');
|
|
if (documentationLines.isEmpty) {
|
|
return null;
|
|
}
|
|
var parser = _ErrorCodeDocumentationParser(errorCode, documentationLines);
|
|
parser.parse();
|
|
return parser.result;
|
|
}
|
|
|
|
/// Enum representing the different documentation sections in which an
|
|
/// [ErrorCodeDocumentationBlock] might appear.
|
|
enum BlockSection {
|
|
// The "Examples" section, where we give examples of code that generates the
|
|
// error.
|
|
examples,
|
|
|
|
// The "Common fixes" section, where we give examples of code that doesn't
|
|
// generate the error.
|
|
commonFixes,
|
|
}
|
|
|
|
/// An [ErrorCodeDocumentationPart] containing a block of code.
|
|
class ErrorCodeDocumentationBlock extends ErrorCodeDocumentationPart {
|
|
/// The code itself.
|
|
final String text;
|
|
|
|
/// The section this block is contained in.
|
|
final BlockSection containingSection;
|
|
|
|
/// A list of the experiments that need to be enabled for this code to behave
|
|
/// as expected (if any).
|
|
final List<String> experiments;
|
|
|
|
/// The file type of this code block (e.g. `dart` or `yaml`).
|
|
final String fileType;
|
|
|
|
/// The language version that must be active for this code to behave as
|
|
/// expected (if any).
|
|
final String? languageVersion;
|
|
|
|
/// If this code is an auxiliary file that supports other blocks, the URI of
|
|
/// the file.
|
|
final String? uri;
|
|
|
|
ErrorCodeDocumentationBlock(this.text,
|
|
{required this.containingSection,
|
|
this.experiments = const [],
|
|
required this.fileType,
|
|
this.languageVersion,
|
|
this.uri});
|
|
|
|
@override
|
|
String formatForDocumentation() => fileType == 'dart'
|
|
? ['{% prettify dart tag=pre+code %}', text, '{% endprettify %}']
|
|
.join('\n')
|
|
: ['```$fileType', text, '```'].join('\n');
|
|
}
|
|
|
|
/// A portion of an error code's documentation. This could be free form
|
|
/// markdown text ([ErrorCodeDocumentationText]) or a code block
|
|
/// ([ErrorCodeDocumentationBlock]).
|
|
abstract class ErrorCodeDocumentationPart {
|
|
/// Formats this documentation part as text suitable for inclusion in the
|
|
/// analyzer's `diagnostics.md` file.
|
|
String formatForDocumentation();
|
|
}
|
|
|
|
/// An [ErrorCodeDocumentationPart] containing free form markdown text.
|
|
class ErrorCodeDocumentationText extends ErrorCodeDocumentationPart {
|
|
/// The text, in markdown format.
|
|
final String text;
|
|
|
|
ErrorCodeDocumentationText(this.text);
|
|
|
|
@override
|
|
String formatForDocumentation() => text;
|
|
}
|
|
|
|
class _ErrorCodeDocumentationParser {
|
|
/// The prefix used on directive lines to specify the experiments that should
|
|
/// be enabled for a snippet.
|
|
static const String experimentsPrefix = '%experiments=';
|
|
|
|
/// The prefix used on directive lines to specify the language version for
|
|
/// the snippet.
|
|
static const String languagePrefix = '%language=';
|
|
|
|
/// The prefix used on directive lines to indicate the uri of an auxiliary
|
|
/// file that is needed for testing purposes.
|
|
static const String uriDirectivePrefix = '%uri="';
|
|
|
|
final String errorCode;
|
|
|
|
final List<String> commentLines;
|
|
|
|
final List<ErrorCodeDocumentationPart> result = [];
|
|
|
|
int currentLineNumber = 0;
|
|
|
|
String? currentSection;
|
|
|
|
_ErrorCodeDocumentationParser(this.errorCode, this.commentLines);
|
|
|
|
bool get done => currentLineNumber >= commentLines.length;
|
|
|
|
String get line => commentLines[currentLineNumber];
|
|
|
|
BlockSection computeCurrentBlockSection() {
|
|
switch (currentSection) {
|
|
case '#### Example':
|
|
case '#### Examples':
|
|
return BlockSection.examples;
|
|
case '#### Common fixes':
|
|
return BlockSection.commonFixes;
|
|
case null:
|
|
problem('Code block before section header');
|
|
default:
|
|
problem('Code block in invalid section ${json.encode(currentSection)}');
|
|
}
|
|
}
|
|
|
|
void parse() {
|
|
var textLines = <String>[];
|
|
|
|
void flushText() {
|
|
if (textLines.isNotEmpty) {
|
|
result.add(ErrorCodeDocumentationText(textLines.join('\n')));
|
|
textLines = [];
|
|
}
|
|
}
|
|
|
|
while (!done) {
|
|
if (line.startsWith('TODO')) {
|
|
// Everything after the "TODO" is ignored.
|
|
break;
|
|
} else if (line.startsWith('%')) {
|
|
problem('% directive outside code block');
|
|
} else if (line.startsWith('```')) {
|
|
flushText();
|
|
processCodeBlock();
|
|
} else {
|
|
if (line.startsWith('#') && !line.startsWith('#####')) {
|
|
currentSection = line;
|
|
}
|
|
textLines.add(line);
|
|
currentLineNumber++;
|
|
}
|
|
}
|
|
flushText();
|
|
}
|
|
|
|
Never problem(String explanation) {
|
|
throw 'In documentation for $errorCode, at line ${currentLineNumber + 1}, '
|
|
'$explanation';
|
|
}
|
|
|
|
void processCodeBlock() {
|
|
var containingSection = computeCurrentBlockSection();
|
|
var codeLines = <String>[];
|
|
String? languageVersion;
|
|
String? uri;
|
|
List<String>? experiments;
|
|
assert(line.startsWith('```'));
|
|
var fileType = line.substring(3);
|
|
if (fileType.isEmpty) {
|
|
problem('Code blocks should have a file type, e.g. "```dart"');
|
|
}
|
|
++currentLineNumber;
|
|
while (true) {
|
|
if (done) {
|
|
problem('Unterminated code block');
|
|
} else if (line.startsWith('```')) {
|
|
if (line != '```') {
|
|
problem('Code blocks should end with "```"');
|
|
}
|
|
++currentLineNumber;
|
|
result.add(ErrorCodeDocumentationBlock(codeLines.join('\n'),
|
|
containingSection: containingSection,
|
|
experiments: experiments ?? const [],
|
|
fileType: fileType,
|
|
languageVersion: languageVersion,
|
|
uri: uri));
|
|
return;
|
|
} else if (line.startsWith('%')) {
|
|
if (line.startsWith(languagePrefix)) {
|
|
if (languageVersion != null) {
|
|
problem('Multiple language version directives');
|
|
}
|
|
languageVersion = line.substring(languagePrefix.length);
|
|
} else if (line.startsWith(uriDirectivePrefix)) {
|
|
if (uri != null) {
|
|
problem('Multiple URI directives');
|
|
}
|
|
if (!line.endsWith('"')) {
|
|
problem('URI directive should be surrounded by double quotes');
|
|
}
|
|
uri = line.substring(uriDirectivePrefix.length, line.length - 1);
|
|
} else if (line.startsWith(experimentsPrefix)) {
|
|
if (experiments != null) {
|
|
problem('Multiple experiments directives');
|
|
}
|
|
experiments = line
|
|
.substring(experimentsPrefix.length)
|
|
.split(',')
|
|
.map((e) => e.trim())
|
|
.toList();
|
|
} else {
|
|
problem('Unrecognized directive ${json.encode(line)}');
|
|
}
|
|
} else {
|
|
codeLines.add(line);
|
|
}
|
|
++currentLineNumber;
|
|
}
|
|
}
|
|
}
|