analyzer: Block doc directives

In this CL, we shake things up a bit to separate the idea of a "doc
directive" and a "doc directive tag". The doc comments have been
updated to reflect the change. We have to recognize and support end-
tags, and recover when an end-tag is missing, a start-tag is missing,
or end-tags are out-of-order.

I also introduce a notion of doc directive nesting, in a way that
should not be computationally expensive, nor memory expensive.

Take this text as an example:

/// {@template foo}
/// Text.
/// {@inject-html}
/// <p>Some HTML.</p>
/// {@end-inject-html}
/// {@youtube ... }
/// {@endtemplate}

Notice the doc directives nested in the following way:

* template directive:
  * text: "Text."
  * inject-html directive:
    * text: "<p>Some HTML.</p>"
    * youtube directive

I want to avoid storing any blocks of text on the DocDirective nodes,
to avoid what could be very excessive memory usage. And if I want to
avoid storing the text, I think there is little benefit in storing the
data for these directives in a tree structure. In this CL, the data
is stored in one List, `docDirectives` on the CommentImpl:

* [0] - template directive, with data about its opening tag and
        closing tag.
* [1] - inject-html directive, with data about its opening tag and
        closing tag.
* [2] - youtube tag, with data about its singular tag.

For syntax highlighting purposes, there is no benefit to understanding
the nesting. And dartdoc currently gets all of the comment text from
the AST (or maybe from offsets in the actual text???) so I think there
is currently no downside to not capturing the nesting structure in
the CommentImpl instance.

But I can see in the future it might be better for dartdoc to consume
an API with the nesting structure, and that nesting structure does not
necessarily need to contain a copy of any text; it could contain some
sort of 'Text' data, with offsets of text contained in block doc
directives.

Change-Id: Ib58ab68fe80eea76ee7fa912d00fc69cc74f72d3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/326883
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
This commit is contained in:
Sam Rawlins 2023-09-20 21:56:50 +00:00 committed by Commit Queue
parent e28faa2462
commit b09ecc45bf
9 changed files with 776 additions and 117 deletions

View file

@ -3462,8 +3462,12 @@ WarningCode.DOC_DIRECTIVE_HAS_UNEXPECTED_NAMED_ARGUMENT:
The fix is to remove the unexpected named argument.
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE:
status: needsEvaluation
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG:
status: needsEvaluation
WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT:
status: noFix
WarningCode.DOC_DIRECTIVE_MISSING_OPENING_TAG:
status: needsEvaluation
WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS:
status: noFix
WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS:

View file

