fix(web): parse destructured and multi-line main signatures in Code node (#44046)

Co-authored-by: Stream <Stream_2@qq.com>
This commit is contained in:
YueliupandStream authored and GitHub committed 2026-10-19 05:33:55 +00:00
1 parent 3adc339814
commit 9aebf35e94
2 files changed
+132 -5

No files matched your search

@@ -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)
})
@@ -5,16 +5,35 @@ import { CodeLanguage } from './types'
export const extractFunctionParams = (code: string, language: CodeLanguage) => {
if (language === CodeLanguage.json) return []
const patterns: Record<Exclude<CodeLanguage, typeof CodeLanguage.json>, 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<Exclude<CodeLanguage, typeof CodeLanguage.json>, 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)