mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 23:06:40 +08:00
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:
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)
|
||||
|
||||
Reference in new issue
Block a user