Skip to content

Commit d1330cf

Browse files
pranaygpclaudeVaguelySerious
authored
Fix node-module-error plugin matching identifiers in multi-line comments (#1554)
* Fix node-module-error plugin matching identifiers in multi-line comments The findIdentifierUsage function only stripped single-line comments and same-line block comments, but didn't track multi-line block comment state. Lines inside JSDoc/block comments (e.g. ` * ... Writable stream`) passed through unstripped, causing the plugin to point at comments instead of actual code usage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Strip string literals before scanning for comment delimiters Move string stripping before comment detection so that comment delimiters inside string literals (e.g. `const s = "/*"`) don't incorrectly trigger block comment mode. Adds regression test. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com>
1 parent 5d22e61 commit d1330cf

3 files changed

Lines changed: 106 additions & 14 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@workflow/builders': patch
3+
---
4+
5+
Fix node-module-error plugin pointing at multi-line comments instead of code usage

packages/builders/src/node-module-esbuild-plugin.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,62 @@ describe('workflow-node-module-error plugin', () => {
378378
}
379379
});
380380

381+
it('should not point to JSDoc comments when finding usage', async () => {
382+
const testCode = `
383+
import { Writable } from "stream";
384+
/**
385+
* Convert a Web WritableStream<string> into a Node.js Writable stream
386+
*/
387+
export function workflow() {
388+
return new Writable();
389+
}
390+
`;
391+
392+
const { failure } = await buildWorkflowWithViolation(testCode);
393+
394+
expect(failure.errors).toHaveLength(1);
395+
const violation = failure.errors[0];
396+
expect(violation.text).toContain('"stream" which is a Node.js module');
397+
// Should point to the actual code usage, not the JSDoc comment
398+
expect(violation.location?.lineText).toContain('new Writable()');
399+
expect(violation.location?.lineText).not.toContain('*');
400+
});
401+
402+
it('should not point to single-line block comments when finding usage', async () => {
403+
const testCode = `
404+
import { Writable } from "stream";
405+
/* Writable is used below */
406+
export function workflow() {
407+
return new Writable();
408+
}
409+
`;
410+
411+
const { failure } = await buildWorkflowWithViolation(testCode);
412+
413+
expect(failure.errors).toHaveLength(1);
414+
const violation = failure.errors[0];
415+
// Should point to the actual code usage, not the comment
416+
expect(violation.location?.lineText).toContain('new Writable()');
417+
});
418+
419+
it('should not treat comment delimiters inside strings as comments', async () => {
420+
const testCode = `
421+
import { Writable } from "stream";
422+
const pattern = "/* Writable */";
423+
export function workflow() {
424+
return new Writable();
425+
}
426+
`;
427+
428+
const { failure } = await buildWorkflowWithViolation(testCode);
429+
430+
expect(failure.errors).toHaveLength(1);
431+
const violation = failure.errors[0];
432+
expect(violation.text).toContain('"stream" which is a Node.js module');
433+
// Should point to actual code, not the string containing "Writable"
434+
expect(violation.location?.lineText).toContain('new Writable()');
435+
});
436+
381437
it('should error on Bun module imports', async () => {
382438
const testCode = `
383439
import { serve } from "bun";

packages/builders/src/node-module-esbuild-plugin.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,28 +159,59 @@ function findIdentifierUsage(
159159
identifier: string
160160
) {
161161
const usageRegex = new RegExp(`\\b${escapeRegExp(identifier)}\\b`);
162+
let inBlockComment = false;
162163

163164
for (let i = startIndex; i < lines.length; i += 1) {
164-
const line = lines[i];
165-
166-
// Skip comments (both // and /* */ style)
167-
const withoutComments = line
168-
.replace(/\/\*[\s\S]*?\*\//g, '')
169-
.replace(/\/\/.*$/, '');
165+
// Strip string literals first so that comment delimiters inside strings
166+
// (e.g. `const s = "/*";`) don't confuse the comment scanner.
167+
const stringsStripped = lines[i]
168+
.replace(/'(?:[^'\\]|\\.)*'/g, (s) => ' '.repeat(s.length))
169+
.replace(/"(?:[^"\\]|\\.)*"/g, (s) => ' '.repeat(s.length))
170+
.replace(/`(?:[^`\\]|\\.)*`/g, (s) => ' '.repeat(s.length));
171+
172+
let line = stringsStripped;
173+
174+
// Handle multi-line block comments (including JSDoc)
175+
if (inBlockComment) {
176+
const endIdx = line.indexOf('*/');
177+
if (endIdx === -1) {
178+
continue;
179+
}
180+
line = ' '.repeat(endIdx + 2) + line.slice(endIdx + 2);
181+
inBlockComment = false;
182+
}
170183

171-
// Remove (replace with spaces) string literals to avoid matching inside paths
172-
// Use escaped quote handling to properly match strings with escaped quotes
173-
const withoutStrings = withoutComments
174-
.replace(/'(?:[^'\\]|\\.)*'/g, (segment) => ' '.repeat(segment.length))
175-
.replace(/"(?:[^"\\]|\\.)*"/g, (segment) => ' '.repeat(segment.length))
176-
.replace(/`(?:[^`\\]|\\.)*`/g, (segment) => ' '.repeat(segment.length));
184+
// Scan for comments, replacing them with spaces
185+
let processed = '';
186+
let j = 0;
187+
while (j < line.length) {
188+
if (line[j] === '/' && line[j + 1] === '/') {
189+
processed += ' '.repeat(line.length - j);
190+
break;
191+
}
192+
if (line[j] === '/' && line[j + 1] === '*') {
193+
const endIdx = line.indexOf('*/', j + 2);
194+
if (endIdx !== -1) {
195+
const len = endIdx + 2 - j;
196+
processed += ' '.repeat(len);
197+
j = endIdx + 2;
198+
} else {
199+
processed += ' '.repeat(line.length - j);
200+
inBlockComment = true;
201+
break;
202+
}
203+
} else {
204+
processed += line[j];
205+
j += 1;
206+
}
207+
}
177208

178-
const match = withoutStrings.match(usageRegex);
209+
const match = processed.match(usageRegex);
179210
if (match && match.index !== undefined) {
180211
return {
181212
line: i,
182213
column: match.index,
183-
lineText: line,
214+
lineText: lines[i],
184215
};
185216
}
186217
}

0 commit comments

Comments
 (0)