jbenden/vscode-c-cpp-flylint

View on GitHub
server/src/server.ts

Summary

Maintainability
D
1 day
Test Coverage
A
100%
// Copyright (c) 2017-2022 The VSCode C/C++ Flylint Authors
//
// SPDX-License-Identifier: MIT

import {
    Connection,
    createConnection,
    Diagnostic,
    DiagnosticSeverity,
    ErrorMessageTracker,
    InitializeResult,
    IPCMessageReader,
    DidChangeConfigurationNotification,
    IPCMessageWriter,
    TextDocuments,
    TextDocumentChangeEvent,
    TextDocumentSyncKind,
} from 'vscode-languageserver/node';
import {
    TextDocument
} from 'vscode-languageserver-textdocument';
import { ExecuteCommandParams, WorkspaceFolder } from 'vscode-languageclient/node';
import { URI } from 'vscode-uri';
import * as fs from 'fs';
import * as path from 'path';
import * as tmp from 'tmp';
import * as _ from 'lodash';
import * as glob from 'fast-glob';
import { EntryItem } from 'fast-glob/out/types';
import { GlobalSettings, Settings, IConfigurations, propertiesPlatform } from './settings';
import { Linter, Lint, fromLint, toLint } from './linters/linter';
import { RobustPromises, path as sysPath } from './utils';

import { Clang } from './linters/clang';
import { CppCheck } from './linters/cppcheck';
import { FlawFinder } from './linters/flawfinder';
import { Flexelint } from './linters/flexelint';
import { PclintPlus } from './linters/pclintplus';
import { Lizard } from './linters/lizard';

const JSON5Module = require('json5');
const JSON5 = JSON5Module.default || JSON5Module;

/** Identifier that is used to associate diagnostic entries with code actions. */
export const FLYLINT_ID = 'c-cpp-flylint';

/** Code that is used to associate diagnostic entries with code actions. */
export const FLYLINT_MATCH = /c-cpp-flylint/;

const substituteVariables = require('var-expansion').substituteVariables; // no types available

// Create a connection for the server. The connection uses Node's IPC as a transport.
const connection: Connection = createConnection(new IPCMessageReader(process), new IPCMessageWriter(process));

// Create a simple text document manager. The text document manager supports full document sync only.
let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

// Does the LS client support the configuration abilities?
let hasConfigurationCapability = false;

let defaultSettings: Settings;
let globalSettings: Settings;

// A mapping between an opened document and its' workspace settings.
let documentSettings: Map<string, Thenable<Settings>> = new Map();

// A mapping between an opened document and its' configured analyzers.
let documentLinters: Map<string, Thenable<Linter[]>> = new Map();

// A mapping between an opened document and its' configured analyzers.
let documentVersions: Map<string, number> = new Map();

export type InternalDiagnostic = { severity: DiagnosticSeverity, line: number, column: number, message: string, code: undefined | number | string, source: string, parseError?: any, fileName: string };

namespace CommandIds {
    export const analyzeActiveDocument: string = 'c-cpp-flylint.analyzeActiveDocument';
    export const analyzeWorkspace: string = 'c-cpp-flylint.analyzeWorkspace';
}

// Clear the entire contents of TextDocument related caches.
/* istanbul ignore next */
function flushCache() {
    documentLinters.clear();
    documentSettings.clear();
    documentVersions.clear();
}

// After the server has started the client sends an initialize request.
/* istanbul ignore next */
connection.onInitialize((params): InitializeResult => {
    let capabilities = params.capabilities;

    hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);

    return {
        capabilities: {
            textDocumentSync: {
                openClose: true,
                change: TextDocumentSyncKind.Full,
                willSaveWaitUntil: false,
                save: {
                    includeText: false
                }
            },
            workspace: {
                workspaceFolders: {
                    supported: true
                }
            },
            executeCommandProvider: {
                commands: [
                    CommandIds.analyzeActiveDocument,
                    CommandIds.analyzeWorkspace,
                ]
            }
        }
    };
});

