diff --git a/web-report/src-e2e/extractComments.test.ts b/web-report/src-e2e/extractComments.test.ts index b8f4548..51a0e3a 100644 --- a/web-report/src-e2e/extractComments.test.ts +++ b/web-report/src-e2e/extractComments.test.ts @@ -67,4 +67,152 @@ describe("extractComments", () => { const code = ["/* hello world */", "def f(): pass"].join("\n"); expect(extractComments(code)).toBe("hello world"); }); + + describe("comments embedded inside a statement are dropped, not shown as documentation", () => { + it("drops a commented-out line in the middle of a fluent method chain (Java/Kotlin style)", () => { + const code = [ + " // Explains the call below", + " given().accept(\"*/*\")", + " .options(url)", + " .then()", + " .assertThat()", + " // .header(\"allow\", \"GET,POST\")", + " .body(isEmptyOrNullString())", + ].join("\n"); + + expect(extractComments(code)).toBe("Explains the call below"); + }); + + it("drops a commented-out line in the middle of a promise chain (JS style)", () => { + const code = [ + "fetch(url)", + " .then(res => res.json())", + " // .then(data => expect(data.length).toBe(3))", + " .then(data => console.log(data));", + ].join("\n"); + + expect(extractComments(code)).toBe(""); + }); + + it("drops a commented-out line in the middle of a builder chain (C#/fluent-API style)", () => { + const code = [ + "var result = builder", + " .WithName(\"test\")", + " // .WithFlag(true)", + " .Build();", + ].join("\n"); + + expect(extractComments(code)).toBe(""); + }); + + it("drops a commented-out argument inside a wrapped, multi-line call (Python style)", () => { + const code = [ + "result = do_something(", + " value,", + " # extra_debug_flag,", + ")", + ].join("\n"); + + expect(extractComments(code)).toBe(""); + }); + + it("drops several consecutive commented-out lines that all continue the same chain", () => { + const code = [ + "obj.step1()", + " .step2()", + " // .step3(true)", + " // .step4(false)", + " .step5()", + ].join("\n"); + + expect(extractComments(code)).toBe(""); + }); + + it("drops a single-line block comment embedded in the middle of a chain", () => { + const code = [ + "chain()", + " .a()", + " /* .b() */", + " .c()", + ].join("\n"); + + expect(extractComments(code)).toBe(""); + }); + + it("drops a multi-line block comment embedded in the middle of a chain", () => { + const code = [ + "chain()", + " .a()", + " /*", + " * disabled: .b(true)", + " */", + " .c()", + ].join("\n"); + + expect(extractComments(code)).toBe(""); + }); + }); + + describe("comments that introduce a fresh statement are kept as documentation", () => { + it("keeps a comment describing an independent statement in the middle of a function body", () => { + const code = [ + "doFirstThing()", + "// Now verify the second call behaves correctly", + "doSecondThing()", + ].join("\n"); + + expect(extractComments(code)).toBe("Now verify the second call behaves correctly"); + }); + + it("keeps a comment that trails the last statement with nothing following it", () => { + const code = [ + "doWork()", + "// Cleanup performed automatically by the test runner", + ].join("\n"); + + expect(extractComments(code)).toBe("Cleanup performed automatically by the test runner"); + }); + + it("keeps documentation comments preceding each of several independent statements", () => { + const code = [ + "/**", + " * Calls:", + " * 1 - (201) PUT:/orders/{id}", + " * 2 - (200) GET:/orders/{id}", + " */", + "fun test() {", + "", + " // First call replaces the resource", + " given().put(url)", + " .then()", + " .statusCode(201)", + "", + " // Second call reads it back", + " given().get(url)", + " .then()", + " .statusCode(200)", + "}", + ].join("\n"); + + expect(extractComments(code)).toBe( + [ + "Calls:\n1 - (201) PUT:/orders/{id}\n2 - (200) GET:/orders/{id}", + "First call replaces the resource", + "Second call reads it back", + ].join("\n\n"), + ); + }); + + it("keeps a comment immediately followed by an opening call, not a continuation", () => { + const code = [ + "// Builds the request payload", + "buildPayload(", + " field1,", + " field2,", + ")", + ].join("\n"); + + expect(extractComments(code)).toBe("Builds the request payload"); + }); + }); }); diff --git a/web-report/src/lib/utils.tsx b/web-report/src/lib/utils.tsx index 9a603f8..c61951b 100644 --- a/web-report/src/lib/utils.tsx +++ b/web-report/src/lib/utils.tsx @@ -84,20 +84,71 @@ export const extractCodeLines = ( return lines.slice(startIndex, endIndex + 2).join('\n'); }; +// A line that opens with a "continuation" token (a leading '.', ',' or closing bracket) is, in +// virtually every mainstream language's formatting convention, the tail end of a statement that +// started on an earlier physical line (a chained method call, a wrapped argument list, ...) - +// never the start of a new one. +const isContinuationLine = (trimmed: string): boolean => /^[.,)\]}]/.test(trimmed); + +// Distinguishes a documentation comment (which introduces the statement that follows it) from a +// comment embedded in the middle of one (e.g. a disabled/commented-out line inside a call chain): +// +// // this explains the call below +// doSomething() +// .step1() +// // .step2() <- the next real line of code continues the previous statement, +// .step3() so this comment is part of that statement, not documentation +// +// A comment group is kept unless the nearest code line that follows it is a continuation line; +// in that case the comment sits inside an ongoing statement and is dropped along with it. This +// only looks at leading punctuation, so it holds regardless of source language or code style. export const extractComments = (code: string): string => { const lines = code.split("\n"); + const lineCount = lines.length; + + const isCodeLine: boolean[] = new Array(lineCount).fill(false); + { + let inBlock = false; + for (let i = 0; i < lineCount; i++) { + const trimmed = lines[i].trim(); + if (inBlock) { + if (trimmed.indexOf("*/") >= 0) inBlock = false; + continue; + } + if (trimmed.startsWith("/*")) { + if (trimmed.indexOf("*/", 2) < 0) inBlock = true; + continue; + } + if (/^(?:#|\/\/)/.test(trimmed)) continue; + if (trimmed.length > 0) isCodeLine[i] = true; + } + } + + const nextCodeLine = (fromIndex: number): string | null => { + for (let i = fromIndex; i < lineCount; i++) { + if (isCodeLine[i]) return lines[i].trim(); + } + return null; + }; + const groups: string[] = []; let current: string[] = []; let inBlock = false; - const flush = () => { + const flush = (endedAtLine: number) => { if (current.length === 0) return; const text = current.join("\n").trim(); - if (text.length > 0) groups.push(text); + if (text.length > 0) { + const following = nextCodeLine(endedAtLine); + if (!(following !== null && isContinuationLine(following))) { + groups.push(text); + } + } current = []; }; - for (const raw of lines) { + for (let i = 0; i < lineCount; i++) { + const raw = lines[i]; const trimmed = raw.trim(); if (inBlock) { @@ -106,7 +157,7 @@ export const extractComments = (code: string): string => { if (body.length > 0) current.push(body); if (endIdx >= 0) { inBlock = false; - flush(); + flush(i + 1); } continue; } @@ -117,7 +168,7 @@ export const extractComments = (code: string): string => { if (endIdx >= 0) { const body = afterOpen.slice(0, endIdx).trim(); if (body) current.push(body); - flush(); + flush(i + 1); } else { inBlock = true; if (afterOpen.length > 0) current.push(afterOpen); @@ -131,9 +182,9 @@ export const extractComments = (code: string): string => { continue; } - flush(); + flush(i); } - flush(); + flush(lineCount); return groups.join("\n\n"); };