analyzer: Support animation doc directive; refactor classes

Work towards https://github.com/dart-lang/sdk/issues/52705

Change-Id: Idc77bf866beace85b40b30d464ff4c7c62e19735
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/325360
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Sam Rawlins 2023-09-12 19:40:38 +00:00 committed by Commit Queue
parent ba70bed261
commit cdf016c6ab
17 changed files with 899 additions and 377 deletions

View file

@ -3450,18 +3450,20 @@ WarningCode.DEPRECATED_MIXIN_FUNCTION:
The fix is to remove `Function` from where it's referenced.
WarningCode.DEPRECATED_NEW_IN_COMMENT_REFERENCE:
status: hasFix
WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS:
status: needsEvaluation
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE:
status: needsEvaluation
WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT:
status: noFix
WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS:
status: noFix
WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS:
status: noFix
WarningCode.DOC_IMPORT_CANNOT_BE_DEFERRED:
status: needsFix
WarningCode.DOC_IMPORT_CANNOT_HAVE_CONFIGURATIONS:
status: needsFix
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_HEIGHT:
status: needsEvaluation
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_URL:
status: needsEvaluation
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_WIDTH:
status: needsEvaluation
WarningCode.DUPLICATE_EXPORT:
status: needsFix
notes: |-

View file

@ -13,19 +13,97 @@ import 'package:meta/meta.dart';
/// 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
sealed class DocDirective {
final class DocDirective {
/// The offset of the starting text, '@docImport'.
final int offset;
final int end;
final int nameOffset;
final int nameEnd;
final DocDirectiveName name;
final List<DocDirectiveArgument> positionalArguments;
final List<DocDirectiveNamedArgument> namedArguments;
DocDirective({
required this.offset,
required this.end,
required this.nameOffset,
required this.nameEnd,
required this.name,
required this.positionalArguments,
required this.namedArguments,
});
}
/// An argument in a doc directive. See [DocDirective] for their syntax.
@experimental
sealed class DocDirectiveArgument {
/// The offset of the start of the argument, from the beginning of the
/// compilation unit.
final int offset;
/// The offset just after the end of the argument, from the beginning of the
/// compilation unit.
final int end;
/// The value of the argument.
final String value;
DocDirectiveArgument({
required this.offset,
required this.end,
required this.value,
});
}
enum DocDirectiveName {
/// The name of a [DocDirective] declaring an embedded video with HTML video
/// controls.
///
/// This directive has three required arguments: the width, the height, and
/// the URL. A named 'id' argument can also be given. For example:
///
/// `{@animation 600 400 https://www.example.com/example.mp4 id=video1}`
animation,
/// The name of a [DocDirective] declaring an embedded YouTube video.
///
/// This directive has three required arguments: the width, the height, and
/// the URL. For example:
///
/// `{@youtube 600 400 https://www.youtube.com/watch?v=abc123}`
youtube;
}
/// A named argument in a doc directive. See [DocDirective] for their syntax.
@experimental
final class DocDirectiveNamedArgument extends DocDirectiveArgument {
/// The name of the argument.
final String name;
DocDirectiveNamedArgument({
required super.offset,
required super.end,
required this.name,
required super.value,
});
}
/// A positional argument in a doc directive. See [DocDirective] for their
/// syntax.
@experimental
final class DocDirectivePositionalArgument extends DocDirectiveArgument {
DocDirectivePositionalArgument({
required super.offset,
required super.end,
required super.value,
});
}
@ -84,32 +162,3 @@ final class MdCodeBlockLine {
MdCodeBlockLine({required this.offset, required this.length});
}
/// A [DocDirective] declaring an embedded YouTube video.
///
/// This directive has three required arguments: the width, the height, and the
/// URL. For example:
///
/// `{@youtube 600 400 https://www.youtube.com/watch?v=abc123}`
@experimental
final class YouTubeDocDirective extends DocDirective {
final int? widthOffset;
final int? widthEnd;
final int? heightOffset;
final int? heightEnd;
final int? urlOffset;
final int? urlEnd;
YouTubeDocDirective({
required super.offset,
required super.end,
required super.nameOffset,
required super.nameEnd,
required this.widthOffset,
required this.widthEnd,
required this.heightOffset,
required this.heightEnd,
required this.urlOffset,
required this.urlEnd,
});
}

View file

@ -6055,12 +6055,56 @@ class WarningCode extends AnalyzerErrorCode {
hasPublishedDocs: true,
);
/// Parameters:
/// 0: the actual number of arguments
/// 1: the expected number of arguments
static const WarningCode DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS = WarningCode(
'DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS',
"Doc directive has '{0}' arguments, but only '{1}' are expected.",
correctionMessage: "Try removing the extra arguments.",
);
static const WarningCode DOC_DIRECTIVE_MISSING_CLOSING_BRACE = WarningCode(
'DOC_DIRECTIVE_MISSING_CLOSING_BRACE',
"Doc directive is missing a closing curly brace ('}').",
correctionMessage: "Try closing the directive with a curly brace.",
);
/// Parameters:
/// 0: the name of the doc directive
/// 1: the name of the missing argument
static const WarningCode DOC_DIRECTIVE_MISSING_ONE_ARGUMENT = WarningCode(
'DOC_DIRECTIVE_MISSING_ARGUMENT',
"The '{0}' directive is missing a '{1}' argument.",
correctionMessage: "Try adding a '{1}' argument before the closing '}'.",
uniqueName: 'DOC_DIRECTIVE_MISSING_ONE_ARGUMENT',
);
/// Parameters:
/// 0: the name of the doc directive
/// 1: the name of the first missing argument
/// 2: the name of the second missing argument
/// 3: the name of the third missing argument
static const WarningCode DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS = WarningCode(
'DOC_DIRECTIVE_MISSING_ARGUMENT',
"The '{0}' directive is missing a '{1}', a '{2}', and a '{3}' argument.",
correctionMessage:
"Try adding the missing arguments before the closing '}'.",
uniqueName: 'DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS',
);
/// Parameters:
/// 0: the name of the doc directive
/// 1: the name of the first missing argument
/// 2: the name of the second missing argument
static const WarningCode DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS = WarningCode(
'DOC_DIRECTIVE_MISSING_ARGUMENT',
"The '{0}' directive is missing a '{1}' and a '{2}' argument.",
correctionMessage:
"Try adding the missing arguments before the closing '}'.",
uniqueName: 'DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS',
);
static const WarningCode DOC_IMPORT_CANNOT_BE_DEFERRED = WarningCode(
'DOC_IMPORT_CANNOT_BE_DEFERRED',
"Doc imports can't be deferred.",
@ -6073,24 +6117,6 @@ class WarningCode extends AnalyzerErrorCode {
correctionMessage: "Try removing the configurations.",
);
static const WarningCode DOC_YOUTUBE_DIRECTIVE_MISSING_HEIGHT = WarningCode(
'DOC_YOUTUBE_DIRECTIVE_MISSING_HEIGHT',
"YouTube directive is missing a height argument.",
correctionMessage: "Try adding a height argument after the width.",
);
static const WarningCode DOC_YOUTUBE_DIRECTIVE_MISSING_URL = WarningCode(
'DOC_YOUTUBE_DIRECTIVE_MISSING_URL',
"YouTube directive is missing a URL argument.",
correctionMessage: "Try adding a URL after the width and height.",
);
static const WarningCode DOC_YOUTUBE_DIRECTIVE_MISSING_WIDTH = WarningCode(
'DOC_YOUTUBE_DIRECTIVE_MISSING_WIDTH',
"YouTube directive is missing a width argument.",
correctionMessage: "Try adding a width argument after '@youtube'.",
);
/// Duplicate exports.
///
/// No parameters.

View file

@ -13,45 +13,106 @@ class DocCommentVerifier {
DocCommentVerifier(this._errorReporter);
void docDirective(DocDirective docDirective) {
switch (docDirective) {
case YouTubeDocDirective():
var widthOffset = docDirective.widthOffset;
var widthEnd = docDirective.widthEnd;
if (widthOffset == null || widthEnd == null) {
var positionalArgumentCount = docDirective.positionalArguments.length;
switch (docDirective.name) {
case DocDirectiveName.animation:
if (positionalArgumentCount == 0) {
_errorReporter.reportErrorForOffset(
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_WIDTH,
WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS,
docDirective.offset,
docDirective.end - docDirective.offset,
['animation', 'width', 'height', 'url'],
);
return;
} else {
// TODO: Validate width.
}
var heightOffset = docDirective.heightOffset;
var heightEnd = docDirective.heightEnd;
if (heightOffset == null || heightEnd == null) {
if (positionalArgumentCount == 1) {
_errorReporter.reportErrorForOffset(
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_HEIGHT,
WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS,
docDirective.offset,
docDirective.end - docDirective.offset,
['animation', 'width', 'height'],
);
return;
} else {
// TODO(srawlins): Validate height.
}
var urlOffset = docDirective.urlOffset;
var urlEnd = docDirective.urlEnd;
if (urlOffset == null || urlEnd == null) {
if (positionalArgumentCount == 2) {
_errorReporter.reportErrorForOffset(
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_URL,
WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT,
docDirective.offset,
docDirective.end - docDirective.offset,
['animation', 'url'],
);
} else {
// TODO(srawlins): Validate URL.
}
if (positionalArgumentCount > 3) {
var errorOffset = docDirective.positionalArguments[3].offset;
var errorLength =
docDirective.positionalArguments.last.end - errorOffset;
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS,
errorOffset,
errorLength,
[positionalArgumentCount, 3],
);
}
// TODO(srawlins): Validate `@animation` named arguments (and report
// unknown).
case DocDirectiveName.youtube:
if (positionalArgumentCount == 0) {
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS,
docDirective.offset,
docDirective.end - docDirective.offset,
['youtube', 'width', 'height', 'url'],
);
return;
} else {
// TODO: Validate width.
}
if (positionalArgumentCount == 1) {
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS,
docDirective.offset,
docDirective.end - docDirective.offset,
['youtube', 'width', 'height'],
);
return;
} else {
// TODO(srawlins): Validate height.
}
if (positionalArgumentCount == 2) {
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT,
docDirective.offset,
docDirective.end - docDirective.offset,
['youtube', 'url'],
);
} else {
// TODO(srawlins): Validate URL.
}
if (positionalArgumentCount > 3) {
var errorOffset = docDirective.positionalArguments[3].offset;
var errorLength =
docDirective.positionalArguments.last.end - errorOffset;
_errorReporter.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS,
errorOffset,
errorLength,
[positionalArgumentCount, 3],
);
}
}
}

View file

@ -950,12 +950,13 @@ const List<ErrorCode> errorCodeValues = [
WarningCode.DEPRECATED_IMPLEMENTS_FUNCTION,
WarningCode.DEPRECATED_MIXIN_FUNCTION,
WarningCode.DEPRECATED_NEW_IN_COMMENT_REFERENCE,
WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS,
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE,
WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT,
WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS,
WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS,
WarningCode.DOC_IMPORT_CANNOT_BE_DEFERRED,
WarningCode.DOC_IMPORT_CANNOT_HAVE_CONFIGURATIONS,
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_HEIGHT,
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_URL,
WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_WIDTH,
WarningCode.DUPLICATE_EXPORT,
WarningCode.DUPLICATE_HIDDEN_NAME,
WarningCode.DUPLICATE_IGNORE,

View file

@ -46,6 +46,8 @@ int _findCommentReferenceEnd(String comment, int index, int end) {
return index;
}
bool _isEqualSign(int character) => character == 0x3D /* '=' */;
/// Given that we have just found bracketed text within the given [comment],
/// looks to see whether that text is (a) followed by a parenthesized link
/// address, (b) followed by a colon, or (c) followed by optional whitespace
@ -77,6 +79,22 @@ bool _isLinkText(String comment, int rightIndex) {
return ch == 0x5B;
}
bool _isRightCurlyBrace(int character) => character == 0x7D /* '}' */;
/// Reads past any opening whitespace in [content], returning the index after
/// the last whitespace character.
int _readWhitespace(String content, [int index = 0]) {
var length = content.length;
if (index >= length) return index;
while (isWhitespace(content.codeUnitAt(index))) {
index++;
if (index >= length) {
return index;
}
}
return index;
}
/// A class which temporarily stores data for a [CommentType.DOCUMENTATION]-type
/// [Comment], which is ultimately built with [build].
class DocCommentBuilder {
@ -146,8 +164,6 @@ class DocCommentBuilder {
}
}
bool _isRightCurlyBrace(int character) => character == 0x7D /* '}' */;
/// Parses a documentation comment.
///
/// All parsed data is added to the fields on this builder.
@ -255,17 +271,26 @@ class DocCommentBuilder {
index = _readWhitespace(content, index);
var name = content.substring(nameIndex, nameEnd);
if (name == 'youtube') {
_parseYouTubeDirective(
offset: startOffset,
nameOffset: _characterSequence._offset + nameIndex,
nameEnd: _characterSequence._offset + nameEnd,
index: index,
content: content,
);
var parser = _DirectiveParser._(
offset: startOffset,
contentOffset: _characterSequence._offset,
nameOffset: _characterSequence._offset + nameIndex,
nameEnd: _characterSequence._offset + nameEnd,
content: content,
index: index,
errorReporter: _errorReporter,
);
if (name == 'animation') {
_docDirectives.add(parser.animationDirective());
return true;
}
// TODO(srawlins): Handle other doc directives: animation, api?,
if (name == 'youtube') {
_docDirectives.add(parser.youTubeDirective());
return true;
}
// TODO(srawlins): Handle other doc directives: api?,
// canonicalFor?, category, example, image, macro, samples?, subCategory.
// TODO(srawlins): Handle block doc directives: inject-html, template.
// TODO(srawlins): Handle unknown (misspelled?) directive.
@ -603,115 +628,6 @@ class DocCommentBuilder {
);
}
}
/// Parses a YouTube doc directive, returning whether one was successfully
/// parsed.
void _parseYouTubeDirective({
required int offset,
required int nameOffset,
required int nameEnd,
required String content,
required int index,
}) {
var contentOffset = _characterSequence._offset;
var length = content.length;
int? end;
if (index == length) {
end = offset + index;
}
(int?, int?) parseArgument() {
int? argumentOffset;
int? argumentEnd;
if (end == null) {
if (_isRightCurlyBrace(content.codeUnitAt(index))) {
index++;
end = offset + index;
} else {
argumentOffset = contentOffset + index;
index = _readDirectiveArgument(content, index);
argumentEnd = contentOffset + index;
index = _readWhitespace(content, index);
}
}
if (end == null && index == length) {
// We've hit EOL without closing brace.
end = offset + index;
_errorReporter?.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE,
index - 1,
1,
);
}
return (argumentOffset, argumentEnd);
}
var (widthOffset, widthEnd) = parseArgument();
var (heightOffset, heightEnd) = parseArgument();
var (urlOffset, urlEnd) = parseArgument();
if (end == null) {
// Read until the closing delimiter, `}` is found.
if (index >= length) {
// Reached EOL without finding a `}`.
end = offset + length;
}
while (!_isRightCurlyBrace(content.codeUnitAt(index))) {
index++;
if (index >= length) {
// Reached EOL without finding a `}`.
// TODO(srawlins): Minimal recovery and error-reporting for extra
// arguments.
return;
}
}
index++;
end = offset + index;
}
_docDirectives.add(YouTubeDocDirective(
offset: offset,
end: end!,
nameOffset: nameOffset,
nameEnd: nameEnd,
widthOffset: widthOffset,
widthEnd: widthEnd,
heightOffset: heightOffset,
heightEnd: heightEnd,
urlOffset: urlOffset,
urlEnd: urlEnd,
));
}
/// Reads past any directive argument text in [content], returning the index
/// after the last character.
///
/// A directive argument is a sequence of non-whitespace,
/// non-right-curly-brace characters.
int _readDirectiveArgument(String content, int index) {
var length = content.length;
//if (index >= length) return index;
while (index < length) {
var character = content.codeUnitAt(index);
if (isWhitespace(character)) return index;
if (_isRightCurlyBrace(character)) return index;
index++;
}
return index;
}
/// Reads past any opening whitespace in [content], returning the index after
/// the last whitespace character.
int _readWhitespace(String content, [int index = 0]) {
var length = content.length;
if (index >= length) return index;
while (isWhitespace(content.codeUnitAt(index))) {
index++;
if (index >= length) {
return index;
}
}
return index;
}
}
class DocImportStringScanner extends StringScanner {
@ -892,6 +808,229 @@ class _CharacterSequenceFromSingleLineComment implements _CharacterSequence {
}
}
final class _DirectiveParser {
/// The offset in the compilation unit at which [content] is found.
final int _offset;
/// The offset of the opening `{@` of this directive.
final int _contentOffset;
/// The offset in the compilation unit at which this directive's name is
/// found.
final int nameOffset;
/// The offset in the compilation unit immediately after the end of this
/// directive's name.
final int nameEnd;
/// The content of the doc comment.
final String content;
/// The length of [content].
final int _length;
final ErrorReporter? _errorReporter;
/// The current position in [content].
int index;
/// The index immediately after the end of this directve.
///
/// This is the index immediately following the `}` if there is one, or
/// the index after the end of the last character in [content], if not.
int? _end;
_DirectiveParser._({
required int offset,
required int contentOffset,
required this.nameOffset,
required this.nameEnd,
required this.content,
required this.index,
required ErrorReporter? errorReporter,
}) : _contentOffset = contentOffset,
_offset = offset,
_length = content.length,
_errorReporter = errorReporter;
/// Parses an animation doc directive.
DocDirective animationDirective() {
if (index == _length) {
_end = _offset + index;
}
var (positionalArguments, namedArguments) = _parseArguments();
_readClosingCurlyBrace();
return DocDirective(
offset: _offset,
end: _end!,
nameOffset: nameOffset,
nameEnd: nameEnd,
name: DocDirectiveName.animation,
positionalArguments: positionalArguments,
namedArguments: namedArguments,
);
}
/// Parses a YouTube doc directive.
DocDirective youTubeDirective() {
if (index == _length) {
_end = _offset + index;
}
var (positionalArguments, namedArguments) = _parseArguments();
_readClosingCurlyBrace();
return DocDirective(
offset: _offset,
end: _end!,
nameOffset: nameOffset,
nameEnd: nameEnd,
name: DocDirectiveName.youtube,
positionalArguments: positionalArguments,
namedArguments: namedArguments,
);
}
/// 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
// and value only if the name consists only of alphanumeric characters.
// If other characters have been read, then the equal sign is treated just
// as another character in the argument, allowing for unquoted arguments
// like YouTube URLs.
// TODO(srawlins): This rule is rather arbitrary; it would probably be
// better to require positional-arguments-with-equal-signs to be surrounded
// with quotes or something similar.
var onlyLettersOrDigits = true;
var argumentStart = index;
while (index < _length) {
var character = content.codeUnitAt(index);
if (isWhitespace(character)) break;
if (_isRightCurlyBrace(character)) break;
if (_isEqualSign(character) && onlyLettersOrDigits) {
// This is a valid named argument name/value delimiter.
var argumentName = content.substring(argumentStart, index);
index++;
if (index == _length) {
// Equal sign is followed by EOL.
return DocDirectiveNamedArgument(
offset: _contentOffset + argumentStart,
end: _contentOffset + index,
name: argumentName,
// Recover an unterminated named argument as having an empty
// argument value.
value: '',
);
}
var argumentValueStart = index;
while (index < _length) {
var character = content.codeUnitAt(index);
if (isWhitespace(character)) break;
if (_isRightCurlyBrace(character)) break;
index++;
}
return DocDirectiveNamedArgument(
offset: _contentOffset + argumentStart,
end: _contentOffset + index,
name: argumentName,
value: content.substring(argumentValueStart, index),
);
}
if (!isLetterOrDigit(character)) {
onlyLettersOrDigits = false;
}
index++;
}
var argumentValue = content.substring(argumentStart, index);
return DocDirectivePositionalArgument(
offset: _contentOffset + argumentStart,
end: _contentOffset + index,
value: argumentValue,
);
}
/// Parses both positional and named doc directive arguments until either a
/// closing curly brace or EOL are reached.
///
/// Returns a record containing the list of positional arguments and a list of
/// named arguments.
///
/// Reports a warning if EOL is reached before a closing curly brace is found.
(List<DocDirectivePositionalArgument>, List<DocDirectiveNamedArgument>)
_parseArguments() {
if (_end != null) return (const [], const []);
var positionalArguments = <DocDirectivePositionalArgument>[];
var namedArguments = <DocDirectiveNamedArgument>[];
while (index < _length) {
if (_isRightCurlyBrace(content.codeUnitAt(index))) {
index++;
_end = _offset + index;
return (positionalArguments, namedArguments);
}
var argument = _parseArgument();
// Remove when https://github.com/dart-lang/linter/issues/4361 is closed.
// ignore: unnecessary_parenthesis
(switch (argument) {
DocDirectivePositionalArgument() => positionalArguments.add(argument),
DocDirectiveNamedArgument() => namedArguments.add(argument),
});
index = _readWhitespace(content, index);
}
// We've hit EOL without closing brace.
_end = _offset + index;
_errorReporter?.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE,
_offset + index - 1,
1,
);
return (positionalArguments, namedArguments);
}
/// Reads the closing curly brace (`}`) expected at the end of a doc
/// directive.
///
/// Reports any extra, unexpected arguments that are found.
///
/// Reports a warning if EOL is reached before a closing curly brace is found.
void _readClosingCurlyBrace() {
if (_end != null) {
return;
}
if (index >= _length) {
// No extra arguments or closing brace.
_end = _offset + _length;
return;
}
if (_isRightCurlyBrace(content.codeUnitAt(index))) {
index++;
_end = _offset + index;
return;
}
var extraArgumentsOffset = _offset + index;
while (!_isRightCurlyBrace(content.codeUnitAt(index))) {
index++;
if (index == _length) {
// Found extra arguments and no closing brace.
_errorReporter?.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE,
_offset + index - 1,
1,
);
break;
}
}
var errorLength = _offset + index - extraArgumentsOffset;
_errorReporter?.reportErrorForOffset(
WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS,
extraArgumentsOffset,
errorLength,
);
_end = _offset + index;
}
}
/// A canonicalized store of fenced code block info strings.
///
/// Across many doc comments with many fenced code blocks, there are likely