@ -8,38 +8,29 @@ library;
import 'package:analyzer/dart/ast/ast.dart';
import 'package:meta/meta.dart';
/// A documentation directive, found in a doc comment.
/// A block doc directive, denoted by an opening tag, and a closing tag.
///
/// Documentation directives are declared with `{@` at the start of a line of a
/// documentation comment, followed the name of a doc directive, arguments, and
/// finally a right curly brace (`}`).
///
/// Arguments are separated from the directive name, and from each other, by
/// whitespace. There are two types of arguments: positional and named. Named
/// arguments are written as `NAME=VALUE`, without any internal whitespace.
/// Named arguments can be optional.
/// The text in between the two tags is not explicitly called out. It can be
/// read from the original compilation unit, between the offsets of the opening
/// and closing tags.
@experimental
final class DocDirective {
/// The offset of the starting text, '@docImport'.
final int offset;
final int end;
final int nameOffset;
final int nameEnd;
final class BlockDocDirective implements DocDirective {
final DocDirectiveTag openingTag;
final DocDirectiveTag? closingTag;
final DocDirectiveType type;
BlockDocDirective(this.openingTag, this.closingTag);
final List<DocDirectiveArgument> positionalArguments;
final List<DocDirectiveNamedArgument> namedArguments;
@override
DocDirectiveType get type => openingTag.type;
}
DocDirective({
required this.offset,
required this.end,
required this.nameOffset,
required this.nameEnd,
required this.type,
required this.positionalArguments,
required this.namedArguments,
});
/// An instance of a [DocDirectiveType] in the text of a doc comment, either
/// as a [SimpleDocDirective], represented by a single [DocDirectiveTag], or a
/// [BlockDocDirective], represented by an opening [DocDirectiveTag] and a
/// closing one (in well-formed text).
@experimental
sealed class DocDirective {
DocDirectiveType get type;
}
/// An argument in a doc directive. See [DocDirective] for their syntax.
@ -88,6 +79,40 @@ final class DocDirectivePositionalArgument extends DocDirectiveArgument {
});
}
/// A documentation directive, found in a doc comment.
///
/// Documentation directives are declared with `{@` at the start of a line of a
/// documentation comment, followed the name of a doc directive, arguments, and
/// finally a right curly brace (`}`).
///
/// Arguments are separated from the directive name, and from each other, by
/// whitespace. There are two types of arguments: positional and named. Named
/// arguments are written as `NAME=VALUE`, without any internal whitespace.
/// Named arguments can be optional.
@experimental
final class DocDirectiveTag {
/// The offset of the starting text; for example: '@animation'.
final int offset;
final int end;
final int nameOffset;
final int nameEnd;
final DocDirectiveType type;
final List<DocDirectiveArgument> positionalArguments;
final List<DocDirectiveNamedArgument> namedArguments;
DocDirectiveTag({
required this.offset,
required this.end,
required this.nameOffset,
required this.nameEnd,
required this.type,
required this.positionalArguments,
required this.namedArguments,
});
}
@experimental
enum DocDirectiveType {
/// A [DocDirective] declaring an embedded video with HTML video controls.
@ -141,6 +166,20 @@ enum DocDirectiveType {
restParametersAllowed: true,
),
/// The end tag for the [DocDirectiveType.injectHtml] tag.
///
/// This tag should not really constitute a "type" of doc directive, but this
/// implementation is a one-to-one mapping of "types" and "tags", so end tags
/// are included. This also allows us to parse (erroneous) dangling end tags.
endInjectHtml.end('end-inject-html', openingTag: 'inject-html'),
/// The end tag for the [DocDirectiveType.template] tag.
///
/// This tag should not really constitute a "type" of doc directive, but this
/// implementation is a one-to-one mapping of "types" and "tags", so end tags
/// are included. This also allows us to parse (erroneous) dangling end tags.
endTemplate.end('endtemplate', openingTag: 'template'),
/// A [DocDirective] declaring an example file.
///
/// This directive has one required argument: the path. A named 'region'
@ -156,6 +195,13 @@ enum DocDirectiveType {
namedParameters: ['region', 'lang'],
),
/// A [DocDirective] declaring a block of HTML content which is to be inserted
/// after all other processing, including Markdown parsing.
///
/// See documentation at
/// https://github.com/dart-lang/dartdoc/wiki/Doc-comment-directives#injected-html.
injectHtml.block('inject-html', 'end-inject-html'),
/// A [DocDirective] declaring amacro application.
///
/// This directive has one required argument: the name. For example:
@ -178,6 +224,18 @@ enum DocDirectiveType {
restParametersAllowed: true,
),
/// A [DocDirective] declaring a template of text which can be applied to
/// other doc comments with a macro.
///
/// A template can contain any recognized doc comment content between the
/// opening and closing tags, like Markdown text, comment references, and
/// simple doc directives.
///
/// See documentation at
/// https://github.com/dart-lang/dartdoc/wiki/Doc-comment-directives#templates-and-macros.
// TODO(srawlins): Migrate users to use 'end-template'.
template.block('template', 'endtemplate', positionalParameters: ['name']),
/// A [DocDirective] declaring an embedded YouTube video.
///
/// This directive has three required arguments: the width, the height, and
@ -189,9 +247,21 @@ enum DocDirectiveType {
/// https://github.com/dart-lang/dartdoc/wiki/Doc-comment-directives#youtube-videos.
youtube('youtube', positionalParameters: ['width', 'height', 'url']);
/// Whether this starts a block directive, which must be closed by a specific
/// closing directive.
///
/// For example, the 'inject-html' directive begins with `{@inject-html}` and
/// ends with `{@end-inject-html}`.
final bool isBlock;
/// The name of the directive, as written in a doc comment.
final String name;
/// The name of the directive that ends this one, in the case of a block
/// directive's opening tag, the name of the directive that starts this one,
/// in the case of a block directive's closing tag, and `null` otherwise.
final String? opposingName;
/// The positional parameter names, which are each required.
final List<String> positionalParameters;
@ -209,7 +279,25 @@ enum DocDirectiveType {
this.positionalParameters = const <String>[],
this.namedParameters = const <String>[],
this.restParametersAllowed = false,
});
}) : isBlock = false,
opposingName = null;
const DocDirectiveType.block(
this.name,
this.opposingName, {
this.positionalParameters = const <String>[],
}) : isBlock = true,
namedParameters = const <String>[],
restParametersAllowed = false;
const DocDirectiveType.end(
this.name, {
required String openingTag,
}) : opposingName = openingTag,
isBlock = false,
positionalParameters = const <String>[],
namedParameters = const <String>[],
restParametersAllowed = false;
}
/// A documentation import, found in a doc comment.
@ -267,3 +355,13 @@ final class MdCodeBlockLine {
MdCodeBlockLine({required this.offset, required this.length});
}
@experimental
final class SimpleDocDirective implements DocDirective {
final DocDirectiveTag tag;
SimpleDocDirective(this.tag);
@override
DocDirectiveType get type => tag.type;
}

