| // 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; | |
| /// A list of the diagnostics that should be ignored for this code block. | |
| final List<String> ignores; | |
| /// 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, | |
| required this.ignores, | |
| this.languageVersion, | |
| this.uri, | |
| }); | |
| } | |
| /// A portion of an error code's documentation. This could be free form | |
| /// markdown text ([ErrorCodeDocumentationText]) or a code block | |
| /// ([ErrorCodeDocumentationBlock]). | |
| abstract class ErrorCodeDocumentationPart {} | |
| /// An [ErrorCodeDocumentationPart] containing free form markdown text. | |
| class ErrorCodeDocumentationText extends ErrorCodeDocumentationPart { | |
| /// The text, in markdown format. | |
| final String text; | |
| ErrorCodeDocumentationText(this.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 a list of diagnostics that | |
| /// should be ignored. These should only be diagnostics that shouldn't be | |
| /// reported. If they are valid diagnostics, then add an `ignore` comment | |
| /// in the code instead. | |
| static const String ignorePrefix = '%ignore='; | |
| /// 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: | |
| return null; | |
| } | |
| } | |
| 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; | |
| List<String>? ignores; | |
| assert(line.startsWith('```')); | |
| var fileType = line.substring(3); | |
| if (fileType.isEmpty && containingSection != null) { | |
| 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; | |
| if (containingSection != null) { | |
| // Ignore code blocks where they're allowed but aren't checked. | |
| result.add( | |
| ErrorCodeDocumentationBlock( | |
| codeLines.join('\n'), | |
| containingSection: containingSection, | |
| experiments: experiments ?? const [], | |
| fileType: fileType, | |
| ignores: ignores ?? const [], | |
| 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(ignorePrefix)) { | |
| if (ignores != null) { | |
| problem('Multiple ignore directives'); | |
| } | |
| ignores = line | |
| .substring(ignorePrefix.length) | |
| .split(',') | |
| .map((e) => e.trim()) | |
| .toList(); | |
| } 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; | |
| } | |
| } | |
| } |