blob: 455667ad151502581bef3fbe4dc8b0bcef184893 [file]
// Copyright (c) 2019, 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:_fe_analyzer_shared/src/scanner/characters.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/error/listener.dart';
/// Instances of the class `ToDoFinder` find to-do comments in Dart code.
class TodoFinder {
/// The diagnostic reporter by which to-do comments will be reported.
final DiagnosticReporter _diagnosticReporter;
/// A regex for whitespace and comment markers to be removed from the text
/// of multiline TODOs in multiline comments.
final RegExp _commentNewlineAndMarker = RegExp('\\s*\\n\\s*\\*\\s*');
/// A regex for any character that is not a comment marker `/` or whitespace
/// used for finding the first "real" character of a comment to compare its
/// indentation for wrapped todos.
final RegExp _nonWhitespaceOrCommentMarker = RegExp('[^/ ]');
/// Initialize a newly created to-do finder to report to-do comments to the
/// given reporter.
///
/// @param errorReporter the error reporter by which to-do comments will be
/// reported
TodoFinder(this._diagnosticReporter);
/// Search the comments in the given compilation unit for to-do comments and
/// report an error for each.
///
/// @param unit the compilation unit containing the to-do comments
void findIn(CompilationUnit unit) {
_gatherTodoComments(unit.beginToken, unit.lineInfo);
}
/// Search the comment tokens reachable from the given token and create errors
/// for each to-do comment.
///
/// @param token the head of the list of tokens being searched
void _gatherTodoComments(Token? token, LineInfo lineInfo) {
while (token != null && (!token.isEof || token.precedingComments != null)) {
Token? commentToken = token.precedingComments;
while (commentToken != null) {
if (commentToken.type == TokenType.SINGLE_LINE_COMMENT ||
commentToken.type == TokenType.MULTI_LINE_COMMENT) {
commentToken = _scrapeTodoComment(commentToken, lineInfo);
} else {
commentToken = commentToken.next;
}
}
if (token.next == token) {
break;
}
token = token.next;
}
}
/// Look for user defined tasks in comments starting [commentToken] and convert
/// them into info level analysis issues.
///
/// Subsequent comments that are indented with an additional space are
/// considered continuations and will be included in a single analysis issue.
///
/// Returns the next comment token to begin searching from (skipping over
/// any continuations).
Token? _scrapeTodoComment(Token commentToken, LineInfo lineInfo) {
// Track the comment that will be returned for looking for the next `todo`.
// This will be moved along if additional comments are consumed by multiline
// TODOs.
var nextComment = commentToken.next;
CharacterLocation? commentLocation;
_TodoFinder todoFinder = _TodoFinder(commentToken.lexeme);
while (todoFinder.moveNext()) {
int matchOffset = todoFinder.offset;
String todoKind = todoFinder.todoKind;
String todoText = todoFinder.todoText;
commentLocation ??= lineInfo.getLocation(commentToken.offset);
int offset = commentToken.offset + matchOffset;
int column = commentLocation.columnNumber + matchOffset;
int end = offset + todoText.length;
if (commentToken.type == TokenType.MULTI_LINE_COMMENT) {
// Remove any `*/` and trim any trailing whitespace.
if (todoText.endsWith('*/')) {
todoText = todoText.substring(0, todoText.length - 2).trimRight();
end = offset + todoText.length;
}
// Replace out whitespace/comment markers to unwrap multiple lines.
// Do not reset length after this, as length must include all characters.
todoText = todoText.replaceAll(_commentNewlineAndMarker, ' ');
} else if (commentToken.type == TokenType.SINGLE_LINE_COMMENT) {
// Append any indented lines onto the end.
var line = commentLocation.lineNumber;
while (nextComment != null) {
var nextCommentLocation = lineInfo.getLocation(nextComment.offset);
var columnOfFirstNoneMarkerOrWhitespace =
nextCommentLocation.columnNumber +
nextComment.lexeme.indexOf(_nonWhitespaceOrCommentMarker);
var isContinuation =
nextComment.type == TokenType.SINGLE_LINE_COMMENT &&
// Don't consider Dartdocs that follow.
!nextComment.lexeme.startsWith('///') &&
// Only consider TODOs on the very next line.
nextCommentLocation.lineNumber == line++ + 1 &&
// Only consider comment tokens starting at the same column.
nextCommentLocation.columnNumber ==
commentLocation.columnNumber &&
// And indented more than the original 'todo' text.
columnOfFirstNoneMarkerOrWhitespace == column + 1 &&
// And not their own todos.
!_TodoFinder(nextComment.lexeme).moveNext();
if (!isContinuation) {
break;
}
// Track the end of the continuation for the diagnostic range.
end = nextComment.end;
var lexemeTextOffset =
columnOfFirstNoneMarkerOrWhitespace -
nextCommentLocation.columnNumber;
var continuationText = nextComment.lexeme
.substring(lexemeTextOffset)
.trimRight();
todoText = '$todoText $continuationText';
nextComment = nextComment.next;
}
}
_diagnosticReporter.report(
Todo.forKind(todoKind)
.withArguments(message: todoText)
.atOffset(offset: offset, length: end - offset),
);
}
return nextComment;
}
}
class _TodoFinder {
final String s;
// We start at 1 to allow for the char before the first find to be \s, / or *.
int _startAt = 1;
int? _offset;
String? _todoText;
String? _todoKind;
_TodoFinder(this.s);
int get offset => _offset!;
String get todoKind => _todoKind!;
String get todoText => _todoText!;
/// This matches the two common Dart task styles
///
/// * `TODO`:
/// * `TODO`(username):
///
/// As well as
/// * `TODO`
///
/// But not
/// * `todo`
/// * `TODOS`
///
/// It also supports wrapped TODOs where the next line is indented by a space:
///
/// /**
/// * `TODO`(username): This line is
/// * wrapped onto the next line
/// */
bool moveNext() {
// We stop 3 before so we can check the next 3 chars without checking
// lengths.
var end = s.length - 3;
for (int i = _startAt; i < end; i++) {
int char = s.codeUnitAt(i);
if (char >= $A && char <= $Z) {
if (char == $T &&
s.codeUnitAt(i + 1) == $O &&
s.codeUnitAt(i + 2) == $D &&
s.codeUnitAt(i + 3) == $O) {
/// Found `TODO`
if (_check(i, i + 4)) return true;
} else if (char == $H &&
s.codeUnitAt(i + 1) == $A &&
s.codeUnitAt(i + 2) == $C &&
s.codeUnitAt(i + 3) == $K) {
/// Found `HACK`
if (_check(i, i + 4)) return true;
} else if (char == $F &&
s.length > i + 4 &&
s.codeUnitAt(i + 1) == $I &&
s.codeUnitAt(i + 2) == $X &&
s.codeUnitAt(i + 3) == $M &&
s.codeUnitAt(i + 4) == $E) {
/// Found `FIXME`
if (_check(i, i + 5)) return true;
} else if (char == $U &&
s.length > i + 5 &&
s.codeUnitAt(i + 1) == $N &&
s.codeUnitAt(i + 2) == $D &&
s.codeUnitAt(i + 3) == $O &&
s.codeUnitAt(i + 4) == $N &&
s.codeUnitAt(i + 5) == $E) {
/// Found `UNDONE`
if (_check(i, i + 6)) return true;
}
}
}
return false;
}
bool _check(int from, int to) {
int charBefore = s.codeUnitAt(from - 1);
if (charBefore != $SPACE &&
charBefore != $TAB &&
charBefore != $LF &&
charBefore != $CR &&
charBefore != $STAR &&
charBefore != $SLASH) {
// Doesn't start with \s, / or *.
return false;
}
if (s.length == to) {
// Line ends with this.
_match(from, to, to);
return true;
}
int charAfter = s.codeUnitAt(to);
// Not allowed to match [A-Za-z0-9_].
if (charAfter >= $0 && charAfter <= $9) return false;
if (charAfter >= $a && charAfter <= $z) return false;
if (charAfter >= $A && charAfter <= $Z) return false;
if (charAfter == $_) return false;
int scanFrom = to + 1;
while (true) {
int? foundLinebreakAt;
for (int i = scanFrom; i < s.length; i++) {
int char = s.codeUnitAt(i);
if (char == $CR || char == $LF) {
foundLinebreakAt = i;
break;
}
}
if (foundLinebreakAt == null) {
// No line breaks - we match until the end.
_match(from, to, s.length);
return true;
}
// Possibly match the next line too.
int includeUntil = foundLinebreakAt;
int nextLineAt = foundLinebreakAt;
if (s.codeUnitAt(nextLineAt) == $CR && s.length > nextLineAt + 1) {
// Allow \n after \r.
if (s.codeUnitAt(nextLineAt + 1) == $LF) {
nextLineAt++;
}
}
int i = nextLineAt + 1;
while (i < s.length) {
int char = s.codeUnitAt(i);
if (char != $SPACE && char != $TAB && char != $LF && char != $CR) {
break;
}
i++;
}
if (!(s.length > i + 2 &&
s.codeUnitAt(i) == $STAR &&
s.codeUnitAt(i + 1) == $SPACE &&
s.codeUnitAt(i + 2) == $SPACE)) {
// This line isn't some number of whitespace, then a * then 2 spaces.
// We don't include it.
_match(from, to, includeUntil);
return true;
}
// Include this line too.
scanFrom = i + 3;
}
}
void _match(int from, int kindTo, int finalTo) {
_offset = from;
_todoText = s.substring(from, finalTo);
_todoKind = s.substring(from, kindTo);
_startAt = finalTo;
}
}