Add documentation comment collection to AST (#548)

- Documentation comments are simply identified by starting with a triple-slash "///". 
- Adjacent comments in indented syntax are now coalesced into a single comment.
This commit is contained in:
Nicholas Shahan 2018-12-20 16:08:26 -08:00 committed by GitHub
parent 94fd7e6e50
commit 3a493f23ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 294 additions and 29 deletions

View File

@ -7,6 +7,7 @@ import 'package:source_span/source_span.dart';
import '../argument_declaration.dart';
import '../statement.dart';
import 'parent.dart';
import 'silent_comment.dart';
/// An abstract class for callables (functions or mixins) that are declared in
/// user code.
@ -16,12 +17,17 @@ abstract class CallableDeclaration extends ParentStatement {
/// This may be `null` for callables without names.
final String name;
/// The comment immediately preceding this declaration.
final SilentComment comment;
/// The declared arguments this callable accepts.
final ArgumentDeclaration arguments;
final FileSpan span;
CallableDeclaration(
this.name, this.arguments, Iterable<Statement> children, this.span)
: super(List.unmodifiable(children));
this.name, this.arguments, Iterable<Statement> children, this.span,
{SilentComment comment})
: comment = comment,
super(List.unmodifiable(children));
}

View File

@ -8,14 +8,16 @@ import '../../../visitor/interface/statement.dart';
import '../argument_declaration.dart';
import '../statement.dart';
import 'callable_declaration.dart';
import 'silent_comment.dart';
/// A function declaration.
///
/// This declares a function that's invoked using normal CSS function syntax.
class FunctionRule extends CallableDeclaration {
FunctionRule(String name, ArgumentDeclaration arguments,
Iterable<Statement> children, FileSpan span)
: super(name, arguments, children, span);
Iterable<Statement> children, FileSpan span,
{SilentComment comment})
: super(name, arguments, children, span, comment: comment);
T accept<T>(StatementVisitor<T> visitor) => visitor.visitFunctionRule(this);

View File

@ -8,6 +8,7 @@ import '../../../visitor/interface/statement.dart';
import '../argument_declaration.dart';
import '../statement.dart';
import 'callable_declaration.dart';
import 'silent_comment.dart';
/// A mixin declaration.
///
@ -23,8 +24,8 @@ class MixinRule extends CallableDeclaration {
/// won't work correctly.
MixinRule(String name, ArgumentDeclaration arguments,
Iterable<Statement> children, FileSpan span,
{this.hasContent = false})
: super(name, arguments, children, span);
{this.hasContent = false, SilentComment comment})
: super(name, arguments, children, span, comment: comment);
T accept<T>(StatementVisitor<T> visitor) => visitor.visitMixinRule(this);

View File

