diff --git a/.gitignore b/.gitignore index 35ad4b7a..d898946a 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/packages/vipeye-internal/src/analyzer/checker.ts b/packages/vipeye-internal/src/analyzer/checker.ts index 9f00fee2..e1269aa9 100644 --- a/packages/vipeye-internal/src/analyzer/checker.ts +++ b/packages/vipeye-internal/src/analyzer/checker.ts @@ -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 import ` 里的模块名还原成源码写法 +// (相对导入的前导点保留),用于 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; diff --git a/packages/vipeye-internal/src/analyzer/parseTreeUtils.ts b/packages/vipeye-internal/src/analyzer/parseTreeUtils.ts index ef69ee62..04f56561 100644 --- a/packages/vipeye-internal/src/analyzer/parseTreeUtils.ts +++ b/packages/vipeye-internal/src/analyzer/parseTreeUtils.ts @@ -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, diff --git a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts index a46c7173..89532a11 100644 --- a/packages/vipeye-internal/src/analyzer/typeEvaluator.ts +++ b/packages/vipeye-internal/src/analyzer/typeEvaluator.ts @@ -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.` / `t.attr.(...)` 属性标记 → t.CAttr 实例。 // // 形态(与 TransPyC HandlesAssign._TAttrName / TransPyV ClassGenBase._tattr_name diff --git a/packages/vipeye-internal/src/languageService/completionProvider.ts b/packages/vipeye-internal/src/languageService/completionProvider.ts index bf0066a4..cae63c95 100644 --- a/packages/vipeye-internal/src/languageService/completionProvider.ts +++ b/packages/vipeye-internal/src/languageService/completionProvider.ts @@ -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', diff --git a/packages/vipeye-internal/src/localization/localize.ts b/packages/vipeye-internal/src/localization/localize.ts index c114f4b7..477b5f31 100644 --- a/packages/vipeye-internal/src/localization/localize.ts +++ b/packages/vipeye-internal/src/localization/localize.ts @@ -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'); diff --git a/packages/vipeye-internal/src/localization/package.nls.en-us.json b/packages/vipeye-internal/src/localization/package.nls.en-us.json index c4e34d33..5315962a 100644 --- a/packages/vipeye-internal/src/localization/package.nls.en-us.json +++ b/packages/vipeye-internal/src/localization/package.nls.en-us.json @@ -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": { diff --git a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json index 6552591f..a1cd489b 100644 --- a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json +++ b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json @@ -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 条目必须是可哈希的", diff --git a/packages/vipeye-internal/src/parser/parseNodes.ts b/packages/vipeye-internal/src/parser/parseNodes.ts index 776c4d3e..2752f519 100644 --- a/packages/vipeye-internal/src/parser/parseNodes.ts +++ b/packages/vipeye-internal/src/parser/parseNodes.ts @@ -1201,6 +1201,12 @@ export interface TypeAnnotationNode extends ParseNodeBase { // 的同名绑定(builtins.vpi 通过 `from t import ...` / `from c import ...` // 绑定了它们),而不是报"未定义 t"。 isViperImplicitModule?: boolean; + + // Viper 扩展:`static x: T = v` 的变量声明目标名(见 + // parser._parseViperStaticAnnotatedStatement)。变量的 Declaration.node + // 就是这个 NameNode,跨模块取参检查依此判定「声明带 static」。 + isViperStatic?: boolean; }; } diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index d594bd65..9f8b1588 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -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 diff --git a/packages/vipeye-internal/src/parser/tokenizer.ts b/packages/vipeye-internal/src/parser/tokenizer.ts index 059c79da..f148a3ef 100644 --- a/packages/vipeye-internal/src/parser/tokenizer.ts +++ b/packages/vipeye-internal/src/parser/tokenizer.ts @@ -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; diff --git a/packages/vipeye-internal/src/tests/viperStaticModifier.test.ts b/packages/vipeye-internal/src/tests/viperStaticModifier.test.ts new file mode 100644 index 00000000..a090071e --- /dev/null +++ b/packages/vipeye-internal/src/tests/viperStaticModifier.test.ts @@ -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) { + 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 = {}; + 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 { + const parseResults = program.getParseResults(uri); + assert(parseResults, 'parse results not found'); + const evaluator = program.evaluator; + assert(evaluator, 'evaluator not available'); + const result = new Map(); + + 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 }); + } +}); diff --git a/packages/vscode-vipeye/CHANGELOG.md b/packages/vscode-vipeye/CHANGELOG.md index dba5436c..1293fa75 100644 --- a/packages/vscode-vipeye/CHANGELOG.md +++ b/packages/vscode-vipeye/CHANGELOG.md @@ -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 diff --git a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json index 82a42eff..443550c3 100644 --- a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json +++ b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json @@ -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": "\"\"\"",