/* istanbul ignore next */
connection.onInitialized(async () => {
    if (hasConfigurationCapability) {
        await connection.client.register(
            DidChangeConfigurationNotification.type,
            undefined
        );
    }
});

let didStart = false;

/* istanbul ignore next */
connection.onDidChangeConfiguration(async change => {
    if (hasConfigurationCapability) {
        flushCache();
    } else {
        globalSettings = <Settings>(change.settings[FLYLINT_ID] || defaultSettings);
    }

    await validateAllDocuments({ force: false });
});

/* istanbul ignore next */
connection.onNotification('begin', async (_params: any) => {
    didStart = true;

    // validateTextDocument(params.document ?? null, false);
    await validateAllDocuments({ force: false });
});

/* istanbul ignore next */
connection.onNotification('end', () => {
    didStart = false;
});

/* istanbul ignore next */
connection.onNotification('onBuild', async (params: any) => {
    // eslint-disable-next-line no-console
    console.log('Received a notification that a build has completed: ' + _.toString(params));

    let settings = await getDocumentSettings(params.document ?? null);

    const userLintOn: Lint = toLint(settings.run);
    if (userLintOn !== Lint.ON_BUILD) {
        // eslint-disable-next-line no-console
        console.log(`Skipping analysis because ${fromLint(userLintOn)} !== ON_BUILD.`);
        return;
    }

    await validateAllDocuments({ force: false });
});

/* istanbul ignore next */
async function getWorkspaceRoot(resource: string): Promise<string> {
    const resourceUri: URI = URI.parse(resource);
    const resourceFsPath: string = resourceUri.fsPath;

    let folders: WorkspaceFolder[] | null = await connection.workspace.getWorkspaceFolders();
    let result: string = '';

    if (folders !== null) {
        // sort folders by length, decending.
        folders = folders.sort((a: WorkspaceFolder, b: WorkspaceFolder): number => {
            return a.uri === b.uri ? 0 : (a.uri.length <= b.uri.length ? 1 : -1);
        });

        // look for a matching workspace folder root.
        folders.forEach(f => {
            const folderUri: URI = URI.parse(f.uri);
            const folderFsPath: string = folderUri.fsPath;

            // does the resource path start with this folder path?
            if (path.normalize(resourceFsPath).startsWith(path.normalize(folderFsPath))) {
                // found the project root for this file.
                result = path.normalize(folderFsPath);
            }
        });
    } else {
        // No matching workspace folder, so return the folder the file lives in.
        result = path.dirname(path.normalize(resourceFsPath));
    }

    return result;
}

/* istanbul ignore next */
async function getDocumentSettings(resource: string): Promise<Settings> {
    /* istanbul ignore next */
    if (!hasConfigurationCapability) {
        return Promise.resolve(globalSettings);
    }

    let result: Thenable<Settings> | undefined = documentSettings.get(resource);
    if (!result) {
        let workspaceRoot: string = await getWorkspaceRoot(resource);
        let globalSettings: Thenable<GlobalSettings> = connection.workspace.getConfiguration({ scopeUri: resource }).then(s => getMergedSettings(s, workspaceRoot));
        result = globalSettings.then(v => v[FLYLINT_ID]);
        documentSettings.set(resource, result);
    }

    return result!;
}

