diff --git a/web/app/components/workflow/nodes/code/__tests__/code-parser.spec.ts b/web/app/components/workflow/nodes/code/__tests__/code-parser.spec.ts index 0b3def7034a..b787bea65ca 100644 --- a/web/app/components/workflow/nodes/code/__tests__/code-parser.spec.ts +++ b/web/app/components/workflow/nodes/code/__tests__/code-parser.spec.ts @@ -13,6 +13,17 @@ const SAMPLE_CODES = { return {"output": result}`, withDefaults: `def main(param1: str = "default", param2: int = 0): return {"data": param1}`, + multiLineParams: `def main( + param1: str, + param2: int, + ) -> dict: + return {"result": param1}`, + commentedOutSignature: `# Previous interface: +# def main( +# old_arg: str, +# ): +def main(arg1: str, arg2: str) -> dict: + return {"result": arg1 + arg2}`, }, javascript: { noParams: 'function main() {', @@ -26,16 +37,109 @@ const SAMPLE_CODES = { return { output: process(param1, param2) } }`, withSpaces: 'function main( param1 , param2 ) {', + destructuredParams: `function main({ param1, param2 }) { + return { result: param1 } + }`, + multiLineParams: `function main( + param1, + param2, + ) { + return { result: param1 } + }`, + commentedOutSignature: `// Previous interface: +// function main( +// oldArg, +// ) { +function main(arg1, arg2) { + return { result: arg1 } +}`, + blockCommentedSignature: `/* +function main( + oldArg, +) { +*/ +function main(arg1, arg2) { + return { result: arg1 } +}`, }, } describe('extractFunctionParams', () => { + it.each([ + [ + 'Python typed defaults', + CodeLanguage.python3, + 'def main(arg1: str = "/*", arg2: str = "*/") -> dict:\n return {"result": arg1 + arg2}', + ], + [ + 'Python strings spanning the signature', + CodeLanguage.python3, + 'OPEN = "/*"\ndef main(arg1: str, arg2: str):\n return {"result": OPEN + arg1 + arg2 + "*/"}', + ], + [ + 'Python triple-quoted strings', + CodeLanguage.python3, + 'OPEN = """/*\n# text\n"""\ndef main(arg1: str, arg2: str):\n return {"result": OPEN + arg1 + arg2 + "*/"}', + ], + [ + 'JavaScript double-quoted strings', + CodeLanguage.javascript, + 'const OPEN = "/*";\nfunction main({ arg1, arg2 }) {\n return {result: OPEN + arg1 + arg2 + "*/"}\n}', + ], + [ + 'JavaScript single-quoted strings', + CodeLanguage.javascript, + "const OPEN = '/*';\nfunction main({ arg1, arg2 }) {\n return {result: OPEN + arg1 + arg2 + '*/'}\n}", + ], + [ + 'JavaScript templates', + CodeLanguage.javascript, + `const OPEN = \`/* +// text +\`; +function main({ arg1, arg2 }) { + return {result: \`\${OPEN}\${arg1}\${arg2}*/\`} +}`, + ], + [ + 'escaped JavaScript quotes before a real comment', + CodeLanguage.javascript, + String.raw`const TEXT = "escaped \" /*"; +/* function main(oldArg) {} */ +function main({ arg1, arg2 }) { + return {result: TEXT + arg1 + arg2 + "*/"} +}`, + ], + [ + 'escaped Python quotes before a real comment', + CodeLanguage.python3, + String.raw`TEXT = 'escaped \' /*' +# def main(oldArg): +def main(arg1: str, arg2: str): + return {"result": TEXT + arg1 + arg2 + "*/"}`, + ], + [ + 'JavaScript comments between tokens', + CodeLanguage.javascript, + 'function/* interface */main({ arg1,/* input */arg2 }) {\n return {result: arg1 + arg2}\n}', + ], + [ + 'Python CRLF comments between parameters', + CodeLanguage.python3, + 'def main(\r\n arg1: str, # first input\r\n arg2: str,\r\n):\r\n return {"result": arg1 + arg2}', + ], + ] as const)('preserves input names with %s', (_description, language, code) => { + expect(extractFunctionParams(code, language)).toEqual(['arg1', 'arg2']) + }) + it.each([ [CodeLanguage.python3, SAMPLE_CODES.python3.noParams, []], [CodeLanguage.python3, SAMPLE_CODES.python3.singleParam, ['param1']], [CodeLanguage.python3, SAMPLE_CODES.python3.multipleParams, ['param1', 'param2', 'param3']], [CodeLanguage.python3, SAMPLE_CODES.python3.withTypes, ['param1', 'param2', 'param3']], [CodeLanguage.python3, SAMPLE_CODES.python3.withDefaults, ['param1', 'param2']], + [CodeLanguage.python3, SAMPLE_CODES.python3.multiLineParams, ['param1', 'param2']], + [CodeLanguage.python3, SAMPLE_CODES.python3.commentedOutSignature, ['arg1', 'arg2']], [CodeLanguage.javascript, SAMPLE_CODES.javascript.noParams, []], [CodeLanguage.javascript, SAMPLE_CODES.javascript.singleParam, ['param1']], [ @@ -45,6 +149,10 @@ describe('extractFunctionParams', () => { ], [CodeLanguage.javascript, SAMPLE_CODES.javascript.withComments, ['param1', 'param2']], [CodeLanguage.javascript, SAMPLE_CODES.javascript.withSpaces, ['param1', 'param2']], + [CodeLanguage.javascript, SAMPLE_CODES.javascript.destructuredParams, ['param1', 'param2']], + [CodeLanguage.javascript, SAMPLE_CODES.javascript.multiLineParams, ['param1', 'param2']], + [CodeLanguage.javascript, SAMPLE_CODES.javascript.commentedOutSignature, ['arg1', 'arg2']], + [CodeLanguage.javascript, SAMPLE_CODES.javascript.blockCommentedSignature, ['arg1', 'arg2']], ] as const)('extracts %s arguments from %s', (language, code, expected) => { expect(extractFunctionParams(code, language)).toEqual(expected) }) diff --git a/web/app/components/workflow/nodes/code/code-parser.ts b/web/app/components/workflow/nodes/code/code-parser.ts index 8a62e92c368..d3461917ebd 100644 --- a/web/app/components/workflow/nodes/code/code-parser.ts +++ b/web/app/components/workflow/nodes/code/code-parser.ts @@ -5,16 +5,35 @@ import { CodeLanguage } from './types' export const extractFunctionParams = (code: string, language: CodeLanguage) => { if (language === CodeLanguage.json) return [] - const patterns: Record, RegExp> = { - [CodeLanguage.python3]: /def\s+main\s*\((.*?)\)/, - [CodeLanguage.javascript]: /function\s+main\s*\((.*?)\)/, + // Match strings first so comment markers inside them cannot consume real code. + // This is lightweight filtering, not a full lexer: quotes in JavaScript regex + // literals or nested template expressions can still confuse string boundaries. + const commentPatterns = { + [CodeLanguage.python3]: + /"""(?:\\[\s\S]|[^\\])*?"""|'''(?:\\[\s\S]|[^\\])*?'''|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|(#[^\r\n]*)/g, + [CodeLanguage.javascript]: + /"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`|(\/\/[^\r\n]*|\/\*[\s\S]*?\*\/)/g, } - const match = patterns[language].exec(code) + const codeWithoutComments = code.replace( + commentPatterns[language], + (match: string, comment: string | undefined) => + comment ? comment.replace(/[^\r\n]/g, ' ') : match, + ) + + const patterns: Record, RegExp> = { + [CodeLanguage.python3]: /def\s+main\s*\(([\s\S]*?)\)/, + [CodeLanguage.javascript]: /function\s+main\s*\(([\s\S]*?)\)/, + } + const match = patterns[language].exec(codeWithoutComments) const params: string[] = [] if (match?.[1]) { + // The JavaScript `main` is invoked as `main(inputs_obj)`, so its parameters are + // usually written as a single destructuring pattern: `function main({ arg1, arg2 })`. + const rawParams = match[1].trim().replace(/^\{([\s\S]*)\}$/, '$1') + params.push( - ...match[1] + ...rawParams .split(',') .map((p) => p.trim()) .filter(Boolean)