Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions $shared/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,10 @@ export type ConfigurationSettings = {
nodePath: string | null;
workspaceFolder: WorkspaceFolder | undefined;
workingDirectory: ModeItem | DirectoryItem | undefined;
/** Enables ESLint's result caching. Only effective when {@link run} is `onSave`. */
cache: boolean;
/** Path to the cache file. Defaults to `.eslintcache` in the working directory. Only used when {@link cache} is `true`. */
cacheLocation?: string;
/** Strategy for detecting changed files: `metadata` (faster) or `content`. Only used when {@link cache} is `true`. */
cacheStrategy?: 'metadata' | 'content';
};
3 changes: 3 additions & 0 deletions client/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,9 @@ export namespace ESLintClient {
nodePath: config.get<string | undefined>('nodePath', undefined) ?? null,
workingDirectory: undefined,
workspaceFolder: undefined,
cache: config.get<boolean>('cache', false),
cacheLocation: config.get<string | undefined>('cacheLocation', undefined),
cacheStrategy: config.get<'metadata' | 'content' | undefined>('cacheStrategy', undefined),
codeAction: {
disableRuleComment: config.get<CodeActionSettings['disableRuleComment']>('codeAction.disableRuleComment', { enable: true, location: 'separateLine' as const, commentStyle: 'line' as const }),
showDocumentation: config.get<CodeActionSettings['showDocumentation']>('codeAction.showDocumentation', { enable: true })
Expand Down
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@
"default": {},
"markdownDescription": "The eslint options object to provide args normally passed to eslint when executed from a command line (see https://eslint.org/docs/developer-guide/nodejs-api#eslint-class)."
},
"eslint.cache": {
"scope": "resource",
"type": "boolean",
"default": false,
"markdownDescription": "Enables ESLint's cache when linting on save (`eslint.run` must be `onSave`). The cache file (`.eslintcache`) can be reused by command-line ESLint and pre-commit hooks, improving their performance."
},
"eslint.cacheLocation": {
"scope": "resource",
"type": "string",
"markdownDescription": "Path to the ESLint cache file. If not specified, defaults to `.eslintcache` in the working directory. Only used when `eslint.cache` is enabled."
},
"eslint.cacheStrategy": {
"scope": "resource",
"type": "string",
"enum": [
"metadata",
"content"
],
"markdownDescription": "Strategy for the ESLint cache to detect changed files. `metadata` uses file metadata (faster) while `content` uses file content (for environments where metadata is unreliable). Only used when `eslint.cache` is enabled."
},
"eslint.trace.server": {
"scope": "window",
"anyOf": [
Expand Down
46 changes: 45 additions & 1 deletion server/src/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export type CLIOptions = {
cwd?: string;
fixTypes?: string[];
fix?: boolean;
/** Enables ESLint's result caching. Only effective when `eslint.run` is `onSave`. */
cache?: boolean;
/** Path to the cache file. Only used when {@link cache} is `true`. */
cacheLocation?: string;
};

export type SeverityConf = 0 | 1 | 2 | 'off' | 'warn' | 'error';
Expand All @@ -115,6 +119,12 @@ export type ESLintClassOptions = {
fix?: boolean;
overrideConfig?: ConfigData;
overrideConfigFile?: string | null;
/** Enables ESLint's result caching. Only effective when `eslint.run` is `onSave`. */
cache?: boolean;
/** Path to the cache file. Only used when {@link cache} is `true`. */
cacheLocation?: string;
/** Strategy for detecting changed files: `metadata` (default, faster) or `content`. Only used when {@link cache} is `true`. */
cacheStrategy?: 'metadata' | 'content';
};

export type RuleMetaData = {
Expand Down Expand Up @@ -252,6 +262,8 @@ export namespace SuggestionsProblem {
interface ESLintClass extends Object {
// https://eslint.org/docs/developer-guide/nodejs-api#-eslintlinttextcode-options
lintText(content: string, options: {filePath?: string; warnIgnored?: boolean}): Promise<ESLintDocumentReport[]>;
// https://eslint.org/docs/developer-guide/nodejs-api#-eslintlintfilespatterns
lintFiles(patterns: string | string[]): Promise<ESLintDocumentReport[]>;
// https://eslint.org/docs/developer-guide/nodejs-api#-eslintispathignoredfilepath
isPathIgnored(path: string): Promise<boolean>;
// https://eslint.org/docs/developer-guide/nodejs-api#-eslintgetrulesmetaforresultsresults
Expand Down Expand Up @@ -333,6 +345,7 @@ namespace RuleData {

interface CLIEngine {
executeOnText(content: string, file?: string, warn?: boolean): ESLintReport;
executeOnFiles(patterns: string[]): ESLintReport;
isPathIgnored(path: string): boolean;
// This is only available from v4.15.0 forward
getRules?(): Map<string, RuleData>;
Expand Down Expand Up @@ -361,6 +374,9 @@ class ESLintClassEmulator implements ESLintClass {
async lintText(content: string, options: { filePath?: string | undefined; warnIgnored?: boolean | undefined }): Promise<ESLintDocumentReport[]> {
return this.cli.executeOnText(content, options.filePath, options.warnIgnored).results;
}
async lintFiles(patterns: string | string[]): Promise<ESLintDocumentReport[]> {
return this.cli.executeOnFiles(Array.isArray(patterns) ? patterns : [patterns]).results;
}
async isPathIgnored(path: string): Promise<boolean> {
return this.cli.isPathIgnored(path);
}
Expand Down Expand Up @@ -1138,11 +1154,27 @@ export namespace ESLint {
return new library.ESLint(newOptions);
}

/** Applies cache-related settings to ESLint options. Cache properties are only set when `settings.cache` is `true`. */
function applyCacheOptions(options: ESLintClassOptions, settings: TextDocumentSettings): void {
if (!settings.cache) {
return;
}
options.cache = true;
if (settings.cacheLocation) {
options.cacheLocation = settings.cacheLocation;
}
if (settings.cacheStrategy) {
options.cacheStrategy = settings.cacheStrategy;
}
}

export async function withClass<T>(func: (eslintClass: ESLintClass) => Promise<T>, settings: TextDocumentSettings & { library: ESLintModule }, options?: ESLintClassOptions | CLIOptions): Promise<T> {
const newOptions: ESLintClassOptions | CLIOptions = options === undefined
? Object.assign(Object.create(null), settings.options)
: Object.assign(Object.create(null), settings.options, options);

applyCacheOptions(newOptions as ESLintClassOptions, settings);

const cwd = process.cwd();
try {
if (settings.workingDirectory) {
Expand Down Expand Up @@ -1196,6 +1228,16 @@ export namespace ESLint {
}

const validFixTypes = new Set<string>(['problem', 'suggestion', 'layout', 'directive']);

/**
* Returns `true` when ESLint should lint from disk (via `lintFiles`) instead of
* from the in-memory document (via `lintText`). Caching requires disk access
* because ESLint's cache tracks files on disk; in-memory content would bypass it.
*/
function shouldUseLintFiles(settings: TextDocumentSettings, file: string | undefined): file is string {
return settings.cache && settings.run === 'onSave' && file !== undefined;
}

export async function validate(document: TextDocument, settings: TextDocumentSettings & { library: ESLintModule }): Promise<Diagnostic[]> {
const newOptions: CLIOptions = Object.assign(Object.create(null), settings.options);
let fixTypes: Set<string> | undefined = undefined;
Expand All @@ -1217,7 +1259,9 @@ export namespace ESLint {

return withClass(async (eslintClass) => {
CodeActions.remove(uri);
const reportResults: ESLintDocumentReport[] = await eslintClass.lintText(content, { filePath: file, warnIgnored: settings.onIgnoredFiles !== ESLintSeverity.off });
const reportResults: ESLintDocumentReport[] = shouldUseLintFiles(settings, file)
? await eslintClass.lintFiles(file)
: await eslintClass.lintText(content, { filePath: file, warnIgnored: settings.onIgnoredFiles !== ESLintSeverity.off });
RuleMetaData.capture(eslintClass, reportResults);
const diagnostics: Diagnostic[] = [];
if (reportResults && Array.isArray(reportResults) && reportResults.length > 0) {
Expand Down