| // 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; |
| } |
| } |