analyzer: Add indented code blocks to Comment

I decided to unify the indented code block and markdown code block
classes, as there will be no functional difference between the two.


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

Change-Id: I6cd60f7a58a83e6a67ad187b5ff25868b6f84651
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/321785
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
This commit is contained in:
Sam Rawlins 2023-08-21 04:43:49 +00:00 committed by Samuel Rawlins
parent cfa03ed971
commit 2e9d983b15
5 changed files with 246 additions and 148 deletions

View file

@ -3225,9 +3225,10 @@ sealed class CombinatorImpl extends AstNodeImpl implements Combinator {
/// '/ **' (CHARACTER | [CommentReference])* '&#42;/'
/// | ('///' (CHARACTER - EOL)* EOL)+
abstract final class Comment implements AstNode {
/// The fenced code blocks parsed in this comment.
/// The Markdown code blocks (both fenced and indented) parsed in this
/// comment.
@experimental
List<MdFencedCodeBlock> get fencedCodeBlocks;
List<MdCodeBlock> get codeBlocks;
/// Return `true` if this is a block comment.
bool get isBlock;
@ -3275,7 +3276,7 @@ final class CommentImpl extends AstNodeImpl implements Comment {
final NodeListImpl<CommentReferenceImpl> _references = NodeListImpl._();
@override
final List<MdFencedCodeBlock> fencedCodeBlocks;
final List<MdCodeBlock> codeBlocks;
/// Initialize a newly created comment. The list of [tokens] must contain at
/// least one token. The [_type] is the type of the comment. The list of
@ -3285,7 +3286,7 @@ final class CommentImpl extends AstNodeImpl implements Comment {
required this.tokens,
required CommentType type,
required List<CommentReferenceImpl> references,
required this.fencedCodeBlocks,
required this.codeBlocks,
}) : _type = type {
_references._initialize(this, references);
}
@ -11952,41 +11953,43 @@ final class MapPatternImpl extends DartPatternImpl implements MapPattern {
/// A Markdown fenced code block found in a documentation comment.
@experimental
final class MdFencedCodeBlock {
final class MdCodeBlock {
/// The 'info string'.
///
/// This includes any text following the opening backticks. For example, in
/// a fenced code block starting with "```dart", the info string is "dart".
/// This includes any text (trimming whitespace) following the opening
/// backticks (for a fenced code block). For example, in a fenced code block
/// starting with "```dart", the info string is "dart".
///
/// If no text follows the opening backticks, the info string is `null`.
/// If the code block is an indented code block, or a fenced code block with
/// no text following the opening backticks, the info string is `null`.
///
/// See CommonMark specification at
/// <https://spec.commonmark.org/0.30/#fenced-code-blocks>.
final String? infoString;
/// Information about the comment lines that make up this code block.
final List<MdFencedCodeBlockLine> lines;
///
/// For a fenced code block, these lines include the opening and closing
/// fence delimiter lines.
final List<MdCodeBlockLine> lines;
MdFencedCodeBlock({
MdCodeBlock({
required this.infoString,
required List<MdFencedCodeBlockLine> lines,
required List<MdCodeBlockLine> lines,
}) : lines = List.of(lines, growable: false);
}
/// A Markdown fenced code block line found in a documentation comment.
/// A Markdown code block line found in a documentation comment.
@experimental
final class MdFencedCodeBlockLine {
/// The offset of the start of the fenced code block, from the beginning of
final class MdCodeBlockLine {
/// The offset of the start of the code block, from the beginning of the
/// compilation unit.
final int offset;
/// The length of the fenced code block.
final int length;
MdFencedCodeBlockLine({
required this.offset,
required this.length,
});
MdCodeBlockLine({required this.offset, required this.length});
}
/// A method declaration.

View file

@ -76,7 +76,7 @@ int _findCommentReferenceEnd(String comment, int index, int end) {
class DocCommentBuilder {
final Parser parser;
final List<CommentReferenceImpl> references = [];
final List<MdFencedCodeBlock> fencedCodeBlocks = [];
final List<MdCodeBlock> codeBlocks = [];
final Token startToken;
final _CharacterSequence characterSequence;
@ -100,7 +100,7 @@ class DocCommentBuilder {
tokens: tokens,
type: CommentType.DOCUMENTATION,
references: references,
fencedCodeBlocks: fencedCodeBlocks,
codeBlocks: codeBlocks,
);
}
@ -114,27 +114,22 @@ class DocCommentBuilder {
var lineInfo = characterSequence.next();
while (lineInfo != null) {
var (:offset, :content) = lineInfo;
var whitespaceEndIndex = _readWhitespace(content);
if (isPreviousLineEmpty && whitespaceEndIndex >= 4) {
lineInfo = _parseIndentedCodeBlock(content);
if (lineInfo != null) {
isPreviousLineEmpty = lineInfo.content.isEmpty;
}
continue;
}
var fencedCodeBlockIndex = _fencedCodeBlockDelimiter(content);
if (fencedCodeBlockIndex > -1) {
_parseFencedCodeBlock(
index: fencedCodeBlockIndex,
content: content,
);
_parseFencedCodeBlock(index: fencedCodeBlockIndex, content: content);
} else {
// TODO(srawlins): I don't think the indented block decision should be
// different for single-line doc comments vs multi-line. But it might
// change behavior so... treading lightly.
var isIndentedCodeBlock = characterSequence._isFromSingleLineComment
? isPreviousLineEmpty && content.startsWith(' ')
: content.startsWith(' ');
if (!isIndentedCodeBlock) {
_parseDocCommentLine(offset, content);
}
// Mark the previous line as being empty if this function is called with
// a comment Token derived from a single-line comment Token, and the
// line is empty.
isPreviousLineEmpty = content.isEmpty;
_parseDocCommentLine(offset, content);
}
isPreviousLineEmpty = content.isEmpty;
lineInfo = characterSequence.next();
}
}
@ -146,15 +141,9 @@ class DocCommentBuilder {
/// Returns the index of the three backticks.
int _fencedCodeBlockDelimiter(String content, {int minimumTickCount = 3}) {
if (content.isEmpty) return -1;
var index = 0;
var length = content.length;
while (isWhitespace(content.codeUnitAt(index))) {
index++;
if (index >= length) {
return -1;
}
}
var index = _readWhitespace(content);
var length = content.length;
if (index + 3 > length) {
return -1;
}
@ -224,7 +213,7 @@ class DocCommentBuilder {
}) {
var tickCount = 0;
var length = content.length;
while (content.codeUnitAt(index) == '`'.codeUnitAt(0)) {
while (content.codeUnitAt(index) == 0x60 /* '`' */) {
tickCount++;
index++;
if (index >= length) {
@ -237,8 +226,8 @@ class DocCommentBuilder {
infoString = null;
}
var fencedCodeBlockLines = <MdFencedCodeBlockLine>[
MdFencedCodeBlockLine(
var fencedCodeBlockLines = <MdCodeBlockLine>[
MdCodeBlockLine(
offset: characterSequence._offset,
length: content.length,
),
@ -249,10 +238,7 @@ class DocCommentBuilder {
var (:offset, :content) = lineInfo;
fencedCodeBlockLines.add(
MdFencedCodeBlockLine(
offset: offset,
length: content.length,
),
MdCodeBlockLine(offset: offset, length: content.length),
);
var fencedCodeBlockIndex =
@ -260,11 +246,8 @@ class DocCommentBuilder {
if (fencedCodeBlockIndex > -1) {
// End the fenced code block.
fencedCodeBlocks.add(
MdFencedCodeBlock(
infoString: infoString,
lines: fencedCodeBlockLines,
),
codeBlocks.add(
MdCodeBlock(infoString: infoString, lines: fencedCodeBlockLines),
);
return;
}
@ -273,13 +256,43 @@ class DocCommentBuilder {
}
// Non-terminating fenced code block.
fencedCodeBlocks.add(
MdFencedCodeBlock(
infoString: infoString,
lines: fencedCodeBlockLines,
),
codeBlocks.add(
MdCodeBlock(infoString: infoString, lines: fencedCodeBlockLines),
);
return;
}
({int offset, String content})? _parseIndentedCodeBlock(String content) {
var codeBlockLines = <MdCodeBlockLine>[
MdCodeBlockLine(
offset: characterSequence._offset,
length: content.length,
),
];
var lineInfo = characterSequence.next();
while (lineInfo != null) {
var (:offset, :content) = lineInfo;
var whitespaceEndIndex = _readWhitespace(content);
if (whitespaceEndIndex >= 4) {
codeBlockLines.add(
MdCodeBlockLine(offset: offset, length: content.length),
);
} else {
// End the code block.
codeBlocks.add(
MdCodeBlock(infoString: null, lines: codeBlockLines),
);
return lineInfo;
}
lineInfo = characterSequence.next();
}
// The indented code block ends the comment.
codeBlocks.add(
MdCodeBlock(infoString: null, lines: codeBlockLines),
);
return lineInfo;
}
/// Parses the [source] text, found at [offset] in a single comment reference.
@ -448,6 +461,21 @@ class DocCommentBuilder {
);
}
}
/// Reads past any opening whitespace in [content], returning the index after
/// the last whitespace character.
int _readWhitespace(String content) {
if (content.isEmpty) return 0;
var index = 0;
var length = content.length;
while (isWhitespace(content.codeUnitAt(index))) {
index++;
if (index >= length) {
return index;
}
}
return index;
}
}
/// An abstraction of the character sequences in either a single-line doc
@ -461,12 +489,6 @@ abstract class _CharacterSequence {
: _CharacterSequenceFromMultiLineComment(token);
}
/// Whether this character sequence is extracted from a single-line doc
/// comment.
// TODO(srawlins): This should be unnecessary, but the current implementation
// requires this knowledge for parsing indented code blocks.
bool get _isFromSingleLineComment;
/// The current offset in the compilation unit, which is found in [_token].
int get _offset;
@ -490,9 +512,6 @@ class _CharacterSequenceFromMultiLineComment implements _CharacterSequence {
_CharacterSequenceFromMultiLineComment(this._token);
@override
bool get _isFromSingleLineComment => false;
@override
({int offset, String content})? next() {
final lexeme = _token.lexeme;
@ -529,8 +548,13 @@ class _CharacterSequenceFromMultiLineComment implements _CharacterSequence {
}
_end = tokenOffset + endIndex;
const starSpaceLength = '* '.length;
const starLength = '*'.length;
if (lexeme.startsWith('* ', _offset - tokenOffset)) {
_offset += '* '.length;
_offset += starSpaceLength;
} else if (_end == _offset + 1 &&
lexeme.codeUnitAt(_offset - tokenOffset) == 0x2A /* '*' */) {
_offset += starLength;
}
return (
@ -548,9 +572,6 @@ class _CharacterSequenceFromSingleLineComment implements _CharacterSequence {
_CharacterSequenceFromSingleLineComment(this._token);
@override
bool get _isFromSingleLineComment => true;
@override
({int offset, String content})? next() {
const threeSlashesLength = '///'.length;

View file

@ -377,29 +377,29 @@ Comment
* ```
* Three.
*/
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 15
length: 3
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 22
length: 12
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 38
length: 3
MdFencedCodeBlock
MdCodeBlock
infoString: dart
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 53
length: 7
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 64
length: 5
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 73
length: 3
''');
@ -421,14 +421,14 @@ Comment
/// ```
/// ```
/// Text.
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 3
length: 4
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 11
length: 4
''');
@ -450,17 +450,17 @@ Comment
/// ```
/// a[i] = b[i];
/// ```
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 3
length: 6
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 13
length: 15
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 32
length: 6
''');
@ -486,23 +486,23 @@ Comment
/// the amount in the opening:
/// ```
/// `````
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: dart
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 3
length: 9
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 16
length: 73
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 93
length: 27
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 124
length: 4
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 132
length: 6
''');
@ -524,17 +524,17 @@ Comment
///```
///a[i] = b[i];
///```
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 3
length: 3
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 10
length: 12
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 26
length: 3
''');
@ -562,17 +562,17 @@ Comment
/// a[i] = b[i];
/// ```
/// Two.
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 12
length: 4
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 60
length: 13
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 78
length: 4
''');
@ -594,14 +594,14 @@ Comment
/// One.
/// ```
/// a[i] = b[i];
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 12
length: 4
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 20
length: 13
''');
@ -629,17 +629,17 @@ Comment
/// a[i] = b[i];
/// ```
/// Two.
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 24
length: 4
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 32
length: 13
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 49
length: 4
''');
@ -663,14 +663,14 @@ Comment
/// Two.
/// ```
/// Three.
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 25
length: 4
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 33
length: 7
''');
@ -704,29 +704,29 @@ Comment
/// code;
/// ```
/// Three.
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 12
length: 4
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 20
length: 13
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 37
length: 4
MdFencedCodeBlock
MdCodeBlock
infoString: dart
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 54
length: 8
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 66
length: 6
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 76
length: 4
''');
@ -748,6 +748,13 @@ Comment
/// Text.
///
/// a[i] = b[i];
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdCodeBlockLine
offset: 17
length: 16
''');
}
@ -789,12 +796,20 @@ class A {}
Comment
tokens
/// a[i] = b[i];
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdCodeBlockLine
offset: 3
length: 16
''');
}
test_indentedCodeBlock_firstLine_blockComment() {
final parseResult = parseStringWithErrors(r'''
/**
*
* a[i] = b[i];
* [c].
*/
@ -812,9 +827,53 @@ Comment
token: c
tokens
/**
*
* a[i] = b[i];
* [c].
*/
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdCodeBlockLine
offset: 10
length: 16
''');
}
test_indentedCodeBlock_withFencedCodeBlock() {
final parseResult = parseStringWithErrors(r'''
/// Text.
/// ```
/// a[i] = b[i];
/// ```
/// More text.
class A {}
''');
parseResult.assertNoErrors();
final node = parseResult.findNode.comment('Text');
assertParsedNodeText(node, r'''
Comment
tokens
/// Text.
/// ```
/// a[i] = b[i];
/// ```
/// More text.
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdCodeBlockLine
offset: 13
length: 8
MdCodeBlockLine
offset: 25
length: 17
MdCodeBlockLine
offset: 46
length: 8
''');
}
@ -858,6 +917,21 @@ Comment
''');
}
test_onlyWhitespace() {
final parseResult = parseStringWithErrors('''
///${" "}
class A {}
''');
parseResult.assertNoErrors();
final node = parseResult.findNode.comment(' ');
assertParsedNodeText(node, '''
Comment
tokens
///${" "}
''');
}
test_referenceLink() {
final parseResult = parseStringWithErrors(r'''
/// [a link][c] [b].

View file

@ -181,17 +181,17 @@ ClassDeclaration
offset: 223
/// and [Object].
offset: 231
fencedCodeBlocks
MdFencedCodeBlock
codeBlocks
MdCodeBlock
infoString: <empty>
lines
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 178
length: 4
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 186
length: 36
MdFencedCodeBlockLine
MdCodeBlockLine
offset: 226
length: 4
metadata

View file

@ -253,11 +253,11 @@ class ResolvedAstPrinter extends ThrowingAstVisitor<void> {
_sink.writeln('Comment');
_sink.withIndent(() {
_writeNamedChildEntities(node);
if (node.fencedCodeBlocks.isNotEmpty) {
_sink.writelnWithIndent('fencedCodeBlocks');
if (node.codeBlocks.isNotEmpty) {
_sink.writelnWithIndent('codeBlocks');
_sink.withIndent(() {
for (var fencedCodeBlock in node.fencedCodeBlocks) {
_writeMdFencedCodeBlock(fencedCodeBlock);
for (var codeBlock in node.codeBlocks) {
_writeMdCodeBlock(codeBlock);
}
});
}
@ -1710,16 +1710,16 @@ Expected parent: (${parent.runtimeType}) $parent
}
}
void _writeMdFencedCodeBlock(MdFencedCodeBlock fencedCodeBlock) {
_sink.writelnWithIndent('MdFencedCodeBlock');
void _writeMdCodeBlock(MdCodeBlock codeBlock) {
_sink.writelnWithIndent('MdCodeBlock');
_sink.withIndent(() {
var infoString = fencedCodeBlock.infoString;
var infoString = codeBlock.infoString;
_sink.writelnWithIndent('infoString: ${infoString ?? '<empty>'}');
assert(fencedCodeBlock.lines.isNotEmpty);
assert(codeBlock.lines.isNotEmpty);
_sink.writelnWithIndent('lines');
_sink.withIndent(() {
for (var line in fencedCodeBlock.lines) {
_sink.writelnWithIndent('MdFencedCodeBlockLine');
for (var line in codeBlock.lines) {
_sink.writelnWithIndent('MdCodeBlockLine');
_sink.withIndent(() {
_sink.writelnWithIndent('offset: ${line.offset}');
_sink.writelnWithIndent('length: ${line.length}');