View file

@ -22124,21 +22124,48 @@ WarningCode:
problemMessage: "Doc imports can't have configurations."
correctionMessage: Try removing the configurations.
hasPublishedDocs: false
DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS:
problemMessage: "Doc directive has '{0}' arguments, but only '{1}' are expected."
correctionMessage: Try removing the extra arguments.
hasPublishedDocs: false
comment: |-
Parameters:
0: the actual number of arguments
1: the expected number of arguments
DOC_DIRECTIVE_MISSING_CLOSING_BRACE:
problemMessage: "Doc directive is missing a closing curly brace ('}')."
correctionMessage: "Try closing the directive with a curly brace."
hasPublishedDocs: false
DOC_YOUTUBE_DIRECTIVE_MISSING_HEIGHT:
problemMessage: YouTube directive is missing a height argument.
correctionMessage: Try adding a height argument after the width.
DOC_YOUTUBE_DIRECTIVE_MISSING_URL:
problemMessage: YouTube directive is missing a URL argument.
correctionMessage: Try adding a URL after the width and height.
DOC_DIRECTIVE_MISSING_ONE_ARGUMENT:
sharedName: DOC_DIRECTIVE_MISSING_ARGUMENT
problemMessage: "The '{0}' directive is missing a '{1}' argument."
correctionMessage: "Try adding a '{1}' argument before the closing '}'."
hasPublishedDocs: false
DOC_YOUTUBE_DIRECTIVE_MISSING_WIDTH:
problemMessage: YouTube directive is missing a width argument.
correctionMessage: "Try adding a width argument after '@youtube'."
comment: |-
Parameters:
0: the name of the doc directive
1: the name of the missing argument
DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS:
sharedName: DOC_DIRECTIVE_MISSING_ARGUMENT
problemMessage: "The '{0}' directive is missing a '{1}', a '{2}', and a '{3}' argument."
correctionMessage: "Try adding the missing arguments before the closing '}'."
hasPublishedDocs: false
comment: |-
Parameters:
0: the name of the doc directive
1: the name of the first missing argument
2: the name of the second missing argument
3: the name of the third missing argument
DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS:
sharedName: DOC_DIRECTIVE_MISSING_ARGUMENT
problemMessage: "The '{0}' directive is missing a '{1}' and a '{2}' argument."
correctionMessage: "Try adding the missing arguments before the closing '}'."
hasPublishedDocs: false
comment: |-
Parameters:
0: the name of the doc directive
1: the name of the first missing argument
2: the name of the second missing argument
DUPLICATE_EXPORT:
problemMessage: Duplicate export.
correctionMessage: Try removing all but one export of the library.