/* istanbul ignore next */
async function reconfigureExtension(currentSettings: Settings, workspaceRoot: string): Promise<Linter[]> {
    let linters: Linter[] = [];  // clear array

    if (currentSettings.clang.enable) { linters.push(await (new Clang(currentSettings, workspaceRoot).initialize()) as Clang); }
    if (currentSettings.cppcheck.enable) { linters.push(await (new CppCheck(currentSettings, workspaceRoot).initialize()) as CppCheck); }
    if (currentSettings.flexelint.enable) { linters.push(await (new Flexelint(currentSettings, workspaceRoot).initialize()) as Flexelint); }
    if (currentSettings.pclintplus.enable) { linters.push(await (new PclintPlus(currentSettings, workspaceRoot).initialize()) as PclintPlus); }
    if (currentSettings.flawfinder.enable) { linters.push(await (new FlawFinder(currentSettings, workspaceRoot).initialize()) as FlawFinder); }
    if (currentSettings.lizard.enable) { linters.push(await (new Lizard(currentSettings, workspaceRoot).initialize()) as Lizard); }

    _.forEach(linters, (linter) => {
        if (linter.isActive() && !linter.isEnabled()) {
            connection.window.showWarningMessage(`Unable to activate ${linter.Name()} analyzer.`);
        }
    });

    return linters;
}

/* istanbul ignore next */
export async function getCppProperties(cCppPropertiesPath: string, currentSettings: GlobalSettings, workspaceRoot: string) {
    try {
        if (fs.existsSync(cCppPropertiesPath)) {
            const matchOn: string = await getActiveConfigurationName(currentSettings[FLYLINT_ID]);
            const cCppProperties: IConfigurations = JSON5.parse(fs.readFileSync(cCppPropertiesPath, 'utf8'));
            const platformConfig = cCppProperties.configurations.find(el => el.name === matchOn);

            if (platformConfig !== undefined) {
                // Found a configuration set; populate the currentSettings
                if (platformConfig.includePath.length > 0) {
                    process.env.workspaceRoot = workspaceRoot;
                    process.env.workspaceFolder = workspaceRoot;

                    _.forEach(platformConfig.includePath, (ipath: string) => {
                        //skip empty paths
                        if (ipath === '') {
                            return;
                        }
                        try {
                            let { value } = substituteVariables(ipath, { env: process.env });
                            let globbed_path = glob.sync(value, { cwd: workspaceRoot, dot: false, onlyDirectories: true, unique: true, absolute: true });

                            if (currentSettings[FLYLINT_ID].debug) {
                                // eslint-disable-next-line no-console
                                console.log('Path: ' + ipath + '  VALUE: ' + value + '  Globbed is: ' + globbed_path.toString());
                            }

                            _.each(globbed_path, (gpath: string | EntryItem) => {
                                let currentFilePath = sysPath(path.resolve(gpath as string));

                                if (path.normalize(currentFilePath).startsWith(path.normalize(workspaceRoot!))) {
                                    let acceptFile: boolean = true;

                                    // see if we are to accept the diagnostics upon this file.
                                    _.each(currentSettings[FLYLINT_ID].excludeFromWorkspacePaths, (excludedPath: string) => {
                                        let substExcludedPath = substituteVariables(excludedPath, { env: process.env, ignoreErrors: true });
                                        let normalizedExcludedPath = path.normalize(substExcludedPath.value || '');

                                        if (currentSettings[FLYLINT_ID].debug) {
                                            // eslint-disable-next-line no-console
                                            console.log('Exclude Path: ' + excludedPath + '  VALUE: ' + substExcludedPath.value + '  Normalized: ' + normalizedExcludedPath);
                                        }

                                        if (!path.isAbsolute(normalizedExcludedPath)) {
                                            // prepend the workspace path and renormalize the path.
                                            normalizedExcludedPath = path.normalize(path.join(workspaceRoot!, normalizedExcludedPath));
                                        }

                                        // does the document match our excluded path?
                                        if (path.normalize(currentFilePath).startsWith(normalizedExcludedPath)) {
                                            // it did; so do not accept diagnostics from this file.
                                            acceptFile = false;
                                        }
                                    });

                                    if (acceptFile) {
                                        if (currentSettings[FLYLINT_ID].debug) {
                                            // eslint-disable-next-line no-console
                                            console.log('Adding path: ' + currentFilePath);
                                        }

                                        currentSettings[FLYLINT_ID].includePaths =
                                            _.uniq(currentSettings[FLYLINT_ID].includePaths.concat(currentFilePath));
                                    }
                                } else {
                                    // file is outside of workspace root, perhaps a system folder
                                    if (currentSettings[FLYLINT_ID].debug) {
                                        // eslint-disable-next-line no-console
                                        console.log('Adding system path: ' + currentFilePath);
                                    }

                                    currentSettings[FLYLINT_ID].includePaths =
                                        _.uniq(currentSettings[FLYLINT_ID].includePaths.concat(currentFilePath));
                                }
                            });
                        }
                        catch (err) {
                            // eslint-disable-next-line no-console
                            console.error(err);
                        }
                    });
                }

                if (platformConfig.defines.length > 0) {
                    currentSettings[FLYLINT_ID].defines =
                        _.uniq(currentSettings[FLYLINT_ID].defines.concat(platformConfig.defines));
                }
            }
        }
    }
    catch (err) {
        // eslint-disable-next-line no-console
        console.log('Could not find or parse the workspace c_cpp_properties.json file; continuing...');
        // eslint-disable-next-line no-console
        console.error(err);
    }

    return currentSettings;
}