@ -2,7 +2,10 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:math' as math;
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
import '../../../visitor/interface/statement.dart';
import '../statement.dart';
@ -12,6 +15,24 @@ class SilentComment implements Statement {
/// The text of this comment, including comment characters.
final String text;
/// The subset of lines in text that are marked as part of the documentation
/// comments by beginning with '///'.
///
/// The leading slashes and space on each line is removed. Returns `null` when
/// there is no documentation comment.
String get docComment {
var buffer = StringBuffer();
for (var line in text.split('\n')) {
var scanner = StringScanner(line.trim());
if (!scanner.scan('///')) continue;
scanner.scan(' ');
buffer.writeln(scanner.rest);
}
var comment = buffer.toString().trimRight();
return comment.isNotEmpty ? comment : null;
}
final FileSpan span;
SilentComment(this.text, this.span);

View File

@ -9,6 +9,7 @@ import '../../../parse/scss.dart';
import '../../../visitor/interface/statement.dart';
import '../expression.dart';
import '../statement.dart';
import 'silent_comment.dart';
/// A variable declaration.
///
@ -17,6 +18,9 @@ class VariableDeclaration implements Statement {
/// The name of the variable.
final String name;
/// The comment immediatly preceding this declaration.
SilentComment comment;
/// The value the variable is being assigned to.
final Expression expression;
@ -33,9 +37,10 @@ class VariableDeclaration implements Statement {
final FileSpan span;
VariableDeclaration(this.name, this.expression, this.span,
{bool guarded = false, bool global = false})
{bool guarded = false, bool global = false, SilentComment comment})
: isGuarded = guarded,
isGlobal = global;
isGlobal = global,
comment = comment;
/// Parses a variable declaration from [contents].
///

View File

@ -185,28 +185,46 @@ class SassParser extends StylesheetParser {
SilentComment _silentComment() {
var start = scanner.state;
scanner.expect("//");
var buffer = StringBuffer();
var parentIndentation = currentIndentation;
while (true) {
buffer.write("//");
// Skip the first two indentation characters because we're already writing
// "//".
for (var i = 2; i < currentIndentation - parentIndentation; i++) {
buffer.writeCharCode($space);
outer:
do {
var commentPrefix = scanner.scanChar($slash) ? "///" : "//";
while (true) {
buffer.write(commentPrefix);
// Skip the initial characters because we're already writing the
// slashes.
for (var i = commentPrefix.length;
i < currentIndentation - parentIndentation;
i++) {
buffer.writeCharCode($space);
}
while (!scanner.isDone && !isNewline(scanner.peekChar())) {
buffer.writeCharCode(scanner.readChar());
}
buffer.writeln();
if (_peekIndentation() < parentIndentation) break outer;
if (_peekIndentation() == parentIndentation) {
// Look ahead to the next line to see if it starts another comment.
if (scanner.peekChar(1 + parentIndentation) == $slash &&
scanner.peekChar(2 + parentIndentation) == $slash) {
_readIndentation();
}
break;
}
_readIndentation();
}
} while (scanner.scan("//"));
while (!scanner.isDone && !isNewline(scanner.peekChar())) {
buffer.writeCharCode(scanner.readChar());
}
buffer.writeln();
if (_peekIndentation() <= parentIndentation) break;
_readIndentation();
}
return SilentComment(buffer.toString(), scanner.spanFrom(start));
lastSilentComment =
SilentComment(buffer.toString(), scanner.spanFrom(start));
return lastSilentComment;
}
/// Consumes an indented-style loud context.

View File

@ -158,8 +158,9 @@ class ScssParser extends StylesheetParser {
scanner.spanFrom(start));
}
return SilentComment(
lastSilentComment = SilentComment(
scanner.substring(start.position), scanner.spanFrom(start));
return lastSilentComment;
}
/// Consumes a statement-level loud comment block.

View File

@ -57,6 +57,10 @@ abstract class StylesheetParser extends Parser {
/// Whether the parser is currently within a parenthesized expression.
var _inParentheses = false;
/// The silent comment this parser encountered previously.
@protected
SilentComment lastSilentComment;
StylesheetParser(String contents, {url, Logger logger})
: super(contents, url: url, logger: logger);
@ -147,6 +151,8 @@ abstract class StylesheetParser extends Parser {
/// Consumes a variable declaration.
@protected
VariableDeclaration variableDeclaration() {
var precedingComment = lastSilentComment;
lastSilentComment = null;
var start = scanner.state;
var name = variableName();
@ -179,7 +185,7 @@ abstract class StylesheetParser extends Parser {
expectStatementSeparator("variable declaration");
return VariableDeclaration(name, value, scanner.spanFrom(start),
guarded: guarded, global: global);
guarded: guarded, global: global, comment: precedingComment);
}
/// Consumes a style rule.
@ -681,6 +687,8 @@ abstract class StylesheetParser extends Parser {
///
/// [start] should point before the `@`.
FunctionRule _functionRule(LineScannerState start) {
var precedingComment = lastSilentComment;
lastSilentComment = null;
var name = identifier();
whitespace();
var arguments = _argumentDeclaration();
@ -708,7 +716,8 @@ abstract class StylesheetParser extends Parser {
whitespace();
var children = this.children(_functionAtRule);
return FunctionRule(name, arguments, children, scanner.spanFrom(start));
return FunctionRule(name, arguments, children, scanner.spanFrom(start),
comment: precedingComment);
}
/// Consumes a `@for` rule.
@ -937,6 +946,8 @@ abstract class StylesheetParser extends Parser {
///
/// [start] should point before the `@`.
MixinRule _mixinRule(LineScannerState start) {
var precedingComment = lastSilentComment;
lastSilentComment = null;
var name = identifier();
whitespace();
var arguments = scanner.peekChar() == $lparen
@ -960,7 +971,7 @@ abstract class StylesheetParser extends Parser {
_mixinHasContent = null;
return MixinRule(name, arguments, children, scanner.spanFrom(start),
hasContent: hadContent);
hasContent: hadContent, comment: precedingComment);
}
/// Consumes a `@moz-document` rule.

200
test/doc_comments_test.dart Normal file
View File

@ -0,0 +1,200 @@
// Copyright 2018 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:sass/src/ast/sass.dart';
import 'package:test/test.dart';
void main() {
group('documentation comments', () {
group('in SCSS', () {
test('attach to variable declarations', () {
final contents = r'''
/// Results my vary.
$vary: 5.16em;''';
final stylesheet = Stylesheet.parseScss(contents);
final variable =
stylesheet.children.whereType<VariableDeclaration>().first;
expect(variable.comment.docComment, equals('Results my vary.'));
});
test('attach to function rules', () {
final contents = r'''
/// A fun function!
@function fun($val) {
// Not a doc comment.
@return ($val / 1000) * 1em;
}''';
final stylesheet = Stylesheet.parseScss(contents);
final function = stylesheet.children.whereType<FunctionRule>().first;
expect(function.comment.docComment, equals('A fun function!'));
});
test('attach to mixin rules', () {
final contents = r'''
/// Mysterious mixin.
@mixin mystery {
// All black.
color: black;
background-color: black;
}''';
final stylesheet = Stylesheet.parseScss(contents);
final mix = stylesheet.children.whereType<MixinRule>().first;
expect(mix.comment.docComment, equals('Mysterious mixin.'));
});
test('are null when there are no triple-slash comments', () {
final contents = r'''
// Regular comment.
$vary: 5.16em;''';
final stylesheet = Stylesheet.parseScss(contents);
final variable =
stylesheet.children.whereType<VariableDeclaration>().first;
expect(variable.comment.docComment, isNull);
});
test('are not carried over across members', () {
final contents = r'''
/// Mysterious mixin.
@mixin mystery {
// All black.
color: black;
background-color: black;
}
/// A fun function!
@function fun($val) {
// Not a doc comment.
@return ($val / 1000) * 1em;
}''';
final stylesheet = Stylesheet.parseScss(contents);
final mix = stylesheet.children.whereType<MixinRule>().first;
final function = stylesheet.children.whereType<FunctionRule>().first;
expect(mix.comment.docComment, equals('Mysterious mixin.'));
expect(function.comment.docComment, equals('A fun function!'));
});
test('do not include double-slash comments', () {
final contents = r'''
// Not a doc comment.
/// Line 1
/// Line 2
// Not a doc comment.
/// Line 3
// Not a doc comment.
$vary: 5.16em;''';
final stylesheet = Stylesheet.parseScss(contents);
final variable =
stylesheet.children.whereType<VariableDeclaration>().first;
expect(variable.comment.docComment, equals('Line 1\nLine 2\nLine 3'));
});
});
group('in indented syntax', () {
test('attach to variable declarations', () {
final contents = r'''
/// Results my vary.
$vary: 5.16em''';
final stylesheet = Stylesheet.parseSass(contents);
final variable =
stylesheet.children.whereType<VariableDeclaration>().first;
expect(variable.comment.docComment, equals('Results my vary.'));
});
test('attach to function rules', () {
final contents = r'''
/// A fun function!
@function fun($val)
// Not a doc comment.
@return ($val / 1000) * 1em''';
final stylesheet = Stylesheet.parseSass(contents);
final function = stylesheet.children.whereType<FunctionRule>().first;
expect(function.comment.docComment, equals('A fun function!'));
});
test('attach to mixin rules', () {
final contents = r'''
/// Mysterious mixin.
@mixin mystery
// All black.
color: black
background-color: black''';
final stylesheet = Stylesheet.parseSass(contents);
final mix = stylesheet.children.whereType<MixinRule>().first;
expect(mix.comment.docComment, equals('Mysterious mixin.'));
});
test('are null when there are no triple-slash comments', () {
final contents = r'''
// Regular comment.
$vary: 5.16em''';
final stylesheet = Stylesheet.parseSass(contents);
final variable =
stylesheet.children.whereType<VariableDeclaration>().first;
expect(variable.comment.docComment, isNull);
});
test('are not carried over across members', () {
final contents = r'''
/// Mysterious mixin.
@mixin mystery
// All black.
color: black
background-color: black
/// A fun function!
@function fun($val)
// Not a doc comment.
@return ($val / 1000) * 1em''';
final stylesheet = Stylesheet.parseSass(contents);
final mix = stylesheet.children.whereType<MixinRule>().first;
final function = stylesheet.children.whereType<FunctionRule>().first;
expect(mix.comment.docComment, equals('Mysterious mixin.'));
expect(function.comment.docComment, equals('A fun function!'));
});
test('do not include double-slash comments', () {
final contents = r'''
// Not a doc comment.
/// Line 1
Line 2
// Not a doc comment.
Should be ignored.
$vary: 5.16em''';
final stylesheet = Stylesheet.parseSass(contents);
final variable =
stylesheet.children.whereType<VariableDeclaration>().first;
expect(variable.comment.docComment, equals('Line 1\nLine 2'));
});
test('are compacted into one from adjacent comments', () {
final contents = r'''
// Not a doc comment.
/// Line 1
/// Line 2
Line 3
/// Line 4
$vary: 5.16em''';
final stylesheet = Stylesheet.parseSass(contents);
final variable =
stylesheet.children.whereType<VariableDeclaration>().first;
expect(stylesheet.children.length, equals(2));
expect(variable.comment.docComment,
equals('Line 1\nLine 2\nLine 3\nLine 4'));
});
});
});
}