From c25a5e1f309e5593e5f73a86d8add655f0724100 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 9 May 2026 17:02:10 +0000 Subject: [PATCH] fix(codegen): sanitize */ in JSDoc comments to prevent syntax errors When codegen.comments is enabled, field descriptions containing literal */ sequences would prematurely close the JSDoc comment block, producing invalid JavaScript/TypeScript output. Apply the same *\/ sanitization to both babel-ast.ts and hooks-ast.ts addJSDocComment functions, and add unit tests to prevent regression. Closes constructive-io/constructive-planning#820 --- .../__tests__/codegen/jsdoc-comment.test.ts | 88 +++++++++++++++++++ graphql/codegen/src/core/codegen/babel-ast.ts | 7 +- graphql/codegen/src/core/codegen/hooks-ast.ts | 7 +- 3 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 graphql/codegen/src/__tests__/codegen/jsdoc-comment.test.ts diff --git a/graphql/codegen/src/__tests__/codegen/jsdoc-comment.test.ts b/graphql/codegen/src/__tests__/codegen/jsdoc-comment.test.ts new file mode 100644 index 0000000000..1c55a47c04 --- /dev/null +++ b/graphql/codegen/src/__tests__/codegen/jsdoc-comment.test.ts @@ -0,0 +1,88 @@ +/** + * Tests for JSDoc comment sanitization in babel-ast and hooks-ast + */ +import * as t from '@babel/types'; + +import { + addJSDocComment, + generateCode, +} from '../../core/codegen/babel-ast'; +import { addJSDocComment as addJSDocCommentHooks } from '../../core/codegen/hooks-ast'; + +describe('addJSDocComment', () => { + describe('babel-ast', () => { + it('produces valid JSDoc for simple descriptions', () => { + const node = t.identifier('x'); + addJSDocComment(node, ['A simple description']); + const code = generateCode([ + t.variableDeclaration('const', [ + t.variableDeclarator(node, t.numericLiteral(1)), + ]), + ]); + expect(code).toContain('/** A simple description */'); + expect(code).not.toContain('*/\n'); + }); + + it('sanitizes */ in single-line descriptions', () => { + const node = t.identifier('x'); + addJSDocComment(node, [ + 'Reads and enables pagination through a set of */ values.', + ]); + const code = generateCode([ + t.variableDeclaration('const', [ + t.variableDeclarator(node, t.numericLiteral(1)), + ]), + ]); + expect(code).toContain('*\\/'); + expect(code).not.toMatch(/\/\*.*\*\/.*\*\//); + }); + + it('sanitizes */ in multi-line descriptions', () => { + const node = t.identifier('x'); + addJSDocComment(node, [ + 'First line with */ embedded', + 'Second line is fine', + 'Third line also has */ inside', + ]); + const code = generateCode([ + t.variableDeclaration('const', [ + t.variableDeclarator(node, t.numericLiteral(1)), + ]), + ]); + const commentMatch = code.match(/\/\*[\s\S]*?\*\//); + expect(commentMatch).not.toBeNull(); + const comment = commentMatch![0]; + const innerSlashes = comment.slice(2, -2); + expect(innerSlashes).not.toContain('*/'); + }); + }); + + describe('hooks-ast', () => { + it('produces valid JSDoc for simple descriptions', () => { + const node = t.identifier('y'); + addJSDocCommentHooks(node, ['A simple description']); + expect(node.leadingComments).toHaveLength(1); + expect(node.leadingComments![0].value).toBe('* A simple description '); + }); + + it('sanitizes */ in single-line descriptions', () => { + const node = t.identifier('y'); + addJSDocCommentHooks(node, [ + 'Reads and enables pagination through a set of */ values.', + ]); + expect(node.leadingComments![0].value).not.toContain('*/'); + expect(node.leadingComments![0].value).toContain('*\\/'); + }); + + it('sanitizes */ in multi-line descriptions', () => { + const node = t.identifier('y'); + addJSDocCommentHooks(node, [ + 'Line with */ problem', + 'Normal line', + ]); + const commentValue = node.leadingComments![0].value; + expect(commentValue).not.toContain('*/'); + expect(commentValue).toContain('*\\/'); + }); + }); +}); diff --git a/graphql/codegen/src/core/codegen/babel-ast.ts b/graphql/codegen/src/core/codegen/babel-ast.ts index 40caa2e765..39d7a1619b 100644 --- a/graphql/codegen/src/core/codegen/babel-ast.ts +++ b/graphql/codegen/src/core/codegen/babel-ast.ts @@ -50,10 +50,11 @@ export const commentLine = (value: string): t.CommentLine => { * Add a leading JSDoc comment to a node */ export function addJSDocComment(node: T, lines: string[]): T { + const sanitized = lines.map((line) => line.replace(/\*\//g, '*\\/')); const commentText = - lines.length === 1 - ? `* ${lines[0]} ` - : `*\n${lines.map((line) => ` * ${line}`).join('\n')}\n `; + sanitized.length === 1 + ? `* ${sanitized[0]} ` + : `*\n${sanitized.map((line) => ` * ${line}`).join('\n')}\n `; if (!node.leadingComments) { node.leadingComments = []; diff --git a/graphql/codegen/src/core/codegen/hooks-ast.ts b/graphql/codegen/src/core/codegen/hooks-ast.ts index 3f9a4c0647..b9f8f58239 100644 --- a/graphql/codegen/src/core/codegen/hooks-ast.ts +++ b/graphql/codegen/src/core/codegen/hooks-ast.ts @@ -570,10 +570,11 @@ export function destructureParamsWithSelectionAndScope( // ============================================================================ export function addJSDocComment(node: T, lines: string[]): T { + const sanitized = lines.map((line) => line.replace(/\*\//g, '*\\/')); const text = - lines.length === 1 - ? `* ${lines[0]} ` - : `*\n${lines.map((line) => ` * ${line}`).join('\n')}\n `; + sanitized.length === 1 + ? `* ${sanitized[0]} ` + : `*\n${sanitized.map((line) => ` * ${line}`).join('\n')}\n `; if (!node.leadingComments) { node.leadingComments = []; }