/* istanbul ignore next */
async function getActiveConfigurationName(_currentSettings: Settings): Promise<string> {
    return RobustPromises.retry(40, 250, 1000, () => connection.sendRequest<string>('c-cpp-flylint.cpptools.activeConfigName')).then(r => {
        if (!_.isArrayLike(r) || r.length === 0) { return propertiesPlatform(); } else { return r; }
    });
}

/* istanbul ignore next */
function getMergedSettings(settings: GlobalSettings, workspaceRoot: string): Promise<GlobalSettings> {
    let currentSettings = _.cloneDeep(settings);
    const cCppPropertiesPath = path.join(workspaceRoot, '.vscode', 'c_cpp_properties.json');

    return getCppProperties(cCppPropertiesPath, currentSettings, workspaceRoot);
}

/* istanbul ignore next */
async function getDocumentLinters(resource: string): Promise<Linter[]> {
    const settings: Settings = await getDocumentSettings(resource);

    let result = documentLinters.get(resource);
    if (!result) {
        const workspaceRoot: string = await getWorkspaceRoot(resource);

        result = Promise.resolve(await reconfigureExtension(settings, workspaceRoot));
        documentLinters.set(resource, result);
    }

    return result!;
}

// Only keep analyzers and settings for opened documents.
/* istanbul ignore next */
documents.onDidClose(e => {
    documentLinters.delete(e.document.uri);
    documentSettings.delete(e.document.uri);
    documentVersions.delete(e.document.uri);
});

/* istanbul ignore next */
async function onChangedContent(event: TextDocumentChangeEvent<TextDocument>): Promise<any> {
    if (didStart) {
        // get the settings for the current file.
        let settings = await getDocumentSettings(event.document.uri);

        const userLintOn: Lint = toLint(settings.run);
        if (userLintOn !== Lint.ON_TYPE) {
            // eslint-disable-next-line no-console
            console.log(`Skipping analysis because ${fromLint(userLintOn)} !== ON_TYPE.`);
            return;
        }

        await validateTextDocument(event.document, false);
    }
}

// FIXME: 1500 should be a configurable property!
documents.onDidChangeContent(_.debounce(onChangedContent, 1500));

/* istanbul ignore next */
documents.onDidSave(async (event: TextDocumentChangeEvent<TextDocument>) => {
    // get the settings for the current file.
    let settings = await getDocumentSettings(event.document.uri);

    const userLintOn: Lint = toLint(settings.run);
    if (userLintOn !== Lint.ON_SAVE && userLintOn !== Lint.ON_TYPE) {
        // eslint-disable-next-line no-console
        console.log(`Skipping analysis because ${fromLint(userLintOn)} !== ON_SAVE|ON_TYPE.`);
        return;
    }

    await validateTextDocument(event.document, false);
});

