@neo4j-cypher/language-support
Version:
Cypher language support
492 lines (463 loc) • 13.6 kB
text/typescript
import { autocomplete } from '../autocompletion/autocompletion';
import {
ParsedCommandNoPosition,
parserWrapper,
setConsoleCommandsEnabled,
} from '../parserWrapper';
import { applySyntaxColouring } from '../syntaxColouring/syntaxColouring';
import { testData } from './testData';
function expectParsedCommands(
query: string,
toEqual: ParsedCommandNoPosition[],
) {
const result = parserWrapper.parse(query);
expect(
result.statementsParsing.flatMap((statement) => statement.diagnostics),
).toEqual([]);
expect(
result.statementsParsing
.flatMap((statement) => [statement.command])
.map((cmd) => {
const copy = { ...cmd };
// These data structures are recursive, so .toEqual doesn't work.
// We test the positions work properly in with the error position tests
delete copy.start;
delete copy.stop;
return copy;
}),
).toEqual(toEqual);
}
function expectErrorMessage(query: string, msg: string) {
const result = parserWrapper.parse(query);
expect(
result.statementsParsing
.flatMap((statement) => statement.diagnostics)
.map((e) => e.message),
).toContain(msg);
}
describe('sanity checks', () => {
beforeAll(() => {
setConsoleCommandsEnabled(true);
});
afterAll(() => {
setConsoleCommandsEnabled(false);
});
test('parses simple commands without args ', () => {
expectParsedCommands(':clear', [{ type: 'clear' }]);
expectParsedCommands(':history', [{ type: 'history' }]);
});
test('properly highlights simple commands', () => {
expect(applySyntaxColouring(':clear')).toEqual([
{
length: 1,
position: {
line: 0,
startCharacter: 0,
startOffset: 0,
},
token: ':',
tokenType: 'consoleCommand',
},
{
length: 5,
position: {
line: 0,
startCharacter: 1,
startOffset: 1,
},
token: 'clear',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':history')).toEqual([
{
length: 1,
position: {
line: 0,
startCharacter: 0,
startOffset: 0,
},
token: ':',
tokenType: 'consoleCommand',
},
{
length: 7,
position: {
line: 0,
startCharacter: 1,
startOffset: 1,
},
token: 'history',
tokenType: 'consoleCommand',
},
]);
});
test('completes basic console cmds on :', () => {
expect(autocomplete(':', {})).toEqual([
{ kind: 23, label: 'use' },
{ kind: 23, label: 'param' },
{ kind: 23, label: 'history' },
{ kind: 23, label: 'clear' },
]);
});
test('accepts trailing ; ', () => {
expectParsedCommands(':history;', [
{ type: 'history' },
{ type: 'cypher', statement: '' },
]);
});
test('parses multiple cmds', () => {
expectParsedCommands(':history;:history;:clear', [
{ type: 'history' },
{ type: 'history' },
{ type: 'clear' },
]);
});
test('accepts upper case', () => {
expectParsedCommands(':HISTORY;', [
{ type: 'history' },
{ type: 'cypher', statement: '' },
]);
});
test('accepts mixed case', () => {
expectParsedCommands(':cLeaR;', [
{ type: 'clear' },
{ type: 'cypher', statement: '' },
]);
});
test('handles misspelled or non-existing command', () => {
expectErrorMessage(':foo', 'Expected any of param, history, clear or use');
expectErrorMessage(':clea', 'Unexpected token. Did you mean clear?');
});
});
describe(':use', () => {
beforeAll(() => {
setConsoleCommandsEnabled(true);
});
afterAll(() => {
setConsoleCommandsEnabled(false);
});
test('parses without arg', () => {
expectParsedCommands(':use', [{ type: 'use' }]);
});
test('parses with arg', () => {
expectParsedCommands(':use foo', [{ type: 'use', database: 'foo' }]);
});
test('completes database & alias names', () => {
expect(
autocomplete(':use ', { databaseNames: ['foo'], aliasNames: ['bar'] }),
).toEqual([
{ kind: 12, label: 'foo' },
{ kind: 12, label: 'bar' },
]);
});
test('gives errors on incorrect usage of :use', () => {
expectErrorMessage(':use 123', "Expected ';' or a database name");
expectErrorMessage(':use foo bar', "Expected ';' or a database name");
});
test('highlights properly', () => {
expect(applySyntaxColouring(':use')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 3,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'use',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':use foo')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 3,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'use',
tokenType: 'consoleCommand',
},
{
length: 3,
position: { line: 0, startCharacter: 5, startOffset: 5 },
token: 'foo',
tokenType: 'symbolicName',
},
]);
});
});
describe('parameters', () => {
beforeAll(() => {
setConsoleCommandsEnabled(true);
});
afterAll(() => {
setConsoleCommandsEnabled(false);
});
test('basic param usage', () => {
expectParsedCommands(':param', [{ type: 'list-parameters' }]);
expectParsedCommands(':params ', [{ type: 'list-parameters' }]);
expectParsedCommands(':params list', [{ type: 'list-parameters' }]);
expectParsedCommands(':params clear', [{ type: 'clear-parameters' }]);
});
test('allows setting parameters', () => {
expectParsedCommands(':param foo => bar', [
{
type: 'set-parameters',
parameters: [{ name: 'foo', expression: 'bar' }],
},
]);
expectParsedCommands(':param {a: 2, b: rand()}', [
{
type: 'set-parameters',
parameters: [
{ name: 'a', expression: '2' },
{ name: 'b', expression: 'rand()' },
],
},
]);
});
test('autocompletes expressions', () => {
const arrowCompletions = autocomplete(':param foo => ', {
functions: {
'duration.inSeconds': {
...testData.emptyFunction,
name: 'duration.inSeconds',
},
},
});
const mapCompletions = autocomplete(':param {a: ', {
functions: {
'duration.inSeconds': {
...testData.emptyFunction,
name: 'duration.inSeconds',
},
},
});
const expected = [
{ detail: '(namespace)', kind: 3, label: 'duration' },
{ detail: '(function)', kind: 3, label: 'duration.inSeconds' },
{ kind: 14, label: 'TRUE' },
{ kind: 14, label: 'FALSE' },
];
expected.forEach((completion) => {
expect(arrowCompletions).toContainEqual(completion);
expect(mapCompletions).toContainEqual(completion);
});
});
test('incorrect usage of :params', () => {
expectErrorMessage(':param x=21', "Expected '>'");
expectErrorMessage(':param x=>', 'Expected an expression');
expectErrorMessage(
':param {a: 3',
"Expected any of '}', ',', AND, OR, XOR or an expression",
);
expectErrorMessage(':param RETURN', "Expected '='");
expectErrorMessage(':param RETURN b', "Expected '='");
expectErrorMessage(':param b => ', 'Expected an expression');
expectErrorMessage(':param {', "Expected '}' or an identifier");
expectErrorMessage(':param {x}', "Expected ':'");
expectErrorMessage(':param {x: ', 'Expected an expression');
expectErrorMessage(':param {: 4} ', "Expected '}' or an identifier");
});
test('highlights :params properly', () => {
expect(applySyntaxColouring(':param')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 5,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'param',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':params')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 6,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'params',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':params list')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 6,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'params',
tokenType: 'consoleCommand',
},
{
length: 4,
position: { line: 0, startCharacter: 8, startOffset: 8 },
token: 'list',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':param clear')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 5,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'param',
tokenType: 'consoleCommand',
},
{
length: 5,
position: { line: 0, startCharacter: 7, startOffset: 7 },
token: 'clear',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':param x => 324')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 5,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'param',
tokenType: 'consoleCommand',
},
{
length: 1,
position: { line: 0, startCharacter: 7, startOffset: 7 },
token: 'x',
tokenType: 'variable',
},
{
length: 1,
position: { line: 0, startCharacter: 9, startOffset: 9 },
token: '=',
tokenType: 'operator',
},
{
length: 1,
position: { line: 0, startCharacter: 10, startOffset: 10 },
token: '>',
tokenType: 'operator',
},
{
length: 3,
position: { line: 0, startCharacter: 12, startOffset: 12 },
token: '324',
tokenType: 'numberLiteral',
},
]);
expect(applySyntaxColouring(':params {d: true}')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 6,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'params',
tokenType: 'consoleCommand',
},
{
bracketInfo: { bracketLevel: 0, bracketType: 'curly' },
length: 1,
position: { line: 0, startCharacter: 8, startOffset: 8 },
token: '{',
tokenType: 'bracket',
},
{
length: 1,
position: { line: 0, startCharacter: 9, startOffset: 9 },
token: 'd',
tokenType: 'property',
},
{
length: 1,
position: { line: 0, startCharacter: 10, startOffset: 10 },
token: ':',
tokenType: 'operator',
},
{
length: 4,
position: { line: 0, startCharacter: 12, startOffset: 12 },
token: 'true',
tokenType: 'booleanLiteral',
},
{
bracketInfo: { bracketLevel: 0, bracketType: 'curly' },
length: 1,
position: { line: 0, startCharacter: 16, startOffset: 16 },
token: '}',
tokenType: 'bracket',
},
]);
});
});
describe('command parser also handles cypher', () => {
beforeAll(() => {
setConsoleCommandsEnabled(true);
});
afterAll(() => {
setConsoleCommandsEnabled(false);
});
test('parses cypher', () => {
expectParsedCommands('MATCH (n) RETURN n', [
{ statement: 'MATCH (n) RETURN n', type: 'cypher' },
]);
});
test('preserves original whitespace', () => {
expectParsedCommands('MATCH\n(n)\nRETURN n', [
{ statement: 'MATCH\n(n)\nRETURN n', type: 'cypher' },
]);
});
test('can split cypher into statements', () => {
expectParsedCommands('CALL db.info(); RETURN 123; SHOW DATABASES', [
{ statement: 'CALL db.info()', type: 'cypher' },
{ statement: 'RETURN 123', type: 'cypher' },
{ statement: 'SHOW DATABASES', type: 'cypher' },
]);
});
test('can weave cypher with cmds', () => {
expectParsedCommands(
':use neo4j; :param x => 23;RETURN $x;:use system; SHOW DATABASES; ',
[
{ database: 'neo4j', type: 'use' },
{
parameters: [{ name: 'x', expression: '23' }],
type: 'set-parameters',
},
{ statement: 'RETURN $x', type: 'cypher' },
{ database: 'system', type: 'use' },
{ statement: 'SHOW DATABASES', type: 'cypher' },
{ statement: '', type: 'cypher' },
],
);
});
});