From 179d5fa67da87e0bfb1e12642475ff6de25fec1f Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 27 Oct 2026 22:16:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=AF=AD=E6=B3=95=E8=A6=81=E6=B1=82):=20?= =?UTF-8?q?=E8=83=BD=E8=A6=81=E6=B1=82=E3=80=8C=E8=B0=83=E7=94=A8=20print(?= =?UTF-8?q?)=20=E6=97=B6=E5=BF=85=E9=A1=BB=E7=94=A8=20sep=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E3=80=8D=EF=BC=8C=E9=A2=98=E7=9B=AE=E8=AF=B4=E8=A6=81?= =?UTF-8?q?=E7=94=A8=20sep/end=20=E7=9A=84=E4=B8=8D=E5=86=8D=E8=A2=AB?= =?UTF-8?q?=E7=BB=95=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来只能数 print 调用几次,写成 print("1-2-3***4") + print() 也算达标。 新加 must_use_keyword(只有 Python),后台「关键字参数」一组里配函数名和参数名。 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/judge/ast.ts | 17 ++++++++++ .../problem/components/AstRulesEditor.vue | 34 +++++++++++++++++-- packages/contract/src/problem.ts | 4 +++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/apps/api/src/judge/ast.ts b/apps/api/src/judge/ast.ts index bff4d7d..4c6365c 100644 --- a/apps/api/src/judge/ast.ts +++ b/apps/api/src/judge/ast.ts @@ -115,6 +115,8 @@ export function describeAstRule(rule: AstRule, language?: string): string { return `不能调用 .${target}()` case 'must_use_operator': return `必须使用 ${astOperatorLabel(target, language)} 运算符` + case 'must_use_keyword': + return `调用 ${target}() 时必须用 ${rule.keyword ?? ''} 参数` case 'must_have_nesting': { // 这两个走 astNodeLabel 而不是裸值 —— 少了这一步文案就是 // 「必须使用 for_loop 嵌套」,旧栈 ast_checker/engines/nesting.py 是翻的 @@ -194,6 +196,9 @@ export function astRulesError(astRules: AstRules | null): string | null { } } else if (!target.trim()) { return `${at}:要检查的函数名/方法名不能为空` + } else if (rule.engine === 'must_use_keyword') { + if (language !== 'Python') return `${at}:${language} 没有关键字参数` + if (!rule.keyword?.trim()) return `${at}:参数名不能为空` } if (!astRuleIsMeaningful(rule)) return `${at}:次数检查至少要填一个数字` } @@ -248,6 +253,13 @@ function functionCalls(root: Node, target: string, language: string) { }) } +/** 带了 `keyword=...` 的那些 target() 调用。`**kwargs` 展开的不算,看不出里面有没有 */ +function keywordCalls(root: Node, target: string, keyword: string, language: string) { + return functionCalls(root, target, language).filter((call) => + call.childForFieldName('arguments')?.namedChildren.some((arg) => arg.type === 'keyword_argument' && arg.childForFieldName('name')?.text === keyword), + ) +} + function methodCalls(root: Node, target: string, language: string) { // C++ 的 `a.push_back()` / `p->push_back()` 都是 call_expression + field_expression, // 和 Python 的 attribute 不是一回事 —— 少了这个分支,C++ 的「必须调用 .push_back()」 @@ -325,6 +337,11 @@ function evaluateRule(root: Node, rule: AstRule, language: string): AstResult | description: describeAstRule(rule, language), passed: hasNode(root, nodeType), } + case 'must_use_keyword': + return { + description: describeAstRule(rule, language), + passed: keywordCalls(root, target, rule.keyword ?? '', language).length > 0, + } case 'must_have_nesting': { const outer = rule.outer ?? '' const inner = rule.inner ?? '' diff --git a/apps/web/src/admin/problem/components/AstRulesEditor.vue b/apps/web/src/admin/problem/components/AstRulesEditor.vue index fc91b58..dfeedec 100644 --- a/apps/web/src/admin/problem/components/AstRulesEditor.vue +++ b/apps/web/src/admin/problem/components/AstRulesEditor.vue @@ -117,6 +117,18 @@ const ENGINE_OPTIONS: SelectOption[] = [ }, ] +// 关键字参数只有 Python 有,C 题不给选 +const KEYWORD_OPTION: SelectOption = { + label: '关键字参数', + type: 'group', + key: 'keyword_group', + children: [{ label: '调用时必须带参数', value: 'must_use_keyword' }], +} + +function engineOptions(lang: string): SelectOption[] { + return lang === 'Python' ? [...ENGINE_OPTIONS, KEYWORD_OPTION] : ENGINE_OPTIONS +} + // 选项按语言生成。原来是一张 C/Python 混合的 15 条表整份铺开,给 C 题也能选到 // 列表推导式、f-string 这些 C 根本没有的东西 —— 存得进去,判题时永远失败 // (或者反过来,「不能使用 f-string」永远通过),两头都不报错。 @@ -139,6 +151,7 @@ const NODE_ENGINES = ['must_exist_node', 'must_not_exist_node', 'count_node'] const FUNCTION_ENGINES = ['must_call_function', 'must_not_call_function', 'count_function_call'] const METHOD_ENGINES = ['must_call_method', 'must_not_call_method'] const OPERATOR_ENGINES = ['must_use_operator'] +const KEYWORD_ENGINES = ['must_use_keyword'] const COUNT_ENGINES = ['count_node', 'count_function_call'] function isNodeEngine(engine: string) { @@ -153,6 +166,9 @@ function isMethodEngine(engine: string) { function isOperatorEngine(engine: string) { return OPERATOR_ENGINES.includes(engine) } +function isKeywordEngine(engine: string) { + return KEYWORD_ENGINES.includes(engine) +} function isCountEngine(engine: string) { return COUNT_ENGINES.includes(engine) } @@ -196,7 +212,7 @@ function needsTargetDropdown(engine: string) { return isNodeEngine(engine) } function needsTargetInput(engine: string) { - return isFunctionEngine(engine) || isMethodEngine(engine) + return isFunctionEngine(engine) || isMethodEngine(engine) || isKeywordEngine(engine) } function needsOperatorDropdown(engine: string) { return isOperatorEngine(engine) @@ -260,6 +276,8 @@ function updateRule(lang: string, index: number, field: string, value: any) { delete rule.min delete rule.max delete rule.exact + delete rule.keyword + if (isKeywordEngine(value)) rule.keyword = '' // 次数引擎不给默认值的话,存下去就是一条恒真规则 if (isCountEngine(value)) rule.exact = 1 } else if (field === 'target') { @@ -273,6 +291,8 @@ function updateRule(lang: string, index: number, field: string, value: any) { } else if (field === 'max') { if (value === null || value === undefined) delete rule.max else rule.max = value + } else if (field === 'keyword') { + rule.keyword = value } else if (field === 'message') { rule.message = value } @@ -322,7 +342,7 @@ watch(supportedLanguages, (langs) => {
{ :value="rule.target" size="small" class="target" - placeholder="函数 / 方法名" + :placeholder="isKeywordEngine(rule.engine) ? '函数名,如 print' : '函数 / 方法名'" @update:value="(v: string) => updateRule(lang, index, 'target', v)" /> +