Files
OJ2/apps/web/src/oj/problem/utils/pythonError.ts
T
xuyueandClaude Opus 5.5 96ce9725b2 fix(语法错误): 有模板的题报错行号多 1;else 后面写了条件被说成「少了冒号」,a+b=c 被说成「判断相等要用 ==」
全库 9182 条 Python 编译错误逐条过了一遍翻译表,1014 条的说明变了:

- 行号:判题跑的是「模板 + 代码」,运行时错误扣了模板行数、编译错误没扣,
  「第 N 行」和标红都落在下一行(691 条)。提交和运行例子两条路都扣上,
  gcc 的 main.c:N: 和代码摘录左边的行号一起扣。报错前先打了一条警告时
  File 那行是缩进的,行号原来解析不出来。
- 同一句报错要看出错那一行才分得开:expected ':' 九成是 else a<60:(行尾有冒号),
  「Maybe you meant '=='」只有 if / while 行才是真想判相等,a+b=c、a=96,b=92、
  print(1+1=2) 各有各的说法。
- 选了 Python 却交了 C 代码(294 条),直接说换语言。只凭 printf 不算:
  Python 里 print 手误成 printf 的不少。
- 单独说清楚的:elif: 没写条件、esle / else if、=< 写反、条件之间用逗号、
  漏了 +、√ ² ≤ ≥、顿号、全角 #、emoji(正则少了 u 标志)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:44:09 +08:00