View file

@ -16,6 +16,97 @@ main() {
@reflectiveTest
class DocCommentParserTest extends ParserDiagnosticsTest {
test_animationDirective_namedArgument_blankValue() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@animation 600 400 http://google.com arg=}
class A {}
''');
parseResult.assertNoErrors();
final node = parseResult.findNode.comment('animation');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@animation 600 400 http://google.com arg=}
docDirectives
DocDirective
offset: [26, 70]
name: [DocDirectiveName.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=
''');
}
test_animationDirective_namedArgument_missingClosingBrace() {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@animation 600 400 http://google.com arg=value
class A {}
''');
parseResult.assertErrors([
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE, 73, 1),
]);
final node = parseResult.findNode.comment('animation');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@animation 600 400 http://google.com arg=value
docDirectives
DocDirective
offset: [26, 74]
name: [DocDirectiveName.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=value
''');
}
test_animationDirective_namedArgument_missingValueAndClosingBrace() async {
final parseResult = parseStringWithErrors(r'''
int x = 0;
/// Text.
/// {@animation 600 400 http://google.com arg=
class A {}
''');
parseResult.assertErrors([
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE, 68, 1),
]);
final node = parseResult.findNode.comment('animation');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// {@animation 600 400 http://google.com arg=
docDirectives
DocDirective
offset: [26, 69]
name: [DocDirectiveName.animation]
positionalArguments
600
400
http://google.com
namedArguments
arg=
''');
}
test_codeSpan() {
final parseResult = parseStringWithErrors(r'''
/// `a[i]` and [b].
@ -1303,12 +1394,13 @@ Comment
/// Text.
/// {@youtube 600 400 http://google.com}
docDirectives
YouTubeDocDirective
DocDirective
offset: [26, 63]
name: [28, 35]
width: [36, 39]
height: [40, 43]
url: [44, 61]
name: [DocDirectiveName.youtube]
positionalArguments
600
400
http://google.com
''');
}
@ -1318,7 +1410,7 @@ Comment
class A {}
''');
parseResult.assertErrors([
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE, 35, 1),
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE, 39, 1),
]);
final node = parseResult.findNode.comment('youtube');
@ -1327,12 +1419,13 @@ Comment
tokens
/// {@youtube 600 400 http://google.com
docDirectives
YouTubeDocDirective
DocDirective
offset: [4, 40]
name: [6, 13]
width: [14, 17]
height: [18, 21]
url: [22, 39]
name: [DocDirectiveName.youtube]
positionalArguments
600
400
http://google.com
''');
}
@ -1349,12 +1442,12 @@ Comment
tokens
/// {@youtube 600 400}
docDirectives
YouTubeDocDirective
DocDirective
offset: [4, 23]
name: [6, 13]
width: [14, 17]
height: [18, 21]
url: [null, null]
name: [DocDirectiveName.youtube]
positionalArguments
600
400
''');
}
@ -1371,12 +1464,11 @@ Comment
tokens
/// {@youtube 600}
docDirectives
YouTubeDocDirective
DocDirective
offset: [4, 19]
name: [6, 13]
width: [14, 17]
height: [null, null]
url: [null, null]
name: [DocDirectiveName.youtube]
positionalArguments
600
''');
}
@ -1393,12 +1485,9 @@ Comment
tokens
/// {@youtube }
docDirectives
YouTubeDocDirective
DocDirective
offset: [4, 16]
name: [6, 13]
width: [null, null]
height: [null, null]
url: [null, null]
name: [DocDirectiveName.youtube]
''');
}
}

