New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@neo4j-cypher/language-support

Package Overview
Dependencies
Maintainers
2
Versions
137
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@neo4j-cypher/language-support - npm Package Compare versions

Comparing version 2.0.0-canary-5860760 to 2.0.0-canary-5acfe46

dist/types/autocompletion/autocompletionHelpers.d.ts

34

CHANGELOG.md
# @neo4j-cypher/language-support
## 2.0.0-next.13
### Patch Changes
- 84a12fc: Added parsing of CYPHER <version> and CYPHER <optionName> = <value>
- d329252: Adds deprecation warning tags on deprecated functions/procedures
- b0e419e: Adds backticking when needed on autocompletions of aliases and database names
## 2.0.0-next.12
### Patch Changes
- 88fbe63: Modified backtick insertion to only happen when really necessary
- 22081b0: Adds autocompletions following YIELD in a procedure call
- 62ac442: Add support for sysinfo, welcome, connect, disconnect and server console commands
## 2.0.0-next.11
### Patch Changes
- bccf518: Fixes signature help bug with arguments that include a default value
## 2.0.0-next.10
### Patch Changes
- 8760c02: Adds backticking to labels, rel types and property names on auto-completions
## 2.0.0-next.9
### Patch Changes
- 2e72ac8: Adds errors for undeclared procedures / functions
## 2.0.0-next.8

@@ -4,0 +38,0 @@

10

dist/types/dbSchema.d.ts

@@ -1,2 +0,4 @@

import { Neo4jFunction, Neo4jProcedure } from './types';
import { CypherVersion, Neo4jFunction, Neo4jProcedure } from './types';
export type Registry<T> = Record<string, T>;
type ScopedRegistry<T> = Partial<Record<CypherVersion, Registry<T>>>;
export interface DbSchema {

@@ -11,4 +13,6 @@ labels?: string[];

propertyKeys?: string[];
procedures?: Record<string, Neo4jProcedure>;
functions?: Record<string, Neo4jFunction>;
procedures?: ScopedRegistry<Neo4jProcedure>;
functions?: ScopedRegistry<Neo4jFunction>;
defaultLanguage?: CypherVersion;
}
export {};
interface FeatureFlags {
consoleCommands: boolean;
cypher25: boolean;
}
export declare const _internalFeatureFlags: FeatureFlags;
export {};

@@ -6,196 +6,196 @@ import { ATN, CharStream, DFA, Lexer } from "antlr4";

static readonly HISTORY = 3;
static readonly EXPLAIN = 4;
static readonly PROFILE = 5;
static readonly SPACE = 6;
static readonly SINGLE_LINE_COMMENT = 7;
static readonly MULTI_LINE_COMMENT = 8;
static readonly DECIMAL_DOUBLE = 9;
static readonly UNSIGNED_DECIMAL_INTEGER = 10;
static readonly UNSIGNED_HEX_INTEGER = 11;
static readonly UNSIGNED_OCTAL_INTEGER = 12;
static readonly STRING_LITERAL1 = 13;
static readonly STRING_LITERAL2 = 14;
static readonly ESCAPED_SYMBOLIC_NAME = 15;
static readonly ACCESS = 16;
static readonly ACTIVE = 17;
static readonly ADMIN = 18;
static readonly ADMINISTRATOR = 19;
static readonly ALIAS = 20;
static readonly ALIASES = 21;
static readonly ALL_SHORTEST_PATHS = 22;
static readonly ALL = 23;
static readonly ALTER = 24;
static readonly AND = 25;
static readonly ANY = 26;
static readonly ARRAY = 27;
static readonly AS = 28;
static readonly ASC = 29;
static readonly ASCENDING = 30;
static readonly ASSERT = 31;
static readonly ASSIGN = 32;
static readonly AT = 33;
static readonly AUTH = 34;
static readonly BAR = 35;
static readonly BINDINGS = 36;
static readonly BOOL = 37;
static readonly BOOLEAN = 38;
static readonly BOOSTED = 39;
static readonly BOTH = 40;
static readonly BREAK = 41;
static readonly BRIEF = 42;
static readonly BTREE = 43;
static readonly BUILT = 44;
static readonly BY = 45;
static readonly CALL = 46;
static readonly CASCADE = 47;
static readonly CASE = 48;
static readonly CHANGE = 49;
static readonly CIDR = 50;
static readonly COLLECT = 51;
static readonly COLON = 52;
static readonly COLONCOLON = 53;
static readonly COMMA = 54;
static readonly COMMAND = 55;
static readonly COMMANDS = 56;
static readonly COMMIT = 57;
static readonly COMPOSITE = 58;
static readonly CONCURRENT = 59;
static readonly CONSTRAINT = 60;
static readonly CONSTRAINTS = 61;
static readonly CONTAINS = 62;
static readonly COPY = 63;
static readonly CONTINUE = 64;
static readonly COUNT = 65;
static readonly CREATE = 66;
static readonly CSV = 67;
static readonly CURRENT = 68;
static readonly DATA = 69;
static readonly DATABASE = 70;
static readonly DATABASES = 71;
static readonly DATE = 72;
static readonly DATETIME = 73;
static readonly DBMS = 74;
static readonly DEALLOCATE = 75;
static readonly DEFAULT = 76;
static readonly DEFINED = 77;
static readonly DELETE = 78;
static readonly DENY = 79;
static readonly DESC = 80;
static readonly DESCENDING = 81;
static readonly DESTROY = 82;
static readonly DETACH = 83;
static readonly DIFFERENT = 84;
static readonly DOLLAR = 85;
static readonly DISTINCT = 86;
static readonly DIVIDE = 87;
static readonly DOT = 88;
static readonly DOTDOT = 89;
static readonly DOUBLEBAR = 90;
static readonly DRIVER = 91;
static readonly DROP = 92;
static readonly DRYRUN = 93;
static readonly DUMP = 94;
static readonly DURATION = 95;
static readonly EACH = 96;
static readonly EDGE = 97;
static readonly ENABLE = 98;
static readonly ELEMENT = 99;
static readonly ELEMENTS = 100;
static readonly ELSE = 101;
static readonly ENCRYPTED = 102;
static readonly END = 103;
static readonly ENDS = 104;
static readonly EQ = 105;
static readonly EXECUTABLE = 106;
static readonly EXECUTE = 107;
static readonly EXIST = 108;
static readonly EXISTENCE = 109;
static readonly EXISTS = 110;
static readonly ERROR = 111;
static readonly FAIL = 112;
static readonly FALSE = 113;
static readonly FIELDTERMINATOR = 114;
static readonly FINISH = 115;
static readonly FLOAT = 116;
static readonly FOR = 117;
static readonly FOREACH = 118;
static readonly FROM = 119;
static readonly FULLTEXT = 120;
static readonly FUNCTION = 121;
static readonly FUNCTIONS = 122;
static readonly GE = 123;
static readonly GRANT = 124;
static readonly GRAPH = 125;
static readonly GRAPHS = 126;
static readonly GROUP = 127;
static readonly GROUPS = 128;
static readonly GT = 129;
static readonly HEADERS = 130;
static readonly HOME = 131;
static readonly ID = 132;
static readonly IF = 133;
static readonly IMPERSONATE = 134;
static readonly IMMUTABLE = 135;
static readonly IN = 136;
static readonly INDEX = 137;
static readonly INDEXES = 138;
static readonly INF = 139;
static readonly INFINITY = 140;
static readonly INSERT = 141;
static readonly INT = 142;
static readonly INTEGER = 143;
static readonly IS = 144;
static readonly JOIN = 145;
static readonly KEY = 146;
static readonly LABEL = 147;
static readonly LABELS = 148;
static readonly AMPERSAND = 149;
static readonly EXCLAMATION_MARK = 150;
static readonly LBRACKET = 151;
static readonly LCURLY = 152;
static readonly LE = 153;
static readonly LEADING = 154;
static readonly LIMITROWS = 155;
static readonly LIST = 156;
static readonly LOAD = 157;
static readonly LOCAL = 158;
static readonly LOOKUP = 159;
static readonly LPAREN = 160;
static readonly LT = 161;
static readonly MANAGEMENT = 162;
static readonly MAP = 163;
static readonly MATCH = 164;
static readonly MERGE = 165;
static readonly MINUS = 166;
static readonly PERCENT = 167;
static readonly INVALID_NEQ = 168;
static readonly NEQ = 169;
static readonly NAME = 170;
static readonly NAMES = 171;
static readonly NAN = 172;
static readonly NFC = 173;
static readonly NFD = 174;
static readonly NFKC = 175;
static readonly NFKD = 176;
static readonly NEW = 177;
static readonly NODE = 178;
static readonly NODETACH = 179;
static readonly NODES = 180;
static readonly NONE = 181;
static readonly NORMALIZE = 182;
static readonly NORMALIZED = 183;
static readonly NOT = 184;
static readonly NOTHING = 185;
static readonly NOWAIT = 186;
static readonly NULL = 187;
static readonly OF = 188;
static readonly OFFSET = 189;
static readonly ON = 190;
static readonly ONLY = 191;
static readonly OPTIONAL = 192;
static readonly OPTIONS = 193;
static readonly OPTION = 194;
static readonly OR = 195;
static readonly ORDER = 196;
static readonly OUTPUT = 197;
static readonly CONNECT = 4;
static readonly DISCONNECT = 5;
static readonly WELCOME = 6;
static readonly SYSINFO = 7;
static readonly EXPLAIN = 8;
static readonly PROFILE = 9;
static readonly CYPHER = 10;
static readonly SPACE = 11;
static readonly SINGLE_LINE_COMMENT = 12;
static readonly MULTI_LINE_COMMENT = 13;
static readonly DECIMAL_DOUBLE = 14;
static readonly UNSIGNED_DECIMAL_INTEGER = 15;
static readonly UNSIGNED_HEX_INTEGER = 16;
static readonly UNSIGNED_OCTAL_INTEGER = 17;
static readonly STRING_LITERAL1 = 18;
static readonly STRING_LITERAL2 = 19;
static readonly ESCAPED_SYMBOLIC_NAME = 20;
static readonly ACCESS = 21;
static readonly ACTIVE = 22;
static readonly ADMIN = 23;
static readonly ADMINISTRATOR = 24;
static readonly ALIAS = 25;
static readonly ALIASES = 26;
static readonly ALL_SHORTEST_PATHS = 27;
static readonly ALL = 28;
static readonly ALTER = 29;
static readonly AND = 30;
static readonly ANY = 31;
static readonly ARRAY = 32;
static readonly AS = 33;
static readonly ASC = 34;
static readonly ASCENDING = 35;
static readonly ASSIGN = 36;
static readonly AT = 37;
static readonly AUTH = 38;
static readonly BAR = 39;
static readonly BINDINGS = 40;
static readonly BOOL = 41;
static readonly BOOLEAN = 42;
static readonly BOOSTED = 43;
static readonly BOTH = 44;
static readonly BREAK = 45;
static readonly BUILT = 46;
static readonly BY = 47;
static readonly CALL = 48;
static readonly CASCADE = 49;
static readonly CASE = 50;
static readonly CHANGE = 51;
static readonly CIDR = 52;
static readonly COLLECT = 53;
static readonly COLON = 54;
static readonly COLONCOLON = 55;
static readonly COMMA = 56;
static readonly COMMAND = 57;
static readonly COMMANDS = 58;
static readonly COMPOSITE = 59;
static readonly CONCURRENT = 60;
static readonly CONSTRAINT = 61;
static readonly CONSTRAINTS = 62;
static readonly CONTAINS = 63;
static readonly COPY = 64;
static readonly CONTINUE = 65;
static readonly COUNT = 66;
static readonly CREATE = 67;
static readonly CSV = 68;
static readonly CURRENT = 69;
static readonly DATA = 70;
static readonly DATABASE = 71;
static readonly DATABASES = 72;
static readonly DATE = 73;
static readonly DATETIME = 74;
static readonly DBMS = 75;
static readonly DEALLOCATE = 76;
static readonly DEFAULT = 77;
static readonly DEFINED = 78;
static readonly DELETE = 79;
static readonly DENY = 80;
static readonly DESC = 81;
static readonly DESCENDING = 82;
static readonly DESTROY = 83;
static readonly DETACH = 84;
static readonly DIFFERENT = 85;
static readonly DOLLAR = 86;
static readonly DISTINCT = 87;
static readonly DIVIDE = 88;
static readonly DOT = 89;
static readonly DOTDOT = 90;
static readonly DOUBLEBAR = 91;
static readonly DRIVER = 92;
static readonly DROP = 93;
static readonly DRYRUN = 94;
static readonly DUMP = 95;
static readonly DURATION = 96;
static readonly EACH = 97;
static readonly EDGE = 98;
static readonly ENABLE = 99;
static readonly ELEMENT = 100;
static readonly ELEMENTS = 101;
static readonly ELSE = 102;
static readonly ENCRYPTED = 103;
static readonly END = 104;
static readonly ENDS = 105;
static readonly EQ = 106;
static readonly EXECUTABLE = 107;
static readonly EXECUTE = 108;
static readonly EXIST = 109;
static readonly EXISTENCE = 110;
static readonly EXISTS = 111;
static readonly ERROR = 112;
static readonly FAIL = 113;
static readonly FALSE = 114;
static readonly FIELDTERMINATOR = 115;
static readonly FINISH = 116;
static readonly FLOAT = 117;
static readonly FOR = 118;
static readonly FOREACH = 119;
static readonly FROM = 120;
static readonly FULLTEXT = 121;
static readonly FUNCTION = 122;
static readonly FUNCTIONS = 123;
static readonly GE = 124;
static readonly GRANT = 125;
static readonly GRAPH = 126;
static readonly GRAPHS = 127;
static readonly GROUP = 128;
static readonly GROUPS = 129;
static readonly GT = 130;
static readonly HEADERS = 131;
static readonly HOME = 132;
static readonly ID = 133;
static readonly IF = 134;
static readonly IMPERSONATE = 135;
static readonly IMMUTABLE = 136;
static readonly IN = 137;
static readonly INDEX = 138;
static readonly INDEXES = 139;
static readonly INF = 140;
static readonly INFINITY = 141;
static readonly INSERT = 142;
static readonly INT = 143;
static readonly INTEGER = 144;
static readonly IS = 145;
static readonly JOIN = 146;
static readonly KEY = 147;
static readonly LABEL = 148;
static readonly LABELS = 149;
static readonly AMPERSAND = 150;
static readonly EXCLAMATION_MARK = 151;
static readonly LBRACKET = 152;
static readonly LCURLY = 153;
static readonly LE = 154;
static readonly LEADING = 155;
static readonly LIMITROWS = 156;
static readonly LIST = 157;
static readonly LOAD = 158;
static readonly LOCAL = 159;
static readonly LOOKUP = 160;
static readonly LPAREN = 161;
static readonly LT = 162;
static readonly MANAGEMENT = 163;
static readonly MAP = 164;
static readonly MATCH = 165;
static readonly MERGE = 166;
static readonly MINUS = 167;
static readonly PERCENT = 168;
static readonly INVALID_NEQ = 169;
static readonly NEQ = 170;
static readonly NAME = 171;
static readonly NAMES = 172;
static readonly NAN = 173;
static readonly NFC = 174;
static readonly NFD = 175;
static readonly NFKC = 176;
static readonly NFKD = 177;
static readonly NEW = 178;
static readonly NODE = 179;
static readonly NODETACH = 180;
static readonly NODES = 181;
static readonly NONE = 182;
static readonly NORMALIZE = 183;
static readonly NORMALIZED = 184;
static readonly NOT = 185;
static readonly NOTHING = 186;
static readonly NOWAIT = 187;
static readonly NULL = 188;
static readonly OF = 189;
static readonly OFFSET = 190;
static readonly ON = 191;
static readonly ONLY = 192;
static readonly OPTIONAL = 193;
static readonly OPTIONS = 194;
static readonly OPTION = 195;
static readonly OR = 196;
static readonly ORDER = 197;
static readonly PASSWORD = 198;

@@ -205,119 +205,118 @@ static readonly PASSWORDS = 199;

static readonly PATHS = 201;
static readonly PERIODIC = 202;
static readonly PLAINTEXT = 203;
static readonly PLUS = 204;
static readonly PLUSEQUAL = 205;
static readonly POINT = 206;
static readonly POPULATED = 207;
static readonly POW = 208;
static readonly PRIMARY = 209;
static readonly PRIMARIES = 210;
static readonly PRIVILEGE = 211;
static readonly PRIVILEGES = 212;
static readonly PROCEDURE = 213;
static readonly PROCEDURES = 214;
static readonly PROPERTIES = 215;
static readonly PROPERTY = 216;
static readonly PROVIDER = 217;
static readonly PROVIDERS = 218;
static readonly QUESTION = 219;
static readonly RANGE = 220;
static readonly RBRACKET = 221;
static readonly RCURLY = 222;
static readonly READ = 223;
static readonly REALLOCATE = 224;
static readonly REDUCE = 225;
static readonly RENAME = 226;
static readonly REGEQ = 227;
static readonly REL = 228;
static readonly RELATIONSHIP = 229;
static readonly RELATIONSHIPS = 230;
static readonly REMOVE = 231;
static readonly REPEATABLE = 232;
static readonly REPLACE = 233;
static readonly REPORT = 234;
static readonly REQUIRE = 235;
static readonly REQUIRED = 236;
static readonly RESTRICT = 237;
static readonly RETURN = 238;
static readonly REVOKE = 239;
static readonly ROLE = 240;
static readonly ROLES = 241;
static readonly ROW = 242;
static readonly ROWS = 243;
static readonly RPAREN = 244;
static readonly SCAN = 245;
static readonly SEC = 246;
static readonly SECOND = 247;
static readonly SECONDARY = 248;
static readonly SECONDARIES = 249;
static readonly SECONDS = 250;
static readonly SEEK = 251;
static readonly SEMICOLON = 252;
static readonly SERVER = 253;
static readonly SERVERS = 254;
static readonly SET = 255;
static readonly SETTING = 256;
static readonly SETTINGS = 257;
static readonly SHORTEST_PATH = 258;
static readonly SHORTEST = 259;
static readonly SHOW = 260;
static readonly SIGNED = 261;
static readonly SINGLE = 262;
static readonly SKIPROWS = 263;
static readonly START = 264;
static readonly STARTS = 265;
static readonly STATUS = 266;
static readonly STOP = 267;
static readonly STRING = 268;
static readonly SUPPORTED = 269;
static readonly SUSPENDED = 270;
static readonly TARGET = 271;
static readonly TERMINATE = 272;
static readonly TEXT = 273;
static readonly THEN = 274;
static readonly TIME = 275;
static readonly TIMES = 276;
static readonly TIMESTAMP = 277;
static readonly TIMEZONE = 278;
static readonly TO = 279;
static readonly TOPOLOGY = 280;
static readonly TRAILING = 281;
static readonly TRANSACTION = 282;
static readonly TRANSACTIONS = 283;
static readonly TRAVERSE = 284;
static readonly TRIM = 285;
static readonly TRUE = 286;
static readonly TYPE = 287;
static readonly TYPED = 288;
static readonly TYPES = 289;
static readonly UNION = 290;
static readonly UNIQUE = 291;
static readonly UNIQUENESS = 292;
static readonly UNWIND = 293;
static readonly URL = 294;
static readonly USE = 295;
static readonly USER = 296;
static readonly USERS = 297;
static readonly USING = 298;
static readonly VALUE = 299;
static readonly VARCHAR = 300;
static readonly VECTOR = 301;
static readonly VERBOSE = 302;
static readonly VERTEX = 303;
static readonly WAIT = 304;
static readonly WHEN = 305;
static readonly WHERE = 306;
static readonly WITH = 307;
static readonly WITHOUT = 308;
static readonly WRITE = 309;
static readonly XOR = 310;
static readonly YIELD = 311;
static readonly ZONE = 312;
static readonly ZONED = 313;
static readonly IDENTIFIER = 314;
static readonly ARROW_LINE = 315;
static readonly ARROW_LEFT_HEAD = 316;
static readonly ARROW_RIGHT_HEAD = 317;
static readonly ErrorChar = 318;
static readonly PLAINTEXT = 202;
static readonly PLUS = 203;
static readonly PLUSEQUAL = 204;
static readonly POINT = 205;
static readonly POPULATED = 206;
static readonly POW = 207;
static readonly PRIMARY = 208;
static readonly PRIMARIES = 209;
static readonly PRIVILEGE = 210;
static readonly PRIVILEGES = 211;
static readonly PROCEDURE = 212;
static readonly PROCEDURES = 213;
static readonly PROPERTIES = 214;
static readonly PROPERTY = 215;
static readonly PROVIDER = 216;
static readonly PROVIDERS = 217;
static readonly QUESTION = 218;
static readonly RANGE = 219;
static readonly RBRACKET = 220;
static readonly RCURLY = 221;
static readonly READ = 222;
static readonly REALLOCATE = 223;
static readonly REDUCE = 224;
static readonly RENAME = 225;
static readonly REGEQ = 226;
static readonly REL = 227;
static readonly RELATIONSHIP = 228;
static readonly RELATIONSHIPS = 229;
static readonly REMOVE = 230;
static readonly REPEATABLE = 231;
static readonly REPLACE = 232;
static readonly REPORT = 233;
static readonly REQUIRE = 234;
static readonly REQUIRED = 235;
static readonly RESTRICT = 236;
static readonly RETURN = 237;
static readonly REVOKE = 238;
static readonly ROLE = 239;
static readonly ROLES = 240;
static readonly ROW = 241;
static readonly ROWS = 242;
static readonly RPAREN = 243;
static readonly SCAN = 244;
static readonly SEC = 245;
static readonly SECOND = 246;
static readonly SECONDARY = 247;
static readonly SECONDARIES = 248;
static readonly SECONDS = 249;
static readonly SEEK = 250;
static readonly SEMICOLON = 251;
static readonly SERVER = 252;
static readonly SERVERS = 253;
static readonly SET = 254;
static readonly SETTING = 255;
static readonly SETTINGS = 256;
static readonly SHORTEST_PATH = 257;
static readonly SHORTEST = 258;
static readonly SHOW = 259;
static readonly SIGNED = 260;
static readonly SINGLE = 261;
static readonly SKIPROWS = 262;
static readonly START = 263;
static readonly STARTS = 264;
static readonly STATUS = 265;
static readonly STOP = 266;
static readonly STRING = 267;
static readonly SUPPORTED = 268;
static readonly SUSPENDED = 269;
static readonly TARGET = 270;
static readonly TERMINATE = 271;
static readonly TEXT = 272;
static readonly THEN = 273;
static readonly TIME = 274;
static readonly TIMES = 275;
static readonly TIMESTAMP = 276;
static readonly TIMEZONE = 277;
static readonly TO = 278;
static readonly TOPOLOGY = 279;
static readonly TRAILING = 280;
static readonly TRANSACTION = 281;
static readonly TRANSACTIONS = 282;
static readonly TRAVERSE = 283;
static readonly TRIM = 284;
static readonly TRUE = 285;
static readonly TYPE = 286;
static readonly TYPED = 287;
static readonly TYPES = 288;
static readonly UNION = 289;
static readonly UNIQUE = 290;
static readonly UNIQUENESS = 291;
static readonly UNWIND = 292;
static readonly URL = 293;
static readonly USE = 294;
static readonly USER = 295;
static readonly USERS = 296;
static readonly USING = 297;
static readonly VALUE = 298;
static readonly VARCHAR = 299;
static readonly VECTOR = 300;
static readonly VERTEX = 301;
static readonly WAIT = 302;
static readonly WHEN = 303;
static readonly WHERE = 304;
static readonly WITH = 305;
static readonly WITHOUT = 306;
static readonly WRITE = 307;
static readonly XOR = 308;
static readonly YIELD = 309;
static readonly ZONE = 310;
static readonly ZONED = 311;
static readonly IDENTIFIER = 312;
static readonly EXTENDED_IDENTIFIER = 313;
static readonly ARROW_LINE = 314;
static readonly ARROW_LEFT_HEAD = 315;
static readonly ARROW_RIGHT_HEAD = 316;
static readonly ErrorChar = 317;
static readonly EOF: number;

