Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion server/src/project/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
RenameParams,
SemanticTokensRangeParams,
SymbolInformation,
SymbolKind,
TextDocuments,
TextEdit,
WorkspaceEdit,
Expand Down Expand Up @@ -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;
Expand All @@ -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';
}
Comment thread
DecimalTurn marked this conversation as resolved.
Outdated

/**
* 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.
Comment thread
DecimalTurn marked this conversation as resolved.
Outdated
*/
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<FoldingRange[] | undefined> {
const logger = Services.logger;
logger.debug('[Event] onFoldingRanges');
Expand Down
217 changes: 217 additions & 0 deletions server/src/test/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
});
});
Loading