359 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 把 Python 的编译错误(`statistic_info.err_info`)翻成中文。
*
* 学生是中职生,英文报错等于没有报错。好在句式非常集中:2025 秋以来 2700 条
* Python 编译错误里,前 12 种句子占 91%、前 16 种占 97%,所以这里是一张规则表,
* 不是通用翻译。第二名「invalid character」(22%)几乎全是中文标点,另有一键修复,
* 见 chinesePunctuation.ts。
*
* 判题机给的原文有两种形状:
*
* File "/judger/run/<随机目录>/solution.py", line 7
* print("{:.0f}"."{:.2f}".format(x))
* ^^^^^^^^
* SyntaxError: invalid syntax
*
* Sorry: IndentationError: unexpected indent (solution.py, line 3)
*
* 第一种的代码行是 Python 去掉**行首空格**之后再垫 4 个空格打出来的,`^` 的列号
* 按去掉之后的算(traceback 的 `_format_syntax_error`,3.10 起如此)。
*/
export interface PythonErrorExplanation {
/** 出错的行,1 起 */
line: number | null
/** 判题机打出来的那一行代码(去掉了行首空格),用来和编辑器里的对位 */
sourceLine: string | null
/** `^` 在 sourceLine 里标的范围,[from, to) */
caret: { from: number; to: number } | null
/** 给学生看的中文说明,不含行号 */
message: string
/** 报错点名的那个中文标点;有它才出「一键换成英文」 */
punctuation: string | null
}
// 行首可能有空格:报错前面先打了一条 SyntaxWarning(比如 `3and`)时,File 那行是缩进的
const WHERE_RE = /^\s*File "[^"]*", line (\d+)/m
const SHORT_RE = /^Sorry: (\w+Error): (.*) \([^,()]*, line (\d+)\)\s*$/m
/**
* 中文空格(U+3000)。写成常量而不是字面量:Oxfmt 会把字符串里的 `\u3000` 转义
* 还原成原字符,那样源码里就是一个看不见的空白。
*/
const IDEOGRAPHIC_SPACE = String.fromCharCode(0x3000)
/** 中文标点 → 英文。「。」不在表里:它多半是句末多打的,换成「.」照样错,只能删 */
export const PUNCTUATION_MAP: Record<string, string> = {
"(": "(",
")": ")",
",": ",",
":": ":",
";": ";",
"【": "[",
"】": "]",
"{": "{",
"}": "}",
"!": "!",
"?": "?",
"=": "=",
"+": "+",
"-": "-",
"*": "*",
"/": "/",
"<": "<",
">": ">",
"“": '"',
"”": '"',
"‘": "'",
"’": "'",
""": '"',
"#": "#",
"×": "*",
"÷": "/",
[IDEOGRAPHIC_SPACE]: " ",
}
/**
* 标点的名字。说明里**只说名字、不拿字形对比**:「:」和「:」、「,」和「,」在正文字体里
* 几乎一样,「把「:」换成「:」」这句话学生看不出区别。「冒号是中文的,要换成英文的」
* 看得懂,要换哪一个由卡片里那行代码的标红来指。
*/
export const PUNCTUATION_NAMES: Record<string, string> = {
"(": "左括号",
")": "右括号",
",": "逗号",
":": "冒号",
";": "分号",
"【": "左方括号",
"】": "右方括号",
"{": "左花括号",
"}": "右花括号",
"!": "感叹号",
"?": "问号",
"=": "等号",
"+": "加号",
"-": "减号",
"*": "星号",
"/": "斜杠",
"<": "小于号",
">": "大于号",
"“": "引号",
"”": "引号",
"‘": "单引号",
"’": "单引号",
""": "引号",
"#": "井号",
[IDEOGRAPHIC_SPACE]: "空格",
}
/**
* 规则除了报错原文,还能看出错的那一行(判题机打出来的,去掉了行首空格)。同一句报错
* 学生错法不同,要看那一行才分得开:`expected ':'` 九成是 `else a < 60:`,行尾明明有冒号;
* 「Maybe you meant '=='」只有 if / while 那行才是真想判断相等。返回 null 就接着往下找。
*/
type Rule = [RegExp, (match: RegExpMatchArray, source: string) => string | null]
/** 去掉引号里的内容,只看代码本身的符号 */
function stripStrings(source: string) {
return source.replace(/(["'])(?:\\.|(?!\1).)*\1/g, '""')
}
/** 括号外面的逗号:`if a > 9, a < 100:` 里有,`if x in (1, 2):` 里没有 */
function hasTopLevelComma(source: string) {
let depth = 0
for (const ch of stripStrings(source)) {
if ("([{".includes(ch)) depth++
else if (")]}".includes(ch)) depth--
else if (ch === "," && depth === 0) return true
}
return false
}
const CONDITION_LINE = /^(if|elif|while)\b/
const RULES: Rule[] = [
[
// u:emoji 是两个 UTF-16 单元,不带 u 的 (.) 对不上
/^invalid character '(.)'/u,
([, char]) => {
// 数学符号不是「中文的」,是写法不对:代码里乘除用 * 和 /
if (char === "×") return "这里用了乘号「×」,代码里的乘法要写成星号「*」。"
if (char === "÷") return "这里用了除号「÷」,代码里的除法要写成斜杠「/」。"
if (char === "√")
return "代码里没有根号「√」:开平方写成 x ** 0.5,或者 import math 之后用 math.sqrt(x)。"
if (char === "²" || char === "³")
return `代码里没有上标的「${char}」:平方写成 x ** 2,立方写成 x ** 3。`
if (char === "≤") return "代码里没有「≤」:小于等于要写成小于号加等号 <=。"
if (char === "≥") return "代码里没有「≥」:大于等于要写成大于号加等号 >=。"
if (char === "≠") return "代码里没有「≠」:不等于要写成感叹号加等号 !=。"
if (char === "、")
return "代码里不用顿号「、」:几样东西之间要用英文逗号隔开。写代码的时候先切换到英文输入法。"
const name = PUNCTUATION_NAMES[char!]
if (name) {
return `这里的${name}是中文输入法打出来的,要换成英文的${name}。写代码的时候先切换到英文输入法。`
}
if (char === "。") return "这里多了一个中文句号「。」,代码里不用句号,删掉它。"
return `这里有一个不能出现在代码里的字符「${char}」,多半是中文输入法打出来的,删掉后用英文输入法重新打。`
},
],
[
/^invalid non-printable character/,
() => "这里有一个看不见的特殊字符,多半是从别处复制过来的。把这一行删掉,自己重新打一遍。",
],
[
/^expected ':'/,
(_, source) =>
/^else\b\s*[^\s:]/.test(source)
? "else 后面不能写条件,它的意思是「上面的条件都不满足」。还要再判断一个条件,就用 elif 条件:。"
: "这一行末尾少了英文冒号「:」。if、elif、else、for、while、def 这些行的最后都要有冒号。",
],
[
/^expected an indented block after '(\w+)' statement on line (\d+)/,
([, keyword, owner]) =>
`这一行要往里缩进(按一下 Tab 键):它是第 ${owner} 行那个 ${keyword} 要管的代码。`,
],
[
/^expected an indented block/,
() => "这一行要往里缩进(按一下 Tab 键):上一行以冒号结尾,它下面的代码都要缩进。",
],
[
/^unexpected indent/,
() => "这一行前面多了空格。只有 if、for 这类以冒号结尾的行,它下面的代码才要往里缩进。",
],
[
/^unindent does not match any outer indentation level/,
() =>
"这一行前面的空格数和上面对不齐。同一层的代码,前面的空格要一样多,建议统一用 Tab 键缩进。",
],
[
/^inconsistent use of tabs and spaces/,
() => "这一行的缩进混用了 Tab 键和空格键,统一只用 Tab 键缩进。",
],
[
/^invalid syntax\. Perhaps you forgot a comma\?/,
// 抽样一半以上是拼文字漏了 +:print(a+"是"a+"岁")
() =>
"这里的几样东西之间少了连接的符号:要分开输出,用英文逗号「,」隔开;要把文字拼在一起,用加号「+」。",
],
[
/Maybe you meant '==' instead of '='|Maybe you meant '==' or ':=' instead of '='|perhaps you meant "=="/,
(_, source) => {
// 只有条件里才是真想判断相等;别的行是想赋值,写法错了
if (CONDITION_LINE.test(source))
return "判断「是不是相等」要用两个等号「==」。一个等号「=」的意思是「把右边的值存进左边的变量」。"
if (/^\w+\s*=[^=].*,\s*\w+\s*=[^=]/.test(stripStrings(source)))
return "一行里给几个变量赋值,要写成 a, b = 96, 92,或者分成几行,一行写一个。"
if (/^\w+\s*\(/.test(source))
return '括号里不能直接写等号「=」。要把等号显示出来,就把它放进引号里,比如 print(a, "+", b, "=", a + b)。'
return null
},
],
[
/^cannot assign to /,
() =>
"等号「=」的左边只能是一个变量名,意思是「把右边算出来的值存进这个变量」。比如要写成 c = a + b,不能写成 a + b = c。",
],
[
/^unterminated (triple-quoted )?(f-)?string literal/,
() => "这一行的引号没有配对。字符串要用一对英文引号括起来,开头一个、结尾一个。",
],
[
/^'(.)' was never closed/,
([, open]) => `这里的「${open}」没有配对的另一半。数一数左右括号是不是一样多。`,
],
[/^unmatched '(.)'/, ([, close]) => `这里多了一个「${close}」,前面没有和它配对的括号。`],
[
/^closing parenthesis '(.)' does not match opening parenthesis '(.)'/,
([, close, open]) => `括号配错了:「${open}」要用和它配对的括号来关,不能用「${close}」。`,
],
[
/^Missing parentheses in call to '(\w+)'/,
([, name]) => `${name} 后面要加括号,写成 ${name}(...)。`,
],
[
/^invalid decimal literal/,
() => "数字后面紧跟着字母了。比如「2a」要写成「2*a」;变量名也不能用数字开头。",
],
[
/^unexpected character after line continuation character/,
() => "这里的反斜杠「\\」用错了。除法要用「/」,「\\」只能出现在引号里面。",
],
[
/^'(break|continue)' (not properly in|outside) loop/,
([, keyword]) => `${keyword} 只能写在 for 或 while 循环里面(要缩进在循环的下面)。`,
],
[
/^'return' outside function/,
() => "return 只能写在 def 定义的函数里面(要缩进在 def 的下面)。",
],
[
/^f-string: (expecting '}'|single '}' is not allowed)/,
() => "f 字符串里的花括号「{ }」没有配对,每个「{」都要有一个「}」。",
],
[
/^f-string: valid expression required/,
() => "f 字符串的花括号「{ }」里要写变量名或算式,不能空着。",
],
[
/^leading zeros in decimal integer literals/,
() => "整数不能用 0 开头,比如「07」要写成「7」。",
],
[
/^invalid syntax/,
(_, source) => {
if (/^elif\s*:/.test(source))
return "elif 后面要写条件,比如 elif a > 60:。不需要条件的话就用 else:。"
if (/^else\s+if\b/.test(source)) return "Python 里「否则如果」要写成 elif,不是 else if。"
if (/^(esle|eles|els|elese|esif|elsif|elseif|eilf|elfi)\b/.test(source))
return "这一行开头的关键字拼错了:「否则」是 else,「否则如果」是 elif。"
const code = stripStrings(source)
if (/=<|=>/.test(code)) return "小于等于要写成 <=,大于等于要写成 >=,等号放在后面。"
if (CONDITION_LINE.test(source) && hasTopLevelComma(source))
return "条件之间不能用逗号连:两个都要满足用 and,满足一个就行用 or,比如 if a > 9 and a < 100:。"
return null
},
],
[
/^invalid syntax/,
() =>
"标出来的地方写法不对。对照教程检查这一行:括号、引号、冒号、运算符有没有写错、漏写或多写。",
],
]
function translate(message: string, source: string) {
for (const [pattern, render] of RULES) {
const match = message.match(pattern)
const text = match && render(match, source)
if (text) return text
}
return null
}
/**
* 选了 Python 却交了 C 代码:全库 310 条,报错各式各样(多半是 invalid syntax),
* 挨个解释毫无意义,该说的只有一句
*/
function looksLikeC(code: string) {
if (/^\s*#\s*include\b|\bint\s+main\s*\(/m.test(code)) return true
// 只凭 printf 不算:Python 代码里把 print 手误成 printf 的不少
return /\b(printf|scanf)\s*\(|\bcout\s*<</.test(code) && !/\b(print|input)\s*\(/.test(code)
}
/**
* 翻不出来(判题机自己的报错、没见过的句式)就返回 null,界面照旧显示原文。
* `code` 是学生的代码,用来认出「交的其实是 C」
*/
export function explainPythonCompileError(
errInfo: string,
code = "",
): PythonErrorExplanation | null {
const text = errInfo.trim()
const short = text.match(SHORT_RE)
if (short) {
const [, , detail, line] = short
const message = translate(detail!, "")
if (!message) return null
return {
line: Number(line),
sourceLine: null,
caret: null,
message,
punctuation: null,
}
}
const where = text.match(WHERE_RE)
const lines = text.split("\n")
const last = lines[lines.length - 1]!
const detail = last.replace(/^\w+Error: /, "")
if (detail === last) return null
let sourceLine: string | null = null
let caret: PythonErrorExplanation["caret"] = null
if (where) {
const index = lines.findIndex((line) => WHERE_RE.test(line))
const code = lines[index + 1]
const marks = lines[index + 2]
if (code?.startsWith(" ")) sourceLine = code.slice(4)
if (sourceLine !== null && marks?.startsWith(" ")) {
const from = marks.indexOf("^")
if (from >= 4) caret = { from: from - 4, to: marks.lastIndexOf("^") - 4 + 1 }
}
}
const message = looksLikeC(code)
? "这是 C 语言的代码,可提交时选的语言是 Python。把语言换成 C 再交。"
: translate(detail, sourceLine?.trim() ?? "")
if (!message) return null
const char = detail.match(/^invalid character '(.)'/u)?.[1]
return {
line: where ? Number(where[1]) : null,
sourceLine,
caret,
message,
punctuation: char && PUNCTUATION_MAP[char] ? char : null,
}
}