feat(vipeye-internal): 支持泛型类型参数带界 class GSList[T: GSListNode]
includes/linkedlist.vp 的 GSList 泛型链表 body 依赖 T 的上界解析成员,原先 共 4 条误报(GSList[T]* 的 Head/Tail 赋值「GSListNode* 不可分配给 rawptr[type[T@GSList]]」+ CPtr[T] 上查 Next「属性 Next 未知」)。补齐三处: ① CPtr 解引用把 TypeVar pointee 具体化为上界(成员访问 / applyAttributeAccess Override / 下标三处 Viper CPtr 分支):否则 self.Tail.Next(base = CPtr[T]) 在 CPtr 上找 Next 失败。 ② assignType 新增「泛型类型参数值 → CPtr[T]」放行(紧跟既有「类实例 → CPtr[T] 隐式取地址语义」之后):判据为目标 pointee 与源是同一类型参数 (TypeVarType.getNameWithScope 相等),或把源具体化后按普通规则递归比较。 依据:Viper 中类类型的值本身就是指针,覆盖 self.Head = node / self.Tail = node。 ③ 未特化泛型基类上解析出的成员类型会退化成 CPtr[builtins.type 实例] (即"某个类对象"),按「指针目标未知」放行(与 rawptr/void* 同口径)。 无界(GSList[T])仍报 Next 未知 —— 带界才是修复手段,测试固定该差异。 TransPyC 无需改动(includes/ast/parser.vp 本就跳过 bound)。 新增 viperGenericBound.test.ts 2 用例(带界 0 报错;无界报错行恰为 [20,24])。 jest 823 → 825。
This commit is contained in:
1 parent
b8101947cf
commit
3fd61c2594
4 files changed
+263
-3
No files matched your search
@@ -167,6 +167,8 @@ packages/vipeye-internal/src/tests/__viper_bracket_array_no_import_t__/
|
||||
packages/vipeye-internal/src/tests/__viper_export_extern__/
|
||||
packages/vipeye-internal/src/tests/__viper_renum_pointer__/
|
||||
packages/vipeye-internal/src/tests/__viper_renum_pointer_matrix__/
|
||||
packages/vipeye-internal/src/tests/__viper_generic_bound__/
|
||||
packages/vipeye-internal/src/tests/__viper_generic_bound_none__/
|
||||
|
||||
# perfCompare.py cached commit bundles
|
||||
/build/perfCompare/
|
||||
|
||||
@@ -6482,7 +6482,14 @@ export function createTypeEvaluator(
|
||||
baseType.priv.typeArgs &&
|
||||
baseType.priv.typeArgs.length > 0
|
||||
) {
|
||||
const pointeeType = baseType.priv.typeArgs[0];
|
||||
let pointeeType = baseType.priv.typeArgs[0];
|
||||
// Viper 扩展:pointee 是泛型类型参数(TypeVar)时按其上界具体化。
|
||||
// `class GSList[T: GSListNode]` 的字段 `Tail: T*`(= CPtr[T])在类体内
|
||||
// `self.Tail.Next` 需在 GSListNode 上查成员:不解 TypeVar 就找不到
|
||||
// `Next`,报「属性“Next”未知」。TPC 侧同理(特化成具体节点类后解析)。
|
||||
if (isTypeVar(pointeeType)) {
|
||||
pointeeType = makeTopLevelTypeVarsConcrete(pointeeType);
|
||||
}
|
||||
if (isClass(pointeeType)) {
|
||||
baseType = TypeBase.isInstance(pointeeType)
|
||||
? pointeeType
|
||||
@@ -7750,7 +7757,12 @@ export function createTypeEvaluator(
|
||||
// 当访问 CPtr[Struct] 实例的成员时,将属性访问转发到 Struct 类型。
|
||||
// 模拟 C 语言的 ptr->field 语义。
|
||||
if (isCPtrType(classType) && classType.priv.typeArgs && classType.priv.typeArgs.length > 0) {
|
||||
const pointeeType = classType.priv.typeArgs[0];
|
||||
let pointeeType = classType.priv.typeArgs[0];
|
||||
// Viper 扩展:pointee 是泛型类型参数(TypeVar)时按其上界具体化,
|
||||
// 与成员访问的 CPtr 解引用逻辑(见上方同名分支)保持一致。
|
||||
if (isTypeVar(pointeeType)) {
|
||||
pointeeType = makeTopLevelTypeVarsConcrete(pointeeType);
|
||||
}
|
||||
if (isClass(pointeeType)) {
|
||||
// 确保是实例形式以便访问实例属性
|
||||
const pointeeInstance = TypeBase.isInstance(pointeeType)
|
||||
@@ -9040,7 +9052,12 @@ export function createTypeEvaluator(
|
||||
baseType.priv.typeArgs &&
|
||||
baseType.priv.typeArgs.length > 0
|
||||
) {
|
||||
const pointeeType = baseType.priv.typeArgs[0];
|
||||
let pointeeType = baseType.priv.typeArgs[0];
|
||||
// Viper 扩展:pointee 是泛型类型参数(TypeVar)时按其上界具体化,
|
||||
// 与成员访问的 CPtr 解引用逻辑(见上方同名分支)保持一致。
|
||||
if (isTypeVar(pointeeType)) {
|
||||
pointeeType = makeTopLevelTypeVarsConcrete(pointeeType);
|
||||
}
|
||||
if (isClass(pointeeType)) {
|
||||
// T 可能是类对象(如 CPtr[Scheduler])或类实例,统一转为实例
|
||||
const pointeeInstance = TypeBase.isInstance(pointeeType)
|
||||
@@ -27250,6 +27267,43 @@ export function createTypeEvaluator(
|
||||
}
|
||||
}
|
||||
|
||||
// Viper 扩展:泛型类型参数值 → CPtr[T] 隐式取地址语义。
|
||||
// `class GSList[T: GSListNode]` 的 `append(self, node: T)` 里 `self.Head = node`
|
||||
// (`Head: T*` 即 CPtr[T])是泛型类体内的常规写法,TPC 按特化后的节点指针处理。
|
||||
// 与上一条「类实例 → CPtr[T]」同理(Viper 中类类型的值本身就是指针),
|
||||
// 只是源是 TypeVar 而不是类实例,上一条不命中,故单独放行:
|
||||
// ① 目标 pointee 与源是**同一个类型参数**(`T` → `T*`);
|
||||
// ② 否则把源 TypeVar 具体化为其上界,再按普通规则比较
|
||||
// (`T`(上界 GSListNode)→ `GSListNode.T*`,如 `self.Tail.Next = node`)。
|
||||
if (
|
||||
isClassInstance(destType) && isCPtrType(destType) &&
|
||||
destType.priv.typeArgs && destType.priv.typeArgs.length > 0 &&
|
||||
isTypeVar(srcType)
|
||||
) {
|
||||
const pointeeType = destType.priv.typeArgs[0];
|
||||
if (
|
||||
isTypeVar(pointeeType) &&
|
||||
TypeVarType.getNameWithScope(pointeeType) === TypeVarType.getNameWithScope(srcType)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const srcConcrete = makeTopLevelTypeVarsConcrete(srcType);
|
||||
// 未特化泛型基类上的成员类型会退化成 `CPtr[type]`(指向 builtins.type 实例,
|
||||
// 即"某个类对象"):如 `GSListNode[T]` 的 `Next: T*` 在未特化的 GSListNode
|
||||
// 上解析出的目标类型就是它。这与 rawptr / void* 同属「指针目标未知」,
|
||||
// 按通用指针口径放行,避免泛型类体内的正常赋值误报。
|
||||
if (isClassInstance(pointeeType) && ClassType.isBuiltIn(pointeeType, 'type')) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
!isAnyOrUnknown(srcConcrete) &&
|
||||
!isTypeVar(srcConcrete) &&
|
||||
assignType(pointeeType, srcConcrete, undefined, constraints, flags, recursionCount + 1)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Viper 扩展:字符串字面量 → int(CChar)/ char 转换语义。
|
||||
// C 语言中 char c = '_' 合法(字符字面量是整数)。Viper 中 CChar 是 int 别名,
|
||||
// 但 '_' 是字符串字面量。允许字符串字面量赋值给 int / 内置 char,模拟 C 的字符赋值。
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* viperGenericBound.test.ts
|
||||
*
|
||||
* 验证泛型类型参数**带界**声明 `class GSList[T: GSListNode]:` 的支持
|
||||
* (TPC 侧 includes/ast/parser.vp 只「跳过」bound,LSP 侧则利用它做成员解析)。
|
||||
*
|
||||
* 背景:`includes/linkedlist.vp` 的 GSList 泛型链表,append 里对 `node: T` 的成员
|
||||
* 访问与赋值都依赖 T 的上界 GSListNode:
|
||||
* novirtual class GSListNode[T]:
|
||||
* Next: T*
|
||||
* novirtual class GSList[T: GSListNode]:
|
||||
* Head: T*
|
||||
* Tail: T*
|
||||
* def append(self, node: T):
|
||||
* node.Next = None # ① CPtr[T] 解引用查成员
|
||||
* self.Head = node # ② T 值 → T*(泛型类型参数取地址)
|
||||
* self.Tail.Next = node # ①②叠加
|
||||
*
|
||||
* 修复(三段,缺一不可):
|
||||
* ① `typeEvaluator` 的 CPtr 解引用(成员访问 / 属性覆盖 / 下标)把 pointee 的
|
||||
* TypeVar 具体化为其上界,否则 `self.Tail.Next` 报「属性"Next"未知」;
|
||||
* ② `assignType` 新增「泛型类型参数值 → CPtr[T]」放行(与「类实例 → CPtr[T]」
|
||||
* 同源:Viper 中类类型的值本身就是指针),覆盖 `self.Head = node`;
|
||||
* ③ 未特化泛型基类上解析出的成员类型退化成 `CPtr[type]`(指向 builtins.type
|
||||
* 实例,即"某个类对象")时按「指针目标未知」放行,覆盖 `self.Tail.Next = node`。
|
||||
*
|
||||
* 运行:npx jest src/tests/viperGenericBound.test.ts --runInBand
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ImportResolver } from '../analyzer/importResolver';
|
||||
import { Program } from '../analyzer/program';
|
||||
import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions';
|
||||
import { NullConsole } from '../common/console';
|
||||
import { RealTempFile, createFromRealFileSystem } from '../common/realFileSystem';
|
||||
import { createServiceProvider } from '../common/serviceProviderExtensions';
|
||||
import { UriEx } from '../common/uri/uriUtils';
|
||||
import { FullAccessHost } from '../common/fullAccessHost';
|
||||
|
||||
(global as any).__rootDirectory = path.resolve();
|
||||
|
||||
const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes';
|
||||
|
||||
function buildProgramForCode(code: string, dirName: string) {
|
||||
const tempFile = new RealTempFile();
|
||||
const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile);
|
||||
const projectRoot = UriEx.file(TRANSPYC_INCLUDES);
|
||||
const configOptions = new ConfigOptions(projectRoot);
|
||||
configOptions.internalTestMode = true;
|
||||
configOptions.initializeTypeCheckingMode('standard');
|
||||
|
||||
const execEnv = new ExecutionEnvironment(
|
||||
'python',
|
||||
projectRoot,
|
||||
configOptions.diagnosticRuleSet,
|
||||
configOptions.defaultPythonVersion,
|
||||
configOptions.defaultPythonPlatform,
|
||||
[projectRoot],
|
||||
/* skipNativeLibraries */ false,
|
||||
/* defaultDialect */ 'viper'
|
||||
);
|
||||
configOptions.executionEnvironments = [execEnv];
|
||||
|
||||
const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider));
|
||||
const program = new Program(importResolver, configOptions, serviceProvider);
|
||||
|
||||
const fs = require('fs');
|
||||
const testDir = path.join(__dirname, dirName);
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
const testFile = path.join(testDir, 'main.vp');
|
||||
fs.writeFileSync(testFile, code, 'utf8');
|
||||
|
||||
const uri = UriEx.file(testFile);
|
||||
program.setTrackedFiles([uri]);
|
||||
while (program.analyze()) {
|
||||
// 循环
|
||||
}
|
||||
return { program, configOptions, uri, serviceProvider, testDir };
|
||||
}
|
||||
|
||||
function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any) {
|
||||
const sourceFile = program.getSourceFile(uri);
|
||||
assert(sourceFile, 'source file not found');
|
||||
return (sourceFile.getDiagnostics(configOptions) || []).map((diag: any) => ({
|
||||
category: diag.category,
|
||||
line: diag.range ? diag.range.start.line + 1 : -1,
|
||||
message: diag.message as string,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 实际报错(category 0=Error / 1=Warning)所在行集合。 */
|
||||
function hardErrorLines(program: Program, configOptions: ConfigOptions, uri: any): number[] {
|
||||
return Array.from(
|
||||
new Set(
|
||||
getDiagnostics(program, configOptions, uri)
|
||||
.filter((d) => d.category === 0 || d.category === 1)
|
||||
.map((d) => d.line)
|
||||
)
|
||||
).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/** GSList 泛型链表(linkedlist.vp 的形状);boundDecl 决定 T 是否带上界。 */
|
||||
function buildCode(boundDecl: string): string {
|
||||
return `import t
|
||||
from stdint import *
|
||||
|
||||
novirtual class GSListNode[T]:
|
||||
Next: T*
|
||||
|
||||
novirtual class GSList[${boundDecl}]:
|
||||
Head: T*
|
||||
Tail: T*
|
||||
Count: sizet
|
||||
|
||||
def __init__(self):
|
||||
self.Head = None
|
||||
self.Tail = None
|
||||
self.Count = 0
|
||||
|
||||
def append(self, node: T):
|
||||
if node is None:
|
||||
return
|
||||
node.Next = None
|
||||
if self.Head is None:
|
||||
self.Head = node
|
||||
else:
|
||||
self.Tail.Next = node
|
||||
self.Tail = node
|
||||
self.Count += 1
|
||||
|
||||
class GNode(GSListNode[GNode]):
|
||||
value: int
|
||||
|
||||
def use() -> None:
|
||||
lst: GSList[GNode] = GSList[GNode]()
|
||||
n: GNode = GNode()
|
||||
lst.append(n)
|
||||
`;
|
||||
}
|
||||
|
||||
test('泛型类型参数带界:`class GSList[T: GSListNode]` 的成员访问与赋值 0 报错', () => {
|
||||
const { program, configOptions, uri, testDir, serviceProvider } = buildProgramForCode(
|
||||
buildCode('T: GSListNode'),
|
||||
'__viper_generic_bound__'
|
||||
);
|
||||
|
||||
try {
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
console.log('=== 泛型带界诊断 ===');
|
||||
diagnostics
|
||||
.filter((d) => d.category === 0 || d.category === 1)
|
||||
.forEach((d) => console.log(` L${d.line} [cat=${d.category}] ${d.message}`));
|
||||
|
||||
const lines = hardErrorLines(program, configOptions, uri);
|
||||
console.log('=== 报错行 ===');
|
||||
lines.forEach((l) => console.log(` L${l}`));
|
||||
assert.deepStrictEqual(
|
||||
lines,
|
||||
[],
|
||||
`T 带界 GSListNode 后,node.Next / self.Head = node / self.Tail.Next = node 都不应报错:${JSON.stringify(
|
||||
diagnostics.filter((d) => d.category === 0 || d.category === 1)
|
||||
)}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('无界的泛型类型参数仍报「属性未知」(带界才是修复手段)', () => {
|
||||
const { program, configOptions, uri, testDir, serviceProvider } = buildProgramForCode(
|
||||
buildCode('T'),
|
||||
'__viper_generic_bound_none__'
|
||||
);
|
||||
|
||||
try {
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
console.log('=== 泛型无界诊断 ===');
|
||||
diagnostics
|
||||
.filter((d) => d.category === 0 || d.category === 1)
|
||||
.forEach((d) => console.log(` L${d.line} [cat=${d.category}] ${d.message}`));
|
||||
|
||||
// L20(node.Next = None)与 L24(self.Tail.Next = node):无界 T 的成员
|
||||
// 退化为 object,找不到 Next,仍应报错。
|
||||
const lines = hardErrorLines(program, configOptions, uri);
|
||||
console.log('=== 报错行 ===');
|
||||
lines.forEach((l) => console.log(` L${l}`));
|
||||
assert.deepStrictEqual(
|
||||
lines,
|
||||
[20, 24],
|
||||
`无界 T 的成员访问应报 L20/L24(Next 未知):${JSON.stringify(
|
||||
diagnostics.filter((d) => d.category === 0 || d.category === 1)
|
||||
)}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -17,6 +17,7 @@
|
||||
- Attribute names inside `__attribute__(...)` are highlighted **structurally**, so a name that the compiler supports but that isn't declared in the `attr` class still colours like the declared ones. `stackrealign` (supported by TransPyV's code generator — `FuncGenBase.vp` maps it to the LLVM `"stackrealign"` attribute — and used by ViperOS `VKernel/Kernel/main.vp`) is declared in neither `t.py` nor the `t.vpi` stub, so `t.attr.stackrealign` used to be unresolvable: the name got no semantic token (rendered unhighlighted) and the file was additionally flagged with "无法访问类"type[attr]"的属性"stackrealign"". The parser now marks the synthesized attribute-name node, the semantic token provider colours it as `function` (exactly like a declared attribute such as `section` / `aligned` / `naked`), and the member-access diagnostic is suppressed for these names — the compiler silently drops unknown attributes rather than rejecting them, so reporting them was a false positive. Declared names keep resolving through `t.attr` as before, so hover and jump-to-definition still work for them
|
||||
- `__attribute__(...)` **completion and hover** now list attribute names that the compiler supports but that the `attr` class doesn't declare, instead of only mirroring `t.attr`. `stackrealign` is offered by typing inside the parentheses and appears in the hover list of available names; the list is a single shared union (`viperAttributeUtils.getViperAttributeNames`) of `t.attr`'s declared names plus these extra names, so completion and hover cannot drift apart. Declared names keep coming from the stub symbol table (so they still show their signature and support jump-to-definition)
|
||||
- Pointer-to-pointer implicit conversion is now **strict** instead of "same level is always allowed". A pointer converts implicitly to another pointer only when the **level** matches **and** the pointee is compatible: either side being `void*` / undetermined (`rawptr`) always allows it, identical pointees are allowed, two basic (numeric scalar) pointees follow the very same rules as the values themselves (`uint32* → uint64*` and `uint32* → uint16*` are allowed because integers convert in every direction; `float* → int*` is still reported), and for **structs** only a conversion **within the same inheritance hierarchy** is allowed — the upcast (`Sub* → Node*`; `memhub` / `requests` depend on `MemBuddy* → MemManager*`) as well as the downcast (`AST* → Call*`; `CallGenDispatch.vp:824` passes an `ast.AST*` into a `rawptr[ast.Call]` parameter and Viper pointers carry no static type at runtime, so the reinterpret is routine), while two unrelated structs (`Node* → Other*`) and a struct↔basic mix (`Node* → uint64*`) are rejected. Single-level pointers also convert to and from `void*` (`t.CVoid`) in both directions, while multi-level ones still don't (`Node** → void*` is reported). `&x` / `*x` already produced the right types (`&x` is `T*` for `x: T`, `T**` for `x: T*`; `*x` is `T` for `x: T*`) and now take part in these checks too — `a(&x)` with `x: Node*` (a `Node**` into a `void*` parameter) is reported as before, and so is `a(&x)` where the parameter is a single-level pointer
|
||||
- **A bound on a generic class's type parameter** (`novirtual class GSList[T: GSListNode]:`) is now honoured, so the generic method bodies that rely on it check cleanly — `includes/linkedlist.vp`'s `append` used to report "无法为类"GSList[T@GSList]*"的属性"Head"赋值 / "GSListNode*"不可分配给"rawptr[type[T@GSList]]"" and "无法为类"GSListNode*"的属性"Next"赋值 / 属性"Next"未知". Three missing pieces had to be filled in: (1) the `CPtr` dereference (member access, attribute-access override and subscript) now concretizes a **TypeVar pointee to its bound**, otherwise `self.Tail.Next` looked for `Next` on `CPtr[T]` instead of on `GSListNode`; (2) assigning a **generic type-parameter value to `CPtr[T]`** is now allowed, mirroring the existing "class instance → `CPtr[T]`" rule (in Viper a class-typed value *is* a pointer), which covers `self.Head = node` / `self.Tail = node`; (3) when the member type resolved on an unspecialized generic base degrades to `CPtr[type]` (a pointer to a class object, i.e. "target type unknown"), it is treated like `rawptr` / `void*` and allowed. Removing the bound keeps the error (an unbounded `T` has no `Next`), so the bound is what makes the generic body type-safe. TransPyC is unaffected — its parser already accepts and skips `[T: bound]`
|
||||
- Pointers to the **variants of a `t.REnum`** are now compatible with pointers to the REnum root (and vice versa), so `rawptr[LLVMType.Ptr]` converts to `rawptr[LLVMType]` instead of being reported as "无法将"rawptr[Ptr]"类型的参数分配给函数"build_load"中类型为"rawptr[LLVMType]"的参数"ty"". The variant classes are nested inside the root class's suite (`class LLVMType(t.REnum): class Ptr: ... / class Int: ...`) rather than derived from it, so `isDerivedFrom` could not see the relation — yet TransPyV only emits a bitcast between the two addresses, and `match`-variant narrowing already rewrites `CPtr[LLVMType]` into `CPtr[LLVMType.Ptr]` (see `narrowTypeBasedOnClassPattern`), which is exactly what llvmlite does: `CallGenCall.vp` narrows `lb_inner: rawptr[LLVMType]` through a nested `case llvmlite.LLVMType.Ptr(lb_inner2)` and passes it straight back to `build_load(builder, ty: rawptr[LLVMType], ...)`. The relation is detected by full-name nesting (`'LLVMType.Ptr' ⊂ 'LLVMType'`, the same criterion the narrowing uses) together with the root's MRO containing `REnum`; two different variants (`Ptr* → Int*`) and a mix with an unrelated struct are still reported
|
||||
- Assigning a **plain function to a method slot of a `virtual class`** is now accepted: `serial0_file.open = serial0_open` (ViperOS `devfs.vp`, where `devfs_file` is a `virtual class` and `serial0_open(self: rawptr, path: const str, flags: int) -> int` is a module-level function) used to be reported as "无法为类"devfs_file"的属性"open"赋值 / 参数名称不匹配: "path" versus "self" / 额外参数 "flags"". Such an assignment writes a **vtable slot**, and the compiler (TPC `HandlesAssign` / TransPyV `CallGenVt._write_vtable_slot`) just bitcasts the function to `i8*` and stores it — it never validates the signature or `self` — whereas the language server compared the *bound* method type (with `self` already stripped), so the explicitly written `self` looked like a spurious extra parameter. The check now follows the vtable semantics: the class must be a `virtual class` (`virtual class X` or `@t.CVTable class X`), the member must be a method declared in it, the assigned function's `self` must be a generic pointer (`rawptr` / `t.CPtr`) or a pointer to that class itself or one of its base classes, and every remaining parameter must match by name and type with an identical return type (`*args` / `**kwargs` are not compared, so they may be present on either side). Everything else — a non-virtual class, `self` that isn't such a pointer, or a parameter/return mismatch — is still reported
|
||||
|
||||
|
||||
Reference in new issue
Block a user