@@ -324,0 +323,0 @@ static readonly channelNames: string[];

import antlrDefaultExport, { CommonTokenStream, ParserRuleContext, ParseTree, Token } from 'antlr4';
import { DbSchema } from './dbSchema';
import CypherLexer from './generated-parser/CypherCmdLexer';
import CypherParser, { StatementsOrCommandsContext } from './generated-parser/CypherCmdParser';
import { ParsedStatement, ParsingResult } from './parserWrapper';
import { CypherVersion } from './types';
export type EnrichedParseTree = ParseTree & {

@@ -30,4 +32,5 @@ parentCtx: ParserRuleContext | undefined;

export declare function isCommentOpener(thisToken: Token, nextToken: Token | undefined): boolean;
export declare function resolveCypherVersion(parsedVersion: CypherVersion | undefined, dbSchema: DbSchema): string;
export declare const rulesDefiningVariables: number[];
export declare const rulesDefiningOrUsingVariables: number[];
export {};
export type { ParserRuleContext } from 'antlr4';
export { autocomplete } from './autocompletion/autocompletion';
export { shouldAutoCompleteYield } from './autocompletion/autocompletionHelpers';
export type { DbSchema } from './dbSchema';
export { _internalFeatureFlags } from './featureFlags';
export { formatQuery } from './formatting/formatting';
export { antlrUtils } from './helpers';

@@ -11,9 +13,10 @@ export { CypherTokenType, lexerSymbols } from './lexerSymbols';

export type { ParsedCypherToken } from './syntaxColouring/syntaxColouringHelpers';
export { lintCypherQuery, validateSemantics, validateSyntax, } from './syntaxValidation/syntaxValidation';
export type { SyntaxDiagnostic } from './syntaxValidation/syntaxValidationHelpers';
export { lintCypherQuery } from './syntaxValidation/syntaxValidation';
export type { SyntaxDiagnostic } from './syntaxValidation/syntaxValidation';
export { testData } from './tests/testData';
export { textMateGrammar } from './textMateGrammar';
export type { CompletionItem, Neo4jFunction, Neo4jProcedure } from './types';
export { cypherVersions } from './types';
export type { CompletionItem, CypherVersion, Neo4jFunction, Neo4jProcedure, } from './types';
export { CypherLexer, CypherParser };
import CypherLexer from './generated-parser/CypherCmdLexer';
import CypherParser from './generated-parser/CypherCmdParser';

@@ -23,3 +23,5 @@ export declare enum CypherTokenType {

none = "none",
consoleCommand = "consoleCommand"
consoleCommand = "consoleCommand",
settingValue = "settingValue",
setting = "setting"
}

@@ -26,0 +28,0 @@ export declare const lexerOperators: number[];

import type { ParserRuleContext, Token } from 'antlr4';
import { default as CypherParser, StatementOrCommandContext, StatementsOrCommandsContext } from './generated-parser/CypherCmdParser';
import { SyntaxDiagnostic } from './syntaxValidation/syntaxValidationHelpers';
import { default as CypherParser, FunctionNameContext, ProcedureNameContext, StatementOrCommandContext, StatementsOrCommandsContext } from './generated-parser/CypherCmdParser';
import { SyntaxDiagnostic } from './syntaxValidation/syntaxValidation';
import { CypherVersion } from './types';
export interface ParsedStatement {

@@ -15,2 +16,3 @@ command: ParsedCommand;

collectedProcedures: ParsedProcedure[];
cypherVersion?: CypherVersion;
}

@@ -35,3 +37,3 @@ export interface ParsingResult {

export type LabelOrRelType = {
labeltype: LabelType;
labelType: LabelType;
labelText: string;

@@ -61,2 +63,3 @@ couldCreateNewLabel: boolean;

export declare function createParsingResult(query: string): ParsingResult;
export declare function getMethodName(ctx: ProcedureNameContext | FunctionNameContext): string;
type CypherCmd = {

@@ -92,3 +95,14 @@ type: 'cypher';

} | {
type: 'connect';
} | {
type: 'disconnect';
} | {
type: 'server';
operation: string;
} | {
type: 'welcome';
} | {
type: 'parse-error';
} | {
type: 'sysinfo';
};

@@ -95,0 +109,0 @@ export type ParsedCommand = ParsedCommandNoPosition & RuleTokens;

@@ -1,11 +0,11 @@

import { DiagnosticSeverity } from 'vscode-languageserver-types';
import { DbSchema } from '../dbSchema';
import { CypherVersion } from '../types';
import { SyntaxDiagnostic } from './syntaxValidation';
export interface SemanticAnalysisResult {
errors: SemanticAnalysisElement[];
notifications: SemanticAnalysisElement[];
errors: SyntaxDiagnostic[];
notifications: SyntaxDiagnostic[];
}
export interface SemanticAnalysisElement {
severity: DiagnosticSeverity;
message: string;
position: {
startPosition: {
offset: number;

@@ -15,3 +15,8 @@ line: number;

};
endPosition: {
offset: number;
line: number;
column: number;
};
}
export declare function wrappedSemanticAnalysis(query: string, dbSchema: DbSchema): SemanticAnalysisResult;
export declare function wrappedSemanticAnalysis(query: string, dbSchema: DbSchema, parsedVersion?: CypherVersion): SemanticAnalysisResult;

@@ -0,9 +1,10 @@

import { Diagnostic } from 'vscode-languageserver-types';
import { DbSchema } from '../dbSchema';
import { SyntaxDiagnostic } from './syntaxValidationHelpers';
export type SyntaxDiagnostic = Diagnostic & {
offsets: {
start: number;
end: number;
};
};
export declare function sortByPositionAndMessage(a: SyntaxDiagnostic, b: SyntaxDiagnostic): number;
export declare function lintCypherQuery(query: string, dbSchema: DbSchema): SyntaxDiagnostic[];
export declare function validateSyntax(query: string, dbSchema: DbSchema): SyntaxDiagnostic[];
/**
* Assumes the provided query has no parse errors
*/
export declare function validateSemantics(query: string, dbSchema: DbSchema): SyntaxDiagnostic[];
import { CommonToken, ErrorListener as ANTLRErrorListener, Recognizer, Token } from 'antlr4';
import { Diagnostic } from 'vscode-languageserver-types';
export type SyntaxDiagnostic = Diagnostic & {
offsets: {
start: number;
end: number;
};
};
import { SyntaxDiagnostic } from './syntaxValidation';
export declare class SyntaxErrorsListener implements ANTLRErrorListener<CommonToken> {

@@ -10,0 +4,0 @@ errors: SyntaxDiagnostic[];

@@ -6,3 +6,3 @@ import { DbSchema } from '../../dbSchema';

};
export declare function getDiagnosticsForQuery({ query, dbSchema, }: SyntaxValidationTestArgs): import("../..").SyntaxDiagnostic[];
export declare function getDiagnosticsForQuery({ query, dbSchema, }: SyntaxValidationTestArgs): import("../../syntaxValidation/syntaxValidation").SyntaxDiagnostic[];
export {};

@@ -8,2 +8,4 @@ import { CompletionItem as VSCodeCompletionItem } from 'vscode-languageserver-types';

};
export declare const cypherVersions: string[];
export type CypherVersion = (typeof cypherVersions)[number];
export type Neo4jStringType = string;

@@ -26,2 +28,3 @@ export type ArgumentDescription = ReturnDescription & {

} & Record<string, unknown>;
deprecatedBy?: string;
};

@@ -38,2 +41,3 @@ export type Neo4jFunction = {

isDeprecated: boolean;
deprecatedBy?: string;
};

@@ -40,0 +44,0 @@ export type CompletionItem = VSCodeCompletionItem & {

@@ -21,3 +21,3 @@ {

],
"version": "2.0.0-canary-5860760",
"version": "2.0.0-canary-5acfe46",
"main": "./dist/cjs/index.cjs",

@@ -46,3 +46,3 @@ "module": "./dist/esm/index.mjs",

"dependencies": {
"antlr4": "*",
"antlr4": "4.13.2",
"antlr4-c3": "*",

@@ -53,3 +53,3 @@ "fastest-levenshtein": "^1.0.16",

"scripts": {
"gen-parser": "antlr4 -Dlanguage=TypeScript -visitor src/antlr-grammar/CypherCmdLexer.g4 src/antlr-grammar/CypherCmdParser.g4 -o src/generated-parser/ -Xexact-output-dir",
"gen-parser": "cross-env ANTLR4_TOOLS_ANTLR_VERSION=4.13.2 antlr4 -Dlanguage=TypeScript -visitor src/antlr-grammar/CypherCmdLexer.g4 src/antlr-grammar/CypherCmdParser.g4 -o src/generated-parser/ -Xexact-output-dir",
"build": "npm run gen-parser && concurrently 'npm:build-types' 'npm:build-esm' 'npm:build-commonjs'",

@@ -56,0 +56,0 @@ "build-types": "tsc --emitDeclarationOnly --outDir dist/types",

@@ -12,6 +12,12 @@ import { Token } from 'antlr4';

import CypherParser, {
CallClauseContext,
Expression2Context,
} from '../generated-parser/CypherCmdParser';
import { findPreviousNonSpace, rulesDefiningVariables } from '../helpers';
import {
findParent,
findPreviousNonSpace,
resolveCypherVersion,
rulesDefiningVariables,
} from '../helpers';
import {
CypherTokenType,

@@ -23,21 +29,88 @@ lexerKeywords,

import { ParsedStatement } from '../parserWrapper';
import { getMethodName, ParsedStatement } from '../parserWrapper';
import { _internalFeatureFlags } from '../featureFlags';
import { CompletionItem, Neo4jFunction, Neo4jProcedure } from '../types';
import {
CompletionItem,
CypherVersion,
Neo4jFunction,
Neo4jProcedure,
} from '../types';
const uniq = <T>(arr: T[]) => Array.from(new Set(arr));
const versions = ['5'];
function backtickIfNeeded(e: string): string | undefined {
if (e == null || e == '') {
return undefined;
} else if (/[^\p{L}\p{N}_]/u.test(e) || /[^\p{L}_]/u.test(e[0])) {
return `\`${e}\``;
} else {
return undefined;
}
}
function backtickDbNameIfNeeded(e: string): string | undefined {
if (e == null || e == '') {
return undefined;
} else if (/[^\p{L}\p{N}_.]/u.test(e) || /[^\p{L}_]/u.test(e[0])) {
return `\`${e}\``;
} else {
return undefined;
}
}
const versionCompletions = () =>
versions.map((v) => {
const result: CompletionItem = {
label: v,
kind: CompletionItemKind.EnumMember,
};
return result;
});
const cypherVersionCompletions = () =>
versions.map((v) => {
const result: CompletionItem = {
label: 'CYPHER ' + v,
kind: CompletionItemKind.Keyword,
};
return result;
});
const labelCompletions = (dbSchema: DbSchema) =>
dbSchema.labels?.map((labelName) => ({
label: labelName,
kind: CompletionItemKind.TypeParameter,
})) ?? [];
dbSchema.labels?.map((labelName) => {
const result: CompletionItem = {
label: labelName,
kind: CompletionItemKind.TypeParameter,
insertText: backtickIfNeeded(labelName),
};
return result;
}) ?? [];
const reltypeCompletions = (dbSchema: DbSchema) =>
dbSchema.relationshipTypes?.map((relType) => ({
label: relType,
kind: CompletionItemKind.TypeParameter,
})) ?? [];
dbSchema.relationshipTypes?.map((relType) => {
const result: CompletionItem = {
label: relType,
kind: CompletionItemKind.TypeParameter,
insertText: backtickIfNeeded(relType),
};
return result;
}) ?? [];
const procedureReturnCompletions = (
procedureName: string,
dbSchema: DbSchema,
cypherVersion: CypherVersion,
): CompletionItem[] => {
return (
dbSchema.procedures?.[cypherVersion]?.[
procedureName
]?.returnDescription?.map((x) => {
return { label: x.name, kind: CompletionItemKind.Variable };
}) ?? []
);
};
const functionNameCompletions = (

@@ -47,2 +120,3 @@ candidateRule: CandidateRule,

dbSchema: DbSchema,
cypherVersion: CypherVersion,
): CompletionItem[] =>

@@ -52,3 +126,3 @@ namespacedCompletion(

tokens,
dbSchema?.functions ?? {},
dbSchema.functions?.[cypherVersion] ?? {},
'function',

@@ -61,2 +135,3 @@ );

dbSchema: DbSchema,
cypherVersion: CypherVersion,
): CompletionItem[] =>

@@ -66,3 +141,3 @@ namespacedCompletion(

tokens,
dbSchema?.procedures ?? {},
dbSchema.procedures?.[cypherVersion] ?? {},
'procedure',

@@ -261,11 +336,21 @@ );

)
.map(([paramName]) => ({
label: `$${paramName}`,
kind: CompletionItemKind.Variable,
}));
.map(([paramName]) => {
const backtickedName = backtickIfNeeded(paramName);
return {
label: `$${paramName}`,
kind: CompletionItemKind.Variable,
...(backtickedName
? { insertText: `$${backtickIfNeeded(paramName)}` }
: {}),
};
});
const propertyKeyCompletions = (dbInfo: DbSchema): CompletionItem[] =>
dbInfo.propertyKeys?.map((propertyKey) => ({
label: propertyKey,
kind: CompletionItemKind.Property,
})) ?? [];
dbInfo.propertyKeys?.map((propertyKey) => {
const result: CompletionItem = {
label: propertyKey,
kind: CompletionItemKind.Property,
insertText: backtickIfNeeded(propertyKey),
};
return result;
}) ?? [];

@@ -366,2 +451,6 @@ enum ExpectedParameterType {

): CompletionItem[] {
const cypherVersion = resolveCypherVersion(
parsingResult.cypherVersion,
dbSchema,
);
const parser = parsingResult.parser;

@@ -410,2 +499,5 @@ const tokens = parsingResult.tokens;

CypherParser.RULE_commandNameExpression,
CypherParser.RULE_procedureResultItem,
CypherParser.RULE_cypherVersion,
CypherParser.RULE_cypher,

@@ -418,2 +510,3 @@ // Either enable the helper rules for lexer clashes,

CypherParser.RULE_listCompletionRule,
CypherParser.RULE_serverCompletionRule,
]

@@ -446,8 +539,51 @@ : [CypherParser.RULE_consoleCommand]),

const [ruleNumber, candidateRule] = candidate;
if (ruleNumber === CypherParser.RULE_cypher) {
return [
...cypherVersionCompletions(),
{
label: 'CYPHER',
kind: CompletionItemKind.Keyword,
},
];
}
if (ruleNumber === CypherParser.RULE_cypherVersion) {
return versionCompletions();
}
if (ruleNumber === CypherParser.RULE_procedureResultItem) {
const callContext = findParent(
parsingResult.stopNode.parentCtx,
(x) => x instanceof CallClauseContext,
);
if (callContext instanceof CallClauseContext) {
const procedureNameCtx = callContext.procedureName();
const existingYieldItems = new Set(
callContext.procedureResultItem_list().map((a) => a.getText()),
);
const name = getMethodName(procedureNameCtx);
return procedureReturnCompletions(
name,
dbSchema,
cypherVersion,
).filter((a) => !existingYieldItems.has(a?.label));
}
}
if (ruleNumber === CypherParser.RULE_functionName) {
return functionNameCompletions(candidateRule, tokens, dbSchema);
return functionNameCompletions(
candidateRule,
tokens,
dbSchema,
cypherVersion,
);
}
if (ruleNumber === CypherParser.RULE_procedureName) {
return procedureNameCompletions(candidateRule, tokens, dbSchema);
return procedureNameCompletions(
candidateRule,
tokens,
dbSchema,
cypherVersion,
);
}

@@ -557,2 +693,6 @@

if (ruleNumber === CypherParser.RULE_serverCompletionRule) {
return [{ label: 'server', kind: CompletionItemKind.Event }];
}
if (ruleNumber === CypherParser.RULE_leftArrow) {

@@ -671,5 +811,6 @@ return [

...parameterSuggestions,
...(dbSchema?.aliasNames ?? []).map((aliasName) => ({
...(dbSchema.aliasNames ?? []).map((aliasName) => ({
label: aliasName,
kind: CompletionItemKind.Value,
insertText: backtickDbNameIfNeeded(aliasName),
})),

@@ -687,2 +828,3 @@ ];

kind: CompletionItemKind.Value,
insertText: backtickDbNameIfNeeded(databaseName),
})),

@@ -736,3 +878,3 @@ ];

...parameterSuggestions,
...(dbSchema?.userNames ?? []).map((userName) => ({
...(dbSchema.userNames ?? []).map((userName) => ({
label: userName,

@@ -755,3 +897,3 @@ kind: CompletionItemKind.Value,

...parameterSuggestions,
...(dbSchema?.roleNames ?? []).map((roleName) => ({
...(dbSchema.roleNames ?? []).map((roleName) => ({
label: roleName,

@@ -758,0 +900,0 @@ kind: CompletionItemKind.Value,

@@ -1,3 +0,6 @@

import { Neo4jFunction, Neo4jProcedure } from './types';
import { CypherVersion, Neo4jFunction, Neo4jProcedure } from './types';
export type Registry<T> = Record<string, T>;
type ScopedRegistry<T> = Partial<Record<CypherVersion, Registry<T>>>;
export interface DbSchema {

@@ -12,4 +15,5 @@ labels?: string[];

propertyKeys?: string[];
procedures?: Record<string, Neo4jProcedure>;
functions?: Record<string, Neo4jFunction>;
procedures?: ScopedRegistry<Neo4jProcedure>;
functions?: ScopedRegistry<Neo4jFunction>;
defaultLanguage?: CypherVersion;
}
interface FeatureFlags {
consoleCommands: boolean;
cypher25: boolean;
}

@@ -14,2 +15,3 @@

consoleCommands: false,
cypher25: false,
};

@@ -9,2 +9,4 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment

} from 'antlr4';
import { DbSchema } from './dbSchema';
import { _internalFeatureFlags } from './featureFlags';
import CypherLexer from './generated-parser/CypherCmdLexer';

@@ -17,2 +19,3 @@ import CypherParser, {

import { ParsedStatement, ParsingResult } from './parserWrapper';
import { CypherVersion } from './types';

@@ -100,6 +103,8 @@ /* In antlr we have

stopNode,
(p) => p instanceof NodePatternContext,
(p) =>
p instanceof NodePatternContext ||
p instanceof RelationshipPatternContext,
);
return isDefined(nodePattern);
return nodePattern instanceof NodePatternContext;
}

@@ -110,6 +115,8 @@

stopNode,
(p) => p instanceof RelationshipPatternContext,
(p) =>
p instanceof NodePatternContext ||
p instanceof RelationshipPatternContext,
);
return isDefined(relPattern);
return relPattern instanceof RelationshipPatternContext;
}

@@ -206,2 +213,16 @@

export function resolveCypherVersion(
parsedVersion: CypherVersion | undefined,
dbSchema: DbSchema,
) {
if (_internalFeatureFlags.cypher25) {
const cypherVersion: CypherVersion =
parsedVersion ?? dbSchema.defaultLanguage ?? 'CYPHER 5';
return cypherVersion;
} else {
return 'CYPHER 5';
}
}
export const rulesDefiningVariables = [

@@ -208,0 +229,0 @@ CypherParser.RULE_returnItem,

export type { ParserRuleContext } from 'antlr4';
export { autocomplete } from './autocompletion/autocompletion';
export { shouldAutoCompleteYield } from './autocompletion/autocompletionHelpers';
export type { DbSchema } from './dbSchema';
export { _internalFeatureFlags } from './featureFlags';
export { formatQuery } from './formatting/formatting';
export { antlrUtils } from './helpers';

@@ -15,13 +17,15 @@ export { CypherTokenType, lexerSymbols } from './lexerSymbols';

export type { ParsedCypherToken } from './syntaxColouring/syntaxColouringHelpers';
export {
lintCypherQuery,
validateSemantics,
validateSyntax,
} from './syntaxValidation/syntaxValidation';
export type { SyntaxDiagnostic } from './syntaxValidation/syntaxValidationHelpers';
export { lintCypherQuery } from './syntaxValidation/syntaxValidation';
export type { SyntaxDiagnostic } from './syntaxValidation/syntaxValidation';
export { testData } from './tests/testData';
export { textMateGrammar } from './textMateGrammar';
export type { CompletionItem, Neo4jFunction, Neo4jProcedure } from './types';
export { cypherVersions } from './types';
export type {
CompletionItem,
CypherVersion,
Neo4jFunction,
Neo4jProcedure,
} from './types';
export { CypherLexer, CypherParser };
import CypherLexer from './generated-parser/CypherCmdLexer';
import CypherParser from './generated-parser/CypherCmdParser';

@@ -26,2 +26,4 @@ import CypherLexer from './generated-parser/CypherCmdLexer';

consoleCommand = 'consoleCommand',
settingValue = 'settingValue',
setting = 'setting',
}

@@ -93,2 +95,3 @@

CypherLexer.ESCAPED_SYMBOLIC_NAME,
CypherLexer.EXTENDED_IDENTIFIER,
];

@@ -117,3 +120,2 @@

CypherLexer.ASCENDING,
CypherLexer.ASSERT,
CypherLexer.ASSIGN,

@@ -128,4 +130,2 @@ CypherLexer.AT,

CypherLexer.BREAK,
CypherLexer.BRIEF,
CypherLexer.BTREE,
CypherLexer.BUILT,

@@ -141,3 +141,2 @@ CypherLexer.BY,

CypherLexer.COMMANDS,
CypherLexer.COMMIT,
CypherLexer.COMPOSITE,

@@ -191,2 +190,3 @@ CypherLexer.CONSTRAINT,

CypherLexer.EXISTS,
CypherLexer.EXTENDED_IDENTIFIER,
CypherLexer.FAIL,

@@ -264,3 +264,2 @@ CypherLexer.FALSE,

CypherLexer.ORDER,
CypherLexer.OUTPUT,
CypherLexer.PASSWORD,

@@ -270,3 +269,2 @@ CypherLexer.PASSWORDS,

CypherLexer.PATHS,
CypherLexer.PERIODIC,
CypherLexer.PLAINTEXT,

@@ -361,3 +359,2 @@ CypherLexer.POINT,

CypherLexer.VECTOR,
CypherLexer.VERBOSE,
CypherLexer.VERTEX,

@@ -377,2 +374,3 @@ CypherLexer.WAIT,

CypherLexer.PROFILE,
CypherLexer.CYPHER,
];

@@ -384,2 +382,6 @@

CypherLexer.CLEAR,
CypherLexer.CONNECT,
CypherLexer.DISCONNECT,
CypherLexer.WELCOME,
CypherLexer.SYSINFO,
];

@@ -386,0 +388,0 @@

@@ -10,8 +10,9 @@ import type { ParserRuleContext, Token } from 'antlr4';

ClauseContext,
CypherVersionContext,
default as CypherParser,
FunctionNameContext,
LabelNameContext,
LabelNameIsContext,
LabelOrRelTypeContext,
ProcedureNameContext,
ProcedureResultItemContext,
StatementOrCommandContext,

@@ -32,6 +33,5 @@ StatementsOrCommandsContext,

} from './helpers';
import {
SyntaxDiagnostic,
SyntaxErrorsListener,
} from './syntaxValidation/syntaxValidationHelpers';
import { SyntaxDiagnostic } from './syntaxValidation/syntaxValidation';
import { SyntaxErrorsListener } from './syntaxValidation/syntaxValidationHelpers';
import { CypherVersion } from './types';

@@ -51,2 +51,3 @@ export interface ParsedStatement {

collectedProcedures: ParsedProcedure[];
cypherVersion?: CypherVersion;
}

@@ -93,3 +94,3 @@

export type LabelOrRelType = {
labeltype: LabelType;
labelType: LabelType;
labelText: string;

@@ -182,4 +183,10 @@ couldCreateNewLabel: boolean;

const methodsFinder = new MethodsCollector(tokens);
const cypherVersionCollector = new CypherVersionCollector();
const errorListener = new SyntaxErrorsListener(tokens);
parser._parseListeners = [labelsCollector, variableFinder, methodsFinder];
parser._parseListeners = [
labelsCollector,
variableFinder,
methodsFinder,
cypherVersionCollector,
];
parser.addErrorListener(errorListener);

@@ -192,4 +199,7 @@ const ctx = parser.statementsOrCommands();

) === undefined;
const syntaxErrors = isEmptyStatement ? [] : errorListener.errors;
const collectedCommand = parseToCommand(ctx, isEmptyStatement);
const collectedCommand = parseToCommand(ctx, tokens, isEmptyStatement);
const syntaxErrors =
collectedCommand.type !== 'cypher' && !isEmptyStatement
? errorListener.errors
: [];

@@ -211,2 +221,3 @@ if (!_internalFeatureFlags.consoleCommands) {

collectedProcedures: methodsFinder.procedures,
cypherVersion: cypherVersionCollector.cypherVersion,
};

@@ -238,3 +249,3 @@ });

exitEveryRule(ctx: unknown) {
if (ctx instanceof LabelNameContext || ctx instanceof LabelNameIsContext) {
if (ctx instanceof LabelNameContext) {
// If the parent ctx start doesn't coincide with this ctx start,

@@ -246,3 +257,3 @@ // it means the parser recovered from an error reading the label

this.labelOrRelTypes.push({
labeltype: getLabelType(ctx),
labelType: getLabelType(ctx),
labelText: ctx.getText(),

@@ -267,3 +278,3 @@ couldCreateNewLabel: couldCreateNewLabel(ctx),

this.labelOrRelTypes.push({
labeltype: getLabelType(ctx),
labelType: getLabelType(ctx),
labelText: symbolicName.start.text,

@@ -300,3 +311,3 @@ couldCreateNewLabel: couldCreateNewLabel(ctx),

if (ctx instanceof VariableContext) {
const variable = ctx.symbolicNameString().getText();
const variable = ctx.symbolicVariableNameString().getText();
// To avoid suggesting the variable that is currently being typed

@@ -321,5 +332,38 @@ // For example RETURN a| <- we don't want to suggest "a" as a variable

}
if (ctx instanceof ProcedureResultItemContext) {
const variable = ctx.getText();
if (variable) {
this.variables.push(variable);
}
}
}
}
export function getMethodName(
ctx: ProcedureNameContext | FunctionNameContext,
): string {
const namespaces = ctx.namespace().symbolicNameString_list();
const methodName = ctx.symbolicNameString();
const normalizedName = [...namespaces, methodName]
.map((symbolicName) => {
return getNamespaceString(symbolicName);
})
.join('.');
return normalizedName;
}
function getNamespaceString(ctx: SymbolicNameStringContext): string {
const text = ctx.getText();
const isEscaped = Boolean(ctx.escapedSymbolicNameString());
const hasDot = text.includes('.');
if (isEscaped && !hasDot) {
return text.slice(1, -1);
}
return text;
}
// This listener collects all functions and procedures

@@ -351,3 +395,3 @@ class MethodsCollector extends ParseTreeListener {

) {
const methodName = this.getMethodName(ctx);
const methodName = getMethodName(ctx);

@@ -382,28 +426,30 @@ const startTokenIndex = ctx.start.tokenIndex;

}
}
private getMethodName(
ctx: ProcedureNameContext | FunctionNameContext,
): string {
const namespaces = ctx.namespace().symbolicNameString_list();
const methodName = ctx.symbolicNameString();
class CypherVersionCollector extends ParseTreeListener {
public cypherVersion: CypherVersion;
const normalizedName = [...namespaces, methodName]
.map((symbolicName) => {
return this.getNamespaceString(symbolicName);
})
.join('.');
constructor() {
super();
}
return normalizedName;
enterEveryRule() {
/* no-op */
}
visitTerminal() {
/* no-op */
}
visitErrorNode() {
/* no-op */
}
private getNamespaceString(ctx: SymbolicNameStringContext): string {
const text = ctx.getText();
const isEscaped = Boolean(ctx.escapedSymbolicNameString());
const hasDot = text.includes('.');
if (isEscaped && !hasDot) {
return text.slice(1, -1);
exitEveryRule(ctx: unknown) {
if (ctx instanceof CypherVersionContext) {
this.cypherVersion =
ctx.getText() === '5'
? 'CYPHER 5'
: ctx.getText() === '25'
? 'CYPHER 25'
: undefined;
}
return text;
}

@@ -430,3 +476,8 @@ }

| { type: 'clear-parameters' }
| { type: 'parse-error' };
| { type: 'connect' }
| { type: 'disconnect' }
| { type: 'server'; operation: string }
| { type: 'welcome' }
| { type: 'parse-error' }
| { type: 'sysinfo' };

@@ -437,2 +488,3 @@ export type ParsedCommand = ParsedCommandNoPosition & RuleTokens;

stmts: StatementsOrCommandsContext,
tokens: Token[],
isEmptyStatement: boolean,

@@ -443,11 +495,17 @@ ): ParsedCommand {

if (stmt) {
const { start, stop } = stmt;
const start = stmts.start;
let stop = stmts.stop;
const cypherStmt = stmt.preparsedStatement();
if (stop && stop.type === CypherLexer.SEMICOLON) {
stop = tokens[stop.tokenIndex - 1];
}
const inputstream = start.getInputStream();
const cypherStmt = stmt.preparsedStatement()?.statement();
if (cypherStmt) {
// we get the original text input to preserve whitespace
const inputstream = start.getInputStream();
const statement = inputstream.getText(start.start, stop.stop);
// stripping the preparser part of it
const stmtStart = cypherStmt.start;
const statement = inputstream.getText(stmtStart.start, stop.stop);
return { type: 'cypher', statement, start, stop };
return { type: 'cypher', statement, start: stmtStart, stop: stop };
}

@@ -514,3 +572,3 @@

const lambda = paramArgs.lambda();
const name = lambda?.unescapedSymbolicNameString()?.getText();
const name = lambda?.parameterName()?.getText();
const expression = lambda?.expression()?.getText();

@@ -537,5 +595,40 @@ if (name && expression) {

const connectCmd = consoleCmd.connectCmd();
if (connectCmd) {
return { type: 'connect', start, stop };
}
const disconnectCmd = consoleCmd.disconnectCmd();
if (disconnectCmd) {
return { type: 'disconnect', start, stop };
}
const serverCmd = consoleCmd.serverCmd();
const serverArgs = serverCmd?.serverArgs();
if (serverCmd && serverArgs) {
const connect = serverArgs.CONNECT();
if (connect) {
return { type: 'server', operation: 'connect', start, stop };
}
const disconnect = serverArgs.DISCONNECT();
if (disconnect) {
return { type: 'server', operation: 'disconnect', start, stop };
}
}
const welcomeCmd = consoleCmd.welcomeCmd();
if (welcomeCmd) {
return { type: 'welcome', start, stop };
}
const sysInfoCmd = consoleCmd.sysInfoCmd();
if (sysInfoCmd) {
return { type: 'sysinfo', start, stop };
}
return { type: 'parse-error', start, stop };
}
return { type: 'parse-error', start, stop };
const statement = inputstream.getText(start.start, stop.stop);
return { type: 'cypher', statement, start: start, stop: stop };
}

@@ -562,3 +655,3 @@ return { type: 'parse-error', start: stmts.start, stop: stmts.stop };

return [command]
.filter((cmd) => cmd.type !== 'cypher' && cmd.type !== 'parse-error')
.filter((cmd) => cmd.type !== 'cypher')
.map(

@@ -565,0 +658,0 @@ ({ start, stop }): SyntaxDiagnostic => ({

@@ -16,3 +16,3 @@ import {

import CypherCmdParserListener from './generated-parser/CypherCmdParserListener';
import { findCaret, isDefined } from './helpers';
import { findCaret, isDefined, resolveCypherVersion } from './helpers';
import { parserWrapper } from './parserWrapper';

@@ -41,10 +41,30 @@ import { Neo4jFunction, Neo4jProcedure } from './types';

const { name, argumentDescription, description } = curr;
const argDescriptions = argumentDescription.map((arg) => {
let label = '';
// If there's a default value, it has the shape
// DefaultParameterValue{value=[0.5, 0.75, 0.9, 0.95, 0.99], type=LIST<FLOAT>}
if (arg.default) {
const startIndex = arg.default.indexOf('value=') + 'value='.length;
const endIndex = arg.default.indexOf(', type=', startIndex);
const defaultArg = arg.default.substring(startIndex, endIndex);
label = `${arg.name} = ${defaultArg} :: ${arg.type}`;
} else {
label = `${arg.name} :: ${arg.type}`;
}
return {
label: label,
documentation: arg.description,
};
});
const argsString = argDescriptions.map((arg) => arg.label).join(', ');
const signature = `${name}(${argsString})`;
return SignatureInformation.create(
name,
signature,
description,
...argumentDescription.map((arg) => ({
label: arg.name,
documentation: arg.description,
})),
...argDescriptions,
);

@@ -185,6 +205,16 @@ }

if (method !== undefined) {
const cypherVersion = resolveCypherVersion(
statement.cypherVersion,
dbSchema,
);
if (method.methodType === MethodType.function) {
result = toSignatureHelp(dbSchema.functions, method);
result = toSignatureHelp(
dbSchema.functions?.[cypherVersion] ?? {},
method,
);
} else {
result = toSignatureHelp(dbSchema.procedures, method);
result = toSignatureHelp(
dbSchema.procedures?.[cypherVersion] ?? {},
method,
);
}

@@ -191,0 +221,0 @@ }

@@ -7,6 +7,7 @@ import { ParseTreeWalker, TerminalNode, Token } from 'antlr4';

ConsoleCommandContext,
CypherOptionNameContext,
CypherOptionValueContext,
FunctionNameContext,
KeywordLiteralContext,
LabelNameContext,
LabelNameIsContext,
LabelOrRelTypeContext,

@@ -18,2 +19,3 @@ LabelTypeContext,

ParameterContext,
ParameterNameContext,
ParamsArgsContext,

@@ -25,2 +27,3 @@ ProcedureNameContext,

RightArrowContext,
ServerCompletionRuleContext,
StringLiteralContext,

@@ -37,3 +40,3 @@ StringsLiteralContext,

} from 'vscode-languageserver-types';
import { CypherLexer } from '..';
import CypherLexer from '../generated-parser/CypherCmdLexer';
import CypherParserListener from '../generated-parser/CypherCmdParserListener';

@@ -80,2 +83,4 @@ import { CypherTokenType } from '../lexerSymbols';

[CypherTokenType.property]: SemanticTokenTypes.property,
[CypherTokenType.setting]: SemanticTokenTypes.enum,
[CypherTokenType.settingValue]: SemanticTokenTypes.enumMember,
[CypherTokenType.label]: SemanticTokenTypes.type,

@@ -117,7 +122,11 @@ [CypherTokenType.variable]: SemanticTokenTypes.variable,

exitLabelName = (ctx: LabelNameContext) => {
this.addToken(ctx.start, CypherTokenType.label, ctx.getText());
exitCypherOptionValue = (ctx: CypherOptionValueContext) => {
this.addToken(ctx.start, CypherTokenType.settingValue, ctx.getText());
};
exitLabelNameIs = (ctx: LabelNameIsContext) => {
exitCypherOptionName = (ctx: CypherOptionNameContext) => {
this.addToken(ctx.start, CypherTokenType.setting, ctx.getText());
};
exitLabelName = (ctx: LabelNameContext) => {
this.addToken(ctx.start, CypherTokenType.label, ctx.getText());

@@ -184,2 +193,6 @@ };

exitParameterName = (ctx: ParameterNameContext) => {
this.addToken(ctx.start, CypherTokenType.variable, ctx.getText());
};
exitProcedureResultItem = (ctx: ProcedureResultItemContext) => {

@@ -259,2 +272,12 @@ this.addToken(ctx.start, CypherTokenType.variable, ctx.getText());

exitServerCompletionRule = (ctx: ServerCompletionRuleContext) => {
const server = ctx.SERVER();
this.addToken(
server.symbol,
CypherTokenType.consoleCommand,
server.getText(),
);
};
exitParamsArgs = (ctx: ParamsArgsContext) => {

@@ -338,3 +361,3 @@ const clear = ctx.CLEAR();

This allows to correclty colour things that are
This allows to correctly colour things that are
recognized as keywords by the lexer in positions

@@ -341,0 +364,0 @@ where they are not keywords (e.g. MATCH (MATCH: MATCH))

@@ -40,3 +40,2 @@ import { Token } from 'antlr4';

[CypherParser.RULE_labelExpression2]: 'a node label / rel type',
[CypherParser.RULE_labelExpression2Is]: 'a node label / rel type',
[CypherParser.RULE_procedureName]: 'a procedure name',

@@ -54,2 +53,3 @@ [CypherParser.RULE_stringLiteral]: 'a string',

[CypherParser.RULE_listCompletionRule]: 'list',
[CypherParser.RULE_serverCompletionRule]: 'server',
}

@@ -131,10 +131,2 @@ : { [CypherParser.RULE_consoleCommand]: null }),

if (options.length === 0) {
// options length is 0 should only happen when RULE_consoleCommand is hit and there are no other options
if (
ruleCandidates.find(
(ruleNumber) => ruleNumber === CypherParser.RULE_consoleCommand,
)
) {
return 'Console commands are unsupported in this environment.';
}
return undefined;

@@ -141,0 +133,0 @@ }

@@ -5,17 +5,18 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */

import { DiagnosticSeverity } from 'vscode-languageserver-types';
import { DbSchema } from '../dbSchema';
import { DiagnosticSeverity, Position } from 'vscode-languageserver-types';
import { DbSchema, Registry } from '../dbSchema';
import { CypherVersion, Neo4jFunction, Neo4jProcedure } from '../types';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { semanticAnalysis, updateSignatureResolver } from './semanticAnalysis';
import { analyzeQuery, updateSignatureResolver } from './semanticAnalysis';
import { SyntaxDiagnostic } from './syntaxValidation';
export interface SemanticAnalysisResult {
errors: SemanticAnalysisElement[];
notifications: SemanticAnalysisElement[];
errors: SyntaxDiagnostic[];
notifications: SyntaxDiagnostic[];
}
export interface SemanticAnalysisElement {
severity: DiagnosticSeverity;
message: string;
position: {
startPosition: {
offset: number;

@@ -25,46 +26,79 @@ line: number;

};
endPosition: {
offset: number;
line: number;
column: number;
};
}
type SemanticAnalysisElementNoSeverity = Omit<
SemanticAnalysisElement,
'severity'
>;
const previousResolvers: {
[cypherVersion: CypherVersion]: {
functions: Registry<Neo4jFunction>;
procedures: Registry<Neo4jProcedure>;
};
} = {};
function copySettingSeverity(
elements: SemanticAnalysisElement[],
severity: DiagnosticSeverity,
): SyntaxDiagnostic[] {
return elements.map(({ message, startPosition, endPosition }) => ({
severity: severity,
message,
range: {
start: Position.create(startPosition.line, startPosition.column),
end: Position.create(endPosition.line, endPosition.column),
},
offsets: {
start: startPosition.offset,
end: endPosition.offset,
},
}));
}
function updateResolverForVersion(
dbSchema: DbSchema,
cypherVersion: CypherVersion,
) {
const previousResolver = previousResolvers?.[cypherVersion];
const currentResolver = {
procedures: dbSchema?.procedures?.[cypherVersion],
functions: dbSchema?.functions?.[cypherVersion],
};
if (JSON.stringify(previousResolver) !== JSON.stringify(currentResolver)) {
previousResolvers[cypherVersion] = currentResolver;
const procedures = Object.values(
dbSchema?.procedures?.[cypherVersion] ?? {},
);
const functions = Object.values(dbSchema?.functions?.[cypherVersion] ?? {});
updateSignatureResolver(
{
procedures: procedures,
functions: functions,
},
cypherVersion,
);
}
}
export function wrappedSemanticAnalysis(
query: string,
dbSchema: DbSchema,
parsedVersion?: CypherVersion,
): SemanticAnalysisResult {
try {
let semanticErrorsResult = undefined;
updateSignatureResolver({
procedures: Object.values(dbSchema.procedures ?? {}),
functions: Object.values(dbSchema.functions ?? {}),
});
semanticAnalysis([query], (a) => {
semanticErrorsResult = a;
});
const errors: SemanticAnalysisElementNoSeverity[] =
semanticErrorsResult.errors;
const notifications: SemanticAnalysisElementNoSeverity[] =
const defaultVersion = dbSchema?.defaultLanguage;
const cypherVersion = parsedVersion ?? defaultVersion ?? 'CYPHER 5';
updateResolverForVersion(dbSchema, cypherVersion);
const semanticErrorsResult = analyzeQuery(query, cypherVersion);
const errors: SemanticAnalysisElement[] = semanticErrorsResult.errors;
const notifications: SemanticAnalysisElement[] =
semanticErrorsResult.notifications;
return {
errors: errors.map(({ message, position }) => ({
severity: DiagnosticSeverity.Error,
message,
position: {
column: position.column,
line: position.line,
offset: position.offset,
},
})),
notifications: notifications.map(({ message, position }) => ({
severity: DiagnosticSeverity.Warning,
message,
position: {
column: position.column,
line: position.line,
offset: position.offset,
},
})),
errors: copySettingSeverity(errors, DiagnosticSeverity.Error),
notifications: copySettingSeverity(
notifications,
DiagnosticSeverity.Warning,
),
};

@@ -71,0 +105,0 @@ } catch (e) {

@@ -1,5 +0,10 @@

import { DiagnosticSeverity, Position } from 'vscode-languageserver-types';
import {
Diagnostic,
DiagnosticSeverity,
DiagnosticTag,
Position,
} from 'vscode-languageserver-types';
import { ParserRuleContext, ParseTree, Token } from 'antlr4';
import { DbSchema } from '../dbSchema';
import { resolveCypherVersion } from '../helpers';
import {

@@ -13,9 +18,9 @@ LabelOrRelType,

} from '../parserWrapper';
import { Neo4jFunction } from '../types';
import {
SemanticAnalysisElement,
wrappedSemanticAnalysis,
} from './semanticAnalysisWrapper';
import { SyntaxDiagnostic } from './syntaxValidationHelpers';
import { Neo4jFunction, Neo4jProcedure } from '../types';
import { wrappedSemanticAnalysis } from './semanticAnalysisWrapper';
export type SyntaxDiagnostic = Diagnostic & {
offsets: { start: number; end: number };
};
function detectNonDeclaredLabel(

@@ -27,34 +32,23 @@ labelOrRelType: LabelOrRelType,

const labelName = labelOrRelType.labelText;
const normalizedLabelName = labelName.replace(/^`|`$/g, '');
const notInDatabase =
(labelOrRelType.labeltype === LabelType.nodeLabelType &&
!dbLabels.has(labelName)) ||
(labelOrRelType.labeltype === LabelType.relLabelType &&
!dbRelationshipTypes.has(labelName)) ||
(!dbLabels.has(labelName) && !dbRelationshipTypes.has(labelName));
(labelOrRelType.labelType === LabelType.nodeLabelType &&
!dbLabels.has(normalizedLabelName)) ||
(labelOrRelType.labelType === LabelType.relLabelType &&
!dbRelationshipTypes.has(normalizedLabelName)) ||
(!dbLabels.has(normalizedLabelName) &&
!dbRelationshipTypes.has(normalizedLabelName));
if (notInDatabase && !labelOrRelType.couldCreateNewLabel) {
const labelChunks = labelName.split('\n');
const linesOffset = labelChunks.length - 1;
const lineIndex = labelOrRelType.line - 1;
const startColumn = labelOrRelType.column;
const endColumn =
linesOffset == 0
? startColumn + labelName.length
: labelChunks.at(-1)?.length ?? 0;
const warning: SyntaxDiagnostic = {
severity: DiagnosticSeverity.Warning,
range: {
start: Position.create(lineIndex, startColumn),
end: Position.create(lineIndex + linesOffset, endColumn),
},
offsets: labelOrRelType.offsets,
message:
labelOrRelType.labeltype +
' ' +
labelName +
" is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
};
return warning;
const message =
labelOrRelType.labelType +
' ' +
labelName +
" is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application";
return generateSyntaxDiagnostic(
labelName,
labelOrRelType,
DiagnosticSeverity.Warning,
message,
);
}

@@ -65,7 +59,58 @@

function generateSyntaxDiagnostic(
rawText: string,
parsedText: ParsedProcedure | LabelOrRelType,
severity: DiagnosticSeverity,
message: string,
deprecation: boolean = false,
): SyntaxDiagnostic {
const nameChunks = rawText.split('\n');
const linesOffset = nameChunks.length - 1;
const lineIndex = parsedText.line - 1;
const startColumn = parsedText.column;
const endColumn =
linesOffset == 0
? startColumn + rawText.length
: nameChunks.at(-1)?.length ?? 0;
const error: SyntaxDiagnostic = {
severity,
range: {
start: Position.create(lineIndex, startColumn),
end: Position.create(lineIndex + linesOffset, endColumn),
},
offsets: parsedText.offsets,
message,
...(deprecation ? { tags: [DiagnosticTag.Deprecated] } : {}),
};
return error;
}
function detectNonDeclaredFunction(
parsedFunction: ParsedFunction,
functionsSchema: Record<string, Neo4jFunction>,
procedureSchema: Record<string, Neo4jProcedure>,
): SyntaxDiagnostic | undefined {
const lowercaseFunctionName = parsedFunction.name.toLowerCase();
const exists = functionExists(parsedFunction, functionsSchema);
if (!exists) {
const existsAsProcedure =
procedureSchema && Boolean(procedureSchema[parsedFunction.name]);
if (existsAsProcedure) {
return generateProcedureUsedAsFunctionError(parsedFunction);
}
return generateFunctionNotFoundError(parsedFunction);
}
}
function functionExists(
functionCandidate: ParsedFunction,
functionsSchema: Record<string, Neo4jFunction>,
): boolean {
if (!functionCandidate || !functionsSchema) {
return false;
}
const functionExistsWithExactName = Boolean(
functionsSchema[functionCandidate.name],
);
const lowercaseFunctionName = functionCandidate.name.toLowerCase();
const caseInsensitiveFunctionInDatabase =

@@ -75,15 +120,18 @@ functionsSchema[lowercaseFunctionName];

// Built-in functions are case-insensitive in the database
if (
caseInsensitiveFunctionInDatabase &&
caseInsensitiveFunctionInDatabase.isBuiltIn
) {
return undefined;
}
return (
functionExistsWithExactName ||
(caseInsensitiveFunctionInDatabase &&
caseInsensitiveFunctionInDatabase.isBuiltIn)
);
}
const functionExistsWithExactName = Boolean(
functionsSchema[parsedFunction.name],
function generateFunctionUsedAsProcedureError(
parsedProcedure: ParsedProcedure,
): SyntaxDiagnostic {
return generateSyntaxDiagnostic(
parsedProcedure.rawText,
parsedProcedure,
DiagnosticSeverity.Error,
`${parsedProcedure.name} is a function, not a procedure. Did you mean to use the function ${parsedProcedure.name} with a RETURN instead of a CALL clause?`,
);
if (!functionExistsWithExactName) {
return generateFunctionNotFoundError(parsedFunction);
}
}

@@ -94,23 +142,19 @@

): SyntaxDiagnostic {
const rawText = parsedFunction.rawText;
const nameChunks = rawText.split('\n');
const linesOffset = nameChunks.length - 1;
const lineIndex = parsedFunction.line - 1;
const startColumn = parsedFunction.column;
const endColumn =
linesOffset == 0
? startColumn + rawText.length
: nameChunks.at(-1)?.length ?? 0;
return generateSyntaxDiagnostic(
parsedFunction.rawText,
parsedFunction,
DiagnosticSeverity.Error,
`Function ${parsedFunction.name} is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application`,
);
}
const error: SyntaxDiagnostic = {
severity: DiagnosticSeverity.Error,
range: {
start: Position.create(lineIndex, startColumn),
end: Position.create(lineIndex + linesOffset, endColumn),
},
offsets: parsedFunction.offsets,
message: `Function ${parsedFunction.name} is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application`,
};
return error;
function generateProcedureUsedAsFunctionError(
parsedFunction: ParsedFunction,
): SyntaxDiagnostic {
return generateSyntaxDiagnostic(
parsedFunction.rawText,
parsedFunction,
DiagnosticSeverity.Error,
`${parsedFunction.name} is a procedure, not a function. Did you mean to call the procedure ${parsedFunction.name} inside a CALL clause?`,
);
}

@@ -121,23 +165,40 @@

): SyntaxDiagnostic {
const rawText = parsedProcedure.rawText;
const nameChunks = rawText.split('\n');
const linesOffset = nameChunks.length - 1;
const lineIndex = parsedProcedure.line - 1;
const startColumn = parsedProcedure.column;
const endColumn =
linesOffset == 0
? startColumn + rawText.length
: nameChunks.at(-1)?.length ?? 0;
return generateSyntaxDiagnostic(
parsedProcedure.rawText,
parsedProcedure,
DiagnosticSeverity.Error,
`Procedure ${parsedProcedure.name} is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application`,
);
}
const error: SyntaxDiagnostic = {
severity: DiagnosticSeverity.Error,
range: {
start: Position.create(lineIndex, startColumn),
end: Position.create(lineIndex + linesOffset, endColumn),
},
offsets: parsedProcedure.offsets,
message: `Procedure ${parsedProcedure.name} is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application`,
};
function generateProcedureDeprecatedWarning(
parsedProcedure: ParsedProcedure,
deprecatedBy: string | undefined,
): SyntaxDiagnostic {
const procDeprecatedWarning = `Procedure ${parsedProcedure.name} is deprecated.`;
return generateSyntaxDiagnostic(
parsedProcedure.rawText,
parsedProcedure,
DiagnosticSeverity.Warning,
deprecatedBy
? procDeprecatedWarning + ` Alternative: ${deprecatedBy}`
: procDeprecatedWarning,
true,
);
}
return error;
function generateFunctionDeprecatedWarning(
parsedFunction: ParsedFunction,
deprecatedBy: string | undefined,
): SyntaxDiagnostic {
const funcDeprecatedWarning = `Function ${parsedFunction.name} is deprecated.`;
return generateSyntaxDiagnostic(
parsedFunction.rawText,
parsedFunction,
DiagnosticSeverity.Warning,
deprecatedBy
? funcDeprecatedWarning + ` Alternative: ${deprecatedBy}`
: funcDeprecatedWarning,
true,
);
}

@@ -170,89 +231,51 @@

export function sortByPositionAndMessage(
a: SyntaxDiagnostic,
b: SyntaxDiagnostic,
) {
const lineDiff = a.range.start.line - b.range.start.line;
if (lineDiff !== 0) return lineDiff;
const columnDiff = a.range.start.character - b.range.start.character;
if (columnDiff !== 0) return columnDiff;
return a.message > b.message ? 1 : -1;
}
type FixSemanticPositionsArgs = {
semanticElements: SemanticAnalysisElement[];
semanticDiagnostics: SyntaxDiagnostic[];
parseResult: ParsedStatement;
};
function fixSemanticAnalysisPositions({
semanticElements,
function fixOffsets({
semanticDiagnostics,
parseResult,
}: FixSemanticPositionsArgs): SyntaxDiagnostic[] {
const cmd = parseResult.command;
return semanticElements.map((e) => {
let token: Token | undefined = undefined;
return semanticDiagnostics.map((e) => {
const { range, offsets } = e;
const lineAdjust = cmd.start.line - 1;
const colAdjust = range.start.line === 0 ? cmd.start.column : 0;
const offsetAdjust = cmd.start.start;
const start = Position.create(
e.position.line - 1 + cmd.start.line - 1,
e.position.column - 1 + (e.position.line === 1 ? cmd.start.column : 0),
range.start.line + lineAdjust,
range.start.character + colAdjust,
);
const startOffset = e.position.offset + cmd.start.start;
const toExplore: ParseTree[] = [parseResult.ctx];
const end = Position.create(
range.end.line + lineAdjust,
range.end.character + colAdjust,
);
while (toExplore.length > 0) {
const current: ParseTree = toExplore.pop();
const adjustedRange = { start, end };
const adjustedOffset = {
start: offsets.start + offsetAdjust,
end: offsets.end + offsetAdjust,
};
if (current instanceof ParserRuleContext) {
const startToken = current.start;
const stopToken = current.stop;
if (
startToken.start <= startOffset &&
stopToken &&
startOffset <= stopToken.stop
) {
token = stopToken;
if (startToken.start < startOffset && current.children) {
current.children.forEach((child) => toExplore.push(child));
}
}
}
}
if (token === undefined) {
return {
severity: e.severity,
message: e.message,
range: {
start: start,
end: start,
},
offsets: {
start: startOffset,
end: startOffset,
},
};
} else {
return {
severity: e.severity,
message: e.message,
range: {
start: start,
end: Position.create(
token.line - 1,
token.column + token.text.length,
),
},
offsets: {
start: startOffset,
end: token.stop + 1,
},
};
}
return { ...e, range: adjustedRange, offsets: adjustedOffset };
});
}
export function sortByPositionAndMessage(
a: SyntaxDiagnostic,
b: SyntaxDiagnostic,
) {
const lineDiff = a.range.start.line - b.range.start.line;
if (lineDiff !== 0) return lineDiff;
const columnDiff = a.range.start.character - b.range.start.character;
if (columnDiff !== 0) return columnDiff;
return a.message > b.message ? 1 : -1;
}
export function lintCypherQuery(

@@ -262,71 +285,100 @@ query: string,

): SyntaxDiagnostic[] {
const syntaxErrors = validateSyntax(query, dbSchema);
// If there are any syntactic errors in the query, do not run the semantic validation
if (syntaxErrors.find((d) => d.severity === DiagnosticSeverity.Error)) {
return syntaxErrors;
if (query.length > 0) {
const cachedParse = parserWrapper.parse(query);
const statements = cachedParse.statementsParsing;
const errors = statements.flatMap((current) => {
const cmd = current.command;
if (cmd.type === 'cypher' && cmd.statement.length > 0) {
const functionErrors = errorOnUndeclaredFunctions(current, dbSchema);
const procedureErrors = errorOnUndeclaredProcedures(current, dbSchema);
const procedureWarnings = warningOnDeprecatedProcedure(
current,
dbSchema,
);
const functionWarnings = warningOnDeprecatedFunction(current, dbSchema);
const labelWarnings = warnOnUndeclaredLabels(current, dbSchema);
const { notifications, errors } = wrappedSemanticAnalysis(
cmd.statement,
dbSchema,
current.cypherVersion,
);
// This contains both the syntax and the semantic errors
const rawSemanticDiagnostics = notifications.concat(errors);
const semanticDiagnostics = fixOffsets({
semanticDiagnostics: rawSemanticDiagnostics,
parseResult: current,
});
return semanticDiagnostics
.concat(
labelWarnings,
functionErrors,
procedureErrors,
functionWarnings,
procedureWarnings,
)
.sort(sortByPositionAndMessage);
}
// There could be console command errors
return current.syntaxErrors;
});
return errors;
}
const semanticErrors = validateSemantics(query, dbSchema);
return syntaxErrors.concat(semanticErrors);
return [];
}
export function validateSyntax(
query: string,
function warningOnDeprecatedProcedure(
parsingResult: ParsedStatement,
dbSchema: DbSchema,
): SyntaxDiagnostic[] {
if (query.length === 0) {
return [];
const warnings: SyntaxDiagnostic[] = [];
if (dbSchema.procedures) {
const cypherVersion = resolveCypherVersion(
parsingResult.cypherVersion,
dbSchema,
);
const proceduresInQuery = parsingResult.collectedProcedures;
proceduresInQuery.forEach((parsedProcedure) => {
const proc = dbSchema.procedures?.[cypherVersion]?.[parsedProcedure.name];
const procedureDeprecated = proc?.option?.deprecated;
const deprecatedBy = proc?.deprecatedBy;
if (deprecatedBy)
if (procedureDeprecated) {
warnings.push(
generateProcedureDeprecatedWarning(parsedProcedure, deprecatedBy),
);
}
});
}
const statements = parserWrapper.parse(query);
const result = statements.statementsParsing.flatMap((statement) => {
const syntaxErrors = statement.syntaxErrors;
const labelWarnings = warnOnUndeclaredLabels(statement, dbSchema);
return syntaxErrors.concat(labelWarnings).sort(sortByPositionAndMessage);
});
return result;
return warnings;
}
/**
* Assumes the provided query has no parse errors
*/
export function validateSemantics(
query: string,
function warningOnDeprecatedFunction(
parsingResult: ParsedStatement,
dbSchema: DbSchema,
): SyntaxDiagnostic[] {
if (query.length > 0) {
const cachedParse = parserWrapper.parse(query);
const statements = cachedParse.statementsParsing;
const semanticErrors = statements.flatMap((current) => {
if (current.syntaxErrors.length === 0) {
const cmd = current.command;
if (cmd.type === 'cypher' && cmd.statement.length > 0) {
const functionErrors = errorOnUndeclaredFunctions(current, dbSchema);
const procedureErrors = errorOnUndeclaredProcedures(
current,
dbSchema,
);
const { notifications, errors } = wrappedSemanticAnalysis(
cmd.statement,
dbSchema,
);
const elements = notifications.concat(errors);
const semanticDiagnostics = fixSemanticAnalysisPositions({
semanticElements: elements,
parseResult: current,
});
return semanticDiagnostics
.concat(functionErrors, procedureErrors)
.sort(sortByPositionAndMessage);
}
const warnings: SyntaxDiagnostic[] = [];
if (dbSchema.functions) {
const cypherVersion = resolveCypherVersion(
parsingResult.cypherVersion,
dbSchema,
);
const functionsInQuery = parsingResult.collectedFunctions;
functionsInQuery.forEach((parsedFunction) => {
const fn = dbSchema.functions?.[cypherVersion]?.[parsedFunction.name];
const functionDeprecated = fn?.isDeprecated;
const deprecatedBy = fn?.deprecatedBy;
if (functionDeprecated) {
warnings.push(
generateFunctionDeprecatedWarning(parsedFunction, deprecatedBy),
);
}
return [];
});
return semanticErrors;
}
return [];
return warnings;
}

@@ -341,2 +393,6 @@

if (dbSchema.functions) {
const cypherVersion = resolveCypherVersion(
parsingResult.cypherVersion,
dbSchema,
);
const functionsInQuery = parsingResult.collectedFunctions;

@@ -347,3 +403,4 @@

parsedFunction,
dbSchema.functions,
dbSchema.functions?.[cypherVersion] ?? {},
dbSchema.procedures?.[cypherVersion] ?? {},
);

@@ -365,2 +422,6 @@

if (dbSchema.procedures) {
const cypherVersion = resolveCypherVersion(
parsingResult.cypherVersion,
dbSchema,
);
const proceduresInQuery = parsingResult.collectedProcedures;

@@ -370,6 +431,14 @@

const procedureExists = Boolean(
dbSchema.procedures[parsedProcedure.name],
dbSchema.procedures?.[cypherVersion]?.[parsedProcedure.name],
);
if (!procedureExists) {
errors.push(generateProcedureNotFoundError(parsedProcedure));
const existsAsFunction = functionExists(
parsedProcedure,
dbSchema.functions?.[cypherVersion] ?? {},
);
if (existsAsFunction) {
errors.push(generateFunctionUsedAsProcedureError(parsedProcedure));
} else {
errors.push(generateProcedureNotFoundError(parsedProcedure));
}
}

@@ -376,0 +445,0 @@ });

@@ -8,7 +8,3 @@ import {

import type { ParserRuleContext } from 'antlr4-c3';
import {
Diagnostic,
DiagnosticSeverity,
Position,
} from 'vscode-languageserver-types';
import { DiagnosticSeverity, Position } from 'vscode-languageserver-types';
import CypherLexer from '../generated-parser/CypherCmdLexer';

@@ -18,7 +14,4 @@ import CypherParser from '../generated-parser/CypherCmdParser';

import { completionCoreErrormessage } from './completionCoreErrors';
import { SyntaxDiagnostic } from './syntaxValidation';
export type SyntaxDiagnostic = Diagnostic & {
offsets: { start: number; end: number };
};
export class SyntaxErrorsListener implements ANTLRErrorListener<CommonToken> {

@@ -25,0 +18,0 @@ errors: SyntaxDiagnostic[];

@@ -7,4 +7,16 @@ import { CompletionItemKind } from 'vscode-languageserver-types';

const dbSchema: DbSchema = {
databaseNames: ['db1', 'db2', 'movies'],
aliasNames: ['myMovies', 'scoped.alias', 'a.b.c.d'],
databaseNames: [
'db1',
'db2',
'movies',
'financial reports',
'neo4j_server',
],
aliasNames: [
'myMovies',
'scoped.alias',
'a.b.c.d',
'panama papers1',
'office-db',
],
parameters: {

@@ -16,5 +28,63 @@ param1: 'something',

},
'param 4': 'something else',
},
};
test('Correctly completes database names/aliases with special symbols using backticks for SHOW DATABASE', () => {
const query = 'SHOW DATABASE ';
testCompletions({
query,
dbSchema,
expected: [
{
label: 'financial reports',
kind: CompletionItemKind.Value,
insertText: '`financial reports`',
},
{
label: 'panama papers1',
kind: CompletionItemKind.Value,
insertText: '`panama papers1`',
},
{
label: 'office-db',
kind: CompletionItemKind.Value,
insertText: '`office-db`',
},
{
label: '$param 4',
kind: CompletionItemKind.Variable,
insertText: '$`param 4`',
},
],
});
});
test('Correctly completes aliases with special symbols using backticks for SHOW ALIAS', () => {
const query = 'SHOW ALIAS ';
testCompletions({
query,
dbSchema,
expected: [
{
label: 'panama papers1',
kind: CompletionItemKind.Value,
insertText: '`panama papers1`',
},
{
label: 'office-db',
kind: CompletionItemKind.Value,
insertText: '`office-db`',
},
{
label: '$param 4',
kind: CompletionItemKind.Variable,
insertText: '$`param 4`',
},
],
});
});
test('Correctly completes database names and aliases in SHOW DATABASE', () => {

@@ -32,2 +102,3 @@ const query = 'SHOW DATABASE ';

{ label: 'movies', kind: CompletionItemKind.Value },
{ label: 'neo4j_server', kind: CompletionItemKind.Value },
{ label: 'myMovies', kind: CompletionItemKind.Value },

@@ -73,3 +144,3 @@ { label: 'scoped.alias', kind: CompletionItemKind.Value },

test("Doesn't suggest existing database names or aliases when createing database", () => {
test("Doesn't suggest existing database names or aliases when creating database", () => {
const query = 'CREATE DATABASE ';

@@ -76,0 +147,0 @@

@@ -6,2 +6,3 @@ import {

import { DbSchema } from '../../dbSchema';
import { _internalFeatureFlags } from '../../featureFlags';
import { testData } from '../testData';

@@ -11,2 +12,13 @@ import { testCompletions } from './completionAssertionHelpers';

describe('function invocations', () => {
let isCypher25: boolean;
beforeAll(() => {
isCypher25 = _internalFeatureFlags.cypher25;
_internalFeatureFlags.cypher25 = true;
});
afterAll(() => {
_internalFeatureFlags.cypher25 = isCypher25;
});
const dbSchema: DbSchema = testData.mockSchema;

@@ -25,4 +37,4 @@ const functions = dbSchema.functions;

detail: '(function)',
signature: functions['acos'].signature,
documentation: functions['acos'].description,
signature: functions['CYPHER 5']['acos'].signature,
documentation: functions['CYPHER 5']['acos'].description,
},

@@ -38,4 +50,4 @@ {

detail: '(function)',
signature: functions['apoc.agg.graph'].signature,
documentation: functions['apoc.agg.graph'].description,
signature: functions['CYPHER 5']['apoc.agg.graph'].signature,
documentation: functions['CYPHER 5']['apoc.agg.graph'].description,
},

@@ -46,4 +58,4 @@ {

detail: '(function)',
signature: functions['apoc.coll.pairs'].signature,
documentation: functions['apoc.coll.pairs'].description,
signature: functions['CYPHER 5']['apoc.coll.pairs'].signature,
documentation: functions['CYPHER 5']['apoc.coll.pairs'].description,
},

@@ -54,4 +66,4 @@ {

detail: '(function)',
signature: functions['apoc.create.uuid'].signature,
documentation: functions['apoc.create.uuid'].description,
signature: functions['CYPHER 5']['apoc.create.uuid'].signature,
documentation: functions['CYPHER 5']['apoc.create.uuid'].description,
tags: [CompletionItemTag.Deprecated],

@@ -73,4 +85,4 @@ },

detail: '(function)',
signature: functions['acos'].signature,
documentation: functions['acos'].description,
signature: functions['CYPHER 5']['acos'].signature,
documentation: functions['CYPHER 5']['acos'].description,
},

@@ -81,4 +93,4 @@ {

detail: '(function)',
signature: functions['apoc.agg.graph'].signature,
documentation: functions['apoc.agg.graph'].description,
signature: functions['CYPHER 5']['apoc.agg.graph'].signature,
documentation: functions['CYPHER 5']['apoc.agg.graph'].description,
},

@@ -89,4 +101,4 @@ {

detail: '(function)',
signature: functions['apoc.coll.pairs'].signature,
documentation: functions['apoc.coll.pairs'].description,
signature: functions['CYPHER 5']['apoc.coll.pairs'].signature,
documentation: functions['CYPHER 5']['apoc.coll.pairs'].description,
},

@@ -97,4 +109,4 @@ {

detail: '(function)',
signature: functions['apoc.create.uuid'].signature,
documentation: functions['apoc.create.uuid'].description,
signature: functions['CYPHER 5']['apoc.create.uuid'].signature,
documentation: functions['CYPHER 5']['apoc.create.uuid'].description,
tags: [CompletionItemTag.Deprecated],

@@ -143,4 +155,4 @@ },

detail: '(function)',
signature: functions['apoc.agg.first'].signature,
documentation: functions['apoc.agg.first'].description,
signature: functions['CYPHER 5']['apoc.agg.first'].signature,
documentation: functions['CYPHER 5']['apoc.agg.first'].description,
},

@@ -151,4 +163,4 @@ {

detail: '(function)',
signature: functions['apoc.agg.last'].signature,
documentation: functions['apoc.agg.last'].description,
signature: functions['CYPHER 5']['apoc.agg.last'].signature,
documentation: functions['CYPHER 5']['apoc.agg.last'].description,
},

@@ -159,4 +171,4 @@ {

detail: '(function)',
signature: functions['apoc.agg.slice'].signature,
documentation: functions['apoc.agg.slice'].description,
signature: functions['CYPHER 5']['apoc.agg.slice'].signature,
documentation: functions['CYPHER 5']['apoc.agg.slice'].description,
},

@@ -194,4 +206,4 @@ ],

detail: '(function)',
signature: functions['apoc.agg.first'].signature,
documentation: functions['apoc.agg.first'].description,
signature: functions['CYPHER 5']['apoc.agg.first'].signature,
documentation: functions['CYPHER 5']['apoc.agg.first'].description,
},

@@ -202,4 +214,4 @@ {

detail: '(function)',
signature: functions['apoc.agg.last'].signature,
documentation: functions['apoc.agg.last'].description,
signature: functions['CYPHER 5']['apoc.agg.last'].signature,
documentation: functions['CYPHER 5']['apoc.agg.last'].description,
},

@@ -210,4 +222,4 @@ {

detail: '(function)',
signature: functions['apoc.agg.slice'].signature,
documentation: functions['apoc.agg.slice'].description,
signature: functions['CYPHER 5']['apoc.agg.slice'].signature,
documentation: functions['CYPHER 5']['apoc.agg.slice'].description,
},

@@ -325,4 +337,5 @@ ],

detail: '(function)',
signature: functions['apoc.agg.percentiles'].signature,
documentation: functions['apoc.agg.percentiles'].description,
signature: functions['CYPHER 5']['apoc.agg.percentiles'].signature,
documentation:
functions['CYPHER 5']['apoc.agg.percentiles'].description,
},

@@ -333,4 +346,4 @@ {

detail: '(function)',
signature: functions['acos'].signature,
documentation: functions['acos'].description,
signature: functions['CYPHER 5']['acos'].signature,
documentation: functions['CYPHER 5']['acos'].description,
},

@@ -351,4 +364,5 @@ ],

detail: '(function)',
signature: functions['apoc.agg.percentiles'].signature,
documentation: functions['apoc.agg.percentiles'].description,
signature: functions['CYPHER 5']['apoc.agg.percentiles'].signature,
documentation:
functions['CYPHER 5']['apoc.agg.percentiles'].description,
},

@@ -364,4 +378,4 @@ {

detail: '(function)',
signature: functions['acos'].signature,
documentation: functions['acos'].description,
signature: functions['CYPHER 5']['acos'].signature,
documentation: functions['CYPHER 5']['acos'].description,
},

@@ -378,4 +392,6 @@ ],

functions: {
math: { ...testData.emptyFunction, name: 'math' },
'math.max': { ...testData.emptyFunction, name: 'math.max' },
'CYPHER 5': {
math: { ...testData.emptyFunction, name: 'math' },
'math.max': { ...testData.emptyFunction, name: 'math.max' },
},
},

@@ -410,4 +426,4 @@ },

detail: '(function)',
signature: functions['apoc.create.uuid'].signature,
documentation: functions['apoc.create.uuid'].description,
signature: functions['CYPHER 5']['apoc.create.uuid'].signature,
documentation: functions['CYPHER 5']['apoc.create.uuid'].description,
tags: [CompletionItemTag.Deprecated],

@@ -418,2 +434,35 @@ },

});
test('Functions are completed based on cypher version', () => {
// apoc.create.uuid was deprecated in Cypher 5 and removed in Cypher 25
testCompletions({
query: 'CYPHER 5 RETURN apoc.create.',
dbSchema,
expected: [
{
label: 'uuid',
kind: CompletionItemKind.Function,
detail: '(function)',
signature: functions['CYPHER 5']['apoc.create.uuid'].signature,
documentation: functions['CYPHER 5']['apoc.create.uuid'].description,
tags: [CompletionItemTag.Deprecated],
},
],
});
testCompletions({
query: 'CYPHER 25 RETURN apoc.create.',
dbSchema,
excluded: [
{
label: 'uuid',
kind: CompletionItemKind.Function,
detail: '(function)',
signature: functions['CYPHER 5']['apoc.create.uuid'].signature,
documentation: functions['CYPHER 5']['apoc.create.uuid'].description,
tags: [CompletionItemTag.Deprecated],
},
],
});
});
});

@@ -8,2 +8,23 @@ import { CompletionItemKind } from 'vscode-languageserver-types';

describe('Misc auto-completion', () => {
test('Correctly completes cypher version number', () => {
const query = 'CYPHER ';
testCompletions({
query,
expected: [{ label: '5', kind: CompletionItemKind.EnumMember }],
});
});
test('Correctly completes CYPHER when keyword is not finished, optionally with version', () => {
const query = 'CYP';
testCompletions({
query,
expected: [
{ label: 'CYPHER 5', kind: CompletionItemKind.Keyword },
{ label: 'CYPHER', kind: CompletionItemKind.Keyword },
],
});
});
test('Correctly completes empty statement', () => {

@@ -207,3 +228,2 @@ const query = '';

{ kind: 14, label: 'FOR' },
{ kind: 14, label: 'ON' },
],

@@ -210,0 +230,0 @@ });

@@ -86,3 +86,3 @@ import { CompletionItemKind } from 'vscode-languageserver-types';

const query =
'CREATE CONSTRAINT abc ON (n:person) ASSERT EXISTS n.name OPTIONS ';
'CREATE CONSTRAINT abc FOR (n:person) REQUIRE n.name IS NOT NULL OPTIONS ';
testCompletions({

@@ -89,0 +89,0 @@ query,

@@ -45,2 +45,23 @@ import { CompletionItemKind } from 'vscode-languageserver-types';

test('Correctly completes even with preparser options', () => {
const query = 'CYPHER 25 runtime=pipelined MATCH (n:P';
testCompletions({
query,
dbSchema: { labels: ['Cat', 'Person', 'Dog'] },
expected: [{ label: 'Person', kind: CompletionItemKind.TypeParameter }],
});
const query2 = 'CYPHER 25 runtime=pipelined MATCH (n:Person)-[:W';
testCompletions({
query: query2,
dbSchema: {
labels: ['Cat', 'Person', 'Dog'],
relationshipTypes: ['WALKS', 'OWNS'],
},
expected: [{ label: 'WALKS', kind: CompletionItemKind.TypeParameter }],
});
});
test("Doesn't complete label before : is entered", () => {

@@ -91,2 +112,125 @@ const query = 'MATCH (n';

test('Completes label with numbers, underscores and non-english letters without backticks in MATCH', () => {
const query = 'MATCH (n:';
testCompletions({
query,
dbSchema: {
labels: ['Cat12', 'Foo_Bar', 'Glögg', 'Glühwein', '_GingerBread_'],
},
expected: [
{
label: 'Cat12',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Foo_Bar',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Glögg',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Glühwein',
kind: CompletionItemKind.TypeParameter,
},
{
label: '_GingerBread_',
kind: CompletionItemKind.TypeParameter,
},
],
});
});
test('Completes relationship types with numbers, underscores and non-english letters without backticks in MATCH', () => {
const query = 'MATCH (n)-[:';
testCompletions({
query,
dbSchema: {
relationshipTypes: [
'Cat12',
'Foo_Bar',
'Glögg',
'Glühwein',
'_GingerBread_',
],
},
expected: [
{
label: 'Cat12',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Foo_Bar',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Glögg',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Glühwein',
kind: CompletionItemKind.TypeParameter,
},
{
label: '_GingerBread_',
kind: CompletionItemKind.TypeParameter,
},
],
});
});
test('Correctly completes label with backticks in MATCH', () => {
const query = 'MATCH (n:';
testCompletions({
query,
dbSchema: { labels: ['Cat', 'Foo Bar', '1Foo'] },
expected: [
{
label: 'Cat',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Foo Bar',
insertText: '`Foo Bar`',
kind: CompletionItemKind.TypeParameter,
},
{
label: '1Foo',
insertText: '`1Foo`',
kind: CompletionItemKind.TypeParameter,
},
],
});
});
test('Correctly completes started label with backticks in MATCH', () => {
const query = 'MATCH (n:F';
testCompletions({
query,
dbSchema: { labels: ['Cat', 'Foo Bar', '1Foo'] },
expected: [
// This first completion would not be offered in a client
{
label: 'Cat',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Foo Bar',
insertText: '`Foo Bar`',
kind: CompletionItemKind.TypeParameter,
},
{
label: '1Foo',
insertText: '`1Foo`',
kind: CompletionItemKind.TypeParameter,
},
],
});
});
test('Correctly completes unstarted label for a first statement when caret is passed', () => {

@@ -410,2 +554,52 @@ const query = 'MATCH (n:); MATCH (m:)';

});
test('Correctly completes relationship type with backticks', () => {
const query = 'MATCH (n)-[r:';
testCompletions({
query,
dbSchema: { relationshipTypes: ['Rel', 'Foo Bar', '1Foo'] },
expected: [
{
label: 'Rel',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Foo Bar',
insertText: '`Foo Bar`',
kind: CompletionItemKind.TypeParameter,
},
{
label: '1Foo',
insertText: '`1Foo`',
kind: CompletionItemKind.TypeParameter,
},
],
});
});
test('Correctly completes started relationship type with backticks', () => {
const query = 'MATCH (n)-[r:Foo';
testCompletions({
query,
dbSchema: { relationshipTypes: ['Rel', 'Foo Bar', '1Foo'] },
expected: [
{
label: 'Rel',
kind: CompletionItemKind.TypeParameter,
},
{
label: 'Foo Bar',
insertText: '`Foo Bar`',
kind: CompletionItemKind.TypeParameter,
},
{
label: '1Foo',
insertText: '`1Foo`',
kind: CompletionItemKind.TypeParameter,
},
],
});
});
});

@@ -412,0 +606,0 @@

@@ -6,2 +6,3 @@ import {

import { DbSchema } from '../../dbSchema';
import { _internalFeatureFlags } from '../../featureFlags';
import { testData } from '../testData';

@@ -11,2 +12,13 @@ import { testCompletions } from './completionAssertionHelpers';

describe('Procedures auto-completion', () => {
let isCypher25: boolean;
beforeAll(() => {
isCypher25 = _internalFeatureFlags.cypher25;
_internalFeatureFlags.cypher25 = true;
});
afterAll(() => {
_internalFeatureFlags.cypher25 = isCypher25;
});
const procedures = testData.mockSchema.procedures;

@@ -16,13 +28,19 @@

procedures: {
'tx.getMetaData': procedures['tx.getMetaData'],
'db.index.fulltext.awaitEventuallyConsistentIndexRefresh':
procedures['db.index.fulltext.awaitEventuallyConsistentIndexRefresh'],
'db.ping': procedures['db.ping'],
'db.stats.retrieve': procedures['db.stats.retrieve'],
'db.stats.collect': procedures['db.stats.collect'],
'db.stats.clear': procedures['db.stats.clear'],
'cdc.current': procedures['cdc.current'],
'jwt.security.requestAccess': {
...testData.emptyProcedure,
name: 'jwt.security.requestAccess',
'CYPHER 5': {
'tx.getMetaData': procedures['CYPHER 5']['tx.getMetaData'],
'db.index.fulltext.awaitEventuallyConsistentIndexRefresh':
procedures['CYPHER 5'][
'db.index.fulltext.awaitEventuallyConsistentIndexRefresh'
],
'db.ping': procedures['CYPHER 5']['db.ping'],
'db.stats.retrieve': procedures['CYPHER 5']['db.stats.retrieve'],
'db.stats.collect': procedures['CYPHER 5']['db.stats.collect'],
'db.stats.clear': procedures['CYPHER 5']['db.stats.clear'],
'dbms.components': procedures['CYPHER 5']['dbms.components'],
'cdc.current': procedures['CYPHER 5']['cdc.current'],
'jwt.security.requestAccess': {
...testData.emptyProcedure,
name: 'jwt.security.requestAccess',
},
'apoc.create.uuids': procedures['CYPHER 5']['apoc.create.uuids'],
},

@@ -70,4 +88,4 @@ },

detail: '(procedure)',
signature: procedures['tx.getMetaData'].signature,
documentation: procedures['tx.getMetaData'].description,
signature: procedures['CYPHER 5']['tx.getMetaData'].signature,
documentation: procedures['CYPHER 5']['tx.getMetaData'].description,
},

@@ -78,4 +96,4 @@ {

detail: '(procedure)',
signature: procedures['cdc.current'].signature,
documentation: procedures['cdc.current'].description,
signature: procedures['CYPHER 5']['cdc.current'].signature,
documentation: procedures['CYPHER 5']['cdc.current'].description,
tags: [CompletionItemTag.Deprecated],

@@ -109,4 +127,4 @@ },

detail: '(procedure)',
signature: procedures['db.ping'].signature,
documentation: procedures['db.ping'].description,
signature: procedures['CYPHER 5']['db.ping'].signature,
documentation: procedures['CYPHER 5']['db.ping'].description,
},

@@ -132,4 +150,5 @@ ],

detail: '(procedure)',
signature: procedures['db.stats.retrieve'].signature,
documentation: procedures['db.stats.retrieve'].description,
signature: procedures['CYPHER 5']['db.stats.retrieve'].signature,
documentation:
procedures['CYPHER 5']['db.stats.retrieve'].description,
},

@@ -140,4 +159,4 @@ {

detail: '(procedure)',
signature: procedures['db.stats.collect'].signature,
documentation: procedures['db.stats.collect'].description,
signature: procedures['CYPHER 5']['db.stats.collect'].signature,
documentation: procedures['CYPHER 5']['db.stats.collect'].description,
},

@@ -148,4 +167,4 @@ {

detail: '(procedure)',
signature: procedures['db.stats.clear'].signature,
documentation: procedures['db.stats.clear'].description,
signature: procedures['CYPHER 5']['db.stats.clear'].signature,
documentation: procedures['CYPHER 5']['db.stats.clear'].description,
},

@@ -183,4 +202,5 @@ ],

detail: '(procedure)',
signature: procedures['db.stats.retrieve'].signature,
documentation: procedures['db.stats.retrieve'].description,
signature: procedures['CYPHER 5']['db.stats.retrieve'].signature,
documentation:
procedures['CYPHER 5']['db.stats.retrieve'].description,
},

@@ -191,4 +211,4 @@ {

detail: '(procedure)',
signature: procedures['db.stats.collect'].signature,
documentation: procedures['db.stats.collect'].description,
signature: procedures['CYPHER 5']['db.stats.collect'].signature,
documentation: procedures['CYPHER 5']['db.stats.collect'].description,
},

@@ -199,4 +219,4 @@ {

detail: '(procedure)',
signature: procedures['db.stats.clear'].signature,
documentation: procedures['db.stats.clear'].description,
signature: procedures['CYPHER 5']['db.stats.clear'].signature,
documentation: procedures['CYPHER 5']['db.stats.clear'].description,
},

@@ -273,4 +293,4 @@ ],

detail: '(procedure)',
signature: procedures['db.ping'].signature,
documentation: procedures['db.ping'].description,
signature: procedures['CYPHER 5']['db.ping'].signature,
documentation: procedures['CYPHER 5']['db.ping'].description,
},

@@ -297,4 +317,4 @@ {

detail: '(procedure)',
signature: procedures['cdc.current'].signature,
documentation: procedures['cdc.current'].description,
signature: procedures['CYPHER 5']['cdc.current'].signature,
documentation: procedures['CYPHER 5']['cdc.current'].description,
tags: [CompletionItemTag.Deprecated],

@@ -305,2 +325,172 @@ },

});
test('Correctly completes YIELD', () => {
const query = 'CALL dbms.components() YIELD ';
testCompletions({
query,
dbSchema,
expected: [
{ label: 'name', kind: CompletionItemKind.Variable },
{ label: 'versions', kind: CompletionItemKind.Variable },
{ label: 'edition', kind: CompletionItemKind.Variable },
],
});
});
test('Correctly completes YIELD when we have extra spaces in the procedure name', () => {
const query = 'CALL dbms . components () YIELD ';
testCompletions({
query,
dbSchema,
expected: [
{ label: 'name', kind: CompletionItemKind.Variable },
{ label: 'versions', kind: CompletionItemKind.Variable },
{ label: 'edition', kind: CompletionItemKind.Variable },
],
});
});
test('Does not autocomplete duplicate variables for YIELD', () => {
const query1 = 'CALL dbms.components() YIELD name, ';
const query2 = 'CALL dbms.components() YIELD versions, ve';
testCompletions({
query: query1,
dbSchema,
excluded: [{ label: 'name', kind: CompletionItemKind.Variable }],
});
testCompletions({
query: query2,
dbSchema,
excluded: [{ label: 'versions', kind: CompletionItemKind.Variable }],
});
});
test('Correctly completes RETURN when we have variables from a YIELD statement', () => {
const query1 = 'CALL dbms.components() YIELD name, edition RETURN ';
const query2 = 'CALL dbms.components() YIELD name, versions RETURN ';
const query3 =
'CALL dbms.components() YIELD name, edition RETURN edition, nam';
testCompletions({
query: query1,
dbSchema,
expected: [
{ label: 'name', kind: CompletionItemKind.Variable },
{ label: 'edition', kind: CompletionItemKind.Variable },
],
});
testCompletions({
query: query2,
dbSchema,
expected: [
{ label: 'name', kind: CompletionItemKind.Variable },
{ label: 'versions', kind: CompletionItemKind.Variable },
],
});
testCompletions({
query: query3,
dbSchema,
expected: [{ label: 'name', kind: CompletionItemKind.Variable }],
});
});
test('Correctly completes YIELD when we have started the return value name', () => {
const query1 = 'CALL dbms.components() YIELD n';
const query2 = 'CALL dbms.components() YIELD name, ed';
const query3 = 'CALL dbms.components() YIELD name, edition,';
testCompletions({
query: query1,
dbSchema,
expected: [
{ label: 'name', kind: CompletionItemKind.Variable },
{ label: 'versions', kind: CompletionItemKind.Variable },
{ label: 'edition', kind: CompletionItemKind.Variable },
],
});
testCompletions({
query: query2,
dbSchema,
expected: [
{ label: 'versions', kind: CompletionItemKind.Variable },
{ label: 'edition', kind: CompletionItemKind.Variable },
],
});
testCompletions({
query: query3,
dbSchema,
expected: [{ label: 'versions', kind: CompletionItemKind.Variable }],
});
});
test('Correctly completes YIELD when we have backticks in the procedure name', () => {
const query1 = 'CALL `dbms`.`components`() YIELD ';
const query2 = 'CALL db.`stats`.`collect` () YIELD ';
testCompletions({
query: query1,
dbSchema,
expected: [
{ label: 'name', kind: CompletionItemKind.Variable },
{ label: 'versions', kind: CompletionItemKind.Variable },
{ label: 'edition', kind: CompletionItemKind.Variable },
],
});
testCompletions({
query: query2,
dbSchema,
expected: [
{ label: 'section', kind: CompletionItemKind.Variable },
{ label: 'success', kind: CompletionItemKind.Variable },
{ label: 'message', kind: CompletionItemKind.Variable },
],
});
});
test('Returns empty YIELD elements when procedure does not exist', () => {
const query = 'CALL does.not.exist() YIELD ';
testCompletions({
query,
dbSchema,
expected: [],
});
});
test('Procedures are completed based on cypher version', () => {
// apoc.create.uuids was deprecated in Cypher 5 and removed in Cypher 25
testCompletions({
query: 'CYPHER 5 CALL apoc.create.',
dbSchema,
expected: [
{
label: 'uuids',
kind: CompletionItemKind.Method,
detail: '(procedure)',
signature: procedures['CYPHER 5']['apoc.create.uuids'].signature,
documentation:
procedures['CYPHER 5']['apoc.create.uuids'].description,
tags: [CompletionItemTag.Deprecated],
},
],
});
testCompletions({
query: 'CYPHER 25 CALL apoc.create.',
dbSchema,
excluded: [
{
label: 'uuids',
kind: CompletionItemKind.Method,
detail: '(procedure)',
signature: procedures['CYPHER 5']['apoc.create.uuids'].signature,
documentation:
procedures['CYPHER 5']['apoc.create.uuids'].description,
tags: [CompletionItemTag.Deprecated],
},
],
});
});
});

@@ -10,5 +10,7 @@ import { CompletionItemKind } from 'vscode-languageserver-types';

functions: {
'apoc.util.sleep': {
...testData.emptyFunction,
name: 'apoc.util.sleep',
'CYPHER 5': {
'apoc.util.sleep': {
...testData.emptyFunction,
name: 'apoc.util.sleep',
},
},

@@ -158,2 +160,75 @@ },

});
test('Completes node property keys with numbers, underscores and non-english letters without backticks in MATCH', () => {
const query = 'MATCH (n) WHERE n.';
testCompletions({
query,
dbSchema: {
propertyKeys: [
'Cat12',
'Foo_Bar',
'Glögg',
'Glühwein',
'_GingerBread_',
],
},
expected: [
{
label: 'Cat12',
kind: CompletionItemKind.Property,
},
{
label: 'Foo_Bar',
kind: CompletionItemKind.Property,
},
{
label: 'Glögg',
kind: CompletionItemKind.Property,
},
{
label: 'Glühwein',
kind: CompletionItemKind.Property,
},
{
label: '_GingerBread_',
kind: CompletionItemKind.Property,
},
],
});
});
test('correctly completes property keys with backticks', () => {
const dbSchema = { propertyKeys: ['foo bar', 'prop'] };
const query = 'MATCH (n) WHERE n.';
testCompletions({
query,
dbSchema,
expected: [
{ label: 'prop', kind: CompletionItemKind.Property },
{
label: 'foo bar',
insertText: '`foo bar`',
kind: CompletionItemKind.Property,
},
],
});
});
test('correctly completes started property keys with backticks', () => {
const dbSchema = { propertyKeys: ['foo bar', 'prop'] };
const query = 'MATCH (n) WHERE n.foo';
testCompletions({
query,
dbSchema,
expected: [
{ label: 'prop', kind: CompletionItemKind.Property },
{
label: 'foo bar',
insertText: '`foo bar`',
kind: CompletionItemKind.Property,
},
],
});
});
});

@@ -119,3 +119,3 @@ import { CompletionItemKind } from 'vscode-languageserver-types';

test('suggests variables for index hint rule', () => {
const query = 'match (n) USING BTREE INDEX ';
const query = 'match (n) USING RANGE INDEX ';

@@ -122,0 +122,0 @@ testCompletions({

@@ -7,6 +7,3 @@ /* eslint-disable no-console */

import { applySyntaxColouring } from '../../syntaxColouring/syntaxColouring';
import {
lintCypherQuery,
validateSyntax,
} from '../../syntaxValidation/syntaxValidation';
import { lintCypherQuery } from '../../syntaxValidation/syntaxValidation';
import { testData } from '../testData';

@@ -39,3 +36,3 @@ import {

parserWrapper.clearCache();
validateSyntax(simpleQuery, testData.mockSchema);
lintCypherQuery(simpleQuery, testData.mockSchema);
})

@@ -42,0 +39,0 @@ .add('simple - autocomplete next statement', function () {

@@ -39,7 +39,11 @@ import { autocomplete } from '../autocompletion/autocompletion';

describe('sanity checks', () => {
let consoleCommands: boolean;
beforeAll(() => {
consoleCommands = _internalFeatureFlags.consoleCommands;
_internalFeatureFlags.consoleCommands = true;
});
afterAll(() => {
_internalFeatureFlags.consoleCommands = false;
_internalFeatureFlags.consoleCommands = consoleCommands;
});

@@ -50,2 +54,5 @@

expectParsedCommands(':history', [{ type: 'history' }]);
expectParsedCommands(':connect', [{ type: 'connect' }]);
expectParsedCommands(':disconnect', [{ type: 'disconnect' }]);
expectParsedCommands(':sysinfo', [{ type: 'sysinfo' }]);
});

@@ -98,2 +105,68 @@

]);
expect(applySyntaxColouring(':connect')).toEqual([
{
length: 1,
position: {
line: 0,
startCharacter: 0,
startOffset: 0,
},
token: ':',
tokenType: 'consoleCommand',
},
{
length: 7,
position: {
line: 0,
startCharacter: 1,
startOffset: 1,
},
token: 'connect',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':disconnect')).toEqual([
{
length: 1,
position: {
line: 0,
startCharacter: 0,
startOffset: 0,
},
token: ':',
tokenType: 'consoleCommand',
},
{
length: 10,
position: {
line: 0,
startCharacter: 1,
startOffset: 1,
},
token: 'disconnect',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':sysinfo')).toEqual([
{
length: 1,
position: {
line: 0,
startCharacter: 0,
startOffset: 0,
},
token: ':',
tokenType: 'consoleCommand',
},
{
length: 7,
position: {
line: 0,
startCharacter: 1,
startOffset: 1,
},
token: 'sysinfo',
tokenType: 'consoleCommand',
},
]);
});

@@ -103,3 +176,8 @@

expect(autocomplete(':', {})).toEqual([
{ kind: 23, label: 'server' },
{ kind: 23, label: 'use' },
{ kind: 23, label: 'sysinfo' },
{ kind: 23, label: 'welcome' },
{ kind: 23, label: 'disconnect' },
{ kind: 23, label: 'connect' },
{ kind: 23, label: 'param' },

@@ -141,3 +219,6 @@ { kind: 23, label: 'history' },

test('handles misspelled or non-existing command', () => {
expectErrorMessage(':foo', 'Expected any of param, history, clear or use');
expectErrorMessage(
':foo',
'Expected any of sysinfo, welcome, disconnect, connect, param, history, clear, server or use',
);

@@ -149,7 +230,10 @@ expectErrorMessage(':clea', 'Unexpected token. Did you mean clear?');

describe(':use', () => {
let consoleCommands: boolean;
beforeAll(() => {
consoleCommands = _internalFeatureFlags.consoleCommands;
_internalFeatureFlags.consoleCommands = true;
});
afterAll(() => {
_internalFeatureFlags.consoleCommands = false;
_internalFeatureFlags.consoleCommands = consoleCommands;
});

@@ -216,7 +300,10 @@ test('parses without arg', () => {

describe('parameters', () => {
let consoleCommands: boolean;
beforeAll(() => {
consoleCommands = _internalFeatureFlags.consoleCommands;
_internalFeatureFlags.consoleCommands = true;
});
afterAll(() => {
_internalFeatureFlags.consoleCommands = false;
_internalFeatureFlags.consoleCommands = consoleCommands;
});

@@ -249,8 +336,29 @@ test('basic param usage', () => {

test('allows setting parameters with escaped name', () => {
expectParsedCommands(':param `foo foo` => bar', [
{
type: 'set-parameters',
parameters: [{ name: '`foo foo`', expression: 'bar' }],
},
]);
expectParsedCommands(':param {`a a a`: 2, `b-b-b`: rand()}', [
{
type: 'set-parameters',
parameters: [
{ name: '`a a a`', expression: '2' },
{ name: '`b-b-b`', expression: 'rand()' },
],
},
]);
});
test('autocompletes expressions', () => {
const arrowCompletions = autocomplete(':param foo => ', {
functions: {
'duration.inSeconds': {
...testData.emptyFunction,
name: 'duration.inSeconds',
'CYPHER 5': {
'duration.inSeconds': {
...testData.emptyFunction,
name: 'duration.inSeconds',
},
},

@@ -261,5 +369,7 @@ },

functions: {
'duration.inSeconds': {
...testData.emptyFunction,
name: 'duration.inSeconds',
'CYPHER 5': {
'duration.inSeconds': {
...testData.emptyFunction,
name: 'duration.inSeconds',
},
},

@@ -460,9 +570,122 @@ },

describe('server', () => {
let consoleCommands: boolean;
beforeAll(() => {
consoleCommands = _internalFeatureFlags.consoleCommands;
_internalFeatureFlags.consoleCommands = true;
});
afterAll(() => {
_internalFeatureFlags.consoleCommands = consoleCommands;
});
test('basic server usage', () => {
expectParsedCommands(':server connect', [
{ type: 'server', operation: 'connect' },
]);
expectParsedCommands(':server disconnect', [
{ type: 'server', operation: 'disconnect' },
]);
});
test('autocompletes operation', () => {
const mapCompletions = autocomplete(':server conn', {
functions: {
'CYPHER 5': {
'duration.inSeconds': {
...testData.emptyFunction,
name: 'duration.inSeconds',
},
},
},
});
const expected = [
{ kind: 23, label: 'connect' },
{ kind: 23, label: 'disconnect' },
];
expected.forEach((completion) => {
expect(mapCompletions).toContainEqual(completion);
});
});
test('incorrect usage of :server', () => {
expectErrorMessage(':server', 'Expected connect or disconnect');
expectErrorMessage(':server xyz', 'Expected connect or disconnect');
});
test('highlights :server properly', () => {
expect(applySyntaxColouring(':server')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 6,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'server',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':server connect')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 6,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'server',
tokenType: 'consoleCommand',
},
{
length: 7,
position: { line: 0, startCharacter: 8, startOffset: 8 },
token: 'connect',
tokenType: 'consoleCommand',
},
]);
expect(applySyntaxColouring(':server disconnect')).toEqual([
{
length: 1,
position: { line: 0, startCharacter: 0, startOffset: 0 },
token: ':',
tokenType: 'consoleCommand',
},
{
length: 6,
position: { line: 0, startCharacter: 1, startOffset: 1 },
token: 'server',
tokenType: 'consoleCommand',
},
{
length: 10,
position: { line: 0, startCharacter: 8, startOffset: 8 },
token: 'disconnect',
tokenType: 'consoleCommand',
},
]);
});
});
describe('command parser also handles cypher', () => {
let consoleCommands: boolean;
beforeAll(() => {
consoleCommands = _internalFeatureFlags.consoleCommands;
_internalFeatureFlags.consoleCommands = true;
});
afterAll(() => {
_internalFeatureFlags.consoleCommands = false;
_internalFeatureFlags.consoleCommands = consoleCommands;
});
test('parses cypher', () => {

@@ -469,0 +692,0 @@ expectParsedCommands('MATCH (n) RETURN n', [

import { SignatureHelp } from 'vscode-languageserver-types';
import { DbSchema } from '../dbSchema';
import { _internalFeatureFlags } from '../featureFlags';
import {

@@ -24,5 +25,17 @@ emptyResult,

describe('Procedures signature help', () => {
let isCypher25: boolean;
beforeAll(() => {
isCypher25 = _internalFeatureFlags.cypher25;
_internalFeatureFlags.cypher25 = true;
});
afterAll(() => {
_internalFeatureFlags.cypher25 = isCypher25;
});
const dbSchema = testData.mockSchema;
const procedureName = 'apoc.do.when';
const signature = toSignatureInformation(dbSchema.procedures[procedureName]);
const procedure = dbSchema.procedures['CYPHER 5'][procedureName];
const signature = toSignatureInformation(procedure);

@@ -230,8 +243,41 @@ function expectedArgIndex(i: number): SignatureHelp {

});
test('Procedures signature help depends on cypher version', () => {
const dbSchema = {
functions: {},
procedures: {
'CYPHER 5': {
[procedureName]: procedure,
},
'CYPHER 25': {},
},
};
testSignatureHelp(
'CYPHER 5 CALL apoc.do.when(',
dbSchema,
expectedArgIndex(0),
);
testSignatureHelp('CYPHER 25 CALL apoc.do.when(', dbSchema, {
activeParameter: 0,
activeSignature: undefined,
signatures: [],
});
});
});
describe('Functions signature help', () => {
beforeAll(() => {
_internalFeatureFlags.cypher25 = true;
});
afterAll(() => {
_internalFeatureFlags.cypher25 = false;
});
const dbSchema = testData.mockSchema;
const functionName = 'apoc.coll.combinations';
const signature = toSignatureInformation(dbSchema.functions[functionName]);
const fn = dbSchema.functions['CYPHER 5'][functionName];
const signature = toSignatureInformation(fn);

@@ -418,2 +464,26 @@ function expectedArgIndex(i: number): SignatureHelp {

});
test('Functions signature help depends on cypher version', () => {
const dbSchema = {
procedures: {},
functions: {
'CYPHER 5': {
[functionName]: fn,
},
'CYPHER 25': {},
},
};
testSignatureHelp(
'CYPHER 5 RETURN apoc.coll.combinations(',
dbSchema,
expectedArgIndex(0),
);
testSignatureHelp('CYPHER 25 RETURN apoc.coll.combinations(', dbSchema, {
activeParameter: 0,
activeSignature: undefined,
signatures: [],
});
});
});
import { applySyntaxColouring } from '../../syntaxColouring/syntaxColouring';
describe('Preparser syntax colouring', () => {
test('Correctly colours cypher versions', () => {
const query = 'CYPHER 25 MATCH (m) RETURN m';
expect(applySyntaxColouring(query)).toEqual([
{
bracketInfo: undefined,
length: 6,
position: {
line: 0,
startCharacter: 0,
startOffset: 0,
},
token: 'CYPHER',
tokenType: 'keyword',
},
{
bracketInfo: undefined,
length: 2,
position: {
line: 0,
startCharacter: 7,
startOffset: 7,
},
token: '25',
tokenType: 'numberLiteral',
},
{
bracketInfo: undefined,
length: 5,
position: {
line: 0,
startCharacter: 10,
startOffset: 10,
},
token: 'MATCH',
tokenType: 'keyword',
},
{
bracketInfo: {
bracketLevel: 0,
bracketType: 'parenthesis',
},
length: 1,
position: {
line: 0,
startCharacter: 16,
startOffset: 16,
},
token: '(',
tokenType: 'bracket',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 0,
startCharacter: 17,
startOffset: 17,
},
token: 'm',
tokenType: 'variable',
},
{
bracketInfo: {
bracketLevel: 0,
bracketType: 'parenthesis',
},
length: 1,
position: {
line: 0,
startCharacter: 18,
startOffset: 18,
},
token: ')',
tokenType: 'bracket',
},
{
bracketInfo: undefined,
length: 6,
position: {
line: 0,
startCharacter: 20,
startOffset: 20,
},
token: 'RETURN',
tokenType: 'keyword',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 0,
startCharacter: 27,
startOffset: 27,
},
token: 'm',
tokenType: 'variable',
},
]);
});
test('Correctly colours CYPHER <setting> = <setting-value> options', () => {
const query = 'CYPHER runtime = slotted RETURN ""';
expect(applySyntaxColouring(query)).toEqual([
{
bracketInfo: undefined,
length: 6,
position: {
line: 0,
startCharacter: 0,
startOffset: 0,
},
token: 'CYPHER',
tokenType: 'keyword',
},
{
bracketInfo: undefined,
length: 7,
position: {
line: 0,
startCharacter: 7,
startOffset: 7,
},
token: 'runtime',
tokenType: 'setting',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 0,
startCharacter: 15,
startOffset: 15,
},
token: '=',
tokenType: 'operator',
},
{
bracketInfo: undefined,
length: 7,
position: {
line: 0,
startCharacter: 17,
startOffset: 17,
},
token: 'slotted',
tokenType: 'settingValue',
},
{
bracketInfo: undefined,
length: 6,
position: {
line: 0,
startCharacter: 25,
startOffset: 25,
},
token: 'RETURN',
tokenType: 'keyword',
},
{
bracketInfo: undefined,
length: 2,
position: {
line: 0,
startCharacter: 32,
startOffset: 32,
},
token: '""',
tokenType: 'stringLiteral',
},
]);
});
test('Correctly colours PROFILE', () => {

@@ -367,2 +538,242 @@ const query = 'PROFILE MATCH (n) RETURN n';

});
test('Correctly colours console commands used as variables or labels', () => {
const query = `CREATE (n:clear {use: 0})
WITH $param AS map
RETURN map.propertyKey`;
expect(applySyntaxColouring(query)).toEqual([
{
bracketInfo: undefined,
length: 6,
position: {
line: 0,
startCharacter: 0,
startOffset: 0,
},
token: 'CREATE',
tokenType: 'keyword',
},
{
bracketInfo: {
bracketLevel: 0,
bracketType: 'parenthesis',
},
length: 1,
position: {
line: 0,
startCharacter: 7,
startOffset: 7,
},
token: '(',
tokenType: 'bracket',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 0,
startCharacter: 8,
startOffset: 8,
},
token: 'n',
tokenType: 'variable',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 0,
startCharacter: 9,
startOffset: 9,
},
token: ':',
tokenType: 'operator',
},
{
bracketInfo: undefined,
length: 5,
position: {
line: 0,
startCharacter: 10,
startOffset: 10,
},
token: 'clear',
tokenType: 'label',
},
{
bracketInfo: {
bracketLevel: 0,
bracketType: 'curly',
},
length: 1,
position: {
line: 0,
startCharacter: 16,
startOffset: 16,
},
token: '{',
tokenType: 'bracket',
},
{
bracketInfo: undefined,
length: 3,
position: {
line: 0,
startCharacter: 17,
startOffset: 17,
},
token: 'use',
tokenType: 'property',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 0,
startCharacter: 20,
startOffset: 20,
},
token: ':',
tokenType: 'operator',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 0,
startCharacter: 22,
startOffset: 22,
},
token: '0',
tokenType: 'numberLiteral',
},
{
bracketInfo: {
bracketLevel: 0,
bracketType: 'curly',
},
length: 1,
position: {
line: 0,
startCharacter: 23,
startOffset: 23,
},
token: '}',
tokenType: 'bracket',
},
{
bracketInfo: {
bracketLevel: 0,
bracketType: 'parenthesis',
},
length: 1,
position: {
line: 0,
startCharacter: 24,
startOffset: 24,
},
token: ')',
tokenType: 'bracket',
},
{
bracketInfo: undefined,
length: 4,
position: {
line: 1,
startCharacter: 0,
startOffset: 26,
},
token: 'WITH',
tokenType: 'keyword',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 1,
startCharacter: 5,
startOffset: 31,
},
token: '$',
tokenType: 'paramDollar',
},
{
bracketInfo: undefined,
length: 5,
position: {
line: 1,
startCharacter: 6,
startOffset: 32,
},
token: 'param',
tokenType: 'paramValue',
},
{
bracketInfo: undefined,
length: 2,
position: {
line: 1,
startCharacter: 12,
startOffset: 38,
},
token: 'AS',
tokenType: 'keyword',
},
{
bracketInfo: undefined,
length: 3,
position: {
line: 1,
startCharacter: 15,
startOffset: 41,
},
token: 'map',
tokenType: 'variable',
},
{
bracketInfo: undefined,
length: 6,
position: {
line: 2,
startCharacter: 0,
startOffset: 45,
},
token: 'RETURN',
tokenType: 'keyword',
},
{
bracketInfo: undefined,
length: 3,
position: {
line: 2,
startCharacter: 7,
startOffset: 52,
},
token: 'map',
tokenType: 'variable',
},
{
bracketInfo: undefined,
length: 1,
position: {
line: 2,
startCharacter: 10,
startOffset: 55,
},
token: '.',
tokenType: 'operator',
},
{
bracketInfo: undefined,
length: 11,
position: {
line: 2,
startCharacter: 11,
startOffset: 56,
},
token: 'propertyKey',
tokenType: 'property',
},
]);
});
});

@@ -0,1 +1,3 @@

import { DiagnosticTag } from 'vscode-languageserver-types';
import { _internalFeatureFlags } from '../../featureFlags';
import { testData } from '../testData';

@@ -5,2 +7,192 @@ import { getDiagnosticsForQuery } from './helpers';

describe('Functions semantic validation spec', () => {
let isCypher25: boolean;
beforeAll(() => {
isCypher25 = _internalFeatureFlags.cypher25;
_internalFeatureFlags.cypher25 = true;
});
afterAll(() => {
_internalFeatureFlags.cypher25 = isCypher25;
});
test('Syntax validation warns on deprecated function when database can be contacted and deprecated by is not present', () => {
const query = `RETURN id()`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['Dog', 'Cat'],
relationshipTypes: ['Person'],
functions: testData.mockSchema.functions,
},
}),
).toEqual(
expect.arrayContaining([
{
tags: [DiagnosticTag.Deprecated],
offsets: {
end: 9,
start: 7,
},
message: 'Function id is deprecated.',
range: {
end: {
character: 9,
line: 0,
},
start: {
character: 7,
line: 0,
},
},
severity: 2,
},
]),
);
});
test('Syntax validation warns on deprecated function when database can be contacted and deprecated by is present', () => {
const query = `RETURN apoc.text.regreplace("Neo4j GraphQL Neo4j GraphQL", "GraphQL", "GRANDstack") AS output;`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['Dog', 'Cat'],
relationshipTypes: ['Person'],
functions: testData.mockSchema.functions,
},
}),
).toEqual([
{
message:
'Function apoc.text.regreplace is deprecated. Alternative: apoc.text.replace',
offsets: {
end: 27,
start: 7,
},
range: {
end: {
character: 27,
line: 0,
},
start: {
character: 7,
line: 0,
},
},
severity: 2,
tags: [2],
},
]);
});
test('Syntax validation error on function used as procedure returns helpful message', () => {
const query = `CALL abs(-50) YIELD *`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['Dog', 'Cat'],
relationshipTypes: ['Person'],
functions: testData.mockSchema.functions,
procedures: testData.mockSchema.procedures,
},
}),
).toEqual([
{
message:
'abs is a function, not a procedure. Did you mean to use the function abs with a RETURN instead of a CALL clause?',
offsets: {
end: 8,
start: 5,
},
range: {
end: {
character: 8,
line: 0,
},
start: {
character: 5,
line: 0,
},
},
severity: 1,
},
]);
});
test('Using improved message for function used as procedure is not case sensitive for built-in functions', () => {
const query = `CALL aBs(-50) YIELD *`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['Dog', 'Cat'],
relationshipTypes: ['Person'],
functions: testData.mockSchema.functions,
procedures: testData.mockSchema.procedures,
},
}),
).toEqual([
{
message:
'aBs is a function, not a procedure. Did you mean to use the function aBs with a RETURN instead of a CALL clause?',
offsets: {
end: 8,
start: 5,
},
range: {
end: {
character: 8,
line: 0,
},
start: {
character: 5,
line: 0,
},
},
severity: 1,
},
]);
});
test('Using improved message for function used as procedure is case sensitive for external functions', () => {
const query = `CALL apoc.text.TOUpperCase("message") YIELD *`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['Dog', 'Cat'],
relationshipTypes: ['Person'],
functions: testData.mockSchema.functions,
procedures: testData.mockSchema.procedures,
},
}),
).toEqual([
{
message:
"Procedure apoc.text.TOUpperCase is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
offsets: {
end: 26,
start: 5,
},
range: {
end: {
character: 26,
line: 0,
},
start: {
character: 5,
line: 0,
},
},
severity: 1,
},
]);
});
test('Syntax validation errors on missing function when database can be contacted', () => {

@@ -554,2 +746,207 @@ const query = `RETURN dontpanic("marvin")`;

});
test('Deprecations and removals for functions are based on the cypher version', () => {
const dbSchema = {
functions: {
'CYPHER 5': {
'apoc.create.uuid': {
name: 'apoc.create.uuid',
category: '',
description: 'Returns a UUID.',
signature: 'apoc.create.uuid(config :: MAP) :: STRING',
isBuiltIn: false,
argumentDescription: [
{
isDeprecated: true,
description: '',
name: 'config',
type: 'MAP',
},
],
returnDescription: 'STRING',
aggregating: false,
isDeprecated: false,
deprecatedBy: 'Neo4j randomUUID() function',
},
},
'CYPHER 25': {
'apoc.create.uuid': {
name: 'apoc.create.uuid',
category: '',
description: 'Returns a UUID.',
signature: 'apoc.create.uuid() :: STRING',
isBuiltIn: false,
argumentDescription: [],
returnDescription: 'STRING',
aggregating: false,
isDeprecated: false,
deprecatedBy: 'Neo4j randomUUID() function',
},
},
},
};
expect(
getDiagnosticsForQuery({
query: 'CYPHER 5 RETURN apoc.create.uuid()',
dbSchema: dbSchema,
}),
).toEqual([
{
message: `Function call does not provide the required number of arguments: expected 1 got 0.
Function apoc.create.uuid has signature: apoc.create.uuid(config :: MAP) :: STRING
meaning that it expects 1 argument of type MAP`,
offsets: {
end: 34,
start: 16,
},
range: {
end: {
character: 34,
line: 0,
},
start: {
character: 16,
line: 0,
},
},
severity: 1,
},
{
message:
"The function has a deprecated field. ('config' used by 'apoc.create.uuid' is deprecated.)",
offsets: {
end: 34,
start: 16,
},
range: {
end: {
character: 34,
line: 0,
},
start: {
character: 16,
line: 0,
},
},
severity: 2,
},
]);
expect(
getDiagnosticsForQuery({
query: 'CYPHER 25 RETURN apoc.create.uuid()',
dbSchema: dbSchema,
}),
).toEqual([]);
});
test('Errors and notifications for functions are different based on the cypher version', () => {
expect(
getDiagnosticsForQuery({
query: `
CYPHER 5 call apoc.cypher.runTimeboxed("match (n:Node), (m:Node)
WHERE n <> m
match path = shortestpath((n)-[:CONNECTED_TO*]-(m))
RETURN n, m, length(path) AS path", {}, 100, {})
YIELD value
RETURN value.n.uuid, value.m.uuid, value.path;`,
dbSchema: testData.mockSchema,
}),
).toEqual([
{
message: `Procedure call provides too many arguments: got 4 expected no more than 3.
Procedure apoc.cypher.runTimeboxed has signature: apoc.cypher.runTimeboxed(statement :: STRING, params :: MAP, timeout :: INTEGER) :: value :: MAP
meaning that it expects at least 3 arguments of types STRING, MAP, INTEGER
`,
offsets: {
end: 297,
start: 20,
},
range: {
end: {
character: 74,
line: 6,
},
start: {
character: 19,
line: 1,
},
},
severity: 1,
},
]);
expect(
getDiagnosticsForQuery({
query: `
CYPHER 25 call apoc.cypher.runTimeboxed("match (n:Node), (m:Node)
WHERE n <> m
match path = shortestpath((n)-[:CONNECTED_TO*]-(m))
RETURN n, m, length(path) AS path", {}, 100, {})
YIELD value
RETURN value.n.uuid, value.m.uuid, value.path;`,
dbSchema: testData.mockSchema,
}),
).toEqual([]);
});
test('Warnings for procedures misused as functions are different depending on cypher the version', () => {
// Procedure misused as function, but deprecated in Cypher5, so removed in Cypher25
expect(
getDiagnosticsForQuery({
query: 'CYPHER 5 RETURN apoc.create.uuids(5)',
dbSchema: testData.mockSchema,
}),
).toEqual([
{
message:
'apoc.create.uuids is a procedure, not a function. Did you mean to call the procedure apoc.create.uuids inside a CALL clause?',
offsets: {
end: 33,
start: 16,
},
range: {
end: {
character: 33,
line: 0,
},
start: {
character: 16,
line: 0,
},
},
severity: 1,
},
]);
expect(
getDiagnosticsForQuery({
query: 'CYPHER 25 RETURN apoc.create.uuids(5)',
dbSchema: testData.mockSchema,
}),
).toEqual([
{
message:
"Function apoc.create.uuids is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
offsets: {
end: 34,
start: 17,
},
range: {
end: {
character: 34,
line: 0,
},
start: {
character: 17,
line: 0,
},
},
severity: 1,
},
]);
});
});

@@ -0,1 +1,2 @@

import { _internalFeatureFlags } from '../../featureFlags';
import { testData } from '../testData';

@@ -5,2 +6,142 @@ import { getDiagnosticsForQuery } from './helpers';

describe('Procedures semantic validation spec', () => {
let isCypher25: boolean;
beforeAll(() => {
_internalFeatureFlags.cypher25 = true;
});
afterAll(() => {
isCypher25 = _internalFeatureFlags.cypher25;
_internalFeatureFlags.cypher25 = isCypher25;
});
test('Syntax validation warns on deprecated procedure when database can be contacted', () => {
const query = `CALL db.create.setVectorProperty()`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['Dog', 'Cat'],
relationshipTypes: ['Person'],
procedures: testData.mockSchema.procedures,
},
}),
).toEqual([
{
message: `Procedure call does not provide the required number of arguments: got 0 expected at least 3 (total: 3, 0 of which have default values).
Procedure db.create.setVectorProperty has signature: db.create.setVectorProperty(node :: NODE, key :: STRING, vector :: ANY) :: node :: NODE
meaning that it expects at least 3 arguments of types NODE, STRING, ANY
`,
offsets: {
end: 34,
start: 0,
},
range: {
end: {
character: 34,
line: 0,
},
start: {
character: 0,
line: 0,
},
},
severity: 1,
},
{
message:
'Procedure db.create.setVectorProperty is deprecated. Alternative: db.create.setNodeVectorProperty',
offsets: {
end: 32,
start: 5,
},
range: {
end: {
character: 32,
line: 0,
},
start: {
character: 5,
line: 0,
},
},
severity: 2,
tags: [2],
},
]);
});
test('Syntax validation error on procedure used as function returns helpful message', () => {
const query = `RETURN apoc.create.uuids(50)`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['Dog', 'Cat'],
relationshipTypes: ['Person'],
functions: testData.mockSchema.functions,
procedures: testData.mockSchema.procedures,
},
}),
).toEqual([
{
message:
'apoc.create.uuids is a procedure, not a function. Did you mean to call the procedure apoc.create.uuids inside a CALL clause?',
offsets: {
end: 24,
start: 7,
},
range: {
end: {
character: 24,
line: 0,
},
start: {
character: 7,
line: 0,
},
},
severity: 1,
},
]);
});
test('Using improved message for procedure used as function is case sensitive', () => {
const query = `RETURN apoc.cReAtE.uuids(50)`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['Dog', 'Cat'],
relationshipTypes: ['Person'],
functions: testData.mockSchema.functions,
procedures: testData.mockSchema.procedures,
},
}),
).toEqual([
{
message:
"Function apoc.cReAtE.uuids is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
offsets: {
end: 24,
start: 7,
},
range: {
end: {
character: 24,
line: 0,
},
start: {
character: 7,
line: 0,
},
},
severity: 1,
},
]);
});
test('Syntax validation warns on missing procedures when database can be contacted', () => {

@@ -51,21 +192,23 @@ const query = `CALL dontpanic("marvin")`;

procedures: {
mockProcedure: {
name: 'mockProcedure',
description:
'Returns the current change identifier that can be used to stream changes from.',
mode: 'READ',
worksOnSystem: false,
argumentDescription: [],
signature: 'mockProcedure() :: (id :: STRING)',
returnDescription: [
{
isDeprecated: false,
description: 'id :: STRING',
name: 'id',
type: 'STRING',
'CYPHER 5': {
mockProcedure: {
name: 'mockProcedure',
description:
'Returns the current change identifier that can be used to stream changes from.',
mode: 'READ',
worksOnSystem: false,
argumentDescription: [],
signature: 'mockProcedure() :: (id :: STRING)',
returnDescription: [
{
isDeprecated: false,
description: 'id :: STRING',
name: 'id',
type: 'STRING',
},
],
admin: false,
option: {
deprecated: false,
},
],
admin: false,
option: {
deprecated: false,
},

@@ -378,21 +521,23 @@ },

procedures: {
mockProcedure: {
name: 'mockProcedure',
description:
'Returns the current change identifier that can be used to stream changes from.',
mode: 'READ',
worksOnSystem: false,
argumentDescription: [],
signature: 'mockProcedure() :: (id :: STRING)',
returnDescription: [
{
isDeprecated: false,
description: 'id :: STRING',
name: 'id',
type: 'STRING',
'CYPHER 5': {
mockProcedure: {
name: 'mockProcedure',
description:
'Returns the current change identifier that can be used to stream changes from.',
mode: 'READ',
worksOnSystem: false,
argumentDescription: [],
signature: 'mockProcedure() :: (id :: STRING)',
returnDescription: [
{
isDeprecated: false,
description: 'id :: STRING',
name: 'id',
type: 'STRING',
},
],
admin: false,
option: {
deprecated: false,
},
],
admin: false,
option: {
deprecated: false,
},

@@ -418,21 +563,23 @@ },

procedures: {
mockProcedure: {
name: 'mockProcedure',
description:
'Returns the current change identifier that can be used to stream changes from.',
mode: 'READ',
worksOnSystem: false,
argumentDescription: [],
signature: 'mockProcedure() :: (id :: STRING)',
returnDescription: [
{
isDeprecated: false,
description: 'id :: STRING',
name: 'id',
type: 'STRING',
'CYPHER 5': {
mockProcedure: {
name: 'mockProcedure',
description:
'Returns the current change identifier that can be used to stream changes from.',
mode: 'READ',
worksOnSystem: false,
argumentDescription: [],
signature: 'mockProcedure() :: (id :: STRING)',
returnDescription: [
{
isDeprecated: false,
description: 'id :: STRING',
name: 'id',
type: 'STRING',
},
],
admin: false,
option: {
deprecated: false,
},
],
admin: false,
option: {
deprecated: false,
},

@@ -711,38 +858,40 @@ },

procedures: {
'apoc.meta.graphSample': {
name: 'apoc.meta.graphSample',
description:
'Examines the full graph and returns a meta-graph.\nUnlike `apoc.meta.graph`, this procedure does not filter away non-existing paths.',
mode: 'DEFAULT',
worksOnSystem: false,
argumentDescription: [
{
isDeprecated: false,
default: 'DefaultParameterValue{value={}, type=MAP}',
description: 'config = {} :: MAP',
name: 'config',
type: 'MAP',
},
],
'CYPHER 5': {
'apoc.meta.graphSample': {
name: 'apoc.meta.graphSample',
description:
'Examines the full graph and returns a meta-graph.\nUnlike `apoc.meta.graph`, this procedure does not filter away non-existing paths.',
mode: 'DEFAULT',
worksOnSystem: false,
argumentDescription: [
{
isDeprecated: false,
default: 'DefaultParameterValue{value={}, type=MAP}',
description: 'config = {} :: MAP',
name: 'config',
type: 'MAP',
},
],
signature:
'apoc.meta.graphSample(config = {} :: MAP) :: (nodes :: LIST<NODE>, relationships :: LIST<RELATIONSHIP>)',
returnDescription: [
{
isDeprecated: true,
description: 'nodes :: LIST<NODE>',
name: 'nodes',
type: 'LIST<NODE>',
signature:
'apoc.meta.graphSample(config = {} :: MAP) :: (nodes :: LIST<NODE>, relationships :: LIST<RELATIONSHIP>)',
returnDescription: [
{
isDeprecated: true,
description: 'nodes :: LIST<NODE>',
name: 'nodes',
type: 'LIST<NODE>',
},
{
isDeprecated: false,
description: 'relationships :: LIST<RELATIONSHIP>',
name: 'relationships',
type: 'LIST<RELATIONSHIP>',
},
],
admin: false,
option: {
deprecated: false,
},
{
isDeprecated: false,
description: 'relationships :: LIST<RELATIONSHIP>',
name: 'relationships',
type: 'LIST<RELATIONSHIP>',
},
],
admin: false,
option: {
deprecated: false,
},

@@ -775,2 +924,167 @@ },

});
test('Deprecations and removals for procedures are based on the cypher version', () => {
expect(
getDiagnosticsForQuery({
query: 'CYPHER 5 CALL apoc.export.arrow.stream.all()',
dbSchema: testData.mockSchema,
}),
).toEqual([
{
message:
'Procedure apoc.export.arrow.stream.all is deprecated. Alternative: This procedure is being moved to APOC Extended.',
offsets: {
end: 42,
start: 14,
},
range: {
end: {
character: 42,
line: 0,
},
start: {
character: 14,
line: 0,
},
},
severity: 2,
tags: [2],
},
]);
expect(
getDiagnosticsForQuery({
query: 'CYPHER 25 CALL apoc.export.arrow.stream.all()',
dbSchema: testData.mockSchema,
}),
).toEqual([
{
message:
"Procedure apoc.export.arrow.stream.all is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
offsets: {
end: 43,
start: 15,
},
range: {
end: {
character: 43,
line: 0,
},
start: {
character: 15,
line: 0,
},
},
severity: 1,
},
]);
});
test('Errors and notifications for procedures are different based on the cypher version', () => {
expect(
getDiagnosticsForQuery({
query: `
CYPHER 5 call apoc.cypher.runTimeboxed("match (n:Node), (m:Node)
WHERE n <> m
match path = shortestpath((n)-[:CONNECTED_TO*]-(m))
RETURN n, m, length(path) AS path", {}, 100, {})
YIELD value
RETURN value.n.uuid, value.m.uuid, value.path;`,
dbSchema: testData.mockSchema,
}),
).toEqual([
{
message: `Procedure call provides too many arguments: got 4 expected no more than 3.
Procedure apoc.cypher.runTimeboxed has signature: apoc.cypher.runTimeboxed(statement :: STRING, params :: MAP, timeout :: INTEGER) :: value :: MAP
meaning that it expects at least 3 arguments of types STRING, MAP, INTEGER
`,
offsets: {
end: 297,
start: 20,
},
range: {
end: {
character: 74,
line: 6,
},
start: {
character: 19,
line: 1,
},
},
severity: 1,
},
]);
expect(
getDiagnosticsForQuery({
query: `
CYPHER 25 call apoc.cypher.runTimeboxed("match (n:Node), (m:Node)
WHERE n <> m
match path = shortestpath((n)-[:CONNECTED_TO*]-(m))
RETURN n, m, length(path) AS path", {}, 100, {})
YIELD value
RETURN value.n.uuid, value.m.uuid, value.path;`,
dbSchema: testData.mockSchema,
}),
).toEqual([]);
});
test('Warnings for functions misused as procedures are different depending on cypher the version', () => {
// Function misused as procedure, but deprecated in Cypher5, so removed in Cypher25
expect(
getDiagnosticsForQuery({
query: 'CYPHER 5 CALL apoc.create.uuid()',
dbSchema: testData.mockSchema,
}),
).toEqual([
{
message:
'apoc.create.uuid is a function, not a procedure. Did you mean to use the function apoc.create.uuid with a RETURN instead of a CALL clause?',
offsets: {
end: 30,
start: 14,
},
range: {
end: {
character: 30,
line: 0,
},
start: {
character: 14,
line: 0,
},
},
severity: 1,
},
]);
expect(
getDiagnosticsForQuery({
query: 'CYPHER 25 CALL apoc.create.uuid()',
dbSchema: testData.mockSchema,
}),
).toEqual([
{
message:
"Procedure apoc.create.uuid is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
offsets: {
end: 31,
start: 15,
},
range: {
end: {
character: 31,
line: 0,
},
start: {
character: 15,
line: 0,
},
},
severity: 1,
},
]);
});
});

@@ -6,2 +6,224 @@ import { _internalFeatureFlags } from '../../featureFlags';

describe('Semantic validation spec', () => {
let isCypher25: boolean;
beforeAll(() => {
isCypher25 = _internalFeatureFlags.cypher25;
_internalFeatureFlags.cypher25 = true;
});
afterAll(() => {
_internalFeatureFlags.cypher25 = isCypher25;
});
test('Semantic analysis is dependant on cypher version', () => {
const query1 = 'CYPHER 5 MATCH (n)-[r]->(m) SET r += m';
const diagnostics1 = getDiagnosticsForQuery({ query: query1 });
const query2 = 'CYPHER 25 MATCH (n)-[r]->(m) SET r += m';
const diagnostics2 = getDiagnosticsForQuery({ query: query2 });
expect(diagnostics1[0].message).not.toEqual(diagnostics2[0].message);
expect(diagnostics1).toEqual([
{
message:
'The use of nodes or relationships for setting properties is deprecated and will be removed in a future version. Please use properties() instead.',
offsets: {
end: 39,
start: 38,
},
range: {
end: {
character: 39,
line: 0,
},
start: {
character: 38,
line: 0,
},
},
severity: 2,
},
]);
expect(diagnostics2).toEqual([
{
message: 'Type mismatch: expected Map but was Node',
offsets: {
end: 39,
start: 38,
},
range: {
end: {
character: 39,
line: 0,
},
start: {
character: 38,
line: 0,
},
},
severity: 1,
},
]);
});
test('Semantic analysis defaults to cypher 5 when no default version is given, and no version is given in query', () => {
const query1 = 'CYPHER 5 MATCH (n)-[r]->(m) SET r += m';
const diagnostics1 = getDiagnosticsForQuery({ query: query1 });
const query2 = 'MATCH (n)-[r]->(m) SET r += m';
const diagnostics2 = getDiagnosticsForQuery({ query: query2 });
expect(diagnostics1[0].message).toEqual(diagnostics2[0].message);
expect(diagnostics1).toEqual([
{
message:
'The use of nodes or relationships for setting properties is deprecated and will be removed in a future version. Please use properties() instead.',
offsets: {
end: 39,
start: 38,
},
range: {
end: {
character: 39,
line: 0,
},
start: {
character: 38,
line: 0,
},
},
severity: 2,
},
]);
expect(diagnostics2).toEqual([
{
message:
'The use of nodes or relationships for setting properties is deprecated and will be removed in a future version. Please use properties() instead.',
offsets: {
end: 29,
start: 28,
},
range: {
end: {
character: 29,
line: 0,
},
start: {
character: 28,
line: 0,
},
},
severity: 2,
},
]);
});
//TODO: Maybe this should actually yield a warning - to be fixed in follow-up, ignoring for now
test('Semantic analysis defaults to cypher 5 when faulty version is given', () => {
const query1 = 'CYPHER 5 MATCH (n)-[r]->(m) SET r += m';
const diagnostics1 = getDiagnosticsForQuery({ query: query1 });
const query2 = 'CYPHER 800 MATCH (n)-[r]->(m) SET r += m';
const diagnostics2 = getDiagnosticsForQuery({ query: query2 });
expect(diagnostics1[0].message).toEqual(diagnostics2[0].message);
});
test('Semantic analysis uses default language if no language is defined in query', () => {
const query1 = 'MATCH (n)-[r]->(m) SET r += m';
const diagnostics1 = getDiagnosticsForQuery({
query: query1,
dbSchema: { defaultLanguage: 'CYPHER 25' },
});
const query2 = 'CYPHER 25 MATCH (n)-[r]->(m) SET r += m';
const diagnostics2 = getDiagnosticsForQuery({ query: query2 });
expect(diagnostics1[0].message).toEqual(diagnostics2[0].message);
expect(diagnostics1).toEqual([
{
message: 'Type mismatch: expected Map but was Node',
offsets: {
end: 29,
start: 28,
},
range: {
end: {
character: 29,
line: 0,
},
start: {
character: 28,
line: 0,
},
},
severity: 1,
},
]);
expect(diagnostics2).toEqual([
{
message: 'Type mismatch: expected Map but was Node',
offsets: {
end: 39,
start: 38,
},
range: {
end: {
character: 39,
line: 0,
},
start: {
character: 38,
line: 0,
},
},
severity: 1,
},
]);
});
test('In-query version takes priority for semantic analysis even if defaultLanguage is defined', () => {
const query1 = 'CYPHER 5 MATCH (n)-[r]->(m) SET r += m';
const diagnostics1 = getDiagnosticsForQuery({
query: query1,
dbSchema: { defaultLanguage: 'CYPHER 25' },
});
const query2 = 'CYPHER 25 MATCH (n)-[r]->(m) SET r += m';
const diagnostics2 = getDiagnosticsForQuery({ query: query2 });
expect(diagnostics1[0].message).not.toEqual(diagnostics2[0].message);
expect(diagnostics1).toEqual([
{
message:
'The use of nodes or relationships for setting properties is deprecated and will be removed in a future version. Please use properties() instead.',
offsets: {
end: 38,
start: 37,
},
range: {
end: {
character: 38,
line: 0,
},
start: {
character: 37,
line: 0,
},
},
severity: 2,
},
]);
expect(diagnostics2).toEqual([
{
message: 'Type mismatch: expected Map but was Node',
offsets: {
end: 39,
start: 38,
},
range: {
end: {
character: 39,
line: 0,
},
start: {
character: 38,
line: 0,
},
},
severity: 1,
},
]);
});
test('Does not trigger semantic errors when there are syntactic errors', () => {

@@ -12,3 +234,4 @@ const query = 'METCH (n) RETURN m';

{
message: 'Unrecognized keyword. Did you mean MATCH?',
message:
"Invalid input 'METCH': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'",
offsets: {

@@ -105,2 +328,227 @@ end: 5,

test('Semantic errors work using empty preparser options. Like CYPHER <rest of query', () => {
const query = `CYPHER MATCH (n);
CYPHER MATCH (m) RETURN n`;
expect(getDiagnosticsForQuery({ query })).toEqual([
{
message:
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
offsets: {
end: 16,
start: 7,
},
range: {
end: {
character: 16,
line: 0,
},
start: {
character: 7,
line: 0,
},
},
severity: 1,
},
{
message: 'Variable `n` not defined',
offsets: {
end: 62,
start: 61,
},
range: {
end: {
character: 44,
line: 1,
},
start: {
character: 43,
line: 1,
},
},
severity: 1,
},
]);
});
test('Semantic errors work using both version syntax and key-value syntax for preparser options. Like CYPHER <version> <option> = <value>', () => {
const query = `CYPHER 25 runtime = pipelined timeout = 1000 MATCH (n);
CYPHER 5 timeout= 555 MATCH (m) RETURN n`;
expect(getDiagnosticsForQuery({ query })).toEqual([
{
message:
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
offsets: {
end: 54,
start: 45,
},
range: {
end: {
character: 54,
line: 0,
},
start: {
character: 45,
line: 0,
},
},
severity: 1,
},
{
message: 'Variable `n` not defined',
offsets: {
end: 115,
start: 114,
},
range: {
end: {
character: 59,
line: 1,
},
start: {
character: 58,
line: 1,
},
},
severity: 1,
},
]);
});
test('Semantic errors work with preparser option CYPHER <option> = <value>', () => {
const query = `CYPHER runtime = pipelined timeout = 1000 MATCH (n);
CYPHER planner=cost MATCH (m) RETURN n`;
expect(getDiagnosticsForQuery({ query })).toEqual([
{
message:
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
offsets: {
end: 53,
start: 44,
},
range: {
end: {
character: 53,
line: 0,
},
start: {
character: 44,
line: 0,
},
},
severity: 1,
},
{
message: 'Variable `n` not defined',
offsets: {
end: 112,
start: 111,
},
range: {
end: {
character: 57,
line: 1,
},
start: {
character: 56,
line: 1,
},
},
severity: 1,
},
]);
});
test('Semantic errors work with preparser option CYPHER <version>', () => {
const query = `CYPHER 25 MATCH (n);
CYPHER 5 MATCH (m) RETURN n`;
expect(getDiagnosticsForQuery({ query })).toEqual([
{
message:
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
offsets: {
end: 19,
start: 10,
},
range: {
end: {
character: 19,
line: 0,
},
start: {
character: 10,
line: 0,
},
},
severity: 1,
},
{
message: 'Variable `n` not defined',
offsets: {
end: 67,
start: 66,
},
range: {
end: {
character: 46,
line: 1,
},
start: {
character: 45,
line: 1,
},
},
severity: 1,
},
]);
});
test('Semantic errors work when we also have preparser options', () => {
const query = `EXPLAIN MATCH (n);
PROFILE MATCH (m) RETURN n`;
expect(getDiagnosticsForQuery({ query })).toEqual([
{
message:
'Query cannot conclude with MATCH (must be a RETURN clause, a FINISH clause, an update clause, a unit subquery call, or a procedure call with no YIELD).',
offsets: {
end: 17,
start: 8,
},
range: {
end: {
character: 17,
line: 0,
},
start: {
character: 8,
line: 0,
},
},
severity: 1,
},
{
message: 'Variable `n` not defined',
offsets: {
end: 64,
start: 63,
},
range: {
end: {
character: 45,
line: 1,
},
start: {
character: 44,
line: 1,
},
},
severity: 1,
},
]);
});
test('Surfaces notifications correctly', () => {

@@ -281,18 +729,19 @@ const query = `

{
message: 'Variable `i` already declared in outer scope',
message:
'Variable in subquery is shadowing a variable with the same name from the outer scope. If you want to use that variable instead, it must be imported into the subquery using importing WITH clause. (the shadowing variable is: i)',
offsets: {
end: 64,
start: 56,
end: 47,
start: 46,
},
range: {
end: {
character: 16,
line: 3,
character: 19,
line: 2,
},
start: {
character: 8,
line: 3,
character: 18,
line: 2,
},
},
severity: 1,
severity: 2,
},

@@ -303,3 +752,3 @@ {

end: 64,
start: 63,
start: 56,
},

@@ -312,3 +761,3 @@ range: {

start: {
character: 15,
character: 8,
line: 3,

@@ -320,18 +769,19 @@ },

{
message: 'Variable `i` already declared',
message:
'Variable in subquery is shadowing a variable with the same name from the outer scope. If you want to use that variable instead, it must be imported into the subquery using importing WITH clause. (the shadowing variable is: i)',
offsets: {
end: 117,
start: 75,
end: 100,
start: 99,
},
range: {
end: {
character: 16,
line: 6,
character: 19,
line: 5,
},
start: {
character: 10,
line: 4,
character: 18,
line: 5,
},
},
severity: 1,
severity: 2,
},

@@ -356,20 +806,2 @@ {

},
{
message: 'Variable `i` already declared in outer scope',
offsets: {
end: 117,
start: 116,
},
range: {
end: {
character: 16,
line: 6,
},
start: {
character: 15,
line: 6,
},
},
severity: 1,
},
]);

@@ -376,0 +808,0 @@ });

@@ -13,3 +13,4 @@ import { getDiagnosticsForQuery } from './helpers';

},
message: 'Unrecognized keyword. Did you mean MATCH?',
message:
"Invalid input 'METCH': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'",
range: {

@@ -35,2 +36,4 @@ end: {

{
message:
"Invalid input 'CAT': expected 'FOREACH', 'ALTER', 'ORDER BY', 'CALL', 'USING PERIODIC COMMIT', 'CREATE', 'LOAD CSV', 'START DATABASE', 'STOP DATABASE', 'DEALLOCATE', 'DELETE', 'DENY', 'DETACH', 'DROP', 'DRYRUN', 'FINISH', 'GRANT', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REALLOCATE', 'REMOVE', 'RENAME', 'RETURN', 'REVOKE', 'ENABLE SERVER', 'SET', 'SHOW', 'SKIP', 'TERMINATE', 'UNWIND', 'USE' or 'WITH'",
offsets: {

@@ -40,4 +43,2 @@ end: 3,

},
message:
'Expected any of ALTER, CALL, CREATE, DEALLOCATE, DELETE, DENY, DETACH, DROP, DRYRUN, ENABLE, EXPLAIN, FINISH, FOREACH, GRANT, INSERT, LIMIT, LOAD, MATCH, MERGE, NODETACH, OFFSET, OPTIONAL, ORDER, PROFILE, REALLOCATE, REMOVE, RENAME, RETURN, REVOKE, SET, SHOW, SKIP, START, STOP, TERMINATE, UNWIND, USE, USING or WITH',
range: {

@@ -63,2 +64,4 @@ end: {

{
message:
"Invalid input 'WERE': expected a graph pattern, 'FOREACH', ',', 'ORDER BY', 'CALL', 'CREATE', 'LOAD CSV', 'DELETE', 'DETACH', 'FINISH', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REMOVE', 'RETURN', 'SET', 'SKIP', 'UNION', 'UNWIND', 'USE', 'USING', 'WHERE', 'WITH' or <EOF>",
offsets: {

@@ -68,3 +71,2 @@ end: 21,

},
message: 'Unexpected token. Did you mean WHERE?',
range: {

@@ -90,2 +92,4 @@ end: {

{
message:
"Invalid input 'WERE': expected a graph pattern, 'FOREACH', ',', 'ORDER BY', 'CALL', 'CREATE', 'LOAD CSV', 'DELETE', 'DETACH', 'FINISH', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REMOVE', 'RETURN', 'SET', 'SKIP', 'UNION', 'UNWIND', 'USE', 'USING', 'WHERE', 'WITH' or <EOF>",
offsets: {

@@ -95,3 +99,2 @@ end: 21,

},
message: 'Unexpected token. Did you mean WHERE?',
range: {

@@ -121,2 +124,4 @@ end: {

{
message:
"Invalid input 'n': expected an expression, 'FOREACH', 'ORDER BY', 'CALL', 'CREATE', 'LOAD CSV', 'DELETE', 'DETACH', 'FINISH', 'INSERT', 'LIMIT', 'MATCH', 'MERGE', 'NODETACH', 'OFFSET', 'OPTIONAL', 'REMOVE', 'RETURN', 'SET', 'SKIP', 'UNION', 'UNWIND', 'USE', 'WITH' or '}'",
offsets: {

@@ -126,4 +131,2 @@ end: 114,

},
message:
"Expected any of '}', AND, CALL, CREATE, DELETE, DETACH, FINISH, FOREACH, INSERT, LIMIT, LOAD, MATCH, MERGE, NODETACH, OFFSET, OPTIONAL, OR, ORDER, REMOVE, RETURN, SET, SKIP, UNION, UNWIND, USE, WITH, XOR or an expression",
range: {

@@ -144,23 +147,29 @@ end: {

test('Misspelt keyword in the middle of the statement', () => {
const query = "MATCH (n:Person) WERE n.name = 'foo'";
test('Syntax validation warns on missing label when database can be contacted', () => {
const query = `MATCH (n: Person) RETURN n`;
expect(getDiagnosticsForQuery({ query })).toEqual([
expect(
getDiagnosticsForQuery({
query,
dbSchema: { labels: ['Dog', 'Cat'], relationshipTypes: ['Person'] },
}),
).toEqual([
{
offsets: {
end: 21,
start: 17,
end: 16,
start: 10,
},
message: 'Unexpected token. Did you mean WHERE?',
message:
"Label Person is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
range: {
end: {
character: 21,
character: 16,
line: 0,
},
start: {
character: 17,
character: 10,
line: 0,
},
},
severity: 1,
severity: 2,
},

@@ -170,21 +179,38 @@ ]);

test('Syntax validation warns on missing label when database can be contacted', () => {
const query = `MATCH (n: Person) RETURN n`;
test('Syntax validation treats labels and relationship types with backticks correctly', () => {
const query = `MATCH (n: \`Foo Bar\`)-[r:\`RR QQ\`]->() RETURN n`;
// With spaces in the label / relationship
expect(
getDiagnosticsForQuery({
query,
dbSchema: { labels: ['Dog', 'Cat'], relationshipTypes: ['Person'] },
dbSchema: { labels: ['Foo Bar'], relationshipTypes: ['RR QQ'] },
}),
).toEqual([]);
// Without spaces in the label / relationship
expect(
getDiagnosticsForQuery({
query: 'MATCH (n: `Foo`)-[r:`RR`]->() RETURN n',
dbSchema: { labels: ['Foo'], relationshipTypes: ['RR'] },
}),
).toEqual([]);
// With incorrect backticked label / rel type
expect(
getDiagnosticsForQuery({
query,
dbSchema: { labels: ['Foo'], relationshipTypes: ['RR'] },
}),
).toEqual([
{
message:
"Label `Foo Bar` is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
offsets: {
end: 16,
end: 19,
start: 10,
},
message:
"Label Person is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
range: {
end: {
character: 16,
character: 19,
line: 0,

@@ -199,2 +225,21 @@ },

},
{
message:
"Relationship type `RR QQ` is not present in the database. Make sure you didn't misspell it or that it is available when you run this statement in your application",
offsets: {
end: 31,
start: 24,
},
range: {
end: {
character: 31,
line: 0,
},
start: {
character: 24,
line: 0,
},
},
severity: 2,
},
]);

@@ -407,3 +452,4 @@ });

{
message: 'Expected a node label / rel type',
message:
"Invalid input ')': expected a node label/relationship type name, '$', '%' or '('",
offsets: {

@@ -501,2 +547,4 @@ end: 10,

{
message:
"Invalid input '': expected 'ALIAS', 'ALIASES', 'ALL', 'BTREE', 'CONSTRAINT', 'CONSTRAINTS', 'DATABASE', 'DEFAULT DATABASE', 'HOME DATABASE', 'DATABASES', 'EXIST', 'EXISTENCE', 'EXISTS', 'FULLTEXT', 'FUNCTION', 'FUNCTIONS', 'BUILT IN', 'INDEX', 'INDEXES', 'KEY', 'LOOKUP', 'NODE', 'POINT', 'POPULATED', 'PRIVILEGE', 'PRIVILEGES', 'PROCEDURE', 'PROCEDURES', 'PROPERTY', 'RANGE', 'REL', 'RELATIONSHIP', 'ROLE', 'ROLES', 'SERVER', 'SERVERS', 'SETTING', 'SETTINGS', 'SUPPORTED', 'TEXT', 'TRANSACTION', 'TRANSACTIONS', 'UNIQUE', 'UNIQUENESS', 'USER', 'CURRENT USER', 'USERS' or 'VECTOR'",
offsets: {

@@ -506,4 +554,2 @@ end: 4,

},
message:
'Expected any of ALIAS, ALIASES, ALL, BTREE, BUILT, CONSTRAINT, CONSTRAINTS, CURRENT, DATABASE, DATABASES, DEFAULT, EXIST, EXISTENCE, EXISTS, FULLTEXT, FUNCTION, FUNCTIONS, HOME, INDEX, INDEXES, KEY, LOOKUP, NODE, POINT, POPULATED, PRIVILEGE, PRIVILEGES, PROCEDURE, PROCEDURES, PROPERTY, RANGE, REL, RELATIONSHIP, ROLE, ROLES, SERVER, SERVERS, SETTING, SETTINGS, SUPPORTED, TEXT, TRANSACTION, TRANSACTIONS, UNIQUE, UNIQUENESS, USER, USERS or VECTOR',
range: {

@@ -543,3 +589,4 @@ end: {

{
message: 'Unfinished string literal',
message:
'Failed to parse string literal. The query must contain an even number of non-escaped quotes.',
offsets: {

@@ -564,3 +611,3 @@ end: 17,

test('Syntax validation errors on multiline unfinished string', () => {
test('Syntax validation errors on multiline single-quoted unfinished string', () => {
const query = `RETURN 'something

@@ -576,3 +623,4 @@ foo

{
message: 'Unfinished string literal',
message:
'Failed to parse string literal. The query must contain an even number of non-escaped quotes.',
offsets: {

@@ -597,7 +645,6 @@ end: 37,

test('Syntax validation errors on multiline unfinished property keys', () => {
const query = `RETURN {\`something
foo
bar: "hello"}`;
test('Syntax validation errors on multiline double-quoted unfinished string', () => {
const query = `RETURN "something
foo
bar`;

@@ -610,14 +657,15 @@ expect(

{
message: 'Unfinished escaped identifier',
message:
'Failed to parse string literal. The query must contain an even number of non-escaped quotes.',
offsets: {
end: 49,
start: 8,
end: 37,
start: 7,
},
range: {
end: {
character: 17,
line: 3,
character: 9,
line: 2,
},
start: {
character: 8,
character: 7,
line: 0,

@@ -631,2 +679,37 @@ },

test.fails(
'Syntax validation errors on multiline unfinished property keys',
() => {
const query = `RETURN {\`something
foo
bar: "hello"}`;
expect(
getDiagnosticsForQuery({
query,
}),
).toEqual([
{
message: "Invalid input '`': expected an identifier or '}'",
offsets: {
end: 49,
start: 8,
},
range: {
end: {
character: 17,
line: 3,
},
start: {
character: 8,
line: 0,
},
},
severity: 1,
},
]);
},
);
test('Syntax validation errors on unfinished multiline comment', () => {

@@ -643,3 +726,4 @@ const query = `/* something

{
message: 'Unfinished comment',
message:
'Failed to parse comment. A comment starting on `/*` must have a closing `*/`.',
offsets: {

@@ -673,3 +757,3 @@ end: 34,

{
message: "Expected any of '*', DISTINCT or an expression",
message: "Invalid input '}': expected an expression, '*' or 'DISTINCT'",
offsets: {

@@ -691,24 +775,8 @@ end: 25,

},
{
message: "Expected any of '*', DISTINCT or an expression",
offsets: {
end: 48,
start: 48,
},
range: {
end: {
character: 48,
line: 0,
},
start: {
character: 48,
line: 0,
},
},
severity: 1,
},
]);
});
test('Syntax validation errors on an expected procedure name', () => {
// TODO FIX ME
// Problem here is we were getting a better error before
test.fails('Syntax validation errors on an expected procedure name', () => {
const query = `CALL ,foo`;

@@ -751,3 +819,3 @@

{
message: "Expected '}' or an identifier",
message: "Invalid input '[': expected an identifier or '}'",
offsets: {

@@ -781,3 +849,3 @@ end: 9,

{
message: "Expected any of '=', ')', '{', ':', IS, WHERE or a parameter",
message: `Invalid input '"foo"': expected a graph pattern, a parameter, ')', ':', 'IS', 'WHERE' or '{'`,
offsets: {

@@ -811,3 +879,3 @@ end: 14,

{
message: 'Expected an identifier',
message: "Invalid input ''bar'': expected an identifier",
offsets: {

@@ -842,3 +910,4 @@ end: 23,

{
message: 'Expected any of CHANGE, a string or a parameter',
message:
"Invalid input 'foo': expected a parameter, a string or 'CHANGE'",
offsets: {

@@ -872,3 +941,3 @@ end: 39,

{
message: "Expected any of '=', a database name or a parameter",
message: `Invalid input '"something"': expected a database name, a graph pattern or a parameter`,
offsets: {

@@ -902,3 +971,3 @@ end: 27,

{
message: "Expected '}' or an unsigned integer",
message: `Invalid input '"foo"': expected '}' or an integer value`,
offsets: {

@@ -932,3 +1001,4 @@ end: 40,

{
message: 'Expected a node label / rel type',
message:
"Invalid input ''Person'': expected a node label/relationship type name, '$', '%' or '('",
offsets: {

@@ -962,3 +1032,3 @@ end: 18,

{
message: 'Expected a node label / rel type',
message: `Invalid input ''Person'': expected an identifier, '$', '%' or '('`,
offsets: {

@@ -995,3 +1065,3 @@ end: 20,

end: 1,
start: 1,
start: 0,
},

@@ -1004,3 +1074,3 @@ range: {

start: {
character: 1,
character: 0,
line: 0,

@@ -1016,2 +1086,3 @@ },

`MATCH (n:Test1) RETURN n.profile`,
`MATCH (n:MyLabel) RETURN n.CYPHER`,
`CREATE (n:Test1 {explain: 'Explain'});`,

@@ -1029,2 +1100,30 @@ `RETURN { clear: 'Clear', params: 'params', history: 'history'}`,

);
test('Existing relationship types are not incorrectly reported inside node predicates', () => {
const query = `MATCH ()-[]->*(n WHERE COUNT{ (n)-[:R]->()} = 0) RETURN n`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: [],
relationshipTypes: ['R'],
},
}),
).toEqual([]);
});
test('Existing node labels are not incorrectly reported inside relationship predicates', () => {
const query = `MATCH ()-[r WHERE COUNT{ (n:A)-[]->()} = 0]->() RETURN r`;
expect(
getDiagnosticsForQuery({
query,
dbSchema: {
labels: ['A'],
relationshipTypes: [],
},
}),
).toEqual([]);
});
});

@@ -10,2 +10,5 @@ import { CompletionItem as VSCodeCompletionItem } from 'vscode-languageserver-types';

export const cypherVersions = ['CYPHER 25', 'CYPHER 5'];
export type CypherVersion = (typeof cypherVersions)[number];
// we could parse this string for better types in the future

@@ -29,2 +32,3 @@ export type Neo4jStringType = string;

option: { deprecated: boolean } & Record<string, unknown>;
deprecatedBy?: string;
};

@@ -42,2 +46,3 @@

isDeprecated: boolean;
deprecatedBy?: string;
};

@@ -44,0 +49,0 @@

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc