style(格式化): 改成单引号、一行最长 160,全仓重新格式化
Deploy / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 13:52:24 +08:00
1 parent 530e1a3d5d
commit 195069450f
480 files changed
+15507 -24650

No files matched your search

+72 -94
View File
@@ -11,19 +11,19 @@ import {
type AstRequirements,
type AstRule,
type AstRules,
} from "@oj2/contract"
import { Language, Parser, type Node } from "web-tree-sitter"
} from '@oj2/contract'
import { Language, Parser, type Node } from 'web-tree-sitter'
// 语法 wasm 内嵌成资源。原来是 `Bun.resolveSync(pkg + "/" + name, import.meta.dir)`,
// 编译成单二进制后 import.meta.dir 是 /$bunfs/root,解析不到 node_modules。见 vendor/jieba.ts
import cWasmPath from "tree-sitter-c/tree-sitter-c.wasm" with { type: "file" }
import cppWasmPath from "tree-sitter-cpp/tree-sitter-cpp.wasm" with { type: "file" }
import pythonWasmPath from "tree-sitter-python/tree-sitter-python.wasm" with { type: "file" }
import cWasmPath from 'tree-sitter-c/tree-sitter-c.wasm' with { type: 'file' }
import cppWasmPath from 'tree-sitter-cpp/tree-sitter-cpp.wasm' with { type: 'file' }
import pythonWasmPath from 'tree-sitter-python/tree-sitter-python.wasm' with { type: 'file' }
// web-tree-sitter 自己的运行时 wasm,Parser.init() 要用
import treeSitterWasmPath from "web-tree-sitter/web-tree-sitter.wasm" with { type: "file" }
import treeSitterWasmPath from 'web-tree-sitter/web-tree-sitter.wasm' with { type: 'file' }
// AstRule 的形状在契约里(astRuleSchema)—— 原来这份和前端两份各写各的。
// 这里 re-export,判题机的调用方不用再去 import 契约。
export type { AstRule } from "@oj2/contract"
export type { AstRule } from '@oj2/contract'
export interface AstResult {
description: string
@@ -57,7 +57,7 @@ async function loadLanguage(language: string) {
const cached = languages.get(language)
if (cached) return cached
const wasmPath = language === "C" ? cWasmPath : language === "C++" ? cppWasmPath : pythonWasmPath
const wasmPath = language === 'C' ? cWasmPath : language === 'C++' ? cppWasmPath : pythonWasmPath
const loaded = await Language.load(wasmPath)
languages.set(language, loaded)
return loaded
@@ -75,8 +75,8 @@ function hasNode(root: Node, type: string): boolean {
}
function targetName(rule: AstRule, language?: string) {
const target = rule.target ?? ""
return rule.label || astNodeLabel(target, language) || "指定语法"
const target = rule.target ?? ''
return rule.label || astNodeLabel(target, language) || '指定语法'
}
function countPhrase(verb: string, rule: AstRule) {
@@ -84,7 +84,7 @@ function countPhrase(verb: string, rule: AstRule) {
if (rule.min !== undefined && rule.max !== undefined) return `${verb} ${rule.min}~${rule.max} 次`
if (rule.min !== undefined) return `至少${verb} ${rule.min} 次`
if (rule.max !== undefined) return `至多${verb} ${rule.max} 次`
return ""
return ''
}
/**
@@ -95,41 +95,41 @@ function countPhrase(verb: string, rule: AstRule) {
export function describeAstRule(rule: AstRule, language?: string): string {
if (rule.message) return rule.message
const name = targetName(rule, language)
const target = rule.target ?? ""
const target = rule.target ?? ''
switch (rule.engine) {
case "must_exist_node":
case 'must_exist_node':
return `必须使用 ${name}`
case "must_not_exist_node":
case 'must_not_exist_node':
return `不能使用 ${name}`
case "count_node":
return `${name} ${countPhrase("出现", rule)}`.trim()
case "must_call_function":
case 'count_node':
return `${name} ${countPhrase('出现', rule)}`.trim()
case 'must_call_function':
return `必须调用 ${target}()`
case "must_not_call_function":
case 'must_not_call_function':
return `不能调用 ${target}()`
case "count_function_call":
return `${target}() ${countPhrase("调用", rule)}`.trim()
case "must_call_method":
case 'count_function_call':
return `${target}() ${countPhrase('调用', rule)}`.trim()
case 'must_call_method':
return `必须调用 .${target}()`
case "must_not_call_method":
case 'must_not_call_method':
return `不能调用 .${target}()`
case "must_use_operator":
case 'must_use_operator':
return `必须使用 ${astOperatorLabel(target, language)} 运算符`
case "must_have_nesting": {
case 'must_have_nesting': {
// 这两个走 astNodeLabel 而不是裸值 —— 少了这一步文案就是
// 「必须使用 for_loop 嵌套」,旧栈 ast_checker/engines/nesting.py 是翻的
const outer = astNodeLabel(rule.outer ?? "", language)
const inner = astNodeLabel(rule.inner ?? "", language)
const outer = astNodeLabel(rule.outer ?? '', language)
const inner = astNodeLabel(rule.inner ?? '', language)
return outer === inner ? `必须使用 ${outer} 嵌套` : `必须在 ${outer} 中嵌套使用 ${inner}`
}
}
}
/** 标签配色用的粗分类,见契约 astRequirementSchema */
function requirementKind(engine: AstRule["engine"]): AstRequirement["kind"] {
if (engine.startsWith("must_not")) return "forbid"
if (engine.startsWith("count")) return "count"
return "require"
function requirementKind(engine: AstRule['engine']): AstRequirement['kind'] {
if (engine.startsWith('must_not')) return 'forbid'
if (engine.startsWith('count')) return 'count'
return 'require'
}
/**
@@ -141,10 +141,7 @@ function requirementKind(engine: AstRule["engine"]): AstRequirement["kind"] {
* checkAst 直接放行 —— 学生看得见要求,判题从不检查。
*/
export function astRequirements(value: unknown): AstRequirements | null {
const grouped =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null
const grouped = value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null
if (!grouped) return null
const out: AstRequirements = {}
for (const [language, rules] of Object.entries(grouped)) {
@@ -180,19 +177,19 @@ export function astRulesError(astRules: AstRules | null): string | null {
for (const [language, rules] of Object.entries(astRules)) {
if (rules.length === 0) continue
if (!AST_SUPPORTED_LANGUAGES.includes(language)) {
return `代码规则暂不支持 ${language},判题机只检查 ${AST_SUPPORTED_LANGUAGES.join(" / ")}`
return `代码规则暂不支持 ${language},判题机只检查 ${AST_SUPPORTED_LANGUAGES.join(' / ')}`
}
const nodes = AST_NODE_TARGETS_BY_LANGUAGE[language] ?? {}
const operators = AST_OPERATOR_TARGETS_BY_LANGUAGE[language] ?? {}
for (const [index, rule] of rules.entries()) {
const at = `代码规则 ${language} 第 ${index + 1} 条`
const target = rule.target ?? ""
if (rule.engine.endsWith("_node")) {
const target = rule.target ?? ''
if (rule.engine.endsWith('_node')) {
if (!(target in nodes)) return `${at}:${language} 没有「${target}」这种语法`
} else if (rule.engine === "must_use_operator") {
} else if (rule.engine === 'must_use_operator') {
if (!(target in operators)) return `${at}:${language} 没有「${target}」运算符`
} else if (rule.engine === "must_have_nesting") {
for (const value of [rule.outer ?? "", rule.inner ?? ""]) {
} else if (rule.engine === 'must_have_nesting') {
for (const value of [rule.outer ?? '', rule.inner ?? '']) {
if (!(value in nodes)) return `${at}:${language} 没有「${value}」这种语法`
}
} else if (!target.trim()) {
@@ -231,21 +228,21 @@ function rangePassed(count: number, rule: AstRule) {
}
const CALL_NODE_TYPES: Record<string, string> = {
C: "call_expression",
"C++": "call_expression",
Python: "call",
C: 'call_expression',
'C++': 'call_expression',
Python: 'call',
}
function functionCalls(root: Node, target: string, language: string) {
const callType = CALL_NODE_TYPES[language] ?? "call"
const callType = CALL_NODE_TYPES[language] ?? 'call'
return collectNodes(root, callType).filter((call) => {
const fn = call.childForFieldName("function")
const fn = call.childForFieldName('function')
if (!fn) return false
if (fn.type === "identifier") return fn.text === target
if (fn.type === 'identifier') return fn.text === target
// `std::sort(...)` 是 qualified_identifier。学生写 sort 还是 std::sort 取决于
// 有没有 using namespace std,两种都得认,所以末段也比一次
if (language === "C++" && fn.type === "qualified_identifier") {
return fn.text === target || fn.text.split("::").pop() === target
if (language === 'C++' && fn.type === 'qualified_identifier') {
return fn.text === target || fn.text.split('::').pop() === target
}
return false
})
@@ -255,37 +252,34 @@ function methodCalls(root: Node, target: string, language: string) {
// C++ 的 `a.push_back()` / `p->push_back()` 都是 call_expression + field_expression,
// 和 Python 的 attribute 不是一回事 —— 少了这个分支,C++ 的「必须调用 .push_back()」
// 会静默地永远失败
if (language === "C++") {
return collectNodes(root, "call_expression").filter((call) => {
const fn = call.childForFieldName("function")
return fn?.type === "field_expression" && fn.childForFieldName("field")?.text === target
if (language === 'C++') {
return collectNodes(root, 'call_expression').filter((call) => {
const fn = call.childForFieldName('function')
return fn?.type === 'field_expression' && fn.childForFieldName('field')?.text === target
})
}
if (language !== "Python") return []
return collectNodes(root, "call").filter((call) => {
const fn = call.childForFieldName("function")
return fn?.type === "attribute" && fn.childForFieldName("attribute")?.text === target
if (language !== 'Python') return []
return collectNodes(root, 'call').filter((call) => {
const fn = call.childForFieldName('function')
return fn?.type === 'attribute' && fn.childForFieldName('attribute')?.text === target
})
}
function evaluateRule(root: Node, rule: AstRule, language: string): AstResult | null {
const target = rule.target ?? ""
const target = rule.target ?? ''
const nodeType = astTargetNodeType(target, language)
switch (rule.engine) {
case "must_exist_node":
case 'must_exist_node':
return {
description: describeAstRule(rule, language),
passed: hasNode(root, nodeType),
}
case "must_not_exist_node": {
case 'must_not_exist_node': {
const nodes = collectNodes(root, nodeType)
return withLines(
{ description: describeAstRule(rule, language), passed: nodes.length === 0 },
nodes,
)
return withLines({ description: describeAstRule(rule, language), passed: nodes.length === 0 }, nodes)
}
case "count_node": {
case 'count_node': {
const nodes = collectNodes(root, nodeType)
return withLines(
{
@@ -297,19 +291,16 @@ function evaluateRule(root: Node, rule: AstRule, language: string): AstResult |
rule.max !== undefined || rule.exact !== undefined ? nodes : [],
)
}
case "must_call_function":
case 'must_call_function':
return {
description: describeAstRule(rule, language),
passed: functionCalls(root, target, language).length > 0,
}
case "must_not_call_function": {
case 'must_not_call_function': {
const calls = functionCalls(root, target, language)
return withLines(
{ description: describeAstRule(rule, language), passed: calls.length === 0 },
calls,
)
return withLines({ description: describeAstRule(rule, language), passed: calls.length === 0 }, calls)
}
case "count_function_call": {
case 'count_function_call': {
const calls = functionCalls(root, target, language)
return withLines(
{
@@ -320,31 +311,26 @@ function evaluateRule(root: Node, rule: AstRule, language: string): AstResult |
rule.max !== undefined || rule.exact !== undefined ? calls : [],
)
}
case "must_call_method":
case 'must_call_method':
return {
description: describeAstRule(rule, language),
passed: methodCalls(root, target, language).length > 0,
}
case "must_not_call_method": {
case 'must_not_call_method': {
const calls = methodCalls(root, target, language)
return withLines(
{ description: describeAstRule(rule, language), passed: calls.length === 0 },
calls,
)
return withLines({ description: describeAstRule(rule, language), passed: calls.length === 0 }, calls)
}
case "must_use_operator":
case 'must_use_operator':
return {
description: describeAstRule(rule, language),
passed: hasNode(root, nodeType),
}
case "must_have_nesting": {
const outer = rule.outer ?? ""
const inner = rule.inner ?? ""
case 'must_have_nesting': {
const outer = rule.outer ?? ''
const inner = rule.inner ?? ''
const outerType = astTargetNodeType(outer, language)
const innerType = astTargetNodeType(inner, language)
const passed = collectNodes(root, outerType).some((node) =>
node.children.some((child) => hasNode(child, innerType)),
)
const passed = collectNodes(root, outerType).some((node) => node.children.some((child) => hasNode(child, innerType)))
return { description: describeAstRule(rule, language), passed }
}
default:
@@ -352,11 +338,7 @@ function evaluateRule(root: Node, rule: AstRule, language: string): AstResult |
}
}
export async function checkAst(
code: string,
language: string,
rules: AstRule[],
): Promise<{ passed: boolean; results: AstResult[] }> {
export async function checkAst(code: string, language: string, rules: AstRule[]): Promise<{ passed: boolean; results: AstResult[] }> {
if (rules.length === 0) return { passed: true, results: [] }
const treeSitterLanguage = await loadLanguage(language)
@@ -390,11 +372,7 @@ export async function checkAst(
* 和 checkAst 不同,**判不了时返回 null 而不是「通过」**:checkAst 是题目的硬要求,
* 判不了就放行、别冤枉学生;挑战判不了就是没做到,放行等于白送。
*/
export async function checkAstGroups(
code: string,
language: string,
groups: AstRule[][],
): Promise<{ passed: boolean; results: AstResult[] }[] | null> {
export async function checkAstGroups(code: string, language: string, groups: AstRule[][]): Promise<{ passed: boolean; results: AstResult[] }[] | null> {
const treeSitterLanguage = await loadLanguage(language)
if (!treeSitterLanguage) return null
const parser = new Parser()