-
Notifications
You must be signed in to change notification settings - Fork 341
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Supports ESLint v8. * fix rule meta * update message
- Loading branch information
Showing
3 changed files
with
113 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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?: { | ||
|
@@ -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; | ||
} | ||
} | ||
|
||
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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); | ||
|
@@ -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 [email protected]`); | ||
connection.console.error(`The eslint library loaded from ${libraryPath} doesn\'t neither exports a CLIEngine nor an ESLint class. You need at least [email protected]`); | ||
} else { | ||
connection.console.info(`ESLint library loaded from: ${libraryPath}`); | ||
settings.library = library; | ||
|
@@ -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; | ||
} | ||
|
@@ -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); | ||
|
@@ -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 | ||
|
@@ -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) { | ||
|
@@ -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) { | ||
|
@@ -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 { | ||
|
@@ -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); | ||
|
||
|
@@ -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 { | ||
|
@@ -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; | ||
|
@@ -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()); | ||
}); | ||
|
||
|
@@ -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; | ||
|
@@ -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.`); | ||
|