/* istanbul ignore next */
documents.onDidOpen(async (event: TextDocumentChangeEvent<TextDocument>) => {
    if (didStart) {
        await validateTextDocument(event.document, false);
    }
});

/* istanbul ignore next */
async function validateAllDocuments(options: { force: boolean }) {
    const { force } = options || {};

    if (didStart) {
        documents.all().forEach(_.bind(validateTextDocument, _, _, force));
    }
}

/* istanbul ignore next */
async function validateTextDocument(textDocument: TextDocument, force: boolean) {
    const tracker: ErrorMessageTracker = new ErrorMessageTracker();
    const fileUri: URI = URI.parse(textDocument.uri);
    const filePath: string = fileUri.fsPath;
    const normalizedFilePath = sysPath(path.normalize(filePath as string));
    const workspaceRoot: string = await getWorkspaceRoot(textDocument.uri);

    const isTrusted: boolean = await connection.sendRequest('isTrusted');
    if (!isTrusted) {
        // eslint-disable-next-line no-console
        console.log('Will not analyze an untrusted workspace.');
        return;
    }

    if (workspaceRoot === undefined ||
        workspaceRoot === null ||
        filePath === undefined ||
        filePath === null) {
        // lint can only successfully happen in a workspace, not per-file basis
        // eslint-disable-next-line no-console
        console.log('Will not analyze a lone file; must open a folder workspace.');
        return;
    }

    if (fileUri.scheme !== 'file') {
        // lint can only lint files on disk.
        // eslint-disable-next-line no-console
        console.log(`Skipping scan of non-local content at ${fileUri.toString()}`);
        return;
    }

    // get the settings for the current file.
    let settings = await getDocumentSettings(textDocument.uri);

    // get the linters for the current file.
    let linters = await getDocumentLinters(textDocument.uri);
    if (linters === undefined || linters === null) {
        // cannot perform lint without active configuration!
        tracker.add(`A problem was encountered; the global list of analyzers is null or undefined.`);

        // Send any exceptions encountered during processing to VSCode.
        tracker.sendErrors(connection);

        return;
    }

    // check document version number
    let documentVersion = textDocument.version;
    let lastVersion = documentVersions.get(textDocument.uri);
    if (lastVersion) {
        if (settings.debug) {
            // eslint-disable-next-line no-console
            console.log(`${filePath} is currently version number ${documentVersion} and ${lastVersion} was already been scanned.`);
        }

        if (documentVersion <= lastVersion && !force) {
            if (settings.debug) {
                // eslint-disable-next-line no-console
                console.log(`Skipping scan of ${filePath} because this file version number ${documentVersion} has already been scanned.`);
            }

            return;
        }
    }

    if (settings.debug) {
        // eslint-disable-next-line no-console
        console.log(`${filePath} force = ${force}.`);
        // eslint-disable-next-line no-console
        console.log(`${filePath} is now at version number ${documentVersion}.`);
    }

    let tmpDocument = tmp.fileSync();
    fs.writeSync(tmpDocument.fd, textDocument.getText(), 0, 'utf8');

    const documentLines: string[] = textDocument.getText().replace(/\r/g, '').split('\n');

    const allDiagnostics: Map<String, Diagnostic[]> = new Map<String, Diagnostic[]>();

    const relativePath = path.relative(workspaceRoot, filePath);
    const normalizedRelativePath = sysPath(path.normalize(relativePath));

    // deep-copy current items, so mid-stream configuration change doesn't spoil the party
    const lintersCopy: Linter[] = _.cloneDeep(linters);

    // eslint-disable-next-line no-console
    console.log(`Performing lint scan of ${filePath}...`);

    let hasSkipLinter = false;

    lintersCopy.forEach(linter => {
        try {
            let result = linter.lint(normalizedFilePath, workspaceRoot, tmpDocument.name);

            while (result.length > 0) {
                let diagnostics: Diagnostic[] = [];
                let currentFile: string = '';
                let i = result.length;

                while (i-- >= 0) {
                    let msg: InternalDiagnostic = result[i];

                    if (msg === null || msg === undefined || msg.parseError || !msg.hasOwnProperty('line') || msg.source === '') {
                        result.splice(i, 1);
                        continue;
                    }

                    let normalizedFileName = sysPath(path.normalize(path.resolve(msg.fileName)));

                    if (currentFile === '') {
                        currentFile = normalizedFileName;
                    }

                    if (currentFile !== normalizedFileName) {
                        continue;
                    }

                    if (normalizedRelativePath === normalizedFileName || (path.isAbsolute(normalizedFileName) && normalizedFilePath === normalizedFileName)) {
                        diagnostics.push(makeDiagnostic(documentLines, msg));
                    } else {
                        diagnostics.push(makeDiagnostic(null, msg));
                    }

                    result.splice(i, 1);
                }

                let currentFileUri = URI.file(currentFile).toString();
                diagnostics = _.uniqBy(diagnostics, function(e) { return e.range.start.line + ':::' + e.code + ':::' + e.message; });

                if (allDiagnostics.has(currentFileUri)) {
                    allDiagnostics.set(currentFileUri, _.union(allDiagnostics.get(currentFileUri), diagnostics));
                } else {
                    allDiagnostics.set(currentFileUri, diagnostics);
                }
            }
        } catch (e: any) {
            tracker.add(getErrorMessage(e, textDocument));
        }
    });

    tmpDocument.removeCallback();

    let sendDiagnosticsToEditor = (diagnostics: Diagnostic[], currentFile: string) => {
        let currentFilePath = sysPath(URI.parse(currentFile).fsPath);
        let normalizedCurrentFilePath = currentFilePath;
        let normalizedWorkspaceRoot = sysPath(path.normalize(workspaceRoot!));

        if (normalizedCurrentFilePath.startsWith(normalizedWorkspaceRoot)) {
            let acceptFile: boolean = true;

            // see if we are to accept the diagnostics upon this file.
            _.each(settings.excludeFromWorkspacePaths, (excludedPath) => {
                let normalizedExcludedPath = path.normalize(excludedPath);

                if (!path.isAbsolute(normalizedExcludedPath)) {
                    // prepend the workspace path and renormalize the path.
                    normalizedExcludedPath = path.normalize(path.join(workspaceRoot!, normalizedExcludedPath));
                }

                // does the document match our excluded path?
                if (normalizedCurrentFilePath.startsWith(sysPath(normalizedExcludedPath))) {
                    // it did; so do not accept diagnostics from this file.
                    acceptFile = false;
                }
            });

            if (acceptFile) {
                let uri = URI.file(currentFilePath);

                connection.sendDiagnostics({ uri: uri.toString(), diagnostics: [] });
                connection.sendDiagnostics({ uri: uri.toString(), diagnostics });
            }
        }
    };

    // Send diagnostics to VSCode
    for (let diagnosticEntry of allDiagnostics) {
        sendDiagnosticsToEditor(diagnosticEntry[1], diagnosticEntry[0].toString());
    }

    // Remove all previous problem reports, when no further exist
    let filePathUri = URI.file(filePath).toString();
    if (!allDiagnostics.has(filePathUri)) {
        connection.sendDiagnostics({ uri: filePathUri, diagnostics: [] });
    }

    // eslint-disable-next-line no-console
    console.log('Completed lint scans...');

    if (!hasSkipLinter) {
        documentVersions.set(textDocument.uri, textDocument.version);
    }
    // Send any exceptions encountered during processing to VSCode.
    tracker.sendErrors(connection);
}

