fix(运行时错误): 列表练习里 append() 参数给错,说明却是「把文字当成数字、input() 读进来的是文字」,代码里根本没有 input()
全库 9228 条 Python 运行时错误在判题沙箱里重跑了一遍(9028 条复现),按真实回溯重定归类表:
- TypeError 原来只认 12 种句式,633 条掉进兜底,一律被说成「把文字当成数字」。
实际多半是函数用法错:参数个数不对(点名 append / insert 并给正确写法)、
end= 拼成 enp=、list.index(x) 该写变量名.index(x)、忘写 ()(int(input)、
input[0],约 200 条)、拿 print() / append() 的返回值去算(约 100 条)等。
兜底从 633 条降到 64 条,ValueError 从 336 降到 5(split("")、%.2f 写错、
值不在列表里)。
- input[0] 原来被说成「对一个数字用了下标」:值的类型名比句式更说明问题,
builtin_function_or_method / NoneType 先于句式判。类型名来自代码不来自输入。
- AttributeError 带上 Python 的「Did you mean」(spilt → split 一种就 188 条),
s.len() 说成函数不是方法,数字没有 split() 说顺序反了。
- 「input() 读进来的是文字」只在学生代码里真有 input( 时才说。
- C:信号 31(system("pause") 被沙箱拦 / 字符数组溢出)和 25(死循环打印)
原来只有「崩溃了」,占 C 运行时错误一半多;void main 不再说成「要写 return 0」。
已经存进库的诊断没有新的 kind,旧提交看到的是新的中性兜底话。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6306b5a919
commit
f413518a85
5 files changed
+260
-36
No files matched your search
@@ -15,31 +15,105 @@ import type { StatisticInfo } from "@oj2/contract"
|
||||
* `invalid literal for int() with base 10: '1 2'`,给学生看就等于把隐藏数据放出去 ——
|
||||
* 故意写 `raise Exception(input())` 就能一个点一个点地套。所以只存:
|
||||
* - 异常类型和归好类的 `kind`(按消息的**句式**判,不取消息里的值)
|
||||
* - NameError / AttributeError 点名的名字,而且只在它**原样出现在学生代码里**时才存
|
||||
* - NameError / AttributeError / 部分 TypeError 点名的名字,而且只在它**原样出现在
|
||||
* 学生代码里**时才存
|
||||
*/
|
||||
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
|
||||
|
||||
const IDENTIFIER = /^[A-Za-z_]\w*$/
|
||||
|
||||
/** 按消息句式细分。键是异常类型,值是 [句式, kind],从上往下取第一个对上的 */
|
||||
/**
|
||||
* 值的类型比句式更能说明错在哪。消息里的类型名是 Python 按学生代码里的值报的,
|
||||
* 不来自输入,所以可以拿来分类。三种在 TypeError 和 AttributeError 里都会出现,
|
||||
* 句式五花八门(`int(input)`、`a, b = input`、`input[0]`、`input.split()`……),
|
||||
* 按句式分会被说成「类型不对」「对数字用了下标」,其实全是同一个错:
|
||||
*/
|
||||
const VALUE_KINDS: [RegExp, string][] = [
|
||||
// 函数名后面忘了写 (),拿到的是函数本身
|
||||
[/\bbuiltin_function_or_method\b/, "func-value"],
|
||||
// print() / append() / sort() 的结果、没有 return 的函数的结果
|
||||
[/'NoneType'/, "none-value"],
|
||||
// 把 list 这个类型名当成了自己的列表变量:list[i]
|
||||
[/\btypes\.GenericAlias\b/, "type-name"],
|
||||
// map() 的结果不能直接 len()、下标
|
||||
[/'map' object is not subscriptable|object of type 'map' has no len/, "map-value"],
|
||||
]
|
||||
|
||||
/**
|
||||
* 按消息句式细分。键是异常类型,值是 [句式, kind],从上往下取第一个对上的。
|
||||
*
|
||||
* 表是照全库 9228 条 Python 运行时错误重跑出来的回溯定的(2026-09):原来只有
|
||||
* 12 种,TypeError 过半掉进兜底,被说成「把文字当成数字」,而代码里往往连 input()
|
||||
* 都没有 —— 实际是参数个数不对、参数名拼错、`list.index(x)` 这类用法错误。
|
||||
*/
|
||||
const KINDS: Record<string, [RegExp, string][]> = {
|
||||
ValueError: [
|
||||
[/^invalid literal for int\(\)/, "int-parse"],
|
||||
[/^could not convert string to float/, "float-parse"],
|
||||
[/^(not enough|too many) values to unpack/, "unpack"],
|
||||
[/^math domain error/, "math-domain"],
|
||||
[/^empty separator/, "empty-sep"],
|
||||
[/(x not in list|is not in list)$/, "not-in-list"],
|
||||
[
|
||||
/^(unsupported format character|Format specifier missing precision|Invalid format specifier|Unknown format code|incomplete format)/,
|
||||
"format-spec",
|
||||
],
|
||||
],
|
||||
TypeError: [
|
||||
...VALUE_KINDS,
|
||||
[/^can only concatenate str/, "str-concat"],
|
||||
[/^unsupported operand type/, "operand"],
|
||||
[/cannot be interpreted as an integer/, "not-int"],
|
||||
[/not supported between instances of/, "compare"],
|
||||
[/object is not callable/, "not-callable"],
|
||||
[/^type '\w+' is not subscriptable/, "type-subscript"],
|
||||
[/object is not subscriptable/, "not-subscriptable"],
|
||||
[/missing \d+ required positional argument/, "missing-arg"],
|
||||
// s[2, 2]:逗号让下标变成了一组值
|
||||
[/^(list|string|tuple) indices must be integers.*, not '?tuple'?$/, "index-comma"],
|
||||
[/^(list|string|tuple) indices must be integers/, "index-type"],
|
||||
[/^slice indices must be integers/, "slice-type"],
|
||||
[/unexpected keyword argument/, "keyword"],
|
||||
[/takes no keyword arguments/, "no-keyword"],
|
||||
[
|
||||
/^(descriptor '\w+' for '\w+' objects doesn't apply|unbound method \w+\.\w+\(\) needs an argument)/,
|
||||
"descriptor",
|
||||
],
|
||||
[
|
||||
/takes (exactly one|no|at most \d+|at least \d+|\d+)( positional)? arguments?|expected (at least |at most )?\d+ arguments?, got \d+/,
|
||||
"arg-count",
|
||||
],
|
||||
// 读进来的是文字,却拿去 % 取余、* 乘
|
||||
[/^not all arguments converted during string formatting/, "str-mod"],
|
||||
[/^can't multiply sequence by non-int/, "seq-mul"],
|
||||
[/^bad operand type for unary [+-]: 'str'/, "unary"],
|
||||
[/^cannot unpack non-iterable (int|float) object/, "unpack-number"],
|
||||
[/^(int|float)\(\) argument must be/, "convert-arg"],
|
||||
[/is not iterable/, "not-iterable"],
|
||||
[/has no len\(\)/, "no-len"],
|
||||
],
|
||||
AttributeError: [
|
||||
...VALUE_KINDS,
|
||||
[/attribute '\w+' is read-only/, "read-only"],
|
||||
// 数字没有 split() / count():多半是先 int() 再 split(),顺序反了
|
||||
[/^'(int|float)' object has no attribute/, "number-attr"],
|
||||
],
|
||||
IndexError: [[/out of range/, "index"]],
|
||||
// 程序本身编译过了,运行时的 SyntaxError 只可能来自 eval() / exec() 读到的内容
|
||||
SyntaxError: [[/./, "eval-syntax"]],
|
||||
}
|
||||
|
||||
/**
|
||||
* TypeError 细分之后点名的名字:函数名(arg-count / no-keyword,消息开头的
|
||||
* `list.append()` / `insert expected` 取最后一段)、写错的参数名(keyword)、
|
||||
* 方法名(descriptor)、类型名(type-subscript)
|
||||
*/
|
||||
const TYPE_ERROR_NAMES: Record<string, RegExp> = {
|
||||
"arg-count": /^(?:\w+\.)*(\w+)(?:\(\) takes| expected)/,
|
||||
"no-keyword": /^(?:\w+\.)*(\w+)\(\) takes no keyword/,
|
||||
keyword: /unexpected keyword argument '([^']+)'/,
|
||||
descriptor: /^(?:descriptor '|unbound method \w+\.)(\w+)/,
|
||||
"type-subscript": /^type '([^']+)'/,
|
||||
}
|
||||
|
||||
/** 名字在学生自己的代码里原样出现过才算数,否则它可能来自输入(`eval(input())`) */
|
||||
@@ -94,9 +168,21 @@ export function parsePythonTraceback(
|
||||
if (name) info.name = name
|
||||
const suggestion = message.match(/Did you mean: '([^']+)'\?/)?.[1]
|
||||
if (suggestion && IDENTIFIER.test(suggestion)) info.suggestion = suggestion
|
||||
} else if (type === "AttributeError") {
|
||||
const name = nameFromCode(message.match(/has no attribute '([^']+)'/)?.[1], code)
|
||||
// 用了 math.sqrt 却没 import math。Python 同时给了「Did you mean」时也是这个优先
|
||||
if (/Did you forget to import '/.test(message)) info.kind = "import"
|
||||
} else if (type === "TypeError" && kind && TYPE_ERROR_NAMES[kind]) {
|
||||
const name = nameFromCode(message.match(TYPE_ERROR_NAMES[kind])?.[1], code)
|
||||
if (name) info.name = name
|
||||
// 「Did you mean 'end'?」是 Python 从函数的参数表里挑的,不来自输入
|
||||
const suggestion = message.match(/Did you mean '([^']+)'\?/)?.[1]
|
||||
if (kind === "keyword" && suggestion && IDENTIFIER.test(suggestion))
|
||||
info.suggestion = suggestion
|
||||
} else if (type === "AttributeError") {
|
||||
const name = nameFromCode(message.match(/(?:has no attribute|attribute) '([^']+)'/)?.[1], code)
|
||||
if (name) info.name = name
|
||||
// 拼错方法名(spilt → split)占 AttributeError 的一半多,Python 从这个值的方法里挑的
|
||||
const suggestion = message.match(/Did you mean: '([^']+)'\?/)?.[1]
|
||||
if (suggestion && IDENTIFIER.test(suggestion)) info.suggestion = suggestion
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user