View file

@ -0,0 +1,65 @@
// Copyright (c) 2023, 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 'package:analyzer/src/error/codes.g.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../dart/resolution/context_collection_resolution.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocDirectiveHasExtraArgumentsTest);
});
}
@reflectiveTest
class DocDirectiveHasExtraArgumentsTest extends PubPackageResolutionTest {
test_animation_hasExtraArgument() async {
await assertErrorsInCode('''
/// {@animation 600 400 http://google.com foo}
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS, 42, 3),
]);
}
test_animation_noExtraArguments() async {
await assertNoErrorsInCode('''
/// {@animation 600 400 http://google.com}
class C {}
''');
}
test_animation_optionalNamedArgument() async {
await assertNoErrorsInCode('''
/// {@animation 600 400 http://google.com id=my-id}
class C {}
''');
}
test_youtube_hasExtraArgument() async {
await assertErrorsInCode('''
/// {@youtube 600 400 http://google.com foo}
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS, 40, 3),
]);
}
test_youtube_hasExtraArgument_trailingWhitespace() async {
await assertErrorsInCode('''
/// {@youtube 600 400 http://google.com foo }
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_HAS_EXTRA_ARGUMENTS, 40, 3),
]);
}
test_youtube_noExtraArguments() async {
await assertNoErrorsInCode('''
/// {@youtube 600 400 http://google.com}
class C {}
''');
}
}

View file

@ -0,0 +1,57 @@
// Copyright (c) 2023, 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 'package:analyzer/src/error/codes.g.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../dart/resolution/context_collection_resolution.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocDirectiveMissingOneArgumentTest);
});
}
@reflectiveTest
class DocDirectiveMissingOneArgumentTest extends PubPackageResolutionTest {
test_animation_hasOptionalIdParameter() async {
await assertNoErrorsInCode('''
/// {@animation 600 400 http://google.com id=my-id}
class C {}
''');
}
test_animation_hasThreeArguments() async {
await assertNoErrorsInCode('''
/// {@animation 600 400 http://google.com}
class C {}
''');
}
test_youtube_hasThreeArguments() async {
await assertNoErrorsInCode('''
/// {@youtube 600 400 http://google.com}
class C {}
''');
}
test_youtube_missingUrl() async {
await assertErrorsInCode('''
/// {@youtube 600 400}
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT, 4, 19),
]);
}
test_youtube_missingUrl_andCurlyBrace() async {
await assertErrorsInCode('''
/// {@youtube 600 400
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_MISSING_ONE_ARGUMENT, 4, 18),
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE, 21, 1),
]);
}
}

View file

@ -0,0 +1,58 @@
// Copyright (c) 2023, 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 'package:analyzer/src/error/codes.g.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../dart/resolution/context_collection_resolution.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocDirectiveMissingThreeArgumentsTest);
});
}
@reflectiveTest
class DocDirectiveMissingThreeArgumentsTest extends PubPackageResolutionTest {
test_animation_hasWidth() async {
await assertNoErrorsInCode('''
/// {@animation 600 400 http://google.com}
class C {}
''');
}
test_animation_missingWidth() async {
await assertErrorsInCode('''
/// {@animation}
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS, 4, 13),
]);
}
test_youtube_hasWidth() async {
await assertNoErrorsInCode('''
/// {@youtube 600 400 http://google.com}
class C {}
''');
}
test_youtube_missingWidth() async {
await assertErrorsInCode('''
/// {@youtube}
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS, 4, 11),
]);
}
test_youtube_missingWidth_andCurlyBrace() async {
await assertErrorsInCode('''
/// {@youtube
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_MISSING_THREE_ARGUMENTS, 4, 10),
]);
}
}