/* istanbul ignore next */
function makeDiagnostic(documentLines: string[] | null, msg: InternalDiagnostic): Diagnostic {
    let line: number;
    /* istanbul ignore next */
    if (documentLines !== null) {
        line = _.chain(msg.line)
            .defaultTo(0)
            .clamp(0, documentLines.length - 1)
            .value();
    } else {
        line = msg.line;
    }

    // 0 <= n
    const column: number = msg.column ?? 0;
    const message: string = msg.message ?? 'Unknown error';
    const code: undefined | number | string = msg.code ?? undefined;
    const source: string = msg.source ? `${msg.source} (${FLYLINT_ID})` : FLYLINT_ID;
    let startColumn: number = column;
    let endColumn: number = column + 1;

    if (documentLines !== null && column === 0 && documentLines.length > 0) {
        let l: string = _.nth(documentLines, line) as string;

        // Find the line's starting column, sans-white-space
        let lineMatches = l.match(/\S/);
        if (!_.isNull(lineMatches) && _.isNumber(lineMatches.index)) {
            startColumn = lineMatches.index;
        }

        // Set the line's ending column to the full length of line
        endColumn = l.length;
    }

    return {
        severity: msg.severity,
        range: {
            start: { line: line, character: startColumn },
            end: { line: line, character: endColumn }
        },
        message: message,
        code: code,
        source: source,
    };
}

