Skip to content

Commit 075c464

Browse files
authored
Schema based linting (#681)
1 parent da0335f commit 075c464

9 files changed

Lines changed: 1305 additions & 168 deletions

File tree

packages/language-server/src/server.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -244,14 +244,8 @@ connection.onNotification(
244244
version: number;
245245
schema: DbSchema;
246246
}) => {
247-
neo4jSchemaPoller.events.once(
248-
'schemaFetched',
249-
// oxlint-disable-next-line typescript-eslint/no-meaningless-void-operator
250-
void symbolFetcher.queueSymbolJob(
251-
params.query,
252-
params.uri,
253-
params.schema,
254-
),
247+
neo4jSchemaPoller.events.once('schemaFetched', () =>
248+
symbolFetcher.queueSymbolJob(params.query, params.uri, params.schema),
255249
);
256250
},
257251
);

packages/language-support/src/autocompletion/completionCoreCompletions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,11 @@ const parameterCompletions = (
307307
? { insertText: `$${backtickedName}` }
308308
: {};
309309
// If there is a preceding token and it's not empty, compute the suffix
310-
if (previousToken.type !== CypherLexer.SPACE) {
310+
if (previousToken && tokens && previousToken.type !== CypherLexer.SPACE) {
311311
const param = maybeInsertText.insertText ?? `$${paramName}`;
312312
const hasDollar =
313313
previousToken.type === CypherLexer.DOLLAR ||
314-
tokens.at(previousToken.tokenIndex - 1).type === CypherLexer.DOLLAR;
314+
tokens.at(previousToken.tokenIndex - 1)?.type === CypherLexer.DOLLAR;
315315
// If the $ symbol is already there, we need to have the insert
316316
// text without the starting $ in VSCode, otherwise when we have
317317
// 'RETURN $' and we get offered $param we would complete

packages/language-support/src/autocompletion/schemaBasedCompletions.ts

Lines changed: 24 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@ import {
55
} from 'vscode-languageserver-types';
66
import { DbSchema } from '../dbSchema.js';
77
import { ParsedStatement } from '../cypherLanguageService.js';
8-
import {
9-
ConditionNode,
10-
isLabelLeaf,
11-
LabelLeaf,
12-
LabelOrCondition,
13-
SymbolsInfo,
14-
} from '../types.js';
8+
import { isLabelLeaf, LabelOrCondition, SymbolsInfo } from '../types.js';
159
import { findParent } from '../helpers.js';
1610
import {
1711
NodePatternContext,
@@ -26,6 +20,11 @@ import {
2620
isNotAnyNode,
2721
removeInnerAnys,
2822
} from '../labelTreeRewriting.js';
23+
import {
24+
getNodesFromRelsSet,
25+
getRelsFromNodesSets,
26+
walkCNFTree,
27+
} from '../labelTreeWalking.js';
2928

3029
export function getShortPathCompletions(
3130
lastNode: RelationshipPatternContext,
@@ -202,120 +201,6 @@ const reltypesToCompletions = (reltypes: string[] = []) =>
202201
export const allReltypeCompletions = (dbSchema: DbSchema) =>
203202
reltypesToCompletions(dbSchema.relationshipTypes);
204203

205-
function walkCNFTree(
206-
incomingLabels: Map<string, Set<string>>,
207-
outGoingLabels: Map<string, Set<string>>,
208-
labelTree: LabelOrCondition,
209-
): { inLabels: Set<string>; outLabels: Set<string> } {
210-
//Bail if tree is not CNF
211-
if (isLabelLeaf(labelTree) || labelTree.condition !== 'and') {
212-
let inLabels = new Set<string>();
213-
let outLabels = new Set<string>();
214-
incomingLabels.values().forEach((x) => (inLabels = inLabels.union(x)));
215-
outGoingLabels.values().forEach((x) => (outLabels = outLabels.union(x)));
216-
217-
return { inLabels, outLabels };
218-
}
219-
const notLabels: LabelLeaf[] = [];
220-
const literalLabels: LabelLeaf[] = [];
221-
const orNodes: ConditionNode[] = [];
222-
223-
labelTree.children.forEach((c) => {
224-
if (isLabelLeaf(c)) {
225-
literalLabels.push(c);
226-
} else if (
227-
c.condition === 'not' &&
228-
c.children.length === 1 &&
229-
isLabelLeaf(c.children[0])
230-
) {
231-
notLabels.push(c.children[0]);
232-
} else if (c.condition === 'or') {
233-
orNodes.push(c);
234-
}
235-
});
236-
237-
let inLabels = new Set<string>();
238-
incomingLabels.forEach((part, key) => {
239-
if (!notLabels.some((c) => c.value === key)) {
240-
inLabels = inLabels.union(part);
241-
}
242-
});
243-
let outLabels = new Set<string>();
244-
outGoingLabels.forEach((part, key) => {
245-
if (!notLabels.some((c) => c.value === key)) {
246-
outLabels = outLabels.union(part);
247-
}
248-
});
249-
250-
for (const label of literalLabels) {
251-
const incoming = incomingLabels.get(label.value) ?? new Set();
252-
const outgoing = outGoingLabels.get(label.value) ?? new Set();
253-
inLabels = inLabels.intersection(incoming);
254-
outLabels = outLabels.intersection(outgoing);
255-
}
256-
257-
for (const node of orNodes) {
258-
let incoming = new Set();
259-
let outGoing = new Set();
260-
for (const c of node.children) {
261-
if (isLabelLeaf(c)) {
262-
const newIncoming = incomingLabels.get(c.value) ?? new Set();
263-
const newOutgoing = outGoingLabels.get(c.value) ?? new Set();
264-
incoming = incoming.union(newIncoming);
265-
outGoing = outGoing.union(newOutgoing);
266-
}
267-
}
268-
inLabels = inLabels.intersection(incoming);
269-
outLabels = outLabels.intersection(outGoing);
270-
}
271-
return { inLabels, outLabels };
272-
}
273-
274-
function getRelsFromNodesSets(dbSchema: DbSchema): {
275-
toNodes: Map<string, Set<string>>;
276-
fromNodes: Map<string, Set<string>>;
277-
} {
278-
if (dbSchema.graphSchema) {
279-
const toNodes: Map<string, Set<string>> = new Map();
280-
const fromNodes: Map<string, Set<string>> = new Map();
281-
dbSchema.graphSchema.forEach((rel) => {
282-
//rels in schema defined like (from)-(relType)->(to)
283-
//Means 'from' is "node going to rel", hence why we
284-
//pass rel.from into toNodes
285-
if (!toNodes.has(rel.from)) {
286-
toNodes.set(rel.from, new Set());
287-
}
288-
if (!fromNodes.has(rel.to)) {
289-
fromNodes.set(rel.to, new Set());
290-
}
291-
toNodes.get(rel.from).add(rel.relType);
292-
fromNodes.get(rel.to).add(rel.relType);
293-
});
294-
return { toNodes, fromNodes };
295-
}
296-
return undefined;
297-
}
298-
299-
function getNodesFromRelsSet(dbSchema: DbSchema): {
300-
toRels: Map<string, Set<string>>;
301-
fromRels: Map<string, Set<string>>;
302-
} {
303-
if (dbSchema.graphSchema) {
304-
const toRels: Map<string, Set<string>> = new Map();
305-
const fromRels: Map<string, Set<string>> = new Map();
306-
dbSchema.graphSchema.forEach((rel) => {
307-
if (!toRels.has(rel.relType)) {
308-
toRels.set(rel.relType, new Set());
309-
fromRels.set(rel.relType, new Set());
310-
}
311-
toRels.get(rel.relType).add(rel.to);
312-
fromRels.get(rel.relType).add(rel.from);
313-
});
314-
return { toRels, fromRels };
315-
}
316-
return undefined;
317-
}
318-
319204
function findLastVariable(
320205
lastValidElement: NodePatternContext | RelationshipPatternContext,
321206
symbolsInfo: SymbolsInfo,
@@ -328,7 +213,7 @@ function findLastVariable(
328213
// Because the anonymous variable created in the AST is "made up", it doesnt have a position of its own in the query.
329214
// It therefor inherits the parent-nodes (The NodePattern/RelationshipPattern = lastValidElement) position
330215
entry.definitionPosition >= lastValidElement.start.start &&
331-
entry.definitionPosition <= lastValidElement.stop.stop,
216+
entry.definitionPosition <= (lastValidElement.stop?.stop ?? -1),
332217
)
333218
: symbolsInfo?.symbolTables
334219
?.flat()
@@ -350,7 +235,7 @@ export function completeNodeLabel(
350235
(x) => x instanceof PatternElementContext,
351236
);
352237

353-
if (callContext instanceof PatternElementContext) {
238+
if (callContext instanceof PatternElementContext && callContext.children) {
354239
const lastValidElement = callContext.children.toReversed().find((child) => {
355240
if (child instanceof RelationshipPatternContext) {
356241
if (child.exception === null) {
@@ -375,11 +260,12 @@ export function completeNodeLabel(
375260
return allLabelCompletions(dbSchema);
376261
}
377262

378-
const direction = lastValidElement.leftArrow()
379-
? 'outgoing'
380-
: lastValidElement.rightArrow()
381-
? 'incoming'
382-
: 'bidirectional';
263+
const direction =
264+
lastValidElement.leftArrow() && !lastValidElement.rightArrow()
265+
? 'outgoing'
266+
: !lastValidElement.leftArrow() && lastValidElement.rightArrow()
267+
? 'incoming'
268+
: 'bidirectional';
383269

384270
// limitation: not checking node label repetition
385271
const { toRels: nodesToRelsSet, fromRels: nodesFromRelsSet } =
@@ -396,14 +282,6 @@ export function completeNodeLabel(
396282
} catch {
397283
return allLabelCompletions(dbSchema);
398284
}
399-
let allIncomingLabels = new Set<string>();
400-
nodesToRelsSet.forEach((part) => {
401-
allIncomingLabels = allIncomingLabels.union(part);
402-
});
403-
let allOutGoingLabels = new Set<string>();
404-
nodesFromRelsSet.forEach((part) => {
405-
allOutGoingLabels = allOutGoingLabels.union(part);
406-
});
407285
const { inLabels, outLabels } = walkCNFTree(
408286
nodesToRelsSet,
409287
nodesFromRelsSet,
@@ -438,7 +316,10 @@ export function completeRelationshipType(
438316
(x) => x instanceof PatternElementContext,
439317
);
440318

441-
if (patternContext instanceof PatternElementContext) {
319+
if (
320+
patternContext instanceof PatternElementContext &&
321+
patternContext.children
322+
) {
442323
const lastValidElement = patternContext.children
443324
.toReversed()
444325
.find((child) => {
@@ -455,11 +336,12 @@ export function completeRelationshipType(
455336
);
456337
let direction = 'bidirectional';
457338
if (thisCtx instanceof RelationshipPatternContext) {
458-
direction = thisCtx.leftArrow()
459-
? 'outgoing'
460-
: thisCtx.rightArrow()
461-
? 'incoming'
462-
: 'bidirectional';
339+
direction =
340+
thisCtx.leftArrow() && !thisCtx.rightArrow()
341+
? 'outgoing'
342+
: !thisCtx.leftArrow() && thisCtx.rightArrow()
343+
? 'incoming'
344+
: 'bidirectional';
463345
}
464346

465347
// limitation: bailing out on quantifiers
@@ -493,14 +375,6 @@ export function completeRelationshipType(
493375
} catch {
494376
return [];
495377
}
496-
let allIncomingLabels = new Set<string>();
497-
relsToNodesSet.forEach((part) => {
498-
allIncomingLabels = allIncomingLabels.union(part);
499-
});
500-
let allOutGoingLabels = new Set<string>();
501-
relsFromNodesSet.forEach((part) => {
502-
allOutGoingLabels = allOutGoingLabels.union(part);
503-
});
504378
const { inLabels, outLabels } = walkCNFTree(
505379
relsToNodesSet,
506380
relsFromNodesSet,

packages/language-support/src/cypherLanguageService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ function parseToCommand(
802802
return { type: 'parse-error', start: stmts.start, stop: stmts.stop };
803803
}
804804

805-
function translateTokensToRange(
805+
export function translateTokensToRange(
806806
start: Token,
807807
stop: Token,
808808
): Pick<SyntaxDiagnostic, 'range' | 'offsets'> {

packages/language-support/src/labelTreeRewriting.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export function childAlreadyExists(
134134
}
135135
}
136136

137-
function equalConditions(c1: LabelOrCondition, c2: LabelOrCondition) {
137+
function equalConditions(c1: LabelOrCondition, c2: LabelOrCondition): boolean {
138138
if (isLabelLeaf(c1)) {
139139
return isLabelLeaf(c2) && c1.value === c2.value;
140140
} else if (isLabelLeaf(c2)) {

0 commit comments

Comments
 (0)