View file

@ -0,0 +1,66 @@
// Copyright (c) 2023, 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 'package:analyzer/src/error/codes.g.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../dart/resolution/context_collection_resolution.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocDirectiveMissingTwoArgumentsTest);
});
}
@reflectiveTest
class DocDirectiveMissingTwoArgumentsTest extends PubPackageResolutionTest {
test_animation_hasOptionalIdParameter() async {
await assertNoErrorsInCode('''
/// {@animation 600 400 http://google.com id=my-id}
class C {}
''');
}
test_animation_hasThreeArguments() async {
await assertNoErrorsInCode('''
/// {@animation 600 400 http://google.com}
class C {}
''');
}
test_animation_missingHeight() async {
await assertErrorsInCode('''
/// {@animation 600}
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS, 4, 17),
]);
}
test_youtube_hasThreeArguments() async {
await assertNoErrorsInCode('''
/// {@youtube 600 400 http://google.com}
class C {}
''');
}
test_youtube_missingHeight() async {
await assertErrorsInCode('''
/// {@youtube 600}
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS, 4, 15),
]);
}
test_youtube_missingHeight_andCurlyBrace() async {
await assertErrorsInCode('''
/// {@youtube 600
class C {}
''', [
error(WarningCode.DOC_DIRECTIVE_MISSING_TWO_ARGUMENTS, 4, 14),
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE, 17, 1),
]);
}
}

