fix(todo): only count real comment markers around TODO keywords (#1128)

The comment filter accepted `#`, `--`, `;` and `%` anywhere before the keyword, so `foo(); TODO`, `if (a % 2) TODO`, `i--; TODO`, URLs and strings produced items. Inline openers (// /* <!-- # -- {- (*) now count only at the line start or after whitespace; `*` `;` `%` and docstring quotes only at the line start. A TODO anywhere inside a comment still counts, as in IntelliJ. Lines the backend truncated keep the lenient rule.

Co-authored-by: qtvbwfn <[email protected]>
Цей коміт міститься в:
Qingтаqtvbwfn authored and GitHub committed 2026-11-04 08:56:00 +08:00
1 джерело e5718d41d8
коміт 893cbede9c
2 files changed
+62 -5

No files matched your search

+15 -5
Переглянути файл
@@ -71,10 +71,20 @@ export function buildTodoQuery(s: TodoSettings, scope: SearchScope = { kind: "pr
};
}
const COMMENT_MARKER = /(\/\/|\/\*|^\s*\*|#|--|<!--|;|%|\{-|\(\*|"""|''')/;
// Comment openers that count anywhere on the line, at its start or after whitespace: // /* <!-- # -- {- (*.
// The whitespace rule drops `//` in URLs, `#` in strings, `f(*)` and `a * b`. Left over without knowing
// the language: `"x # y"` in a string and a prefix `--y`.
// `*` `;` `%` `"""` `'''` are code characters too, so they count only at the line start (LINE_START_MARKER).
// IDEA's default pattern matches a TODO anywhere inside a comment, so the keyword need not lead it.
const INLINE_MARKER = /(?:^|\s)(?:\/\/|\/\*|<!--|#|--|\{-|\(\*)/;
const LINE_START_MARKER = /^\s*(?:\*|;|%|"""|''')/;
// A long line the backend truncated (ide-search line.rs, `textOffset > 0`) doesn't start at the real line
// start, so neither rule can apply: any marker counts, a false positive rather than a missed TODO.
const LENIENT_MARKER = /(\/\/|\/\*|<!--|#|--|\{-|\(\*|\*|;|%|"""|''')/;
export function looksLikeComment(prefix: string): boolean {
return COMMENT_MARKER.test(prefix);
export function looksLikeComment(prefix: string, truncated = false): boolean {
if (truncated) return LENIENT_MARKER.test(prefix);
return INLINE_MARKER.test(prefix) || LINE_START_MARKER.test(prefix);
}
function cleanText(text: string): string {
@@ -96,7 +106,7 @@ export function itemsFromMatches(fm: FileMatches, s: TodoSettings): TodoItem[] {
const matched = lm.text.slice(start, end);
const p = patternFor(s, matched);
if (!p) continue;
if (s.commentsOnly && !looksLikeComment(lm.text.slice(0, start))) continue;
if (s.commentsOnly && !looksLikeComment(lm.text.slice(0, start), lm.textOffset > 0)) continue;
out.push({
path: fm.path,
line: lm.line,
@@ -125,7 +135,7 @@ export function scanText(path: string, text: string, s: TodoSettings): TodoItem[
}
const p = patternFor(s, m[0]);
if (!p) continue;
if (s.commentsOnly && !looksLikeComment(text.slice(lineStart, idx))) continue;
if (s.commentsOnly && !looksLikeComment(text.slice(lineStart, idx), false)) continue;
out.push({ path, line, col: idx - lineStart, keyword: p.keyword.trim(), text: cleanText(m[0]) });
}
return out;
+47
Переглянути файл
@@ -12,6 +12,7 @@ import {
DEFAULT_TODO_SETTINGS,
groupTodos,
itemsFromMatches,
looksLikeComment,
mergeLive,
scanText,
todoRegexSource,
@@ -22,6 +23,52 @@ import { todoDecorations } from "./highlight";
const S = DEFAULT_TODO_SETTINGS;
describe("comment markers", () => {
const keep = [
"// TODO",
"/* TODO */",
" * TODO", // Javadoc continuation
"/** TODO: document this */", // doc comment led by the keyword
"# TODO", // line-start Python/Shell
"foo() # TODO", // trailing Python/Shell comment
"SELECT 1 -- TODO", // trailing SQL comment
"<!-- TODO -->",
" ; TODO", // line-start ini comment
" % TODO", // line-start LaTeX comment
'""" TODO', // Python docstring
"''' TODO",
"x = 1; // TODO", // marker after whitespace, not just line start
"x (* TODO *)", // OCaml trailing comment
"x {- TODO -}", // Haskell trailing comment
// IDEA's default pattern matches anywhere inside a comment, so mentions count too:
"/** 业务类别:WORK_NOTICE 工作通知,TODO 待办。 */", // documents an enum value
"// fixed the TODO bug",
"/* see the TODO list */",
];
const drop = [
"foo(); TODO", // statement separator
"if (a % 2) TODO", // modulo
"i--; TODO", // postfix decrement
'let s = "a#b"; TODO', // # inside a string, no preceding space
"x = a--b; TODO", // -- with no preceding space
"see http://example.com TODO", // // inside a URL
'"//"; TODO', // // inside a string
"f(*) TODO", // OCaml-style wildcard
"a * b; TODO", // infix multiplication
];
it.each(keep)("keeps %s", (line) => expect(looksLikeComment(`${line.slice(0, line.indexOf("TODO"))}`)).toBe(true));
it.each(drop)("drops %s", (line) => expect(looksLikeComment(`${line.slice(0, line.indexOf("TODO"))}`)).toBe(false));
it("falls back to the lenient rule on truncated lines", () => {
// A 600-char shell line truncated by the backend: the window starts
// mid-line, so the leading rule cannot apply — any marker counts.
expect(looksLikeComment("very long code # remainder ", true)).toBe(true);
expect(looksLikeComment("very long code % remainder ", true)).toBe(true);
expect(looksLikeComment("i-- ", true)).toBe(true);
});
});
describe("query", () => {
it("builds the keyword regex from enabled patterns", () => {
expect(todoRegexSource(S)).toBe("\\b(TODO|FIXME|HACK|XXX)\\b.*");