Commit 6dccbee3 authored by Elwyn Benson's avatar Elwyn Benson 2️⃣ Committed by Tristan Read
Browse files

feat: add 'get diagnostics' RPC request handler

parent 4d10ab44
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -87,6 +87,8 @@ module.exports = {
    registerInlineCompletionItemProvider: returnsDisposable(),
    registerCompletionItemProvider: jest.fn(),
    registerCodeActionsProvider: jest.fn(),
    getDiagnostics: jest.fn(),
    onDidChangeDiagnostics: jest.fn(),
  },
  workspace: {
    openTextDocument: jest.fn(),
@@ -202,6 +204,12 @@ module.exports = {
    Source: 'source',
  },
  Diagnostic: jest.fn(),
  DiagnosticSeverity: {
    Error: 0,
    Warning: 1,
    Information: 2,
    Hint: 3,
  },
  CallHierarchyItem: jest.fn(),
  TypeHierarchyItem: jest.fn(),
  SymbolInformation: jest.fn(),
+2 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ export enum FeatureFlag {
  // used for testing purposes
  TestFlag = 'testflag',
  RemoteSecurityScans = 'remoteSecurityScans',
  EditFileDiagnosticsResponse = 'editFileDiagnosticsResponse',
  SecurityScans = 'securityScansFlag',
  ForceCodeSuggestionsViaMonolith = 'forceCodeSuggestionsViaMonolith',
  LanguageServer = 'languageServer',
@@ -21,6 +22,7 @@ export enum FeatureFlag {
// Set the feature flag default value here
export const FEATURE_FLAGS_DEFAULT_VALUES = {
  [FeatureFlag.RemoteSecurityScans]: true,
  [FeatureFlag.EditFileDiagnosticsResponse]: false,
  [FeatureFlag.SecurityScans]: true,
  [FeatureFlag.ForceCodeSuggestionsViaMonolith]: false,
  [FeatureFlag.TestFlag]: false,
+307 −0
Original line number Diff line number Diff line
import {
  ApplyWorkspaceEditParams,
  ApplyWorkspaceEditResult,
  TextDocumentEdit,
  WorkspaceEdit,
} from 'vscode-languageclient';
import { createFakePartial } from '../test_utils/create_fake_partial';
import { DiagnosticsDelayMiddleware } from './diagnostics_delay_middleware';
import { waitForDiagnosticsUpdate } from './wait_for_diagnostics';

jest.mock('./wait_for_diagnostics');

describe('DiagnosticsDelayMiddleware', () => {
  let middleware: DiagnosticsDelayMiddleware;
  let mockNext: jest.MockedFunction<
    (params: ApplyWorkspaceEditParams) => Promise<ApplyWorkspaceEditResult>
  >;
  let mockWaitForDiagnosticsUpdate: jest.MockedFunction<typeof waitForDiagnosticsUpdate>;

  const testUri = 'file:///test.ts';

  let successResult: ApplyWorkspaceEditResult;
  let failureResult: ApplyWorkspaceEditResult;

  beforeEach(() => {
    middleware = new DiagnosticsDelayMiddleware();
    mockNext = jest.fn();
    mockWaitForDiagnosticsUpdate = jest.mocked(waitForDiagnosticsUpdate);
    mockWaitForDiagnosticsUpdate.mockResolvedValue();

    successResult = createFakePartial<ApplyWorkspaceEditResult>({ applied: true });
    failureResult = createFakePartial<ApplyWorkspaceEditResult>({ applied: false });
  });

  describe('when workspace edit has no document changes', () => {
    it('should call next without waiting for diagnostics', async () => {
      const params = createFakePartial<ApplyWorkspaceEditParams>({
        edit: createFakePartial<WorkspaceEdit>({}),
      });

      mockNext.mockResolvedValue(successResult);

      const result = await middleware.process(params, mockNext);

      expect(mockNext).toHaveBeenCalledWith(params);
      expect(mockWaitForDiagnosticsUpdate).not.toHaveBeenCalled();
      expect(result).toBe(successResult);
    });

    it('should call next without waiting when documentChanges is undefined', async () => {
      const params = createFakePartial<ApplyWorkspaceEditParams>({
        edit: createFakePartial<WorkspaceEdit>({
          documentChanges: undefined,
        }),
      });

      mockNext.mockResolvedValue(successResult);

      const result = await middleware.process(params, mockNext);

      expect(mockNext).toHaveBeenCalledWith(params);
      expect(mockWaitForDiagnosticsUpdate).not.toHaveBeenCalled();
      expect(result).toBe(successResult);
    });

    it('should call next without waiting when documentChanges is empty array', async () => {
      const params = createFakePartial<ApplyWorkspaceEditParams>({
        edit: createFakePartial<WorkspaceEdit>({
          documentChanges: [],
        }),
      });

      mockNext.mockResolvedValue(successResult);

      const result = await middleware.process(params, mockNext);

      expect(mockNext).toHaveBeenCalledWith(params);
      expect(mockWaitForDiagnosticsUpdate).not.toHaveBeenCalled();
      expect(result).toBe(successResult);
    });
  });

  describe('when workspace edit has document changes', () => {
    describe('with single text document edit', () => {
      it('should wait for diagnostics when edit is applied successfully', async () => {
        const params = createFakePartial<ApplyWorkspaceEditParams>({
          edit: createFakePartial<WorkspaceEdit>({
            documentChanges: [
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri },
                edits: [{ range: {}, newText: 'new content' }],
              }),
            ],
          }),
        });

        mockNext.mockResolvedValue(successResult);

        const result = await middleware.process(params, mockNext);

        expect(mockNext).toHaveBeenCalledWith(params);
        expect(mockWaitForDiagnosticsUpdate).toHaveBeenCalledWith([testUri]);
        expect(result).toBe(successResult);
      });

      it('should not wait for diagnostics when edit is not applied', async () => {
        jest.useFakeTimers();

        const params = createFakePartial<ApplyWorkspaceEditParams>({
          edit: createFakePartial<WorkspaceEdit>({
            documentChanges: [
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri },
                edits: [{ range: {}, newText: 'new content' }],
              }),
            ],
          }),
        });

        mockNext.mockResolvedValue(failureResult);

        // Create a promise that resolves after a timeout with a flag
        let promiseResolved = false;
        const delayedPromise = new Promise<void>(resolve => {
          setTimeout(() => {
            promiseResolved = true;
            resolve();
          }, 100);
        });
        mockWaitForDiagnosticsUpdate.mockReturnValue(delayedPromise);

        const result = await middleware.process(params, mockNext);

        expect(mockNext).toHaveBeenCalledWith(params);
        expect(mockWaitForDiagnosticsUpdate).toHaveBeenCalledWith([testUri]);
        expect(result).toBe(failureResult);

        expect(promiseResolved).toBe(false);

        jest.useRealTimers();
      });

      it('should not wait for diagnostics when text document edit has no edits', async () => {
        const params = createFakePartial<ApplyWorkspaceEditParams>({
          edit: createFakePartial<WorkspaceEdit>({
            documentChanges: [
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri },
                edits: [],
              }),
            ],
          }),
        });

        mockNext.mockResolvedValue(successResult);

        const result = await middleware.process(params, mockNext);

        expect(mockNext).toHaveBeenCalledWith(params);
        expect(mockWaitForDiagnosticsUpdate).not.toHaveBeenCalled();
        expect(result).toBe(successResult);
      });

      it('should not wait for diagnostics when text document edit has undefined edits', async () => {
        const params = createFakePartial<ApplyWorkspaceEditParams>({
          edit: createFakePartial<WorkspaceEdit>({
            documentChanges: [
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri },
                edits: undefined,
              }),
            ],
          }),
        });

        mockNext.mockResolvedValue(successResult);

        const result = await middleware.process(params, mockNext);

        expect(mockNext).toHaveBeenCalledWith(params);
        expect(mockWaitForDiagnosticsUpdate).not.toHaveBeenCalled();
        expect(result).toBe(successResult);
      });
    });

    describe('with multiple text document edits', () => {
      const testUri1 = 'file:///test1.ts';
      const testUri2 = 'file:///test2.ts';
      const testUri3 = 'file:///test3.ts';
      const otherUri = 'file:///other.ts';

      it('should wait for diagnostics for all edited files', async () => {
        const testUris = [testUri1, testUri2, testUri3];
        const params = createFakePartial<ApplyWorkspaceEditParams>({
          edit: createFakePartial<WorkspaceEdit>({
            documentChanges: testUris.map(uri =>
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri },
                edits: [{ range: {}, newText: 'new content' }],
              }),
            ),
          }),
        });

        mockNext.mockResolvedValue(successResult);

        const result = await middleware.process(params, mockNext);

        expect(mockNext).toHaveBeenCalledWith(params);
        expect(mockWaitForDiagnosticsUpdate).toHaveBeenCalledWith(testUris);
        expect(result).toBe(successResult);
      });

      it('should only wait for files that have edits', async () => {
        const params = createFakePartial<ApplyWorkspaceEditParams>({
          edit: createFakePartial<WorkspaceEdit>({
            documentChanges: [
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri1 },
                edits: [{ range: {}, newText: 'new content' }],
              }),
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri2 },
                edits: [], // No edits
              }),
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri3 },
                edits: [{ range: {}, newText: 'another content' }],
              }),
            ],
          }),
        });

        mockNext.mockResolvedValue(successResult);

        const result = await middleware.process(params, mockNext);

        expect(mockNext).toHaveBeenCalledWith(params);
        expect(mockWaitForDiagnosticsUpdate).toHaveBeenCalledWith([testUri1, testUri3]);
        expect(result).toBe(successResult);
      });

      it('should handle duplicate URIs in document changes', async () => {
        const params = createFakePartial<ApplyWorkspaceEditParams>({
          edit: createFakePartial<WorkspaceEdit>({
            documentChanges: [
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri },
                edits: [{ range: {}, newText: 'content 1' }],
              }),
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri },
                edits: [{ range: {}, newText: 'content 2' }],
              }),
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: otherUri },
                edits: [{ range: {}, newText: 'other content' }],
              }),
            ],
          }),
        });

        mockNext.mockResolvedValue(successResult);

        const result = await middleware.process(params, mockNext);

        expect(mockNext).toHaveBeenCalledWith(params);
        expect(mockWaitForDiagnosticsUpdate).toHaveBeenCalledWith([testUri, testUri, otherUri]);
        expect(result).toBe(successResult);
      });
    });

    describe('with mixed document change types', () => {
      const anotherUri = 'file:///another.ts';

      it('should only extract URIs from text document edits', async () => {
        const params = createFakePartial<ApplyWorkspaceEditParams>({
          edit: createFakePartial<WorkspaceEdit>({
            documentChanges: [
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: testUri },
                edits: [{ range: {}, newText: 'new content' }],
              }),
              // Non-TextDocumentEdit change (e.g., CreateFile, RenameFile, DeleteFile)
              {
                kind: 'create',
                uri: 'file:///new-file.ts',
              },
              createFakePartial<TextDocumentEdit>({
                textDocument: { uri: anotherUri },
                edits: [{ range: {}, newText: 'another content' }],
              }),
            ],
          }),
        });

        mockNext.mockResolvedValue(successResult);

        const result = await middleware.process(params, mockNext);

        expect(mockNext).toHaveBeenCalledWith(params);
        expect(mockWaitForDiagnosticsUpdate).toHaveBeenCalledWith([testUri, anotherUri]);
        expect(result).toBe(successResult);
      });
    });
  });
});
+57 −0
Original line number Diff line number Diff line
import {
  ApplyWorkspaceEditParams,
  ApplyWorkspaceEditResult,
  TextDocumentEdit,
  WorkspaceEdit,
} from 'vscode-languageclient';
import { ApplyEditMiddleware } from './apply_edit_client_wrapper';
import { waitForDiagnosticsUpdate } from './wait_for_diagnostics';