View file

@ -1,43 +0,0 @@
// Copyright (c) 2023, 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 'package:analyzer/src/error/codes.g.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../dart/resolution/context_collection_resolution.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocYouTubeDirectiveMissingHeightTest);
});
}
@reflectiveTest
class DocYouTubeDirectiveMissingHeightTest extends PubPackageResolutionTest {
test_hasHeight() async {
await assertNoErrorsInCode('''
/// {@youtube 600 400 http://google.com}
class C {}
''');
}
test_missingHeight() async {
await assertErrorsInCode('''
/// {@youtube 600}
class C {}
''', [
error(WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_HEIGHT, 4, 15),
]);
}
test_missingHeight_andCurlyBrace() async {
await assertErrorsInCode('''
/// {@youtube 600
class C {}
''', [
error(WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_HEIGHT, 4, 14),
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE, 13, 1),
]);
}
}

View file

@ -1,43 +0,0 @@
// Copyright (c) 2023, 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 'package:analyzer/src/error/codes.g.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../dart/resolution/context_collection_resolution.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocYouTubeDirectiveMissingUrlTest);
});
}
@reflectiveTest
class DocYouTubeDirectiveMissingUrlTest extends PubPackageResolutionTest {
test_hasUrl() async {
await assertNoErrorsInCode('''
/// {@youtube 600 400 http://google.com}
class C {}
''');
}
test_missingUrl() async {
await assertErrorsInCode('''
/// {@youtube 600 400}
class C {}
''', [
error(WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_URL, 4, 19),
]);
}
test_missingUrl_andCurlyBrace() async {
await assertErrorsInCode('''
/// {@youtube 600 400
class C {}
''', [
error(WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_URL, 4, 18),
error(WarningCode.DOC_DIRECTIVE_MISSING_CLOSING_BRACE, 17, 1),
]);
}
}

