fix(scripts): count comments after regex literals in the comment ratchet (#4879)

* fix(scripts): count comments after regex literals in the comment ratchet

* fix(scripts): keep comments after JSX tags, ++/-- and wrapped division in the ratchet
This commit is contained in:
Miguel Ángel
2026-10-01 14:09:44 -07:00
committed by GitHub
parent 83035e08b8
commit fd105807c4
2 changed files with 71 additions and 1 deletions
+40 -1
View File
@@ -150,6 +150,45 @@ function stripStrings(line) {
return line.replace(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, (m) => " ".repeat(m.length));
}
// Index just past the literal opened at `start`, or -1 when it does not close on this line.
function literalEnd(line, start, regex) {
const close = regex ? "/" : line[start];
let inClass = false;
for (let i = start + 1; i < line.length; i++) {
const ch = line[i];
if (ch === "\\") i++;
else if (regex && ch === "[") inClass = true;
else if (regex && ch === "]") inClass = false;
else if (ch === close && !inClass) return i + 1;
}
return -1;
}
/** stripStrings for C-like code, plus regex literals, so `/["'`]/` opens no string or template.
* A `/` after a value, `<`, `++` or `--` is division or a tag (`a / b`, `</p>`, `i++ / 2`).
* Stops at the first comment. */
function stripLiterals(line) {
let out = "";
for (let i = 0; i < line.length; ) {
const ch = line[i];
const comment = ch === "/" && (line[i + 1] === "/" || line[i + 1] === "*");
if (comment) return out + line.slice(i);
// ponytail: `return /re/` reads as division; add a keyword check if that ever hides a comment.
const regex = ch === "/" && !/(?:[\w$)\]<]|\+\+|--)$/.test(out.trimEnd());
let end = regex || `"'\``.includes(ch) ? literalEnd(line, i, regex) : -1;
// A regex never closes on a comment opener: this `/` is division, e.g. continued from the line above.
if (regex && (line[end] === "/" || line[end] === "*")) end = -1;
if (end === -1) {
out += ch;
i++;
} else {
out += " ".repeat(end - i);
i = end;
}
}
return out;
}
function scanCLike(lines) {
const out = [];
let inBlock = false;
@@ -177,7 +216,7 @@ function scanCLike(lines) {
continue;
}
// Complete literals are blanked, so a backtick left over opens a multi-line template.
const code = stripStrings(rest);
const code = stripLiterals(rest);
const marks = [
[code.indexOf("//"), "line"],
[code.indexOf("/*"), "block"],
+31
View File
@@ -32,6 +32,37 @@ test("a JSX comment counts as comment, and code does not", () => {
assert.equal(measure("const a = 1;\n", ".ts").comment, 0);
});
test("quotes inside a regex literal open no string, so later comments still count", () => {
const source = [
"const keys = text.match(/[\"'`]([^\"'`]+)[\"'`]/g);",
"// one",
"const half = (a + b) / 2; // two",
"/* three */",
"const n = total / count / 2;",
"// four",
].join("\n");
assert.equal(measure(source, ".ts").comment, 4);
});
test("a JSX closing tag's slash opens no regex", () => {
assert.equal(measure("const a = <p>x</p>; // c\n", ".tsx").comment, 1);
assert.equal(measure("const a = <a>x</a>{/* note */};\n", ".tsx").comment, 1);
assert.equal(measure("const a = <div></div> {/* note */};\n", ".tsx").comment, 1);
assert.equal(measure("const a = <p>x</p>, b = 'a/b'; // it's\n", ".tsx").comment, 1);
});
test("a slash after ++ or -- is division", () => {
assert.equal(measure("const a = i++ / 2; // c\n", ".ts").comment, 1);
assert.equal(measure("const a = i-- / 2; // c\n", ".ts").comment, 1);
assert.equal(measure("const a = i++ / 2 + 'a/b'; // it's\n", ".ts").comment, 1);
});
test("a line opening with division continued from the line above keeps its comment", () => {
assert.equal(measure("const a = total\n / 2; // c\n", ".ts").comment, 1);
assert.equal(measure("const a = total\n / 2; /* c */\n", ".ts").comment, 1);
assert.equal(measure("const a = ok &&\n /[\"'`]/.test(s); // c\n", ".ts").comment, 1);
});
// 14 physical lines: a bare opener, 12 of prose, a bare closer.
test("a docblock is measured from its opener to its closer", () => {
const body = Array.from({ length: 12 }, (_, i) => ` * line ${i + 1}`).join("\n");