/**
 * A middleware that resolves a race condition between files being edited, vscode recalculating diagnostics, and
 * the Language Server requesting those diagnostics.
 * After editing the file, if there are diagnostic issues in the file, there will be a delay (anywhere from 200ms - 2000ms)
 * before vscode recalculates them and `onDidChangeDiagnostics` fires to update our DocumentQualityService cache.
 * This middleware waits until it sees an event fire after editing before resolving, to ensure the cache is ready
 * for the next request.
 */
export class DiagnosticsDelayMiddleware implements ApplyEditMiddleware {
  async process(
    params: ApplyWorkspaceEditParams,
    next: (params: ApplyWorkspaceEditParams) => Promise<ApplyWorkspaceEditResult>,
  ): Promise<ApplyWorkspaceEditResult> {
    const workspaceEdit: WorkspaceEdit = params.edit;
    const editedUris = this.#extractFileUrisFromWorkspaceEdit(workspaceEdit);

    if (editedUris.length === 0) {
      return next(params);
    }

    const waitForDiagnosticsPromise = waitForDiagnosticsUpdate(editedUris);

    const result = await next(params);

    if (result.applied) {
      await waitForDiagnosticsPromise;
    }

    return result;
  }

  #extractFileUrisFromWorkspaceEdit(workspaceEdit: WorkspaceEdit): string[] {
    const fileUris: string[] = [];

    if (workspaceEdit.documentChanges) {
      for (const change of workspaceEdit.documentChanges) {
        if ('textDocument' in change && 'edits' in change) {
          const textDocumentEdit = change as TextDocumentEdit;
          if (textDocumentEdit.edits && textDocumentEdit.edits.length > 0) {
            fileUris.push(textDocumentEdit.textDocument.uri);
          }
        }
      }
    }

    return fileUris;
  }
}
+349 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading