Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

fix: handle multiline comments #1129

Merged
merged 1 commit into from
Jan 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
* feat: support ignoring nesting for [`prefer-conditional-expressions`](https://dartcodemetrics.dev/docs/rules/common/prefer-conditional-expressions).
* fix: ignore Providers for ['avoid-returning-widgets'](https://dartcodemetrics.dev/docs/rules/common/avoid-returning-widgets).
* feat: add [`use-setstate-synchronously`](https://dartcodemetrics.dev/docs/rules/flutter/use-setstate-synchronously).
* fix: correctly invalidate edge cases for [`use-setstate-synchronously`](https://dartcodemetrics.dev/docs/rules/flutter/use-setstate-synchronously)
* fix: correctly invalidate edge cases for [`use-setstate-synchronously`](https://dartcodemetrics.dev/docs/rules/flutter/use-setstate-synchronously).
* fix: handle multiline comments for [`format-comment`](https://dartcodemetrics.dev/docs/rules/common/format-comment).

## 5.3.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import '../../models/common_rule.dart';
import '../../rule_utils.dart';

part 'config_parser.dart';
part 'models/comment_info.dart';
part 'models/comment_type.dart';
part 'visitor.dart';

class FormatCommentRule extends CommonRule {
Expand Down Expand Up @@ -53,61 +51,72 @@ class FormatCommentRule extends CommonRule {
final visitor = _Visitor(
_ignoredPatterns,
_onlyDocComments,
)..checkComments(source.unit.root);
)..checkRegularComments(source.unit.root);

return [
for (final comment in visitor.comments)
createIssue(
source.unit.visitChildren(visitor);

final issues = <Issue>[];

for (final comment in visitor.comments) {
if (comment is _DocCommentInfo) {
issues.add(createIssue(
rule: this,
location: nodeLocation(
node: comment.token,
source: source,
),
location: nodeLocation(node: comment.comment, source: source),
message: _warning,
replacement: _createReplacement(comment),
),
];
replacement: _docCommentReplacement(comment.comment),
));
}
if (comment is _RegularCommentInfo) {
issues.addAll(
comment.tokens
.map((token) => createIssue(
rule: this,
location: nodeLocation(node: token, source: source),
message: _warning,
replacement: _regularCommentReplacement(
token,
comment.tokens.length == 1,
),
))
.toList(),
);
}
}

return issues;
}

Replacement _createReplacement(_CommentInfo commentInfo) {
final commentToken = commentInfo.token;
var resultString = commentToken.toString();

switch (commentInfo.type) {
case _CommentType.base:
String commentText;

final isHasNextComment = commentToken.next != null &&
commentToken.next!.type == TokenType.SINGLE_LINE_COMMENT &&
commentToken.next!.offset ==
commentToken.offset + resultString.length + 1;
final subString = resultString.substring(2, resultString.length);

commentText = isHasNextComment
? subString.trim().capitalize()
: formatComment(subString);

resultString = '// $commentText';
break;
case _CommentType.documentation:
final commentText =
formatComment(resultString.substring(3, resultString.length));
resultString = '/// $commentText';
break;
Replacement? _docCommentReplacement(Comment comment) {
if (comment.tokens.length == 1) {
final commentToken = comment.tokens.first;
final text = commentToken.toString();
final commentText = formatComment(text.substring(3, text.length));

return Replacement(
comment: 'Format comment.',
replacement: '/// $commentText',
);
}

return Replacement(
comment: 'Format comment like sentences',
replacement: resultString,
);
return null;
}

String formatComment(String res) => res.trim().capitalize().replaceEnd();
}
Replacement? _regularCommentReplacement(Token token, bool isSingle) {
if (isSingle) {
final text = token.toString();
final commentText = formatComment(text.substring(2, text.length));

return Replacement(
comment: 'Format comment.',
replacement: '// $commentText',
);
}

return null;
}

const _punctuation = ['.', '!', '?'];
String formatComment(String res) => replaceEnd(res.trim().capitalize());

extension _StringExtension on String {
String replaceEnd() =>
!_punctuation.contains(this[length - 1]) ? '$this.' : this;
String replaceEnd(String text) =>
!_punctuation.contains(text[text.length - 1]) ? '$text.' : text;
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
part of 'format_comment_rule.dart';

const _commentsOperator = {
_CommentType.base: '//',
_CommentType.documentation: '///',
};
const _punctuation = ['.', '!', '?', ':'];

final _sentencesRegExp = RegExp(r'(?<=([\.|:](?=\s|\n|$)))');
final _regMacrosExp = RegExp('{@(template|macro) .+}');
const _macrosEndExp = '{@endtemplate}';
const _ignoreExp = 'ignore:';
const _ignoreForFileExp = 'ignore_for_file:';

class _Visitor extends RecursiveAstVisitor<void> {
final Iterable<RegExp> _ignoredPatterns;

final bool _onlyDocComments;

// ignore: avoid_positional_boolean_parameters
Expand All @@ -21,15 +20,59 @@ class _Visitor extends RecursiveAstVisitor<void> {

Iterable<_CommentInfo> get comments => _comments;

void checkComments(AstNode node) {
@override
void visitComment(Comment node) {
super.visitComment(node);

if (node.isDocumentation) {
final isValid = node.tokens.length == 1
? _hasValidSingleLine(node.tokens.first, _CommentType.doc)
: _hasValidMultiline(node.tokens, _CommentType.doc);
if (!isValid) {
_comments.add(_DocCommentInfo(node));
}
}
}

void checkRegularComments(AstNode node) {
if (_onlyDocComments) {
return;
}

Token? token = node.beginToken;
while (token != null) {
final extractedComments = <Token>[];

Token? commentToken = token.precedingComments;
while (commentToken != null) {
_commentValidation(commentToken);
if (_isRegularComment(commentToken)) {
extractedComments.add(commentToken);
}
commentToken = commentToken.next;
}

if (extractedComments.isNotEmpty) {
final isValid = extractedComments.length > 1
? _hasValidMultiline(extractedComments, _CommentType.regular)
: _hasValidSingleLine(
extractedComments.first,
_CommentType.regular,
);
if (!isValid) {
final notIgnored = extractedComments.where((comment) {
final trimmed = comment
.toString()
.replaceAll(_CommentType.regular.pattern, '')
.trim();

return !_isIgnoreComment(trimmed) && !_isIgnoredPattern(trimmed);
}).toList();
_comments.add(_RegularCommentInfo(notIgnored));
}

extractedComments.clear();
}

if (token == token.next) {
break;
}
Expand All @@ -38,48 +81,99 @@ class _Visitor extends RecursiveAstVisitor<void> {
}
}

void _commentValidation(Token commentToken) {
if (commentToken.type == TokenType.SINGLE_LINE_COMMENT) {
final token = commentToken.toString();
if (token.startsWith('///')) {
_checkCommentByType(commentToken, _CommentType.documentation);
} else if (token.startsWith('//') && !_onlyDocComments) {
_checkCommentByType(commentToken, _CommentType.base);
}
}
bool _isRegularComment(Token commentToken) {
final token = commentToken.toString();

return !token.startsWith('///') && token.startsWith('//');
}

void _checkCommentByType(Token commentToken, _CommentType type) {
final commentText =
commentToken.toString().substring(_commentsOperator[type]!.length);
bool _hasValidMultiline(List<Token> commentTokens, _CommentType type) {
final text = _extractText(commentTokens, type);
final sentences = text.split(_sentencesRegExp);

var text = commentText.trim();
return sentences.every(_isValidSentence);
}

final isIgnoreComment =
text.startsWith(_ignoreExp) || text.startsWith(_ignoreForFileExp);
bool _hasValidSingleLine(Token commentToken, _CommentType type) {
final commentText = commentToken.toString().substring(type.pattern.length);
final text = commentText.trim();

final isMacros = _regMacrosExp.hasMatch(text) || text == _macrosEndExp;
if (text.isEmpty ||
_isIgnoreComment(text) ||
_isMacros(text) ||
_isIgnoredPattern(text)) {
return true;
}

final isAnIgnoredPattern = _ignoredPatterns.any(
(regExp) => regExp.hasMatch(text),
);
return _isValidSentence(commentText);
}

{
if (text.isEmpty || isIgnoreComment || isMacros || isAnIgnoredPattern) {
return;
} else {
text = text.trim();
final upperCase = text[0] == text[0].toUpperCase();
final lastSymbol = _punctuation.contains(text[text.length - 1]);
final hasEmptySpace = commentText[0] == ' ';
final incorrectFormat = !upperCase || !hasEmptySpace || !lastSymbol;
final single =
commentToken.previous == null && commentToken.next == null;
bool _isValidSentence(String sentence) {
final trimmedSentence = sentence.trim();

if (incorrectFormat && single) {
_comments.add(_CommentInfo(type, commentToken));
}
final upperCase = trimmedSentence[0] == trimmedSentence[0].toUpperCase();
final lastSymbol =
_punctuation.contains(trimmedSentence[trimmedSentence.length - 1]);
final hasEmptySpace = sentence[0] == ' ';

return upperCase && lastSymbol && hasEmptySpace;
}

String _extractText(List<Token> commentTokens, _CommentType type) {
var result = '';
var shouldSkipNext = false;
for (final token in commentTokens) {
final commentText = token.toString().replaceAll(type.pattern, '');
if (commentText.contains('```')) {
shouldSkipNext = !shouldSkipNext;
} else if (!_shouldSkip(commentText) && !shouldSkipNext) {
result += commentText;
}
}

return result;
}

bool _shouldSkip(String text) {
final trimmed = text.trim();

return _regMacrosExp.hasMatch(text) ||
text.contains(_macrosEndExp) ||
_isIgnoreComment(trimmed) ||
_isIgnoredPattern(trimmed);
}

bool _isIgnoreComment(String text) =>
text.startsWith(_ignoreExp) || text.startsWith(_ignoreForFileExp);

bool _isMacros(String text) =>
_regMacrosExp.hasMatch(text) || text == _macrosEndExp;

bool _isIgnoredPattern(String text) =>
_ignoredPatterns.any((regExp) => regExp.hasMatch(text));
}

abstract class _CommentInfo {
const _CommentInfo();
}

class _DocCommentInfo extends _CommentInfo {
final Comment comment;

const _DocCommentInfo(this.comment);
}

class _RegularCommentInfo extends _CommentInfo {
final List<Token> tokens;

const _RegularCommentInfo(this.tokens);
}

class _CommentType {
final String pattern;

const _CommentType(this.pattern);

static const regular = _CommentType('//');
static const doc = _CommentType('///');
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ class Test {

/// With start space without dot
function() {
/// with start space with dot.
//Any other comment
}
// ignore:
}

// ignore_for_file:
var a;

/// [WidgetModel] for widget.
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// With start space without dot.
var a;

/// With start space without dot.
var a;
/* With start space without dot.*/
var a;
// TODO: Asdasd:asd:Asdasdasd.
var a;
// TODO(vlad): Asdasd:asd:Asdasdasd.
var a;
Loading