From 6cda4bbf5e0ce901cdba36998a13fb6db54dc030 Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Sun, 1 Mar 2026 15:52:33 -0500 Subject: [PATCH 1/2] fix: improve missing symbols logging and severity determination --- server/src/project/workspace.ts | 67 ++++++++- server/src/test/workspace.test.ts | 217 ++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 1 deletion(-) diff --git a/server/src/project/workspace.ts b/server/src/project/workspace.ts index 9c952c3..f1dc329 100644 --- a/server/src/project/workspace.ts +++ b/server/src/project/workspace.ts @@ -22,6 +22,7 @@ import { RenameParams, SemanticTokensRangeParams, SymbolInformation, + SymbolKind, TextDocuments, TextEdit, WorkspaceEdit, @@ -414,7 +415,7 @@ class WorkspaceEvents { const symbols = document?.languageServerSymbolInformation() ?? []; if (document) { - switch (getMissingSymbolsLogSeverity(document.textDocument.getText(), symbols)) { + switch (this.getMissingSymbolsLogSeverity(document, symbols)) { case 'error': Services.logger.error(`No document symbols produced for ${document.name}`); break; @@ -429,6 +430,70 @@ class WorkspaceEvents { return symbols; } + /** + * Determines diagnostic log severity for missing outline symbols. + * + * - `error`: no symbols at all (module/class symbol missing) + * - `warn`: only module/class symbol exists but member symbols are expected + * - `none`: symbols are present as expected or document can legitimately have none + */ + private getMissingSymbolsLogSeverity(document: BaseProjectDocument, symbols: SymbolInformation[]): 'none' | 'warn' | 'error' { + if (symbols.length === 0) { + return 'error'; + } + + const hasMemberSymbols = symbols.some(x => x.kind !== SymbolKind.File); + if (hasMemberSymbols) { + return 'none'; + } + + return this.shouldLogMissingSymbols(document) ? 'warn' : 'none'; + } + + /** + * Returns true when an empty symbol result is unexpected and should be surfaced. + * + * Files containing only module options, attributes, preprocessor directives, + * comments, and blank lines can legitimately produce no symbols. + */ + private shouldLogMissingSymbols(document: BaseProjectDocument): boolean { + const lines = document.textDocument.getText().split(/\r?\n/); + let preprocessorDepth = 0; + + for (const rawLine of lines) { + const line = rawLine.trim(); + if (line.length === 0) continue; + if (/^'/.test(line)) continue; + if (/^rem(?:\s|$)/i.test(line)) continue; + if (/^option\b/i.test(line)) continue; + if (/^attribute\s+vb_/i.test(line)) continue; + + if (/^#if\b/i.test(line)) { + preprocessorDepth++; + continue; + } + + if (/^#elseif\b/i.test(line) || /^#else\b/i.test(line)) { + continue; + } + + if (/^#end\s*if\b/i.test(line)) { + preprocessorDepth = Math.max(0, preprocessorDepth - 1); + continue; + } + + if (/^#const\b/i.test(line)) continue; + + if (preprocessorDepth > 0) { + continue; + } + + return true; + } + + return false; + } + private async onFoldingRangesAsync(params: FoldingRangeParams, token: CancellationToken): Promise { const logger = Services.logger; logger.debug('[Event] onFoldingRanges'); diff --git a/server/src/test/workspace.test.ts b/server/src/test/workspace.test.ts index 4b43d73..fd84483 100644 --- a/server/src/test/workspace.test.ts +++ b/server/src/test/workspace.test.ts @@ -3,8 +3,10 @@ import '../extensions/stringExtensions'; import { describe, it } from 'mocha'; import * as assert from 'assert'; +import dedent from 'dedent'; import { container } from 'tsyringe'; import { CancellationTokenSource } from 'vscode-languageserver'; +import { SymbolKind } from 'vscode-languageserver'; import { Workspace } from '../project/workspace'; import { ILanguageServer } from '../injection/interface'; @@ -134,3 +136,218 @@ describe('Workspace document replacement race', () => { } }); }); + +describe('Workspace zero-symbol classification', () => { + it('does not flag legitimate no-symbol module content', () => { + container.clearInstances(); + + const connection = createMockConnection(); + const server = createMockServer(); + + container.registerInstance('_Connection', connection); + container.registerInstance('ILanguageServer', server); + + const workspace = new Workspace(connection, server); + const events = (workspace as any).events; + const moduleText = dedent` + Option Explicit + Attribute VB_Name = "Module1" + ' comment + Rem comment + #If VBA7 Then + #Else + #End If + `; + + const result = (events as any).shouldLogMissingSymbols({ + textDocument: { + getText: () => moduleText + } + }); + + assert.strictEqual(result, false, 'Expected legitimate directive-only content to skip missing-symbol error logging'); + }); + + it('flags substantive code with empty symbol list', () => { + container.clearInstances(); + + const connection = createMockConnection(); + const server = createMockServer(); + + container.registerInstance('_Connection', connection); + container.registerInstance('ILanguageServer', server); + + const workspace = new Workspace(connection, server); + const events = (workspace as any).events; + const moduleText = dedent` + Option Explicit + Public Sub Test() + End Sub + `; + + const result = (events as any).shouldLogMissingSymbols({ + textDocument: { + getText: () => moduleText + } + }); + + assert.strictEqual(result, true, 'Expected substantive code to be flagged when symbols are missing'); + }); + + it('does not flag a procedure wrapped in conditional compilation', () => { + container.clearInstances(); + + const connection = createMockConnection(); + const server = createMockServer(); + + container.registerInstance('_Connection', connection); + container.registerInstance('ILanguageServer', server); + + const workspace = new Workspace(connection, server); + const events = (workspace as any).events; + const moduleText = dedent` + Option Explicit + #If Win64 Then + Public Sub ConditionalProc() + End Sub + #End If + `; + + const result = (events as any).shouldLogMissingSymbols({ + textDocument: { + getText: () => moduleText + } + }); + + assert.strictEqual(result, false, 'Expected conditional-compilation-only procedures to be treated as legitimate zero-symbol content'); + }); +}); + +describe('Workspace missing-symbol log severity', () => { + it('returns error when no symbols are produced at all', () => { + container.clearInstances(); + + const connection = createMockConnection(); + const server = createMockServer(); + + container.registerInstance('_Connection', connection); + container.registerInstance('ILanguageServer', server); + + const workspace = new Workspace(connection, server); + const events = (workspace as any).events; + const moduleText = dedent` + Option Explicit + Public Sub Test() + End Sub + `; + + const severity = (events as any).getMissingSymbolsLogSeverity( + { textDocument: { getText: () => moduleText } }, + [] + ); + + assert.strictEqual(severity, 'error'); + }); + + it('returns warn when only module symbol exists but member symbols are expected', () => { + container.clearInstances(); + + const connection = createMockConnection(); + const server = createMockServer(); + + container.registerInstance('_Connection', connection); + container.registerInstance('ILanguageServer', server); + + const workspace = new Workspace(connection, server); + const events = (workspace as any).events; + const moduleText = dedent` + Option Explicit + Public Sub Test() + End Sub + `; + + const severity = (events as any).getMissingSymbolsLogSeverity( + { textDocument: { getText: () => moduleText } }, + [{ kind: SymbolKind.File }] + ); + + assert.strictEqual(severity, 'warn'); + }); + + it('returns none when only module symbol exists and content is legitimately non-symbolic', () => { + container.clearInstances(); + + const connection = createMockConnection(); + const server = createMockServer(); + + container.registerInstance('_Connection', connection); + container.registerInstance('ILanguageServer', server); + + const workspace = new Workspace(connection, server); + const events = (workspace as any).events; + const moduleText = dedent` + Attribute VB_Name = "Module1" + Option Explicit + `; + + const severity = (events as any).getMissingSymbolsLogSeverity( + { textDocument: { getText: () => moduleText } }, + [{ kind: SymbolKind.File }] + ); + + assert.strictEqual(severity, 'none'); + }); + + it('returns none when only module symbol exists and procedures are in inactive compiler branch', () => { + container.clearInstances(); + + const connection = createMockConnection(); + const server = createMockServer(); + + container.registerInstance('_Connection', connection); + container.registerInstance('ILanguageServer', server); + + const workspace = new Workspace(connection, server); + const events = (workspace as any).events; + const moduleText = dedent` + Option Explicit + #If Win64 Then + #Else + Public Sub ConditionalProc() + End Sub + #End If + `; + + const severity = (events as any).getMissingSymbolsLogSeverity( + { textDocument: { getText: () => moduleText } }, + [{ kind: SymbolKind.File }] + ); + + assert.strictEqual(severity, 'none'); + }); + + it('returns none when member symbols are present', () => { + container.clearInstances(); + + const connection = createMockConnection(); + const server = createMockServer(); + + container.registerInstance('_Connection', connection); + container.registerInstance('ILanguageServer', server); + + const workspace = new Workspace(connection, server); + const events = (workspace as any).events; + const moduleText = dedent` + Option Explicit + Public Sub Test() + End Sub + `; + + const severity = (events as any).getMissingSymbolsLogSeverity( + { textDocument: { getText: () => moduleText } }, + [{ kind: SymbolKind.File }, { kind: SymbolKind.Method }] + ); + + assert.strictEqual(severity, 'none'); + }); +}); From f78bfd73a9607068a86f9ed97351bf2936ee600b Mon Sep 17 00:00:00 2001 From: Martin Leduc <31558169+DecimalTurn@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:13:12 -0400 Subject: [PATCH 2/2] refactor: reuse shared missing-symbol severity helper --- server/src/project/workspace.ts | 67 +-------- server/src/test/workspace.test.ts | 217 ------------------------------ 2 files changed, 1 insertion(+), 283 deletions(-) diff --git a/server/src/project/workspace.ts b/server/src/project/workspace.ts index f1dc329..9c952c3 100644 --- a/server/src/project/workspace.ts +++ b/server/src/project/workspace.ts @@ -22,7 +22,6 @@ import { RenameParams, SemanticTokensRangeParams, SymbolInformation, - SymbolKind, TextDocuments, TextEdit, WorkspaceEdit, @@ -415,7 +414,7 @@ class WorkspaceEvents { const symbols = document?.languageServerSymbolInformation() ?? []; if (document) { - switch (this.getMissingSymbolsLogSeverity(document, symbols)) { + switch (getMissingSymbolsLogSeverity(document.textDocument.getText(), symbols)) { case 'error': Services.logger.error(`No document symbols produced for ${document.name}`); break; @@ -430,70 +429,6 @@ class WorkspaceEvents { return symbols; } - /** - * Determines diagnostic log severity for missing outline symbols. - * - * - `error`: no symbols at all (module/class symbol missing) - * - `warn`: only module/class symbol exists but member symbols are expected - * - `none`: symbols are present as expected or document can legitimately have none - */ - private getMissingSymbolsLogSeverity(document: BaseProjectDocument, symbols: SymbolInformation[]): 'none' | 'warn' | 'error' { - if (symbols.length === 0) { - return 'error'; - } - - const hasMemberSymbols = symbols.some(x => x.kind !== SymbolKind.File); - if (hasMemberSymbols) { - return 'none'; - } - - return this.shouldLogMissingSymbols(document) ? 'warn' : 'none'; - } - - /** - * Returns true when an empty symbol result is unexpected and should be surfaced. - * - * Files containing only module options, attributes, preprocessor directives, - * comments, and blank lines can legitimately produce no symbols. - */ - private shouldLogMissingSymbols(document: BaseProjectDocument): boolean { - const lines = document.textDocument.getText().split(/\r?\n/); - let preprocessorDepth = 0; - - for (const rawLine of lines) { - const line = rawLine.trim(); - if (line.length === 0) continue; - if (/^'/.test(line)) continue; - if (/^rem(?:\s|$)/i.test(line)) continue; - if (/^option\b/i.test(line)) continue; - if (/^attribute\s+vb_/i.test(line)) continue; - - if (/^#if\b/i.test(line)) { - preprocessorDepth++; - continue; - } - - if (/^#elseif\b/i.test(line) || /^#else\b/i.test(line)) { - continue; - } - - if (/^#end\s*if\b/i.test(line)) { - preprocessorDepth = Math.max(0, preprocessorDepth - 1); - continue; - } - - if (/^#const\b/i.test(line)) continue; - - if (preprocessorDepth > 0) { - continue; - } - - return true; - } - - return false; - } - private async onFoldingRangesAsync(params: FoldingRangeParams, token: CancellationToken): Promise { const logger = Services.logger; logger.debug('[Event] onFoldingRanges'); diff --git a/server/src/test/workspace.test.ts b/server/src/test/workspace.test.ts index fd84483..4b43d73 100644 --- a/server/src/test/workspace.test.ts +++ b/server/src/test/workspace.test.ts @@ -3,10 +3,8 @@ import '../extensions/stringExtensions'; import { describe, it } from 'mocha'; import * as assert from 'assert'; -import dedent from 'dedent'; import { container } from 'tsyringe'; import { CancellationTokenSource } from 'vscode-languageserver'; -import { SymbolKind } from 'vscode-languageserver'; import { Workspace } from '../project/workspace'; import { ILanguageServer } from '../injection/interface'; @@ -136,218 +134,3 @@ describe('Workspace document replacement race', () => { } }); }); - -describe('Workspace zero-symbol classification', () => { - it('does not flag legitimate no-symbol module content', () => { - container.clearInstances(); - - const connection = createMockConnection(); - const server = createMockServer(); - - container.registerInstance('_Connection', connection); - container.registerInstance('ILanguageServer', server); - - const workspace = new Workspace(connection, server); - const events = (workspace as any).events; - const moduleText = dedent` - Option Explicit - Attribute VB_Name = "Module1" - ' comment - Rem comment - #If VBA7 Then - #Else - #End If - `; - - const result = (events as any).shouldLogMissingSymbols({ - textDocument: { - getText: () => moduleText - } - }); - - assert.strictEqual(result, false, 'Expected legitimate directive-only content to skip missing-symbol error logging'); - }); - - it('flags substantive code with empty symbol list', () => { - container.clearInstances(); - - const connection = createMockConnection(); - const server = createMockServer(); - - container.registerInstance('_Connection', connection); - container.registerInstance('ILanguageServer', server); - - const workspace = new Workspace(connection, server); - const events = (workspace as any).events; - const moduleText = dedent` - Option Explicit - Public Sub Test() - End Sub - `; - - const result = (events as any).shouldLogMissingSymbols({ - textDocument: { - getText: () => moduleText - } - }); - - assert.strictEqual(result, true, 'Expected substantive code to be flagged when symbols are missing'); - }); - - it('does not flag a procedure wrapped in conditional compilation', () => { - container.clearInstances(); - - const connection = createMockConnection(); - const server = createMockServer(); - - container.registerInstance('_Connection', connection); - container.registerInstance('ILanguageServer', server); - - const workspace = new Workspace(connection, server); - const events = (workspace as any).events; - const moduleText = dedent` - Option Explicit - #If Win64 Then - Public Sub ConditionalProc() - End Sub - #End If - `; - - const result = (events as any).shouldLogMissingSymbols({ - textDocument: { - getText: () => moduleText - } - }); - - assert.strictEqual(result, false, 'Expected conditional-compilation-only procedures to be treated as legitimate zero-symbol content'); - }); -}); - -describe('Workspace missing-symbol log severity', () => { - it('returns error when no symbols are produced at all', () => { - container.clearInstances(); - - const connection = createMockConnection(); - const server = createMockServer(); - - container.registerInstance('_Connection', connection); - container.registerInstance('ILanguageServer', server); - - const workspace = new Workspace(connection, server); - const events = (workspace as any).events; - const moduleText = dedent` - Option Explicit - Public Sub Test() - End Sub - `; - - const severity = (events as any).getMissingSymbolsLogSeverity( - { textDocument: { getText: () => moduleText } }, - [] - ); - - assert.strictEqual(severity, 'error'); - }); - - it('returns warn when only module symbol exists but member symbols are expected', () => { - container.clearInstances(); - - const connection = createMockConnection(); - const server = createMockServer(); - - container.registerInstance('_Connection', connection); - container.registerInstance('ILanguageServer', server); - - const workspace = new Workspace(connection, server); - const events = (workspace as any).events; - const moduleText = dedent` - Option Explicit - Public Sub Test() - End Sub - `; - - const severity = (events as any).getMissingSymbolsLogSeverity( - { textDocument: { getText: () => moduleText } }, - [{ kind: SymbolKind.File }] - ); - - assert.strictEqual(severity, 'warn'); - }); - - it('returns none when only module symbol exists and content is legitimately non-symbolic', () => { - container.clearInstances(); - - const connection = createMockConnection(); - const server = createMockServer(); - - container.registerInstance('_Connection', connection); - container.registerInstance('ILanguageServer', server); - - const workspace = new Workspace(connection, server); - const events = (workspace as any).events; - const moduleText = dedent` - Attribute VB_Name = "Module1" - Option Explicit - `; - - const severity = (events as any).getMissingSymbolsLogSeverity( - { textDocument: { getText: () => moduleText } }, - [{ kind: SymbolKind.File }] - ); - - assert.strictEqual(severity, 'none'); - }); - - it('returns none when only module symbol exists and procedures are in inactive compiler branch', () => { - container.clearInstances(); - - const connection = createMockConnection(); - const server = createMockServer(); - - container.registerInstance('_Connection', connection); - container.registerInstance('ILanguageServer', server); - - const workspace = new Workspace(connection, server); - const events = (workspace as any).events; - const moduleText = dedent` - Option Explicit - #If Win64 Then - #Else - Public Sub ConditionalProc() - End Sub - #End If - `; - - const severity = (events as any).getMissingSymbolsLogSeverity( - { textDocument: { getText: () => moduleText } }, - [{ kind: SymbolKind.File }] - ); - - assert.strictEqual(severity, 'none'); - }); - - it('returns none when member symbols are present', () => { - container.clearInstances(); - - const connection = createMockConnection(); - const server = createMockServer(); - - container.registerInstance('_Connection', connection); - container.registerInstance('ILanguageServer', server); - - const workspace = new Workspace(connection, server); - const events = (workspace as any).events; - const moduleText = dedent` - Option Explicit - Public Sub Test() - End Sub - `; - - const severity = (events as any).getMissingSymbolsLogSeverity( - { textDocument: { getText: () => moduleText } }, - [{ kind: SymbolKind.File }, { kind: SymbolKind.Method }] - ); - - assert.strictEqual(severity, 'none'); - }); -});