fix(vipeye-internal): 拦截 const 指针下标赋值并放行 const 指针迭代

- const 指针的 p[i] = v 下标赋值与 *p = v 解引用赋值共用
  viperConstDerefAssignment 诊断,指向只读数据一律报错;
  非 const 指针(buf: str)的下标/解引用赋值仍合法
- 迭代判据改认全 Viper 指针(CPtr / str / bytes):新增
  viperTypeContainsPointer 并替换 getTypeOfIterator / getTypeOfIterable
  里只认 CPtr 的旧写法,path: const str 的 for ch in path: 不再
  误报「“CConst”不可迭代 / 未定义 __iter__」;const int 仍不可迭代
- 补 2 个回归用例(下标读写、const 指针可迭代),jest 803 -> 805
This commit is contained in:
TermiNexus committed 2026-09-27 13:49:50 +08:00
1 parent a7a00a23d5
commit 2a7757e3b7
4 files changed
+135 -10

No files matched your search

+2
View File
@@ -152,6 +152,8 @@ packages/vipeye-internal/src/tests/__viper_static__/
packages/vipeye-internal/src/tests/__viper_bracket_array_expr__/
packages/vipeye-internal/src/tests/__viper_type_binop_bound__/
packages/vipeye-internal/src/tests/__viper_const_ptr_deref__/
packages/vipeye-internal/src/tests/__viper_const_ptr_index__/
packages/vipeye-internal/src/tests/__viper_const_ptr_iter__/
# perfCompare.py cached commit bundles
/build/perfCompare/
@@ -3300,10 +3300,8 @@ export function createTypeEvaluator(
if (ClassType.isBuiltIn(subtype, ['str', 'bytes'])) {
return getBuiltInObject(errorNode, 'char');
}
// 检查联合类型中是否包含 CPtr
const typeContainsCPtr = findSubtype(type, (s) =>
isClassInstance(s) && isCPtrType(s)) !== undefined;
if (typeContainsCPtr && (
// 检查联合类型中是否包含 Viper 指针(CPtr / str / bytes)
if (viperTypeContainsPointer(type) && (
isCConstType(subtype) || isCStaticType(subtype) || isCVolatileType(subtype) ||
isCVoidType(subtype) ||
ClassType.isBuiltIn(subtype, 'int')
@@ -3453,9 +3451,11 @@ export function createTypeEvaluator(
}
return UnknownType.create();
}
const typeContainsCPtr = findSubtype(type, (s) =>
isClassInstance(s) && isCPtrType(s)) !== undefined;
if (typeContainsCPtr && (
// 判据必须认全 Viper 指针(CPtr / str / bytes),不能只认 CPtr:
// `path: const str`(AST = `CConst | str`)里的指针成员是 str,
// 旧写法 findSubtype(isCPtrType) 匹配不上,CConst 落到末尾
// 报「“CConst”不可迭代 / 未定义 __iter__」。
if (viperTypeContainsPointer(type) && (
isCConstType(subtype) || isCStaticType(subtype) || isCVolatileType(subtype) ||
isCVoidType(subtype) ||
ClassType.isBuiltIn(subtype, 'int')
@@ -4837,6 +4837,23 @@ export function createTypeEvaluator(
case ParseNodeType.Index: {
const baseTypeResult = getTypeOfExpression(target.d.leftExpr, EvalFlags.IndexBaseDefaults);
// Viper 扩展:const 指针(如 `lfn: const str` = `CConst | str`)指向只读
// 数据 —— 下标**读**允许,下标**赋值**(`lfn[i] = c`)必须报错,
// 与解引用赋值(`*lfn = c`)共用同一诊断。
if (
IsViperDialect(AnalyzerNodeInfo.getFileInfo(target).executionEnvironment.dialect) &&
isViperConstPointerType(baseTypeResult.type)
) {
addDiagnostic(
DiagnosticRule.reportGeneralTypeIssues,
LocMessage.viperConstDerefAssignment().format({
type: printType(baseTypeResult.type),
}),
target
);
break;
}
getTypeOfIndexWithBaseType(
target,
baseTypeResult,
@@ -9554,6 +9571,19 @@ export function createTypeEvaluator(
return hasConstModifier && hasPointer;
}
// Viper 扩展:联合类型里是否存在「Viper 指针」子类型(CPtr[T] / str / bytes)。
// str / bytes 在 Viper 里就是 C 的 i8*(char*),与 CPtr 同为指针;
// 迭代 / 解引用等路径据此判断「联合里的 C 修饰符(CConst 等)只是修饰,可跳过」,
// 而 `const int`(CConst | int,无指针成员)仍按不可迭代/不可解引用报错。
function viperTypeContainsPointer(type: Type): boolean {
return (
findSubtype(
type,
(s) => isClassInstance(s) && (isCPtrType(s) || ClassType.isBuiltIn(s, ['str', 'bytes']))
) !== undefined
);
}
// Viper 扩展:解析 `t.attr.<name>` / `t.attr.<name>(...)` 属性标记 → t.CAttr 实例。
//
// 形态(与 TransPyC HandlesAssign._TAttrName / TransPyV ClassGenBase._tattr_name
@@ -15,8 +15,12 @@
* 边界:两侧不是整数的运算(如类名相加)仍须报错。
* ② `const` 指针(如 `lfn: const str` = `CConst | str`)指向只读数据:
* 解引用**读**允许(`*lfn`、`lfn[0]`);
* 解引用**赋值**必须报错(`*lfn = c`);
* 非 const 指针(`buf: str`)的 `*buf = c` 仍合法。
* 解引用**赋值**必须报错(`*lfn = c`、`lfn[i] = c`);
* 非 const 指针(`buf: str`)的 `*buf = c` / `buf[i] = c` 仍合法。
* ③ C 修饰符(CConst / CStatic / CVolatile / CVoid)在迭代 / 解引用路径上
* **只是修饰**,跳过即可:`path: const str` 可 `for ch in path:` 迭代,
* 不再误报「“CConst”不可迭代 / 未定义 __iter__」(判据须认 str / bytes,
* 不能只认 CPtr,见 viperTypeContainsPointer)。
*
* 运行:npx jest src/tests/viperBracketArrayAndConstPtr.test.ts --runInBand
*/
@@ -206,3 +210,91 @@ def mutable_ok(buf: str) -> None:
require('fs').rmSync(testDir, { recursive: true, force: true });
}
});
test('const 指针:下标读允许、下标赋值报错;非 const 指针下标赋值仍合法', () => {
const code = `def index_read(lfn: const str) -> char:
return lfn[0]
def index_write(lfn: const str) -> None:
lfn[0] = char(65)
def mutable_index_ok(buf: str) -> None:
buf[0] = char(65)
`;
const { program, configOptions, uri, serviceProvider, testDir } = buildProgramForCode(
code,
'__viper_const_ptr_index__'
);
try {
const diagnostics = getDiagnostics(program, configOptions, uri);
console.log('=== const 指针下标诊断 ===');
diagnostics.forEach((d) => console.log(` L${d.line}:${d.col} [category=${d.category}] ${d.message}`));
const constWriteDiags = diagnostics.filter((d) => d.message.includes('const 指针的解引用赋值'));
assert.strictEqual(
constWriteDiags.length,
1,
`只应报 1 处 const 下标赋值错误(lfn[0] = ...):${JSON.stringify(diagnostics)}`
);
// 报错行应为 `lfn[0] = char(65)` 所在行(第 5 行)。
assert.strictEqual(
constWriteDiags[0].line,
5,
`const 下标赋值错误应落在第 5 行:${JSON.stringify(constWriteDiags)}`
);
const otherErrors = diagnostics.filter(
(d) => d.category === 0 && !d.message.includes('const 指针的解引用赋值')
);
assert.strictEqual(
otherErrors.length,
0,
`下标读 / 非 const 指针下标赋值不应报错:${JSON.stringify(otherErrors)}`
);
} finally {
program.dispose();
serviceProvider.dispose();
require('fs').rmSync(testDir, { recursive: true, force: true });
}
});
test('const 指针可迭代:for ch in path 不报「不可迭代 / 未定义 __iter__」', () => {
const code = `import t
def walk(path: const str) -> int:
count: int = 0
for ch in path:
count = count + 1
return count
def walk_parts(parts: const t.CPtr) -> int:
total: int = 0
for p in parts:
total = total + 1
return total
`;
const { program, configOptions, uri, serviceProvider, testDir } = buildProgramForCode(
code,
'__viper_const_ptr_iter__'
);
try {
const diagnostics = getDiagnostics(program, configOptions, uri);
console.log('=== const 指针迭代诊断 ===');
diagnostics.forEach((d) => console.log(` L${d.line}:${d.col} [category=${d.category}] ${d.message}`));
const notIterableDiags = diagnostics.filter(
(d) => d.message.includes('不可迭代') || d.message.includes('__iter__')
);
assert.strictEqual(
notIterableDiags.length,
0,
`const 指针(const str 等)应可迭代,不应报不可迭代:${JSON.stringify(diagnostics)}`
);
} finally {
program.dispose();
serviceProvider.dispose();
require('fs').rmSync(testDir, { recursive: true, force: true });
}
});
+2 -1
View File
@@ -32,7 +32,8 @@
- The **`static` modifier** is now supported (LSP side only; TransPyC/TransPyV are unchanged). It is a *soft* keyword: `static def f(...)` / `static fn f(...)` marks a function (recorded on `FunctionNode.d.viperModifier`, exactly like `virtual`) and `static x: T = v` marks a variable (the annotation is rewritten to `t.CStatic | T`, the same tree TransPyC's `_parse_type_ann` builds by inserting `t.CStatic` at the left of the union, so `static SECRET: int` renders as `static int`). Anywhere else `static` stays an ordinary identifier — `static = 1`, `static: int`, `static(x)`, `def f(static: int)` and `self.static` all parse as before. Its meaning is C's **internal linkage**: a static symbol **cannot be used across modules**. Referencing one from another module is reported as `'lib.helper' 是 static 符号,禁止跨模块使用(static 仅限定义模块内可见)` for both `mod.name` / `mod.fn()` (checked in the module member-access path) and `from mod import name` (checked on the import statement), while same-module references are allowed — the decision is made per declaration file, so a self-import is not flagged either. `static` is also offered by completion and highlighted as a modifier (`storage.modifier.viper`, matched only in front of `def`-family or `name :`)
- The `f` prefix of an f-string is now highlighted as `storage.type.string.viper` (blue in the default dark theme) instead of being left unstyled. The rule matches the `r` / `u` / `b` / `f` prefix combination immediately preceding a quote, so it covers `f"..."`, `f'''...'''` and mixed spellings like `rf"..."`; a bare `r`/`f`/`b` identifier not followed by a quote is unaffected, and the string body keeps the usual `string.quoted.*` scope
- The **size of a fixed-length array type may be any integer constant expression** now: `part_types: [MAX_VOLUMES * MAX_PARTITIONS]uint8`, `part_lit: [MAX_VOLUMES * 2]uint8`, `part_left: [MAX_VOLUMES - 1]uint8` and `part_shift: [1 << 3]uint8` all parse and check cleanly. Two bugs made this fail before: the parser parsed the `[SIZE]` part while still flagged as a type annotation, so `_parseArithmeticTerm` deliberately stopped at the `*` (it is the C pointer suffix marker there) — the bracket was reported as "“[" 未关闭" and the leftover `* MAX_PARTITIONS]uint8` as "类型表达式中不允许使用解包运算符"; and the type evaluator then evaluated that arithmetic with `EvalFlags.TypeExpression`, reporting "类型表达式中不允许使用二元运算符". The size is now parsed as the value expression it is (the flag is cleared for its duration), and the evaluator allows arithmetic/bitwise operators in a type expression when **both operands are Viper integers** (the case the enclosing comment already described, e.g. `t.CArray[T, MAX_ORDER + 1]`); non-integer operands (`C1 + C2`) are still reported
- **`const` pointers are read-only for dereference assignment**: `lfn: const str` (`CConst | str`, i.e. `const char *`) now allows dereferencing — `*lfn` and `lfn[0]` evaluate to `char` — but writing through the dereference (`*lfn = char(65)`) is reported as "不能给 const 指针的解引用赋值("const str" 指向的数据为只读)". Previously the dereference itself was rejected with a bogus "类型"const str"不支持运算符"*"" (the `CConst` modifier in the union was mistaken for a non-pointer operand), and there was no read-only check at all. A non-`const` pointer (`buf: str`) still accepts `*buf = char(65)`
- **`const` pointers are read-only for dereference assignment**: `lfn: const str` (`CConst | str`, i.e. `const char *`) now allows dereferencing — `*lfn` and `lfn[0]` evaluate to `char` — but writing through the dereference is reported as "不能给 const 指针的解引用赋值("const str" 指向的数据为只读)". This covers **both** spellings, `*lfn = char(65)` (dereference) and `lfn[0] = char(65)` (**subscript assignment**), which share the same diagnostic. Previously the dereference itself was rejected with a bogus "类型"const str"不支持运算符"*"" (the `CConst` modifier in the union was mistaken for a non-pointer operand), and there was no read-only check at all. A non-`const` pointer (`buf: str`) still accepts `*buf = char(65)` and `buf[0] = char(65)`
- C type descriptors are **skipped** when checking iterability, so a `const` pointer can be iterated: `for ch in path:` with `path: const str` (and `const t.CPtr`) no longer reports "“CConst”不可迭代 / 未定义“__iter__”方法". The predicate now recognizes every Viper pointer (CPtr, `str`, `bytes`) rather than only `CPtr`, which is why `const str` had been missed — `CConst` fell through to the "not iterable" path
## 1.1.425