diff --git a/server/src/project/parser/vbaListener.ts b/server/src/project/parser/vbaListener.ts index d4c36c3..18c14db 100644 --- a/server/src/project/parser/vbaListener.ts +++ b/server/src/project/parser/vbaListener.ts @@ -371,8 +371,19 @@ export class VbaListener extends vbaListener { this.registerNameElement(); }; - enterUnrestrictedName = (ctx: UnrestrictedNameContext) => this.addNameElementContext(ctx, 'enterUnrestrictedName'); - enterSimpleNameExpression = (ctx: SimpleNameExpressionContext) => this.addNameElementContext(ctx, 'enterSimpleNameExpression'); + enterUnrestrictedName = (ctx: UnrestrictedNameContext) => { + if (!this.hasActiveNameElement()) { + return; + } + this.addNameElementContext(ctx, 'enterUnrestrictedName'); + }; + + enterSimpleNameExpression = (ctx: SimpleNameExpressionContext) => { + if (!this.hasActiveNameElement()) { + return; + } + this.addNameElementContext(ctx, 'enterSimpleNameExpression'); + }; private addNameElementContext(ctx: UnrestrictedNameContext | SimpleNameExpressionContext | AmbiguousIdentifierContext, source: string) { if (this.verbose) Services.logger.debug(`${source}: ${ctx.getText()}`, this.parserStateStack.length); @@ -385,6 +396,10 @@ export class VbaListener extends vbaListener { nameElement.addName(ctx); } + private hasActiveNameElement(): boolean { + return this.parserState.nameElements.length > 0; + } + /** * Creates and pushes the active name-expression container for the current * parse context. diff --git a/server/src/test/vbaListener.test.ts b/server/src/test/vbaListener.test.ts index fa92252..d16e096 100644 --- a/server/src/test/vbaListener.test.ts +++ b/server/src/test/vbaListener.test.ts @@ -1,6 +1,8 @@ import 'reflect-metadata'; import { describe, it } from 'mocha'; import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; import dedent from 'dedent'; import { container } from 'tsyringe'; import { CancellationTokenSource, MessageType } from 'vscode-languageserver'; @@ -31,6 +33,28 @@ function assertNoErrorLogs(logs: LogNotification[], context: string): void { ); } +function findScopeItem( + root: ScopeItemCapability, + predicate: (item: ScopeItemCapability) => boolean +): ScopeItemCapability | undefined { + if (predicate(root)) { + return root; + } + + for (const map of root.maps) { + for (const items of map.values()) { + for (const item of items) { + const result = findScopeItem(item, predicate); + if (result) { + return result; + } + } + } + } + + return undefined; +} + function registerTestServices(logs: LogNotification[]): void { container.clearInstances(); @@ -103,4 +127,61 @@ describe('VBA Listener Integration', () => { assertNoErrorLogs(logs, 'ParamArray parse'); }); + + it('does not log errors for worksheet assignment', async () => { + const logs: LogNotification[] = []; + const vbaCode = dedent` + Attribute VB_Name = "aaaaaaaaa" + + option explicit + + Public Sub Identifier() + + dim g_vouTempSht As Worksheet + Set g_vouTempSht = g_wb.sheets("科目表") + End Sub + `; + + await parseText('file:///test/WorksheetAssignment.bas', vbaCode, logs); + + const projectScope = container.resolve('ProjectScope'); + const subroutineScope = findScopeItem(projectScope, item => + item.type === ScopeType.SUBROUTINE && item.name === 'Identifier' + ); + + assert.ok(subroutineScope, 'Expected to resolve subroutine scope for Identifier'); + + const variableScope = findScopeItem(projectScope, item => + item.type === ScopeType.VARIABLE + && item.name === 'g_vouTempSht' + && item.parent?.name === 'Identifier' + ); + + assert.ok(variableScope, 'Expected to resolve variable scope for g_vouTempSht'); + assert.strictEqual(variableScope?.name, 'g_vouTempSht'); + assert.strictEqual(variableScope?.classTypeName, 'Worksheet'); + + const variableReference = findScopeItem(projectScope, item => + item.type === ScopeType.REFERENCE + && item.name === 'g_vouTempSht' + && item.parent?.name === 'Identifier' + ); + + assert.ok(variableReference, 'Expected to resolve reference scope for g_vouTempSht'); + assert.strictEqual(variableReference?.link?.name, 'g_vouTempSht'); + assert.strictEqual(variableReference?.link?.type, ScopeType.VARIABLE); + + assertNoErrorLogs(logs, 'Worksheet assignment parse'); + }); + + it('does not log errors for ExternalTypeReferences fixture', async () => { + const logs: LogNotification[] = []; + const fixturePath = path.join(__dirname, '../../../test/fixtures/ExternalTypeReferences.bas'); + const vbaCode = fs.readFileSync(fixturePath, 'utf8'); + + await parseText('file:///test/ExternalTypeReferences.bas', vbaCode, logs); + + assertNoErrorLogs(logs, 'ExternalTypeReferences fixture parse'); + }); + });