Skip to content

Commit

Permalink
Wires semantic analysis for procedures and functions (#175)
Browse files Browse the repository at this point in the history
  • Loading branch information
ncordon authored Mar 5, 2024
1 parent 2cd3281 commit 96ab47c
Show file tree
Hide file tree
Showing 26 changed files with 49,079 additions and 5,221 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
],
"outFiles": [
"${workspaceRoot}/packages/vscode-extension/**/*.js",
"${workspaceRoot}/packages/language-support/**/*.cjs",
"${workspaceRoot}/packages/schema-poller/**/*.cjs"
"${workspaceRoot}/packages/language-support/**/*.js",
"${workspaceRoot}/packages/schema-poller/**/*.js"
],
"autoAttachChildProcesses": true,
"sourceMaps": true
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions packages/language-server/src/linting.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { validateSyntax } from '@neo4j-cypher/language-support';
import { Neo4jSchemaPoller } from '@neo4j-cypher/schema-poller';
import debounce from 'lodash.debounce';
import { join } from 'path';
import { Diagnostic, TextDocumentChangeEvent } from 'vscode-languageserver';
Expand All @@ -16,6 +17,7 @@ let lastSemanticJob: LinterTask | undefined;
async function rawLintDocument(
change: TextDocumentChangeEvent<TextDocument>,
sendDiagnostics: (diagnostics: Diagnostic[]) => void,
neo4j: Neo4jSchemaPoller,
) {
const { document } = change;

Expand All @@ -25,7 +27,8 @@ async function rawLintDocument(
return;
}

const syntaxErrors = validateSyntax(query, {});
const dbSchema = neo4j.metadata?.dbSchema ?? {};
const syntaxErrors = validateSyntax(query, dbSchema);

sendDiagnostics(syntaxErrors);

Expand All @@ -36,7 +39,7 @@ async function rawLintDocument(
}

const proxyWorker = (await pool.proxy()) as unknown as LintWorker;
lastSemanticJob = proxyWorker.validateSemantics(query);
lastSemanticJob = proxyWorker.validateSemantics(query, dbSchema);
const result = await lastSemanticJob;

sendDiagnostics(result);
Expand Down
28 changes: 16 additions & 12 deletions packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { cleanupWorkers, lintDocument } from './linting';
// Create a simple text document manager.
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

const neo4jSdk = new Neo4jSchemaPoller();
const neo4jSchemaPoller = new Neo4jSchemaPoller();

connection.onInitialize(() => {
const result: InitializeResult = {
Expand Down Expand Up @@ -74,12 +74,16 @@ connection.onInitialized(() => {
});

documents.onDidChangeContent((change) =>
lintDocument(change, (diagnostics: Diagnostic[]) => {
void connection.sendDiagnostics({
uri: change.document.uri,
diagnostics,
});
}),
lintDocument(
change,
(diagnostics: Diagnostic[]) => {
void connection.sendDiagnostics({
uri: change.document.uri,
diagnostics,
});
},
neo4jSchemaPoller,
),
);

// Trigger the syntax colouring
Expand All @@ -88,23 +92,23 @@ connection.languages.semanticTokens.on(
);

// Trigger the signature help, providing info about functions / procedures
connection.onSignatureHelp(doSignatureHelp(documents, neo4jSdk));
connection.onSignatureHelp(doSignatureHelp(documents, neo4jSchemaPoller));
// Trigger the auto completion
connection.onCompletion(doAutoCompletion(documents, neo4jSdk));
connection.onCompletion(doAutoCompletion(documents, neo4jSchemaPoller));

connection.onDidChangeConfiguration(
(params: { settings: { neo4j: Neo4jSettings } }) => {
neo4jSdk.disconnect();
neo4jSchemaPoller.disconnect();

const neo4jConfig = params.settings.neo4j;
if (
neo4jSdk.connection === undefined &&
neo4jSchemaPoller.connection === undefined &&
neo4jConfig.connect &&
neo4jConfig.password &&
neo4jConfig.connectURL &&
neo4jConfig.user
) {
void neo4jSdk.persistentConnect(
void neo4jSchemaPoller.persistentConnect(
neo4jConfig.connectURL,
{
username: neo4jConfig.user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const functionNameCompletions = (
namespacedCompletion(
candidateRule,
tokens,
Object.keys(dbSchema?.functionSignatures ?? {}),
Object.keys(dbSchema?.functions ?? {}),
'function',
);

Expand All @@ -56,7 +56,7 @@ const procedureNameCompletions = (
namespacedCompletion(
candidateRule,
tokens,
Object.keys(dbSchema?.procedureSignatures ?? {}),
Object.keys(dbSchema?.procedures ?? {}),
'procedure',
);

Expand Down
6 changes: 3 additions & 3 deletions packages/language-support/src/dbSchema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { SignatureInformation } from 'vscode-languageserver-types';
import { Neo4jFunction, Neo4jProcedure } from './types';

export interface DbSchema {
procedureSignatures?: Record<string, SignatureInformation>;
functionSignatures?: Record<string, SignatureInformation>;
labels?: string[];
relationshipTypes?: string[];
databaseNames?: string[];
aliasNames?: string[];
parameters?: Record<string, unknown>;
propertyKeys?: string[];
procedures?: Record<string, Neo4jProcedure>;
functions?: Record<string, Neo4jFunction>;
}
3 changes: 2 additions & 1 deletion packages/language-support/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export {
parserWrapper,
setConsoleCommandsEnabled,
} from './parserWrapper';
export { signatureHelp } from './signatureHelp';
export { signatureHelp, toSignatureInformation } from './signatureHelp';
export {
applySyntaxColouring,
mapCypherToSemanticTokenIndex,
Expand All @@ -22,6 +22,7 @@ export {
} from './syntaxValidation/syntaxValidation';
export type { SyntaxDiagnostic } from './syntaxValidation/syntaxValidationHelpers';
export { testData } from './tests/testData';
export type { Neo4jFunction, Neo4jProcedure } from './types';
export { CypherLexer, CypherParser };

import CypherLexer from './generated-parser/CypherCmdLexer';
Expand Down
26 changes: 21 additions & 5 deletions packages/language-support/src/signatureHelp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DbSchema } from './dbSchema';
import CypherCmdParserListener from './generated-parser/CypherCmdParserListener';
import { findCaret, isDefined } from './helpers';
import { parserWrapper } from './parserWrapper';
import { Neo4jFunction, Neo4jProcedure } from './types';

export const emptyResult: SignatureHelp = {
signatures: [],
Expand All @@ -32,13 +33,28 @@ interface ParsedMethod {
methodType: MethodType;
}

export function toSignatureInformation(
curr: Neo4jFunction | Neo4jProcedure,
): SignatureInformation {
const { name, argumentDescription, description } = curr;

return SignatureInformation.create(
name,
description,
...argumentDescription.map((arg) => ({
label: arg.name,
documentation: arg.description,
})),
);
}

function toSignatureHelp(
methodSignatures: Record<string, SignatureInformation> = {},
methodSignatures: Record<string, Neo4jFunction | Neo4jProcedure> = {},
parsedMethod: ParsedMethod,
) {
): SignatureHelp {
const methodName = parsedMethod.methodName;
const method = methodSignatures[methodName];
const signatures = method ? [method] : [];
const signatures = method ? [toSignatureInformation(method)] : [];

const signatureHelp: SignatureHelp = {
signatures: signatures,
Expand Down Expand Up @@ -165,9 +181,9 @@ export function signatureHelp(

if (method !== undefined) {
if (method.methodType === MethodType.function) {
result = toSignatureHelp(dbSchema.functionSignatures, method);
result = toSignatureHelp(dbSchema.functions, method);
} else {
result = toSignatureHelp(dbSchema.procedureSignatures, method);
result = toSignatureHelp(dbSchema.procedures, method);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */

import { DiagnosticSeverity } from 'vscode-languageserver-types';
import { DbSchema } from '../dbSchema';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { semanticAnalysis } from './semanticAnalysis';
import { semanticAnalysis, updateSignatureResolver } from './semanticAnalysis';

export interface SemanticAnalysisResult {
errors: SemanticAnalysisElement[];
Expand All @@ -26,9 +27,19 @@ type SemanticAnalysisElementNoSeverity = Omit<
'severity'
>;

export function wrappedSemanticAnalysis(query: string): SemanticAnalysisResult {
export function wrappedSemanticAnalysis(
query: string,
dbSchema: DbSchema,
): SemanticAnalysisResult {
try {
let semanticErrorsResult = undefined;

if (dbSchema.functions && dbSchema.procedures) {
updateSignatureResolver({
procedures: Object.values(dbSchema.procedures),
functions: Object.values(dbSchema.functions),
});
}
semanticAnalysis([query], (a) => {
semanticErrorsResult = a;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export function lintCypherQuery(
return syntaxErrors;
}

const semanticErrors = validateSemantics(query);
const semanticErrors = validateSemantics(query, dbSchema);
return semanticErrors;
}

Expand All @@ -193,7 +193,10 @@ export function validateSyntax(
/**
* Assumes the provided query has no parse errors
*/
export function validateSemantics(query: string): SyntaxDiagnostic[] {
export function validateSemantics(
query: string,
dbSchema: DbSchema,
): SyntaxDiagnostic[] {
if (query.length > 0) {
const cachedParse = parserWrapper.parse(query);
const statements = cachedParse.statementsParsing;
Expand All @@ -203,6 +206,7 @@ export function validateSemantics(query: string): SyntaxDiagnostic[] {
if (cmd.type === 'cypher' && cmd.statement.length > 0) {
const { notifications, errors } = wrappedSemanticAnalysis(
cmd.statement,
dbSchema,
);

const elements = notifications.concat(errors);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,10 @@
import { CompletionItemKind } from 'vscode-languageserver-types';
import { DbSchema } from '../../dbSchema';
import { testData } from '../testData';
import { testCompletions } from './completionAssertionHelpers';

describe('function invocations', () => {
const dbSchema: DbSchema = {
functionSignatures: {
abs: { label: 'abs' },
acos: { label: 'acos' },
all: { label: 'all' },
any: { label: 'any' },
'apoc.agg.first': { label: 'apoc.agg.first' },
'apoc.agg.graph': { label: 'apoc.agg.graph' },
'apoc.agg.last': { label: 'apoc.agg.last' },
'apoc.agg.maxItems': { label: 'apoc.agg.maxItems' },
'apoc.agg.median': { label: 'apoc.agg.median' },
'apoc.agg.minItems': { label: 'apoc.agg.minItems' },
'apoc.agg.nth': { label: 'apoc.agg.nth' },
'apoc.agg.percentiles': { label: 'apoc.agg.percentiles' },
'apoc.agg.product': { label: 'apoc.agg.product' },
'apoc.agg.slice': { label: 'apoc.agg.slice' },
'apoc.agg.statistics': { label: 'apoc.agg.statistics' },
'apoc.any.isDeleted': { label: 'apoc.any.isDeleted' },
'apoc.any.properties': { label: 'apoc.any.properties' },
'apoc.any.property': { label: 'apoc.any.property' },
'apoc.any.rebind': { label: 'apoc.any.rebind' },
'apoc.bitwise.op': { label: 'apoc.bitwise.op' },
'apoc.coll.avg': { label: 'apoc.coll.avg' },
'apoc.coll.avgDuration': { label: 'apoc.coll.avgDuration' },
'apoc.coll.combinations': { label: 'apoc.coll.combinations' },
'apoc.coll.contains': { label: 'apoc.coll.contains' },
'apoc.coll.containsAll': { label: 'apoc.coll.containsAll' },
'apoc.coll.containsAllSorted': { label: 'apoc.coll.containsAllSorted' },
'apoc.coll.containsDuplicates': { label: 'apoc.coll.containsDuplicates' },
'apoc.coll.containsSorted': { label: 'apoc.coll.containsSorted' },
'apoc.coll.duplicates': { label: 'apoc.coll.duplicates' },
'apoc.coll.fill': { label: 'apoc.coll.fill' },
'apoc.coll.flatten': { label: 'apoc.coll.flatten' },
'apoc.coll.frequencies': { label: 'apoc.coll.frequencies' },
'apoc.coll.frequenciesAsMap': { label: 'apoc.coll.frequenciesAsMap' },
'apoc.coll.indexOf': { label: 'apoc.coll.indexOf' },
'apoc.coll.insert': { label: 'apoc.coll.insert' },
'apoc.coll.insertAll': { label: 'apoc.coll.insertAll' },
'apoc.coll.intersection': { label: 'apoc.coll.intersection' },
'apoc.coll.isEqualCollection': { label: 'apoc.coll.isEqualCollection' },
'apoc.coll.max': { label: 'apoc.coll.max' },
'apoc.coll.min': { label: 'apoc.coll.min' },
'apoc.coll.occurrences': { label: 'apoc.coll.occurrences' },
'apoc.coll.pairs': { label: 'apoc.coll.pairs' },
},
};
const dbSchema: DbSchema = testData.mockSchema;

test('Correctly completes unstarted function name in left hand side of WHERE', () => {
const query = 'MATCH (n) WHERE ';
Expand Down Expand Up @@ -340,9 +296,9 @@ describe('function invocations', () => {
testCompletions({
query,
dbSchema: {
functionSignatures: {
math: { label: 'math' },
'math.max': { label: 'math.max' },
functions: {
math: { ...testData.emptyFunction, name: 'math' },
'math.max': { ...testData.emptyFunction, name: 'math.max' },
},
},
expected: [
Expand Down
Loading

0 comments on commit 96ab47c

Please sign in to comment.