Skip to content

Download lint worker #521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
4 changes: 3 additions & 1 deletion packages/language-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@
"neo4j-driver": "catalog:",
"vscode-languageserver": "^8.1.0",
"vscode-languageserver-textdocument": "^1.0.8",
"workerpool": "^9.0.4"
"workerpool": "^9.0.4",
"languageSupport-next.13": "npm:@neo4j-cypher/[email protected]",
"languageSupport-next.3": "npm:@neo4j-cypher/[email protected]"
},
"scripts": {
"build": "tsc -b && pnpm bundle && pnpm make-executable && pnpm copy-lint-worker",
Expand Down
34 changes: 28 additions & 6 deletions packages/language-server/src/linting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,33 @@ import { join } from 'path';
import { Diagnostic } from 'vscode-languageserver';
import { TextDocument } from 'vscode-languageserver-textdocument';
import workerpool from 'workerpool';
import type { LinterTask, LintWorker } from '@neo4j-cypher/lint-worker';
import {
convertDbSchema,
LinterTask,
LintWorker,
} from '@neo4j-cypher/lint-worker';

const pool = workerpool.pool(join(__dirname, 'lintWorker.cjs'), {
let pool = workerpool.pool(join(__dirname, 'lintWorker.cjs'), {
minWorkers: 2,
workerTerminateTimeout: 2000,
});

const defaultWorkerPath = join(__dirname, 'lintWorker.cjs');
export let workerPath = defaultWorkerPath;
let lastSemanticJob: LinterTask | undefined;

/**Sets the lintworker to the one specified by the given path, reverting to default if the path is undefined */
export async function setLintWorker(lintWorkerPath: string | undefined) {
lintWorkerPath = lintWorkerPath ? lintWorkerPath : defaultWorkerPath;
if (lintWorkerPath !== workerPath) {
await cleanupWorkers();
workerPath = lintWorkerPath;
pool = workerpool.pool(workerPath, {
minWorkers: 2,
workerTerminateTimeout: 2000,
});
}
}

async function rawLintDocument(
document: TextDocument,
sendDiagnostics: (diagnostics: Diagnostic[]) => void,
Expand All @@ -32,9 +50,13 @@ async function rawLintDocument(
}

const proxyWorker = (await pool.proxy()) as unknown as LintWorker;

const fixedDbSchema = _internalFeatureFlags.versionedLinters
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The feature flags should be accessed through the corresponding data structure and propagated here, see this snippet from server.ts to see how the linting feature flag is accessed:

async function lintSingleDocument(document: TextDocument): Promise<void> {
  if (settings?.features?.linting) {
    return lintDocument(

? await convertDbSchema(dbSchema, neo4j)
: dbSchema;
lastSemanticJob = proxyWorker.lintCypherQuery(
query,
dbSchema,
fixedDbSchema,
_internalFeatureFlags,
);
const result = await lastSemanticJob;
Expand All @@ -56,6 +78,6 @@ export const lintDocument: typeof rawLintDocument = debounce(
},
);

export const cleanupWorkers = () => {
void pool.terminate();
export const cleanupWorkers = async () => {
await pool.terminate();
};
18 changes: 15 additions & 3 deletions packages/language-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import {
} from 'vscode-languageserver/node';

import { TextDocument } from 'vscode-languageserver-textdocument';

import {
_internalFeatureFlags,
syntaxColouringLegend,
} from '@neo4j-cypher/language-support';
import { Neo4jSchemaPoller } from '@neo4j-cypher/query-tools';
import { doAutoCompletion } from './autocompletion';
import { formatDocument } from './formatting';
import { cleanupWorkers, lintDocument } from './linting';
import { cleanupWorkers, lintDocument, setLintWorker } from './linting';
import { doSignatureHelp } from './signatureHelp';
import { applySyntaxColouringForDocument } from './syntaxColouring';
import {
Expand Down Expand Up @@ -127,9 +126,22 @@ connection.onSignatureHelp(doSignatureHelp(documents, neo4jSchemaPoller));
// Trigger the auto completion
connection.onCompletion(doAutoCompletion(documents, neo4jSchemaPoller));

connection.onNotification(
'updateLintWorker',
(connectionSettings: Neo4jConnectionSettings) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't reuse this type of message, it should be kept for connections only, we should create a linter specific structure:

export type LintWorkerSettings = {
   lintWorkerPath: string
}

const lintWorkerPath = connectionSettings.lintWorkerPath;
_internalFeatureFlags.versionedLinters = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed

void (async () => {
await setLintWorker(lintWorkerPath);
relintAllDocuments();
})();
},
);

connection.onNotification(
'connectionUpdated',
(connectionSettings: Neo4jConnectionSettings) => {
_internalFeatureFlags.versionedLinters = false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed

changeConnection(connectionSettings);
neo4jSchemaPoller.events.once('schemaFetched', relintAllDocuments);
},
Expand All @@ -154,7 +166,7 @@ documents.listen(connection);
connection.listen();

connection.onExit(() => {
cleanupWorkers();
void cleanupWorkers();
});

const changeConnection = (connectionSettings: Neo4jConnectionSettings) => {
Expand Down
1 change: 1 addition & 0 deletions packages/language-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type Neo4jConnectionSettings = {
password?: string;
connectURL?: string;
database?: string;
lintWorkerPath?: string;
};

export type Neo4jSettings = {
Expand Down
2 changes: 2 additions & 0 deletions packages/language-support/src/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
interface FeatureFlags {
consoleCommands: boolean;
cypher25: boolean;
versionedLinters: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't needed here, it should be in the Neo4jSettings / features in the language server

}

export const _internalFeatureFlags: FeatureFlags = {
Expand All @@ -13,4 +14,5 @@ export const _internalFeatureFlags: FeatureFlags = {
*/
consoleCommands: false,
cypher25: false,
versionedLinters: false,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be here

};
1 change: 1 addition & 0 deletions packages/language-support/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type {
Neo4jFunction,
Neo4jProcedure,
} from './types';
export { compareVersions, compareMajorMinorVersions } from './version';
export { CypherLexer, CypherParser, CypherParserListener, CypherParserVisitor };

import CypherLexer from './generated-parser/CypherCmdLexer';
Expand Down
73 changes: 73 additions & 0 deletions packages/language-support/src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import semver from 'semver';
import { integer } from 'vscode-languageserver-types';

/**Like semver.compare - Returns:
* - -1 if v1 < v2
* - 0 if v1 === v2
* - 1 if v1 > v2
* But ignores patch version,
* and returns undefined if versions are of incorrect format */
export function compareMajorMinorVersions(
version1: string,
version2: string,
): integer | undefined {
Comment on lines +10 to +13
Copy link
Collaborator

@ncordon ncordon Jun 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also any reason why we exposing this in the language support and not have in the lint worker package?

We should probably have tests for this as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason anymore - moving it and adding tests

let semVer1: semver.SemVer;
let semVer2: semver.SemVer;
try {
semVer1 = semver.coerce(version1, {
includePrerelease: false,
});
semVer2 = semver.coerce(version2, {
includePrerelease: false,
});
} catch (e) {
return undefined;
}
if (semVer1 && semVer2) {
semVer1.patch = 0;
semVer2.patch = 0;
return semver.compare(semVer1, semVer2);
}
}

/**Like semver.compare - Returns:
* - -1 if v1 < v2
* - 0 if v1 === v2
* - 1 if v1 > v2
* But if versions are of incorrect format, returns undefined
*
* If major- minor- and patch versions are equal,
* compares prerelease versions too, provided that they are numerical. */
export function compareVersions(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not used anywhere

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, a remnant of a bygone age

version1: string,
version2: string,
): integer | undefined {
let semVer1: semver.SemVer;
let semVer2: semver.SemVer;
try {
semVer1 = semver.coerce(version1, {
includePrerelease: false,
});
semVer2 = semver.coerce(version2, {
includePrerelease: false,
});
} catch (e) {
return undefined;
}

if (semVer1 && semVer2) {
const comparison = semver.compare(semVer1, semVer2);
const prerelease1 = semver.prerelease(version1)?.at(0);
const prerelease2 = semver.prerelease(version2)?.at(0);
if (
comparison === 0 &&
typeof prerelease1 === 'number' &&
typeof prerelease2 === 'number'
) {
return semver.compare(prerelease1.toString(), prerelease2.toString());
} else {
return comparison;
}
}
return -1;
}
26 changes: 16 additions & 10 deletions packages/lint-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"cypher",
"lint worker"
],
"version": "2025.4.0",
"version": "2025.7.0",
"repository": {
"type": "git",
"url": "git://github.com/neo4j/cypher-language-support.git"
Expand All @@ -31,25 +31,31 @@
},
"dependencies": {
"@neo4j-cypher/language-support": "workspace:*",
"workerpool": "^9.0.4"
"languageSupport-next.13": "npm:@neo4j-cypher/[email protected]",
"languageSupport-next.3": "npm:@neo4j-cypher/[email protected]",
"@neo4j-cypher/query-tools": "workspace:*",
"workerpool": "^9.0.4",
"vscode-languageserver": "^8.1.0"
},
"scripts": {
"build": "pnpm build-types && pnpm build-esm && pnpm build-commonjs",
"build-commonjs": "esbuild ./src/lintWorker.ts --bundle --platform=node --conditions=require --outfile=dist/cjs/lintWorker.cjs --format=cjs --minify",
"build-esm": "esbuild ./src/lintWorker.ts --bundle --platform=node --conditions=require --outfile=dist/esm/lintWorker.mjs --format=esm --minify",
"build": "pnpm build-types && pnpm build-lintworker-esm && pnpm build-lintworker-commonjs && pnpm build-commonjs && pnpm build-esm",
"build-lintworker-commonjs": "esbuild ./src/lintWorker.ts --bundle --platform=node --conditions=require --outfile=dist/cjs/lintWorker.cjs --format=cjs --minify",
"build-lintworker-esm": "esbuild ./src/lintWorker.ts --bundle --platform=node --conditions=require --outfile=dist/esm/lintWorker.mjs --format=esm --minify",
"build-commonjs": "esbuild ./src/index.ts --bundle --platform=node --conditions=require --outfile=dist/cjs/index.cjs --format=cjs --minify",
"build-esm": "esbuild ./src/index.ts --bundle --platform=node --conditions=require --outfile=dist/esm/index.mjs --format=esm --minify",
"clean": "rm -rf dist",
"build-types": "tsc --emitDeclarationOnly --outDir dist/types"
},
"devDependencies": {
"@types/lodash.debounce": "^4.0.9"
},
"types": "./dist/types/src/lintWorker.d.ts",
"types": "./dist/types/src/index.d.ts",
"exports": {
".": {
"types": "./dist/types/src/lintWorker.d.ts",
"require": "./dist/cjs/lintWorker.cjs",
"import": "./dist/esm/lintWorker.mjs",
"default": "./dist/esm/lintWorker.mjs"
"types": "./dist/esm/index.d.ts",
"require": "./dist/cjs/index.cjs",
"import": "./dist/esm/index.mjs",
"default": "./dist/esm/index.mjs"
}
}
}
Loading
Loading