feat(vipeye-internal): 支持 static 修饰符(禁止跨模块取参)与 f-string 前缀高亮
- static 是软关键字:`static def f` / `static fn f`(走 virtual 同一机制,记在 FunctionNode.d.viperModifier)与 `static x: T = v`(注解脱糖为 `t.CStatic | T`, 与 TPC _parse_type_ann 同形,展示为 `static int`)。其余位置仍是普通标识符 (`static = 1` / `static: int` / `static(x)` / `def f(static: int)`)。 - 语义 = C 的 internal linkage:跨模块取参报错。两处拦截:模块成员访问 (`mod.name` / `mod.fn()`)与 `from mod import name`;按声明所在文件判定, 同模块自引用/自导入放行。新增诊断 viperStaticUsedOutsideOfModule(zh-cn/en-us)。 判据 parseTreeUtils.isViperStaticDeclarationNode 供两处共用。 - static 并入补全关键字与 TextMate 修饰符高亮。 - f-string 的 r/u/b/f 前缀以 storage.type.string.viper 高亮(默认深色主题下为蓝色), 之前该前缀不在字符串范围内、无任何规则命中,显示为白色。 - 新增 viperStaticModifier.test.ts(4 用例);全量 jest 796 → 800 通过。
This commit is contained in:
1 parent
4d5a0d203b
commit
90f4118603
14 files changed
+548
-3
No files matched your search
@@ -148,6 +148,7 @@ packages/vipeye-internal/src/tests/__viper_attribute_implicit_scope__/
|
||||
packages/vipeye-internal/src/tests/__viper_volatile__/
|
||||
packages/vipeye-internal/src/tests/__viper_volatile_bad__/
|
||||
packages/vipeye-internal/src/tests/__viper_callable__/
|
||||
packages/vipeye-internal/src/tests/__viper_static__/
|
||||
|
||||
# perfCompare.py cached commit bundles
|
||||
/build/perfCompare/
|
||||
|
||||
@@ -1644,6 +1644,8 @@ export class Checker extends ParseTreeWalker {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._validateViperStaticImport(node);
|
||||
|
||||
const decls = this._evaluator.getDeclInfoForNameNode(node.d.name)?.decls;
|
||||
if (!decls) {
|
||||
return false;
|
||||
@@ -1686,6 +1688,42 @@ export class Checker extends ParseTreeWalker {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Viper 扩展:`from mod import name` —— 若 name 是目标模块里的 `static` 符号
|
||||
// (C 的 internal linkage),禁止跨模块取参。与 TransPyV ImportsGenMixin 的
|
||||
// `from mod import name` 拦截同语义(同模块自引用放行,按声明文件判定)。
|
||||
// `mod.name` / `mod.fn()` 形态由 typeEvaluator 的模块成员访问分支负责。
|
||||
private _validateViperStaticImport(node: ImportFromAsNode) {
|
||||
if (!IsViperDialect(this._fileInfo.executionEnvironment.dialect)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decls = this._evaluator.getDeclInfoForNameNode(node.d.name)?.decls;
|
||||
if (!decls) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFileUri = this._fileInfo.fileUri.toString();
|
||||
for (const decl of decls) {
|
||||
const resolvedDecl = this._evaluator.resolveAliasDeclaration(decl, /* resolveLocalNames */ true);
|
||||
if (
|
||||
!resolvedDecl ||
|
||||
!ParseTreeUtils.isViperStaticDeclarationNode(resolvedDecl.node) ||
|
||||
resolvedDecl.uri.toString() === currentFileUri
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._evaluator.addDiagnostic(
|
||||
DiagnosticRule.reportGeneralTypeIssues,
|
||||
LocMessage.viperStaticUsedOutsideOfModule().format({
|
||||
name: `${_getViperImportFromModuleName(node)}.${node.d.name.d.value}`,
|
||||
}),
|
||||
node.d.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
override visitModuleName(node: ModuleNameNode): boolean {
|
||||
if (this._fileInfo.isStubFile) {
|
||||
return false;
|
||||
@@ -8150,6 +8188,18 @@ function getViperWrittenTypeName(declaredTypeNode: ExpressionNode): string | und
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Viper 扩展:把 `from <module> import <name>` 里的模块名还原成源码写法
|
||||
// (相对导入的前导点保留),用于 static 跨模块取参的诊断文案。
|
||||
function _getViperImportFromModuleName(node: ImportFromAsNode): string {
|
||||
const importFrom = node.parent;
|
||||
if (importFrom?.nodeType !== ParseNodeType.ImportFrom) {
|
||||
return '';
|
||||
}
|
||||
const leadingDots = '.'.repeat(importFrom.d.module.d.leadingDots);
|
||||
const nameParts = importFrom.d.module.d.nameParts.map((part) => part.d.value).join('.');
|
||||
return leadingDots + nameParts;
|
||||
}
|
||||
|
||||
function viperSignedRange(typeName: string, bits: number): ViperIntegerRange | undefined {
|
||||
if (!Number.isInteger(bits) || bits < 1 || bits > 64) {
|
||||
return undefined;
|
||||
|
||||
@@ -83,6 +83,21 @@ export function getNodeDepth(node: ParseNode): number {
|
||||
return depth;
|
||||
}
|
||||
|
||||
// Viper 扩展:判定一条声明的「离节点」是否带 `static` 修饰(C 的 internal linkage,
|
||||
// 语义为禁止跨模块取参)。两种来源:
|
||||
// - 函数:parser 把 `static` 记到 FunctionNode.d.viperModifier(与 virtual 同法);
|
||||
// - 变量:`static x: T` 的变量声明离节点是目标 NameNode(parser 打 isViperStatic 标记),
|
||||
// 注解节点(TypeAnnotationNode)上也有兜底标记。
|
||||
// 供 typeEvaluator(`mod.name` 取参)与 checker(`from mod import name`)共用,
|
||||
// 保证两处判据一致。
|
||||
export function isViperStaticDeclarationNode(node: ParseNode): boolean {
|
||||
const declarationFields = node.d as { isViperStatic?: boolean; viperModifier?: string };
|
||||
if (declarationFields.isViperStatic === true) {
|
||||
return true;
|
||||
}
|
||||
return node.nodeType === ParseNodeType.Function && declarationFields.viperModifier === 'static';
|
||||
}
|
||||
|
||||
// Returns the deepest node that contains the specified position.
|
||||
export function findNodeByPosition(
|
||||
node: ParseNode,
|
||||
|
||||
@@ -6630,6 +6630,21 @@ export function createTypeEvaluator(
|
||||
node.d.member
|
||||
);
|
||||
}
|
||||
|
||||
// Viper 扩展:`static` 声明的符号禁止跨模块取参(C 的 internal linkage)。
|
||||
// 与 TransPyV ExprGenAttr 的 `mod.name` 拦截同语义:同模块自引用放行。
|
||||
if (
|
||||
IsViperDialect(AnalyzerNodeInfo.getFileInfo(node).executionEnvironment.dialect) &&
|
||||
isViperStaticSymbolFromOtherFile(symbol, fileInfo)
|
||||
) {
|
||||
addDiagnostic(
|
||||
DiagnosticRule.reportGeneralTypeIssues,
|
||||
LocMessage.viperStaticUsedOutsideOfModule().format({
|
||||
name: `${baseType.priv.moduleName}.${memberName}`,
|
||||
}),
|
||||
node.d.member
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Viper 扩展:模块级 __getattr__(PEP 562)语义不存在,跳过动态属性解析。
|
||||
|
||||
@@ -9486,6 +9501,21 @@ export function createTypeEvaluator(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Viper 扩展:判定单条声明是否为 `static` 声明(C 的 internal linkage)。
|
||||
// 判据在 ParseTreeUtils.isViperStaticDeclarationNode(与 checker 的
|
||||
// `from mod import name` 检查共用,保证两处口径一致)。
|
||||
function isViperStaticDeclaration(decl: Declaration): boolean {
|
||||
return ParseTreeUtils.isViperStaticDeclarationNode(decl.node);
|
||||
}
|
||||
|
||||
// Viper 扩展:该符号是否存在「来自其它文件」的 static 声明。
|
||||
// 语义为「禁止跨模块取参」(TransPyV 的 ExprGenAttr / CallGenMixin / ImportsGenMixin
|
||||
// 三处拦截):按声明所在文件与引用所在文件是否相同判定,同模块自引用放行。
|
||||
function isViperStaticSymbolFromOtherFile(symbol: Symbol, fileInfo: AnalyzerFileInfo): boolean {
|
||||
const fileUri = fileInfo.fileUri.toString();
|
||||
return symbol.getDeclarations().some((decl) => isViperStaticDeclaration(decl) && decl.uri.toString() !== fileUri);
|
||||
}
|
||||
|
||||
// Viper 扩展:解析 `t.attr.<name>` / `t.attr.<name>(...)` 属性标记 → t.CAttr 实例。
|
||||
//
|
||||
// 形态(与 TransPyC HandlesAssign._TAttrName / TransPyV ClassGenBase._tattr_name
|
||||
|
||||
@@ -191,8 +191,8 @@ namespace Keywords {
|
||||
// 不并入就会"看不见"(如 define/typedef/const 等):
|
||||
// - 硬关键字:define(宏函数,等价 def)、const / volatile(类型描述符前缀);
|
||||
// - 软关键字:typedef(类型别名);inline / export / extern / virtual /
|
||||
// novirtual(C 风格修饰);enum / rnum / union / struct(类/枚举声明前缀);
|
||||
// fn / fun / func / function(def 别名)。
|
||||
// novirtual / static(C 风格修饰;static 意为禁止跨模块取参);enum /
|
||||
// rnum / union / struct(类/枚举声明前缀);fn / fun / func / function(def 别名)。
|
||||
// - `__attribute__`:宏属性装饰器/注解修饰器(`@__attribute__(...)` /
|
||||
// `a: __attribute__(...) T`),非关键字(普通标识符),但写法特殊,
|
||||
// 单独键入 `__a` 时若不列出就"看不见"。
|
||||
@@ -206,6 +206,7 @@ namespace Keywords {
|
||||
'extern',
|
||||
'virtual',
|
||||
'novirtual',
|
||||
'static',
|
||||
'enum',
|
||||
'rnum',
|
||||
'union',
|
||||
|
||||
@@ -1208,6 +1208,8 @@ export namespace Localizer {
|
||||
getRawString('Diagnostic.viperVirtualDefInNoVirtualClass');
|
||||
export const viperExternBodyInvalid = () =>
|
||||
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperExternBodyInvalid'));
|
||||
export const viperStaticUsedOutsideOfModule = () =>
|
||||
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperStaticUsedOutsideOfModule'));
|
||||
export const viperLenNoLenMethod = () =>
|
||||
new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperLenNoLenMethod'));
|
||||
export const unhashableDictKey = () => getRawString('Diagnostic.unhashableDictKey');
|
||||
|
||||
@@ -1637,6 +1637,7 @@
|
||||
"viperNoVirtualDefPlacement": "'novirtual def' must appear inside a 'class', 'virtual class' or 'novirtual class'",
|
||||
"viperVirtualDefInNoVirtualClass": "'virtual def' cannot appear inside a 'novirtual class' (this class form emits no vtable)",
|
||||
"viperExternBodyInvalid": "The extern function \"{name}\" must have a body of only 'pass' or 'None'",
|
||||
"viperStaticUsedOutsideOfModule": "\"{name}\" is a static symbol and cannot be used across modules (static is visible only inside its defining module)",
|
||||
"viperLenNoLenMethod": "Type \"{name}\" has no __len__ method and cannot be used with len()",
|
||||
"unhashableDictKey": "Dictionary key must be hashable",
|
||||
"unhashableSetEntry": {
|
||||
|
||||
@@ -629,6 +629,7 @@
|
||||
"viperNoVirtualDefPlacement": "\"novirtual def\" 必须位于 class、virtual class 或 novirtual class 内",
|
||||
"viperVirtualDefInNoVirtualClass": "\"virtual def\" 不能位于 novirtual class 内(该形态的类不生成 vtable)",
|
||||
"viperExternBodyInvalid": "extern 函数 \"{name}\" 的函数体必须为 pass 或 None",
|
||||
"viperStaticUsedOutsideOfModule": "\"{name}\" 是 static 符号,禁止跨模块使用(static 仅限定义模块内可见)",
|
||||
"viperLenNoLenMethod": "类型 \"{name}\" 没有 __len__ 方法,不能用于 len()",
|
||||
"unhashableDictKey": "字典密钥必须可哈希",
|
||||
"unhashableSetEntry": "Set 条目必须是可哈希的",
|
||||
|
||||
@@ -1201,6 +1201,12 @@ export interface TypeAnnotationNode extends ParseNodeBase<ParseNodeType.TypeAnno
|
||||
d: {
|
||||
valueExpr: ExpressionNode;
|
||||
annotation: ExpressionNode;
|
||||
|
||||
// Viper 扩展:`static x: T = v` 的注解(见
|
||||
// parser._parseViperStaticAnnotatedStatement)。注解已被改写为
|
||||
// `t.CStatic | T`,此标记供跨模块取参检查兜底判定(变量声明的
|
||||
// 离节点是目标 NameNode,见 NameNode.d.isViperStatic)。
|
||||
isViperStatic?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1712,6 +1718,11 @@ export interface NameNode extends ParseNodeBase<ParseNodeType.Name> {
|
||||
// 的同名绑定(builtins.vpi 通过 `from t import ...` / `from c import ...`
|
||||
// 绑定了它们),而不是报"未定义 t"。
|
||||
isViperImplicitModule?: boolean;
|
||||
|
||||
// Viper 扩展:`static x: T = v` 的变量声明目标名(见
|
||||
// parser._parseViperStaticAnnotatedStatement)。变量的 Declaration.node
|
||||
// 就是这个 NameNode,跨模块取参检查依此判定「声明带 static」。
|
||||
isViperStatic?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -486,6 +486,35 @@ export class Parser {
|
||||
this._getNextToken();
|
||||
return this._parseSimpleStatement();
|
||||
}
|
||||
if (modifierText === 'static') {
|
||||
// Viper 扩展:`static` 修饰符(C 的 internal linkage)——
|
||||
// 语义为「禁止跨模块取参」,跨模块引用由 checker / typeEvaluator 报错。
|
||||
// `static def f(...)` → 与 virtual 同法,把 static 记到 FunctionNode.d.viperModifier;
|
||||
// `static x: T = v` → 在注解联合最左插入 `t.CStatic`(`t.CStatic | T`,
|
||||
// 与 TPC includes/ast/parser.vp _parse_type_ann 同形),
|
||||
// 并在声明节点上打 isViperStatic 标记。
|
||||
if (
|
||||
this._peekToken(1).type === TokenType.Keyword &&
|
||||
(this._peekToken(1) as KeywordToken).keywordType === KeywordType.Def
|
||||
) {
|
||||
this._getNextToken();
|
||||
return this._parseFunctionDef(
|
||||
/* asyncToken */ undefined,
|
||||
/* decorators */ undefined,
|
||||
modifierText
|
||||
);
|
||||
}
|
||||
if (this._peekToken(1).type === TokenType.Identifier) {
|
||||
this._getNextToken();
|
||||
return this._parseViperStaticAnnotatedStatement(modifierToken);
|
||||
}
|
||||
this._addSyntaxError(
|
||||
LocMessage.viperModifierRequiresDef().format({ name: modifierText }),
|
||||
modifierToken
|
||||
);
|
||||
this._getNextToken();
|
||||
return this._parseSimpleStatement();
|
||||
}
|
||||
if (
|
||||
this._peekToken(1).type === TokenType.Keyword &&
|
||||
(this._peekToken(1) as KeywordToken).keywordType === KeywordType.Def
|
||||
@@ -2702,6 +2731,18 @@ export class Parser {
|
||||
modifierToken
|
||||
);
|
||||
}
|
||||
if (modifierText === 'static') {
|
||||
// Viper 扩展:装饰器之后的 `static def f(...)`(与 _parseStatement 同法)。
|
||||
// 变量形式 `static x: T` 不跟装饰器(装饰器只作用于 def / class)。
|
||||
const afterModifierKeywordType =
|
||||
this._peekToken(1).type === TokenType.Keyword
|
||||
? (this._peekToken(1) as KeywordToken).keywordType
|
||||
: undefined;
|
||||
if (afterModifierKeywordType === KeywordType.Def) {
|
||||
this._getNextToken();
|
||||
return this._parseFunctionDef(undefined, decoratorList, modifierText);
|
||||
}
|
||||
}
|
||||
// 其他修饰关键字(inline/export/extern)不在装饰器后支持,
|
||||
// 保持原行为:落到下方 expectedAfterDecorator 报错。
|
||||
}
|
||||
@@ -5467,6 +5508,68 @@ export class Parser {
|
||||
return nameNode;
|
||||
}
|
||||
|
||||
// Viper 扩展:合成 `t.CStatic` 属性标记(模块根是隐式名,不要求源码 import t)。
|
||||
private _createViperStaticMarker(start: number): ExpressionNode {
|
||||
return MemberAccessNode.create(
|
||||
this._createViperImplicitModuleName(start, 't'),
|
||||
NameNode.create(IdentifierToken.create(start, 0, 'CStatic', /* comments */ undefined))
|
||||
);
|
||||
}
|
||||
|
||||
// Viper 扩展:`static x: T = v` 变量声明(C 的 internal linkage)。
|
||||
// 已消费 `static` 关键字后调用;把注解改写为 `t.CStatic | T`
|
||||
// (与 TPC includes/ast/parser.vp 在 _parse_type_ann 里把 t.CStatic 插到
|
||||
// 联合最左同形),并在声明节点上打 isViperStatic 标记,供「禁止跨模块取参」
|
||||
// 检查(typeEvaluator / checker)使用。注解范围向前扩展到 `static` 起点。
|
||||
private _parseViperStaticAnnotatedStatement(modifierToken: KeywordToken): StatementNode {
|
||||
const statementList = this._parseSimpleStatement();
|
||||
|
||||
let annotationNode: TypeAnnotationNode | undefined;
|
||||
for (const inner of statementList.d.statements) {
|
||||
if (inner.nodeType === ParseNodeType.TypeAnnotation) {
|
||||
annotationNode = inner;
|
||||
break;
|
||||
}
|
||||
if (inner.nodeType === ParseNodeType.Assignment) {
|
||||
const leftExpr = inner.d.leftExpr;
|
||||
if (leftExpr.nodeType === ParseNodeType.TypeAnnotation) {
|
||||
annotationNode = leftExpr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!annotationNode) {
|
||||
// tokenizer 已保证 `static` 后是 `名字 :`;走到这里说明写法异常。
|
||||
this._addSyntaxError(LocMessage.viperModifierRequiresDef().format({ name: 'static' }), modifierToken);
|
||||
return statementList;
|
||||
}
|
||||
|
||||
// 变量声明的「离节点」是注解的目标表达式(NameNode),在那里打标记。
|
||||
const targetExpr = annotationNode.d.valueExpr;
|
||||
if (targetExpr.nodeType === ParseNodeType.Name) {
|
||||
targetExpr.d.isViperStatic = true;
|
||||
}
|
||||
annotationNode.d.isViperStatic = true;
|
||||
|
||||
// 合成的 `t.CStatic` 标记与被改写的联合都必须留在原注解范围内
|
||||
// (注解节点的 start/length 由 valueExpr/annotation 决定;此处不能把
|
||||
// 范围向前扩到 `static`,否则子节点会跑到父节点范围之外,绑定期
|
||||
// 的 testWalker 会报 "Child node ... is not contained within its parent")。
|
||||
// 故锚点取内层注解自身的起点,零长度合成名正好落在注解内侧。
|
||||
const anchorStart = annotationNode.d.annotation.start;
|
||||
const wrappedAnnotation = BinaryOperationNode.create(
|
||||
this._createViperStaticMarker(anchorStart),
|
||||
annotationNode.d.annotation,
|
||||
OperatorToken.create(anchorStart, 0, OperatorType.BitwiseOr, /* comments */ undefined),
|
||||
OperatorType.BitwiseOr
|
||||
);
|
||||
wrappedAnnotation.parent = annotationNode;
|
||||
annotationNode.d.annotation = wrappedAnnotation;
|
||||
|
||||
return statementList;
|
||||
}
|
||||
|
||||
// Viper 扩展:把 `@__attribute__(...)` 装饰器脱糖为
|
||||
// `@c.Attribute(t.attr.a, t.attr.b)`——与 TPC/TPV 的
|
||||
// includes/ast/parser.vp `_DesugarMagicAttribute` 逐字段同形,因此 class 与 def
|
||||
|
||||
@@ -658,7 +658,7 @@ enum MagicsKind {
|
||||
// 返回 'classEnum':把 enum 当作 class 关键字,并标记为 Viper 枚举
|
||||
// (等价 `class Foo(t.CEnum)`,analyzer 会赋予 CEnum 枚举语义);
|
||||
// 返回 'def':把 fn/fun/func/function 当作 def 关键字;
|
||||
// 返回 'viperModifier':inline/export/extern/virtual/novirtual 生成修饰关键字
|
||||
// 返回 'viperModifier':inline/export/extern/virtual/novirtual/static 生成修饰关键字
|
||||
// token,由 parser 判断搭配是否合法(单独使用则报错);
|
||||
// undefined:按普通标识符处理(变量名、成员名、导入名、`state:` 注解等)。
|
||||
function getViperStatementKeywordAction(
|
||||
@@ -669,6 +669,32 @@ function getViperStatementKeywordAction(
|
||||
precededByViperModifier: boolean
|
||||
): 'skip' | 'class' | 'classEnum' | 'def' | 'viperModifier' | undefined {
|
||||
const nextWord = peekViperKeywordWord(text, identifierEnd);
|
||||
if (name === 'static') {
|
||||
// Viper 扩展:`static` 是**软关键字**,只在语句起始且后随
|
||||
// `def` 族(static def f())或 `名字 :`(static x: T = v)
|
||||
// 时才作为修饰关键字;`static = 1` / `static: int` / `static.x` /
|
||||
// `static(x)` 仍按普通标识符处理(与 TPC includes/ast/parser.vp
|
||||
// 的 _IsStaticModifier 同判据)。
|
||||
if (!isViperAtStatementStart(text, identifierStart)) {
|
||||
return undefined;
|
||||
}
|
||||
// 后跟 ':' 直接结束(`static: t.CInt` 这类字段声明)按普通标识符处理。
|
||||
if (isViperNextCharColon(text, identifierEnd)) {
|
||||
return undefined;
|
||||
}
|
||||
const wordAndNextChar = peekViperKeywordWordAndNextChar(text, identifierEnd);
|
||||
if (isViperDefFamilyWord(wordAndNextChar.word)) {
|
||||
return 'viperModifier';
|
||||
}
|
||||
if (
|
||||
wordAndNextChar.word.length > 0 &&
|
||||
isIdentifierStartChar(wordAndNextChar.word.charCodeAt(0)) &&
|
||||
wordAndNextChar.nextChar === ':'
|
||||
) {
|
||||
return 'viperModifier';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
name === 'inline' ||
|
||||
name === 'export' ||
|
||||
@@ -740,6 +766,35 @@ function peekViperKeywordWord(text: string, from: number): string {
|
||||
return text.slice(start, pos);
|
||||
}
|
||||
|
||||
// 跳过空白读出一个标识符词,并返回其后的首个非空白字符(用于判断
|
||||
// `static x: T` 里名字之后是否紧跟 ':')。到行尾/文件尾时 nextChar 为 undefined。
|
||||
function peekViperKeywordWordAndNextChar(
|
||||
text: string,
|
||||
from: number
|
||||
): { word: string; nextChar: string | undefined } {
|
||||
let pos = from;
|
||||
const len = text.length;
|
||||
while (pos < len && (text[pos] === ' ' || text[pos] === '\t')) {
|
||||
pos++;
|
||||
}
|
||||
const start = pos;
|
||||
while (pos < len && isIdentifierChar(text.charCodeAt(pos))) {
|
||||
pos++;
|
||||
}
|
||||
const word = text.slice(start, pos);
|
||||
|
||||
let after = pos;
|
||||
while (after < len && (text[after] === ' ' || text[after] === '\t')) {
|
||||
after++;
|
||||
}
|
||||
return { word, nextChar: after < len ? text[after] : undefined };
|
||||
}
|
||||
|
||||
// def 及其别名(fn/fun/func/function):`static def f()` 与 `static fn f()` 等价。
|
||||
function isViperDefFamilyWord(word: string): boolean {
|
||||
return word === 'def' || word === 'fn' || word === 'fun' || word === 'func' || word === 'function';
|
||||
}
|
||||
|
||||
function isViperNextCharColon(text: string, from: number): boolean {
|
||||
let pos = from;
|
||||
const len = text.length;
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* viperStaticModifier.test.ts
|
||||
*
|
||||
* 验证 Viper 的 `static` 修饰符(仅 LSP 侧实现,TPC/TPV 不动):
|
||||
* ① `static def f(...)` —— 与 virtual 同法,记到 FunctionNode.d.viperModifier;
|
||||
* ② `static x: T = v` —— 注解脱糖为 `t.CStatic | T`(与 TPC _parse_type_ann
|
||||
* 把 t.CStatic 插到联合最左同形),并在声明节点打 isViperStatic;
|
||||
* ③ 语义 = C 的 internal linkage:禁止跨模块取参。
|
||||
* 跨模块 `mod.name` / `mod.fn()` 由 typeEvaluator 的模块成员访问分支报错,
|
||||
* `from mod import name` 由 checker 的 visitImportFromAs 报错;同模块自引用放行。
|
||||
* ④ `static` 是软关键字:`static = 1` / `static: int` / `static(x)` 仍按普通标识符处理。
|
||||
*
|
||||
* 运行:npx jest src/tests/viperStaticModifier.test.ts --runInBand
|
||||
*/
|
||||
|
||||
import assert from 'assert';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ImportResolver } from '../analyzer/importResolver';
|
||||
import { ParseTreeWalker } from '../analyzer/parseTreeWalker';
|
||||
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';
|
||||
import { ParseNodeType, TypeAnnotationNode } from '../parser/parseNodes';
|
||||
|
||||
(global as any).__rootDirectory = path.resolve();
|
||||
|
||||
function buildProgram(files: Record<string, string>) {
|
||||
const tempFile = new RealTempFile();
|
||||
const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile);
|
||||
const fs = require('fs');
|
||||
const testRoot = path.join(__dirname, '__viper_static__');
|
||||
fs.rmSync(testRoot, { recursive: true, force: true });
|
||||
for (const [relPath, content] of Object.entries(files)) {
|
||||
const absPath = path.join(testRoot, relPath);
|
||||
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
||||
fs.writeFileSync(absPath, content, 'utf8');
|
||||
}
|
||||
|
||||
const projectRoot = UriEx.file(testRoot);
|
||||
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 uris: Record<string, any> = {};
|
||||
for (const relPath of Object.keys(files)) {
|
||||
uris[relPath] = UriEx.file(path.join(testRoot, relPath));
|
||||
}
|
||||
program.setTrackedFiles(Object.values(uris));
|
||||
while (program.analyze()) {
|
||||
// 循环
|
||||
}
|
||||
|
||||
return { program, configOptions, uris, parseResults: undefined as any, testRoot, serviceProvider };
|
||||
}
|
||||
|
||||
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,
|
||||
}));
|
||||
}
|
||||
|
||||
/** 收集注解变量的展示类型(按变量名索引)。 */
|
||||
function collectAnnotatedVarTypes(program: Program, uri: any): Map<string, string> {
|
||||
const parseResults = program.getParseResults(uri);
|
||||
assert(parseResults, 'parse results not found');
|
||||
const evaluator = program.evaluator;
|
||||
assert(evaluator, 'evaluator not available');
|
||||
const result = new Map<string, string>();
|
||||
|
||||
class Collector extends ParseTreeWalker {
|
||||
override visitTypeAnnotation(node: TypeAnnotationNode) {
|
||||
const valueExpr: any = node.d.valueExpr;
|
||||
if (valueExpr.nodeType === ParseNodeType.Name) {
|
||||
const type = evaluator!.getType(valueExpr);
|
||||
if (!result.has(valueExpr.d.value)) {
|
||||
result.set(valueExpr.d.value, type ? program.printType(type) : '(undefined)');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
new Collector().walk(parseResults.parserOutput.parseTree);
|
||||
return result;
|
||||
}
|
||||
|
||||
test('static def / static x: T 解析与脱糖;同模块自引用不报错', () => {
|
||||
const code = `static def helper() -> int:
|
||||
return 1
|
||||
|
||||
static SECRET: int = 42
|
||||
|
||||
def use() -> int:
|
||||
return helper() + SECRET
|
||||
`;
|
||||
const { program, configOptions, uris, testRoot, serviceProvider } = buildProgram({ 'lib.vp': code });
|
||||
|
||||
try {
|
||||
const diagnostics = getDiagnostics(program, configOptions, uris['lib.vp']);
|
||||
console.log('=== static 声明诊断 ===');
|
||||
diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`));
|
||||
|
||||
assert.strictEqual(
|
||||
diagnostics.filter((d) => d.category === 0).length,
|
||||
0,
|
||||
`static 修饰符不应报语法错误:${JSON.stringify(diagnostics)}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
diagnostics.filter((d) => d.message.includes('static 符号')).length,
|
||||
0,
|
||||
`同模块自引用 static 符号不应报错:${JSON.stringify(diagnostics)}`
|
||||
);
|
||||
|
||||
// 变量注解脱糖为 `t.CStatic | int`,展示为 `static int`(修饰符前缀)。
|
||||
const types = collectAnnotatedVarTypes(program, uris['lib.vp']);
|
||||
console.log('=== 注解变量类型 ===');
|
||||
types.forEach((v, k) => console.log(` ${k}: ${v}`));
|
||||
assert(
|
||||
types.get('SECRET')!.includes('static') && types.get('SECRET')!.includes('int'),
|
||||
`static SECRET 应展示为 static int,实际: ${types.get('SECRET')}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('跨模块 mod.name / mod.fn() 取用 static 符号报错', () => {
|
||||
const { program, configOptions, uris, testRoot, serviceProvider } = buildProgram({
|
||||
'lib.vp': `static def helper() -> int:
|
||||
return 1
|
||||
|
||||
static SECRET: int = 42
|
||||
|
||||
def public_fn() -> int:
|
||||
return helper()
|
||||
`,
|
||||
'main.vp': `import lib
|
||||
|
||||
def use() -> int:
|
||||
return lib.helper() + lib.SECRET
|
||||
`,
|
||||
});
|
||||
|
||||
try {
|
||||
const libDiags = getDiagnostics(program, configOptions, uris['lib.vp']);
|
||||
assert.strictEqual(
|
||||
libDiags.filter((d) => d.message.includes('static 符号')).length,
|
||||
0,
|
||||
`定义模块内部使用不应报错:${JSON.stringify(libDiags)}`
|
||||
);
|
||||
|
||||
const mainDiags = getDiagnostics(program, configOptions, uris['main.vp']);
|
||||
console.log('=== 跨模块取参诊断 ===');
|
||||
mainDiags.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`));
|
||||
|
||||
const staticDiags = mainDiags.filter((d) => d.message.includes('static 符号'));
|
||||
assert.strictEqual(
|
||||
staticDiags.length,
|
||||
2,
|
||||
`lib.helper / lib.SECRET 取用应各报一条 static 跨模块错误:${JSON.stringify(mainDiags)}`
|
||||
);
|
||||
assert(
|
||||
staticDiags.some((d) => d.message.includes('lib.helper')) &&
|
||||
staticDiags.some((d) => d.message.includes('lib.SECRET')),
|
||||
`诊断文案应含 lib.helper / lib.SECRET:${JSON.stringify(staticDiags)}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('from mod import static 符号报错', () => {
|
||||
const { program, configOptions, uris, testRoot, serviceProvider } = buildProgram({
|
||||
'lib.vp': `static def helper() -> int:
|
||||
return 1
|
||||
|
||||
def public_fn() -> int:
|
||||
return 0
|
||||
`,
|
||||
'main.vp': `from lib import helper, public_fn
|
||||
|
||||
def use() -> int:
|
||||
return helper() + public_fn()
|
||||
`,
|
||||
});
|
||||
|
||||
try {
|
||||
const mainDiags = getDiagnostics(program, configOptions, uris['main.vp']);
|
||||
console.log('=== from import static 诊断 ===');
|
||||
mainDiags.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`));
|
||||
|
||||
const staticDiags = mainDiags.filter((d) => d.message.includes('static 符号'));
|
||||
assert.strictEqual(
|
||||
staticDiags.length,
|
||||
1,
|
||||
`from lib import helper 应报一条 static 跨模块错误(public_fn 不报):${JSON.stringify(mainDiags)}`
|
||||
);
|
||||
assert(
|
||||
staticDiags[0].message.includes('lib.helper'),
|
||||
`诊断文案应含 lib.helper:${JSON.stringify(staticDiags)}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('static 是软关键字:非修饰位置仍按普通标识符处理', () => {
|
||||
const code = `static: int = 0
|
||||
static = 1
|
||||
|
||||
def f(static: int) -> int:
|
||||
return static
|
||||
`;
|
||||
const { program, configOptions, uris, testRoot, serviceProvider } = buildProgram({ 'main.vp': code });
|
||||
|
||||
try {
|
||||
const diagnostics = getDiagnostics(program, configOptions, uris['main.vp']);
|
||||
console.log('=== 软关键字诊断 ===');
|
||||
diagnostics.forEach((d) => console.log(` L${d.line} [category=${d.category}] ${d.message}`));
|
||||
|
||||
assert.strictEqual(
|
||||
diagnostics.filter((d) => d.category === 0).length,
|
||||
0,
|
||||
`static 作为普通标识符不应报语法错误:${JSON.stringify(diagnostics)}`
|
||||
);
|
||||
assert.strictEqual(
|
||||
diagnostics.filter((d) => d.message.includes('static 符号')).length,
|
||||
0,
|
||||
`同模块内不应报 static 跨模块错误:${JSON.stringify(diagnostics)}`
|
||||
);
|
||||
} finally {
|
||||
program.dispose();
|
||||
serviceProvider.dispose();
|
||||
require('fs').rmSync(testRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -29,6 +29,8 @@
|
||||
- Lowercase **`callable[[A, B], R]`** is now supported in a type annotation and means exactly `t.Callable[[A, B], R]` — all three spellings (`callable[...]`, `Callable[...]`, `t.Callable[...]`) and all the usual argument forms (`callable[[int, str], bool]`, `callable[..., bool]`, `callable[[], bool]`, as a parameter, return or variable annotation) evaluate to the same callable type. The parser rewrites the bare name `callable` to `Callable` **only in type-annotation position**, so the built-in function is untouched: `callable(x)` / `if callable(x):` still behave as before. `Callable` is declared in the built-in `builtins.vpi` stub (`Callable: t.CTypedef = t.Callable`, next to `rawptr` / `CConst`), so no `import t` is required. Previously `callable[[int], bool]` silently degraded to the type of the `callable` *function* (the Viper-dialect rule that ignores explicit type arguments on a callable), and bare `Callable` was reported as undefined
|
||||
- `__attribute__` no longer requires the file to `import t` / `import c`: the `t` / `c` module roots that `__attribute__(...)` desugars into are synthesized as **implicit module names** (zero-length, marked on the AST node — the compiler's attribute handling is AST-level and never checks symbol visibility either). When such a synthetic name cannot be resolved through the lexical scope chain, it falls back to the same-named binding in the built-in `builtins.vpi` stub; the fallback only applies to the desugared nodes, so a hand-written bare `t` / `c` in a file without the import still reports "未定义"
|
||||
- **Analysis progress in the status bar**: while the language server builds the dependency graph and type-checks the workspace, the bottom-left status bar now shows `$(sync~spin) C:\xxx\1.vp (4/90) 4.44% loading...` — the file currently being analyzed, how many files have been processed out of how many, and the percentage. The worker reports per-file progress (throttled to one message every 50 ms) through a new `analysisProgress` background message and a new `viper/analysisStatus` notification; the client shows a left-aligned `StatusBarItem` and hides it as soon as analysis finishes (or when there is nothing to analyze). Previously the only feedback was the work-done progress bar for open-files analysis, which said nothing about which file was being processed, so a long full-workspace pass was indistinguishable from a hung server
|
||||
- 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
|
||||
|
||||
## 1.1.425
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
"name": "storage.modifier.viper",
|
||||
"match": "\\b(virtual|novirtual)\\b(?=\\s+(?:class|def|fn|fun|func|function)\\b)"
|
||||
},
|
||||
{
|
||||
"name": "storage.modifier.viper",
|
||||
"match": "\\bstatic\\b(?=\\s+(?:def|fn|fun|func|function)\\b|\\s+[A-Za-z_][A-Za-z0-9_]*\\s*:)"
|
||||
},
|
||||
{
|
||||
"name": "storage.modifier.viper",
|
||||
"match": "\\b(const|volatile)\\b(?=\\s+[A-Za-z_][A-Za-z0-9_]*|\\s*\\[)"
|
||||
@@ -89,6 +93,10 @@
|
||||
},
|
||||
"string": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "storage.type.string.viper",
|
||||
"match": "(?i)\\b[rubf]{1,3}(?=\"\"\"|'''|\"|')"
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.double.block.viper",
|
||||
"begin": "\"\"\"",
|
||||
|
||||
Reference in new issue
Block a user