diff --git a/packages/vipeye-internal/src/localization/localize.ts b/packages/vipeye-internal/src/localization/localize.ts index 17f1b76d..6d29b3a8 100644 --- a/packages/vipeye-internal/src/localization/localize.ts +++ b/packages/vipeye-internal/src/localization/localize.ts @@ -1178,6 +1178,8 @@ export namespace Localizer { export const unexpectedExprToken = () => getRawString('Diagnostic.unexpectedExprToken'); export const unexpectedIndent = () => getRawString('Diagnostic.unexpectedIndent'); export const unexpectedUnindent = () => getRawString('Diagnostic.unexpectedUnindent'); + export const viperModifierRequiresDef = () => + new ParameterizedString<{ name: string }>(getRawString('Diagnostic.viperModifierRequiresDef')); 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 b1027d57..c4a71b1b 100644 --- a/packages/vipeye-internal/src/localization/package.nls.en-us.json +++ b/packages/vipeye-internal/src/localization/package.nls.en-us.json @@ -1618,6 +1618,7 @@ "unexpectedExprToken": "Unexpected token at end of expression", "unexpectedIndent": "Unexpected indentation", "unexpectedUnindent": "Unindent not expected", + "viperModifierRequiresDef": "The modifier \"{name}\" must be used with 'def'", "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 ea974697..aa9ac1d7 100644 --- a/packages/vipeye-internal/src/localization/package.nls.zh-cn.json +++ b/packages/vipeye-internal/src/localization/package.nls.zh-cn.json @@ -616,6 +616,7 @@ "unexpectedExprToken": "表达式末尾出现意外标记", "unexpectedIndent": "意外缩进", "unexpectedUnindent": "不应取消缩进", + "viperModifierRequiresDef": "修饰关键字 \"{name}\" 必须与 def 搭配使用", "unhashableDictKey": "字典密钥必须可哈希", "unhashableSetEntry": "Set 条目必须是可哈希的", "uninitializedAbstractVariables": "抽象基类中定义的变量未在 final 类 \"{classType}\" 中初始化", diff --git a/packages/vipeye-internal/src/parser/parser.ts b/packages/vipeye-internal/src/parser/parser.ts index d1908ae5..f8aea30b 100644 --- a/packages/vipeye-internal/src/parser/parser.ts +++ b/packages/vipeye-internal/src/parser/parser.ts @@ -429,6 +429,30 @@ export class Parser { } switch (this._peekKeywordType()) { + case KeywordType.ViperModifier: { + // Viper 扩展:inline/export/extern 必须与 def 搭配使用。 + // 与 def 搭配时跳过修饰关键字并正常解析函数;单独使用时报错。 + const modifierToken = this._peekToken() as KeywordToken; + if ( + this._peekToken(1).type === TokenType.Keyword && + (this._peekToken(1) as KeywordToken).keywordType === KeywordType.Def + ) { + this._getNextToken(); + return this._parseFunctionDef(); + } + this._addSyntaxError( + LocMessage.viperModifierRequiresDef().format({ + name: this._fileContents!.slice( + modifierToken.start, + modifierToken.start + modifierToken.length + ), + }), + modifierToken + ); + this._getNextToken(); + return this._parseSimpleStatement(); + } + case KeywordType.If: return this._parseIfStatement(); diff --git a/packages/vipeye-internal/src/parser/tokenizer.ts b/packages/vipeye-internal/src/parser/tokenizer.ts index 56b703df..077adfcf 100644 --- a/packages/vipeye-internal/src/parser/tokenizer.ts +++ b/packages/vipeye-internal/src/parser/tokenizer.ts @@ -643,22 +643,35 @@ enum MagicsKind { // Viper 扩展:C 风格修饰/声明关键字处理。 // Viper 支持将 C 修饰/声明语义写成语句前缀关键字: -// inline/export/extern/state def f(...) —— 等价 t.CInline / t.CExport / t.CExtern / t.State +// inline/export/extern def 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 关键字;undefined:按普通标识符处理 -// (变量名、成员名、导入名等,如 `import struct as s` 不受影响)。 +// 返回 'class':把 enum/rnum/struct 当作 class 关键字; +// 返回 'viperModifier':inline/export/extern 单独使用(未与 def 搭配),由 parser 报错; +// undefined:按普通标识符处理(变量名、成员名、导入名、`state:` 注解等)。 function getViperStatementKeywordAction( text: string, identifierStart: number, identifierEnd: number, name: string -): 'skip' | 'class' | undefined { +): 'skip' | 'class' | 'viperModifier' | undefined { const nextWord = peekViperKeywordWord(text, identifierEnd); - if (name === 'inline' || name === 'export' || name === 'extern' || name === 'state') { - return nextWord === 'def' ? 'skip' : undefined; + 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 报错。 + return 'viperModifier'; } if (name === 'enum' || name === 'rnum') { if (nextWord === 'class' || nextWord === 'struct') { @@ -695,6 +708,15 @@ function peekViperKeywordWord(text: string, from: number): string { return text.slice(start, pos); } +function isViperNextCharColon(text: string, from: number): boolean { + let pos = from; + const len = text.length; + while (pos < len && (text[pos] === ' ' || text[pos] === '\t')) { + pos++; + } + return pos < len && text[pos] === ':'; +} + function isViperAtStatementStart(text: string, identifierStart: number): boolean { let pos = identifierStart; while (pos > 0 && (text[pos - 1] === ' ' || text[pos - 1] === '\t')) { @@ -1446,9 +1468,11 @@ export class Tokenizer { } else { const value = this._internIdentifier(text, start, end, length); // Viper 扩展:C 风格修饰/声明关键字。 - // inline/export/extern/state 作为 def 前缀修饰、enum/rnum 作为 class/struct + // inline/export/extern 作为 def 前缀修饰、enum/rnum 作为 class/struct // 前缀时跳过该 token(保留原始位置与缩进;着色由 TextMate 规则处理); - // struct 在语句起始且后跟类名时按 class 关键字处理。 + // enum/rnum/struct 在语句起始且后跟类名时按 class 关键字处理; + // inline/export/extern 单独使用(未与 def 搭配)时生成修饰关键字 token, + // 由 parser 报错。 const viperAction = getViperStatementKeywordAction(text, start, end, value); if (viperAction === 'skip') { return true; @@ -1457,6 +1481,12 @@ export class Tokenizer { this._tokens.push(KeywordToken.create(start, length, KeywordType.Class, this._getComments())); return true; } + if (viperAction === 'viperModifier') { + this._tokens.push( + KeywordToken.create(start, length, KeywordType.ViperModifier, this._getComments()) + ); + return true; + } this._tokens.push(IdentifierToken.create(start, length, value, this._getComments())); } return true; diff --git a/packages/vipeye-internal/src/parser/tokenizerTypes.ts b/packages/vipeye-internal/src/parser/tokenizerTypes.ts index 86b9fece..b7c6f843 100644 --- a/packages/vipeye-internal/src/parser/tokenizerTypes.ts +++ b/packages/vipeye-internal/src/parser/tokenizerTypes.ts @@ -150,6 +150,8 @@ export const enum KeywordType { While, With, Yield, + // Viper 扩展:inline/export/extern 等 C 风格修饰关键字(必须与 def 搭配)。 + ViperModifier, } export const softKeywords = [ diff --git a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts index e623db12..6c9448dc 100644 --- a/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts +++ b/packages/vipeye-internal/src/tests/viperGenericTypeVarCall.test.ts @@ -332,7 +332,7 @@ struct IntPair: a: t.CInt b: t.CInt -# 修饰关键字 + def(等价 t.CInline / t.CExport / t.CExtern / t.State) +# 修饰关键字 + def(等价 t.CInline / t.CExport / t.CExtern) inline def double_inline(x: t.CInt) -> t.CInt: return x * 2 @@ -342,8 +342,8 @@ export def double_export(x: t.CInt) -> t.CInt: extern def double_extern(x: t.CInt) -> t.CInt: return x * 2 -state def double_state(x: t.CInt) -> t.CInt: - return x * 2 +# state 不是关键字,仍可作字段名/成员名 +state: t.CInt = 0 # enum / rnum + class(等价 t.CEnum / t.REnum) enum class Color: @@ -405,4 +405,70 @@ def test_usage(): fs.rmSync(testDir, { recursive: true, force: true }); } serviceProvider.dispose(); +}); + +test('Viper 修饰关键字 inline/export/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 + +inline +export = 5 +extern + 1 + +inline def ok(x: t.CInt) -> t.CInt: + return x +`; + const testDir = path.join(__dirname, '__viper_modifier_test__'); + const fs = require('fs'); + fs.mkdirSync(testDir, { recursive: true }); + const testFile = path.join(testDir, 'modifier.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('=== modifier.vp diagnostics ==='); + diagnostics.forEach((d) => console.log(` ${d}`)); + + // 期望:3 个"必须与 def 搭配"错误(inline / export / extern 单独使用), + // 而 `inline def ok(...)` 不报错。 + const modifierErrors = diagnostics.filter( + (d) => d.includes('必须与 def 搭配') || d.includes('must be used with') + ); + assert.strictEqual( + modifierErrors.length, + 3, + `expected 3 modifier errors, 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 63c31629..bb59706e 100644 --- a/packages/vscode-vipeye/language-configuration.json +++ b/packages/vscode-vipeye/language-configuration.json @@ -28,7 +28,7 @@ ], "onEnterRules": [ { - "beforeText": "^\\s*(?:def|class|for|if|elif|else|while|try|finally|with|except|async).*?:\\s*$", + "beforeText": "^\\s*(?:(?:inline|export|extern|enum|rnum)\\s+)?(?:def|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*(class|def|elif|else|except|finally|for|if|try|while|with|async\\s+def|async\\s+for|async\\s+with)\\b.*:\\s*$", - "decreaseIndentPattern": "^\\s*(elif|else|except|finally)\\b.*:\\s*$" + "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*$", + "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 f0224fe3..186cb352 100644 --- a/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json +++ b/packages/vscode-vipeye/syntaxes/viper.tmLanguage.json @@ -43,7 +43,7 @@ }, { "name": "storage.modifier.viper", - "match": "\\b(inline|export|extern|state)\\b(?=\\s+def\\b)" + "match": "\\b(inline|export|extern)\\b(?=\\s+def\\b|[ \\t]*$)" }, { "name": "storage.type.class.viper",