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:
TermiNexus committed 2026-09-27 13:19:57 +08:00
1 parent 4d5a0d203b
commit 90f4118603
14 files changed
+548 -3

No files matched your search

+1
View File
@@ -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 });
}
});
+2
View File
@@ -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": "\"\"\"",