Giant blob of minor changes
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-eslint / server / index.ts
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  * ------------------------------------------------------------------------------------------ */
5 'use strict'
6
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
17
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'
26 }
27
28 namespace OpenESLintDocRequest {
29   export const type = new RequestType<OpenESLintDocParams, OpenESLintDocResult, void, void>('eslint/openDoc')
30 }
31
32 interface OpenESLintDocParams {
33   url: string
34 }
35
36 interface OpenESLintDocResult {
37 }
38
39 enum Status {
40   ok = 1,
41   warn = 2,
42   error = 3
43 }
44
45 interface StatusParams {
46   state: Status
47 }
48
49 namespace StatusNotification {
50   export const type = new NotificationType<StatusParams, void>('eslint/status')
51 }
52
53 interface NoConfigParams {
54   message: string
55   document: TextDocumentIdentifier
56 }
57
58 interface NoConfigResult { }
59
60 namespace NoConfigRequest {
61   export const type = new RequestType<
62     NoConfigParams,
63     NoConfigResult,
64     void,
65     void
66   >('eslint/noConfig')
67 }
68
69 interface NoESLintLibraryParams {
70   source: TextDocumentIdentifier
71 }
72
73 interface NoESLintLibraryResult { }
74
75 namespace NoESLintLibraryRequest {
76   export const type = new RequestType<
77     NoESLintLibraryParams,
78     NoESLintLibraryResult,
79     void,
80     void
81   >('eslint/noLibrary')
82 }
83
84 interface RuleCodeActions {
85   fixes: CodeAction[]
86   disable?: CodeAction
87   fixAll?: CodeAction
88   disableFile?: CodeAction
89   showDocumentation?: CodeAction
90 }
91
92 class CodeActionResult {
93   private _actions: Map<string, RuleCodeActions>
94   private _fixAll: CodeAction | undefined
95
96   public constructor() {
97     this._actions = new Map()
98   }
99
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)
105     }
106     return result
107   }
108
109   public set fixAll(action: CodeAction) {
110     this._fixAll = action
111   }
112
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)
119       }
120       if (actions.fixAll) {
121         result.push(actions.fixAll)
122       }
123       if (actions.disableFile) {
124         result.push(actions.disableFile)
125       }
126       if (actions.showDocumentation) {
127         result.push(actions.showDocumentation)
128       }
129     }
130     if (this._fixAll !== undefined) {
131       result.push(this._fixAll)
132     }
133     return result
134   }
135
136   public get length(): number {
137     let result = 0
138     for (let actions of this._actions.values()) {
139       result += actions.fixes.length
140     }
141     return result
142   }
143 }
144
145 function makeDiagnostic(problem: ESLintProblem): Diagnostic {
146   let message =
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)
152   let endLine =
153     problem.endLine != null ? Math.max(0, problem.endLine - 1) : startLine
154   let endChar =
155     problem.endColumn != null ? Math.max(0, problem.endColumn - 1) : startChar
156   return {
157     message,
158     severity: convertSeverity(problem.severity),
159     source: 'eslint',
160     range: {
161       start: { line: startLine, character: startChar },
162       end: { line: endLine, character: endChar }
163     },
164     code: problem.ruleId
165   }
166 }
167
168 interface FixableProblem {
169   label: string
170   documentVersion: number
171   ruleId: string
172   line: number
173   edit?: ESLintAutoFixEdit
174 }
175
176 function computeKey(diagnostic: Diagnostic): string {
177   let range = diagnostic.range
178   return `[${range.start.line},${range.start.character},${range.end.line},${
179     range.end.character
180     }]-${diagnostic.code}`
181 }
182
183 let codeActions: Map<string, Map<string, FixableProblem>> = new Map<
184   string,
185   Map<string, FixableProblem>
186 >()
187 function recordCodeAction(
188   document: TextDocument,
189   diagnostic: Diagnostic,
190   problem: ESLintProblem
191 ): void {
192   if (!problem.ruleId) {
193     return
194   }
195   let uri = document.uri
196   let edits: Map<string, FixableProblem> = codeActions.get(uri)
197   if (!edits) {
198     edits = new Map<string, FixableProblem>()
199     codeActions.set(uri, edits)
200   }
201   edits.set(computeKey(diagnostic), { label: `Fix this ${problem.ruleId} problem`, documentVersion: document.version, ruleId: problem.ruleId, edit: problem.fix, line: problem.line })
202 }
203
204 function convertSeverity(severity: number): DiagnosticSeverity {
205   switch (severity) {
206     // Eslint 1 is warning
207     case 1:
208       return DiagnosticSeverity.Warning
209     case 2:
210       return DiagnosticSeverity.Error
211     default:
212       return DiagnosticSeverity.Error
213   }
214 }
215
216 const exitCalled = new NotificationType<[number, string], void>(
217   'eslint/exitCalled'
218 )
219
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])
224   setTimeout(() => {
225     nodeExit(code)
226   }, 1000)
227 }) as any
228 process.on('uncaughtException', (error: any) => {
229   let message: string
230   if (error) {
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') {
236       message = error
237     }
238     if (!message) {
239       try {
240         message = JSON.stringify(error, undefined, 4)
241       } catch (e) {
242         // Should not happen.
243       }
244     }
245   }
246   connection.console.error(`Uncaught exception recevied.
247   ${message || ''}`)
248 })
249
250 let connection = createConnection()
251 connection.console.info(`ESLint server running in node ${process.version}`)
252 let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument)
253
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
260     }
261   }
262   if (_globalNpmPath === null) {
263     return undefined
264   }
265   return _globalNpmPath
266 }
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
273     }
274   }
275   if (_globalYarnPath === null) {
276     return undefined
277   }
278   return _globalYarnPath
279 }
280 let path2Library: Map<string, ESLintModule> = new Map<string, ESLintModule>()
281 let document2Settings: Map<string, Thenable<TextDocumentSettings>> = new Map<
282   string,
283   Thenable<TextDocumentSettings>
284 >()
285
286 let ruleDocData: {
287   handled: Set<string>
288   urls: Map<string, string>
289 } = {
290   handled: new Set<string>(),
291   urls: new Map<string, string>()
292 }
293
294 function resolveSettings(
295   document: TextDocument
296 ): Thenable<TextDocumentSettings> {
297   let uri = document.uri
298   let resultPromise = document2Settings.get(uri)
299   if (resultPromise) {
300     return resultPromise
301   }
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())
310         }
311         if (!path.isAbsolute(nodePath)) {
312           nodePath = path.join(URI.parse(settings.workspaceFolder.uri).fsPath, nodePath)
313         }
314       }
315
316       let resolvedGlobalPackageManagerPath: string | undefined
317       if (settings.packageManager === 'npm') {
318         resolvedGlobalPackageManagerPath = globalNpmPath()
319       } else if (settings.packageManager === 'yarn') {
320         resolvedGlobalPackageManagerPath = globalYarnPath()
321       }
322
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)
328       } else {
329         directory = settings.workspaceFolder ? URI.parse(settings.workspaceFolder.uri).fsPath : undefined
330       }
331
332       if (nodePath !== undefined) {
333         promise = Files.resolve('eslint', nodePath, nodePath, trace).then<string, string>(undefined, () => {
334           return Files.resolve('eslint', resolvedGlobalPackageManagerPath, directory, trace);
335         });
336       } else {
337         promise = Files.resolve('eslint', resolvedGlobalPackageManagerPath, directory, trace);
338       }
339
340       return promise.then(path => {
341         let library = path2Library.get(path)
342         if (!library) {
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`
348             )
349           } else {
350             connection.console.info(`ESLint library loaded from: ${path}`)
351             settings.library = library
352           }
353           path2Library.set(path, library)
354         } else {
355           settings.library = library
356         }
357         return settings
358       }, () => {
359         settings.validate = false
360         connection.sendRequest(NoESLintLibraryRequest.type, {
361           source: { uri: document.uri }
362         })
363         return settings
364       })
365     })
366   document2Settings.set(uri, resultPromise)
367   return resultPromise
368 }
369
370 interface Request<P, R> {
371   method: string
372   params: P
373   documentVersion: number | undefined
374   resolve: (value: R | Thenable<R>) => void | undefined
375   reject: (error: any) => void | undefined
376   token: CancellationToken | undefined
377 }
378
379 namespace Request {
380   export function is(value: any): value is Request<any, any> {
381     let candidate: Request<any, any> = value
382     return (
383       candidate &&
384       !!candidate.token &&
385       !!candidate.resolve &&
386       !!candidate.reject
387     )
388   }
389 }
390
391 interface Notifcation<P> {
392   method: string
393   params: P
394   documentVersion: number
395 }
396
397 type Message<P, R> = Notifcation<P> | Request<P, R>
398
399 type VersionProvider<P> = (params: P) => number
400
401 namespace Thenable {
402   export function is<T>(value: any): value is Thenable<T> {
403     let candidate: Thenable<T> = value
404     return candidate && typeof candidate.then === 'function'
405   }
406 }
407
408 class BufferedMessageQueue {
409   private queue: Message<any, any>[]
410   private requestHandlers: Map<
411     string,
412     {
413       handler: RequestHandler<any, any, any>
414       versionProvider?: VersionProvider<any>
415     }
416   >
417   private notificationHandlers: Map<
418     string,
419     { handler: NotificationHandler<any>; versionProvider?: VersionProvider<any> }
420   >
421   private timer: NodeJS.Immediate | undefined
422
423   constructor(private connection: IConnection) {
424     this.queue = []
425     this.requestHandlers = new Map()
426     this.notificationHandlers = new Map()
427   }
428
429   public registerRequest<P, R, E, RO>(
430     type: RequestType<P, R, E, RO>,
431     handler: RequestHandler<P, R, E>,
432     versionProvider?: VersionProvider<P>
433   ): void {
434     this.connection.onRequest(type, (params, token) => {
435       return new Promise<R>((resolve, reject) => {
436         this.queue.push({
437           method: type.method,
438           params,
439           documentVersion: versionProvider
440             ? versionProvider(params)
441             : undefined,
442           resolve,
443           reject,
444           token
445         })
446         this.trigger()
447       })
448     })
449     this.requestHandlers.set(type.method, { handler, versionProvider })
450   }
451
452   public registerNotification<P, RO>(
453     type: NotificationType<P, RO>,
454     handler: NotificationHandler<P>,
455     versionProvider?: (params: P) => number
456   ): void {
457     connection.onNotification(type, params => {
458       this.queue.push({
459         method: type.method,
460         params,
461         documentVersion: versionProvider ? versionProvider(params) : undefined
462       })
463       this.trigger()
464     })
465     this.notificationHandlers.set(type.method, { handler, versionProvider })
466   }
467
468   public addNotificationMessage<P, RO>(
469     type: NotificationType<P, RO>,
470     params: P,
471     version: number
472   ): void {
473     this.queue.push({
474       method: type.method,
475       params,
476       documentVersion: version
477     })
478     this.trigger()
479   }
480
481   public onNotification<P, RO>(
482     type: NotificationType<P, RO>,
483     handler: NotificationHandler<P>,
484     versionProvider?: (params: P) => number
485   ): void {
486     this.notificationHandlers.set(type.method, { handler, versionProvider })
487   }
488
489   private trigger(): void {
490     if (this.timer || this.queue.length === 0) {
491       return
492     }
493     this.timer = setImmediate(() => {
494       this.timer = undefined
495       this.processQueue()
496     })
497   }
498
499   private processQueue(): void {
500     let message = this.queue.shift()
501     if (!message) {
502       return
503     }
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
509           new ResponseError(
510             ErrorCodes.RequestCancelled,
511             'Request got cancelled'
512           )
513         )
514         return
515       }
516       let elem = this.requestHandlers.get(requestMessage.method)
517       if (
518         elem.versionProvider &&
519         requestMessage.documentVersion !== void 0 &&
520         requestMessage.documentVersion !==
521         elem.versionProvider(requestMessage.params)
522       ) {
523         requestMessage.reject(
524           // tslint:disable-next-line: no-inferred-empty-object-type
525           new ResponseError(
526             ErrorCodes.RequestCancelled,
527             'Request got cancelled'
528           )
529         )
530         return
531       }
532       let result = elem.handler(requestMessage.params, requestMessage.token)
533       if (Thenable.is(result)) {
534         result.then(
535           value => {
536             requestMessage.resolve(value)
537           },
538           error => {
539             requestMessage.reject(error)
540           }
541         )
542       } else {
543         requestMessage.resolve(result)
544       }
545     } else {
546       let notificationMessage = message
547       let elem = this.notificationHandlers.get(notificationMessage.method)
548       if (
549         elem.versionProvider &&
550         notificationMessage.documentVersion !== void 0 &&
551         notificationMessage.documentVersion !==
552         elem.versionProvider(notificationMessage.params)
553       ) {
554         return
555       }
556       elem.handler(notificationMessage.params)
557     }
558     this.trigger()
559   }
560 }
561
562 let messageQueue: BufferedMessageQueue = new BufferedMessageQueue(connection)
563
564 namespace ValidateNotification {
565   export const type: NotificationType<
566     TextDocument,
567     void
568   > = new NotificationType<TextDocument, void>('eslint/validate')
569 }
570
571 messageQueue.onNotification(
572   ValidateNotification.type,
573   document => {
574     validateSingle(document, true)
575   },
576   (document): number => {
577     return document.version
578   }
579 )
580
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) {
587       return
588     }
589     if (settings.run === 'onSave') {
590       messageQueue.addNotificationMessage(
591         ValidateNotification.type,
592         event.document,
593         event.document.version
594       )
595     }
596   })
597 })
598
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') {
603       return
604     }
605     messageQueue.addNotificationMessage(
606       ValidateNotification.type,
607       event.document,
608       event.document.version
609     )
610   })
611 })
612
613 documents.onWillSaveWaitUntil(event => {
614   if (event.reason === TextDocumentSaveReason.AfterDelay) {
615     return []
616   }
617
618   let document = event.document
619   return resolveSettings(document).then(settings => {
620     if (!settings.autoFixOnSave) {
621       return []
622     }
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))
628     } else {
629       return getAllFixEdits(document, settings)
630     }
631   })
632 })
633
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') {
638       return
639     }
640     messageQueue.addNotificationMessage(
641       ValidateNotification.type,
642       event.document,
643       event.document.version
644     )
645   })
646 })
647
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: [] })
655     }
656   })
657 })
658
659 function environmentChanged(): void {
660   document2Settings.clear()
661   for (let document of documents.all()) {
662     messageQueue.addNotificationMessage(
663       ValidateNotification.type,
664       document,
665       document.version
666     )
667   }
668 }
669
670 function trace(message: string, verbose?: string): void {
671   connection.tracer.log(message, verbose)
672 }
673
674 connection.onInitialize(_params => {
675   return {
676     capabilities: {
677       textDocumentSync: {
678         openClose: true,
679         change: TextDocumentSyncKind.Full,
680         willSaveWaitUntil: true,
681         save: {
682           includeText: false
683         }
684       },
685       codeActionProvider: true,
686       executeCommandProvider: {
687         commands: [
688           CommandIds.applySingleFix,
689           CommandIds.applySameFixes,
690           CommandIds.applyAllFixes,
691           CommandIds.applyAutoFix,
692           CommandIds.applyDisableLine,
693           CommandIds.applyDisableFile,
694           CommandIds.openRuleDoc,
695         ]
696       }
697     }
698   }
699 })
700
701 connection.onInitialized(() => {
702   connection.client.register(DidChangeConfigurationNotification.type, undefined)
703 })
704
705 messageQueue.registerNotification(
706   DidChangeConfigurationNotification.type,
707   _params => {
708     environmentChanged()
709   }
710 )
711
712 // messageQueue.registerNotification(
713 //   DidChangeWorkspaceFoldersNotification.type,
714 //   _params => {
715 //     environmentChanged()
716 //   }
717 // )
718
719 const singleErrorHandlers: ((
720   error: any,
721   document: TextDocument,
722   library: ESLintModule
723 ) => Status)[] = [
724     tryHandleNoConfig,
725     tryHandleConfigError,
726     tryHandleMissingModule,
727     showErrorMessage
728   ]
729
730 function validateSingle(
731   document: TextDocument,
732   publishDiagnostics = true
733 ): Thenable<void> {
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)
738   }
739   return resolveSettings(document).then(settings => {
740     if (!settings.validate) {
741       return
742     }
743     try {
744       validate(document, settings, publishDiagnostics)
745       connection.sendNotification(StatusNotification.type, { state: Status.ok })
746     } catch (err) {
747       let status
748       for (let handler of singleErrorHandlers) {
749         status = handler(err, document, settings.library)
750         if (status) {
751           break
752         }
753       }
754       status = status || Status.error
755       connection.sendNotification(StatusNotification.type, { state: status })
756     }
757   })
758 }
759
760 function validateMany(documents: TextDocument[]): void {
761   documents.forEach(document => {
762     messageQueue.addNotificationMessage(
763       ValidateNotification.type,
764       document,
765       document.version
766     )
767   })
768 }
769
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)
777     }
778   } else {
779     result = `An unknown error occured while validating document: ${
780       document.uri
781       }`
782   }
783   return result
784 }
785
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 => {
800           if (problem) {
801             const isWarning = convertSeverity(problem.severity) === DiagnosticSeverity.Warning
802             if (settings.quiet && isWarning) {
803               // Filter out warnings when quiet mode is enabled
804               return
805             }
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)
813                 }
814               } else {
815                 recordCodeAction(document, diagnostic, problem)
816               }
817             }
818           }
819         })
820       }
821     }
822     if (publishDiagnostics) {
823       connection.sendDiagnostics({ uri, diagnostics })
824     }
825
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)
832         }
833       })
834     }
835   })
836 }
837
838 let noConfigReported: Map<string, ESLintModule> = new Map<
839   string,
840   ESLintModule
841 >()
842
843 function isNoConfigFoundError(error: any): boolean {
844   let candidate = error as ESLintError
845   return (
846     candidate.messageTemplate === 'no-config-found' ||
847     candidate.message === 'No ESLint configuration found.'
848   )
849 }
850
851 function tryHandleNoConfig(
852   error: any,
853   document: TextDocument,
854   library: ESLintModule
855 ): Status {
856   if (!isNoConfigFoundError(error)) {
857     return undefined
858   }
859   if (!noConfigReported.has(document.uri)) {
860     connection
861       .sendRequest(NoConfigRequest.type, {
862         message: getMessage(error, document),
863         document: {
864           uri: document.uri
865         }
866       })
867       .then(undefined, () => {
868         // noop
869       })
870     noConfigReported.set(document.uri, library)
871   }
872   return Status.warn
873 }
874
875 let configErrorReported: Map<string, ESLintModule> = new Map<
876   string,
877   ESLintModule
878 >()
879
880 function tryHandleConfigError(
881   error: any,
882   document: TextDocument,
883   library: ESLintModule
884 ): Status {
885   if (!error.message) {
886     return undefined
887   }
888
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))
894       }
895       configErrorReported.set(filename, library)
896     }
897     return Status.warn
898   }
899
900   let matches = /Cannot read config file:\s+(.*)\nError:\s+(.*)/.exec(
901     error.message
902   )
903   if (matches && matches.length === 3) {
904     return handleFileName(matches[1])
905   }
906
907   matches = /(.*):\n\s*Configuration for rule \"(.*)\" is /.exec(error.message)
908   if (matches && matches.length === 3) {
909     return handleFileName(matches[1])
910   }
911
912   matches = /Cannot find module '([^']*)'\nReferenced from:\s+(.*)/.exec(
913     error.message
914   )
915   if (matches && matches.length === 3) {
916     return handleFileName(matches[2])
917   }
918
919   return undefined
920 }
921
922 let missingModuleReported: Map<string, ESLintModule> = new Map<
923   string,
924   ESLintModule
925 >()
926
927 function tryHandleMissingModule(
928   error: any,
929   document: TextDocument,
930   library: ESLintModule
931 ): Status {
932   if (!error.message) {
933     return undefined
934   }
935
936   function handleMissingModule(
937     plugin: string,
938     module: string,
939     error: ESLintError
940   ): Status {
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(
946           [
947             '',
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.`,
954             '',
955             `Consider running eslint --debug ${
956             fsPath ? fsPath : document.uri
957             } from a terminal to obtain a trace about the configuration files used.`
958           ].join('\n')
959         )
960       } else {
961         connection.console.error(
962           [
963             `${error.message.toString()}`,
964             `Happend while validating ${fsPath ? fsPath : document.uri}`
965           ].join('\n')
966         )
967       }
968     }
969     return Status.warn
970   }
971
972   let matches = /Failed to load plugin (.*): Cannot find module (.*)/.exec(
973     error.message
974   )
975   if (matches && matches.length === 3) {
976     return handleMissingModule(matches[1], matches[2], error)
977   }
978
979   return undefined
980 }
981
982 function showErrorMessage(error: any, document: TextDocument): Status {
983   connection.window.showErrorMessage(
984     `ESLint: ${getMessage(
985       error,
986       document
987     )}. Please see the 'ESLint' output channel for details.`
988   )
989   if (Is.string(error.stack)) {
990     connection.console.error('ESLint stack trace:')
991     connection.console.error(error.stack)
992   }
993   return Status.error
994 }
995
996 messageQueue.registerNotification(
997   DidChangeWatchedFilesNotification.type,
998   params => {
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)) {
1006         return
1007       }
1008       let dirname = path.dirname(fsPath)
1009       if (dirname) {
1010         let library = configErrorReported.get(fsPath)
1011         if (library) {
1012           let cli = new library.CLIEngine({})
1013           try {
1014             cli.executeOnText('', path.join(dirname, '___test___.js'))
1015             configErrorReported.delete(fsPath)
1016           } catch (error) {
1017             // noop
1018           }
1019         }
1020       }
1021     })
1022     validateMany(documents.all())
1023   }
1024 )
1025
1026 class Fixes {
1027   constructor(private edits: Map<string, FixableProblem>) { }
1028
1029   public static overlaps(lastEdit: FixableProblem, newEdit: FixableProblem): boolean {
1030     return !!lastEdit && lastEdit.edit.range[1] > newEdit.edit.range[0]
1031   }
1032
1033   public isEmpty(): boolean {
1034     return this.edits.size === 0
1035   }
1036
1037   public getDocumentVersion(): number {
1038     if (this.isEmpty()) {
1039       throw new Error('No edits recorded.')
1040     }
1041     return this.edits.values().next().value.documentVersion
1042   }
1043
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)
1049       if (editInfo) {
1050         result.push(editInfo)
1051       }
1052     }
1053     return result
1054   }
1055
1056   public getAllSorted(): FixableProblem[] {
1057     let result: FixableProblem[] = []
1058     this.edits.forEach(value => {
1059       if (value.edit != null) {
1060         result.push(value)
1061       }
1062     })
1063     return result.sort((a, b) => {
1064       let d = a.edit.range[0] - b.edit.range[0]
1065       if (d !== 0) {
1066         return d
1067       }
1068       if (a.edit.range[1] === 0) {
1069         return -1
1070       }
1071       if (b.edit.range[1] === 0) {
1072         return 1
1073       }
1074       return a.edit.range[1] - b.edit.range[1]
1075     })
1076   }
1077
1078   public getOverlapFree(): FixableProblem[] {
1079     let sorted = this.getAllSorted()
1080     if (sorted.length <= 1) {
1081       return sorted
1082     }
1083     let result: FixableProblem[] = []
1084     let last: FixableProblem = sorted[0]
1085     result.push(last)
1086     for (let i = 1; i < sorted.length; i++) {
1087       let current = sorted[i]
1088       if (!Fixes.overlaps(last, current)) {
1089         result.push(current)
1090         last = current
1091       }
1092     }
1093     return result
1094   }
1095 }
1096
1097 let commands: Map<string, WorkspaceChange>
1098 messageQueue.registerRequest(
1099   CodeActionRequest.type,
1100   params => {
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 []
1108
1109     let textDocument = documents.get(uri)
1110     let documentVersion = -1
1111     let allFixableRuleIds: string[] = []
1112
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 || '')
1115     }
1116
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`)
1119     }
1120
1121     function createDisableSameLineTextEdit(editInfo: FixableProblem): TextEdit {
1122       return TextEdit.insert(Position.create(editInfo.line - 1, Number.MAX_VALUE), ` // eslint-disable-line ${editInfo.ruleId}`)
1123     }
1124
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`)
1130     }
1131
1132     function getLastEdit(array: FixableProblem[]): FixableProblem {
1133       let length = array.length
1134       if (length === 0) {
1135         return undefined
1136       }
1137       return array[length - 1]
1138     }
1139
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)
1145
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(
1151             editInfo.label,
1152             Command.create(editInfo.label, CommandIds.applySingleFix, ruleId),
1153             CodeActionKind.QuickFix
1154           )
1155           action.isPreferred = true
1156           result.get(ruleId).fixes.push(action)
1157         }
1158
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))
1163           } else {
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))
1167           }
1168           commands.set(`${CommandIds.applyDisableLine}:${ruleId}`, workspaceChange)
1169           let title = `Disable ${ruleId} for this line`
1170           result.get(ruleId).disable = CodeAction.create(
1171             title,
1172             Command.create(title, CommandIds.applyDisableLine, ruleId),
1173             CodeActionKind.QuickFix
1174           )
1175
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(
1182               title,
1183               Command.create(title, CommandIds.applyDisableFile, ruleId),
1184               CodeActionKind.QuickFix
1185             )
1186           }
1187         }
1188
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(
1193               title,
1194               Command.create(title, CommandIds.openRuleDoc, ruleId),
1195               CodeActionKind.QuickFix
1196             )
1197           }
1198         }
1199       }
1200
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[] = []
1204
1205         for (let editInfo of fixes.getAllSorted()) {
1206           if (documentVersion === -1) {
1207             documentVersion = editInfo.documentVersion
1208           }
1209           if (sameProblems.has(editInfo.ruleId)) {
1210             let same = sameProblems.get(editInfo.ruleId)
1211             if (!Fixes.overlaps(getLastEdit(same), editInfo)) {
1212               same.push(editInfo)
1213             }
1214           }
1215           if (!Fixes.overlaps(getLastEdit(all), editInfo)) {
1216             all.push(editInfo)
1217           }
1218         }
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(
1228               title,
1229               command,
1230               CodeActionKind.QuickFix
1231             )
1232           }
1233         })
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(
1242             title,
1243             command,
1244             CodeActionKind.SourceFixAll
1245           )
1246         }
1247       }
1248       return result.all()
1249     })
1250   },
1251   (params): number => {
1252     let document = documents.get(params.textDocument.uri)
1253     return document ? document.version : undefined
1254   }
1255 )
1256
1257 messageQueue.registerRequest(
1258   ExecuteCommandRequest.type,
1259   async params => {
1260     let workspaceChange: WorkspaceChange
1261     if (params.command === CommandIds.applyAutoFix) {
1262       let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
1263       if (!identifier.uri.startsWith('file:')) {
1264         return {}
1265       }
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))
1273       }
1274     } else {
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)
1281         if (url) {
1282           await connection.sendRequest(OpenESLintDocRequest.type, { url })
1283         }
1284       } else {
1285         workspaceChange = commands.get(params.command)
1286       }
1287     }
1288
1289     if (!workspaceChange) {
1290       return {}
1291     }
1292     try {
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}`)
1296       }
1297     } catch (e) {
1298       connection.console.error(`Failed to apply command: ${params.command}`)
1299     }
1300     return {}
1301   },
1302   (params): number => {
1303     if (params.command === CommandIds.applyAutoFix) {
1304       let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
1305       return identifier.version
1306     } else {
1307       return undefined
1308     }
1309   }
1310 )
1311
1312 connection.tracer.connection.listen()