fix(代码规则): 给 C 题配的规则一半是哑弹,C++ 题根本配不了
Some checks failed
Deploy / deploy (push) Has been cancelled

后台的节点下拉是一张 C/Python 混在一起的 15 条表,整份铺给每种语言。给 C 题选到
只有 Python 有的 list_comprehension、f-string,规则存得进去,判题时拿裸名去比节点
类型,C 的语法树里永远不存在它——「必须使用列表推导式」永远失败、「不能使用
f-string」永远通过,两头都不报错,只有学生受着。反过来 mappings 支持的 do_while、
switch、struct、include 在表里没有,编辑器根本选不到。

标签表改成按语言分组,和判题机 mappings 的键集逐条对齐;保存时校验规则与语言是否
匹配,不匹配给中文提示。C 的 target 从 8 个可用变成 14 个。

C++ 接上了 tree-sitter-cpp,386 道 C++ 题从此能配规则。它继承 tree-sitter-c 的语法,
C 那 14 个 target 实测全部通用,另加范围 for、类定义、try-catch、throw、namespace、
模板、lambda、using 共 22 个。调用形态和 C/Python 都不同,一并处理:a.push_back()
和 p->push_back() 是 call_expression + field_expression,不是 Python 的 attribute;
std::sort(...) 的 function 是 qualified_identifier 而不是 identifier,所以额外比一次
:: 末段,否则学生写没写 using namespace std 会得到不同判定。

一起收掉的几处:

- Java/Golang/JavaScript 配的规则一条都不会跑,题目页却照常把它们渲染成「要求」
  挂给学生看。现在后台不给这些语言开 tab,下发给学生的要求也按语言过滤。
- 「出现次数」不填数字存下来是一条恒真规则,描述还退化成光秃秃一个「for 循环」。
  切换引擎时给默认值,保存时拦下,读取时整条丢弃。
- 次数规则失败只说「if 条件 出现 2 次 ✗」,学生不知道自己写了几次,补上「当前 N 次」。
  旧栈的引擎其实算了这个数,但 checker 只取 describe,算完就扔。
- must_have_nesting 的文案没走标签表,学生看到的是「必须使用 for_loop 嵌套」。
- 运算符文案给的是逻辑名,C 题的学生看到「必须使用 and 运算符」,而 C 里写的是 &&。

语义校验放在 astRulesError() 而不是 zod 的 refine 上:astRulesSchema 同时用于读后台
题目详情,在读路径上抛错会让历史脏数据把整个题目详情打不开。保存前先 pickAstRules()
剔除够不着的分组再校验,否则早年配过 C++ 规则的题会把老师锁死——tab 里看不到那组
规则,保存却被拦下。

生产库那 17 条规则(全是 Python3 的 must_exist_node / count_node)行为不变,逐条实跑
核对过。C++ 的 22 个节点 target、25 个运算符也逐个跑了,没有恒假的哑弹。改了带 wasm
内嵌的 ast.ts,dev 和编译两种形态都验过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 11:53:37 -06:00
parent 280443c892
commit f38444c97a
9 changed files with 444 additions and 94 deletions

View File

