1 /* --------------------------------------------------------------------------------------------
2 * Copyright (c) Microsoft Corporation. All rights reserved.
3 * Licensed under the MIT License. See License.txt in the project root for license information.
4 * ------------------------------------------------------------------------------------------ */
7 import * as os from 'os'
8 import * as path from 'path'
9 import { Position, CancellationToken, CodeAction, CodeActionKind, CodeActionRequest, Command, createConnection, Diagnostic, DiagnosticSeverity, DidChangeConfigurationNotification, DidChangeWatchedFilesNotification, ErrorCodes, ExecuteCommandRequest, Files, IConnection, NotificationHandler, NotificationType, Range, RequestHandler, RequestType, ResponseError, TextDocumentIdentifier, TextDocuments, TextDocumentSaveReason, TextDocumentSyncKind, TextEdit, VersionedTextDocumentIdentifier, WorkspaceChange } from 'vscode-languageserver'
10 import { URI } from 'vscode-uri'
11 import { CLIOptions, ESLintAutoFixEdit, ESLintError, ESLintModule, ESLintProblem, ESLintReport, Is, TextDocumentSettings } from './types'
12 import { getAllFixEdits, executeInWorkspaceDirectory, getFilePath, isUNC } from './util'
13 import { TextDocument } from 'vscode-languageserver-textdocument'
14 declare var __webpack_require__: any
15 declare var __non_webpack_require__: any
16 const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require
18 namespace CommandIds {
19 export const applySingleFix = 'eslint.applySingleFix'
20 export const applySameFixes = 'eslint.applySameFixes'
21 export const applyAllFixes = 'eslint.applyAllFixes'
22 export const applyAutoFix = 'eslint.applyAutoFix'
23 export const applyDisableLine = 'eslint.applyDisableLine'
24 export const applyDisableFile = 'eslint.applyDisableFile'
25 export const openRuleDoc = 'eslint.openRuleDoc'
28 namespace OpenESLintDocRequest {
29 export const type = new RequestType<OpenESLintDocParams, OpenESLintDocResult, void, void>('eslint/openDoc')
32 interface OpenESLintDocParams {
36 interface OpenESLintDocResult {
45 interface StatusParams {
49 namespace StatusNotification {
50 export const type = new NotificationType<StatusParams, void>('eslint/status')
53 interface NoConfigParams {
55 document: TextDocumentIdentifier
58 interface NoConfigResult { }
60 namespace NoConfigRequest {
61 export const type = new RequestType<
69 interface NoESLintLibraryParams {
70 source: TextDocumentIdentifier
73 interface NoESLintLibraryResult { }
75 namespace NoESLintLibraryRequest {
76 export const type = new RequestType<
77 NoESLintLibraryParams,
78 NoESLintLibraryResult,
84 interface RuleCodeActions {
88 disableFile?: CodeAction
89 showDocumentation?: CodeAction
92 class CodeActionResult {
93 private _actions: Map<string, RuleCodeActions>
94 private _fixAll: CodeAction | undefined
96 public constructor() {
97 this._actions = new Map()
100 public get(ruleId: string): RuleCodeActions {
101 let result: RuleCodeActions = this._actions.get(ruleId)
102 if (result === undefined) {
103 result = { fixes: [] }
104 this._actions.set(ruleId, result)
109 public set fixAll(action: CodeAction) {
110 this._fixAll = action
113 public all(): CodeAction[] {
114 let result: CodeAction[] = []
115 for (let actions of this._actions.values()) {
116 result.push(...actions.fixes)
117 if (actions.disable) {
118 result.push(actions.disable)
120 if (actions.fixAll) {
121 result.push(actions.fixAll)
123 if (actions.disableFile) {
124 result.push(actions.disableFile)
126 if (actions.showDocumentation) {
127 result.push(actions.showDocumentation)
130 if (this._fixAll !== undefined) {
131 result.push(this._fixAll)
136 public get length(): number {
138 for (let actions of this._actions.values()) {
139 result += actions.fixes.length
145 function makeDiagnostic(problem: ESLintProblem): Diagnostic {
147 problem.ruleId != null
148 ? `${problem.message} (${problem.ruleId})`
149 : `${problem.message}`
150 let startLine = Math.max(0, problem.line - 1)
151 let startChar = Math.max(0, problem.column - 1)
153 problem.endLine != null ? Math.max(0, problem.endLine - 1) : startLine
155 problem.endColumn != null ? Math.max(0, problem.endColumn - 1) : startChar
158 severity: convertSeverity(problem.severity),
161 start: { line: startLine, character: startChar },
162 end: { line: endLine, character: endChar }
168 interface FixableProblem {
170 documentVersion: number
173 edit?: ESLintAutoFixEdit
176 function computeKey(diagnostic: Diagnostic): string {
177 let range = diagnostic.range
178 return `[${range.start.line},${range.start.character},${range.end.line},${
180 }]-${diagnostic.code}`
183 let codeActions: Map<string, Map<string, FixableProblem>> = new Map<
185 Map<string, FixableProblem>
187 function recordCodeAction(
188 document: TextDocument,
189 diagnostic: Diagnostic,
190 problem: ESLintProblem
192 if (!problem.ruleId) {
195 let uri = document.uri
196 let edits: Map<string, FixableProblem> = codeActions.get(uri)
198 edits = new Map<string, FixableProblem>()
199 codeActions.set(uri, edits)
201 edits.set(computeKey(diagnostic), { label: `Fix this ${problem.ruleId} problem`, documentVersion: document.version, ruleId: problem.ruleId, edit: problem.fix, line: problem.line })
204 function convertSeverity(severity: number): DiagnosticSeverity {
206 // Eslint 1 is warning
208 return DiagnosticSeverity.Warning
210 return DiagnosticSeverity.Error
212 return DiagnosticSeverity.Error
216 const exitCalled = new NotificationType<[number, string], void>(
220 const nodeExit = process.exit
221 process.exit = ((code?: number): void => {
222 let stack = new Error('stack')
223 connection.sendNotification(exitCalled, [code ? code : 0, stack.stack])
228 process.on('uncaughtException', (error: any) => {
231 if (typeof error.stack === 'string') {
232 message = error.stack
233 } else if (typeof error.message === 'string') {
234 message = error.message
235 } else if (typeof error === 'string') {
240 message = JSON.stringify(error, undefined, 4)
242 // Should not happen.
246 connection.console.error(`Uncaught exception recevied.
250 let connection = createConnection()
251 connection.console.info(`ESLint server running in node ${process.version}`)
252 let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument)
254 let _globalNpmPath: string | null | undefined
255 function globalNpmPath(): string {
256 if (_globalNpmPath === void 0) {
257 _globalNpmPath = Files.resolveGlobalNodePath(trace)
258 if (_globalNpmPath === void 0) {
259 _globalNpmPath = null
262 if (_globalNpmPath === null) {
265 return _globalNpmPath
267 let _globalYarnPath: string | undefined
268 function globalYarnPath(): string {
269 if (_globalYarnPath === void 0) {
270 _globalYarnPath = Files.resolveGlobalYarnPath(trace)
271 if (_globalYarnPath === void 0) {
272 _globalYarnPath = null
275 if (_globalYarnPath === null) {
278 return _globalYarnPath
280 let path2Library: Map<string, ESLintModule> = new Map<string, ESLintModule>()
281 let document2Settings: Map<string, Thenable<TextDocumentSettings>> = new Map<
283 Thenable<TextDocumentSettings>
288 urls: Map<string, string>
290 handled: new Set<string>(),
291 urls: new Map<string, string>()
294 function resolveSettings(
295 document: TextDocument
296 ): Thenable<TextDocumentSettings> {
297 let uri = document.uri
298 let resultPromise = document2Settings.get(uri)
302 resultPromise = connection.workspace
303 .getConfiguration({ scopeUri: uri, section: '' })
304 .then((settings: TextDocumentSettings) => {
305 let nodePath: string | undefined
306 if (settings.nodePath) {
307 nodePath = settings.nodePath
308 if (nodePath.startsWith('~')) {
309 nodePath = nodePath.replace(/^~/, os.homedir())
311 if (!path.isAbsolute(nodePath)) {
312 nodePath = path.join(URI.parse(settings.workspaceFolder.uri).fsPath, nodePath)
316 let resolvedGlobalPackageManagerPath: string | undefined
317 if (settings.packageManager === 'npm') {
318 resolvedGlobalPackageManagerPath = globalNpmPath()
319 } else if (settings.packageManager === 'yarn') {
320 resolvedGlobalPackageManagerPath = globalYarnPath()
323 let uri = URI.parse(document.uri)
324 let promise: Thenable<string>
325 let directory: string
326 if (uri.scheme === 'file') {
327 directory = path.dirname(uri.fsPath)
329 directory = settings.workspaceFolder ? URI.parse(settings.workspaceFolder.uri).fsPath : undefined
332 if (nodePath !== undefined) {
333 promise = Files.resolve('eslint', nodePath, nodePath, trace).then<string, string>(undefined, () => {
334 return Files.resolve('eslint', resolvedGlobalPackageManagerPath, directory, trace);
337 promise = Files.resolve('eslint', resolvedGlobalPackageManagerPath, directory, trace);
340 return promise.then(path => {
341 let library = path2Library.get(path)
343 library = requireFunc(path)
344 if (!library.CLIEngine) {
345 settings.validate = false
346 connection.console.error(
347 `The eslint library loaded from ${path} doesn\'t export a CLIEngine. You need at least eslint@1.0.0`
350 connection.console.info(`ESLint library loaded from: ${path}`)
351 settings.library = library
353 path2Library.set(path, library)
355 settings.library = library
359 settings.validate = false
360 connection.sendRequest(NoESLintLibraryRequest.type, {
361 source: { uri: document.uri }
366 document2Settings.set(uri, resultPromise)
370 interface Request<P, R> {
373 documentVersion: number | undefined
374 resolve: (value: R | Thenable<R>) => void | undefined
375 reject: (error: any) => void | undefined
376 token: CancellationToken | undefined
380 export function is(value: any): value is Request<any, any> {
381 let candidate: Request<any, any> = value
385 !!candidate.resolve &&
391 interface Notifcation<P> {
394 documentVersion: number
397 type Message<P, R> = Notifcation<P> | Request<P, R>
399 type VersionProvider<P> = (params: P) => number
402 export function is<T>(value: any): value is Thenable<T> {
403 let candidate: Thenable<T> = value
404 return candidate && typeof candidate.then === 'function'
408 class BufferedMessageQueue {
409 private queue: Message<any, any>[]
410 private requestHandlers: Map<
413 handler: RequestHandler<any, any, any>
414 versionProvider?: VersionProvider<any>
417 private notificationHandlers: Map<
419 { handler: NotificationHandler<any>; versionProvider?: VersionProvider<any> }
421 private timer: NodeJS.Immediate | undefined
423 constructor(private connection: IConnection) {
425 this.requestHandlers = new Map()
426 this.notificationHandlers = new Map()
429 public registerRequest<P, R, E, RO>(
430 type: RequestType<P, R, E, RO>,
431 handler: RequestHandler<P, R, E>,
432 versionProvider?: VersionProvider<P>
434 this.connection.onRequest(type, (params, token) => {
435 return new Promise<R>((resolve, reject) => {
439 documentVersion: versionProvider
440 ? versionProvider(params)
449 this.requestHandlers.set(type.method, { handler, versionProvider })
452 public registerNotification<P, RO>(
453 type: NotificationType<P, RO>,
454 handler: NotificationHandler<P>,
455 versionProvider?: (params: P) => number
457 connection.onNotification(type, params => {
461 documentVersion: versionProvider ? versionProvider(params) : undefined
465 this.notificationHandlers.set(type.method, { handler, versionProvider })
468 public addNotificationMessage<P, RO>(
469 type: NotificationType<P, RO>,
476 documentVersion: version
481 public onNotification<P, RO>(
482 type: NotificationType<P, RO>,
483 handler: NotificationHandler<P>,
484 versionProvider?: (params: P) => number
486 this.notificationHandlers.set(type.method, { handler, versionProvider })
489 private trigger(): void {
490 if (this.timer || this.queue.length === 0) {
493 this.timer = setImmediate(() => {
494 this.timer = undefined
499 private processQueue(): void {
500 let message = this.queue.shift()
504 if (Request.is(message)) {
505 let requestMessage = message
506 if (requestMessage.token.isCancellationRequested) {
507 requestMessage.reject(
508 // tslint:disable-next-line: no-inferred-empty-object-type
510 ErrorCodes.RequestCancelled,
511 'Request got cancelled'
516 let elem = this.requestHandlers.get(requestMessage.method)
518 elem.versionProvider &&
519 requestMessage.documentVersion !== void 0 &&
520 requestMessage.documentVersion !==
521 elem.versionProvider(requestMessage.params)
523 requestMessage.reject(
524 // tslint:disable-next-line: no-inferred-empty-object-type
526 ErrorCodes.RequestCancelled,
527 'Request got cancelled'
532 let result = elem.handler(requestMessage.params, requestMessage.token)
533 if (Thenable.is(result)) {
536 requestMessage.resolve(value)
539 requestMessage.reject(error)
543 requestMessage.resolve(result)
546 let notificationMessage = message
547 let elem = this.notificationHandlers.get(notificationMessage.method)
549 elem.versionProvider &&
550 notificationMessage.documentVersion !== void 0 &&
551 notificationMessage.documentVersion !==
552 elem.versionProvider(notificationMessage.params)
556 elem.handler(notificationMessage.params)
562 let messageQueue: BufferedMessageQueue = new BufferedMessageQueue(connection)
564 namespace ValidateNotification {
565 export const type: NotificationType<
568 > = new NotificationType<TextDocument, void>('eslint/validate')
571 messageQueue.onNotification(
572 ValidateNotification.type,
574 validateSingle(document, true)
576 (document): number => {
577 return document.version
581 // The documents manager listen for text document create, change
582 // and close on the connection
583 documents.listen(connection)
584 documents.onDidOpen(event => {
585 resolveSettings(event.document).then(settings => {
586 if (!settings.validate) {
589 if (settings.run === 'onSave') {
590 messageQueue.addNotificationMessage(
591 ValidateNotification.type,
593 event.document.version
599 // A text document has changed. Validate the document according the run setting.
600 documents.onDidChangeContent(event => {
601 resolveSettings(event.document).then(settings => {
602 if (!settings.validate || settings.run !== 'onType') {
605 messageQueue.addNotificationMessage(
606 ValidateNotification.type,
608 event.document.version
613 documents.onWillSaveWaitUntil(event => {
614 if (event.reason === TextDocumentSaveReason.AfterDelay) {
618 let document = event.document
619 return resolveSettings(document).then(settings => {
620 if (!settings.autoFixOnSave) {
623 // If we validate on save and want to apply fixes on will save
624 // we need to validate the file.
625 if (settings.run === 'onSave') {
626 // Do not queue this since we want to get the fixes as fast as possible.
627 return validateSingle(document, false).then(() => getAllFixEdits(document, settings))
629 return getAllFixEdits(document, settings)
634 // A text document has been saved. Validate the document according the run setting.
635 documents.onDidSave(event => {
636 resolveSettings(event.document).then(settings => {
637 if (!settings.validate || settings.run !== 'onSave') {
640 messageQueue.addNotificationMessage(
641 ValidateNotification.type,
643 event.document.version
648 documents.onDidClose(event => {
649 resolveSettings(event.document).then(settings => {
650 let uri = event.document.uri
651 document2Settings.delete(uri)
652 codeActions.delete(uri)
653 if (settings.validate) {
654 connection.sendDiagnostics({ uri, diagnostics: [] })
659 function environmentChanged(): void {
660 document2Settings.clear()
661 for (let document of documents.all()) {
662 messageQueue.addNotificationMessage(
663 ValidateNotification.type,
670 function trace(message: string, verbose?: string): void {
671 connection.tracer.log(message, verbose)
674 connection.onInitialize(_params => {
679 change: TextDocumentSyncKind.Full,
680 willSaveWaitUntil: true,
685 codeActionProvider: true,
686 executeCommandProvider: {
688 CommandIds.applySingleFix,
689 CommandIds.applySameFixes,
690 CommandIds.applyAllFixes,
691 CommandIds.applyAutoFix,
692 CommandIds.applyDisableLine,
693 CommandIds.applyDisableFile,
694 CommandIds.openRuleDoc,
701 connection.onInitialized(() => {
702 connection.client.register(DidChangeConfigurationNotification.type, undefined)
705 messageQueue.registerNotification(
706 DidChangeConfigurationNotification.type,
712 // messageQueue.registerNotification(
713 // DidChangeWorkspaceFoldersNotification.type,
715 // environmentChanged()
719 const singleErrorHandlers: ((
721 document: TextDocument,
722 library: ESLintModule
725 tryHandleConfigError,
726 tryHandleMissingModule,
730 function validateSingle(
731 document: TextDocument,
732 publishDiagnostics = true
734 // We validate document in a queue but open / close documents directly. So we need to deal with the
735 // fact that a document might be gone from the server.
736 if (!documents.get(document.uri)) {
737 return Promise.resolve(undefined)
739 return resolveSettings(document).then(settings => {
740 if (!settings.validate) {
744 validate(document, settings, publishDiagnostics)
745 connection.sendNotification(StatusNotification.type, { state: Status.ok })
748 for (let handler of singleErrorHandlers) {
749 status = handler(err, document, settings.library)
754 status = status || Status.error
755 connection.sendNotification(StatusNotification.type, { state: status })
760 function validateMany(documents: TextDocument[]): void {
761 documents.forEach(document => {
762 messageQueue.addNotificationMessage(
763 ValidateNotification.type,
770 function getMessage(err: any, document: TextDocument): string {
771 let result: string = null
772 if (typeof err.message === 'string' || err.message instanceof String) {
773 result = err.message as string
774 result = result.replace(/\r?\n/g, ' ')
775 if (/^CLI: /.test(result)) {
776 result = result.substr(5)
779 result = `An unknown error occured while validating document: ${
786 function validate(document: TextDocument, settings: TextDocumentSettings, publishDiagnostics = true): void {
787 const uri = document.uri
788 const content = document.getText()
789 const newOptions: CLIOptions = Object.assign(Object.create(null), settings.options)
790 executeInWorkspaceDirectory(document, settings, newOptions, (file: string, options: CLIOptions) => {
791 const cli = new settings.library.CLIEngine(options)
792 // Clean previously computed code actions.
793 codeActions.delete(uri)
794 const report: ESLintReport = cli.executeOnText(content, file)
795 const diagnostics: Diagnostic[] = []
796 if (report && report.results && Array.isArray(report.results) && report.results.length > 0) {
797 const docReport = report.results[0]
798 if (docReport.messages && Array.isArray(docReport.messages)) {
799 docReport.messages.forEach(problem => {
801 const isWarning = convertSeverity(problem.severity) === DiagnosticSeverity.Warning
802 if (settings.quiet && isWarning) {
803 // Filter out warnings when quiet mode is enabled
806 const diagnostic = makeDiagnostic(problem)
807 diagnostics.push(diagnostic)
808 if (settings.autoFix) {
809 if (typeof cli.getRules === 'function' && problem.ruleId !== undefined && problem.fix !== undefined) {
810 const rule = cli.getRules().get(problem.ruleId)
811 if (rule !== undefined && rule.meta && typeof rule.meta.fixable == 'string') {
812 recordCodeAction(document, diagnostic, problem)
815 recordCodeAction(document, diagnostic, problem)
822 if (publishDiagnostics) {
823 connection.sendDiagnostics({ uri, diagnostics })
826 // cache documentation urls for all rules
827 if (typeof cli.getRules === 'function' && !ruleDocData.handled.has(uri)) {
828 ruleDocData.handled.add(uri)
829 cli.getRules().forEach((rule, key) => {
830 if (rule.meta && rule.meta.docs && Is.string(rule.meta.docs.url)) {
831 ruleDocData.urls.set(key, rule.meta.docs.url)
838 let noConfigReported: Map<string, ESLintModule> = new Map<
843 function isNoConfigFoundError(error: any): boolean {
844 let candidate = error as ESLintError
846 candidate.messageTemplate === 'no-config-found' ||
847 candidate.message === 'No ESLint configuration found.'
851 function tryHandleNoConfig(
853 document: TextDocument,
854 library: ESLintModule
856 if (!isNoConfigFoundError(error)) {
859 if (!noConfigReported.has(document.uri)) {
861 .sendRequest(NoConfigRequest.type, {
862 message: getMessage(error, document),
867 .then(undefined, () => {
870 noConfigReported.set(document.uri, library)
875 let configErrorReported: Map<string, ESLintModule> = new Map<
880 function tryHandleConfigError(
882 document: TextDocument,
883 library: ESLintModule
885 if (!error.message) {
889 function handleFileName(filename: string): Status {
890 if (!configErrorReported.has(filename)) {
891 connection.console.error(getMessage(error, document))
892 if (!documents.get(URI.file(filename).toString())) {
893 connection.window.showInformationMessage(getMessage(error, document))
895 configErrorReported.set(filename, library)
900 let matches = /Cannot read config file:\s+(.*)\nError:\s+(.*)/.exec(
903 if (matches && matches.length === 3) {
904 return handleFileName(matches[1])
907 matches = /(.*):\n\s*Configuration for rule \"(.*)\" is /.exec(error.message)
908 if (matches && matches.length === 3) {
909 return handleFileName(matches[1])
912 matches = /Cannot find module '([^']*)'\nReferenced from:\s+(.*)/.exec(
915 if (matches && matches.length === 3) {
916 return handleFileName(matches[2])
922 let missingModuleReported: Map<string, ESLintModule> = new Map<
927 function tryHandleMissingModule(
929 document: TextDocument,
930 library: ESLintModule
932 if (!error.message) {
936 function handleMissingModule(
941 if (!missingModuleReported.has(plugin)) {
942 let fsPath = getFilePath(document)
943 missingModuleReported.set(plugin, library)
944 if (error.messageTemplate === 'plugin-missing') {
945 connection.console.error(
948 `${error.message.toString()}`,
949 `Happened while validating ${fsPath ? fsPath : document.uri}`,
950 `This can happen for a couple of reasons:`,
951 `1. The plugin name is spelled incorrectly in an ESLint configuration file (e.g. .eslintrc).`,
952 `2. If ESLint is installed globally, then make sure ${module} is installed globally as well.`,
953 `3. If ESLint is installed locally, then ${module} isn't installed correctly.`,
955 `Consider running eslint --debug ${
956 fsPath ? fsPath : document.uri
957 } from a terminal to obtain a trace about the configuration files used.`
961 connection.console.error(
963 `${error.message.toString()}`,
964 `Happend while validating ${fsPath ? fsPath : document.uri}`
972 let matches = /Failed to load plugin (.*): Cannot find module (.*)/.exec(
975 if (matches && matches.length === 3) {
976 return handleMissingModule(matches[1], matches[2], error)
982 function showErrorMessage(error: any, document: TextDocument): Status {
983 connection.window.showErrorMessage(
984 `ESLint: ${getMessage(
987 )}. Please see the 'ESLint' output channel for details.`
989 if (Is.string(error.stack)) {
990 connection.console.error('ESLint stack trace:')
991 connection.console.error(error.stack)
996 messageQueue.registerNotification(
997 DidChangeWatchedFilesNotification.type,
999 // A .eslintrc has change. No smartness here.
1000 // Simply revalidate all file.
1001 noConfigReported = new Map<string, ESLintModule>()
1002 missingModuleReported = new Map<string, ESLintModule>()
1003 params.changes.forEach(change => {
1004 let fsPath = getFilePath(change.uri)
1005 if (!fsPath || isUNC(fsPath)) {
1008 let dirname = path.dirname(fsPath)
1010 let library = configErrorReported.get(fsPath)
1012 let cli = new library.CLIEngine({})
1014 cli.executeOnText('', path.join(dirname, '___test___.js'))
1015 configErrorReported.delete(fsPath)
1022 validateMany(documents.all())
1027 constructor(private edits: Map<string, FixableProblem>) { }
1029 public static overlaps(lastEdit: FixableProblem, newEdit: FixableProblem): boolean {
1030 return !!lastEdit && lastEdit.edit.range[1] > newEdit.edit.range[0]
1033 public isEmpty(): boolean {
1034 return this.edits.size === 0
1037 public getDocumentVersion(): number {
1038 if (this.isEmpty()) {
1039 throw new Error('No edits recorded.')
1041 return this.edits.values().next().value.documentVersion
1044 public getScoped(diagnostics: Diagnostic[]): FixableProblem[] {
1045 let result: FixableProblem[] = []
1046 for (let diagnostic of diagnostics) {
1047 let key = computeKey(diagnostic)
1048 let editInfo = this.edits.get(key)
1050 result.push(editInfo)
1056 public getAllSorted(): FixableProblem[] {
1057 let result: FixableProblem[] = []
1058 this.edits.forEach(value => {
1059 if (value.edit != null) {
1063 return result.sort((a, b) => {
1064 let d = a.edit.range[0] - b.edit.range[0]
1068 if (a.edit.range[1] === 0) {
1071 if (b.edit.range[1] === 0) {
1074 return a.edit.range[1] - b.edit.range[1]
1078 public getOverlapFree(): FixableProblem[] {
1079 let sorted = this.getAllSorted()
1080 if (sorted.length <= 1) {
1083 let result: FixableProblem[] = []
1084 let last: FixableProblem = sorted[0]
1086 for (let i = 1; i < sorted.length; i++) {
1087 let current = sorted[i]
1088 if (!Fixes.overlaps(last, current)) {
1089 result.push(current)
1097 let commands: Map<string, WorkspaceChange>
1098 messageQueue.registerRequest(
1099 CodeActionRequest.type,
1101 commands = new Map<string, WorkspaceChange>()
1102 let result: CodeActionResult = new CodeActionResult()
1103 let uri = params.textDocument.uri
1104 let edits = codeActions.get(uri)
1105 if (!edits) return []
1106 let fixes = new Fixes(edits)
1107 if (fixes.isEmpty()) return []
1109 let textDocument = documents.get(uri)
1110 let documentVersion = -1
1111 let allFixableRuleIds: string[] = []
1113 function createTextEdit(editInfo: FixableProblem): TextEdit {
1114 return TextEdit.replace(Range.create(textDocument.positionAt(editInfo.edit.range[0]), textDocument.positionAt(editInfo.edit.range[1])), editInfo.edit.text || '')
1117 function createDisableLineTextEdit(editInfo: FixableProblem, indentationText: string): TextEdit {
1118 return TextEdit.insert(Position.create(editInfo.line - 1, 0), `${indentationText}// eslint-disable-next-line ${editInfo.ruleId}\n`)
1121 function createDisableSameLineTextEdit(editInfo: FixableProblem): TextEdit {
1122 return TextEdit.insert(Position.create(editInfo.line - 1, Number.MAX_VALUE), ` // eslint-disable-line ${editInfo.ruleId}`)
1125 function createDisableFileTextEdit(editInfo: FixableProblem): TextEdit {
1126 // If firts line contains a shebang, insert on the next line instead.
1127 const shebang = textDocument?.getText(Range.create(Position.create(0, 0), Position.create(0, 2)))
1128 const line = shebang === '#!' ? 1 : 0
1129 return TextEdit.insert(Position.create(line, 0), `/* eslint-disable ${editInfo.ruleId} */\n`)
1132 function getLastEdit(array: FixableProblem[]): FixableProblem {
1133 let length = array.length
1137 return array[length - 1]
1140 return resolveSettings(textDocument).then(settings => {
1141 for (let editInfo of fixes.getScoped(params.context.diagnostics)) {
1142 documentVersion = editInfo.documentVersion
1143 let ruleId = editInfo.ruleId
1144 allFixableRuleIds.push(ruleId)
1146 if (editInfo.edit != null) {
1147 let workspaceChange = new WorkspaceChange()
1148 workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createTextEdit(editInfo))
1149 commands.set(`${CommandIds.applySingleFix}:${ruleId}`, workspaceChange)
1150 let action = CodeAction.create(
1152 Command.create(editInfo.label, CommandIds.applySingleFix, ruleId),
1153 CodeActionKind.QuickFix
1155 action.isPreferred = true
1156 result.get(ruleId).fixes.push(action)
1159 if (settings.codeAction.disableRuleComment.enable) {
1160 let workspaceChange = new WorkspaceChange()
1161 if (settings.codeAction.disableRuleComment.location === 'sameLine') {
1162 workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableSameLineTextEdit(editInfo))
1164 let lineText = textDocument.getText(Range.create(Position.create(editInfo.line - 1, 0), Position.create(editInfo.line - 1, Number.MAX_VALUE)))
1165 let indentationText = /^([ \t]*)/.exec(lineText)[1]
1166 workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableLineTextEdit(editInfo, indentationText))
1168 commands.set(`${CommandIds.applyDisableLine}:${ruleId}`, workspaceChange)
1169 let title = `Disable ${ruleId} for this line`
1170 result.get(ruleId).disable = CodeAction.create(
1172 Command.create(title, CommandIds.applyDisableLine, ruleId),
1173 CodeActionKind.QuickFix
1176 if (result.get(ruleId).disableFile === undefined) {
1177 workspaceChange = new WorkspaceChange()
1178 workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableFileTextEdit(editInfo))
1179 commands.set(`${CommandIds.applyDisableFile}:${ruleId}`, workspaceChange)
1180 title = `Disable ${ruleId} for the entire file`
1181 result.get(ruleId).disableFile = CodeAction.create(
1183 Command.create(title, CommandIds.applyDisableFile, ruleId),
1184 CodeActionKind.QuickFix
1189 if (settings.codeAction.showDocumentation.enable && result.get(ruleId).showDocumentation === undefined) {
1190 if (ruleDocData.urls.has(ruleId)) {
1191 let title = `Show documentation for ${ruleId}`
1192 result.get(ruleId).showDocumentation = CodeAction.create(
1194 Command.create(title, CommandIds.openRuleDoc, ruleId),
1195 CodeActionKind.QuickFix
1201 if (result.length > 0) {
1202 let sameProblems: Map<string, FixableProblem[]> = new Map<string, FixableProblem[]>(allFixableRuleIds.map<[string, FixableProblem[]]>(s => [s, []]))
1203 let all: FixableProblem[] = []
1205 for (let editInfo of fixes.getAllSorted()) {
1206 if (documentVersion === -1) {
1207 documentVersion = editInfo.documentVersion
1209 if (sameProblems.has(editInfo.ruleId)) {
1210 let same = sameProblems.get(editInfo.ruleId)
1211 if (!Fixes.overlaps(getLastEdit(same), editInfo)) {
1215 if (!Fixes.overlaps(getLastEdit(all), editInfo)) {
1219 sameProblems.forEach((same, ruleId) => {
1220 if (same.length > 1) {
1221 let sameFixes: WorkspaceChange = new WorkspaceChange()
1222 let sameTextChange = sameFixes.getTextEditChange({ uri, version: documentVersion })
1223 same.map(createTextEdit).forEach(edit => sameTextChange.add(edit))
1224 commands.set(CommandIds.applySameFixes, sameFixes)
1225 let title = `Fix all ${ruleId} problems`
1226 let command = Command.create(title, CommandIds.applySameFixes)
1227 result.get(ruleId).fixAll = CodeAction.create(
1230 CodeActionKind.QuickFix
1234 if (all.length > 1) {
1235 let allFixes: WorkspaceChange = new WorkspaceChange()
1236 let allTextChange = allFixes.getTextEditChange({ uri, version: documentVersion })
1237 all.map(createTextEdit).forEach(edit => allTextChange.add(edit))
1238 commands.set(CommandIds.applyAllFixes, allFixes)
1239 let title = `Fix all auto-fixable problems`
1240 let command = Command.create(title, CommandIds.applyAllFixes)
1241 result.fixAll = CodeAction.create(
1244 CodeActionKind.SourceFixAll
1251 (params): number => {
1252 let document = documents.get(params.textDocument.uri)
1253 return document ? document.version : undefined
1257 messageQueue.registerRequest(
1258 ExecuteCommandRequest.type,
1260 let workspaceChange: WorkspaceChange
1261 if (params.command === CommandIds.applyAutoFix) {
1262 let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
1263 if (!identifier.uri.startsWith('file:')) {
1266 let textDocument = documents.get(identifier.uri)
1267 let settings = await Promise.resolve(resolveSettings(textDocument))
1268 let edits = getAllFixEdits(textDocument, settings)
1269 if (edits && edits.length) {
1270 workspaceChange = new WorkspaceChange()
1271 let textChange = workspaceChange.getTextEditChange(identifier)
1272 edits.forEach(edit => textChange.add(edit))
1275 if ([CommandIds.applySingleFix, CommandIds.applyDisableLine, CommandIds.applyDisableFile].indexOf(params.command) !== -1) {
1276 let ruleId = params.arguments[0]
1277 workspaceChange = commands.get(`${params.command}:${ruleId}`)
1278 } else if (params.command === CommandIds.openRuleDoc) {
1279 let ruleId = params.arguments[0]
1280 let url = ruleDocData.urls.get(ruleId)
1282 await connection.sendRequest(OpenESLintDocRequest.type, { url })
1285 workspaceChange = commands.get(params.command)
1289 if (!workspaceChange) {
1293 let response = await Promise.resolve(connection.workspace.applyEdit(workspaceChange.edit))
1294 if (!response.applied) {
1295 connection.console.error(`Failed to apply command: ${params.command}`)
1298 connection.console.error(`Failed to apply command: ${params.command}`)
1302 (params): number => {
1303 if (params.command === CommandIds.applyAutoFix) {
1304 let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
1305 return identifier.version
1312 connection.tracer.connection.listen()