View file

@ -1,42 +0,0 @@
// Copyright (c) 2023, 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 'package:analyzer/src/error/codes.g.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../dart/resolution/context_collection_resolution.dart';
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocYouTubeDirectiveMissingWidthTest);
});
}
@reflectiveTest
class DocYouTubeDirectiveMissingWidthTest extends PubPackageResolutionTest {
test_hasWidth() async {
await assertNoErrorsInCode('''
/// {@youtube 600 400 http://google.com}
class C {}
''');
}
test_missingWidth() async {
await assertErrorsInCode('''
/// {@youtube}
class C {}
''', [
error(WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_WIDTH, 4, 11),
]);
}
test_missingWidth_andCurlyBrace() async {
await assertErrorsInCode('''
/// {@youtube
class C {}
''', [
error(WarningCode.DOC_YOUTUBE_DIRECTIVE_MISSING_WIDTH, 4, 10),
]);
}
}

View file

@ -167,16 +167,18 @@ import 'deprecated_implements_function_test.dart'
import 'deprecated_member_use_test.dart' as deprecated_member_use;
import 'deprecated_mixin_function_test.dart' as deprecated_mixin_function;
import 'division_optimization_test.dart' as division_optimization;
import 'doc_directive_has_extra_arguments_test.dart'
as doc_directive_has_extra_arguments;
import 'doc_directive_missing_one_argument_test.dart'
as doc_directive_missing_one_argument;
import 'doc_directive_missing_three_arguments_test.dart'
as doc_directive_missing_three_arguments;
import 'doc_directive_missing_two_arguments_test.dart'
as doc_directive_missing_two_arguments;
import 'doc_import_cannot_be_deferred_test.dart'
as doc_import_cannot_be_deferred;
import 'doc_import_cannot_have_configurations_test.dart'
as doc_import_cannot_have_configurations;
import 'doc_youtube_directive_missing_height_test.dart'
as doc_youtube_directive_missing_height;
import 'doc_youtube_directive_missing_url_test.dart'
as doc_youtube_directive_missing_url;
import 'doc_youtube_directive_missing_width_test.dart'
as doc_youtube_directive_missing_width;
import 'duplicate_augmentation_import_test.dart'
as duplicate_augmentation_import;
import 'duplicate_constructor_default_test.dart'
@ -1022,11 +1024,12 @@ main() {
deprecated_member_use.main();
deprecated_mixin_function.main();
division_optimization.main();
doc_directive_has_extra_arguments.main();
doc_directive_missing_one_argument.main();
doc_directive_missing_three_arguments.main();
doc_directive_missing_two_arguments.main();
doc_import_cannot_be_deferred.main();
doc_import_cannot_have_configurations.main();
doc_youtube_directive_missing_height.main();
doc_youtube_directive_missing_url.main();
doc_youtube_directive_missing_width.main();
duplicate_augmentation_import.main();
duplicate_constructor_default.main();
duplicate_constructor_name.main();

View file

@ -1708,22 +1708,29 @@ Expected parent: (${parent.runtimeType}) $parent
}
void _writeDocDirective(DocDirective docDirective) {
switch (docDirective) {
case YouTubeDocDirective():
_sink.writelnWithIndent('YouTubeDocDirective');
_sink.writelnWithIndent('DocDirective');
_sink.withIndent(() {
_sink.writelnWithIndent(
'offset: [${docDirective.offset}, ${docDirective.end}]');
_sink.writelnWithIndent('name: [${docDirective.name}]');
if (docDirective.positionalArguments.isNotEmpty) {
_sink.writelnWithIndent('positionalArguments');
_sink.withIndent(() {
_sink.writelnWithIndent(
'offset: [${docDirective.offset}, ${docDirective.end}]');
_sink.writelnWithIndent(
'name: [${docDirective.nameOffset}, ${docDirective.nameEnd}]');
_sink.writelnWithIndent(
'width: [${docDirective.widthOffset}, ${docDirective.widthEnd}]');
_sink.writelnWithIndent(
'height: [${docDirective.heightOffset}, ${docDirective.heightEnd}]');
_sink.writelnWithIndent(
'url: [${docDirective.urlOffset}, ${docDirective.urlEnd}]');
for (var argument in docDirective.positionalArguments) {
_sink.writelnWithIndent(argument.value);
}
});
}
}
if (docDirective.namedArguments.isNotEmpty) {
_sink.writelnWithIndent('namedArguments');
_sink.withIndent(() {
for (var argument in docDirective.namedArguments) {
_sink.writeWithIndent(argument.name);
_sink.writeln('=${argument.value}');
}
});
}
});
}
void _writeDocImport(DocImport docImport) {