View file

@ -6081,6 +6081,15 @@ class WarningCode extends AnalyzerErrorCode {
correctionMessage: "Try closing the directive with a curly brace.",
);
/// Parameters:
/// 0: the name of the corresponding doc directive tag
static const WarningCode DOC_DIRECTIVE_MISSING_CLOSING_TAG = WarningCode(
'DOC_DIRECTIVE_MISSING_CLOSING_TAG',
"Doc directive is missing a closing tag.",
correctionMessage:
"Try closing the directive with the appropriate closing tag, '{0}'.",
);
/// Parameters:
/// 0: the name of the doc directive
/// 1: the name of the missing argument
@ -6091,6 +6100,15 @@ class WarningCode extends AnalyzerErrorCode {
uniqueName: 'DOC_DIRECTIVE_MISSING_ONE_ARGUMENT',
);
/// Parameters:
/// 0: the name of the corresponding doc directive tag
static const WarningCode DOC_DIRECTIVE_MISSING_OPENING_TAG = WarningCode(
'DOC_DIRECTIVE_MISSING_OPENING_TAG',
"Doc directive is missing an opening tag.",
correctionMessage:
"Try opening the directive with the appropriate opening tag, '{0}'.",
);
/// Parameters:
/// 0: the name of the doc directive
/// 1: the name of the first missing argument

View file

