From f05cc00836df0d300b5ced9c232380576d8aa0f4 Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:26:34 -0500 Subject: [PATCH 1/3] test: add listener integration regression test for optional/param-array name handling This test verifies that enterOptionalParam and enterParamArray correctly push a name element before calling addNameElementContext, preventing spurious 'Cannot add name' errors for parameter identifiers. --- package-lock.json | 16 ++++ package.json | 5 +- server/src/test/vbaListener.test.ts | 110 ++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 server/src/test/vbaListener.test.ts 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/test/vbaListener.test.ts b/server/src/test/vbaListener.test.ts new file mode 100644 index 0000000..1c9cc0e --- /dev/null +++ b/server/src/test/vbaListener.test.ts @@ -0,0 +1,110 @@ +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; +}; + +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); + + const cannotAddParam = logs.filter(log => + log.message.includes('Cannot add name test_param') + ); + + assert.strictEqual( + cannotAddParam.length, + 0, + `Expected no "Cannot add name test_param" errors, got ${cannotAddParam.map(x => x.message).join(' | ')}` + ); + }); + + 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); + + const cannotAddParamArray = logs.filter(log => + log.message.includes('Cannot add name args') + ); + + assert.strictEqual( + cannotAddParamArray.length, + 0, + `Expected no "Cannot add name args" errors, got ${cannotAddParamArray.map(x => x.message).join(' | ')}` + ); + }); +}); From b303573a7eb96aa346242dad95fe02d2d32d6fc7 Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Sun, 1 Mar 2026 01:58:53 -0500 Subject: [PATCH 2/3] fix(parser): initialize name stack for Optional/ParamArray parameter identifiers --- server/src/project/elements/naming.ts | 4 ++++ server/src/project/parser/vbaListener.ts | 6 ++++++ 2 files changed, 10 insertions(+) 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); From 29d23a1433bc8af555ddea6e19e772c9d6825223 Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Sun, 1 Mar 2026 11:48:38 -0500 Subject: [PATCH 3/3] test: make sure any error log makes the test fail --- server/src/test/vbaListener.test.ts | 32 +++++++++++++---------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/server/src/test/vbaListener.test.ts b/server/src/test/vbaListener.test.ts index 1c9cc0e..1fce5b5 100644 --- a/server/src/test/vbaListener.test.ts +++ b/server/src/test/vbaListener.test.ts @@ -19,6 +19,18 @@ type LogNotification = { 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(); @@ -73,15 +85,7 @@ describe('VBA Listener Integration', () => { await parseText('file:///test/ScopeDiagnostics.bas', vbaCode, logs); - const cannotAddParam = logs.filter(log => - log.message.includes('Cannot add name test_param') - ); - - assert.strictEqual( - cannotAddParam.length, - 0, - `Expected no "Cannot add name test_param" errors, got ${cannotAddParam.map(x => x.message).join(' | ')}` - ); + assertNoErrorLogs(logs, 'Optional parameter parse'); }); it('does not log ParamArray identifiers as unresolved names', async () => { @@ -97,14 +101,6 @@ describe('VBA Listener Integration', () => { await parseText('file:///test/ParamArrayTest.bas', vbaCode, logs); - const cannotAddParamArray = logs.filter(log => - log.message.includes('Cannot add name args') - ); - - assert.strictEqual( - cannotAddParamArray.length, - 0, - `Expected no "Cannot add name args" errors, got ${cannotAddParamArray.map(x => x.message).join(' | ')}` - ); + assertNoErrorLogs(logs, 'ParamArray parse'); }); });