diff --git a/packages/vipeye-internal/src/analyzer/checker.ts b/packages/vipeye-internal/src/analyzer/checker.ts index d48caaab..36fd9d6e 100644 --- a/packages/vipeye-internal/src/analyzer/checker.ts +++ b/packages/vipeye-internal/src/analyzer/checker.ts @@ -36,6 +36,7 @@ import { ClassNode, ComprehensionIfNode, ComprehensionNode, + ConstantNode, DelNode, DictionaryNode, ErrorNode, @@ -89,7 +90,7 @@ import { } from '../parser/parseNodes'; import { ParserOutput } from '../parser/parser'; import { UnescapeError, UnescapeErrorType, getUnescapedString } from '../parser/stringTokenUtils'; -import { OperatorType, StringTokenFlags, TokenType } from '../parser/tokenizerTypes'; +import { KeywordType, OperatorType, StringTokenFlags, TokenType } from '../parser/tokenizerTypes'; import { AnalyzerFileInfo } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; import { ConstraintTracker } from './constraintTracker'; @@ -217,6 +218,26 @@ interface TypeVarUsageInfo { // functions to be emitted. const isPrintCodeComplexityEnabled = false; +// Viper 扩展:extern 函数体允许的语句——只允许 pass 或 None(仅声明原型)。 +function isViperExternAllowedBody(stmt: ParseNode): boolean { + if (stmt.nodeType === ParseNodeType.Pass) { + return true; + } + if (stmt.nodeType === ParseNodeType.StatementList) { + const smallStatements = (stmt as StatementListNode).d.statements; + return smallStatements.every((s) => { + if (s.nodeType === ParseNodeType.Pass) { + return true; + } + if (s.nodeType === ParseNodeType.Constant) { + return (s as ConstantNode).d.constType === KeywordType.None; + } + return false; + }); + } + return false; +} + export class Checker extends ParseTreeWalker { private readonly _moduleNode: ModuleNode; private readonly _fileInfo: AnalyzerFileInfo; @@ -430,6 +451,22 @@ export class Checker extends ParseTreeWalker { ); } + // Viper 扩展:extern 声明的函数体必须为 pass 或 None(仅声明原型,不定义实现)。 + if ( + node.d.viperModifier === 'extern' && + IsViperDialect(this._fileInfo.executionEnvironment.dialect) + ) { + const bodyStatements = node.d.suite.d.statements; + const invalidStatement = bodyStatements.find((stmt) => !isViperExternAllowedBody(stmt)); + if (invalidStatement) { + this._evaluator.addDiagnostic( + DiagnosticRule.reportGeneralTypeIssues, + LocMessage.viperExternBodyInvalid().format({ name: node.d.name.d.value }), + invalidStatement + ); + } + } + if (functionTypeResult) { // Track whether we have seen a *args: P.args parameter. Named // parameters after this need to be flagged as an error. @@ -4793,7 +4830,19 @@ export class Checker extends ParseTreeWalker { declaredReturnType !== undefined && isPureCModifierType(declaredReturnType); - if (declaredReturnType && !functionNeverReturns && implicitlyReturnsNone && !isPureModifierReturn) { + // Viper 扩展:extern 关键字声明的函数仅声明原型(函数体必须为 pass/None), + // 不要求返回语句与声明类型匹配。 + const isViperExternDecl = + IsViperDialect(this._fileInfo.executionEnvironment.dialect) && + node.d.viperModifier === 'extern'; + + if ( + declaredReturnType && + !functionNeverReturns && + implicitlyReturnsNone && + !isPureModifierReturn && + !isViperExternDecl + ) { if (isNever(declaredReturnType)) { // If the function consists entirely of "...", assume that it's // an abstract method or a protocol method and don't require that diff --git a/packages/vipeye-internal/src/localization/localize.ts b/packages/vipeye-internal/src/localization/localize.ts index 6d29b3a8..63fdb856 100644 --- a/packages/vipeye-internal/src/localization/localize.ts +++ b/packages/vipeye-internal/src/localization/localize.ts @@ -1180,6 +1180,8 @@ export namespace Localizer { export const unexpectedUnindent = () => getRawString('Diagnostic.unexpectedUnindent'); export const viperModifierRequiresDef = () => new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperModifierRequiresDef')); + export const viperExternBodyInvalid = () => + new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperExternBodyInvalid')); export const unhashableDictKey = () => getRawString('Diagnostic.unhashableDictKey'); export const unhashableSetEntry = () => getRawString('Diagnostic.unhashableSetEntry'); export const unionForwardReferenceNotAllowed = () => getRawString('Diagnostic.unionForwardReferenceNotAllowed'); 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 c4a71b1b..b9487649 100644 --- a/packages/vipeye-internal/src/localization/package.nls.en-us.json +++ b/packages/vipeye-internal/src/localization/package.nls.en-us.json @@ -1619,6 +1619,7 @@ "unexpectedIndent": "Unexpected indentation", "unexpectedUnindent": "Unindent not expected", "viperModifierRequiresDef": "The modifier \"{name}\" must be used with 'def'", + "viperExternBodyInvalid": "The extern function \"{name}\" must have a body of only 'pass' or 'None'", "unhashableDictKey": "Dictionary key must be hashable", "unhashableSetEntry": { "message": "Set entry must be hashable", 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 aa9ac1d7..a64690d7 100644 --- a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json +++ b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json @@ -617,6 +617,7 @@ "unexpectedIndent": "意外缩进", "unexpectedUnindent": "不应取消缩进", "viperModifierRequiresDef": "修饰关键字 \"{name}\" 必须与 def 搭配使用", + "viperExternBodyInvalid": "extern 函数 \"{name}\" 的函数体必须为 pass 或 None", "unhashableDictKey": "字典密钥必须可哈希", "unhashableSetEntry": "Set 条目必须是可哈希的", "uninitializedAbstractVariables": "抽象基类中定义的变量未在 final 类 \"{classType}\" 中初始化", diff --git a/packages/vipeye-internal/src/parser/parseNodes.ts b/packages/vipeye-internal/src/parser/parseNodes.ts index b3d79d9a..45acfe1a 100644 --- a/packages/vipeye-internal/src/parser/parseNodes.ts +++ b/packages/vipeye-internal/src/parser/parseNodes.ts @@ -471,11 +471,19 @@ export interface FunctionNode extends ParseNodeBase { returnAnnotation: ExpressionNode | undefined; funcAnnotationComment: FunctionAnnotationNode | undefined; suite: SuiteNode; + // Viper 扩展:函数声明前的 C 修饰关键字(inline/export/extern),用于语义校验。 + viperModifier: string | undefined; }; } export namespace FunctionNode { - export function create(defToken: Token, name: NameNode, suite: SuiteNode, typeParams?: TypeParameterListNode) { + export function create( + defToken: Token, + name: NameNode, + suite: SuiteNode, + typeParams?: TypeParameterListNode, + viperModifier?: string + ) { const node: FunctionNode = { start: defToken.start, length: defToken.length, @@ -493,6 +501,7 @@ export namespace FunctionNode { returnAnnotation: undefined, funcAnnotationComment: undefined, suite: suite, + viperModifier, }, }; diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index f8aea30b..bf2c9252 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -430,23 +430,22 @@ export class Parser { switch (this._peekKeywordType()) { case KeywordType.ViperModifier: { - // Viper 扩展:inline/export/extern 必须与 def 搭配使用。 + // Viper 扩展:inline/export/extern 必须与 def(或其别名)搭配使用。 // 与 def 搭配时跳过修饰关键字并正常解析函数;单独使用时报错。 const modifierToken = this._peekToken() as KeywordToken; + const modifierText = this._fileContents!.slice( + modifierToken.start, + modifierToken.start + modifierToken.length + ); if ( this._peekToken(1).type === TokenType.Keyword && (this._peekToken(1) as KeywordToken).keywordType === KeywordType.Def ) { this._getNextToken(); - return this._parseFunctionDef(); + return this._parseFunctionDef(/* asyncToken */ undefined, /* decorators */ undefined, modifierText); } this._addSyntaxError( - LocMessage.viperModifierRequiresDef().format({ - name: this._fileContents!.slice( - modifierToken.start, - modifierToken.start + modifierToken.length - ), - }), + LocMessage.viperModifierRequiresDef().format({ name: modifierText }), modifierToken ); this._getNextToken(); @@ -2050,7 +2049,11 @@ export class Parser { // funcdef: 'def' NAME parameters ['->' test] ':' suite // parameters: '(' [typedargslist] ')' - private _parseFunctionDef(asyncToken?: KeywordToken, decorators?: DecoratorNode[]): FunctionNode | ErrorNode { + private _parseFunctionDef( + asyncToken?: KeywordToken, + decorators?: DecoratorNode[], + viperModifier?: string + ): FunctionNode | ErrorNode { const defToken = this._getKeywordToken(KeywordType.Def); const nameToken = this._getTokenIfIdentifier(); @@ -2119,6 +2122,7 @@ export class Parser { this._isInFinallyLoop = wasInFinallyLoop; const functionNode = FunctionNode.create(defToken, NameNode.create(nameToken), suite, typeParameters); + functionNode.d.viperModifier = viperModifier; if (asyncToken) { functionNode.d.isAsync = true; extendRange(functionNode, asyncToken); diff --git a/packages/vipeye-internal/src/parser/tokenizer.ts b/packages/vipeye-internal/src/parser/tokenizer.ts index 077adfcf..c4c09030 100644 --- a/packages/vipeye-internal/src/parser/tokenizer.ts +++ b/packages/vipeye-internal/src/parser/tokenizer.ts @@ -644,35 +644,44 @@ enum MagicsKind { // Viper 扩展:C 风格修饰/声明关键字处理。 // Viper 支持将 C 修饰/声明语义写成语句前缀关键字: // inline/export/extern def f(...) —— 修饰关键字,必须与 def 搭配 +// fn/fun/func/function f(...) —— def 的别名 // enum/rnum class|struct Foo —— 等价 t.CEnum / t.REnum // enum Foo: / rnum Foo: —— 单独使用(等价 class Foo:) // struct Foo: —— 独立的 struct 关键字(等价 class) // 返回 'skip':跳过该关键字 token(后续 def/class 正常解析,位置与缩进不变); // 返回 'class':把 enum/rnum/struct 当作 class 关键字; -// 返回 'viperModifier':inline/export/extern 单独使用(未与 def 搭配),由 parser 报错; +// 返回 'def':把 fn/fun/func/function 当作 def 关键字; +// 返回 'viperModifier':inline/export/extern 生成修饰关键字 token,由 parser +// 判断是否与 def 搭配(搭配则跳过并记录,单独使用则报错); // undefined:按普通标识符处理(变量名、成员名、导入名、`state:` 注解等)。 function getViperStatementKeywordAction( text: string, identifierStart: number, identifierEnd: number, - name: string -): 'skip' | 'class' | 'viperModifier' | undefined { + name: string, + precededByViperModifier: boolean +): 'skip' | 'class' | 'def' | 'viperModifier' | undefined { const nextWord = peekViperKeywordWord(text, identifierEnd); if (name === 'inline' || name === 'export' || name === 'extern') { // 修饰关键字仅在语句起始处生效,避免误伤 `self.inline` 等成员名。 if (!isViperAtStatementStart(text, identifierStart)) { return undefined; } - if (nextWord === 'def') { - return 'skip'; - } // 后跟 ':'(注解,如 `state: t.CArray[...]` 风格的字段声明)按普通标识符处理。 if (isViperNextCharColon(text, identifierEnd)) { return undefined; } - // 单独使用(未与 def 搭配)→ 关键字 token,由 parser 报错。 + // 一律生成修饰关键字 token,由 parser 判断是否与 def 搭配。 return 'viperModifier'; } + if (name === 'fn' || name === 'fun' || name === 'func' || name === 'function') { + // def 别名:语句起始(或紧跟在修饰关键字后,如 `inline fn f()`)且后跟 + // 标识符(函数名)时按 def 关键字处理。 + if (!isViperAtStatementStart(text, identifierStart) && !precededByViperModifier) { + return undefined; + } + return nextWord.length > 0 && isIdentifierStartChar(nextWord.charCodeAt(0)) ? 'def' : undefined; + } if (name === 'enum' || name === 'rnum') { if (nextWord === 'class' || nextWord === 'struct') { return 'skip'; @@ -1468,12 +1477,20 @@ export class Tokenizer { } else { const value = this._internIdentifier(text, start, end, length); // Viper 扩展:C 风格修饰/声明关键字。 - // inline/export/extern 作为 def 前缀修饰、enum/rnum 作为 class/struct - // 前缀时跳过该 token(保留原始位置与缩进;着色由 TextMate 规则处理); + // inline/export/extern 生成修饰关键字 token(parser 判断是否配 def); // enum/rnum/struct 在语句起始且后跟类名时按 class 关键字处理; - // inline/export/extern 单独使用(未与 def 搭配)时生成修饰关键字 token, - // 由 parser 报错。 - const viperAction = getViperStatementKeywordAction(text, start, end, value); + // fn/fun/func/function 在语句起始(或紧跟修饰关键字)时按 def 关键字处理。 + const precededByViperModifier = + this._tokens.length > 0 && + this._tokens[this._tokens.length - 1].type === TokenType.Keyword && + (this._tokens[this._tokens.length - 1] as KeywordToken).keywordType === KeywordType.ViperModifier; + const viperAction = getViperStatementKeywordAction( + text, + start, + end, + value, + precededByViperModifier + ); if (viperAction === 'skip') { return true; } @@ -1481,6 +1498,10 @@ export class Tokenizer { this._tokens.push(KeywordToken.create(start, length, KeywordType.Class, this._getComments())); return true; } + if (viperAction === 'def') { + this._tokens.push(KeywordToken.create(start, length, KeywordType.Def, this._getComments())); + return true; + } if (viperAction === 'viperModifier') { this._tokens.push( KeywordToken.create(start, length, KeywordType.ViperModifier, this._getComments()) diff --git a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts index 6c9448dc..86906f9e 100644 --- a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts +++ b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts @@ -340,7 +340,7 @@ export def double_export(x: t.CInt) -> t.CInt: return x * 2 extern def double_extern(x: t.CInt) -> t.CInt: - return x * 2 + pass # state 不是关键字,仍可作字段名/成员名 state: t.CInt = 0 @@ -471,4 +471,89 @@ inline def ok(x: t.CInt) -> t.CInt: fs.rmSync(testDir, { recursive: true, force: true }); } serviceProvider.dispose(); +}); + +test('Viper def 别名 fn/fun/func/function 与 extern 函数体校验', () => { + const tempFile = new RealTempFile(); + const serviceProvider = createServiceProvider(createFromRealFileSystem(tempFile), new NullConsole(), tempFile); + const projectRoot = UriEx.file(TRANSPYC_INCLUDES); + const configOptions = new ConfigOptions(projectRoot); + configOptions.internalTestMode = true; + configOptions.initializeTypeCheckingMode('standard'); + + const execEnv = new ExecutionEnvironment( + 'python', + projectRoot, + configOptions.diagnosticRuleSet, + configOptions.defaultPythonVersion, + configOptions.defaultPythonPlatform, + [projectRoot], + /* skipNativeLibraries */ false, + /* defaultDialect */ 'viper' + ); + configOptions.executionEnvironments = [execEnv]; + + const code = `import t + +# def 别名 +fn f1(x: t.CInt) -> t.CInt: + return x + +fun f2(x: t.CInt) -> t.CInt: + return x + +func f3(x: t.CInt) -> t.CInt: + return x + +function f4(x: t.CInt) -> t.CInt: + return x + +# extern:函数体必须为 pass 或 None +extern def e_ok1(x: t.CInt) -> t.CInt: + pass + +extern def e_ok2(x: t.CInt) -> t.CInt: + None + +extern def e_bad(x: t.CInt) -> t.CInt: + return x + +def use(): + a: t.CInt = f1(1) + f2(2) + f3(3) + f4(4) +`; + const testDir = path.join(__dirname, '__viper_alias_extern_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'alias_extern.vp'); + fs.writeFileSync(testFile, code, 'utf8'); + + try { + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); + const program = new Program(importResolver, configOptions, serviceProvider); + const uri = UriEx.file(testFile); + program.setTrackedFiles([uri]); + while (program.analyze()) { + // 循环 + } + + const diagnostics = getDiagnostics(program, configOptions, uri); + console.log('=== alias_extern.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望恰好 1 个 extern 函数体错误(e_bad),fn/fun/func/function 别名与 + // extern pass/None 函数体都不应报错。 + const externErrors = diagnostics.filter( + (d) => d.includes('必须为 pass 或 None') || d.includes("body of only 'pass' or 'None'") + ); + assert.strictEqual( + externErrors.length, + 1, + `expected 1 extern body error, got: ${JSON.stringify(diagnostics)}` + ); + + program.dispose(); + } finally { + fs.rmSync(testDir, { recursive: true, force: true }); + } + serviceProvider.dispose(); }); \ No newline at end of file diff --git a/packages/vscode-vipeye/language-configuration.json b/packages/vscode-vipeye/language-configuration.json index bb59706e..7e67bec8 100644 --- a/packages/vscode-vipeye/language-configuration.json +++ b/packages/vscode-vipeye/language-configuration.json @@ -28,7 +28,7 @@ ], "onEnterRules": [ { - "beforeText": "^\\s*(?:(?:inline|export|extern|enum|rnum)\\s+)?(?:def|class|for|if|elif|else|while|try|finally|with|except|async|struct)\\b.*?:\\s*$", + "beforeText": "^\\s*(?:(?:inline|export|extern|enum|rnum)\\s+)?(?:def|fn|fun|func|function|class|for|if|elif|else|while|try|finally|with|except|async|struct)\\b.*?:\\s*$", "action": { "indent": "indent" } }, { @@ -45,7 +45,7 @@ }, "wordPattern": "(?x)\n \\b\\w*\\b \n | (?<=\\.)\\w+\n | (?<=@)\\w+\n | \\p{Lu}[\\p{Ll}_\\p{Lu}\\p{Nd}]*\n | [[:alpha:]_][[:alpha:]_\\p{Nd}]*\n | \\p{Lu}[\\p{Ll}_\\p{Lu}\\p{Nd}]*f?\\b", "indentationRules": { - "increaseIndentPattern": "^\\s*(?:(?:inline|export|extern|enum|rnum)\\s+)?(?:class|def|elif|else|except|finally|for|if|try|while|with|struct|async\\s+def|async\\s+for|async\\s+with)\\b.*?:\\s*$", + "increaseIndentPattern": "^\\s*(?:(?:inline|export|extern|enum|rnum)\\s+)?(?:class|def|fn|fun|func|function|elif|else|except|finally|for|if|try|while|with|struct|async\\s+def|async\\s+for|async\\s+with)\\b.*?:\\s*$", "decreaseIndentPattern": "^\\s*(elif|else|except|finally)\\b.*?:\\s*$" } } diff --git a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json index 186cb352..37994255 100644 --- a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json +++ b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json @@ -37,6 +37,10 @@ "name": "storage.type.function.viper", "match": "\\bdef\\b" }, + { + "name": "storage.type.function.viper", + "match": "\\b(fn|fun|func|function)\\b(?=\\s+[A-Za-z_][A-Za-z0-9_]*)" + }, { "name": "storage.type.class.viper", "match": "\\bclass\\b"