Skip to content
This repository was archived by the owner on Nov 20, 2024. It is now read-only.

Commit ec373f4

Browse files
authored
New rule: dangling_library_doc_comments (#3796)
1 parent 8dee1bb commit ec373f4

File tree

5 files changed

+329
-0
lines changed

5 files changed

+329
-0
lines changed

example/all.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ linter:
6363
- constant_identifier_names
6464
- control_flow_in_finally
6565
- curly_braces_in_flow_control_structures
66+
- dangling_library_doc_comments
6667
- depend_on_referenced_packages
6768
- deprecated_consistency
6869
- diagnostic_describe_all_properties

lib/src/rules.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import 'rules/conditional_uri_does_not_exist.dart';
6565
import 'rules/constant_identifier_names.dart';
6666
import 'rules/control_flow_in_finally.dart';
6767
import 'rules/curly_braces_in_flow_control_structures.dart';
68+
import 'rules/dangling_library_doc_comments.dart';
6869
import 'rules/depend_on_referenced_packages.dart';
6970
import 'rules/deprecated_consistency.dart';
7071
import 'rules/diagnostic_describe_all_properties.dart';
@@ -285,6 +286,7 @@ void registerLintRules({bool inTestMode = false}) {
285286
..register(ConstantIdentifierNames())
286287
..register(ControlFlowInFinally())
287288
..register(CurlyBracesInFlowControlStructures())
289+
..register(DanglingLibraryDocComments())
288290
..register(DependOnReferencedPackages())
289291
..register(DeprecatedConsistency())
290292
..register(DiagnosticsDescribeAllProperties())
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analyzer/dart/ast/ast.dart';
6+
import 'package:analyzer/dart/ast/token.dart';
7+
import 'package:analyzer/dart/ast/visitor.dart';
8+
9+
import '../analyzer.dart';
10+
11+
const _desc = r'Attach library doc comments to library directives.';
12+
13+
const _details = r'''
14+
Attach library doc comments (with `///`) to library directives, rather than
15+
leaving them dangling near the top of a library.
16+
17+
**BAD:**
18+
```dart
19+
/// This is a great library.
20+
import 'package:math';
21+
```
22+
23+
```dart
24+
/// This is a great library.
25+
26+
class C {}
27+
```
28+
29+
**GOOD:**
30+
```dart
31+
/// This is a great library.
32+
library;
33+
34+
import 'package:math';
35+
36+
class C {}
37+
```
38+
39+
**NOTE:** An unnamed library, like `library;` above, is only supported in Dart
40+
2.19 and later. Code which might run in earlier versions of Dart will need to
41+
provide a name in the `library` directive.
42+
''';
43+
44+
class DanglingLibraryDocComments extends LintRule {
45+
static const LintCode code = LintCode(
46+
'dangling_library_doc_comments', 'Dangling library doc comment.',
47+
correctionMessage:
48+
"Add a 'library' directive after the library comment.");
49+
50+
DanglingLibraryDocComments()
51+
: super(
52+
name: 'dangling_library_doc_comments',
53+
description: _desc,
54+
details: _details,
55+
group: Group.style);
56+
57+
@override
58+
LintCode get lintCode => code;
59+
60+
@override
61+
void registerNodeProcessors(
62+
NodeLintRegistry registry, LinterContext context) {
63+
var visitor = _Visitor(this);
64+
registry.addCompilationUnit(this, visitor);
65+
}
66+
}
67+
68+
class _Visitor extends SimpleAstVisitor<void> {
69+
final DanglingLibraryDocComments rule;
70+
71+
_Visitor(this.rule);
72+
73+
@override
74+
void visitCompilationUnit(CompilationUnit node) {
75+
if (node.directives.isNotEmpty) {
76+
// Only consider a doc comment on the first directive. Doc comments on
77+
// other directives do not have the appearance of documenting the library.
78+
var firstDirective = node.directives.first;
79+
if (firstDirective is LibraryDirective) {
80+
// Given the presense of library directive, don't worry about later doc
81+
// comments in the library.
82+
return;
83+
}
84+
if (firstDirective is PartOfDirective) {
85+
// Don't worry about possible "library doc comments" in a part.
86+
return;
87+
}
88+
89+
var docComment = firstDirective.documentationComment;
90+
if (docComment != null) {
91+
rule.reportLintForToken(docComment.beginToken);
92+
return;
93+
}
94+
95+
return;
96+
}
97+
98+
if (node.declarations.isEmpty) {
99+
// Without any declarations, we only need to check for a doc comment as
100+
// the last thing in a file.
101+
Token? endComment = node.endToken.precedingComments;
102+
while (endComment is CommentToken) {
103+
if (endComment is DocumentationCommentToken) {
104+
rule.reportLintForToken(endComment);
105+
}
106+
endComment = endComment.next;
107+
}
108+
return;
109+
}
110+
111+
var firstDeclaration = node.declarations.first;
112+
var docComment = firstDeclaration.documentationComment;
113+
if (docComment == null) {
114+
return;
115+
}
116+
var lineInfo = node.lineInfo;
117+
118+
if (docComment.tokens.length > 1) {
119+
for (var i = 0; i < docComment.tokens.length - 1; i++) {
120+
var commentToken = docComment.tokens[i];
121+
var followingCommentToken = docComment.tokens[i + 1];
122+
var commentEndLine = lineInfo.getLocation(commentToken.end).lineNumber;
123+
var followingCommentLine =
124+
lineInfo.getLocation(followingCommentToken.offset).lineNumber;
125+
if (followingCommentLine > commentEndLine + 1) {
126+
// There is a blank line within the declaration's doc comments.
127+
rule.reportLintForToken(commentToken);
128+
return;
129+
}
130+
}
131+
}
132+
133+
// We must walk through the comments following the doc comment, tracking
134+
// pairs of consecutive comments so as to check whether any two are
135+
// separated by a blank line.
136+
var commentToken = docComment.endToken;
137+
var followingCommentToken = commentToken.next;
138+
while (followingCommentToken != null) {
139+
// Any blank line between the doc comment and following comments makes
140+
// the doc comment look dangling.
141+
var commentEndLine = lineInfo.getLocation(commentToken.end).lineNumber;
142+
var followingCommentLine =
143+
lineInfo.getLocation(followingCommentToken.offset).lineNumber;
144+
if (followingCommentLine > commentEndLine + 1) {
145+
// There is a blank line between the declaration's doc comment and the
146+
// declaration.
147+
rule.reportLint(docComment);
148+
return;
149+
}
150+
151+
commentToken = followingCommentToken;
152+
followingCommentToken = followingCommentToken.next;
153+
}
154+
155+
var commentEndLine = lineInfo.getLocation(commentToken.end).lineNumber;
156+
// The syntactic entity to which a comment is "attached" is the
157+
// [Comment]'s `parent`, not it's `endToken`'s `next` [Token].
158+
var tokenAfterDocComment =
159+
(docComment.endToken as DocumentationCommentToken).parent;
160+
if (tokenAfterDocComment == null) {
161+
// We shouldn't get here as the doc comment is attached to
162+
// [firstDeclaration].
163+
return;
164+
}
165+
var declarationStartLine =
166+
lineInfo.getLocation(tokenAfterDocComment.offset).lineNumber;
167+
if (declarationStartLine > commentEndLine + 1) {
168+
// There is a blank line between the declaration's doc comment and the
169+
// declaration.
170+
rule.reportLint(docComment);
171+
}
172+
}
173+
}

test/rules/all.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import 'collection_methods_unrelated_type_test.dart'
3030
import 'conditional_uri_does_not_exist_test.dart'
3131
as conditional_uri_does_not_exist;
3232
import 'constant_identifier_names_test.dart' as constant_identifier_names;
33+
import 'dangling_library_doc_comments_test.dart'
34+
as dangling_library_doc_comments;
3335
import 'deprecated_consistency_test.dart' as deprecated_consistency;
3436
import 'discarded_futures_test.dart' as discarded_futures;
3537
import 'file_names_test.dart' as file_names;
@@ -108,6 +110,7 @@ void main() {
108110
collection_methods_unrelated_type.main();
109111
conditional_uri_does_not_exist.main();
110112
constant_identifier_names.main();
113+
dangling_library_doc_comments.main();
111114
deprecated_consistency.main();
112115
discarded_futures.main();
113116
file_names.main();
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:test_reflective_loader/test_reflective_loader.dart';
6+
7+
import '../rule_test_support.dart';
8+
9+
main() {
10+
defineReflectiveSuite(() {
11+
defineReflectiveTests(DanglingLibraryDocCommentsTest);
12+
});
13+
}
14+
15+
@reflectiveTest
16+
class DanglingLibraryDocCommentsTest extends LintRuleTest {
17+
@override
18+
String get lintRule => 'dangling_library_doc_comments';
19+
20+
test_docComment_aboveDeclaration() async {
21+
await assertDiagnostics(
22+
r'''
23+
/// Doc comment.
24+
25+
class C {}
26+
''',
27+
[lint(0, 16)],
28+
);
29+
}
30+
31+
test_docComment_aboveDeclaration_endingInReference() async {
32+
await assertNoDiagnostics(r'''
33+
/// Doc comment [C]
34+
class C {}
35+
''');
36+
}
37+
38+
test_docComment_aboveDeclarationWithAnnotation() async {
39+
await assertNoDiagnostics(r'''
40+
/// Doc comment.
41+
@deprecated
42+
class C {}
43+
''');
44+
}
45+
46+
test_docComment_aboveDeclarationWithDocComment() async {
47+
await assertDiagnostics(
48+
r'''
49+
/// Library comment.
50+
51+
/// Class comment.
52+
class C {}
53+
''',
54+
[lint(0, 20)],
55+
);
56+
}
57+
58+
test_docComment_aboveDeclarationWithOtherComment1() async {
59+
await assertNoDiagnostics(r'''
60+
/// Doc comment.
61+
// Comment.
62+
class C {}
63+
''');
64+
}
65+
66+
test_docComment_aboveDeclarationWithOtherComment2() async {
67+
await assertDiagnostics(
68+
r'''
69+
/// Doc comment.
70+
71+
// Comment.
72+
class C {}
73+
''',
74+
[lint(0, 16)],
75+
);
76+
}
77+
78+
test_docComment_aboveDeclarationWithOtherComment3() async {
79+
await assertDiagnostics(
80+
r'''
81+
/// Doc comment.
82+
// Comment.
83+
84+
class C {}
85+
''',
86+
[lint(0, 16)],
87+
);
88+
}
89+
90+
test_docComment_aboveDeclarationWithOtherComment4() async {
91+
await assertNoDiagnostics(r'''
92+
/// Doc comment.
93+
// Comment.
94+
/* Comment 2. */
95+
class C {}
96+
''');
97+
}
98+
99+
test_docComment_atEndOfFile() async {
100+
await assertDiagnostics(
101+
r'''
102+
/// Doc comment with [int].
103+
''',
104+
[lint(0, 27)],
105+
);
106+
}
107+
108+
test_docComment_atEndOfFile_precededByComment() async {
109+
await assertDiagnostics(
110+
r'''
111+
// Copyright something.
112+
113+
/// Doc comment with [int].
114+
''',
115+
[lint(25, 27)],
116+
);
117+
}
118+
119+
test_docComment_attachedToDeclaration() async {
120+
await assertNoDiagnostics(r'''
121+
/// Doc comment.
122+
class C {}
123+
''');
124+
}
125+
126+
test_docComment_onFirstDirective() async {
127+
await assertDiagnostics(
128+
r'''
129+
/// Doc comment.
130+
export 'dart:math';
131+
''',
132+
[lint(0, 16)],
133+
);
134+
}
135+
136+
test_docComment_onLaterDirective() async {
137+
await assertNoDiagnostics(r'''
138+
export 'dart:math';
139+
/// Doc comment for some reason.
140+
export 'dart:io';
141+
''');
142+
}
143+
144+
test_docComment_onLibraryDirective() async {
145+
await assertNoDiagnostics(r'''
146+
/// Doc comment.
147+
library l;
148+
''');
149+
}
150+
}

0 commit comments

Comments
 (0)