blob: d48a3cae664f2e136730aaa2e277773bdb2abc01 [file] [log] [blame]
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import '../analyzer.dart';
const _desc = r'Only reference in scope identifiers in doc comments.';
const _details = r'''
**DO** reference only in scope identifiers in doc comments.
If you surround things like variable, method, or type names in square brackets,
then [`dart doc`](https://dart.dev/tools/dart-doc) will look up the name and
link to its docs. For this all to work, ensure that all identifiers in docs
wrapped in brackets are in scope.
For example, assuming `outOfScopeId` is out of scope:
**BAD:**
```dart
/// Return true if [value] is larger than [outOfScopeId].
bool isOutOfRange(int value) { ... }
```
**GOOD:**
```dart
/// Return the larger of [a] or [b].
int max_int(int a, int b) { ... }
```
Note that the square bracket comment format is designed to allow comments to
refer to declarations using a fairly natural format but does not allow
*arbitrary expressions*. In particular, code references within square brackets
can consist of either
- a single identifier where the identifier is any identifier in scope for the
comment (see the spec for what is in scope in doc comments),
- two identifiers separated by a period where the first identifier is the name
of a class that is in scope and the second is the name of a member declared in
the class,
- a single identifier followed by a pair of parentheses where the identifier is
the name of a class that is in scope (used to refer to the unnamed constructor
for the class), or
- two identifiers separated by a period and followed by a pair of parentheses
where the first identifier is the name of a class that is in scope and the
second is the name of a named constructor (not strictly necessary, but allowed
for consistency).
**Known limitations**
The `comment_references` linter rule aligns with the Dart analyzer's notion of
comment references, which is separate from Dartdoc's notion of comment
references. The linter rule may report comment references which cannot be
resolved by the analyzer, but which Dartdoc can. See
[dartdoc#1142](https://github.com/dart-lang/linter/issues/1142) for more
information.
''';
class CommentReferences extends LintRule {
static const LintCode code = LintCode(
'comment_references', "The referenced name isn't visible in scope.",
correctionMessage: 'Try adding an import for the referenced name.');
CommentReferences()
: super(
name: 'comment_references',
description: _desc,
details: _details,
group: Group.errors);
@override
LintCode get lintCode => code;
@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this);
registry.addComment(this, visitor);
registry.addCommentReference(this, visitor);
}
}
class _Visitor extends SimpleAstVisitor<void> {
final LintRule rule;
final links = <String>[];
_Visitor(this.rule);
@override
void visitComment(Comment node) {
// clear links of previous comments
links.clear();
// Check for keywords that are not treated as references by the parser
// but should be flagged by the linter.
// Note that no special care is taken to handle embedded code blocks.
for (var token in node.tokens) {
if (!token.isSynthetic) {
var comment = token.lexeme;
var leftIndex = comment.indexOf('[');
while (leftIndex >= 0) {
var rightIndex = comment.indexOf(']', leftIndex);
if (rightIndex >= 0) {
var reference = comment.substring(leftIndex + 1, rightIndex);
if (_isParserSpecialCase(reference)) {
var nameOffset = token.offset + leftIndex + 1;
rule.reporter.atOffset(
offset: nameOffset,
length: reference.length,
errorCode: rule.lintCode,
);
}
if (rightIndex + 1 < comment.length &&
comment[rightIndex + 1] == ':') {
links.add(reference);
}
}
leftIndex = rightIndex < 0 ? -1 : comment.indexOf('[', rightIndex);
}
}
}
}
@override
void visitCommentReference(CommentReference node) {
var expression = node.expression;
if (expression.isSynthetic) return;
if (expression is Identifier &&
expression.staticElement == null &&
!links.contains(expression.name)) {
rule.reportLint(expression);
}
}
bool _isParserSpecialCase(String reference) =>
reference == 'this' ||
reference == 'null' ||
reference == 'true' ||
reference == 'false';
}