基本对齐了现阶段 Viper 的所有必要功能,但还缺少反向测试
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
This commit is contained in:
1 parent
b47f43d5f2
commit
be9ff90d11
14 files changed
+958
-40
No files matched your search
File renamed without changes.
File renamed without changes.
@@ -2,7 +2,7 @@
|
||||
"name": "vipeye-internal",
|
||||
"displayName": "vipeye",
|
||||
"description": "Type checker for the Viper language",
|
||||
"version": "1.1.411",
|
||||
"version": "1.1.412",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"files": [
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import { appendArray } from '../common/collectionUtils';
|
||||
import { IsViperDialect } from '../common/dialect';
|
||||
import { ArgCategory, CallNode, DecoratorNode, FunctionNode, ParamCategory, ParseNodeType } from '../parser/parseNodes';
|
||||
import { getDeclaration, getFileInfo } from './analyzerNodeInfo';
|
||||
import {
|
||||
@@ -412,6 +413,22 @@ function getTypeOfDecorator(evaluator: TypeEvaluator, node: DecoratorNode, funct
|
||||
|
||||
const decoratorTypeResult = evaluator.getTypeOfExpression(node.d.expr, flags);
|
||||
|
||||
// Viper 扩展:t.CVTable / t.Object / t.NoVTable 是类型透传装饰器
|
||||
// (签名 `(cls: Type[T]) -> Type[T]`)。在 Viper 方言下,泛型 `Type[T]`
|
||||
// 的参数绑定在 stub 求值中可能退化为裸 `Type`,导致被装饰类被解析为
|
||||
// `Type` 而非 `type[Class]`,进而丢失全部成员(如 __enter__/__exit__)。
|
||||
// 这些装饰器语义上就是"返回原类",因此直接透传输入类型。
|
||||
if (IsViperDialect(getFileInfo(node).executionEnvironment.dialect)) {
|
||||
const decExpr = node.d.expr as any;
|
||||
const decName =
|
||||
decExpr?.nodeType === 'Call'
|
||||
? decExpr?.d?.leftExpr?.d?.member?.d?.value ?? decExpr?.d?.leftExpr?.d?.value
|
||||
: decExpr?.d?.member?.d?.value ?? decExpr?.d?.value;
|
||||
if (decName === 'CVTable' || decName === 'Object' || decName === 'NoVTable') {
|
||||
return functionOrClassType;
|
||||
}
|
||||
}
|
||||
|
||||
// Special-case the combination of a classmethod decorator applied
|
||||
// to a property. This is allowed in Python 3.9, but it's not reflected
|
||||
// in the builtins.pyi stub for classmethod.
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import { CodeFlowReferenceExpressionNode } from './codeFlowTypes';
|
||||
import { addConstraintsForExpectedType } from './constraintSolver';
|
||||
import { ConstraintTracker } from './constraintTracker';
|
||||
import { DeclarationType } from './declaration';
|
||||
import { getTypeVarScopesForNode, isMatchingExpression } from './parseTreeUtils';
|
||||
import { getTypedDictMembersForClass } from './typedDicts';
|
||||
import { EvalFlags, TypeEvaluator, TypeResult } from './typeEvaluatorTypes';
|
||||
@@ -54,6 +55,9 @@ import {
|
||||
isAnyOrUnknown,
|
||||
isClass,
|
||||
isClassInstance,
|
||||
isCPtrType,
|
||||
isCTypeSubclassType,
|
||||
isCVoidType,
|
||||
isInstantiableClass,
|
||||
isNever,
|
||||
isSameWithoutLiteralValue,
|
||||
@@ -664,6 +668,42 @@ function getPositionalMatchArgNames(evaluator: TypeEvaluator, type: ClassType):
|
||||
}
|
||||
}
|
||||
|
||||
// Viper 扩展:REnum 变体类的 match 位置参数。
|
||||
// Viper 的 REnum(t.REnum)嵌套类变体(如 class LLVMType 内的 class Ptr)
|
||||
// 没有 __match_args__,但 match 模式 `LLVMType.Ptr(pointee)` 允许按声明顺序
|
||||
// 解构其类型注解字段。Pyright 默认对无 __match_args__ 的类只允许 0 个位置参数,
|
||||
// 会把 `LLVMType.Ptr(pointee)` 误报为"位置模式太多"。
|
||||
// 此处返回类中所有类型注解字段名作为位置参数名。
|
||||
// 判断依据:类自身是 CType 子类(CPtr/CVoid),或声明在 CType 子类内部
|
||||
// (REnum 的嵌套类变体)。
|
||||
const isNestedInCType =
|
||||
(() => {
|
||||
const decl = type.shared.declaration;
|
||||
const classNode = decl?.type === DeclarationType.Class ? decl.node : undefined;
|
||||
if (!classNode) {
|
||||
return false;
|
||||
}
|
||||
// 嵌套类的直接父节点是外层类的 Suite(nodeType=50),
|
||||
// Suite 的父节点才是外层 ClassNode(nodeType=10)。
|
||||
const parentNode = classNode.parent;
|
||||
const grandparentNode =
|
||||
parentNode?.nodeType === ParseNodeType.Suite ? parentNode.parent : undefined;
|
||||
return grandparentNode?.nodeType === ParseNodeType.Class;
|
||||
})();
|
||||
if (isCTypeSubclassType(type) || isCPtrType(type) || isCVoidType(type) || isNestedInCType) {
|
||||
const fieldNames: string[] = [];
|
||||
const fields = type.shared.fields;
|
||||
if (fields) {
|
||||
for (const [name, symbol] of fields) {
|
||||
const decls = symbol.getDeclarations();
|
||||
if (decls.some((decl) => decl.type === DeclarationType.Variable)) {
|
||||
fieldNames.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -2655,7 +2655,6 @@ export function createTypeEvaluator(
|
||||
if (!boundMethodResult || boundMethodResult.typeErrors) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isFunctionOrOverloaded(boundMethodResult.type)) {
|
||||
return boundMethodResult.type;
|
||||
}
|
||||
@@ -5343,10 +5342,19 @@ export function createTypeEvaluator(
|
||||
if (name === 'reveal_type' || name === 'reveal_locals') {
|
||||
type = AnyType.create();
|
||||
} else {
|
||||
// Viper 扩展:前向引用支持。
|
||||
// Viper 是 AOT 语言,结构体/函数不在乎定义顺序(类似 C 语言的前向声明)。
|
||||
// 当符号未找到时(可能在更后方定义),不报 undefinedVariable 错误,
|
||||
// 而是返回 UnknownType,允许后续定义的符号被引用。
|
||||
// Viper 扩展:前向引用支持(收紧版)。
|
||||
// Viper 是 AOT 语言,函数/类的定义顺序无关(类似 C 语言的前向声明),
|
||||
// 但绑定器在分析开始前已预扫描全部声明,真实的前向引用目标
|
||||
// (如 viperlib.vp 中 sprintf 引用后方的 vsnprintf)会出现在符号表中
|
||||
// (symbolWithScope 非空),不会进入此分支。
|
||||
// 走到这里的名字(symbolWithScope 为空)在整个文件+可见作用域中
|
||||
// 均无任何声明,属于真正的未定义符号(如拼写错误 count1),
|
||||
// 此时应报 undefined variable,而不是静默返回 Unknown。
|
||||
addDiagnostic(
|
||||
DiagnosticRule.reportUndefinedVariable,
|
||||
LocMessage.symbolIsUndefined().format({ name }),
|
||||
node
|
||||
);
|
||||
type = UnknownType.create();
|
||||
}
|
||||
}
|
||||
@@ -8615,7 +8623,7 @@ export function createTypeEvaluator(
|
||||
// 返回特殊化为 Unknown 的 CPtr[Unknown] 实例,使任何 CPtr[X] 特殊化指针
|
||||
// 都能通过泛型匹配赋值(Unknown 匹配一切类型参数)。
|
||||
// 查找失败返回 undefined。
|
||||
function getViperCPtrType(node: ExpressionNode): Type | undefined {
|
||||
function getViperCPtrType(node: ParseNode): Type | undefined {
|
||||
const tSymbolWithScope = lookUpSymbolRecursive(node, 't', /* honorCodeFlow */ true);
|
||||
if (tSymbolWithScope) {
|
||||
const tEffectiveType = getEffectiveTypeOfSymbolForUsage(
|
||||
@@ -8640,6 +8648,28 @@ export function createTypeEvaluator(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Viper 扩展:判断类型是否为 REnum 变体类。
|
||||
// Viper 的 REnum(t.REnum)类内部定义嵌套类变体(如 class LLVMType 内的
|
||||
// class Int/Ptr/Func/Struct 等)。这些嵌套类不继承 CType,但语义上是
|
||||
// REnum 的 tagged-union 变体:match 模式 `LLVMType.Ptr(pointee)` 按字段
|
||||
// 顺序解构,构造调用 `LLVMType.Int(bits)` 按字段顺序传参。
|
||||
// 判断依据:类声明嵌套在另一个类(外层 REnum)的 suite 中。
|
||||
function isViperREnumVariantType(type: Type): boolean {
|
||||
if (!isClass(type)) {
|
||||
return false;
|
||||
}
|
||||
const decl = type.shared.declaration;
|
||||
const classNode = decl?.type === DeclarationType.Class ? decl.node : undefined;
|
||||
if (!classNode) {
|
||||
return false;
|
||||
}
|
||||
// 嵌套类的直接父节点是外层类的 Suite(nodeType=50),
|
||||
// Suite 的父节点才是外层 ClassNode(nodeType=10)。
|
||||
const parentNode = classNode.parent;
|
||||
const grandparentNode = parentNode?.nodeType === ParseNodeType.Suite ? parentNode.parent : undefined;
|
||||
return grandparentNode?.nodeType === ParseNodeType.Class;
|
||||
}
|
||||
|
||||
// Viper 扩展:构造带 t.CChar 类型别名信息的 int 类型(表示 C 的 char 类型)。
|
||||
// Viper 中 str/bytes 是 i8* 指针,下标访问/迭代/字符运算返回 t.CChar(int 别名)。
|
||||
// 优先从 t 模块查找 CChar 类型别名以保留显示信息;找不到时回退为
|
||||
@@ -9285,6 +9315,34 @@ export function createTypeEvaluator(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Viper 扩展:判断调用节点是否为 stdarg.va_arg 调用。
|
||||
// va_arg 通过 `from stdarg import va_arg` 引入,baseTypeResult 为 FunctionType,
|
||||
// 其 shared.name 为 'va_arg'。TPV 将 va_arg 视为 builtin,第二参数是类型名。
|
||||
function isViperVaArgCall(node: CallNode, baseTypeResult: TypeResult): boolean {
|
||||
return (
|
||||
isFunction(baseTypeResult.type) &&
|
||||
baseTypeResult.type.shared.name === 'va_arg' &&
|
||||
baseTypeResult.type.shared.moduleName === 'stdarg'
|
||||
);
|
||||
}
|
||||
|
||||
// Viper 扩展:判断调用节点是否为 c 模块的 Deref/Load/DerefAs/Set 调用。
|
||||
// 这些函数在 c.vpi 存根中声明为 int 参数,但 Viper 语义接受任意指针表达式。
|
||||
function isViperCDerefCall(node: CallNode, baseTypeResult: TypeResult): boolean {
|
||||
const leftExpr = node.d.leftExpr;
|
||||
if (leftExpr.nodeType !== ParseNodeType.MemberAccess) {
|
||||
return false;
|
||||
}
|
||||
const memberName = leftExpr.d.member.d.value;
|
||||
if (memberName !== 'Deref' && memberName !== 'Load' && memberName !== 'DerefAs' && memberName !== 'Set') {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
isFunction(baseTypeResult.type) &&
|
||||
baseTypeResult.type.shared.moduleName === 'c'
|
||||
);
|
||||
}
|
||||
|
||||
// Viper 扩展:检查类型注解 AST 节点是否包含 CPtr 引用。
|
||||
// 用于判断循环引用是否被指针打破(如 next: 'Node' | t.CPtr)。
|
||||
// 递归遍历类型注解表达式,查找名称为 CPtr/CNeedPtr/CAutoPtr/CArrayPtr 的标识符。
|
||||
@@ -9618,6 +9676,18 @@ export function createTypeEvaluator(
|
||||
} else {
|
||||
typeResult.type = instanceType;
|
||||
}
|
||||
} else if (
|
||||
// Viper 扩展:拦截 REnum 变体类构造调用。
|
||||
// Viper 的 REnum 变体(如 LLVMType.Int(bits)、LLVMType.Struct(fields, fcount, name))
|
||||
// 没有 __init__,但构造调用按字段声明顺序接收参数(tagged-union 语义)。
|
||||
// Pyright 默认对无 __init__ 的类只允许 0 个位置参数,会把
|
||||
// `LLVMType.Int(1)` 误报为"应为 0 个位置参数"。
|
||||
// 此处跳过参数校验,直接返回变体类的实例类型。
|
||||
IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) &&
|
||||
isInstantiableClass(baseTypeResult.type) &&
|
||||
isViperREnumVariantType(baseTypeResult.type)
|
||||
) {
|
||||
typeResult.type = ClassType.cloneAsInstance(baseTypeResult.type);
|
||||
} else if (
|
||||
// Viper 扩展:拦截 __sizeof__ 调用。
|
||||
// Viper 中 __sizeof__ 语义是 C 语言的 sizeof(Type) -> int,
|
||||
@@ -9630,6 +9700,48 @@ export function createTypeEvaluator(
|
||||
// Viper 的 __sizeof__ 是类方法(sizeof(Type)),返回类型大小(字节数)。
|
||||
// 跳过 object.__sizeof__ 的实例方法参数检查,直接返回 int。
|
||||
typeResult.type = getBuiltInObject(node, 'int');
|
||||
} else if (
|
||||
// Viper 扩展:拦截 stdarg.va_arg 调用。
|
||||
// TPV 将 va_arg 视为 builtin(见 HandlesExprBuiltin._HandleVaArgLlvm),
|
||||
// 第二参数是类型名(如 t.CInt64T / t.CPtr / t.CInt64T | t.CPtr),
|
||||
// 而非 CPtr 值。若按 stdarg.vp 的声明 `va_arg(args: t.CPtr, type: t.CPtr)`
|
||||
// 做普通参数校验,会把类型名当作 CPtr 值检查而误报。
|
||||
// 返回类型 = 第二参数类型形式对应的实例类型;无第二参数时返回 Unknown
|
||||
// (由赋值上下文注解推断,如 `ptr_i: t.CInt = va_arg(ap)`)。
|
||||
IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) &&
|
||||
isViperVaArgCall(node, baseTypeResult)
|
||||
) {
|
||||
if (node.d.args.length >= 2) {
|
||||
const typeExpr = node.d.args[1].d.valueExpr;
|
||||
const typeFormResult = getTypeOfExpression(typeExpr, EvalFlags.TypeExpression);
|
||||
const typeFormType = stripTypeForm(typeFormResult.type);
|
||||
typeResult.type = isInstantiableClass(typeFormType)
|
||||
? convertToInstance(typeFormType)
|
||||
: typeFormType;
|
||||
} else {
|
||||
typeResult.type = UnknownType.create();
|
||||
}
|
||||
} else if (
|
||||
// Viper 扩展:拦截 c.Deref / c.Load / c.DerefAs / c.Set 调用。
|
||||
// TPV 将 Deref 等视为 builtin(见 lib/core/Handles/HandlesSpecialCall.py),
|
||||
// 指针参数接受任意指针表达式(str/bytes/t.CPtr 等,i8*)。
|
||||
// c.vpi 存根中 `Deref(ptr: int)` 的 int 参数声明与 Viper 语义不符
|
||||
// (Viper 中 str 是 i8*,str→int 不允许隐式转换),
|
||||
// 按普通校验会误报 "str 不可分配给 int"。
|
||||
// 此处跳过参数检查,返回:Deref → 类型参数 T 或 Unknown;其余 → Unknown。
|
||||
IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) &&
|
||||
isViperCDerefCall(node, baseTypeResult)
|
||||
) {
|
||||
if (node.d.leftExpr.nodeType === ParseNodeType.MemberAccess &&
|
||||
node.d.leftExpr.d.member.d.value === 'Deref' &&
|
||||
node.d.args.length >= 1) {
|
||||
// c.Deref(ptr) → 解引用结果。返回类型为 c.Deref 的 T 类型参数
|
||||
// 对应的实例类型(从 c.vpi 的 `def Deref(ptr: int) -> T` 推导)。
|
||||
// 简化:返回 Unknown,由赋值上下文推断(如 `v: int = c.Deref(p)`)。
|
||||
typeResult.type = UnknownType.create();
|
||||
} else {
|
||||
typeResult.type = UnknownType.create();
|
||||
}
|
||||
} else {
|
||||
const callResult = validateCallArgs(
|
||||
node,
|
||||
@@ -20534,7 +20646,27 @@ export function createTypeEvaluator(
|
||||
!isOptionalType(type) &&
|
||||
!AnalyzerNodeInfo.getFileInfo(param).diagnosticRuleSet.strictParameterNoneValue
|
||||
) {
|
||||
return combineTypes([type, getNoneType()]);
|
||||
// Viper 扩展:None 在 Viper 中对应 C 的 NULL(空指针),而不是
|
||||
// Python 语义下的 Optional 标记。当参数注解本身就是指针类类型
|
||||
// (str/bytes 是 i8*、CPtr、CVoid、函数指针、纯 C 修饰符),
|
||||
// `= None` 只是表示默认值为空指针,无需展开为 `X | None`。
|
||||
// 若展开,`app_deps: str = None` 会变成 `str | None`,
|
||||
// 导致函数体内 app_deps[0] 这类下标访问误报 reportOptionalSubscript。
|
||||
// 仅对值类型(int/bool/float 等)保留 PEP 484 展开语义。
|
||||
const isViperPtrLike = (() => {
|
||||
const variants = isUnion(type) ? type.priv.subtypes : [type];
|
||||
return variants.some(
|
||||
(variant) =>
|
||||
isCPtrType(variant) ||
|
||||
isCVoidType(variant) ||
|
||||
isPureCModifierType(variant) ||
|
||||
isFunction(variant) ||
|
||||
(isClassInstance(variant) && ClassType.isBuiltIn(variant, ['str', 'bytes']))
|
||||
);
|
||||
})();
|
||||
if (!isViperPtrLike) {
|
||||
return combineTypes([type, getNoneType()]);
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
@@ -20701,6 +20833,19 @@ export function createTypeEvaluator(
|
||||
return ClassType.cloneForPacked(type);
|
||||
}
|
||||
|
||||
// Viper 扩展:`*args` 在函数体内绑定为 `va_list`(即 `CPtr[Unknown]`,
|
||||
// 对应 LLVM 的 i8*),而非 Python 语义下的 tuple。TPV 在函数入口对
|
||||
// `*args` 变量执行 va_start 并注册为裸指针(见 HandlesFunctions.py:
|
||||
// `Gen._direct_values[vararg_name] = va_list_ptr`),因此函数体内
|
||||
// 直接传递 `args` 给另一个 variadic 函数(如 vsnprintf 的 `ap` 参数)
|
||||
// 是合法的指针透传,不应报 tuple 类型不兼容。
|
||||
if (IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect)) {
|
||||
const cptrType = getViperCPtrType(node);
|
||||
if (cptrType) {
|
||||
return cptrType;
|
||||
}
|
||||
}
|
||||
|
||||
return makeTupleObject(evaluatorInterface, [{ type, isUnbounded: !isTypeVarTuple(type) }]);
|
||||
}
|
||||
|
||||
@@ -21276,11 +21421,20 @@ export function createTypeEvaluator(
|
||||
}
|
||||
|
||||
if (isClass(subtype)) {
|
||||
// Viper 扩展:`__exit__` 无需异常参数(Viper 无异常机制),
|
||||
// 签名仅为 `def __exit__(self):`。若按 CPython 惯例传
|
||||
// (exc_type, exc_val, exc_tb) 三个参数会报参数过多,
|
||||
// 导致 magic method 调用失败并误报 "not usable with"。
|
||||
const anyArg: TypeResult = { type: AnyType.create() };
|
||||
const exitArgList: TypeResult[] = IsViperDialect(
|
||||
AnalyzerNodeInfo.getFileInfo(node.d.expr).executionEnvironment.dialect
|
||||
)
|
||||
? []
|
||||
: [anyArg, anyArg, anyArg];
|
||||
const exitTypeResult = getTypeOfMagicMethodCall(
|
||||
subtype,
|
||||
exitMethodName,
|
||||
[anyArg, anyArg, anyArg],
|
||||
exitArgList,
|
||||
node.d.expr,
|
||||
/* inferenceContext */ undefined,
|
||||
exitDiag
|
||||
@@ -23104,7 +23258,23 @@ export function createTypeEvaluator(
|
||||
}
|
||||
|
||||
if (usageScopeNode === declScopeNode) {
|
||||
if (!isFlowPathBetweenNodes(declNode, node)) {
|
||||
// Viper 扩展:类/函数声明的引用不受代码流可达性限制。
|
||||
// Viper 是 AOT 语言,支持前向引用(类似 C 语言的前向声明)。
|
||||
// 例如 `class ParamNode(GSListNode[ParamNode])` 中基类类型参数
|
||||
// 引用自身(ParamNode 声明节点在引用节点之后,代码流不可达),
|
||||
// 以及函数体引用后方定义的函数/类。这些都应视为合法引用,
|
||||
// 跳过代码流过滤,否则会误报未定义。
|
||||
const declIsClassOrFunc =
|
||||
decl.type === DeclarationType.Class ||
|
||||
decl.type === DeclarationType.Function ||
|
||||
decl.type === DeclarationType.TypeAlias;
|
||||
if (
|
||||
!isFlowPathBetweenNodes(declNode, node) &&
|
||||
!(
|
||||
IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) &&
|
||||
declIsClassOrFunc
|
||||
)
|
||||
) {
|
||||
// If there was no control flow path from the usage back
|
||||
// to the source, see if the usage node is reachable by
|
||||
// any path.
|
||||
@@ -24102,13 +24272,47 @@ export function createTypeEvaluator(
|
||||
}
|
||||
}
|
||||
|
||||
return applyLoaderActionsToModuleType(
|
||||
moduleType,
|
||||
// Viper 扩展:合并同一符号的所有多段 import 声明的 implicitImports。
|
||||
// 例如 `import os.path` + `import os._win32` + `import os._posix` 时,
|
||||
// os 符号有多个 Alias 声明,每个声明只记录各自的子模块。Python/Viper
|
||||
// 的模块加载器会把所有子模块都挂载到父模块命名空间,因此这里把所有
|
||||
// 声明的 implicitImports 合并后再应用 loader actions,避免 `os._win32`
|
||||
// 被误报为"不是模块 os 的已知属性"。
|
||||
const baseLoader =
|
||||
resolvedDecl.symbolName && resolvedDecl.submoduleFallback
|
||||
? resolvedDecl.submoduleFallback
|
||||
: resolvedDecl,
|
||||
importLookup
|
||||
);
|
||||
: resolvedDecl;
|
||||
const mergedImplicitImports = new Map(baseLoader.implicitImports ?? []);
|
||||
symbol.getDeclarations().forEach((siblingDecl) => {
|
||||
if (siblingDecl.type !== DeclarationType.Alias) {
|
||||
return;
|
||||
}
|
||||
const siblingResolved = resolveAliasDeclaration(siblingDecl, /* resolveLocalNames */ true, {
|
||||
allowExternallyHiddenAccess: AnalyzerNodeInfo.getFileInfo(siblingDecl.node).isStubFile,
|
||||
});
|
||||
if (!siblingResolved || siblingResolved.type !== DeclarationType.Alias) {
|
||||
return;
|
||||
}
|
||||
const siblingLoader =
|
||||
siblingResolved.symbolName && siblingResolved.submoduleFallback
|
||||
? siblingResolved.submoduleFallback
|
||||
: siblingResolved;
|
||||
siblingLoader.implicitImports?.forEach((action, name) => {
|
||||
if (!mergedImplicitImports.has(name)) {
|
||||
mergedImplicitImports.set(name, action);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let mergedLoaderActions = baseLoader;
|
||||
if (mergedImplicitImports.size > (baseLoader.implicitImports?.size ?? 0)) {
|
||||
mergedLoaderActions = {
|
||||
...baseLoader,
|
||||
implicitImports: mergedImplicitImports,
|
||||
};
|
||||
}
|
||||
|
||||
return applyLoaderActionsToModuleType(moduleType, mergedLoaderActions, importLookup);
|
||||
}
|
||||
|
||||
const declaredType = getTypeForDeclaration(resolvedDecl);
|
||||
@@ -25999,6 +26203,59 @@ export function createTypeEvaluator(
|
||||
if (destIsStrBytes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Viper 扩展:数组名退化为指向首元素的指针(C 语义 array-to-pointer decay)。
|
||||
// 栈上数组 t.CArray[T, N] 作为实参传递时退化为 T*(CPtr[T]),如
|
||||
// `transform(block: CPtr[CUInt8T])` 接收 `buf: CArray[CUInt8T, CDefine]`。
|
||||
// 目标可以是 CPtr[T] 或 CVoid(void* 通用指针)。
|
||||
if (isClassInstance(destType) && (isCPtrType(destType) || isCVoidType(destType))) {
|
||||
const destTypeArgs = destType.priv.typeArgs;
|
||||
if (isCVoidType(destType)) {
|
||||
return true;
|
||||
}
|
||||
const srcTypeArgs = (srcType as ClassType).priv.typeArgs;
|
||||
const srcElemType = srcTypeArgs && srcTypeArgs.length > 0 ? srcTypeArgs[0] : undefined;
|
||||
const destPointee =
|
||||
destTypeArgs && destTypeArgs.length > 0 ? destTypeArgs[0] : undefined;
|
||||
if (srcElemType && destPointee) {
|
||||
// 数组元素类型与指针目标类型兼容即可(同类或可隐式转换)。
|
||||
return assignType(
|
||||
destPointee,
|
||||
srcElemType,
|
||||
diag,
|
||||
constraints,
|
||||
flags | AssignTypeFlags.SkipRecursiveTypeCheck,
|
||||
recursionCount + 1
|
||||
);
|
||||
}
|
||||
return srcElemType !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Viper 扩展:指针 → CArray 形参调整(C 语义 parameter adjustment,与
|
||||
// array-to-pointer decay 互逆)。C 中函数参数声明为数组 T[N] 时自动调整为
|
||||
// 指针 T*,因此实参指针(CPtr[T] / void*)可以传给"数组"形参。例如
|
||||
// `transform(block: t.CArray[t.CUInt8T, N])` 接收 `data: t.CUInt8T | t.CPtr`。
|
||||
if (isCArrayType(destType) && isClassInstance(srcType) && (isCPtrType(srcType) || isCVoidType(srcType))) {
|
||||
if (isCVoidType(srcType)) {
|
||||
return true;
|
||||
}
|
||||
const srcTypeArgs = srcType.priv.typeArgs;
|
||||
const srcPointee = srcTypeArgs && srcTypeArgs.length > 0 ? srcTypeArgs[0] : undefined;
|
||||
const destTypeArgs = (destType as ClassType).priv.typeArgs;
|
||||
const destElemType = destTypeArgs && destTypeArgs.length > 0 ? destTypeArgs[0] : undefined;
|
||||
if (srcPointee && destElemType) {
|
||||
// 指针目标类型与数组元素类型兼容即可。
|
||||
return assignType(
|
||||
destElemType,
|
||||
srcPointee,
|
||||
diag,
|
||||
constraints,
|
||||
flags | AssignTypeFlags.SkipRecursiveTypeCheck,
|
||||
recursionCount + 1
|
||||
);
|
||||
}
|
||||
return srcPointee !== undefined;
|
||||
}
|
||||
|
||||
// Viper 扩展:函数名退化为函数指针(C 语义)。
|
||||
@@ -26191,11 +26448,15 @@ export function createTypeEvaluator(
|
||||
}
|
||||
if (pointeeAsClass) {
|
||||
const inheritanceChain: InheritanceChain = [];
|
||||
const isDerivedFrom = ClassType.isDerivedFrom(
|
||||
srcType,
|
||||
pointeeAsClass,
|
||||
inheritanceChain
|
||||
);
|
||||
// 统一比较形式:baseClasses 里存的是 instantiable 基类,
|
||||
// 与 destPointee 统一转成 instantiable 形式再比较,避免
|
||||
// isSameGenericClass 因 instance/instantiable 差异误判。
|
||||
const srcForCompare = convertToInstantiable(srcType);
|
||||
const pointeeForCompare = convertToInstantiable(pointeeAsClass);
|
||||
const isDerivedFrom =
|
||||
isInstantiableClass(srcForCompare) &&
|
||||
isInstantiableClass(pointeeForCompare) &&
|
||||
ClassType.isDerivedFrom(srcForCompare, pointeeForCompare, inheritanceChain);
|
||||
if (isDerivedFrom) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -635,6 +635,7 @@ export function getOffDiagnosticRuleSet(dialect?: LanguageDialect): DiagnosticRu
|
||||
diagSettings.reportDeprecated = 'none';
|
||||
diagSettings.reportMissingTypeStubs = 'none';
|
||||
diagSettings.reportMissingModuleSource = 'none';
|
||||
diagSettings.reportMissingImports = 'warning';
|
||||
diagSettings.reportTypeCommentUsage = 'none';
|
||||
diagSettings.reportIncompleteStub = 'none';
|
||||
diagSettings.reportInvalidStubStatement = 'none';
|
||||
@@ -742,7 +743,7 @@ export function getBasicDiagnosticRuleSet(dialect?: LanguageDialect): Diagnostic
|
||||
reportImplicitStringConcatenation: 'none',
|
||||
reportUnboundVariable: 'error',
|
||||
reportUnhashable: 'error',
|
||||
reportUndefinedVariable: 'error',
|
||||
reportUndefinedVariable: 'warning',
|
||||
reportInvalidStubStatement: 'none',
|
||||
reportIncompleteStub: 'none',
|
||||
reportUnsupportedDunderAll: 'warning',
|
||||
@@ -867,7 +868,7 @@ export function getStandardDiagnosticRuleSet(dialect?: LanguageDialect): Diagnos
|
||||
reportImplicitStringConcatenation: 'none',
|
||||
reportUnboundVariable: 'error',
|
||||
reportUnhashable: 'error',
|
||||
reportUndefinedVariable: 'error',
|
||||
reportUndefinedVariable: 'warning',
|
||||
reportInvalidStubStatement: 'none',
|
||||
reportIncompleteStub: 'none',
|
||||
reportUnsupportedDunderAll: 'warning',
|
||||
@@ -885,6 +886,7 @@ export function getStandardDiagnosticRuleSet(dialect?: LanguageDialect): Diagnos
|
||||
diagSettings.reportDeprecated = 'none';
|
||||
diagSettings.reportMissingTypeStubs = 'none';
|
||||
diagSettings.reportMissingModuleSource = 'none';
|
||||
diagSettings.reportMissingImports = 'warning';
|
||||
diagSettings.reportTypeCommentUsage = 'none';
|
||||
diagSettings.reportIncompleteStub = 'none';
|
||||
diagSettings.reportInvalidStubStatement = 'none';
|
||||
@@ -995,7 +997,7 @@ export function getStrictDiagnosticRuleSet(dialect?: LanguageDialect): Diagnosti
|
||||
reportImplicitStringConcatenation: 'none',
|
||||
reportUnboundVariable: 'error',
|
||||
reportUnhashable: 'error',
|
||||
reportUndefinedVariable: 'error',
|
||||
reportUndefinedVariable: 'warning',
|
||||
reportInvalidStubStatement: 'error',
|
||||
reportIncompleteStub: 'error',
|
||||
reportUnsupportedDunderAll: 'error',
|
||||
@@ -1013,6 +1015,7 @@ export function getStrictDiagnosticRuleSet(dialect?: LanguageDialect): Diagnosti
|
||||
diagSettings.reportDeprecated = 'none';
|
||||
diagSettings.reportMissingTypeStubs = 'none';
|
||||
diagSettings.reportMissingModuleSource = 'none';
|
||||
diagSettings.reportMissingImports = 'warning';
|
||||
diagSettings.reportTypeCommentUsage = 'none';
|
||||
diagSettings.reportIncompleteStub = 'none';
|
||||
diagSettings.reportInvalidStubStatement = 'none';
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
* - 基础类型分类:class/function/parameter/namespace
|
||||
*/
|
||||
|
||||
import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo';
|
||||
import { Declaration, DeclarationType, VariableDeclaration } from '../analyzer/declaration';
|
||||
import { ParseTreeWalker } from '../analyzer/parseTreeWalker';
|
||||
import { isWithinTypeAnnotation } from '../analyzer/parseTreeUtils';
|
||||
import { TypeCategory } from '../analyzer/types';
|
||||
import { TypeCategory, isCDefineType } from '../analyzer/types';
|
||||
import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes';
|
||||
import { LogTracker } from '../common/logTracker';
|
||||
import { ProgramView } from '../common/extensibility';
|
||||
@@ -468,9 +469,14 @@ class SemanticTokenWalker extends ParseTreeWalker {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 访问 ModuleNameNode:import 语句中的模块名路径部分 → namespace。
|
||||
/** 访问 ModuleNameNode:import 语句中的模块名路径部分。
|
||||
* 解析成功的模块 → namespace(绿色);未解析的模块(如不存在的
|
||||
* `from .visitor import ...`)→ undefined(白色,与诊断一致)。
|
||||
* 返回 false 阻止遍历 nameParts(避免重复 token,因 visitName 也会处理) */
|
||||
override visitModuleName(node: ModuleNameNode): boolean {
|
||||
const importInfo = AnalyzerNodeInfo.getImportInfo(node);
|
||||
// 模块整体未解析:每个 namePart 都视为未定义(白色)
|
||||
const isUnresolved = !!importInfo && !importInfo.isImportFound && !importInfo.isNativeLib;
|
||||
for (const part of node.d.nameParts) {
|
||||
const position = convertOffsetToPosition(part.start, this._lines);
|
||||
if (position && part.length > 0) {
|
||||
@@ -478,7 +484,9 @@ class SemanticTokenWalker extends ParseTreeWalker {
|
||||
line: position.line,
|
||||
startChar: position.character,
|
||||
length: part.length,
|
||||
tokenType: SemanticTokenTypes.namespace,
|
||||
tokenType: isUnresolved
|
||||
? SemanticTokenTypes.undefined
|
||||
: SemanticTokenTypes.namespace,
|
||||
tokenModifiers: 0,
|
||||
});
|
||||
}
|
||||
@@ -634,8 +642,43 @@ class SemanticTokenWalker extends ParseTreeWalker {
|
||||
}
|
||||
break;
|
||||
case DeclarationType.Alias:
|
||||
// import 的模块别名
|
||||
tokenType = SemanticTokenTypes.namespace;
|
||||
// import 的模块别名。按别名实际指向的类型分类:
|
||||
// `from .base import AST` 中 AST 实际是类 → class;
|
||||
// `import os.path` 中 os 是模块 → namespace。
|
||||
// 避免"无论实际上是什么都显示成绿色(namespace)"。
|
||||
if (type) {
|
||||
if (type.category === TypeCategory.Unknown ||
|
||||
type.category === TypeCategory.Any ||
|
||||
type.category === TypeCategory.Unbound) {
|
||||
// 未解析的导入(如 `from .visitor import ASTVisitor`,
|
||||
// visitor 模块不存在):标 undefined(白色),与诊断一致。
|
||||
tokenType = SemanticTokenTypes.undefined;
|
||||
} else if (isCDefineType(type)) {
|
||||
// t.CDefine 常量(如 FLAG_IS_ASYNC: t.CDefine = 1):
|
||||
// 编译时元指令,按常量显示(variable + readonly)。
|
||||
tokenType = SemanticTokenTypes.variable;
|
||||
tokenModifiers |= SemanticTokenModifiers.readonly;
|
||||
} else if (type.category === TypeCategory.Class) {
|
||||
tokenType = BUILTIN_CLASS_NAMES.has(nameText)
|
||||
? SemanticTokenTypes.class
|
||||
: (BUILTIN_TYPE_NAMES.has(nameText)
|
||||
? SemanticTokenTypes.builtin
|
||||
: SemanticTokenTypes.class);
|
||||
} else if (
|
||||
type.category === TypeCategory.Function ||
|
||||
type.category === TypeCategory.Overloaded
|
||||
) {
|
||||
tokenType = SemanticTokenTypes.function;
|
||||
} else if (type.category === TypeCategory.Module) {
|
||||
tokenType = SemanticTokenTypes.namespace;
|
||||
} else if (type.category === TypeCategory.TypeVar) {
|
||||
tokenType = SemanticTokenTypes.type;
|
||||
} else {
|
||||
tokenType = SemanticTokenTypes.variable;
|
||||
}
|
||||
} else {
|
||||
tokenType = SemanticTokenTypes.namespace;
|
||||
}
|
||||
tokenModifiers |= defMod;
|
||||
break;
|
||||
case DeclarationType.TypeAlias:
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* viperUnionMemberAccess.test.ts
|
||||
*
|
||||
* 验证 Viper 扩展:联合类型 `MemBuddy | CPtr` 的变量访问成员方法(如 realloc)
|
||||
* 不应报 "Cannot access attribute for class UnionType"。
|
||||
*
|
||||
* 场景:TransPyC/Test/BuddyTest/App/mbuddytest.vp
|
||||
* bd3: memhub.MemBuddy | CPtr = memhub.MemBuddy(arena3, 4096)
|
||||
* rp2: bytes = bd3.realloc(rp, 128)
|
||||
*
|
||||
* 同时覆盖 `with memhub.MemBuddy(...) as wbd:` 上下文管理器场景:
|
||||
* - @t.CVTable 装饰的 MemManager 类必须解析为 type[MemManager] 而非 Type
|
||||
* - __enter__ 返回 'MemManager' | t.CPtr 折叠为 CPtr[MemManager]
|
||||
* - wbd.alloc / bd3.realloc 成员访问不报 Unknown
|
||||
*
|
||||
* 运行:npx jest src/tests/viperUnionMemberAccess.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';
|
||||
|
||||
// 允许 vipershed-fallback(t.vpi/c.vpi 存根)被定位。
|
||||
(global as any).__rootDirectory = path.resolve();
|
||||
|
||||
const BUDDY_TEST = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\Test\\BuddyTest\\App\\mbuddytest.vp';
|
||||
const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes';
|
||||
|
||||
function buildProgram(targetPath: 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 uri = UriEx.file(targetPath);
|
||||
program.setTrackedFiles([uri]);
|
||||
while (program.analyze()) {
|
||||
// 循环直到分析完成
|
||||
}
|
||||
|
||||
return { program, serviceProvider, uri, configOptions };
|
||||
}
|
||||
|
||||
function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): any[] {
|
||||
const sourceFile = program.getSourceFile(uri);
|
||||
assert(sourceFile, 'source file not found');
|
||||
return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => ({
|
||||
category: diag.category,
|
||||
message: diag.message,
|
||||
range: diag.range ? `L${diag.range.start.line + 1}` : '(no range)',
|
||||
}));
|
||||
}
|
||||
|
||||
test('BuddyTest 联合类型变量访问 realloc/alloc 成员不报 UnionType/Unknown 错误', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(BUDDY_TEST);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 断言:联合类型 `MemBuddy | CPtr` 的成员访问(realloc/alloc/__enter__/__exit__)无错误。
|
||||
const unionErrors = diagnostics.filter(
|
||||
(d) =>
|
||||
d.message.includes('UnionType') ||
|
||||
d.message.includes('realloc') ||
|
||||
d.message.includes('alloc') ||
|
||||
d.message.includes('__enter__') ||
|
||||
d.message.includes('__exit__') ||
|
||||
d.message.includes('不能与 "with" 一起使用')
|
||||
);
|
||||
assert.strictEqual(
|
||||
unionErrors.length,
|
||||
0,
|
||||
`unexpected union/member access errors: ${JSON.stringify(unionErrors)}`
|
||||
);
|
||||
|
||||
// 排除合理的既有告警(未使用的 import/变量),不应有 category=0/1 的其它错误。
|
||||
// 唯一允许的已知源码问题:mbuddytest.vp L209 引用了 MemBuddy 不存在的属性 "mem"
|
||||
// (应为 base,属于 TransPyC 测试源码自身的问题,LSP 报错是正确的)。
|
||||
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
|
||||
const knownSourceIssue = hardErrors.filter((d) => d.range === 'L209' && d.message.includes('mem'));
|
||||
assert.strictEqual(
|
||||
hardErrors.length,
|
||||
knownSourceIssue.length,
|
||||
`unexpected hard errors: ${JSON.stringify(hardErrors)}`
|
||||
);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
@@ -0,0 +1,399 @@
|
||||
/*
|
||||
* viperVariadicArgs.test.ts
|
||||
*
|
||||
* 验证 Viper 扩展:`*args` 在函数体内应绑定为 `va_list`(指针),而非 Python 语义的 tuple。
|
||||
*
|
||||
* 场景:TransPyC/includes/viperlib.vp
|
||||
* def sprintf(buf: t.CChar | t.CPtr, rule: str, *args) -> t.CVoid | t.CExport:
|
||||
* if buf is None or rule is None: return
|
||||
* vsnprintf(buf, 0x7FFFFFFF, rule, args) # 不应报 tuple 不能赋值给 CPtr[CChar]
|
||||
*
|
||||
* 同时覆盖 vsprintf / snprintf 内部将 `args` 透传给 vsnprintf 的场景。
|
||||
*
|
||||
* 运行:npx jest src/tests/viperVariadicArgs.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';
|
||||
|
||||
// 允许 vipershed-fallback(t.vpi/c.vpi 存根)被定位。
|
||||
(global as any).__rootDirectory = path.resolve();
|
||||
|
||||
const VIPERLIB_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\viperlib.vp';
|
||||
const MEMHUB_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\memhub.vp';
|
||||
const REQUESTS_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\requests.vp';
|
||||
const ARGPARSE_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\argparse.vp';
|
||||
const TYPES_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\llvmlite\\__types.vp';
|
||||
const OS_INIT_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\os\\__init__.vp';
|
||||
const SHA256_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\hashlib\\__sha256.vp';
|
||||
const SHA512_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\hashlib\\__sha512.vp';
|
||||
const AST_INIT_PATH = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes\\ast\\__init__.vp';
|
||||
const TRANSPYC_INCLUDES = 'd:\\Users\\TermiNexus\\Desktop\\TransPyC\\includes';
|
||||
|
||||
function buildProgram(targetPath: 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 uri = UriEx.file(targetPath);
|
||||
program.setTrackedFiles([uri]);
|
||||
while (program.analyze()) {
|
||||
// 循环直到分析完成
|
||||
}
|
||||
|
||||
return { program, serviceProvider, uri, configOptions };
|
||||
}
|
||||
|
||||
function getDiagnostics(program: Program, configOptions: ConfigOptions, uri: any): any[] {
|
||||
const sourceFile = program.getSourceFile(uri);
|
||||
assert(sourceFile, 'source file not found');
|
||||
return (sourceFile.getDiagnostics(configOptions) || []).map((diag) => ({
|
||||
category: diag.category,
|
||||
message: diag.message,
|
||||
range: diag.range ? `L${diag.range.start.line + 1}` : '(no range)',
|
||||
}));
|
||||
}
|
||||
|
||||
test('viperlib.vp 中 *args 透传 vsnprintf 不报 tuple 类型错误', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(VIPERLIB_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 断言:sprintf/vsprintf/snprintf 内部的 `args` 透传(vsnprintf 的 ap 参数)不应报
|
||||
// tuple 类型不能赋值给 CPtr 的错误。
|
||||
const variadicErrors = diagnostics.filter(
|
||||
(d) =>
|
||||
d.message.includes('tuple') ||
|
||||
d.message.includes('vsnprintf') ||
|
||||
d.message.includes('cannot be assigned to parameter "ap"') ||
|
||||
d.message.includes('参数 "ap"')
|
||||
);
|
||||
assert.strictEqual(
|
||||
variadicErrors.length,
|
||||
0,
|
||||
`unexpected variadic arg errors: ${JSON.stringify(variadicErrors)}`
|
||||
);
|
||||
|
||||
// va_arg / c.Deref 的误报(stdarg.vp 声明与 Viper 语义不符)也应消除。
|
||||
const derefVaArgErrors = diagnostics.filter(
|
||||
(d) => d.message.includes('va_arg') || d.message.includes('Deref')
|
||||
);
|
||||
assert.strictEqual(
|
||||
derefVaArgErrors.length,
|
||||
0,
|
||||
`unexpected va_arg/Deref errors: ${JSON.stringify(derefVaArgErrors)}`
|
||||
);
|
||||
|
||||
// 不应有任何 category=0/1 的错误。
|
||||
// 唯一允许的已知源码问题:viperlib.vp L13 `return count1` 引用不存在的变量
|
||||
// (应为 `return count`,属于 TransPyC 源码自身的拼写错误,LSP 报错正确)。
|
||||
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
|
||||
const knownSourceIssue = hardErrors.filter(
|
||||
(d) => d.range === 'L13' && (d.message.includes('count1') || d.message.includes('未定义'))
|
||||
);
|
||||
assert.strictEqual(
|
||||
hardErrors.length,
|
||||
knownSourceIssue.length,
|
||||
`unexpected hard errors: ${JSON.stringify(hardErrors)}`
|
||||
);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
test('memhub.vp 中 viperio.Buf.__sizeof__() 不报 Argument missing for parameter "self"', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(MEMHUB_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// __sizeof__ 是 Viper 的类方法(sizeof(Type) -> int),不需要 self 参数。
|
||||
// 命中场景:memhub.vp L109 `self.alloc(viperio.Buf.__sizeof__())`。
|
||||
const sizeOfErrors = diagnostics.filter(
|
||||
(d) => d.message.includes('__sizeof__') || d.message.includes('parameter "self"')
|
||||
);
|
||||
assert.strictEqual(
|
||||
sizeOfErrors.length,
|
||||
0,
|
||||
`unexpected __sizeof__ errors: ${JSON.stringify(sizeOfErrors)}`
|
||||
);
|
||||
|
||||
// 不应有任何 category=0/1 的错误。
|
||||
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
|
||||
assert.strictEqual(hardErrors.length, 0, `unexpected hard errors: ${JSON.stringify(hardErrors)}`);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
test('argparse.vp 中 str = None 参数默认值不报类型不兼容(回归)', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(ARGPARSE_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 命中场景:argparse.vp L155-158 `short: str = None` / `help: str = None`。
|
||||
// Viper 下 None 是空指针,str 是 i8*,`str = None` 合法且不应展开为 str | None。
|
||||
const strNoneErrors = diagnostics.filter(
|
||||
(d) => d.message.includes('str') && (d.message.includes('None') || d.message.includes('default'))
|
||||
);
|
||||
assert.strictEqual(
|
||||
strNoneErrors.length,
|
||||
0,
|
||||
`unexpected str=None errors: ${JSON.stringify(strNoneErrors)}`
|
||||
);
|
||||
|
||||
// 不应有任何 category=0/1 的错误。
|
||||
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
|
||||
assert.strictEqual(hardErrors.length, 0, `unexpected hard errors: ${JSON.stringify(hardErrors)}`);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
test('__types.vp 嵌套类成员 Pointee: LLVMType | t.CPtr 自我引用不被标为 Unknown', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(TYPES_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 命中场景:llvmlite/__types.vp 的 class LLVMType(t.REnum) 中,
|
||||
// class Ptr 的成员 Pointee: LLVMType | t.CPtr 引用外层类 LLVMType。
|
||||
// 嵌套类成员注解引用外层类名(自我指定)应解析为类类型而非 Unknown。
|
||||
const unknownErrors = diagnostics.filter(
|
||||
(d) => d.message.includes('Unknown') && d.message.includes('LLVMType')
|
||||
);
|
||||
assert.strictEqual(
|
||||
unknownErrors.length,
|
||||
0,
|
||||
`unexpected Unknown errors for nested self-ref: ${JSON.stringify(unknownErrors)}`
|
||||
);
|
||||
|
||||
// 不应有 TypePrint 参数不匹配(Struct | Unknown)错误。
|
||||
const typePrintErrors = diagnostics.filter(
|
||||
(d) => d.message.includes('TypePrint') && d.message.includes('Struct')
|
||||
);
|
||||
assert.strictEqual(
|
||||
typePrintErrors.length,
|
||||
0,
|
||||
`unexpected TypePrint errors: ${JSON.stringify(typePrintErrors)}`
|
||||
);
|
||||
|
||||
// REnum 变体构造(LLVMType.Int(1) / LLVMType.Struct(f, c, n))不报位置参数错误。
|
||||
const variantCtorErrors = diagnostics.filter(
|
||||
(d) => d.message.includes('位置参数') && d.message.includes('应为 0')
|
||||
);
|
||||
assert.strictEqual(
|
||||
variantCtorErrors.length,
|
||||
0,
|
||||
`unexpected REnum variant ctor errors: ${JSON.stringify(variantCtorErrors)}`
|
||||
);
|
||||
|
||||
// 泛型自引用基类(class ParamNode(GSListNode[ParamNode]))不报未定义。
|
||||
const paramNodeErrors = diagnostics.filter((d) => d.message.includes('ParamNode'));
|
||||
assert.strictEqual(
|
||||
paramNodeErrors.length,
|
||||
0,
|
||||
`unexpected ParamNode errors: ${JSON.stringify(paramNodeErrors)}`
|
||||
);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
test('os/__init__.vp 中 os._win32 子模块访问不报未知属性', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(OS_INIT_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 命中场景:os/__init__.vp L6 `import os._win32` 后,L25 `os._win32.exists(path)`。
|
||||
// Viper 中 `import os._win32` 应把子模块 _win32 注册为 os 模块的属性。
|
||||
const win32Errors = diagnostics.filter(
|
||||
(d) => d.message.includes('_win32') && (d.message.includes('known attribute') || d.message.includes('属性'))
|
||||
);
|
||||
assert.strictEqual(
|
||||
win32Errors.length,
|
||||
0,
|
||||
`unexpected os._win32 errors: ${JSON.stringify(win32Errors)}`
|
||||
);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
test('requests.vp 中 str = None 默认值不展开为 str | None,无 Optional 误报', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(REQUESTS_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 命中场景:requests.vp L258 `def post(url: str, data: str = None, ...)`。
|
||||
// Viper 下 str 是 i8*(可空指针),None 是空指针,不应展开为 str | None,
|
||||
// 否则 data[0] 等下标访问会误报 reportOptionalSubscript。
|
||||
const optionalErrors = diagnostics.filter(
|
||||
(d) => d.message.includes('OptionalSubscript') || d.message.includes('is not subscriptable')
|
||||
);
|
||||
assert.strictEqual(
|
||||
optionalErrors.length,
|
||||
0,
|
||||
`unexpected optional subscript errors: ${JSON.stringify(optionalErrors)}`
|
||||
);
|
||||
|
||||
// 不应有任何 category=0/1 的错误。
|
||||
const hardErrors = diagnostics.filter((d) => d.category === 0 || d.category === 1);
|
||||
assert.strictEqual(hardErrors.length, 0, `unexpected hard errors: ${JSON.stringify(hardErrors)}`);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
test('__sha256.vp 中 CArray 传给 CPtr 参数(数组名退化)不报类型错误', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(SHA256_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 命中场景:__sha256.vp L95/100/111 `self.transform(self.buf)`,
|
||||
// self.buf 是 `t.CArray[t.CUInt8T, SHA256_BLOCK_LEN]`,transform 的
|
||||
// block 参数是 `t.CUInt8T | t.CPtr`(折叠为 CPtr[CUInt8T])。
|
||||
// C 语义下数组名退化为指向首元素的指针,应允许 CArray → CPtr 传参。
|
||||
const cptrErrors = diagnostics.filter(
|
||||
(d) =>
|
||||
d.message.includes('cannot be assigned to parameter "block"') ||
|
||||
d.message.includes('参数 "block"') ||
|
||||
(d.message.includes('CArray') && d.message.includes('CPtr'))
|
||||
);
|
||||
assert.strictEqual(cptrErrors.length, 0, `unexpected CArray->CPtr errors: ${JSON.stringify(cptrErrors)}`);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
test('__sha512.vp 中 CPtr 传给 CArray 参数(形参调整)不报类型错误', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(SHA512_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 命中场景:__sha512.vp L119 `self.transform(data)`,
|
||||
// data 是 `t.CUInt8T | t.CPtr`(折叠为 CPtr[CUInt8T]),transform 的
|
||||
// block 参数是 `t.CArray[t.CUInt8T, SHA512_BLOCK_LEN]`。
|
||||
// C 语义下函数参数声明为数组时自动调整为指针(parameter adjustment),
|
||||
// 应允许 CPtr → CArray 形参传参。
|
||||
const cptrErrors = diagnostics.filter(
|
||||
(d) =>
|
||||
d.message.includes('cannot be assigned to parameter "block"') ||
|
||||
d.message.includes('参数 "block"') ||
|
||||
(d.message.includes('CArray') && d.message.includes('CPtr'))
|
||||
);
|
||||
assert.strictEqual(cptrErrors.length, 0, `unexpected CPtr->CArray errors: ${JSON.stringify(cptrErrors)}`);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
test('ast/__init__.vp 中派生类实例传给基类指针(Expression→CPtr[AST])不报类型错误', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(AST_INIT_PATH);
|
||||
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
|
||||
// 命中场景:ast/__init__.vp L105 `return Expression(pool, body)`,
|
||||
// Expression 继承自 AST,函数返回类型是 `AST | t.CPtr`(折叠为 CPtr[AST])。
|
||||
// C 语义下派生类指针可隐式 upcast 为基类指针(CPtr[Expression] → CPtr[AST])。
|
||||
const exprErrors = diagnostics.filter(
|
||||
(d) => d.message.includes('Expression') && (d.message.includes('返回类型') || d.message.includes('assignable'))
|
||||
);
|
||||
assert.strictEqual(exprErrors.length, 0, `unexpected Expression->CPtr[AST] errors: ${JSON.stringify(exprErrors)}`);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
|
||||
/**
|
||||
* 语义 token 辅助:解码 SemanticTokens 相对编码,返回 line 级 token 映射。
|
||||
* 返回 Map<line, Array<{start, text, tokenType, tokenModifiers}>>
|
||||
*/
|
||||
function getSemanticTokens(program: Program, uri: any): Map<number, any[]> {
|
||||
const { SemanticTokenProvider } = require('../languageService/semanticTokenProvider');
|
||||
const { LogTracker } = require('../common/logTracker');
|
||||
const provider = new SemanticTokenProvider(new LogTracker());
|
||||
const result = provider.provideFull(program, uri);
|
||||
assert(result, 'no semantic tokens produced');
|
||||
|
||||
const sourceFile = program.getSourceFile(uri);
|
||||
const lines = sourceFile!.getParseResults()!.text.split('\n');
|
||||
|
||||
const map = new Map<number, any[]>();
|
||||
let cursorLine = 0;
|
||||
let cursorChar = 0;
|
||||
for (let i = 0; i < result.data.length; i += 5) {
|
||||
const deltaLine = result.data[i];
|
||||
const deltaStart = result.data[i + 1];
|
||||
const length = result.data[i + 2];
|
||||
const tokenType = result.data[i + 3];
|
||||
const tokenMod = result.data[i + 4];
|
||||
cursorLine += deltaLine;
|
||||
cursorChar = deltaLine === 0 ? cursorChar + deltaStart : deltaStart;
|
||||
const lineText = lines[cursorLine] || '';
|
||||
const text = lineText.substring(cursorChar, cursorChar + length);
|
||||
const entry = { start: cursorChar, text, tokenType, tokenModifiers: tokenMod };
|
||||
if (!map.has(cursorLine)) {
|
||||
map.set(cursorLine, []);
|
||||
}
|
||||
map.get(cursorLine)!.push(entry);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
test('ast/__init__.vp 中不存在的 from .visitor import ASTVisitor 标黄且标白(undefined)', () => {
|
||||
const { program, serviceProvider, uri, configOptions } = buildProgram(AST_INIT_PATH);
|
||||
|
||||
// 1. 诊断:不存在的模块 `.visitor` 应标黄(warning,cat=1=Warning)
|
||||
const diagnostics = getDiagnostics(program, configOptions, uri);
|
||||
const visitorDiag = diagnostics.find(
|
||||
(d) => d.range === 'L51' && d.message.includes('visitor')
|
||||
);
|
||||
assert(visitorDiag, `expected unresolved '.visitor' diagnostic, got: ${JSON.stringify(diagnostics)}`);
|
||||
assert.strictEqual(visitorDiag.category, 1, `expected warning(1) for .visitor, got cat=${visitorDiag.category}`);
|
||||
|
||||
// 2. 语义 token:`visitor` 和 `ASTVisitor` 都应为 undefined(白色,type=11)
|
||||
const tokensByLine = getSemanticTokens(program, uri);
|
||||
const line51 = tokensByLine.get(50); // L51 → 行索引 50
|
||||
assert(line51, 'expected tokens on line 51');
|
||||
|
||||
const visitorToken = line51.find((t) => t.text === 'visitor');
|
||||
assert(visitorToken, 'expected "visitor" token on line 51');
|
||||
assert.strictEqual(visitorToken.tokenType, 11, `"visitor" should be undefined(11), got ${visitorToken.tokenType}`);
|
||||
|
||||
const astVisitorToken = line51.find((t) => t.text === 'ASTVisitor');
|
||||
assert(astVisitorToken, 'expected "ASTVisitor" token on line 51');
|
||||
assert.strictEqual(
|
||||
astVisitorToken.tokenType,
|
||||
11,
|
||||
`"ASTVisitor" should be undefined(11), got ${astVisitorToken.tokenType}`
|
||||
);
|
||||
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "vipeye",
|
||||
"displayName": "Vipeye",
|
||||
"description": "Type checker for the Viper language",
|
||||
"version": "1.1.411",
|
||||
"version": "1.1.412",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "TermiNexus"
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Change Log
|
||||
|
||||
## 1.1.412
|
||||
|
||||
- 语义高亮:不存在的模块名与未解析的导入符号(如 `from .visitor import ASTVisitor`)标为 undefined(白色)
|
||||
- 诊断:Viper 方言下 `reportMissingImports` 降级为 warning(黄色波浪线),与 `reportAttributeAccessIssue` 一致
|
||||
- 语义高亮:import 别名按实际类型着色(类/函数/模块/常量)
|
||||
- 语义高亮:`t.CDefine` 常量显示为常量色(variable + readonly)
|
||||
- 类型检查:CArray → CPtr 数组名退化、CPtr → CArray 形参调整
|
||||
- 类型检查:派生类实例 → 基类指针 upcast(`CPtr[Expression]` → `CPtr[AST]`)
|
||||
- 类型检查:`os._win32` 等子模块合并解析,多子模块访问不再误报未知属性
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial release of Vipeye (Viper language support for VS Code)
|
||||
@@ -1,13 +1,43 @@
|
||||
# Vipeye
|
||||
|
||||
Vipeye is a VS Code extension that provides language server features for the Viper programming language.
|
||||
Vipeye 是面向 Viper 语言的 VS Code 扩展,提供完整的语言服务支持。
|
||||
|
||||
## Features
|
||||
Viper 是一门基于 CPython 语法的 AOT 静态类型语言(编译器为 TransPyC / TPV),与 Python 的语义完全不同。Vipeye 在 Pyright 基础上深度改造而来,现已全面切换为 Viper 语义。
|
||||
|
||||
- Syntax highlighting for Viper (.vp, .vpi) files
|
||||
- Project file (.vpj) support
|
||||
- Type checking and diagnostics
|
||||
- Code completion and hover information
|
||||
## 功能特性
|
||||
|
||||
- **语法高亮**:`.vp` / `.vpi` 文件的完整 Viper 语法高亮(关键字、类型、装饰器 `@`、f-string、常量、未定义符号等语义化区分)
|
||||
- **语义高亮**:基于 LSP 3.16 SemanticTokens 协议,区分已定义/未定义符号、类/函数/变量/模块/参数/常量
|
||||
- **类型检查与诊断**:遵循 Viper 语义的类型推断与静态检查
|
||||
- 指针语义:`CPtr` / `CArray`(数组名退化、形参调整、派生类指针 upcast)
|
||||
- 类型别名与 C 类型系统:`t.CInt` / `t.CTypedef` / `t.CDefine` / `t.CEnum` 等
|
||||
- Viper 特有的 `T | t.CPtr` 指针折叠表示
|
||||
- 全局变量访问规则(读/写/global 声明判定)
|
||||
- **项目支持**:`.vpj` 项目配置文件、`viperconfig.json` 配置
|
||||
- **代码补全与悬停信息**
|
||||
- **导航**:定义跳转、查找引用、重命名
|
||||
|
||||
## 项目架构
|
||||
|
||||
- `vscode-vipeye`:LSP 客户端扩展(本包)
|
||||
- `vipeye`:Viper 语言命令行类型检查器(pyright 命令行工具改造)
|
||||
- `vipeye-internal`:核心分析引擎(类型推断、语义高亮、诊断)
|
||||
- `vipeye-typeserver`:类型服务进程
|
||||
- Viper 核心扩展 `vscode-viper-core`:管理 `.vpj` 项目配置与工具链
|
||||
|
||||
## 配置
|
||||
|
||||
在 `.vscode/settings.json` 或 `viperconfig.json` 中配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"viper.analysis.typeCheckingMode": "standard",
|
||||
"viper.analysis.extraPaths": ["d:/path/to/includes"],
|
||||
"viper.analysis.diagnosticSeverityOverrides": {
|
||||
"reportMissingImports": "warning"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "vscode-vipeye",
|
||||
"displayName": "Vipeye",
|
||||
"description": "VS Code language support for Viper (LSP client)",
|
||||
"version": "1.1.411",
|
||||
"version": "1.1.412",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
@@ -76,6 +76,10 @@
|
||||
"variable": [
|
||||
"variable"
|
||||
],
|
||||
"variable.readonly": [
|
||||
"variable.other.constant",
|
||||
"constant"
|
||||
],
|
||||
"function": [
|
||||
"entity.name.function.viper",
|
||||
"entity.name.function"
|
||||
@@ -111,9 +115,7 @@
|
||||
"variable.other.property",
|
||||
"variable.other.member"
|
||||
],
|
||||
"undefined": [
|
||||
"variable"
|
||||
]
|
||||
"undefined": []
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1377,7 +1379,7 @@
|
||||
"boolean"
|
||||
],
|
||||
"description": "Diagnostics for undefined variables.",
|
||||
"default": "error",
|
||||
"default": "warning",
|
||||
"enum": [
|
||||
"none",
|
||||
"information",
|
||||
|
||||
Reference in new issue
Block a user