Actualizacion maquina principal
[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, resolveModule } 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
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       } else if (settings.packageManager === 'npm') {
315         nodePath = globalNpmPath()
316       } else if (settings.packageManager === 'yarn') {
317         nodePath = globalYarnPath()
318       }
319       let uri = URI.parse(document.uri)
320       let promise: Thenable<string>
321       let directory: string
322       if (uri.scheme === 'file') {
323         directory = path.dirname(uri.fsPath)
324       } else {
325         directory = settings.workspaceFolder ? URI.parse(settings.workspaceFolder.uri).fsPath : undefined
326       }
327       promise = resolveModule('./eslint', directory, nodePath).catch(() => {
328         return resolveModule('eslint', directory, nodePath)
329       })
330       return promise.then(path => {
331         let library = path2Library.get(path)
332         if (!library) {
333           library = requireFunc(path)
334           if (!library.CLIEngine) {
335             settings.validate = false
336             connection.console.error(
337               `The eslint library loaded from ${path} doesn\'t export a CLIEngine. You need at least eslint@1.0.0`
338             )
339           } else {
340             connection.console.info(`ESLint library loaded from: ${path}`)
341             settings.library = library
342           }
343           path2Library.set(path, library)
344         } else {
345           settings.library = library
346         }
347         return settings
348       }, () => {
349         settings.validate = false
350         connection.sendRequest(NoESLintLibraryRequest.type, {
351           source: { uri: document.uri }
352         })
353         return settings
354       }
355       )
356     })
357   document2Settings.set(uri, resultPromise)
358   return resultPromise
359 }
360
361 interface Request<P, R> {
362   method: string
363   params: P
364   documentVersion: number | undefined
365   resolve: (value: R | Thenable<R>) => void | undefined
366   reject: (error: any) => void | undefined
367   token: CancellationToken | undefined
368 }
369
370 namespace Request {
371   export function is(value: any): value is Request<any, any> {
372     let candidate: Request<any, any> = value
373     return (
374       candidate &&
375       !!candidate.token &&
376       !!candidate.resolve &&
377       !!candidate.reject
378     )
379   }
380 }
381
382 interface Notifcation<P> {
383   method: string
384   params: P
385   documentVersion: number
386 }
387
388 type Message<P, R> = Notifcation<P> | Request<P, R>
389
390 type VersionProvider<P> = (params: P) => number
391
392 namespace Thenable {
393   export function is<T>(value: any): value is Thenable<T> {
394     let candidate: Thenable<T> = value
395     return candidate && typeof candidate.then === 'function'
396   }
397 }
398
399 class BufferedMessageQueue {
400   private queue: Message<any, any>[]
401   private requestHandlers: Map<
402     string,
403     {
404       handler: RequestHandler<any, any, any>
405       versionProvider?: VersionProvider<any>
406     }
407   >
408   private notificationHandlers: Map<
409     string,
410     { handler: NotificationHandler<any>; versionProvider?: VersionProvider<any> }
411   >
412   private timer: NodeJS.Immediate | undefined
413
414   constructor(private connection: IConnection) {
415     this.queue = []
416     this.requestHandlers = new Map()
417     this.notificationHandlers = new Map()
418   }
419
420   public registerRequest<P, R, E, RO>(
421     type: RequestType<P, R, E, RO>,
422     handler: RequestHandler<P, R, E>,
423     versionProvider?: VersionProvider<P>
424   ): void {
425     this.connection.onRequest(type, (params, token) => {
426       return new Promise<R>((resolve, reject) => {
427         this.queue.push({
428           method: type.method,
429           params,
430           documentVersion: versionProvider
431             ? versionProvider(params)
432             : undefined,
433           resolve,
434           reject,
435           token
436         })
437         this.trigger()
438       })
439     })
440     this.requestHandlers.set(type.method, { handler, versionProvider })
441   }
442
443   public registerNotification<P, RO>(
444     type: NotificationType<P, RO>,
445     handler: NotificationHandler<P>,
446     versionProvider?: (params: P) => number
447   ): void {
448     connection.onNotification(type, params => {
449       this.queue.push({
450         method: type.method,
451         params,
452         documentVersion: versionProvider ? versionProvider(params) : undefined
453       })
454       this.trigger()
455     })
456     this.notificationHandlers.set(type.method, { handler, versionProvider })
457   }
458
459   public addNotificationMessage<P, RO>(
460     type: NotificationType<P, RO>,
461     params: P,
462     version: number
463   ): void {
464     this.queue.push({
465       method: type.method,
466       params,
467       documentVersion: version
468     })
469     this.trigger()
470   }
471
472   public onNotification<P, RO>(
473     type: NotificationType<P, RO>,
474     handler: NotificationHandler<P>,
475     versionProvider?: (params: P) => number
476   ): void {
477     this.notificationHandlers.set(type.method, { handler, versionProvider })
478   }
479
480   private trigger(): void {
481     if (this.timer || this.queue.length === 0) {
482       return
483     }
484     this.timer = setImmediate(() => {
485       this.timer = undefined
486       this.processQueue()
487     })
488   }
489
490   private processQueue(): void {
491     let message = this.queue.shift()
492     if (!message) {
493       return
494     }
495     if (Request.is(message)) {
496       let requestMessage = message
497       if (requestMessage.token.isCancellationRequested) {
498         requestMessage.reject(
499           // tslint:disable-next-line: no-inferred-empty-object-type
500           new ResponseError(
501             ErrorCodes.RequestCancelled,
502             'Request got cancelled'
503           )
504         )
505         return
506       }
507       let elem = this.requestHandlers.get(requestMessage.method)
508       if (
509         elem.versionProvider &&
510         requestMessage.documentVersion !== void 0 &&
511         requestMessage.documentVersion !==
512         elem.versionProvider(requestMessage.params)
513       ) {
514         requestMessage.reject(
515           // tslint:disable-next-line: no-inferred-empty-object-type
516           new ResponseError(
517             ErrorCodes.RequestCancelled,
518             'Request got cancelled'
519           )
520         )
521         return
522       }
523       let result = elem.handler(requestMessage.params, requestMessage.token)
524       if (Thenable.is(result)) {
525         result.then(
526           value => {
527             requestMessage.resolve(value)
528           },
529           error => {
530             requestMessage.reject(error)
531           }
532         )
533       } else {
534         requestMessage.resolve(result)
535       }
536     } else {
537       let notificationMessage = message
538       let elem = this.notificationHandlers.get(notificationMessage.method)
539       if (
540         elem.versionProvider &&
541         notificationMessage.documentVersion !== void 0 &&
542         notificationMessage.documentVersion !==
543         elem.versionProvider(notificationMessage.params)
544       ) {
545         return
546       }
547       elem.handler(notificationMessage.params)
548     }
549     this.trigger()
550   }
551 }
552
553 let messageQueue: BufferedMessageQueue = new BufferedMessageQueue(connection)
554
555 namespace ValidateNotification {
556   export const type: NotificationType<
557     TextDocument,
558     void
559   > = new NotificationType<TextDocument, void>('eslint/validate')
560 }
561
562 messageQueue.onNotification(
563   ValidateNotification.type,
564   document => {
565     validateSingle(document, true)
566   },
567   (document): number => {
568     return document.version
569   }
570 )
571
572 // The documents manager listen for text document create, change
573 // and close on the connection
574 documents.listen(connection)
575 documents.onDidOpen(event => {
576   resolveSettings(event.document).then(settings => {
577     if (!settings.validate) {
578       return
579     }
580     if (settings.run === 'onSave') {
581       messageQueue.addNotificationMessage(
582         ValidateNotification.type,
583         event.document,
584         event.document.version
585       )
586     }
587   })
588 })
589
590 // A text document has changed. Validate the document according the run setting.
591 documents.onDidChangeContent(event => {
592   resolveSettings(event.document).then(settings => {
593     if (!settings.validate || settings.run !== 'onType') {
594       return
595     }
596     messageQueue.addNotificationMessage(
597       ValidateNotification.type,
598       event.document,
599       event.document.version
600     )
601   })
602 })
603
604 documents.onWillSaveWaitUntil(event => {
605   if (event.reason === TextDocumentSaveReason.AfterDelay) {
606     return []
607   }
608
609   let document = event.document
610   return resolveSettings(document).then(settings => {
611     if (!settings.autoFixOnSave) {
612       return []
613     }
614     // If we validate on save and want to apply fixes on will save
615     // we need to validate the file.
616     if (settings.run === 'onSave') {
617       // Do not queue this since we want to get the fixes as fast as possible.
618       return validateSingle(document, false).then(() => getAllFixEdits(document, settings))
619     } else {
620       return getAllFixEdits(document, settings)
621     }
622   })
623 })
624
625 // A text document has been saved. Validate the document according the run setting.
626 documents.onDidSave(event => {
627   resolveSettings(event.document).then(settings => {
628     if (!settings.validate || settings.run !== 'onSave') {
629       return
630     }
631     messageQueue.addNotificationMessage(
632       ValidateNotification.type,
633       event.document,
634       event.document.version
635     )
636   })
637 })
638
639 documents.onDidClose(event => {
640   resolveSettings(event.document).then(settings => {
641     let uri = event.document.uri
642     document2Settings.delete(uri)
643     codeActions.delete(uri)
644     if (settings.validate) {
645       connection.sendDiagnostics({ uri, diagnostics: [] })
646     }
647   })
648 })
649
650 function environmentChanged(): void {
651   document2Settings.clear()
652   for (let document of documents.all()) {
653     messageQueue.addNotificationMessage(
654       ValidateNotification.type,
655       document,
656       document.version
657     )
658   }
659 }
660
661 function trace(message: string, verbose?: string): void {
662   connection.tracer.log(message, verbose)
663 }
664
665 connection.onInitialize(_params => {
666   return {
667     capabilities: {
668       textDocumentSync: {
669         openClose: true,
670         change: TextDocumentSyncKind.Full,
671         willSaveWaitUntil: true,
672         save: {
673           includeText: false
674         }
675       },
676       codeActionProvider: true,
677       executeCommandProvider: {
678         commands: [
679           CommandIds.applySingleFix,
680           CommandIds.applySameFixes,
681           CommandIds.applyAllFixes,
682           CommandIds.applyAutoFix,
683           CommandIds.applyDisableLine,
684           CommandIds.applyDisableFile,
685           CommandIds.openRuleDoc,
686         ]
687       }
688     }
689   }
690 })
691
692 connection.onInitialized(() => {
693   connection.client.register(DidChangeConfigurationNotification.type, undefined)
694 })
695
696 messageQueue.registerNotification(
697   DidChangeConfigurationNotification.type,
698   _params => {
699     environmentChanged()
700   }
701 )
702
703 // messageQueue.registerNotification(
704 //   DidChangeWorkspaceFoldersNotification.type,
705 //   _params => {
706 //     environmentChanged()
707 //   }
708 // )
709
710 const singleErrorHandlers: ((
711   error: any,
712   document: TextDocument,
713   library: ESLintModule
714 ) => Status)[] = [
715     tryHandleNoConfig,
716     tryHandleConfigError,
717     tryHandleMissingModule,
718     showErrorMessage
719   ]
720
721 function validateSingle(
722   document: TextDocument,
723   publishDiagnostics = true
724 ): Thenable<void> {
725   // We validate document in a queue but open / close documents directly. So we need to deal with the
726   // fact that a document might be gone from the server.
727   if (!documents.get(document.uri)) {
728     return Promise.resolve(undefined)
729   }
730   return resolveSettings(document).then(settings => {
731     if (!settings.validate) {
732       return
733     }
734     try {
735       validate(document, settings, publishDiagnostics)
736       connection.sendNotification(StatusNotification.type, { state: Status.ok })
737     } catch (err) {
738       let status
739       for (let handler of singleErrorHandlers) {
740         status = handler(err, document, settings.library)
741         if (status) {
742           break
743         }
744       }
745       status = status || Status.error
746       connection.sendNotification(StatusNotification.type, { state: status })
747     }
748   })
749 }
750
751 function validateMany(documents: TextDocument[]): void {
752   documents.forEach(document => {
753     messageQueue.addNotificationMessage(
754       ValidateNotification.type,
755       document,
756       document.version
757     )
758   })
759 }
760
761 function getMessage(err: any, document: TextDocument): string {
762   let result: string = null
763   if (typeof err.message === 'string' || err.message instanceof String) {
764     result = err.message as string
765     result = result.replace(/\r?\n/g, ' ')
766     if (/^CLI: /.test(result)) {
767       result = result.substr(5)
768     }
769   } else {
770     result = `An unknown error occured while validating document: ${
771       document.uri
772       }`
773   }
774   return result
775 }
776
777 function validate(document: TextDocument, settings: TextDocumentSettings, publishDiagnostics = true): void {
778   const uri = document.uri
779   const content = document.getText()
780   const newOptions: CLIOptions = Object.assign(Object.create(null), settings.options)
781   executeInWorkspaceDirectory(document, settings, newOptions, (file: string, options: CLIOptions) => {
782     const cli = new settings.library.CLIEngine(options)
783     // Clean previously computed code actions.
784     codeActions.delete(uri)
785     const report: ESLintReport = cli.executeOnText(content, file)
786     const diagnostics: Diagnostic[] = []
787     if (report && report.results && Array.isArray(report.results) && report.results.length > 0) {
788       const docReport = report.results[0]
789       if (docReport.messages && Array.isArray(docReport.messages)) {
790         docReport.messages.forEach(problem => {
791           if (problem) {
792             const isWarning = convertSeverity(problem.severity) === DiagnosticSeverity.Warning
793             if (settings.quiet && isWarning) {
794               // Filter out warnings when quiet mode is enabled
795               return
796             }
797             const diagnostic = makeDiagnostic(problem)
798             diagnostics.push(diagnostic)
799             if (settings.autoFix) {
800               if (typeof cli.getRules === 'function' && problem.ruleId !== undefined && problem.fix !== undefined) {
801                 const rule = cli.getRules().get(problem.ruleId)
802                 if (rule !== undefined && rule.meta && typeof rule.meta.fixable == 'string') {
803                   recordCodeAction(document, diagnostic, problem)
804                 }
805               } else {
806                 recordCodeAction(document, diagnostic, problem)
807               }
808             }
809           }
810         })
811       }
812     }
813     if (publishDiagnostics) {
814       connection.sendDiagnostics({ uri, diagnostics })
815     }
816
817     // cache documentation urls for all rules
818     if (typeof cli.getRules === 'function' && !ruleDocData.handled.has(uri)) {
819       ruleDocData.handled.add(uri)
820       cli.getRules().forEach((rule, key) => {
821         if (rule.meta && rule.meta.docs && Is.string(rule.meta.docs.url)) {
822           ruleDocData.urls.set(key, rule.meta.docs.url)
823         }
824       })
825     }
826   })
827 }
828
829 let noConfigReported: Map<string, ESLintModule> = new Map<
830   string,
831   ESLintModule
832 >()
833
834 function isNoConfigFoundError(error: any): boolean {
835   let candidate = error as ESLintError
836   return (
837     candidate.messageTemplate === 'no-config-found' ||
838     candidate.message === 'No ESLint configuration found.'
839   )
840 }
841
842 function tryHandleNoConfig(
843   error: any,
844   document: TextDocument,
845   library: ESLintModule
846 ): Status {
847   if (!isNoConfigFoundError(error)) {
848     return undefined
849   }
850   if (!noConfigReported.has(document.uri)) {
851     connection
852       .sendRequest(NoConfigRequest.type, {
853         message: getMessage(error, document),
854         document: {
855           uri: document.uri
856         }
857       })
858       .then(undefined, () => {
859         // noop
860       })
861     noConfigReported.set(document.uri, library)
862   }
863   return Status.warn
864 }
865
866 let configErrorReported: Map<string, ESLintModule> = new Map<
867   string,
868   ESLintModule
869 >()
870
871 function tryHandleConfigError(
872   error: any,
873   document: TextDocument,
874   library: ESLintModule
875 ): Status {
876   if (!error.message) {
877     return undefined
878   }
879
880   function handleFileName(filename: string): Status {
881     if (!configErrorReported.has(filename)) {
882       connection.console.error(getMessage(error, document))
883       if (!documents.get(URI.file(filename).toString())) {
884         connection.window.showInformationMessage(getMessage(error, document))
885       }
886       configErrorReported.set(filename, library)
887     }
888     return Status.warn
889   }
890
891   let matches = /Cannot read config file:\s+(.*)\nError:\s+(.*)/.exec(
892     error.message
893   )
894   if (matches && matches.length === 3) {
895     return handleFileName(matches[1])
896   }
897
898   matches = /(.*):\n\s*Configuration for rule \"(.*)\" is /.exec(error.message)
899   if (matches && matches.length === 3) {
900     return handleFileName(matches[1])
901   }
902
903   matches = /Cannot find module '([^']*)'\nReferenced from:\s+(.*)/.exec(
904     error.message
905   )
906   if (matches && matches.length === 3) {
907     return handleFileName(matches[2])
908   }
909
910   return undefined
911 }
912
913 let missingModuleReported: Map<string, ESLintModule> = new Map<
914   string,
915   ESLintModule
916 >()
917
918 function tryHandleMissingModule(
919   error: any,
920   document: TextDocument,
921   library: ESLintModule
922 ): Status {
923   if (!error.message) {
924     return undefined
925   }
926
927   function handleMissingModule(
928     plugin: string,
929     module: string,
930     error: ESLintError
931   ): Status {
932     if (!missingModuleReported.has(plugin)) {
933       let fsPath = getFilePath(document)
934       missingModuleReported.set(plugin, library)
935       if (error.messageTemplate === 'plugin-missing') {
936         connection.console.error(
937           [
938             '',
939             `${error.message.toString()}`,
940             `Happened while validating ${fsPath ? fsPath : document.uri}`,
941             `This can happen for a couple of reasons:`,
942             `1. The plugin name is spelled incorrectly in an ESLint configuration file (e.g. .eslintrc).`,
943             `2. If ESLint is installed globally, then make sure ${module} is installed globally as well.`,
944             `3. If ESLint is installed locally, then ${module} isn't installed correctly.`,
945             '',
946             `Consider running eslint --debug ${
947             fsPath ? fsPath : document.uri
948             } from a terminal to obtain a trace about the configuration files used.`
949           ].join('\n')
950         )
951       } else {
952         connection.console.error(
953           [
954             `${error.message.toString()}`,
955             `Happend while validating ${fsPath ? fsPath : document.uri}`
956           ].join('\n')
957         )
958       }
959     }
960     return Status.warn
961   }
962
963   let matches = /Failed to load plugin (.*): Cannot find module (.*)/.exec(
964     error.message
965   )
966   if (matches && matches.length === 3) {
967     return handleMissingModule(matches[1], matches[2], error)
968   }
969
970   return undefined
971 }
972
973 function showErrorMessage(error: any, document: TextDocument): Status {
974   connection.window.showErrorMessage(
975     `ESLint: ${getMessage(
976       error,
977       document
978     )}. Please see the 'ESLint' output channel for details.`
979   )
980   if (Is.string(error.stack)) {
981     connection.console.error('ESLint stack trace:')
982     connection.console.error(error.stack)
983   }
984   return Status.error
985 }
986
987 messageQueue.registerNotification(
988   DidChangeWatchedFilesNotification.type,
989   params => {
990     // A .eslintrc has change. No smartness here.
991     // Simply revalidate all file.
992     noConfigReported = new Map<string, ESLintModule>()
993     missingModuleReported = new Map<string, ESLintModule>()
994     params.changes.forEach(change => {
995       let fsPath = getFilePath(change.uri)
996       if (!fsPath || isUNC(fsPath)) {
997         return
998       }
999       let dirname = path.dirname(fsPath)
1000       if (dirname) {
1001         let library = configErrorReported.get(fsPath)
1002         if (library) {
1003           let cli = new library.CLIEngine({})
1004           try {
1005             cli.executeOnText('', path.join(dirname, '___test___.js'))
1006             configErrorReported.delete(fsPath)
1007           } catch (error) {
1008             // noop
1009           }
1010         }
1011       }
1012     })
1013     validateMany(documents.all())
1014   }
1015 )
1016
1017 class Fixes {
1018   constructor(private edits: Map<string, FixableProblem>) { }
1019
1020   public static overlaps(lastEdit: FixableProblem, newEdit: FixableProblem): boolean {
1021     return !!lastEdit && lastEdit.edit.range[1] > newEdit.edit.range[0]
1022   }
1023
1024   public isEmpty(): boolean {
1025     return this.edits.size === 0
1026   }
1027
1028   public getDocumentVersion(): number {
1029     if (this.isEmpty()) {
1030       throw new Error('No edits recorded.')
1031     }
1032     return this.edits.values().next().value.documentVersion
1033   }
1034
1035   public getScoped(diagnostics: Diagnostic[]): FixableProblem[] {
1036     let result: FixableProblem[] = []
1037     for (let diagnostic of diagnostics) {
1038       let key = computeKey(diagnostic)
1039       let editInfo = this.edits.get(key)
1040       if (editInfo) {
1041         result.push(editInfo)
1042       }
1043     }
1044     return result
1045   }
1046
1047   public getAllSorted(): FixableProblem[] {
1048     let result: FixableProblem[] = []
1049     this.edits.forEach(value => result.push(value))
1050     return result.sort((a, b) => {
1051       let d = a.edit.range[0] - b.edit.range[0]
1052       if (d !== 0) {
1053         return d
1054       }
1055       if (a.edit.range[1] === 0) {
1056         return -1
1057       }
1058       if (b.edit.range[1] === 0) {
1059         return 1
1060       }
1061       return a.edit.range[1] - b.edit.range[1]
1062     })
1063   }
1064
1065   public getOverlapFree(): FixableProblem[] {
1066     let sorted = this.getAllSorted()
1067     if (sorted.length <= 1) {
1068       return sorted
1069     }
1070     let result: FixableProblem[] = []
1071     let last: FixableProblem = sorted[0]
1072     result.push(last)
1073     for (let i = 1; i < sorted.length; i++) {
1074       let current = sorted[i]
1075       if (!Fixes.overlaps(last, current)) {
1076         result.push(current)
1077         last = current
1078       }
1079     }
1080     return result
1081   }
1082 }
1083
1084 let commands: Map<string, WorkspaceChange>
1085 messageQueue.registerRequest(
1086   CodeActionRequest.type,
1087   params => {
1088     commands = new Map<string, WorkspaceChange>()
1089     let result: CodeActionResult = new CodeActionResult()
1090     let uri = params.textDocument.uri
1091     let edits = codeActions.get(uri)
1092     if (!edits) return []
1093     let fixes = new Fixes(edits)
1094     if (fixes.isEmpty()) return []
1095
1096     let textDocument = documents.get(uri)
1097     let documentVersion = -1
1098     let allFixableRuleIds: string[] = []
1099
1100     function createTextEdit(editInfo: FixableProblem): TextEdit {
1101       return TextEdit.replace(Range.create(textDocument.positionAt(editInfo.edit.range[0]), textDocument.positionAt(editInfo.edit.range[1])), editInfo.edit.text || '')
1102     }
1103
1104     function createDisableLineTextEdit(editInfo: FixableProblem, indentationText: string): TextEdit {
1105       return TextEdit.insert(Position.create(editInfo.line - 1, 0), `${indentationText}// eslint-disable-next-line ${editInfo.ruleId}\n`)
1106     }
1107
1108     function createDisableSameLineTextEdit(editInfo: FixableProblem): TextEdit {
1109       return TextEdit.insert(Position.create(editInfo.line - 1, Number.MAX_VALUE), ` // eslint-disable-line ${editInfo.ruleId}`)
1110     }
1111
1112     function createDisableFileTextEdit(editInfo: FixableProblem): TextEdit {
1113       return TextEdit.insert(Position.create(0, 0), `/* eslint-disable ${editInfo.ruleId} */\n`)
1114     }
1115
1116     function getLastEdit(array: FixableProblem[]): FixableProblem {
1117       let length = array.length
1118       if (length === 0) {
1119         return undefined
1120       }
1121       return array[length - 1]
1122     }
1123
1124     return resolveSettings(textDocument).then(settings => {
1125       for (let editInfo of fixes.getScoped(params.context.diagnostics)) {
1126         documentVersion = editInfo.documentVersion
1127         let ruleId = editInfo.ruleId
1128         allFixableRuleIds.push(ruleId)
1129
1130         if (!!editInfo.edit) {
1131           let workspaceChange = new WorkspaceChange()
1132           workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createTextEdit(editInfo))
1133           commands.set(`${CommandIds.applySingleFix}:${ruleId}`, workspaceChange)
1134           result.get(ruleId).fixes.push(CodeAction.create(
1135             editInfo.label,
1136             Command.create(editInfo.label, CommandIds.applySingleFix, ruleId),
1137             CodeActionKind.QuickFix
1138           ))
1139         }
1140
1141         if (settings.codeAction.disableRuleComment.enable) {
1142           let workspaceChange = new WorkspaceChange()
1143           if (settings.codeAction.disableRuleComment.location === 'sameLine') {
1144             workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableSameLineTextEdit(editInfo))
1145           } else {
1146             let lineText = textDocument.getText(Range.create(Position.create(editInfo.line - 1, 0), Position.create(editInfo.line - 1, Number.MAX_VALUE)))
1147             let indentationText = /^([ \t]*)/.exec(lineText)[1]
1148             workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableLineTextEdit(editInfo, indentationText))
1149           }
1150           commands.set(`${CommandIds.applyDisableLine}:${ruleId}`, workspaceChange)
1151           let title = `Disable ${ruleId} for this line`
1152           result.get(ruleId).disable = CodeAction.create(
1153             title,
1154             Command.create(title, CommandIds.applyDisableLine, ruleId),
1155             CodeActionKind.QuickFix
1156           )
1157
1158           if (result.get(ruleId).disableFile === undefined) {
1159             workspaceChange = new WorkspaceChange()
1160             workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableFileTextEdit(editInfo))
1161             commands.set(`${CommandIds.applyDisableFile}:${ruleId}`, workspaceChange)
1162             title = `Disable ${ruleId} for the entire file`
1163             result.get(ruleId).disableFile = CodeAction.create(
1164               title,
1165               Command.create(title, CommandIds.applyDisableFile, ruleId),
1166               CodeActionKind.QuickFix
1167             )
1168           }
1169         }
1170
1171         if (settings.codeAction.showDocumentation.enable && result.get(ruleId).showDocumentation === undefined) {
1172           if (ruleDocData.urls.has(ruleId)) {
1173             let title = `Show documentation for ${ruleId}`
1174             result.get(ruleId).showDocumentation = CodeAction.create(
1175               title,
1176               Command.create(title, CommandIds.openRuleDoc, ruleId),
1177               CodeActionKind.QuickFix
1178             )
1179           }
1180         }
1181       }
1182
1183       if (result.length > 0) {
1184         let sameProblems: Map<string, FixableProblem[]> = new Map<string, FixableProblem[]>(allFixableRuleIds.map<[string, FixableProblem[]]>(s => [s, []]))
1185         let all: FixableProblem[] = []
1186
1187         for (let editInfo of fixes.getAllSorted()) {
1188           if (documentVersion === -1) {
1189             documentVersion = editInfo.documentVersion
1190           }
1191           if (sameProblems.has(editInfo.ruleId)) {
1192             let same = sameProblems.get(editInfo.ruleId)
1193             if (!Fixes.overlaps(getLastEdit(same), editInfo)) {
1194               same.push(editInfo)
1195             }
1196           }
1197           if (!Fixes.overlaps(getLastEdit(all), editInfo)) {
1198             all.push(editInfo)
1199           }
1200         }
1201         sameProblems.forEach((same, ruleId) => {
1202           if (same.length > 1) {
1203             let sameFixes: WorkspaceChange = new WorkspaceChange()
1204             let sameTextChange = sameFixes.getTextEditChange({ uri, version: documentVersion })
1205             same.map(createTextEdit).forEach(edit => sameTextChange.add(edit))
1206             commands.set(CommandIds.applySameFixes, sameFixes)
1207             let title = `Fix all ${ruleId} problems`
1208             let command = Command.create(title, CommandIds.applySameFixes)
1209             result.get(ruleId).fixAll = CodeAction.create(
1210               title,
1211               command,
1212               CodeActionKind.QuickFix
1213             )
1214           }
1215         })
1216         if (all.length > 1) {
1217           let allFixes: WorkspaceChange = new WorkspaceChange()
1218           let allTextChange = allFixes.getTextEditChange({ uri, version: documentVersion })
1219           all.map(createTextEdit).forEach(edit => allTextChange.add(edit))
1220           commands.set(CommandIds.applyAllFixes, allFixes)
1221           let title = `Fix all auto-fixable problems`
1222           let command = Command.create(title, CommandIds.applyAllFixes)
1223           result.fixAll = CodeAction.create(
1224             title,
1225             command,
1226             CodeActionKind.SourceFixAll
1227           )
1228         }
1229       }
1230       return result.all()
1231     })
1232   },
1233   (params): number => {
1234     let document = documents.get(params.textDocument.uri)
1235     return document ? document.version : undefined
1236   }
1237 )
1238
1239 messageQueue.registerRequest(
1240   ExecuteCommandRequest.type,
1241   async params => {
1242     let workspaceChange: WorkspaceChange
1243     if (params.command === CommandIds.applyAutoFix) {
1244       let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
1245       if (!identifier.uri.startsWith('file:')) {
1246         return {}
1247       }
1248       let textDocument = documents.get(identifier.uri)
1249       let settings = await Promise.resolve(resolveSettings(textDocument))
1250       let edits = getAllFixEdits(textDocument, settings)
1251       if (edits && edits.length) {
1252         workspaceChange = new WorkspaceChange()
1253         let textChange = workspaceChange.getTextEditChange(identifier)
1254         edits.forEach(edit => textChange.add(edit))
1255       }
1256     } else {
1257       if ([CommandIds.applySingleFix, CommandIds.applyDisableLine, CommandIds.applyDisableFile].indexOf(params.command) !== -1) {
1258         let ruleId = params.arguments[0]
1259         workspaceChange = commands.get(`${params.command}:${ruleId}`)
1260       } else if (params.command === CommandIds.openRuleDoc) {
1261         let ruleId = params.arguments[0]
1262         let url = ruleDocData.urls.get(ruleId)
1263         if (url) {
1264           await connection.sendRequest(OpenESLintDocRequest.type, { url })
1265         }
1266       } else {
1267         workspaceChange = commands.get(params.command)
1268       }
1269     }
1270
1271     if (!workspaceChange) {
1272       return {}
1273     }
1274     try {
1275       let response = await Promise.resolve(connection.workspace.applyEdit(workspaceChange.edit))
1276       if (!response.applied) {
1277         connection.console.error(`Failed to apply command: ${params.command}`)
1278       }
1279     } catch (e) {
1280       connection.console.error(`Failed to apply command: ${params.command}`)
1281     }
1282     return {}
1283   },
1284   (params): number => {
1285     if (params.command === CommandIds.applyAutoFix) {
1286       let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
1287       return identifier.version
1288     } else {
1289       return undefined
1290     }
1291   }
1292 )
1293
1294 connection.tracer.connection.listen()