@@ -1,5 +1,9 @@
<script setup lang="ts">
import { AST_NODE_TARGET_LABELS } from "@oj2/contract"
import {
AST_NODE_TARGETS_BY_LANGUAGE,
AST_OPERATOR_TARGETS_BY_LANGUAGE,
AST_SUPPORTED_LANGUAGES,
} from "@oj2/contract"
import type { AstRule, AstRules, LANGUAGE } from "utils/types"
interface Props {
@@ -12,7 +16,17 @@ const emit = defineEmits<{
(e: "update:modelValue", value: AstRules | null): void
}>()
const activeTab = ref(props.languages[0] || "Python3")
// 判题机只认 C / Python3别的语言配了规则也一条都不会跑judge/ast.ts 的
// loadLanguage 返回 null 就直接放行)。原来这里按题目的全部语言开 tab老师给
// C++ 配的规则存得下、题目页也照常显示成「要求」,判题却从不检查。
const supportedLanguages = computed(() =>
props.languages.filter((lang) => AST_SUPPORTED_LANGUAGES.includes(lang)),
)
const unsupportedLanguages = computed(() =>
props.languages.filter((lang) => !AST_SUPPORTED_LANGUAGES.includes(lang)),
)
const activeTab = ref(supportedLanguages.value[0] || "Python3")
const ENGINE_OPTIONS: SelectOption[] = [
{
@@ -52,32 +66,21 @@ const ENGINE_OPTIONS: SelectOption[] = [
},
]
// 选项从契约的 AST_NODE_TARGET_LABELS 生成 —— 这 15 条原来在这里和
// ProblemContent.vue 各手抄一份
const NODE_TARGET_OPTIONS: SelectOption[] = Object.entries(
AST_NODE_TARGET_LABELS,
).map(([value, label]) => ({ label, value }))
// 选项按语言生成。原来是一张 C/Python 混合的 15 条表整份铺开,给 C 题也能选到
// 列表推导式、f-string 这些 C 根本没有的东西 —— 存得进去,判题时永远失败
// (或者反过来,「不能使用 f-string」永远通过两头都不报错。
function nodeTargetOptions(lang: string): SelectOption[] {
return Object.entries(AST_NODE_TARGETS_BY_LANGUAGE[lang] ?? {}).map(
([value, label]) => ({ label, value }),
)
}
const OPERATOR_TARGET_OPTIONS: SelectOption[] = [
{ label: "+", value: "+" },
{ label: "-", value: "-" },
{ label: "*", value: "*" },
{ label: "/", value: "/" },
{ label: "//", value: "//" },
{ label: "%", value: "%" },
{ label: "**", value: "**" },
{ label: "+=", value: "+=" },
{ label: "-=", value: "-=" },
{ label: "==", value: "==" },
{ label: "!=", value: "!=" },
{ label: ">", value: ">" },
{ label: ">=", value: ">=" },
{ label: "<", value: "<" },
{ label: "<=", value: "<=" },
{ label: "and / &&", value: "and" },
{ label: "or / ||", value: "or" },
{ label: "not / !", value: "not" },
]
// 逻辑名 and/or/not 在 C 里显示成 && / || / !,存进去的还是逻辑名
function operatorTargetOptions(lang: string): SelectOption[] {
return Object.entries(AST_OPERATOR_TARGETS_BY_LANGUAGE[lang] ?? {}).map(
([value, label]) => ({ label: label === value ? value : `${label}${value}`, value }),
)
}
const NODE_ENGINES = ["must_exist_node", "must_not_exist_node", "count_node"]
const FUNCTION_ENGINES = [
@@ -122,6 +125,9 @@ function updateCountMode(lang: string, index: number, mode: "exact" | "range") {
delete rule.min
delete rule.max
} else {
// 留空的 count 规则恒真rangePassed 三个字段全 undefined 就返回 true
// 描述还会退化成光秃秃一个「for 循环」。切过来先给个 1
rule.min = rule.min ?? 1
delete rule.exact
}
rules[index] = rule
@@ -162,12 +168,14 @@ function updateRules(lang: string, rules: AstRule[]) {
emit("update:modelValue", Object.keys(current).length > 0 ? current : null)
}
function getTargetLabel(engine: string, target: string): string | undefined {
if (isNodeEngine(engine))
return (NODE_TARGET_OPTIONS.find((o) => o.value === target) as any)?.label
if (isOperatorEngine(engine))
return (OPERATOR_TARGET_OPTIONS.find((o) => o.value === target) as any)
?.label
function getTargetLabel(
lang: string,
engine: string,
target: string,
): string | undefined {
if (isNodeEngine(engine)) return AST_NODE_TARGETS_BY_LANGUAGE[lang]?.[target]
// 运算符不写 label判题结果的文案按语言翻译astOperatorLabel
// 存一个固定 label 反而会把 C 的 && 钉死成 and
return undefined
}
@@ -176,7 +184,7 @@ function addRule(lang: string) {
rules.push({
engine: "must_exist_node",
target: "for_loop",
label: "for 循环",
label: getTargetLabel(lang, "must_exist_node", "for_loop"),
message: "",
})
updateRules(lang, rules)
@@ -196,10 +204,10 @@ function updateRule(lang: string, index: number, field: string, value: any) {
rule.engine = value
if (isNodeEngine(value)) {
rule.target = "for_loop"
rule.label = "for 循环"
rule.label = getTargetLabel(lang, value, "for_loop")
} else if (isOperatorEngine(value)) {
rule.target = "+"
rule.label = "+"
delete rule.label
} else {
rule.target = ""
delete rule.label
@@ -207,9 +215,11 @@ function updateRule(lang: string, index: number, field: string, value: any) {
delete rule.min
delete rule.max
delete rule.exact
// 次数引擎不给默认值的话,存下去就是一条恒真规则
if (isCountEngine(value)) rule.exact = 1
} else if (field === "target") {
rule.target = value
const lbl = getTargetLabel(rule.engine, value)
const lbl = getTargetLabel(lang, rule.engine, value)
if (lbl) rule.label = lbl
else delete rule.label
} else if (field === "min") {
@@ -226,22 +236,32 @@ function updateRule(lang: string, index: number, field: string, value: any) {
updateRules(lang, rules)
}
watch(
() => props.languages,
(langs) => {
if (langs.length && !langs.includes(activeTab.value as LANGUAGE)) {
activeTab.value = langs[0]
}
},
)
watch(supportedLanguages, (langs) => {
if (langs.length && !langs.includes(activeTab.value as LANGUAGE)) {
activeTab.value = langs[0]
}
})
</script>
<template>
<n-collapse>
<n-collapse-item title="代码规则检查(选填)" name="ast-rules">
<n-tabs v-if="languages.length" type="segment" v-model:value="activeTab">
<n-alert
v-if="unsupportedLanguages.length"
type="info"
:bordered="false"
style="margin-bottom: 8px"
>
{{ unsupportedLanguages.join("、") }} 暂不支持代码规则检查判题机只能检查
{{ AST_SUPPORTED_LANGUAGES.join(" / ") }}
</n-alert>
<n-tabs
v-if="supportedLanguages.length"
type="segment"
v-model:value="activeTab"
>
<n-tab-pane
v-for="lang in languages"
v-for="lang in supportedLanguages"
:key="lang"
:name="lang"
:tab="lang"
@@ -264,7 +284,7 @@ watch(
/>
<n-select
v-if="needsTargetDropdown(rule.engine)"
:options="NODE_TARGET_OPTIONS"
:options="nodeTargetOptions(lang)"
:value="rule.target"
@update:value="
(v: string) => updateRule(lang, index, 'target', v)
@@ -285,7 +305,7 @@ watch(
/>
<n-select
v-if="needsOperatorDropdown(rule.engine)"
:options="OPERATOR_TARGET_OPTIONS"
:options="operatorTargetOptions(lang)"
:value="rule.target"
@update:value="
(v: string) => updateRule(lang, index, 'target', v)
@@ -370,7 +390,14 @@ watch(
</n-flex>
</n-tab-pane>
</n-tabs>
<n-empty v-else description="请先选择编程语言" />
<n-empty
v-else
:description="
languages.length
? '当前语言不支持代码规则检查'
: '请先选择编程语言'
"
/>
</n-collapse-item>
</n-collapse>
</template>

View File

@@ -178,6 +178,13 @@ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
<Icon :icon="rule.passed ? 'ph:check-bold' : 'ph:x-bold'" />
</n-icon>
<span>{{ rule.description }}</span>
<!-- 次数类规则光说「出现 2 次 ✗」,学生不知道自己写了几次 -->
<span
v-if="!rule.passed && rule.actual !== undefined"
:style="{ color: theme.errorColor }"
>
当前 {{ rule.actual }} 次
</span>
</n-flex>
</n-flex>
</n-card>

View File

@@ -289,7 +289,12 @@ export interface StatisticInfo {
err_info?: string
time_cost?: number
memory_cost?: number
ast_results?: Array<{ description: string; passed: boolean }>
ast_results?: Array<{
description: string
passed: boolean
/** count_* 规则实际数到的次数,判题机只在这两个引擎上写 */
actual?: number
}>
}
/**