// 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? 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 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, }); } /// 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 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 commentLines; final List 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 = []; 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? languageVersion; String? uri; List? experiments; 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, 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; } } }