diff --git a/.changeset/blue-penguins-worry.md b/.changeset/blue-penguins-worry.md new file mode 100644 index 000000000..548acbc09 --- /dev/null +++ b/.changeset/blue-penguins-worry.md @@ -0,0 +1,6 @@ +--- +'@neo4j-cypher/language-support': patch +'@neo4j-cypher/schema-poller': patch +--- + +Updates semantic error worker to use given cypher version diff --git a/packages/language-support/src/dbSchema.ts b/packages/language-support/src/dbSchema.ts index a140db7f1..8ea8b0375 100644 --- a/packages/language-support/src/dbSchema.ts +++ b/packages/language-support/src/dbSchema.ts @@ -1,4 +1,4 @@ -import { Neo4jFunction, Neo4jProcedure } from './types'; +import { CypherVersion, Neo4jFunction, Neo4jProcedure } from './types'; export interface DbSchema { labels?: string[]; @@ -11,4 +11,5 @@ export interface DbSchema { propertyKeys?: string[]; procedures?: Record; functions?: Record; + defaultLanguage?: CypherVersion; } diff --git a/packages/language-support/src/parserWrapper.ts b/packages/language-support/src/parserWrapper.ts index 8fef7206f..b8849c803 100644 --- a/packages/language-support/src/parserWrapper.ts +++ b/packages/language-support/src/parserWrapper.ts @@ -7,6 +7,7 @@ import { DiagnosticSeverity, Position } from 'vscode-languageserver-types'; import { _internalFeatureFlags } from './featureFlags'; import { ClauseContext, + CypherVersionContext, default as CypherParser, FunctionNameContext, LabelNameContext, @@ -31,6 +32,7 @@ import { } from './helpers'; import { SyntaxDiagnostic } from './syntaxValidation/syntaxValidation'; import { SyntaxErrorsListener } from './syntaxValidation/syntaxValidationHelpers'; +import { CypherVersion } from './types'; export interface ParsedStatement { command: ParsedCommand; @@ -45,6 +47,7 @@ export interface ParsedStatement { collectedVariables: string[]; collectedFunctions: ParsedFunction[]; collectedProcedures: ParsedProcedure[]; + cypherVersion?: CypherVersion; } export interface ParsingResult { @@ -174,8 +177,14 @@ export function createParsingResult(query: string): ParsingResult { const labelsCollector = new LabelAndRelTypesCollector(); const variableFinder = new VariableCollector(); 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); const ctx = parser.statementsOrCommands(); // The statement is empty if we cannot find anything that is not EOF or a space @@ -204,6 +213,7 @@ export function createParsingResult(query: string): ParsingResult { collectedVariables: variableFinder.variables, collectedFunctions: methodsFinder.functions, collectedProcedures: methodsFinder.procedures, + cypherVersion: cypherVersionCollector.cypherVersion, }; }); @@ -403,6 +413,35 @@ class MethodsCollector extends ParseTreeListener { } } +class CypherVersionCollector extends ParseTreeListener { + public cypherVersion: CypherVersion; + + constructor() { + super(); + } + + enterEveryRule() { + /* no-op */ + } + visitTerminal() { + /* no-op */ + } + visitErrorNode() { + /* no-op */ + } + + exitEveryRule(ctx: unknown) { + if (ctx instanceof CypherVersionContext) { + this.cypherVersion = + ctx.getText() === '5' + ? 'CYPHER 5' + : ctx.getText() === '25' + ? 'CYPHER 25' + : undefined; + } + } +} + type CypherCmd = { type: 'cypher'; query: string }; type RuleTokens = { start: Token; diff --git a/packages/language-support/src/syntaxValidation/semanticAnalysisWrapper.ts b/packages/language-support/src/syntaxValidation/semanticAnalysisWrapper.ts index 2aa40608e..9863b0169 100644 --- a/packages/language-support/src/syntaxValidation/semanticAnalysisWrapper.ts +++ b/packages/language-support/src/syntaxValidation/semanticAnalysisWrapper.ts @@ -6,6 +6,7 @@ import { DiagnosticSeverity, Position } from 'vscode-languageserver-types'; import { DbSchema } from '../dbSchema'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore +import { CypherVersion } from '../types'; import { analyzeQuery, updateSignatureResolver } from './semanticAnalysis'; import { SyntaxDiagnostic } from './syntaxValidation'; @@ -51,6 +52,7 @@ function copySettingSeverity( export function wrappedSemanticAnalysis( query: string, dbSchema: DbSchema, + parsedVersion?: CypherVersion, ): SemanticAnalysisResult { try { if (JSON.stringify(dbSchema) !== JSON.stringify(previousSchema)) { @@ -63,7 +65,18 @@ export function wrappedSemanticAnalysis( }); } - const semanticErrorsResult = analyzeQuery(query, 'cypher 5'); + let cypherVersion = 'CYPHER 5'; + const defaultVersion = dbSchema.defaultLanguage?.toUpperCase(); + + if (parsedVersion) { + cypherVersion = parsedVersion; + } else if (dbSchema.defaultLanguage) { + cypherVersion = defaultVersion; + } + const semanticErrorsResult = analyzeQuery( + query, + cypherVersion.toLowerCase(), + ); const errors: SemanticAnalysisElement[] = semanticErrorsResult.errors; const notifications: SemanticAnalysisElement[] = semanticErrorsResult.notifications; diff --git a/packages/language-support/src/syntaxValidation/syntaxValidation.ts b/packages/language-support/src/syntaxValidation/syntaxValidation.ts index 178e20335..1196eb528 100644 --- a/packages/language-support/src/syntaxValidation/syntaxValidation.ts +++ b/packages/language-support/src/syntaxValidation/syntaxValidation.ts @@ -292,6 +292,7 @@ export function lintCypherQuery( const { notifications, errors } = wrappedSemanticAnalysis( cmd.statement, dbSchema, + current.cypherVersion, ); // This contains both the syntax and the semantic errors diff --git a/packages/language-support/src/tests/syntaxValidation/semanticValidation.test.ts b/packages/language-support/src/tests/syntaxValidation/semanticValidation.test.ts index ed3efedaf..d389e8e12 100644 --- a/packages/language-support/src/tests/syntaxValidation/semanticValidation.test.ts +++ b/packages/language-support/src/tests/syntaxValidation/semanticValidation.test.ts @@ -3,6 +3,217 @@ import { testData } from '../testData'; import { getDiagnosticsForQuery } from './helpers'; describe('Semantic validation spec', () => { + 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', () => { const query = 'METCH (n) RETURN m'; diff --git a/packages/language-support/src/types.ts b/packages/language-support/src/types.ts index 516d8ebc0..dd0060d8b 100644 --- a/packages/language-support/src/types.ts +++ b/packages/language-support/src/types.ts @@ -7,7 +7,7 @@ export type ReturnDescription = { isDeprecated: boolean; }; -export type CypherVersion = 'cypher 25' | 'cypher 5'; +export type CypherVersion = 'CYPHER 25' | 'CYPHER 5'; // we could parse this string for better types in the future export type Neo4jStringType = string; diff --git a/packages/schema-poller/src/metadataPoller.ts b/packages/schema-poller/src/metadataPoller.ts index 4857a50d5..01af47443 100644 --- a/packages/schema-poller/src/metadataPoller.ts +++ b/packages/schema-poller/src/metadataPoller.ts @@ -105,6 +105,13 @@ export class MetadataPoller { this.dbSchema.aliasNames = result.data.databases.flatMap( (db) => db.aliases ?? [], ); + const dbs = result.data.databases; + const currentDb = dbs.find( + (db) => db.name === this.connection.currentDb, + ); + if (currentDb) { + this.dbSchema.defaultLanguage = currentDb.defaultLanguage; + } } }, }); diff --git a/packages/schema-poller/src/queries/databases.ts b/packages/schema-poller/src/queries/databases.ts index be078804f..d27892df3 100644 --- a/packages/schema-poller/src/queries/databases.ts +++ b/packages/schema-poller/src/queries/databases.ts @@ -1,3 +1,4 @@ +import { CypherVersion } from '@neo4j-cypher/language-support/dist/types/types'; import { resultTransformers } from 'neo4j-driver'; import { ExecuteQueryArgs } from '../types/sdkTypes'; @@ -30,6 +31,7 @@ export type Database = { writer?: boolean; access?: string; constituents?: string[]; + defaultLanguage?: CypherVersion; }; /**