|  | // 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.isEof) { | 
|  | 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; | 
|  | var 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) { | 
|  | 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. | 
|  | !Todo.TODO_REGEX.hasMatch(nextComment.lexeme); | 
|  | 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; | 
|  | } | 
|  | } | 
|  |  | 
|  | _errorReporter.atOffset( | 
|  | offset: offset, | 
|  | length: end - offset, | 
|  | errorCode: Todo.forKind(todoKind), | 
|  | arguments: [todoText], | 
|  | ); | 
|  | } | 
|  |  | 
|  | return nextComment; | 
|  | } | 
|  | } |