diff --git a/package-lock.json b/package-lock.json index e8097e7..8e4d977 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.7.1", "antlr4ng-cli": "^2.0.0", + "dedent": "^1.7.1", "esbuild": "^0.25.4", "eslint": "^9.27.0", "js-yaml": "^4.1.0", @@ -3041,6 +3042,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", diff --git a/package.json b/package.json index c5c383b..ceb8e9e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ }, "main": "dist/client/out/extension", "browser": "dist/web/webextension.js", - "activationEvents": ["onLanguage:vba"], + "activationEvents": [ + "onLanguage:vba" + ], "contributes": { "languages": [ { @@ -183,6 +185,7 @@ "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.7.1", "antlr4ng-cli": "^2.0.0", + "dedent": "^1.7.1", "esbuild": "^0.25.4", "eslint": "^9.27.0", "js-yaml": "^4.1.0", diff --git a/server/src/project/elements/naming.ts b/server/src/project/elements/naming.ts index 81f7fc3..ae1c479 100644 --- a/server/src/project/elements/naming.ts +++ b/server/src/project/elements/naming.ts @@ -9,6 +9,8 @@ import { IndexExpressionContext, LExpressionContext, MemberAccessExpressionContext, + OptionalParamContext, + ParamArrayContext, PositionalParamContext, SimpleNameExpressionContext, UnrestrictedNameContext, @@ -37,6 +39,8 @@ export class WithStatementElement extends BaseRuleSyntaxElement { if (this.verbose) Services.logger.debug(`enterOptionalParam: ${ctx.getText()}`, this.parserStateStack.length); + this.pushNameElement(ctx); const identifierCtx = ctx.paramDcl().untypedNameParamDcl()?.ambiguousIdentifier() ?? ctx.paramDcl().typedNameParamDcl()?.typedName().ambiguousIdentifier(); @@ -361,6 +362,7 @@ export class VbaListener extends vbaListener { enterParamArray = (ctx: ParamArrayContext) => { if (this.verbose) Services.logger.debug(`enterParamArray: ${ctx.getText()}`, this.parserStateStack.length); + this.pushNameElement(ctx); this.addNameElementContext(ctx.ambiguousIdentifier(), 'ambigiousNameContext'); }; @@ -383,6 +385,10 @@ export class VbaListener extends vbaListener { nameElement.addName(ctx); } + /** + * Creates and pushes the active name-expression container for the current + * parse context. + */ private pushNameElement(ctx: NameExpressionContext): void { if (this.verbose) Services.logger.debug('Pushing name', this.parserStateStack.length); const element = new NameExpressionElement(ctx, this.document.textDocument); diff --git a/server/src/test/vbaListener.test.ts b/server/src/test/vbaListener.test.ts new file mode 100644 index 0000000..1fce5b5 --- /dev/null +++ b/server/src/test/vbaListener.test.ts @@ -0,0 +1,106 @@ +import 'reflect-metadata'; +import { describe, it } from 'mocha'; +import * as assert from 'assert'; +import dedent from 'dedent'; +import { container } from 'tsyringe'; +import { CancellationTokenSource } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + +import '../extensions/antlrCoreExtensions'; +import '../extensions/antlrVbaParserExtensions'; +import '../extensions/stringExtensions'; + +import { ScopeItemCapability, ScopeType } from '../capabilities/capabilities'; +import { BaseProjectDocument } from '../project/document'; + +type LogNotification = { + type: number; + message: string; + level: number; +}; + +const ERROR_LOG_TYPE = 1; + +function assertNoErrorLogs(logs: LogNotification[], context: string): void { + const errorLogs = logs.filter(log => log.type === ERROR_LOG_TYPE); + + assert.strictEqual( + errorLogs.length, + 0, + `${context} produced error logs: ${errorLogs.map(x => x.message).join(' | ')}` + ); +} + +function registerTestServices(logs: LogNotification[]): void { + container.clearInstances(); + + container.registerInstance('_Connection', { + sendNotification: (_method: string, payload: LogNotification) => logs.push(payload) + } as any); + + container.registerInstance('ILanguageServer', { + clientConfiguration: Promise.resolve({ + maxDocumentLines: 50000, + maxNumberOfProblems: 100, + doWarnOptionExplicitMissing: true, + environment: { os: 'test', version: 'test' }, + logLevel: { outputChannel: 'debug' } + }) + } as any); + + container.registerInstance('IWorkspace', { + clearDocumentsConfiguration: () => undefined, + formatParseDocument: async () => undefined, + parseDocument: async () => undefined, + openDocument: () => undefined, + closeDocument: () => undefined, + addWorkspaceFolder: async () => undefined + } as any); + + const languageScope = new ScopeItemCapability(undefined, ScopeType.VBA); + const appScope = new ScopeItemCapability(undefined, ScopeType.APPLICATION, undefined, languageScope); + const projectScope = new ScopeItemCapability(undefined, ScopeType.PROJECT, undefined, appScope); + container.registerInstance('ProjectScope', projectScope); +} + +async function parseText(uri: string, text: string, logs: LogNotification[]): Promise { + registerTestServices(logs); + const textDocument = TextDocument.create(uri, 'vba', 1, text); + const projectDocument = BaseProjectDocument.create(textDocument); + await projectDocument.parse(new CancellationTokenSource().token); +} + +describe('VBA Listener Integration', () => { + it('does not log optional parameter identifiers as unresolved names', async () => { + const logs: LogNotification[] = []; + const vbaCode = dedent` + Attribute VB_Name = "ScopeDiagnostics" + + Option Explicit + + Public Sub TestSub(Optional test_param As Variant = -0.1) + Attribute TestSub.VB_Description = "docstring." + End Sub + `; + + await parseText('file:///test/ScopeDiagnostics.bas', vbaCode, logs); + + assertNoErrorLogs(logs, 'Optional parameter parse'); + }); + + it('does not log ParamArray identifiers as unresolved names', async () => { + const logs: LogNotification[] = []; + const vbaCode = dedent` + Attribute VB_Name = "ParamArrayTest" + + Option Explicit + + Public Sub TestVariadicSub(ParamArray args() As Variant) + End Sub + `; + + await parseText('file:///test/ParamArrayTest.bas', vbaCode, logs); + + assertNoErrorLogs(logs, 'ParamArray parse'); + }); +});