blob: d68eea817d81d0ee24b9fa0c9fc01822c3c58930 [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';
import '../linter_lint_codes.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 identifiers like variable, method, or type names in square
brackets, then tools like your IDE and
[`dart doc`](https://dart.dev/tools/dart-doc) can link to them. For this to
work, ensure that all identifiers in docs wrapped in brackets are in scope.
For example, assuming `outOfScopeId` is out of scope:
**BAD:**
```dart
/// Returns whether [value] is larger than [outOfScopeId].
bool isOutOfRange(int value) { ... }
```
**GOOD:**
```dart
/// Returns 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 any of the following:
- A bare identifier which is in-scope for the comment (see the spec for what is
"in-scope" in doc comments). Examples include `[print]` and `[Future]`.
- Two identifiers separated by a period (a "prefixed identifier"), such that the
first identifier acts as a namespacing identifier, such as a class property
name or method name prefixed by the containing class's name, or a top-level
identifier prefixed by an import prefix. Examples include `[Future.new]` (an
unnamed constructor), `[Future.value]` (a constructor), `[Future.wait]` (a
static method), `[Future.then]` (an instance method), `[math.max]` (given that
'dart:async' is imported with a `max` prefix).
- A prefixed identifier followed by a pair of parentheses, used to disambiguate
named constructors from instance members (whose names are allowed to collide).
Examples include `[Future.value()]`.
- Three identifiers separated by two periods, such that the first identifier is
an import prefix name, the second identifier is a top-level element like a
class or an extension, and the third identifier is a member of that top-level
element. Examples include `[async.Future.then]` (given that 'dart:async' is
imported with an `async` prefix).
**Known limitations**
The `comment_references` lint rule aligns with the Dart analyzer's notion of
comment references, which is occasionally distinct from Dartdoc's notion of
comment references. The lint rule may report comment references which Dartdoc
can resolve, even though the analyzer cannot. See
[linter#1142](https://github.com/dart-lang/linter/issues/1142) for more
information.
''';
class CommentReferences extends LintRule {
CommentReferences()
: super(
name: 'comment_references',
description: _desc,
details: _details,
);
@override
LintCode get lintCode => LinterLintCode.comment_references;
@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this);
registry.addComment(this, visitor);
registry.addCommentReference(this, visitor);
}
}
class _Visitor extends SimpleAstVisitor<void> {
static final _commentStartPattern = RegExp(r'^///+\s*$');
final LintRule rule;
/// Recognized Markdown link references (see
/// https://spec.commonmark.org/0.31.2/#link-reference-definitions).
final linkReferences = <String>[];
_Visitor(this.rule);
@override
void visitComment(Comment node) {
// Clear any link references of the previous comment.
linkReferences.clear();
// Check for keywords that are not treated as references by the parser
// but should be reported.
// Note that no special care is taken to handle embedded code blocks.
// TODO(srawlins): Skip over code blocks, made available via
// `Comment.codeBlocks`.
for (var token in node.tokens) {
if (token.isSynthetic) continue;
var comment = token.lexeme;
var referenceIndices = comment.referenceIndices(0);
if (referenceIndices == null) continue;
var (leftIndex, rightIndex) = referenceIndices;
var prefix = comment.substring(0, leftIndex);
if (_commentStartPattern.hasMatch(prefix)) {
// Check for a Markdown [link reference
// definition](https://spec.commonmark.org/0.31.2/#link-reference-definitions).
var reference = comment.substring(leftIndex + 1, rightIndex);
if (rightIndex + 1 < comment.length && comment[rightIndex + 1] == ':') {
linkReferences.add(reference);
}
}
while (referenceIndices != null) {
(leftIndex, rightIndex) = referenceIndices;
var reference = comment.substring(leftIndex + 1, rightIndex);
if (_isParserSpecialCase(reference)) {
var nameOffset = token.offset + leftIndex + 1;
rule.reportLintForOffset(nameOffset, reference.length);
}
referenceIndices = comment.referenceIndices(rightIndex);
}
}
}
@override
void visitCommentReference(CommentReference node) {
if (node.isSynthetic) return;
var expression = node.expression;
if (expression.isSynthetic) return;
if (expression is Identifier &&
expression.staticElement == null &&
!linkReferences.contains(expression.name)) {
rule.reportLint(expression);
} else if (expression is PropertyAccess &&
expression.propertyName.staticElement == null) {
var target = expression.target;
if (target is PrefixedIdentifier) {
var name = '${target.name}.${expression.propertyName.name}';
if (!linkReferences.contains(name)) {
rule.reportLint(expression);
}
}
}
}
bool _isParserSpecialCase(String reference) =>
reference == 'this' ||
reference == 'null' ||
reference == 'true' ||
reference == 'false';
}
extension on String {
/// Returns the first indices of a left and right bracket, if a left bracket
/// is found before a right bracket in this [String], starting at [start], and
/// `null` otherwise.
(int, int)? referenceIndices(int start) {
var leftIndex = indexOf('[', start);
if (leftIndex < 0) return null;
var rightIndex = indexOf(']', leftIndex);
if (rightIndex < 0) return null;
return (leftIndex, rightIndex);
}
}