blob: e6a4df37f39790827408732714681dd64b3f8c04 [file] [log] [blame]
// 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:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/error/codes.dart';
/// Instances of the class `ToDoFinder` find to-do comments in Dart code.
class TodoFinder {
/// The error reporter by which to-do comments will be reported.
final ErrorReporter _errorReporter;
/// 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._errorReporter);
/// 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.type != TokenType.EOF) {
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;
}
}
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) {
Iterable<RegExpMatch> matches =
Todo.TODO_REGEX.allMatches(commentToken.lexeme);
// 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;
final commentLocation = lineInfo.getLocation(commentToken.offset);
for (RegExpMatch match in matches) {
int offset = commentToken.offset + match.start + match.group(1)!.length;
int column =
commentLocation.columnNumber + match.start + match.group(1)!.length;
String todoText = match.group(2)!;
String todoKind = match.namedGroup('kind1') ?? match.namedGroup('kind2')!;
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) {
final nextCommentLocation = lineInfo.getLocation(nextComment.offset);
final columnOfFirstNoneMarkerOrWhitespace =
nextCommentLocation.columnNumber +
nextComment.lexeme.indexOf(_nonWhitespaceOrCommentMarker);
final isContinuation =
nextComment.type == TokenType.SINGLE_LINE_COMMENT &&
// 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.
!Todo.TODO_REGEX.hasMatch(nextComment.lexeme);
if (!isContinuation) {
break;
}
// Track the end of the continuation for the diagnostic range.
end = nextComment.end;
final lexemeTextOffset = columnOfFirstNoneMarkerOrWhitespace -
nextCommentLocation.columnNumber;
final continuationText =
nextComment.lexeme.substring(lexemeTextOffset).trimRight();
todoText = '$todoText $continuationText';
nextComment = nextComment.next;
}
}
_errorReporter.reportErrorForOffset(
Todo.forKind(todoKind), offset, end - offset, [todoText]);
}
return nextComment;
}
}