@jupyter-lsp/jupyterlab-lsp
Version:
Language Server Protocol integration for JupyterLab
353 lines • 15.4 kB
JavaScript
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