Skip to content

Commit

Permalink
Supports ESLint v8. (#1317)
Browse files Browse the repository at this point in the history
* Supports ESLint v8.

* fix rule meta

* update message
  • Loading branch information
ota-meshi authored Aug 23, 2021
1 parent 3aef00d commit e8ef2a9
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 49 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ This extension contributes the following variables to the [settings](https://cod
}
```
- `eslint.packageManager`: controls the package manager to be used to resolve the ESLint library. This has only an influence if the ESLint library is resolved globally. Valid values are `"npm"` or `"yarn"` or `"pnpm"`.
- `eslint.options`: options to configure how ESLint is started using the [ESLint CLI Engine API](http://eslint.org/docs/developer-guide/nodejs-api#cliengine). Defaults to an empty option bag.
- `eslint.options`: options to configure how ESLint is started using the [ESLint class API](http://eslint.org/docs/developer-guide/nodejs-api#eslint-class). (If you use ESLint<=v7, it will be used as an option for [CLI Engine](http://eslint.org/docs/developer-guide/nodejs-api#cliengine).) Defaults to an empty option bag.
An example to point to a custom `.eslintrc.json` file is:
```json
{
"eslint.options": { "configFile": "C:/mydirectory/.eslintrc.json" }
"eslint.options": { "overrideConfigFile": "C:/mydirectory/.eslintrc.json" }
}
```
- `eslint.run` - run the linter `onSave` or `onType`, default is `onType`.
Expand All @@ -145,7 +145,7 @@ This extension contributes the following variables to the [settings](https://cod
- `eslint.probe` = an array for language identifiers for which the ESLint extension should be activated and should try to validate the file. If validation fails for probed languages the extension says silent. Defaults to `["javascript", "javascriptreact", "typescript", "typescriptreact", "html", "vue", "markdown"]`.
- `eslint.validate` - an array of language identifiers specifying the files for which validation is to be enforced. This is an old legacy setting and should in normal cases not be necessary anymore. Defaults to `["javascript", "javascriptreact"]`.
- `eslint.format.enable`: enables ESLint as a formatter for validated files. Although you can also use the formatter on save using the setting `editor.formatOnSave` it is recommended to use the `editor.codeActionsOnSave` feature since it allows for better configurability.
- `eslint.workingDirectories` - specifies how the working directories ESLint is using are computed. ESLint resolves configuration files (e.g. `eslintrc`, `.eslintignore`) relative to a working directory so it is important to configure this correctly. If executing ESLint in the terminal requires you to change the working directory in the terminal into a sub folder then it is usually necessary to tweak this setting. (see also [CLIEngine options#cwd](https://eslint.org/docs/developer-guide/nodejs-api#cliengine)). Please also keep in mind that the `.eslintrc*` file is resolved considering the parent directories whereas the `.eslintignore` file is only honored in the current working directory. The following values can be used:
- `eslint.workingDirectories` - specifies how the working directories ESLint is using are computed. ESLint resolves configuration files (e.g. `eslintrc`, `.eslintignore`) relative to a working directory so it is important to configure this correctly. If executing ESLint in the terminal requires you to change the working directory in the terminal into a sub folder then it is usually necessary to tweak this setting. (see also [ESLint class options#cwd](https://eslint.org/docs/developer-guide/nodejs-api#eslint-class)). Please also keep in mind that the `.eslintrc*` file is resolved considering the parent directories whereas the `.eslintignore` file is only honored in the current working directory. The following values can be used:
- `[{ "mode": "location" }]` (@since 2.0.0): instructs ESLint to uses the workspace folder location or the file location (if no workspace folder is open) as the working directory. This is the default and is the same strategy as used in older versions of the ESLint extension (1.9.x versions).
- `[{ "mode": "auto" }]` (@since 2.0.0): instructs ESLint to infer a working directory based on the location of `package.json`, `.eslintignore` and `.eslintrc*` files. This might work in many cases but can lead to unexpected results as well.
- `string[]`: an array of working directories to use.
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"scope": "resource",
"type": "object",
"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#cliengine)."
"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.trace.server": {
"scope": "window",
Expand Down Expand Up @@ -250,7 +250,7 @@
}
]
},
"markdownDescription": "Specifies how the working directories ESLint is using are computed. ESLint resolves configuration files (e.g. `eslintrc`, `.eslintignore`) relative to a working directory so it is important to configure this correctly."
"markdownDescription": "Specifies how the working directories ESLint is using are computed. ESLint resolves configuration files (e.g. `eslintrc`, `.eslintignore`) relative to a working directory so it is important to configure this correctly."
},
"eslint.validate": {
"scope": "resource",
Expand Down Expand Up @@ -353,7 +353,7 @@
}
},
"additionalProperties": false,
"markdownDescription": "Show disable lint rule in the quick fix menu."
"markdownDescription": "Show disable lint rule in the quick fix menu."
},
"eslint.codeAction.showDocumentation": {
"scope": "resource",
Expand All @@ -369,7 +369,7 @@
}
},
"additionalProperties": false,
"markdownDescription": "Show open lint rule documentation web page in the quick fix menu."
"markdownDescription": "Show open lint rule documentation web page in the quick fix menu."
},
"eslint.codeActionsOnSave.mode": {
"scope": "resource",
Expand Down
148 changes: 106 additions & 42 deletions server/src/eslintServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ interface CLIOptions {
fix?: boolean;
}

interface ESLintClassOptions {
cwd?: string;
fixTypes?: string[];
fix?: boolean;
}

// { meta: { docs: [Object], schema: [Array] }, create: [Function: create] }
interface RuleData {
meta?: {
Expand All @@ -297,8 +303,8 @@ interface RuleData {
}

namespace RuleData {
export function hasMetaType(value: RuleData | undefined): value is RuleData & { meta: { type: string; }; } {
return value !== undefined && value.meta !== undefined && value.meta.type !== undefined;
export function hasMetaType(value: RuleData['meta'] | undefined): value is RuleData['meta'] & { type: string; } {
return value !== undefined && value.type !== undefined;
}
}

Expand All @@ -323,6 +329,28 @@ interface ESLintConfig {
settings: object;
}

namespace ESLintClass {
export function newESLintClass(library: ESLintModule, newOptions: ESLintClassOptions | CLIOptions): ESLintClass {
if (library.CLIEngine === undefined) {
return new library.ESLint(newOptions);
} else {
const cli = new library.CLIEngine(newOptions);
return new ESLintClassEmulator(cli);
}
}
}

interface ESLintClass {
// 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#-eslintispathignoredfilepath
isPathIgnored(path: string): Promise<boolean>;
// https://eslint.org/docs/developer-guide/nodejs-api#-eslintgetrulesmetaforresultsresults
getRulesMetaForResults(results: ESLintDocumentReport[]): Record<string, RuleData['meta']> | undefined /* for ESLintClassEmulator */;
// https://eslint.org/docs/developer-guide/nodejs-api#-eslintcalculateconfigforfilefilepath
calculateConfigForFile(path: string): Promise<ESLintConfig | undefined /* for ESLintClassEmulator */>;
}

interface CLIEngine {
executeOnText(content: string, file?: string, warn?: boolean): ESLintReport;
isPathIgnored(path: string): boolean;
Expand All @@ -337,13 +365,21 @@ namespace CLIEngine {
}
}

interface ESLintClassConstructor {
new(options: ESLintClassOptions): ESLintClass;
}

interface CLIEngineConstructor {
new(options: CLIOptions): CLIEngine;
}

interface ESLintModule {
type ESLintModule = {
CLIEngine: CLIEngineConstructor;
}
} | {
// for ESLint >= v8
ESLint: ESLintClassConstructor;
CLIEngine: undefined;
};

declare const __webpack_require__: typeof require;
declare const __non_webpack_require__: typeof require;
Expand Down Expand Up @@ -890,7 +926,7 @@ function resolveSettings(document: TextDocument): Promise<TextDocumentSettings>
}

settings.silent = settings.validate === Validate.probe;
return promise.then((libraryPath) => {
return promise.then(async (libraryPath) => {
let library = path2Library.get(libraryPath);
if (library === undefined) {
library = loadNodeModule(libraryPath);
Expand All @@ -899,9 +935,9 @@ function resolveSettings(document: TextDocument): Promise<TextDocumentSettings>
if (!settings.silent) {
connection.console.error(`Failed to load eslint library from ${libraryPath}. See output panel for more information.`);
}
} else if (library.CLIEngine === undefined) {
} else if (library.CLIEngine === undefined && library.ESLint === undefined) {
settings.validate = Validate.off;
connection.console.error(`The eslint library loaded from ${libraryPath} doesn\'t export a CLIEngine. You need at least eslint@1.0.0`);
connection.console.error(`The eslint library loaded from ${libraryPath} doesn\'t neither exports a CLIEngine nor an ESLint class. You need at least eslint@1.0.0`);
} else {
connection.console.info(`ESLint library loaded from: ${libraryPath}`);
settings.library = library;
Expand All @@ -928,13 +964,9 @@ function resolveSettings(document: TextDocument): Promise<TextDocumentSettings>
if (defaultLanguageIds.has(document.languageId)) {
settings.validate = Validate.on;
} else if (parserRegExps !== undefined || pluginName !== undefined || parserOptions !== undefined) {
const eslintConfig: ESLintConfig | undefined = withCLIEngine((cli) => {
const eslintConfig: ESLintConfig | undefined = await withESLintClass((eslintClass) => {
try {
if (typeof cli.getConfigForFile === 'function') {
return cli.getConfigForFile(filePath!);
} else {
return undefined;
}
return eslintClass.calculateConfigForFile(filePath!);
} catch (err) {
return undefined;
}
Expand Down Expand Up @@ -995,8 +1027,8 @@ function resolveSettings(document: TextDocument): Promise<TextDocumentSettings>
formatterRegistrations.set(uri, connection.client.register(DocumentFormattingRequest.type, options));
} else {
const filePath = getFilePath(uri)!;
withCLIEngine((cli) => {
if (!cli.isPathIgnored(filePath)) {
await withESLintClass(async (eslintClass) => {
if (!await eslintClass.isPathIgnored(filePath)) {
formatterRegistrations.set(uri, connection.client.register(DocumentFormattingRequest.type, options));
}
}, settings);
Expand Down Expand Up @@ -1304,12 +1336,12 @@ function validateSingle(document: TextDocument, publishDiagnostics: boolean = tr
if (!documents.get(document.uri)) {
return Promise.resolve(undefined);
}
return resolveSettings(document).then((settings) => {
return resolveSettings(document).then(async (settings) => {
if (settings.validate !== Validate.on || !TextDocumentSettings.hasLibrary(settings)) {
return;
}
try {
validate(document, settings, publishDiagnostics);
await validate(document, settings, publishDiagnostics);
connection.sendNotification(StatusNotification.type, { uri: document.uri, state: Status.ok });
} catch (err) {
// if an exception has occurred while validating clear all errors to ensure
Expand Down Expand Up @@ -1362,7 +1394,7 @@ const ruleDocData: {
};

const validFixTypes = new Set<string>(['problem', 'suggestion', 'layout']);
function validate(document: TextDocument, settings: TextDocumentSettings & { library: ESLintModule }, publishDiagnostics: boolean = true): void {
async function validate(document: TextDocument, settings: TextDocumentSettings & { library: ESLintModule }, publishDiagnostics: boolean = true): Promise<void> {
const newOptions: CLIOptions = Object.assign(Object.create(null), settings.options);
let fixTypes: Set<string> | undefined = undefined;
if (Array.isArray(newOptions.fixTypes) && newOptions.fixTypes.length > 0) {
Expand All @@ -1387,20 +1419,21 @@ function validate(document: TextDocument, settings: TextDocumentSettings & { lib
const uri = document.uri;
const file = getFilePath(document);

withCLIEngine((cli) => {
await withESLintClass(async (eslintClass) => {
codeActions.delete(uri);
const report: ESLintReport = cli.executeOnText(content, file, settings.onIgnoredFiles !== ESLintSeverity.off);
if (CLIEngine.hasRule(cli) && !ruleDocData.handled.has(uri)) {
const reportResults: ESLintDocumentReport[] = await eslintClass.lintText(content, { filePath: file, warnIgnored: settings.onIgnoredFiles !== ESLintSeverity.off });
const rulesMeta = eslintClass.getRulesMetaForResults(reportResults);
if (rulesMeta && !ruleDocData.handled.has(uri)) {
ruleDocData.handled.add(uri);
cli.getRules().forEach((rule, key) => {
if (rule.meta && rule.meta.docs && Is.string(rule.meta.docs.url)) {
ruleDocData.urls.set(key, rule.meta.docs.url);
Object.entries(rulesMeta).forEach(([key, meta]) => {
if (meta && meta.docs && Is.string(meta.docs.url)) {
ruleDocData.urls.set(key, meta.docs.url);
}
});
}
const diagnostics: Diagnostic[] = [];
if (report && report.results && Array.isArray(report.results) && report.results.length > 0) {
const docReport = report.results[0];
if (reportResults && Array.isArray(reportResults) && reportResults.length > 0) {
const docReport = reportResults[0];
if (docReport.messages && Array.isArray(docReport.messages)) {
docReport.messages.forEach((problem) => {
if (problem) {
Expand All @@ -1416,9 +1449,9 @@ function validate(document: TextDocument, settings: TextDocumentSettings & { lib
}
const diagnostic = makeDiagnostic(settings, problem);
diagnostics.push(diagnostic);
if (fixTypes !== undefined && CLIEngine.hasRule(cli) && problem.ruleId !== undefined && problem.fix !== undefined) {
const rule = cli.getRules().get(problem.ruleId);
if (RuleData.hasMetaType(rule) && fixTypes.has(rule.meta.type)) {
if (fixTypes !== undefined && rulesMeta && problem.ruleId !== undefined && problem.fix !== undefined) {
const meta = rulesMeta[problem.ruleId];
if (RuleData.hasMetaType(meta) && fixTypes.has(meta.type)) {
recordCodeAction(document, diagnostic, problem);
}
} else {
Expand All @@ -1434,8 +1467,8 @@ function validate(document: TextDocument, settings: TextDocumentSettings & { lib
}, settings);
}

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

Expand All @@ -1447,15 +1480,46 @@ function withCLIEngine<T>(func: (cli: CLIEngine) => T, settings: TextDocumentSet
process.chdir(settings.workingDirectory.directory);
}
}
const cli = new settings.library.CLIEngine(newOptions);
return func(cli);

const eslintClass = ESLintClass.newESLintClass(settings.library, newOptions);
return func(eslintClass);
} finally {
if (cwd !== process.cwd()) {
process.chdir(cwd);
}
}
}

/**
* ESLint class emulator using CLI Engine.
*/
class ESLintClassEmulator implements ESLintClass {
private cli: CLIEngine;

constructor(cli: CLIEngine) {
this.cli = cli;
}
async lintText(content: string, options: { filePath?: string | undefined; warnIgnored?: boolean | undefined; }): Promise<ESLintDocumentReport[]> {
return this.cli.executeOnText(content, options.filePath, options.warnIgnored).results;
}
async isPathIgnored(path: string): Promise<boolean> {
return this.cli.isPathIgnored(path);
}
getRulesMetaForResults(_results: ESLintDocumentReport[]): Record<string, RuleData['meta']> | undefined {
if (!CLIEngine.hasRule(this.cli)) {
return undefined;
}
const rules: Record<string, RuleData['meta']> = {};
for (const [name, rule] of this.cli.getRules()) {
rules[name] = rule.meta;
}
return rules;
}
async calculateConfigForFile(path: string): Promise<ESLintConfig | undefined> {
return typeof this.cli.getConfigForFile === 'function' ? this.cli.getConfigForFile(path) : undefined;
}
}

const noConfigReported: Map<string, ESLintModule> = new Map<string, ESLintModule>();

function isNoConfigFoundError(error: any): boolean {
Expand Down Expand Up @@ -1572,15 +1636,15 @@ function showErrorMessage(error: any, document: TextDocument): Status {
return Status.error;
}

messageQueue.registerNotification(DidChangeWatchedFilesNotification.type, (params) => {
messageQueue.registerNotification(DidChangeWatchedFilesNotification.type, async (params) => {
// A .eslintrc has change. No smartness here.
// Simply revalidate all file.
ruleDocData.handled.clear();
ruleDocData.urls.clear();
noConfigReported.clear();
missingModuleReported.clear();
document2Settings.clear(); // config files can change plugins and parser.
params.changes.forEach((change) => {
await Promise.all(params.changes.map(async (change) => {
const fsPath = getFilePath(change.uri);
if (fsPath === undefined || fsPath.length === 0 || isUNC(fsPath)) {
return;
Expand All @@ -1589,15 +1653,15 @@ messageQueue.registerNotification(DidChangeWatchedFilesNotification.type, (param
if (dirname) {
const library = configErrorReported.get(fsPath);
if (library !== undefined) {
const cli = new library.CLIEngine({});
const eslintClass = ESLintClass.newESLintClass(library, {});
try {
cli.executeOnText('', path.join(dirname, '___test___.js'));
await eslintClass.lintText('', { filePath: path.join(dirname, '___test___.js') });
configErrorReported.delete(fsPath);
} catch (error) {
}
}
}
});
}));
validateMany(documents.all());
});

Expand Down Expand Up @@ -2043,7 +2107,7 @@ function computeAllFixes(identifier: VersionedTextDocumentIdentifier, mode: AllF
return [];
}
const filePath = getFilePath(textDocument);
return withCLIEngine((cli) => {
return withESLintClass(async (eslintClass) => {
const problems = codeActions.get(uri);
const originalContent = textDocument.getText();
let problemFixes: TextEdit[] | undefined;
Expand All @@ -2069,10 +2133,10 @@ function computeAllFixes(identifier: VersionedTextDocumentIdentifier, mode: AllF
} else {
content = originalContent;
}
const report = cli.executeOnText(content, filePath);
const reportResults = await eslintClass.lintText(content, { filePath });
connection.tracer.log(`Computing all fixes took: ${Date.now() - start} ms.`);
if (Array.isArray(report.results) && report.results.length === 1 && report.results[0].output !== undefined) {
const fixedContent = report.results[0].output;
if (Array.isArray(reportResults) && reportResults.length === 1 && reportResults[0].output !== undefined) {
const fixedContent = reportResults[0].output;
start = Date.now();
const diffs = stringDiff(originalContent, fixedContent, false);
connection.tracer.log(`Computing minimal edits took: ${Date.now() - start} ms.`);
Expand Down

0 comments on commit e8ef2a9

Please sign in to comment.