Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions web-report/src-e2e/extractComments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
65 changes: 58 additions & 7 deletions web-report/src/lib/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -131,9 +182,9 @@ export const extractComments = (code: string): string => {
continue;
}

flush();
flush(i);
}
flush();
flush(lineCount);

return groups.join("\n\n");
};
Expand Down
Loading