/******************************************************************************* * Copyright (c) 2007, 2009 Google, Inc and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Sergey Prigogin (Google) - initial API and implementation *******************************************************************************/ package org.eclipse.cdt.internal.core.pdom.indexer; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import org.eclipse.cdt.core.dom.ast.IASTComment; import org.eclipse.cdt.core.dom.ast.IASTFileLocation; import org.eclipse.cdt.internal.core.CharOperation; public class TodoTaskParser { private static final Task[] EMPTY_TASK_ARRAY = new Task[0]; private final char[][] tags; private final int[] priorities; private final boolean isTaskCaseSensitive; private final int[] order; public TodoTaskParser(char[][] taskTags, int[] taskPriorities, boolean isTaskCaseSensitive) { assert taskPriorities.length == taskTags.length; this.tags = taskTags; this.priorities = taskPriorities; this.isTaskCaseSensitive = isTaskCaseSensitive; // Calculate task checking order that gives preference to the longest matching tag. this.order = new int[taskTags.length]; for (int i = 0; i < order.length; i++) { order[i] = i; } // Sort order array in reverse order of tag lengths. // Shell sort algorithm from http://en.wikipedia.org/wiki/Shell_sort for (int inc = order.length / 2; inc > 0; inc /= 2) { for (int i = inc; i < order.length; i++) { for (int j = i; j >= inc && taskTags[order[j - inc]].length < taskTags[order[j]].length; j -= inc) { int temp = order[j]; order[j] = order[j - inc]; order[j - inc] = temp; } } } } public Task[] parse(IASTComment[] comments) { HashSet<String> locKeys= new HashSet<String>(); List<Task> tasks = new ArrayList<Task>(); for (IASTComment comment : comments) { IASTFileLocation location = comment.getFileLocation(); if (location != null) { // be defensive, bug 213307 final String fileName = location.getFileName(); final int nodeOffset = location.getNodeOffset(); final String key= fileName + ':' + nodeOffset; // full indexer can yield duplicate comments, make sure to handle each comment only once (bug 287181) if (locKeys.add(key)) { parse(comment.getComment(), fileName, nodeOffset, location.getStartingLineNumber(), tasks); } } } if (tasks.isEmpty()) { return EMPTY_TASK_ARRAY; } return tasks.toArray(new Task[tasks.size()]); } private void parse(char[] comment, String filename, int offset, int lineNumber, List<Task> tasks) { int commentLength = comment.length; int foundTaskIndex = tasks.size(); char previous = comment[1]; // Should be '*' or '/' for (int i = 2; i < commentLength; i++) { char[] tag = null; nextTag : for (int j = 0; j < order.length; j++) { int itag = order[j]; tag = tags[itag]; int tagLength = tag.length; if (tagLength == 0 || i + tagLength > commentLength) continue nextTag; // Ensure tag is not leaded by a letter if the tag starts with a letter. if (isIdentifierStart(tag[0]) && isIdentifierPart(previous)) { continue nextTag; } for (int t = 0; t < tagLength; t++) { int x = i + t; if (x >= commentLength) continue nextTag; char sc = comment[x]; char tc = tag[t]; if (sc != tc) { // case sensitive check if (isTaskCaseSensitive || Character.toLowerCase(sc) != Character.toLowerCase(tc)) { // case insensitive check continue nextTag; } } } // Ensure tag is not followed by a letter if the tag ends with a letter. if (i + tagLength < commentLength && isIdentifierPart(comment[i + tagLength - 1]) && isIdentifierPart(comment[i + tagLength])) { continue nextTag; } Task task = new Task(filename, i, i + tagLength, lineNumber, String.valueOf(tag), "", priorities[itag]); //$NON-NLS-1$ tasks.add(task); i += tagLength - 1; // Will be incremented when looping break nextTag; } previous = comment[i]; } boolean containsEmptyTask = false; for (int i = foundTaskIndex; i < tasks.size(); i++) { Task task = tasks.get(i); // Retrieve message start and end positions int msgStart = task.start + task.tag.length(); int maxValue = i + 1 < tasks.size() ? tasks.get(i + 1).start : commentLength; // At most beginning of next task if (maxValue < msgStart) { maxValue = msgStart; // Would only occur if tag is before EOF. } int end = -1; char c; for (int j = msgStart; j < maxValue; j++) { if ((c = comment[j]) == '\n' || c == '\r') { end = j; break; } } if (end == -1) { for (int j = maxValue; --j >= msgStart;) { if ((c = comment[j]) == '*') { end = j; break; } } if (end == -1) { end = maxValue; } } // Trim the message while (msgStart < end && CharOperation.isWhitespace(comment[end - 1])) { end--; } while (msgStart < end && (CharOperation.isWhitespace(comment[msgStart]) || comment[msgStart] == ':')) { msgStart++; } if (msgStart == end) { // If the description is empty, we might want to see if two tags // are not sharing the same message. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=110797 containsEmptyTask = true; continue; } // Update the end position of the task task.end = end; // Get the message source int messageLength = end - msgStart; task.message = String.valueOf(comment, msgStart, messageLength); } if (containsEmptyTask) { for (int i = foundTaskIndex; i < tasks.size(); i++) { Task task1 = tasks.get(i); if (task1.message.length() == 0) { for (int j = i + 1; j < tasks.size(); j++) { Task task2 = tasks.get(j); if (task2.message.length() != 0) { task1.message = task2.message; task1.end = task2.end; break; } } } } } // Add comment offset. for (int i = foundTaskIndex; i < tasks.size(); i++) { Task task = tasks.get(i); task.lineNumber += getLineOffset(comment, task.start); task.start += offset; task.end += offset; } } /** * Returns zero-based line number for a given character position. */ private static int getLineOffset(char[] buffer, int pos) { int count = 0; for (int i = 0; i < pos && i < buffer.length; i++) { if (buffer[i] == '\n') { count++; } } return count; } private static boolean isIdentifierStart(char c) { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' || Character.isUnicodeIdentifierPart(c); } private static boolean isIdentifierPart(char c) { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c == '_' || c >= '0' && c <= '9' || Character.isUnicodeIdentifierPart(c); } public static class Task { private String fileLocation; private int start; private int end; private int lineNumber; private String tag; private String message; private int priority; Task(String fileLocation, int start, int end, int lineNumber, String tag, String message, int priority) { this.fileLocation = fileLocation; this.start = start; this.end = end; this.lineNumber = lineNumber; this.tag = tag; this.message = message; this.priority = priority; } public String getFileLocation() { return fileLocation; } public int getStart() { return start; } public int getEnd() { return end; } public int getLineNumber() { return lineNumber; } public String getTag() { return tag; } public String getMessage() { return message; } public int getPriority() { return priority; } } }