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

Commit 4a2f6c2

Browse files
Konoshenko Vladdkrutskikh
Konoshenko Vlad
andauthored
feat: add static code diagnostics format-comment. (#622)
* feat: capitalize comment * feat: capitalize comment * feat: refactored validation process * feat: added rule to factory * chore: added documentation * chore: added changelog * chore: fix test * chore: fix test * formatted code * fix test * review changes * review changes * review changes * part * fix: Valid value range is empty * fix: remove not using params * add ignore file * fix: update master * formatted code * feat: add static code diagnostics `format-single-line-comment`. * formatted code * fix: rename rule refactored code * fix: remove single line * fix: remove single line * chore: some changes Co-authored-by: Dmitry Krutskikh <[email protected]>
1 parent 3e6b525 commit 4a2f6c2

File tree

12 files changed

+359
-0
lines changed

12 files changed

+359
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
* feat: add static code diagnostics `format-comment`.
56
* feat: add static code diagnostics `avoid-border-all`.
67
* feat: improve `avoid-returning-widgets` builder functions handling.
78
* fix: correctly handle const maps in `no-magic-number`.

lib/src/analyzers/lint_analyzer/rules/rules_factory.dart

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'rules_list/avoid_wrapping_in_padding/avoid_wrapping_in_padding_rule.dart
2020
import 'rules_list/binary_expression_operand_order/binary_expression_operand_order_rule.dart';
2121
import 'rules_list/component_annotation_arguments_ordering/component_annotation_arguments_ordering_rule.dart';
2222
import 'rules_list/double_literal_format/double_literal_format_rule.dart';
23+
import 'rules_list/format_comment/format_comment_rule.dart';
2324
import 'rules_list/member_ordering/member_ordering_rule.dart';
2425
import 'rules_list/member_ordering_extended/member_ordering_extended_rule.dart';
2526
import 'rules_list/newline_before_return/newline_before_return_rule.dart';
@@ -81,6 +82,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
8182
ComponentAnnotationArgumentsOrderingRule.ruleId: (config) =>
8283
ComponentAnnotationArgumentsOrderingRule(config),
8384
DoubleLiteralFormatRule.ruleId: (config) => DoubleLiteralFormatRule(config),
85+
FormatCommentRule.ruleId: (config) => FormatCommentRule(config),
8486
MemberOrderingRule.ruleId: (config) => MemberOrderingRule(config),
8587
MemberOrderingExtendedRule.ruleId: (config) =>
8688
MemberOrderingExtendedRule(config),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/token.dart';
3+
import 'package:analyzer/dart/ast/visitor.dart';
4+
5+
import '../../../../../utils/node_utils.dart';
6+
import '../../../../../utils/string_extensions.dart';
7+
import '../../../lint_utils.dart';
8+
import '../../../models/internal_resolved_unit_result.dart';
9+
import '../../../models/issue.dart';
10+
import '../../../models/replacement.dart';
11+
import '../../../models/severity.dart';
12+
import '../../models/common_rule.dart';
13+
import '../../rule_utils.dart';
14+
15+
part 'models/comment_info.dart';
16+
17+
part 'models/comment_type.dart';
18+
19+
part 'visitor.dart';
20+
21+
class FormatCommentRule extends CommonRule {
22+
static const String ruleId = 'format-comment';
23+
24+
static const _warning = 'Prefer formatting comments like sentences.';
25+
26+
FormatCommentRule([Map<String, Object> config = const {}])
27+
: super(
28+
id: ruleId,
29+
severity: readSeverity(config, Severity.style),
30+
excludes: readExcludes(config),
31+
);
32+
33+
@override
34+
Iterable<Issue> check(InternalResolvedUnitResult source) {
35+
final visitor = _Visitor()..checkComments(source.unit.root);
36+
37+
return [
38+
for (final comment in visitor.comments)
39+
createIssue(
40+
rule: this,
41+
location: nodeLocation(
42+
node: comment.token,
43+
source: source,
44+
),
45+
message: _warning,
46+
replacement: _createReplacement(comment),
47+
),
48+
];
49+
}
50+
51+
Replacement _createReplacement(_CommentInfo commentInfo) {
52+
final commentToken = commentInfo.token;
53+
var resultString = commentToken.toString();
54+
55+
switch (commentInfo.type) {
56+
case _CommentType.base:
57+
String subString;
58+
59+
final isHasNextComment = commentToken.next != null &&
60+
commentToken.next!.type == TokenType.SINGLE_LINE_COMMENT &&
61+
commentToken.next!.offset ==
62+
commentToken.offset + resultString.length + 1;
63+
64+
subString = isHasNextComment
65+
? resultString.substring(2, resultString.length).trim().capitalize()
66+
: formatComment(resultString.substring(2, resultString.length));
67+
68+
resultString = '// $subString';
69+
break;
70+
case _CommentType.documentation:
71+
final subString =
72+
formatComment(resultString.substring(3, resultString.length));
73+
resultString = '/// $subString';
74+
break;
75+
}
76+
77+
return Replacement(
78+
comment: 'Format comment like sentences',
79+
replacement: resultString,
80+
);
81+
}
82+
83+
String formatComment(String res) => res.trim().capitalize().replaceEnd();
84+
}
85+
86+
const _punctuation = ['.', '!', '?'];
87+
88+
extension _StringExtension on String {
89+
String replaceEnd() =>
90+
!_punctuation.contains(this[length - 1]) ? '$this.' : this;
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
part of '../format_comment_rule.dart';
2+
3+
class _CommentInfo {
4+
final Token token;
5+
final _CommentType type;
6+
7+
_CommentInfo(this.type, this.token);
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
part of '../format_comment_rule.dart';
2+
3+
enum _CommentType { base, documentation }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
part of 'format_comment_rule.dart';
2+
3+
const commentsOperator = {
4+
_CommentType.base: '//',
5+
_CommentType.documentation: '///',
6+
};
7+
8+
class _Visitor extends RecursiveAstVisitor<void> {
9+
final _comments = <_CommentInfo>[];
10+
11+
Iterable<_CommentInfo> get comments => _comments;
12+
13+
void checkComments(AstNode node) {
14+
Token? token = node.beginToken;
15+
while (token != null) {
16+
Token? commentToken = token.precedingComments;
17+
while (commentToken != null) {
18+
_commentValidation(commentToken);
19+
commentToken = commentToken.next;
20+
}
21+
22+
if (token == token.next) {
23+
break;
24+
}
25+
26+
token = token.next;
27+
}
28+
}
29+
30+
void _commentValidation(Token commentToken) {
31+
if (commentToken.type == TokenType.SINGLE_LINE_COMMENT) {
32+
if (commentToken.toString().startsWith('///')) {
33+
_checkCommentByType(commentToken, _CommentType.documentation);
34+
} else if (commentToken.toString().startsWith('//')) {
35+
_checkCommentByType(commentToken, _CommentType.base);
36+
}
37+
}
38+
}
39+
40+
void _checkCommentByType(Token commentToken, _CommentType type) {
41+
final commentText =
42+
commentToken.toString().substring(commentsOperator[type]!.length);
43+
44+
var text = commentText.trim();
45+
46+
if (text.isEmpty ||
47+
text.startsWith('ignore:') ||
48+
text.startsWith('ignore_for_file:')) {
49+
return;
50+
} else {
51+
text = text.trim();
52+
final upperCase = text[0] == text[0].toUpperCase();
53+
final lastSymbol = _punctuation.contains(text[text.length - 1]);
54+
final hasEmptySpace = commentText[0] == ' ';
55+
final incorrectFormat = !upperCase || !hasEmptySpace || !lastSymbol;
56+
final single = commentToken.previous == null && commentToken.next == null;
57+
58+
if (incorrectFormat && single) {
59+
_comments.add(_CommentInfo(type, commentToken));
60+
}
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// With start space without dot
2+
class Test {
3+
//Without start space without dot
4+
Test() {
5+
// with start space with dot.
6+
}
7+
8+
/// With start space without dot
9+
function() {
10+
/// with start space with dot.
11+
}
12+
// ignore:
13+
}
14+
15+
// ignore_for_file:
16+
var a;
17+
18+
/// [WidgetModel] for widget.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class Box {
2+
/// The value this wraps
3+
Object? _value;
4+
5+
/// true if this box contains a value.
6+
bool get hasValue => _value != null;
7+
}
8+
9+
//not if there is nothing before it
10+
test() => false;
11+
12+
void greet(String name) {
13+
// assume we have a valid name.
14+
print('Hi, $name!');
15+
}
16+
17+
/// deletes the file at [path] from the file system.
18+
void delete(String path) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// With start space without dot.
2+
/// With start space without dot.
3+
/* With start space without dot.*/
4+
// TODO: Asdasd:asd:Asdasdasd.
5+
// TODO(vlad): Asdasd:asd:Asdasdasd.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart';
2+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/format_comment/format_comment_rule.dart';
3+
import 'package:test/test.dart';
4+
5+
import '../../../../../helpers/rule_test_helper.dart';
6+
7+
const _examplePath = 'format_comment/examples/example.dart';
8+
const _withoutIssuePath = 'format_comment/examples/example_without_issue.dart';
9+
const _multiline = 'format_comment/examples/example_documentation.dart';
10+
11+
void main() {
12+
group('FormatCommentRule', () {
13+
test('initialization', () async {
14+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
15+
final issues = FormatCommentRule().check(unit);
16+
17+
RuleTestHelper.verifyInitialization(
18+
issues: issues,
19+
ruleId: 'format-comment',
20+
severity: Severity.style,
21+
);
22+
});
23+
24+
test('reports about found issues', () async {
25+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
26+
final issues = FormatCommentRule().check(unit);
27+
28+
RuleTestHelper.verifyIssues(
29+
issues: issues,
30+
startLines: [1, 3, 5, 8, 10],
31+
startColumns: [1, 3, 5, 3, 5],
32+
locationTexts: [
33+
'// With start space without dot',
34+
'//Without start space without dot',
35+
'// with start space with dot.',
36+
'/// With start space without dot',
37+
'/// with start space with dot.',
38+
],
39+
messages: [
40+
'Prefer formatting comments like sentences.',
41+
'Prefer formatting comments like sentences.',
42+
'Prefer formatting comments like sentences.',
43+
'Prefer formatting comments like sentences.',
44+
'Prefer formatting comments like sentences.',
45+
],
46+
replacements: [
47+
'// With start space without dot.',
48+
'// Without start space without dot.',
49+
'// With start space with dot.',
50+
'/// With start space without dot.',
51+
'/// With start space with dot.',
52+
],
53+
);
54+
});
55+
56+
test('reports about found issues in cases from documentation', () async {
57+
final unit = await RuleTestHelper.resolveFromFile(_multiline);
58+
final issues = FormatCommentRule().check(unit);
59+
60+
RuleTestHelper.verifyIssues(
61+
issues: issues,
62+
startLines: [2, 5, 9, 13, 17],
63+
startColumns: [3, 3, 1, 3, 1],
64+
locationTexts: [
65+
'/// The value this wraps',
66+
'/// true if this box contains a value.',
67+
'//not if there is nothing before it',
68+
'// assume we have a valid name.',
69+
'/// deletes the file at [path] from the file system.',
70+
],
71+
messages: [
72+
'Prefer formatting comments like sentences.',
73+
'Prefer formatting comments like sentences.',
74+
'Prefer formatting comments like sentences.',
75+
'Prefer formatting comments like sentences.',
76+
'Prefer formatting comments like sentences.',
77+
],
78+
replacements: [
79+
'/// The value this wraps.',
80+
'/// True if this box contains a value.',
81+
'// Not if there is nothing before it.',
82+
'// Assume we have a valid name.',
83+
'/// Deletes the file at [path] from the file system.',
84+
],
85+
);
86+
});
87+
88+
test('reports no issues', () async {
89+
final unit = await RuleTestHelper.resolveFromFile(_withoutIssuePath);
90+
RuleTestHelper.verifyNoIssues(FormatCommentRule().check(unit));
91+
});
92+
});
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Format comments
2+
3+
## Rule id {#rule-id}
4+
5+
format-comment
6+
7+
## Severity {#severity}
8+
9+
Style
10+
11+
## Description {#description}
12+
13+
Prefer format comments like sentences.
14+
15+
### Example {#example}
16+
17+
Bad:
18+
19+
```dart
20+
// prefer format comments like sentences // LINT
21+
class Test {
22+
/// with start space with dot. // LINT
23+
Test() {
24+
// with start space with dot. // LINT
25+
}
26+
27+
/// With start space without dot // LINT
28+
function() {
29+
//Without start space without dot // LINT
30+
}
31+
}
32+
/* prefer format comments
33+
like sentences */ // LINT
34+
```
35+
36+
Good:
37+
38+
```dart
39+
// Prefer format comments like sentences.
40+
class Test {
41+
/// With start space with dot.
42+
Test() {
43+
// With start space with dot.
44+
}
45+
46+
/// With start space without dot.
47+
function() {
48+
// Without start space without dot.
49+
}
50+
}
51+
/* Prefer format comments
52+
like sentences. */
53+
```

website/docs/rules/overview.md

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ Rules configuration is [described here](../getting-started/configuration#configu
6767

6868
Checks that double literals should begin with `0.` instead of just `.`, and should not end with a trailing `0`.
6969

70+
- [format-comment](./common/format-comment.md) &nbsp; ![Has auto-fix](https://img.shields.io/badge/-has%20auto--fix-success)
71+
72+
Prefer format comments like sentences.
73+
7074
- [member-ordering](./common/member-ordering.md) &nbsp; [![Configurable](https://img.shields.io/badge/-configurable-informational)](./common/member-ordering.md#config-example)
7175

7276
Enforces ordering for a class members.

0 commit comments

Comments
 (0)