@ -17,18 +17,30 @@ class DocCommentVerifier {
// animation directive's width must be an int, a youtube directive's URL
// must be a valid YouTube URL, etc.
var positionalArgumentCount = docDirective.positionalArguments.length;
var required = docDirective.type.positionalParameters;
var requiredCount = docDirective.type.positionalParameters.length;
switch (docDirective) {
case SimpleDocDirective():
docDirectiveTag(docDirective.tag);
case BlockDocDirective(:var openingTag, :var closingTag):
docDirectiveTag(openingTag);
if (closingTag != null) {
docDirectiveTag(closingTag);
}
}
}
void docDirectiveTag(DocDirectiveTag tag) {
var positionalArgumentCount = tag.positionalArguments.length;
var required = tag.type.positionalParameters;
var requiredCount = tag.type.positionalParameters.length;
if (positionalArgumentCount < requiredCount) {
var gap = requiredCount - positionalArgumentCount;
if (gap == 1) {
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT,
docDirective.offset,
docDirective.end - docDirective.offset,
[docDirective.type.name, required.last],
tag.offset,
tag.end - tag.offset,
[tag.type.name, required.last],
);
} else if (gap == 2) {
var missingArguments = [
@ -37,9 +49,9 @@ class DocCommentVerifier {
];
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS,
docDirective.offset,
docDirective.end - docDirective.offset,
[docDirective.type.name, ...missingArguments],
tag.offset,
tag.end - tag.offset,
[tag.type.name, ...missingArguments],
);
} else if (gap == 3) {
var missingArguments = [
@ -49,37 +61,37 @@ class DocCommentVerifier {
];
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS,
docDirective.offset,
docDirective.end - docDirective.offset,
[docDirective.type.name, ...missingArguments],
tag.offset,
tag.end - tag.offset,
[tag.type.name, ...missingArguments],
);
}
}
if (docDirective.type.restParametersAllowed) {
if (tag.type.restParametersAllowed) {
// TODO(srawlins): We probably want to enforce that at least one argument
// is given, particularly for 'category' and 'subCategory'.
return;
}
if (positionalArgumentCount > requiredCount) {
var errorOffset = docDirective.positionalArguments[requiredCount].offset;
var errorLength = docDirective.positionalArguments.last.end - errorOffset;
var errorOffset = tag.positionalArguments[requiredCount].offset;
var errorLength = tag.positionalArguments.last.end - errorOffset;
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS,
errorOffset,
errorLength,
[docDirective.type.name, positionalArgumentCount, requiredCount],
[tag.type.name, positionalArgumentCount, requiredCount],
);
}
for (var namedArgument in docDirective.namedArguments) {
if (!docDirective.type.namedParameters.contains(namedArgument.name)) {
for (var namedArgument in tag.namedArguments) {
if (!tag.type.namedParameters.contains(namedArgument.name)) {
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_HAS_UNEXPECTED_NAMED_ARGUMENT,
namedArgument.offset,
namedArgument.end - namedArgument.offset,
[docDirective.type.name, namedArgument.name],
[tag.type.name, namedArgument.name],
);
}
}

View file

@ -953,7 +953,9 @@ const List<ErrorCode> errorCodeValues = [
WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS,
WarningCode.DOC_DIRECTIVE_HAS_UNEXPECTED_NAMED_ARGUMENT,
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE,
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG,
WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT,
WarningCode.DOC_DIRECTIVE_MISSING_OPENING_TAG,
WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS,
WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS,
WarningCode.DOC_IMPORT_CANNOT_BE_DEFERRED,

View file

@ -97,7 +97,7 @@ int _readWhitespace(String content, [int index = 0]) {
/// A class which temporarily stores data for a [CommentType.DOCUMENTATION]-type
/// [Comment], which is ultimately built with [build].
class DocCommentBuilder {
final class DocCommentBuilder {
final Parser _parser;
final ErrorReporter? _errorReporter;
final Uri _uri;
@ -111,6 +111,8 @@ class DocCommentBuilder {
final Token _startToken;
final _CharacterSequence _characterSequence;
final _blockDocDirectiveBuilderStack = <_BlockDocDirectiveBuilder>[];
DocCommentBuilder(
this._parser,
this._errorReporter,
@ -144,6 +146,71 @@ class DocCommentBuilder {
);
}
/// Parses a closing tag for a block doc directive, matching it with it's
/// opening tag on [_blockDirectiveTagStack], if one can be found.
///
/// The algorithm is as follows:
///
/// * Beginning at the top of the stack, and working towards the bottom,
/// identify the first opening tag that matches this closing tag.
/// * If no matching opening tag is found, report the closing tag as dangling,
/// and add it to [_docDirectives]; do not remove any tags from the stack.
/// * Otherwise, add a [BlockDocDirective] to [_docDirectives] with the
/// matched opening tag and the closing tag. Also, take each unmatched
/// opening tag above the matched opening tag, which are each missing a
/// closing tag, reporting each as such, and add each to [_docDirectives].
void _endBlockDocDirectiveTag(
_DirectiveParser parser, DocDirectiveType type) {
var closingTag = parser.directive(type);
for (var i = _blockDocDirectiveBuilderStack.length - 1; i >= 0; i--) {
var builder = _blockDocDirectiveBuilderStack[i];
if (builder.matches(closingTag)) {
builder.closingTag = closingTag;
// Add all inner doc directives.
_blockDocDirectiveBuilderStack.removeAt(i);
// First add this block directive to it's parent on the stack, then add
// the directives which it contained, as their offsets are greater than
// this one's.
var higherDirective = _blockDocDirectiveBuilderStack[i - 1];
higherDirective.push(builder.build());
for (var docDirective in builder.innerDocDirectives) {
higherDirective.push(docDirective);
}
// Remove each opening tag with no associated closing tag from the
// stack.
while (_blockDocDirectiveBuilderStack.length > i) {
var builder = _blockDocDirectiveBuilderStack.removeAt(i);
// It would not be useful to create a synthetic closing tag; just use
// `null`.
var openingTag = builder.openingTag;
if (openingTag != null) {
_errorReporter?.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG,
openingTag.offset,
openingTag.end - openingTag.offset,
[openingTag.type.opposingName!],
);
}
higherDirective.push(builder.build());
for (var docDirective in builder.innerDocDirectives) {
higherDirective.push(docDirective);
}
}
return;
}
}
// No matching opening tag was found.
_errorReporter?.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_OPENING_TAG,
closingTag.offset,
closingTag.end - closingTag.offset,
[closingTag.type.name],
);
_pushDocDirective(SimpleDocDirective(closingTag));
}
/// Determines if [content] can represent a fenced codeblock delimiter
/// (opening or closing) (starts with optional whitespace, then at least three
/// backticks).
@ -164,6 +231,12 @@ class DocCommentBuilder {
}
}
void _parseBlockDocDirectiveTag(
_DirectiveParser parser, DocDirectiveType type) {
var opening = parser.directive(type);
_blockDocDirectiveBuilderStack.add(_BlockDocDirectiveBuilder(opening));
}
/// Parses a documentation comment.
///
/// All parsed data is added to the fields on this builder.
@ -172,6 +245,7 @@ class DocCommentBuilder {
// indented code block.
var isPreviousLineEmpty = true;
var lineInfo = _characterSequence.next();
_blockDocDirectiveBuilderStack.add(_BlockDocDirectiveBuilder.placeholder());
while (lineInfo != null) {
var (:offset, :content) = lineInfo;
var whitespaceEndIndex = _readWhitespace(content);
@ -185,7 +259,7 @@ class DocCommentBuilder {
if (_parseFencedCodeBlock(content: content)) {
isPreviousLineEmpty = false;
} else if (_parseDocDirective(
} else if (_parseDocDirectiveTag(
index: whitespaceEndIndex,
content: content,
)) {
@ -200,6 +274,30 @@ class DocCommentBuilder {
}
lineInfo = _characterSequence.next();
}
// Resolve any unclosed block directives.
while (_blockDocDirectiveBuilderStack.length > 1) {
var builder = _blockDocDirectiveBuilderStack.removeLast();
// It would not be useful to create a synthetic closing tag; just use
// `null`.
var openingTag = builder.openingTag;
if (openingTag != null) {
_errorReporter?.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG,
openingTag.offset,
openingTag.end - openingTag.offset,
[openingTag.type.opposingName!],
);
}
_pushBlockDocDirectiveAndInnerDirectives(builder);
}
// Move all directives from the top block doc directive builder to the
// comment.
assert(_blockDocDirectiveBuilderStack.length == 1);
var blockDocDirectiveBuilder = _blockDocDirectiveBuilderStack.first;
assert(blockDocDirectiveBuilder.openingTag == null);
_docDirectives.addAll(blockDocDirectiveBuilder.innerDocDirectives);
}
/// Parses the comment references in [content] which starts at [offset].
@ -249,7 +347,7 @@ class DocCommentBuilder {
}
}
bool _parseDocDirective({required int index, required String content}) {
bool _parseDocDirectiveTag({required int index, required String content}) {
const openingLength = '{@'.length;
if (!content.startsWith('{@', index)) return false;
@ -284,25 +382,37 @@ class DocCommentBuilder {
switch (name) {
case 'animation':
_docDirectives.add(parser.directive(DocDirectiveType.animation));
_pushDocDirective(parser.simpleDirective(DocDirectiveType.animation));
return true;
case 'canonicalFor':
_docDirectives.add(parser.directive(DocDirectiveType.canonicalFor));
_pushDocDirective(
parser.simpleDirective(DocDirectiveType.canonicalFor));
return true;
case 'category':
_docDirectives.add(parser.directive(DocDirectiveType.category));
_pushDocDirective(parser.simpleDirective(DocDirectiveType.category));
return true;
case 'end-inject-html':
_endBlockDocDirectiveTag(parser, DocDirectiveType.endInjectHtml);
return true;
case 'endtemplate':
_endBlockDocDirectiveTag(parser, DocDirectiveType.endTemplate);
return true;
case 'example':
_docDirectives.add(parser.directive(DocDirectiveType.example));
_pushDocDirective(parser.simpleDirective(DocDirectiveType.example));
return true;
case 'inject-html':
_parseBlockDocDirectiveTag(parser, DocDirectiveType.injectHtml);
return true;
case 'subCategory':
_docDirectives.add(parser.directive(DocDirectiveType.subCategory));
_pushDocDirective(parser.simpleDirective(DocDirectiveType.subCategory));
return true;
case 'template':
_parseBlockDocDirectiveTag(parser, DocDirectiveType.template);
return true;
case 'youtube':
_docDirectives.add(parser.directive(DocDirectiveType.youtube));
_pushDocDirective(parser.simpleDirective(DocDirectiveType.youtube));
return true;
}
// TODO(srawlins): Handle block doc directives: inject-html, template.
// TODO(srawlins): Handle unknown (misspelled?) directive.
return false;
}
@ -638,6 +748,18 @@ class DocCommentBuilder {
);
}
}
void _pushBlockDocDirectiveAndInnerDirectives(
_BlockDocDirectiveBuilder builder) {
_pushDocDirective(builder.build());
for (var docDirective in builder.innerDocDirectives) {
_pushDocDirective(docDirective);
}
}
/// Push [docDirective] onto the current block doc directive.
void _pushDocDirective(DocDirective docDirective) =>
_blockDocDirectiveBuilderStack.last.push(docDirective);
}
class DocImportStringScanner extends StringScanner {
@ -697,6 +819,54 @@ class DocImportStringScanner extends StringScanner {
}
}
/// A builder for a [BlockDocDirective], which keeps track of various
/// information until the closing tag is found, or the directive is closed as
/// part of recovery.
///
/// Instances are also used as a nesting level, so that [SimpleDocDirective]s
/// and [BlockDocDirective]s are all listed in their correct order. At the top
/// level of the nesting is [_BlockDocDirectiveBuilder.placeholder], a synthetic
/// builder with a `null` [openingTag].
final class _BlockDocDirectiveBuilder {
/// The opening tag.
///
/// This is non-null for every builder except for
/// [_BlockDocDirectiveBuilder.placeholder].
final DocDirectiveTag? openingTag;
/// The closing tag, which is non-`null` for well-formed block doc directives.
DocDirectiveTag? closingTag;
/// The inner doc directives which are all declared between this block doc
/// directive's opening and closing tags, in the order they appear in the
/// comment.
final innerDocDirectives = <DocDirective>[];
_BlockDocDirectiveBuilder(this.openingTag);
factory _BlockDocDirectiveBuilder.placeholder() =>
_BlockDocDirectiveBuilder(null);
BlockDocDirective build() {
final openingTag = this.openingTag;
if (openingTag == null) {
throw StateError(
'Attempting to build a block doc directive with no opening tag.');
}
return BlockDocDirective(openingTag, closingTag);
}
/// Whether this doc directive's opening tag is the opposing tag for [tag].
bool matches(DocDirectiveTag tag) {
final openingTag = this.openingTag;
return openingTag == null
? false
: openingTag.type.opposingName == tag.type.name;
}
void push(DocDirective docDirective) => innerDocDirectives.add(docDirective);
}
/// An abstraction of the character sequences in either a single-line doc
/// comment (which consists of a series of [Token]s) or a multi-line doc comment
/// (which consists of a single [Token]).
@ -863,13 +1033,14 @@ final class _DirectiveParser {
_length = content.length,
_errorReporter = errorReporter;
DocDirective directive(DocDirectiveType type) {
/// Parses a non-block (single line) doc directive.
DocDirectiveTag directive(DocDirectiveType type) {
if (index == _length) {
_end = _offset + index;
}
var (positionalArguments, namedArguments) = _parseArguments();
_readClosingCurlyBrace();
return DocDirective(
return DocDirectiveTag(
offset: _offset,
end: _end!,
nameOffset: nameOffset,
@ -880,6 +1051,9 @@ final class _DirectiveParser {
);
}
SimpleDocDirective simpleDirective(DocDirectiveType type) =>
SimpleDocDirective(directive(type));
/// Parses and returns a positional or named doc directive argument.
DocDirectiveArgument _parseArgument() {
// An equal sign is parsed as a delimiter between a named argument's name

View file

@ -22145,6 +22145,13 @@ WarningCode:
problemMessage: "Doc directive is missing a closing curly brace ('}')."
correctionMessage: "Try closing the directive with a curly brace."
hasPublishedDocs: false
DOC_DIRECTIVE_MISSING_CLOSING_TAG:
problemMessage: "Doc directive is missing a closing tag."
correctionMessage: "Try closing the directive with the appropriate closing tag, '{0}'."
hasPublishedDocs: false
comment: |-
Parameters:
0: the name of the corresponding doc directive tag
DOC_DIRECTIVE_MISSING_ONE_ARGUMENT:
sharedName: DOC_DIRECTIVE_MISSING_ARGUMENT
problemMessage: "The '{0}' directive is missing a '{1}' argument."
@ -22154,6 +22161,13 @@ WarningCode:
Parameters:
0: the name of the doc directive
1: the name of the missing argument
DOC_DIRECTIVE_MISSING_OPENING_TAG:
problemMessage: "Doc directive is missing an opening tag."
correctionMessage: "Try opening the directive with the appropriate opening tag, '{0}'."
hasPublishedDocs: false
comment: |-
Parameters:
0: the name of the corresponding doc directive tag
DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS:
sharedName: DOC_DIRECTIVE_MISSING_ARGUMENT
problemMessage: "The '{0}' directive is missing a '{1}', a '{2}', and a '{3}' argument."

View file

@ -33,15 +33,16 @@ Comment
/// Text.
/// {@animation 600 400 http://google.com arg=}
docDirectives
DocDirective
offset: [26, 70]
type: [DocDirectiveType.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=
SimpleDocDirective
tag
offset: [26, 70]
type: [DocDirectiveType.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=
''');
}
@ -64,15 +65,16 @@ Comment
/// Text.
/// {@animation 600 400 http://google.com arg=value
docDirectives
DocDirective
offset: [26, 74]
type: [DocDirectiveType.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=value
SimpleDocDirective
tag
offset: [26, 74]
type: [DocDirectiveType.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=value
''');
}
@ -95,15 +97,16 @@ Comment
/// Text.
/// {@animation 600 400 http://google.com arg=
docDirectives
DocDirective
offset: [26, 69]
type: [DocDirectiveType.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=
SimpleDocDirective
tag
offset: [26, 69]
type: [DocDirectiveType.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=
''');
}
@ -691,6 +694,34 @@ Comment
''');
}
test_endTemplate_missingOpeningTag() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@endtemplate}
/// More text.
class A {}
''');
parseResult.assertErrors([
error(WarningCode.DOC_DIRECTIVE_MISSING_OPENING_TAG, 26, 15),
]);
final node = parseResult.findNode.comment('endtemplate');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@endtemplate}
/// More text.
docDirectives
SimpleDocDirective
tag
offset: [26, 41]
type: [DocDirectiveType.endTemplate]
''');
}
test_fencedCodeBlock_blockComment() {
final parseResult = parseStringWithErrors(r'''
/**
@ -1377,6 +1408,291 @@ Comment
''');
}
test_template() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@template name}
/// More text.
/// {@endtemplate}
class A {}
''');
parseResult.assertNoErrors();
final node = parseResult.findNode.comment('template name');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@template name}
/// More text.
/// {@endtemplate}
docDirectives
BlockDocDirective
openingTag
offset: [26, 43]
type: [DocDirectiveType.template]
positionalArguments
name
closingTag
offset: [62, 77]
type: [DocDirectiveType.endTemplate]
''');
}
test_template_containingInnerTags() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@template name}
/// More text.
/// {@example path}
/// {@endtemplate}
class A {}
''');
parseResult.assertNoErrors();
final node = parseResult.findNode.comment('template name');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@template name}
/// More text.
/// {@example path}
/// {@endtemplate}
docDirectives
BlockDocDirective
openingTag
offset: [26, 43]
type: [DocDirectiveType.template]
positionalArguments
name
closingTag
offset: [82, 97]
type: [DocDirectiveType.endTemplate]
SimpleDocDirective
tag
offset: [62, 78]
type: [DocDirectiveType.example]
positionalArguments
path
''');
}
test_template_containingInnerTemplate() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@template name}
/// More text.
/// {@template name2}
/// Text three.
/// {@endtemplate}
/// Text four.
/// {@endtemplate}
class A {}
''');
parseResult.assertNoErrors();
final node = parseResult.findNode.comment('template name2');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@template name}
/// More text.
/// {@template name2}
/// Text three.
/// {@endtemplate}
/// Text four.
/// {@endtemplate}
docDirectives
BlockDocDirective
openingTag
offset: [26, 43]
type: [DocDirectiveType.template]
positionalArguments
name
closingTag
offset: [134, 149]
type: [DocDirectiveType.endTemplate]
BlockDocDirective
openingTag
offset: [62, 80]
type: [DocDirectiveType.template]
positionalArguments
name2
closingTag
offset: [100, 115]
type: [DocDirectiveType.endTemplate]
''');
}
test_template_missingClosingTag() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@template name}
/// More text.
class A {}
''');
parseResult.assertErrors([
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG, 26, 17),
]);
final node = parseResult.findNode.comment('template name');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@template name}
/// More text.
docDirectives
BlockDocDirective
openingTag
offset: [26, 43]
type: [DocDirectiveType.template]
positionalArguments
name
''');
}
test_template_missingClosingTag_multiple() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@template name}
/// More text.
/// {@template name2}
/// More text.
class A {}
''');
parseResult.assertErrors([
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG, 26, 17),
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG, 62, 18),
]);
final node = parseResult.findNode.comment('template name2');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@template name}
/// More text.
/// {@template name2}
/// More text.
docDirectives
BlockDocDirective
openingTag
offset: [26, 43]
type: [DocDirectiveType.template]
positionalArguments
name
BlockDocDirective
openingTag
offset: [62, 80]
type: [DocDirectiveType.template]
positionalArguments
name2
''');
}
test_template_missingClosingTag_withInnerTag() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@template name}
/// More text.
/// {@animation 600 400 http://google.com}
class A {}
''');
parseResult.assertErrors([
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG, 26, 17),
]);
final node = parseResult.findNode.comment('template name');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@template name}
/// More text.
/// {@animation 600 400 http://google.com}
docDirectives
BlockDocDirective
openingTag
offset: [26, 43]
type: [DocDirectiveType.template]
positionalArguments
name
SimpleDocDirective
tag
offset: [62, 101]
type: [DocDirectiveType.animation]
positionalArguments
600
400
http://google.com
''');
}
test_template_outOfOrderClosingTag() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@template name}
/// More text.
/// {@inject-html}
/// HTML.
/// {@endtemplate}
/// {@end-inject-html}
class A {}
''');
parseResult.assertErrors([
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_TAG, 62, 15),
error(WarningCode.DOC_DIRECTIVE_MISSING_OPENING_TAG, 110, 19),
]);
final node = parseResult.findNode.comment('template name');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@template name}
/// More text.
/// {@inject-html}
/// HTML.
/// {@endtemplate}
/// {@end-inject-html}
docDirectives
BlockDocDirective
openingTag
offset: [26, 43]
type: [DocDirectiveType.template]
positionalArguments
name
closingTag
offset: [91, 106]
type: [DocDirectiveType.endTemplate]
BlockDocDirective
openingTag
offset: [62, 77]
type: [DocDirectiveType.injectHtml]
SimpleDocDirective
tag
offset: [110, 129]
type: [DocDirectiveType.endInjectHtml]
''');
}
test_youTubeDirective() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
@ -1394,13 +1710,14 @@ Comment
/// Text.
/// {@youtube 600 400 http://google.com}
docDirectives
DocDirective
offset: [26, 63]
type: [DocDirectiveType.youtube]
positionalArguments
600
400
http://google.com
SimpleDocDirective
tag
offset: [26, 63]
type: [DocDirectiveType.youtube]
positionalArguments
600
400
http://google.com
''');
}
@ -1419,13 +1736,14 @@ Comment
tokens
/// {@youtube 600 400 http://google.com
docDirectives
DocDirective
offset: [4, 40]
type: [DocDirectiveType.youtube]
positionalArguments
600
400
http://google.com
SimpleDocDirective
tag
offset: [4, 40]
type: [DocDirectiveType.youtube]
positionalArguments
600
400
http://google.com
''');
}
@ -1442,12 +1760,13 @@ Comment
tokens
/// {@youtube 600 400}
docDirectives
DocDirective
offset: [4, 23]
type: [DocDirectiveType.youtube]
positionalArguments
600
400
SimpleDocDirective
tag
offset: [4, 23]
type: [DocDirectiveType.youtube]
positionalArguments
600
400
''');
}
@ -1464,11 +1783,12 @@ Comment
tokens
/// {@youtube 600}
docDirectives
DocDirective
offset: [4, 19]
type: [DocDirectiveType.youtube]
positionalArguments
600
SimpleDocDirective
tag
offset: [4, 19]
type: [DocDirectiveType.youtube]
positionalArguments
600
''');
}
@ -1485,9 +1805,10 @@ Comment
tokens
/// {@youtube }
docDirectives
DocDirective
offset: [4, 16]
type: [DocDirectiveType.youtube]
SimpleDocDirective
tag
offset: [4, 16]
type: [DocDirectiveType.youtube]
''');
}
}

View file

@ -274,7 +274,24 @@ class ResolvedAstPrinter extends ThrowingAstVisitor<void> {
_sink.writelnWithIndent('docDirectives');
_sink.withIndent(() {
for (var docDirective in node.docDirectives) {
_writeDocDirective(docDirective);
switch (docDirective) {
case SimpleDocDirective():
_sink.writelnWithIndent('SimpleDocDirective');
_sink.withIndent(() {
_sink.writelnWithIndent('tag');
_writeDocDirectiveTag(docDirective.tag);
});
case BlockDocDirective(:var openingTag, :var closingTag):
_sink.writelnWithIndent('BlockDocDirective');
_sink.withIndent(() {
_sink.writelnWithIndent('openingTag');
_writeDocDirectiveTag(openingTag);
if (closingTag != null) {
_sink.writelnWithIndent('closingTag');
_writeDocDirectiveTag(closingTag);
}
});
}
}
});
}
@ -1707,8 +1724,7 @@ Expected parent: (${parent.runtimeType}) $parent
}
}
void _writeDocDirective(DocDirective docDirective) {
_sink.writelnWithIndent('DocDirective');
void _writeDocDirectiveTag(DocDirectiveTag docDirective) {
_sink.withIndent(() {
_sink.writelnWithIndent(
'offset: [${docDirective.offset}, ${docDirective.end}]');