UNPKG

@jupyter-lsp/jupyterlab-lsp

Version:

Language Server Protocol integration for JupyterLab

353 lines 15.4 kB
import { diagnosticCount, forceLinting, forEachDiagnostic } from '@codemirror/lint'; import { CodeExtractorsManager, isEqual } from '@jupyterlab/lsp'; import { framePromise } from '@jupyterlab/testing'; import { nullTranslator } from '@jupyterlab/translation'; import { Signal } from '@lumino/signaling'; import { DiagnosticSeverity } from '../../lsp'; import { FileEditorTestEnvironment, MockSettings, NotebookTestEnvironment, codeCell, setNotebookContent, showAllCells } from '../../testutils'; import { foreignCodeExtractors } from '../../transclusions/ipython/extractors'; import { diagnosticsPanel } from './diagnostics'; import { DiagnosticsFeature } from './feature'; import { messageWithoutCode } from './listing'; const SETTING_DEFAULTS = { ignoreCodes: [], ignoreMessagesPatterns: [], ignoreSeverities: [], defaultSeverity: 'Warning' }; class ShellMock { constructor() { this.currentChanged = new Signal(this); } } class ConfigurableDiagnosticsFeature extends DiagnosticsFeature { } function getDiagnostics(state) { const markers = []; forEachDiagnostic(state, d => markers.push(d)); return markers; } describe('Diagnostics', () => { let feature; let defaultSettings = new MockSettings({ ...SETTING_DEFAULTS }); describe('FileEditor integration', () => { let env; beforeEach(async () => { env = new FileEditorTestEnvironment(); feature = new ConfigurableDiagnosticsFeature({ trans: nullTranslator.load(''), settings: defaultSettings, connectionManager: env.connectionManager, shell: new ShellMock(), editorExtensionRegistry: env.editorExtensionRegistry, themeManager: null }); env.featureManager.register(feature); await env.init(); }); afterEach(() => { env.dispose(); }); const diagnostics = [ { range: { start: { line: 0, character: 7 }, end: { line: 0, character: 9 } }, message: 'Undefined symbol "aa"', code: 'E001', severity: DiagnosticSeverity['Error'] }, { range: { start: { line: 1, character: 3 }, end: { line: 1, character: 4 } }, message: 'Trimming whitespace', code: 'W001', severity: DiagnosticSeverity['Warning'] } ]; const text = 'res = aa + 1\nres '; it('renders inspections', async () => { env.activeEditor.model.sharedModel.setSource(text); await env.adapter.updateDocuments(); let markers; markers = diagnosticCount(env.activeEditor.editor.state); expect(markers).toBe(0); forceLinting(env.activeEditor.editor); await feature.handleDiagnostic({ uri: env.documentOptions.path, diagnostics: diagnostics }, env.adapter.virtualDocument, env.adapter); await framePromise(); markers = diagnosticCount(env.activeEditor.editor.state); expect(markers).toBe(2); }); it('filters out inspections by code', async () => { feature.settings = new MockSettings({ ...SETTING_DEFAULTS, ignoreCodes: ['W001'] }); env.activeEditor.model.sharedModel.setSource(text); await env.adapter.updateDocuments(); forceLinting(env.activeEditor.editor); await feature.handleDiagnostic({ uri: env.documentOptions.path, diagnostics: diagnostics }, env.adapter.virtualDocument, env.adapter); await framePromise(); const markers = getDiagnostics(env.activeEditor.editor.state); expect(markers.length).toBe(1); expect(markers[0].message).toBe('Undefined symbol "aa"'); }); it('filters out inspections by severity', async () => { feature.settings = new MockSettings({ ...SETTING_DEFAULTS, ignoreSeverities: ['Warning'] }); env.activeEditor.model.sharedModel.setSource(text); await env.adapter.updateDocuments(); forceLinting(env.activeEditor.editor); await feature.handleDiagnostic({ uri: env.documentOptions.path, diagnostics: diagnostics }, env.adapter.virtualDocument, env.adapter); await framePromise(); const markers = getDiagnostics(env.activeEditor.editor.state); expect(markers.length).toBe(1); expect(markers[0].message).toBe('Undefined symbol "aa"'); }); it('filters out inspections by message text', async () => { feature.settings = new MockSettings({ ...SETTING_DEFAULTS, ignoreMessagesPatterns: ['Undefined symbol "\\w+"'] }); env.activeEditor.model.sharedModel.setSource(text); await env.adapter.updateDocuments(); forceLinting(env.activeEditor.editor); await feature.handleDiagnostic({ uri: env.documentOptions.path, diagnostics: diagnostics }, env.adapter.virtualDocument, env.adapter); await framePromise(); const markers = getDiagnostics(env.activeEditor.editor.state); expect(markers.length).toBe(1); expect(markers[0].message).toBe('Trimming whitespace'); }); }); describe('Notebook integration', () => { let env; beforeEach(async () => { const manager = new CodeExtractorsManager(); for (let language of Object.keys(foreignCodeExtractors)) { for (let extractor of foreignCodeExtractors[language]) { manager.register(extractor, language); } } env = new NotebookTestEnvironment({ document: { foreignCodeExtractors: manager } }); feature = new ConfigurableDiagnosticsFeature({ trans: nullTranslator.load(''), settings: defaultSettings, connectionManager: env.connectionManager, shell: new ShellMock(), editorExtensionRegistry: env.editorExtensionRegistry, themeManager: null }); env.featureManager.register(feature); await env.init(); }); afterEach(() => { env.dispose(); }); it('renders inspections across cells', async () => { setNotebookContent(env.notebook, [ codeCell(['x =1\n', 'test']), codeCell([' ']) ]); showAllCells(env.notebook); await env.adapter.updateDocuments(); let document = env.adapter.virtualDocument; let uri = document.uri; env.adapter.editors.map(editor => forceLinting(editor.ceEditor.getEditor().editor)); await framePromise(); await feature.handleDiagnostic({ uri: uri, diagnostics: [ { source: 'pyflakes', range: { start: { line: 1, character: 0 }, end: { line: 1, character: 5 } }, message: "undefined name 'test'", severity: 1 }, { source: 'pycodestyle', range: { start: { line: 0, character: 3 }, end: { line: 0, character: 5 } }, message: 'E225 missing whitespace around operator', code: 'E225', severity: 2 }, { source: 'pycodestyle', range: { start: { line: 4, character: 0 }, end: { line: 4, character: 5 } }, message: 'W391 blank line at end of file', code: 'W391', severity: 2 }, { source: 'pycodestyle', range: { start: { line: 4, character: 0 }, end: { line: 4, character: 5 } }, message: 'W293 blank line contains whitespace', code: 'W293', severity: 2 }, { source: 'mypy', range: { start: { line: 1, character: 0 }, end: { line: 1, character: 4 } }, message: "Name 'test' is not defined", severity: 1 } ] }, env.adapter.virtualDocument, env.adapter); await framePromise(); let cmEditors = env.adapter.editors.map(editor => editor.ceEditor.getEditor().editor); const marksCell1 = getDiagnostics(cmEditors[0].state); // test from mypy, test from pyflakes, whitespace around operator from pycodestyle expect(marksCell1.length).toBe(3); const marksCell2 = getDiagnostics(cmEditors[1].state); expect(marksCell2.length).toBe(2); expect(marksCell2[1].message).toContain('W391'); expect(marksCell2[0].message).toContain('W293'); expect(feature.getDiagnosticsDB(env.adapter).size).toBe(1); expect(feature.getDiagnosticsDB(env.adapter).get(document).length).toBe(5); feature.switchDiagnosticsPanelSource(env.adapter); diagnosticsPanel.widget.content.update(); // the panel should contain all 5 diagnostics let db = diagnosticsPanel.content.model.diagnostics; expect(db.size).toBe(1); expect(db.get(document).length).toBe(5); }); it.skip('works in foreign documents', async () => { setNotebookContent(env.notebook, [ codeCell(['valid = 0\n', 'code = 1\n', '# here']), codeCell(['%%python\n', 'y = 1\n', 'x']) ]); showAllCells(env.notebook); await env.adapter.updateDocuments(); let document = env.adapter.virtualDocument; console.log(document.foreignDocuments); // expect(document.foreignDocuments.size).toBe(1); let foreignDocument = [...document.foreignDocuments.values()][document.foreignDocuments.size - 1]; let response = { uri: foreignDocument.uri, diagnostics: [ { source: 'pyflakes', range: { start: { line: 1, character: 0 }, end: { line: 1, character: 2 } }, message: "undefined name 'x'", severity: 1 } ] }; // test guards against wrongly propagated responses: await feature.handleDiagnostic(response, document, env.adapter); await env.adapter.updateDocuments(); await env.adapter.foreingDocumentOpened.promise; let cmEditors = env.adapter.editors.map(editor => editor.ceEditor.getEditor()); let marksCell1 = getDiagnostics(cmEditors[0].state); let marksCell2 = getDiagnostics(cmEditors[1].state); expect(marksCell1.length).toBe(0); expect(marksCell2.length).toBe(0); // update the foreignDocument as it may have been re-created in update foreignDocument = [...document.foreignDocuments.values()][document.foreignDocuments.size - 1]; response.uri = foreignDocument.uri; // correct propagation await feature.handleDiagnostic(response, foreignDocument, env.adapter); await framePromise(); await framePromise(); //await env.adapter.updateDocuments(); //await (env.adapter as MockNotebookAdapter).foreingDocumentOpened.promise; marksCell1 = getDiagnostics(cmEditors[0].state); marksCell2 = getDiagnostics(cmEditors[1].state); expect(marksCell1.length).toBe(0); expect(marksCell2.length).toBe(1); let mark = marksCell2[0]; const from = cmEditors[1].getPositionAt(mark.from); const to = cmEditors[1].getPositionAt(mark.to); // second line (0th and 1st virtual lines) + 1 line for '%%python\n' => line: 2 expect(isEqual({ line: from.line, ch: from.column }, { line: 2, ch: 0 })).toBe(true); expect(isEqual({ line: to.line, ch: to.column }, { line: 2, ch: 1 })).toBe(true); // the silenced diagnostic for the %%python magic should be ignored await feature.handleDiagnostic({ uri: document.uri, diagnostics: [ { source: 'pyflakes', range: { start: { line: 5, character: 0 }, end: { line: 5, character: 52 } }, message: "undefined name 'get_ipython'", severity: 1 } ] }, document, env.adapter); expect(marksCell1.length).toBe(0); }); }); }); describe('message_without_code', () => { it('Removes redundant code', () => { let message = messageWithoutCode({ source: 'pycodestyle', range: { start: { line: 4, character: 0 }, end: { line: 4, character: 5 } }, message: 'W293 blank line contains whitespace', code: 'W293', severity: 2 }); expect(message).toBe('blank line contains whitespace'); }); it('Keeps messages without code intact', () => { let message = messageWithoutCode({ source: 'pyflakes', range: { start: { line: 1, character: 0 }, end: { line: 1, character: 2 } }, // a message starting from "undefined" is particularly tricky as // a lazy implementation can have a coercion of undefined "code" // to a string "undefined" which would wrongly chop off "undefined" from message message: "undefined name 'x'", severity: 1 }); expect(message).toBe("undefined name 'x'"); }); }); //# sourceMappingURL=diagnostics.spec.js.map