UNPKG

@neo4j-cypher/language-support

Version:
237 lines (206 loc) 6.52 kB
import { DiagnosticSeverity, Position } from 'vscode-languageserver-types'; import { ParserRuleContext, ParseTree, Token } from 'antlr4'; import { DbSchema } from '../dbSchema'; import { LabelOrRelType, LabelType, ParsedStatement, parserWrapper, } from '../parserWrapper'; import { SemanticAnalysisElement, wrappedSemanticAnalysis, } from './semanticAnalysisWrapper'; import { SyntaxDiagnostic } from './syntaxValidationHelpers'; function detectNonDeclaredLabel( labelOrRelType: LabelOrRelType, dbLabels: Set<string>, dbRelationshipTypes: Set<string>, ): SyntaxDiagnostic | undefined { const labelName = labelOrRelType.labelText; const notInDatabase = (labelOrRelType.labeltype === LabelType.nodeLabelType && !dbLabels.has(labelName)) || (labelOrRelType.labeltype === LabelType.relLabelType && !dbRelationshipTypes.has(labelName)) || (!dbLabels.has(labelName) && !dbRelationshipTypes.has(labelName)); 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; } return undefined; } function warnOnUndeclaredLabels( parsingResult: ParsedStatement, dbSchema: DbSchema, ): SyntaxDiagnostic[] { const warnings: SyntaxDiagnostic[] = []; if (dbSchema.labels && dbSchema.relationshipTypes) { const dbLabels = new Set(dbSchema.labels); const dbRelationshipTypes = new Set(dbSchema.relationshipTypes); const labelsAndRelTypes = parsingResult.collectedLabelOrRelTypes; labelsAndRelTypes.forEach((labelOrRelType) => { const warning = detectNonDeclaredLabel( labelOrRelType, dbLabels, dbRelationshipTypes, ); if (warning) warnings.push(warning); }); } return warnings; } type FixSemanticPositionsArgs = { semanticElements: SemanticAnalysisElement[]; parseResult: ParsedStatement; }; function fixSemanticAnalysisPositions({ semanticElements, parseResult, }: FixSemanticPositionsArgs): SyntaxDiagnostic[] { const cmd = parseResult.command; return semanticElements.map((e) => { let token: Token | undefined = undefined; const start = Position.create( e.position.line - 1 + cmd.start.line - 1, e.position.column - 1 + (e.position.line === 1 ? cmd.start.column : 0), ); const startOffset = e.position.offset + cmd.start.start; const toExplore: ParseTree[] = [parseResult.ctx]; while (toExplore.length > 0) { const current: ParseTree = toExplore.pop(); 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, }, }; } }); } 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( query: string, dbSchema: DbSchema, ): SyntaxDiagnostic[] { const syntaxErrors = validateSyntax(query, dbSchema); if (syntaxErrors.length > 0) { return syntaxErrors; } const semanticErrors = validateSemantics(query, dbSchema); return semanticErrors; } export function validateSyntax( query: string, dbSchema: DbSchema, ): SyntaxDiagnostic[] { if (query.length === 0) { return []; } const statements = parserWrapper.parse(query); const result = statements.statementsParsing.flatMap((statement) => { const diagnostics = statement.diagnostics; const labelWarnings = warnOnUndeclaredLabels(statement, dbSchema); return diagnostics.concat(labelWarnings).sort(sortByPositionAndMessage); }); return result; } /** * Assumes the provided query has no parse errors */ export function validateSemantics( query: string, dbSchema: DbSchema, ): SyntaxDiagnostic[] { if (query.length > 0) { const cachedParse = parserWrapper.parse(query); const statements = cachedParse.statementsParsing; const semanticErrors = statements.flatMap((current) => { if (current.diagnostics.length === 0) { const cmd = current.command; if (cmd.type === 'cypher' && cmd.statement.length > 0) { const { notifications, errors } = wrappedSemanticAnalysis( cmd.statement, dbSchema, ); const elements = notifications.concat(errors); const result = fixSemanticAnalysisPositions({ semanticElements: elements, parseResult: current, }).sort(sortByPositionAndMessage); return result; } } return []; }); return semanticErrors; } return []; }