/* istanbul ignore next */
function getErrorMessage(err: Error, document: TextDocument): string {
    let errorMessage = 'unknown error';
    if (_.isString(err.message)) {
        errorMessage = (err.message as string);
    }

    const fsPathUri = URI.parse(document.uri);
    return `'${errorMessage}' while validating: ${fsPathUri.fsPath}. Please analyze the 'C/C++ FlyLint' Output console. Stacktrace: ${err.stack}`;
}

/* istanbul ignore next */
connection.onDidChangeWatchedFiles((params) => {
    params.changes.forEach(async element => {
        let configFilePath = URI.parse(element.uri);

        if (path.basename(configFilePath.fsPath) === 'c_cpp_properties.json') {
            flushCache();

            await validateAllDocuments({ force: true });
        }
    });
});

/* istanbul ignore next */
connection.onRequest('getLocalConfig', async (activeDocument: TextDocument) => {
    const tracker = new ErrorMessageTracker();

    if (activeDocument !== undefined && activeDocument !== null) {
        let fileUri: URI = <URI>(<any>activeDocument.uri);

        for (const document of documents.all()) {
            try {
                const documentUri = URI.parse(document.uri);

                if (fileUri.fsPath === documentUri.fsPath) {
                    return Promise.resolve(await getDocumentSettings(document.uri));
                }
            } catch (err: any) {
                tracker.add(getErrorMessage(err, document));
            }
        }
    }

    tracker.sendErrors(connection);

    return Promise.reject();
});

/* istanbul ignore next */
connection.onExecuteCommand(async (params: ExecuteCommandParams) => {
    const tracker = new ErrorMessageTracker();

    if (params.command === CommandIds.analyzeActiveDocument) {
        (connection.sendRequest('activeTextDocument') as Thenable<TextDocument>)
            .then(async (activeDocument) => {
                if (activeDocument !== undefined && activeDocument !== null) {
                    let fileUri: URI = <URI>(<any>activeDocument.uri);

                    for (const document of documents.all()) {
                        try {
                            const documentUri = URI.parse(document.uri);

                            if (fileUri.fsPath === documentUri.fsPath) {
                                await validateTextDocument(document, true);
                            }
                        } catch (err: any) {
                            tracker.add(getErrorMessage(err, document));
                        }
                    }
                }
            });
    } else if (params.command === CommandIds.analyzeWorkspace) {
        await validateAllDocuments({ force: true });
    }

    tracker.sendErrors(connection);
});

// Make the text document manager listen on the connection for open, change, and close text document events.
documents.listen(connection);

// Listen on the connection.
connection.listen();