--- /dev/null
+/* --------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ * ------------------------------------------------------------------------------------------ */
+'use strict'
+
+import * as os from 'os'
+import * as path from 'path'
+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'
+import { URI } from 'vscode-uri'
+import { CLIOptions, ESLintAutoFixEdit, ESLintError, ESLintModule, ESLintProblem, ESLintReport, Is, TextDocumentSettings } from './types'
+import { getAllFixEdits, executeInWorkspaceDirectory, getFilePath, isUNC, resolveModule } from './util'
+import { TextDocument } from 'vscode-languageserver-textdocument'
+declare var __webpack_require__: any
+declare var __non_webpack_require__: any
+const requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : require
+
+namespace CommandIds {
+ export const applySingleFix = 'eslint.applySingleFix'
+ export const applySameFixes = 'eslint.applySameFixes'
+ export const applyAllFixes = 'eslint.applyAllFixes'
+ export const applyAutoFix = 'eslint.applyAutoFix'
+ export const applyDisableLine = 'eslint.applyDisableLine'
+ export const applyDisableFile = 'eslint.applyDisableFile'
+ export const openRuleDoc = 'eslint.openRuleDoc'
+}
+
+namespace OpenESLintDocRequest {
+ export const type = new RequestType<OpenESLintDocParams, OpenESLintDocResult, void, void>('eslint/openDoc')
+}
+
+interface OpenESLintDocParams {
+ url: string
+}
+
+interface OpenESLintDocResult {
+}
+
+enum Status {
+ ok = 1,
+ warn = 2,
+ error = 3
+}
+
+interface StatusParams {
+ state: Status
+}
+
+namespace StatusNotification {
+ export const type = new NotificationType<StatusParams, void>('eslint/status')
+}
+
+interface NoConfigParams {
+ message: string
+ document: TextDocumentIdentifier
+}
+
+interface NoConfigResult { }
+
+namespace NoConfigRequest {
+ export const type = new RequestType<
+ NoConfigParams,
+ NoConfigResult,
+ void,
+ void
+ >('eslint/noConfig')
+}
+
+interface NoESLintLibraryParams {
+ source: TextDocumentIdentifier
+}
+
+interface NoESLintLibraryResult { }
+
+namespace NoESLintLibraryRequest {
+ export const type = new RequestType<
+ NoESLintLibraryParams,
+ NoESLintLibraryResult,
+ void,
+ void
+ >('eslint/noLibrary')
+}
+
+interface RuleCodeActions {
+ fixes: CodeAction[]
+ disable?: CodeAction
+ fixAll?: CodeAction
+ disableFile?: CodeAction
+ showDocumentation?: CodeAction
+}
+
+class CodeActionResult {
+ private _actions: Map<string, RuleCodeActions>
+ private _fixAll: CodeAction | undefined
+
+ public constructor() {
+ this._actions = new Map()
+ }
+
+ public get(ruleId: string): RuleCodeActions {
+ let result: RuleCodeActions = this._actions.get(ruleId)
+ if (result === undefined) {
+ result = { fixes: [] }
+ this._actions.set(ruleId, result)
+ }
+ return result
+ }
+
+ public set fixAll(action: CodeAction) {
+ this._fixAll = action
+ }
+
+ public all(): CodeAction[] {
+ let result: CodeAction[] = []
+ for (let actions of this._actions.values()) {
+ result.push(...actions.fixes)
+ if (actions.disable) {
+ result.push(actions.disable)
+ }
+ if (actions.fixAll) {
+ result.push(actions.fixAll)
+ }
+ if (actions.disableFile) {
+ result.push(actions.disableFile)
+ }
+ if (actions.showDocumentation) {
+ result.push(actions.showDocumentation)
+ }
+ }
+ if (this._fixAll !== undefined) {
+ result.push(this._fixAll)
+ }
+ return result
+ }
+
+ public get length(): number {
+ let result = 0
+ for (let actions of this._actions.values()) {
+ result += actions.fixes.length
+ }
+ return result
+ }
+}
+
+function makeDiagnostic(problem: ESLintProblem): Diagnostic {
+ let message =
+ problem.ruleId != null
+ ? `${problem.message} (${problem.ruleId})`
+ : `${problem.message}`
+ let startLine = Math.max(0, problem.line - 1)
+ let startChar = Math.max(0, problem.column - 1)
+ let endLine =
+ problem.endLine != null ? Math.max(0, problem.endLine - 1) : startLine
+ let endChar =
+ problem.endColumn != null ? Math.max(0, problem.endColumn - 1) : startChar
+ return {
+ message,
+ severity: convertSeverity(problem.severity),
+ source: 'eslint',
+ range: {
+ start: { line: startLine, character: startChar },
+ end: { line: endLine, character: endChar }
+ },
+ code: problem.ruleId
+ }
+}
+
+interface FixableProblem {
+ label: string
+ documentVersion: number
+ ruleId: string
+ line: number
+ edit?: ESLintAutoFixEdit
+}
+
+function computeKey(diagnostic: Diagnostic): string {
+ let range = diagnostic.range
+ return `[${range.start.line},${range.start.character},${range.end.line},${
+ range.end.character
+ }]-${diagnostic.code}`
+}
+
+let codeActions: Map<string, Map<string, FixableProblem>> = new Map<
+ string,
+ Map<string, FixableProblem>
+>()
+function recordCodeAction(
+ document: TextDocument,
+ diagnostic: Diagnostic,
+ problem: ESLintProblem
+): void {
+ if (!problem.ruleId) {
+ return
+ }
+ let uri = document.uri
+ let edits: Map<string, FixableProblem> = codeActions.get(uri)
+ if (!edits) {
+ edits = new Map<string, FixableProblem>()
+ codeActions.set(uri, edits)
+ }
+ edits.set(computeKey(diagnostic), { label: `Fix this ${problem.ruleId} problem`, documentVersion: document.version, ruleId: problem.ruleId, edit: problem.fix, line: problem.line })
+}
+
+function convertSeverity(severity: number): DiagnosticSeverity {
+ switch (severity) {
+ // Eslint 1 is warning
+ case 1:
+ return DiagnosticSeverity.Warning
+ case 2:
+ return DiagnosticSeverity.Error
+ default:
+ return DiagnosticSeverity.Error
+ }
+}
+
+const exitCalled = new NotificationType<[number, string], void>(
+ 'eslint/exitCalled'
+)
+
+const nodeExit = process.exit
+process.exit = ((code?: number): void => {
+ let stack = new Error('stack')
+ connection.sendNotification(exitCalled, [code ? code : 0, stack.stack])
+ setTimeout(() => {
+ nodeExit(code)
+ }, 1000)
+}) as any
+process.on('uncaughtException', (error: any) => {
+ let message: string
+ if (error) {
+ if (typeof error.stack === 'string') {
+ message = error.stack
+ } else if (typeof error.message === 'string') {
+ message = error.message
+ } else if (typeof error === 'string') {
+ message = error
+ }
+ if (!message) {
+ try {
+ message = JSON.stringify(error, undefined, 4)
+ } catch (e) {
+ // Should not happen.
+ }
+ }
+ }
+ connection.console.error(`Uncaught exception recevied.
+ ${message || ''}`)
+})
+
+let connection = createConnection()
+connection.console.info(`ESLint server running in node ${process.version}`)
+let documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument)
+
+let _globalNpmPath: string | null | undefined
+function globalNpmPath(): string {
+ if (_globalNpmPath === void 0) {
+ _globalNpmPath = Files.resolveGlobalNodePath(trace)
+ if (_globalNpmPath === void 0) {
+ _globalNpmPath = null
+ }
+ }
+ if (_globalNpmPath === null) {
+ return undefined
+ }
+ return _globalNpmPath
+}
+let _globalYarnPath: string | undefined
+function globalYarnPath(): string {
+ if (_globalYarnPath === void 0) {
+ _globalYarnPath = Files.resolveGlobalYarnPath(trace)
+ if (_globalYarnPath === void 0) {
+ _globalYarnPath = null
+ }
+ }
+ if (_globalYarnPath === null) {
+ return undefined
+ }
+ return _globalYarnPath
+}
+let path2Library: Map<string, ESLintModule> = new Map<string, ESLintModule>()
+let document2Settings: Map<string, Thenable<TextDocumentSettings>> = new Map<
+ string,
+ Thenable<TextDocumentSettings>
+>()
+
+let ruleDocData: {
+ handled: Set<string>
+ urls: Map<string, string>
+} = {
+ handled: new Set<string>(),
+ urls: new Map<string, string>()
+}
+
+function resolveSettings(
+ document: TextDocument
+): Thenable<TextDocumentSettings> {
+ let uri = document.uri
+ let resultPromise = document2Settings.get(uri)
+ if (resultPromise) {
+ return resultPromise
+ }
+ resultPromise = connection.workspace
+ .getConfiguration({ scopeUri: uri, section: '' })
+ .then((settings: TextDocumentSettings) => {
+ let nodePath: string
+ if (settings.nodePath) {
+ nodePath = settings.nodePath
+ if (nodePath.startsWith('~')) {
+ nodePath = nodePath.replace(/^~/, os.homedir())
+ }
+ if (!path.isAbsolute(nodePath)) {
+ nodePath = path.join(URI.parse(settings.workspaceFolder.uri).fsPath, nodePath)
+ }
+ } else if (settings.packageManager === 'npm') {
+ nodePath = globalNpmPath()
+ } else if (settings.packageManager === 'yarn') {
+ nodePath = globalYarnPath()
+ }
+ let uri = URI.parse(document.uri)
+ let promise: Thenable<string>
+ let directory: string
+ if (uri.scheme === 'file') {
+ directory = path.dirname(uri.fsPath)
+ } else {
+ directory = settings.workspaceFolder ? URI.parse(settings.workspaceFolder.uri).fsPath : undefined
+ }
+ promise = resolveModule('./eslint', directory, nodePath).catch(() => {
+ return resolveModule('eslint', directory, nodePath)
+ })
+ return promise.then(path => {
+ let library = path2Library.get(path)
+ if (!library) {
+ library = requireFunc(path)
+ if (!library.CLIEngine) {
+ settings.validate = false
+ connection.console.error(
+ `The eslint library loaded from ${path} doesn\'t export a CLIEngine. You need at least eslint@1.0.0`
+ )
+ } else {
+ connection.console.info(`ESLint library loaded from: ${path}`)
+ settings.library = library
+ }
+ path2Library.set(path, library)
+ } else {
+ settings.library = library
+ }
+ return settings
+ }, () => {
+ settings.validate = false
+ connection.sendRequest(NoESLintLibraryRequest.type, {
+ source: { uri: document.uri }
+ })
+ return settings
+ }
+ )
+ })
+ document2Settings.set(uri, resultPromise)
+ return resultPromise
+}
+
+interface Request<P, R> {
+ method: string
+ params: P
+ documentVersion: number | undefined
+ resolve: (value: R | Thenable<R>) => void | undefined
+ reject: (error: any) => void | undefined
+ token: CancellationToken | undefined
+}
+
+namespace Request {
+ export function is(value: any): value is Request<any, any> {
+ let candidate: Request<any, any> = value
+ return (
+ candidate &&
+ !!candidate.token &&
+ !!candidate.resolve &&
+ !!candidate.reject
+ )
+ }
+}
+
+interface Notifcation<P> {
+ method: string
+ params: P
+ documentVersion: number
+}
+
+type Message<P, R> = Notifcation<P> | Request<P, R>
+
+type VersionProvider<P> = (params: P) => number
+
+namespace Thenable {
+ export function is<T>(value: any): value is Thenable<T> {
+ let candidate: Thenable<T> = value
+ return candidate && typeof candidate.then === 'function'
+ }
+}
+
+class BufferedMessageQueue {
+ private queue: Message<any, any>[]
+ private requestHandlers: Map<
+ string,
+ {
+ handler: RequestHandler<any, any, any>
+ versionProvider?: VersionProvider<any>
+ }
+ >
+ private notificationHandlers: Map<
+ string,
+ { handler: NotificationHandler<any>; versionProvider?: VersionProvider<any> }
+ >
+ private timer: NodeJS.Immediate | undefined
+
+ constructor(private connection: IConnection) {
+ this.queue = []
+ this.requestHandlers = new Map()
+ this.notificationHandlers = new Map()
+ }
+
+ public registerRequest<P, R, E, RO>(
+ type: RequestType<P, R, E, RO>,
+ handler: RequestHandler<P, R, E>,
+ versionProvider?: VersionProvider<P>
+ ): void {
+ this.connection.onRequest(type, (params, token) => {
+ return new Promise<R>((resolve, reject) => {
+ this.queue.push({
+ method: type.method,
+ params,
+ documentVersion: versionProvider
+ ? versionProvider(params)
+ : undefined,
+ resolve,
+ reject,
+ token
+ })
+ this.trigger()
+ })
+ })
+ this.requestHandlers.set(type.method, { handler, versionProvider })
+ }
+
+ public registerNotification<P, RO>(
+ type: NotificationType<P, RO>,
+ handler: NotificationHandler<P>,
+ versionProvider?: (params: P) => number
+ ): void {
+ connection.onNotification(type, params => {
+ this.queue.push({
+ method: type.method,
+ params,
+ documentVersion: versionProvider ? versionProvider(params) : undefined
+ })
+ this.trigger()
+ })
+ this.notificationHandlers.set(type.method, { handler, versionProvider })
+ }
+
+ public addNotificationMessage<P, RO>(
+ type: NotificationType<P, RO>,
+ params: P,
+ version: number
+ ): void {
+ this.queue.push({
+ method: type.method,
+ params,
+ documentVersion: version
+ })
+ this.trigger()
+ }
+
+ public onNotification<P, RO>(
+ type: NotificationType<P, RO>,
+ handler: NotificationHandler<P>,
+ versionProvider?: (params: P) => number
+ ): void {
+ this.notificationHandlers.set(type.method, { handler, versionProvider })
+ }
+
+ private trigger(): void {
+ if (this.timer || this.queue.length === 0) {
+ return
+ }
+ this.timer = setImmediate(() => {
+ this.timer = undefined
+ this.processQueue()
+ })
+ }
+
+ private processQueue(): void {
+ let message = this.queue.shift()
+ if (!message) {
+ return
+ }
+ if (Request.is(message)) {
+ let requestMessage = message
+ if (requestMessage.token.isCancellationRequested) {
+ requestMessage.reject(
+ // tslint:disable-next-line: no-inferred-empty-object-type
+ new ResponseError(
+ ErrorCodes.RequestCancelled,
+ 'Request got cancelled'
+ )
+ )
+ return
+ }
+ let elem = this.requestHandlers.get(requestMessage.method)
+ if (
+ elem.versionProvider &&
+ requestMessage.documentVersion !== void 0 &&
+ requestMessage.documentVersion !==
+ elem.versionProvider(requestMessage.params)
+ ) {
+ requestMessage.reject(
+ // tslint:disable-next-line: no-inferred-empty-object-type
+ new ResponseError(
+ ErrorCodes.RequestCancelled,
+ 'Request got cancelled'
+ )
+ )
+ return
+ }
+ let result = elem.handler(requestMessage.params, requestMessage.token)
+ if (Thenable.is(result)) {
+ result.then(
+ value => {
+ requestMessage.resolve(value)
+ },
+ error => {
+ requestMessage.reject(error)
+ }
+ )
+ } else {
+ requestMessage.resolve(result)
+ }
+ } else {
+ let notificationMessage = message
+ let elem = this.notificationHandlers.get(notificationMessage.method)
+ if (
+ elem.versionProvider &&
+ notificationMessage.documentVersion !== void 0 &&
+ notificationMessage.documentVersion !==
+ elem.versionProvider(notificationMessage.params)
+ ) {
+ return
+ }
+ elem.handler(notificationMessage.params)
+ }
+ this.trigger()
+ }
+}
+
+let messageQueue: BufferedMessageQueue = new BufferedMessageQueue(connection)
+
+namespace ValidateNotification {
+ export const type: NotificationType<
+ TextDocument,
+ void
+ > = new NotificationType<TextDocument, void>('eslint/validate')
+}
+
+messageQueue.onNotification(
+ ValidateNotification.type,
+ document => {
+ validateSingle(document, true)
+ },
+ (document): number => {
+ return document.version
+ }
+)
+
+// The documents manager listen for text document create, change
+// and close on the connection
+documents.listen(connection)
+documents.onDidOpen(event => {
+ resolveSettings(event.document).then(settings => {
+ if (!settings.validate) {
+ return
+ }
+ if (settings.run === 'onSave') {
+ messageQueue.addNotificationMessage(
+ ValidateNotification.type,
+ event.document,
+ event.document.version
+ )
+ }
+ })
+})
+
+// A text document has changed. Validate the document according the run setting.
+documents.onDidChangeContent(event => {
+ resolveSettings(event.document).then(settings => {
+ if (!settings.validate || settings.run !== 'onType') {
+ return
+ }
+ messageQueue.addNotificationMessage(
+ ValidateNotification.type,
+ event.document,
+ event.document.version
+ )
+ })
+})
+
+documents.onWillSaveWaitUntil(event => {
+ if (event.reason === TextDocumentSaveReason.AfterDelay) {
+ return []
+ }
+
+ let document = event.document
+ return resolveSettings(document).then(settings => {
+ if (!settings.autoFixOnSave) {
+ return []
+ }
+ // If we validate on save and want to apply fixes on will save
+ // we need to validate the file.
+ if (settings.run === 'onSave') {
+ // Do not queue this since we want to get the fixes as fast as possible.
+ return validateSingle(document, false).then(() => getAllFixEdits(document, settings))
+ } else {
+ return getAllFixEdits(document, settings)
+ }
+ })
+})
+
+// A text document has been saved. Validate the document according the run setting.
+documents.onDidSave(event => {
+ resolveSettings(event.document).then(settings => {
+ if (!settings.validate || settings.run !== 'onSave') {
+ return
+ }
+ messageQueue.addNotificationMessage(
+ ValidateNotification.type,
+ event.document,
+ event.document.version
+ )
+ })
+})
+
+documents.onDidClose(event => {
+ resolveSettings(event.document).then(settings => {
+ let uri = event.document.uri
+ document2Settings.delete(uri)
+ codeActions.delete(uri)
+ if (settings.validate) {
+ connection.sendDiagnostics({ uri, diagnostics: [] })
+ }
+ })
+})
+
+function environmentChanged(): void {
+ document2Settings.clear()
+ for (let document of documents.all()) {
+ messageQueue.addNotificationMessage(
+ ValidateNotification.type,
+ document,
+ document.version
+ )
+ }
+}
+
+function trace(message: string, verbose?: string): void {
+ connection.tracer.log(message, verbose)
+}
+
+connection.onInitialize(_params => {
+ return {
+ capabilities: {
+ textDocumentSync: {
+ openClose: true,
+ change: TextDocumentSyncKind.Full,
+ willSaveWaitUntil: true,
+ save: {
+ includeText: false
+ }
+ },
+ codeActionProvider: true,
+ executeCommandProvider: {
+ commands: [
+ CommandIds.applySingleFix,
+ CommandIds.applySameFixes,
+ CommandIds.applyAllFixes,
+ CommandIds.applyAutoFix,
+ CommandIds.applyDisableLine,
+ CommandIds.applyDisableFile,
+ CommandIds.openRuleDoc,
+ ]
+ }
+ }
+ }
+})
+
+connection.onInitialized(() => {
+ connection.client.register(DidChangeConfigurationNotification.type, undefined)
+})
+
+messageQueue.registerNotification(
+ DidChangeConfigurationNotification.type,
+ _params => {
+ environmentChanged()
+ }
+)
+
+// messageQueue.registerNotification(
+// DidChangeWorkspaceFoldersNotification.type,
+// _params => {
+// environmentChanged()
+// }
+// )
+
+const singleErrorHandlers: ((
+ error: any,
+ document: TextDocument,
+ library: ESLintModule
+) => Status)[] = [
+ tryHandleNoConfig,
+ tryHandleConfigError,
+ tryHandleMissingModule,
+ showErrorMessage
+ ]
+
+function validateSingle(
+ document: TextDocument,
+ publishDiagnostics = true
+): Thenable<void> {
+ // We validate document in a queue but open / close documents directly. So we need to deal with the
+ // fact that a document might be gone from the server.
+ if (!documents.get(document.uri)) {
+ return Promise.resolve(undefined)
+ }
+ return resolveSettings(document).then(settings => {
+ if (!settings.validate) {
+ return
+ }
+ try {
+ validate(document, settings, publishDiagnostics)
+ connection.sendNotification(StatusNotification.type, { state: Status.ok })
+ } catch (err) {
+ let status
+ for (let handler of singleErrorHandlers) {
+ status = handler(err, document, settings.library)
+ if (status) {
+ break
+ }
+ }
+ status = status || Status.error
+ connection.sendNotification(StatusNotification.type, { state: status })
+ }
+ })
+}
+
+function validateMany(documents: TextDocument[]): void {
+ documents.forEach(document => {
+ messageQueue.addNotificationMessage(
+ ValidateNotification.type,
+ document,
+ document.version
+ )
+ })
+}
+
+function getMessage(err: any, document: TextDocument): string {
+ let result: string = null
+ if (typeof err.message === 'string' || err.message instanceof String) {
+ result = err.message as string
+ result = result.replace(/\r?\n/g, ' ')
+ if (/^CLI: /.test(result)) {
+ result = result.substr(5)
+ }
+ } else {
+ result = `An unknown error occured while validating document: ${
+ document.uri
+ }`
+ }
+ return result
+}
+
+function validate(document: TextDocument, settings: TextDocumentSettings, publishDiagnostics = true): void {
+ const uri = document.uri
+ const content = document.getText()
+ const newOptions: CLIOptions = Object.assign(Object.create(null), settings.options)
+ executeInWorkspaceDirectory(document, settings, newOptions, (file: string, options: CLIOptions) => {
+ const cli = new settings.library.CLIEngine(options)
+ // Clean previously computed code actions.
+ codeActions.delete(uri)
+ const report: ESLintReport = cli.executeOnText(content, file)
+ const diagnostics: Diagnostic[] = []
+ if (report && report.results && Array.isArray(report.results) && report.results.length > 0) {
+ const docReport = report.results[0]
+ if (docReport.messages && Array.isArray(docReport.messages)) {
+ docReport.messages.forEach(problem => {
+ if (problem) {
+ const isWarning = convertSeverity(problem.severity) === DiagnosticSeverity.Warning
+ if (settings.quiet && isWarning) {
+ // Filter out warnings when quiet mode is enabled
+ return
+ }
+ const diagnostic = makeDiagnostic(problem)
+ diagnostics.push(diagnostic)
+ if (settings.autoFix) {
+ if (typeof cli.getRules === 'function' && problem.ruleId !== undefined && problem.fix !== undefined) {
+ const rule = cli.getRules().get(problem.ruleId)
+ if (rule !== undefined && rule.meta && typeof rule.meta.fixable == 'string') {
+ recordCodeAction(document, diagnostic, problem)
+ }
+ } else {
+ recordCodeAction(document, diagnostic, problem)
+ }
+ }
+ }
+ })
+ }
+ }
+ if (publishDiagnostics) {
+ connection.sendDiagnostics({ uri, diagnostics })
+ }
+
+ // cache documentation urls for all rules
+ if (typeof cli.getRules === 'function' && !ruleDocData.handled.has(uri)) {
+ ruleDocData.handled.add(uri)
+ cli.getRules().forEach((rule, key) => {
+ if (rule.meta && rule.meta.docs && Is.string(rule.meta.docs.url)) {
+ ruleDocData.urls.set(key, rule.meta.docs.url)
+ }
+ })
+ }
+ })
+}
+
+let noConfigReported: Map<string, ESLintModule> = new Map<
+ string,
+ ESLintModule
+>()
+
+function isNoConfigFoundError(error: any): boolean {
+ let candidate = error as ESLintError
+ return (
+ candidate.messageTemplate === 'no-config-found' ||
+ candidate.message === 'No ESLint configuration found.'
+ )
+}
+
+function tryHandleNoConfig(
+ error: any,
+ document: TextDocument,
+ library: ESLintModule
+): Status {
+ if (!isNoConfigFoundError(error)) {
+ return undefined
+ }
+ if (!noConfigReported.has(document.uri)) {
+ connection
+ .sendRequest(NoConfigRequest.type, {
+ message: getMessage(error, document),
+ document: {
+ uri: document.uri
+ }
+ })
+ .then(undefined, () => {
+ // noop
+ })
+ noConfigReported.set(document.uri, library)
+ }
+ return Status.warn
+}
+
+let configErrorReported: Map<string, ESLintModule> = new Map<
+ string,
+ ESLintModule
+>()
+
+function tryHandleConfigError(
+ error: any,
+ document: TextDocument,
+ library: ESLintModule
+): Status {
+ if (!error.message) {
+ return undefined
+ }
+
+ function handleFileName(filename: string): Status {
+ if (!configErrorReported.has(filename)) {
+ connection.console.error(getMessage(error, document))
+ if (!documents.get(URI.file(filename).toString())) {
+ connection.window.showInformationMessage(getMessage(error, document))
+ }
+ configErrorReported.set(filename, library)
+ }
+ return Status.warn
+ }
+
+ let matches = /Cannot read config file:\s+(.*)\nError:\s+(.*)/.exec(
+ error.message
+ )
+ if (matches && matches.length === 3) {
+ return handleFileName(matches[1])
+ }
+
+ matches = /(.*):\n\s*Configuration for rule \"(.*)\" is /.exec(error.message)
+ if (matches && matches.length === 3) {
+ return handleFileName(matches[1])
+ }
+
+ matches = /Cannot find module '([^']*)'\nReferenced from:\s+(.*)/.exec(
+ error.message
+ )
+ if (matches && matches.length === 3) {
+ return handleFileName(matches[2])
+ }
+
+ return undefined
+}
+
+let missingModuleReported: Map<string, ESLintModule> = new Map<
+ string,
+ ESLintModule
+>()
+
+function tryHandleMissingModule(
+ error: any,
+ document: TextDocument,
+ library: ESLintModule
+): Status {
+ if (!error.message) {
+ return undefined
+ }
+
+ function handleMissingModule(
+ plugin: string,
+ module: string,
+ error: ESLintError
+ ): Status {
+ if (!missingModuleReported.has(plugin)) {
+ let fsPath = getFilePath(document)
+ missingModuleReported.set(plugin, library)
+ if (error.messageTemplate === 'plugin-missing') {
+ connection.console.error(
+ [
+ '',
+ `${error.message.toString()}`,
+ `Happened while validating ${fsPath ? fsPath : document.uri}`,
+ `This can happen for a couple of reasons:`,
+ `1. The plugin name is spelled incorrectly in an ESLint configuration file (e.g. .eslintrc).`,
+ `2. If ESLint is installed globally, then make sure ${module} is installed globally as well.`,
+ `3. If ESLint is installed locally, then ${module} isn't installed correctly.`,
+ '',
+ `Consider running eslint --debug ${
+ fsPath ? fsPath : document.uri
+ } from a terminal to obtain a trace about the configuration files used.`
+ ].join('\n')
+ )
+ } else {
+ connection.console.error(
+ [
+ `${error.message.toString()}`,
+ `Happend while validating ${fsPath ? fsPath : document.uri}`
+ ].join('\n')
+ )
+ }
+ }
+ return Status.warn
+ }
+
+ let matches = /Failed to load plugin (.*): Cannot find module (.*)/.exec(
+ error.message
+ )
+ if (matches && matches.length === 3) {
+ return handleMissingModule(matches[1], matches[2], error)
+ }
+
+ return undefined
+}
+
+function showErrorMessage(error: any, document: TextDocument): Status {
+ connection.window.showErrorMessage(
+ `ESLint: ${getMessage(
+ error,
+ document
+ )}. Please see the 'ESLint' output channel for details.`
+ )
+ if (Is.string(error.stack)) {
+ connection.console.error('ESLint stack trace:')
+ connection.console.error(error.stack)
+ }
+ return Status.error
+}
+
+messageQueue.registerNotification(
+ DidChangeWatchedFilesNotification.type,
+ params => {
+ // A .eslintrc has change. No smartness here.
+ // Simply revalidate all file.
+ noConfigReported = new Map<string, ESLintModule>()
+ missingModuleReported = new Map<string, ESLintModule>()
+ params.changes.forEach(change => {
+ let fsPath = getFilePath(change.uri)
+ if (!fsPath || isUNC(fsPath)) {
+ return
+ }
+ let dirname = path.dirname(fsPath)
+ if (dirname) {
+ let library = configErrorReported.get(fsPath)
+ if (library) {
+ let cli = new library.CLIEngine({})
+ try {
+ cli.executeOnText('', path.join(dirname, '___test___.js'))
+ configErrorReported.delete(fsPath)
+ } catch (error) {
+ // noop
+ }
+ }
+ }
+ })
+ validateMany(documents.all())
+ }
+)
+
+class Fixes {
+ constructor(private edits: Map<string, FixableProblem>) { }
+
+ public static overlaps(lastEdit: FixableProblem, newEdit: FixableProblem): boolean {
+ return !!lastEdit && lastEdit.edit.range[1] > newEdit.edit.range[0]
+ }
+
+ public isEmpty(): boolean {
+ return this.edits.size === 0
+ }
+
+ public getDocumentVersion(): number {
+ if (this.isEmpty()) {
+ throw new Error('No edits recorded.')
+ }
+ return this.edits.values().next().value.documentVersion
+ }
+
+ public getScoped(diagnostics: Diagnostic[]): FixableProblem[] {
+ let result: FixableProblem[] = []
+ for (let diagnostic of diagnostics) {
+ let key = computeKey(diagnostic)
+ let editInfo = this.edits.get(key)
+ if (editInfo) {
+ result.push(editInfo)
+ }
+ }
+ return result
+ }
+
+ public getAllSorted(): FixableProblem[] {
+ let result: FixableProblem[] = []
+ this.edits.forEach(value => result.push(value))
+ return result.sort((a, b) => {
+ let d = a.edit.range[0] - b.edit.range[0]
+ if (d !== 0) {
+ return d
+ }
+ if (a.edit.range[1] === 0) {
+ return -1
+ }
+ if (b.edit.range[1] === 0) {
+ return 1
+ }
+ return a.edit.range[1] - b.edit.range[1]
+ })
+ }
+
+ public getOverlapFree(): FixableProblem[] {
+ let sorted = this.getAllSorted()
+ if (sorted.length <= 1) {
+ return sorted
+ }
+ let result: FixableProblem[] = []
+ let last: FixableProblem = sorted[0]
+ result.push(last)
+ for (let i = 1; i < sorted.length; i++) {
+ let current = sorted[i]
+ if (!Fixes.overlaps(last, current)) {
+ result.push(current)
+ last = current
+ }
+ }
+ return result
+ }
+}
+
+let commands: Map<string, WorkspaceChange>
+messageQueue.registerRequest(
+ CodeActionRequest.type,
+ params => {
+ commands = new Map<string, WorkspaceChange>()
+ let result: CodeActionResult = new CodeActionResult()
+ let uri = params.textDocument.uri
+ let edits = codeActions.get(uri)
+ if (!edits) return []
+ let fixes = new Fixes(edits)
+ if (fixes.isEmpty()) return []
+
+ let textDocument = documents.get(uri)
+ let documentVersion = -1
+ let allFixableRuleIds: string[] = []
+
+ function createTextEdit(editInfo: FixableProblem): TextEdit {
+ return TextEdit.replace(Range.create(textDocument.positionAt(editInfo.edit.range[0]), textDocument.positionAt(editInfo.edit.range[1])), editInfo.edit.text || '')
+ }
+
+ function createDisableLineTextEdit(editInfo: FixableProblem, indentationText: string): TextEdit {
+ return TextEdit.insert(Position.create(editInfo.line - 1, 0), `${indentationText}// eslint-disable-next-line ${editInfo.ruleId}\n`)
+ }
+
+ function createDisableSameLineTextEdit(editInfo: FixableProblem): TextEdit {
+ return TextEdit.insert(Position.create(editInfo.line - 1, Number.MAX_VALUE), ` // eslint-disable-line ${editInfo.ruleId}`)
+ }
+
+ function createDisableFileTextEdit(editInfo: FixableProblem): TextEdit {
+ return TextEdit.insert(Position.create(0, 0), `/* eslint-disable ${editInfo.ruleId} */\n`)
+ }
+
+ function getLastEdit(array: FixableProblem[]): FixableProblem {
+ let length = array.length
+ if (length === 0) {
+ return undefined
+ }
+ return array[length - 1]
+ }
+
+ return resolveSettings(textDocument).then(settings => {
+ for (let editInfo of fixes.getScoped(params.context.diagnostics)) {
+ documentVersion = editInfo.documentVersion
+ let ruleId = editInfo.ruleId
+ allFixableRuleIds.push(ruleId)
+
+ if (!!editInfo.edit) {
+ let workspaceChange = new WorkspaceChange()
+ workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createTextEdit(editInfo))
+ commands.set(`${CommandIds.applySingleFix}:${ruleId}`, workspaceChange)
+ result.get(ruleId).fixes.push(CodeAction.create(
+ editInfo.label,
+ Command.create(editInfo.label, CommandIds.applySingleFix, ruleId),
+ CodeActionKind.QuickFix
+ ))
+ }
+
+ if (settings.codeAction.disableRuleComment.enable) {
+ let workspaceChange = new WorkspaceChange()
+ if (settings.codeAction.disableRuleComment.location === 'sameLine') {
+ workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableSameLineTextEdit(editInfo))
+ } else {
+ let lineText = textDocument.getText(Range.create(Position.create(editInfo.line - 1, 0), Position.create(editInfo.line - 1, Number.MAX_VALUE)))
+ let indentationText = /^([ \t]*)/.exec(lineText)[1]
+ workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableLineTextEdit(editInfo, indentationText))
+ }
+ commands.set(`${CommandIds.applyDisableLine}:${ruleId}`, workspaceChange)
+ let title = `Disable ${ruleId} for this line`
+ result.get(ruleId).disable = CodeAction.create(
+ title,
+ Command.create(title, CommandIds.applyDisableLine, ruleId),
+ CodeActionKind.QuickFix
+ )
+
+ if (result.get(ruleId).disableFile === undefined) {
+ workspaceChange = new WorkspaceChange()
+ workspaceChange.getTextEditChange({ uri, version: documentVersion }).add(createDisableFileTextEdit(editInfo))
+ commands.set(`${CommandIds.applyDisableFile}:${ruleId}`, workspaceChange)
+ title = `Disable ${ruleId} for the entire file`
+ result.get(ruleId).disableFile = CodeAction.create(
+ title,
+ Command.create(title, CommandIds.applyDisableFile, ruleId),
+ CodeActionKind.QuickFix
+ )
+ }
+ }
+
+ if (settings.codeAction.showDocumentation.enable && result.get(ruleId).showDocumentation === undefined) {
+ if (ruleDocData.urls.has(ruleId)) {
+ let title = `Show documentation for ${ruleId}`
+ result.get(ruleId).showDocumentation = CodeAction.create(
+ title,
+ Command.create(title, CommandIds.openRuleDoc, ruleId),
+ CodeActionKind.QuickFix
+ )
+ }
+ }
+ }
+
+ if (result.length > 0) {
+ let sameProblems: Map<string, FixableProblem[]> = new Map<string, FixableProblem[]>(allFixableRuleIds.map<[string, FixableProblem[]]>(s => [s, []]))
+ let all: FixableProblem[] = []
+
+ for (let editInfo of fixes.getAllSorted()) {
+ if (documentVersion === -1) {
+ documentVersion = editInfo.documentVersion
+ }
+ if (sameProblems.has(editInfo.ruleId)) {
+ let same = sameProblems.get(editInfo.ruleId)
+ if (!Fixes.overlaps(getLastEdit(same), editInfo)) {
+ same.push(editInfo)
+ }
+ }
+ if (!Fixes.overlaps(getLastEdit(all), editInfo)) {
+ all.push(editInfo)
+ }
+ }
+ sameProblems.forEach((same, ruleId) => {
+ if (same.length > 1) {
+ let sameFixes: WorkspaceChange = new WorkspaceChange()
+ let sameTextChange = sameFixes.getTextEditChange({ uri, version: documentVersion })
+ same.map(createTextEdit).forEach(edit => sameTextChange.add(edit))
+ commands.set(CommandIds.applySameFixes, sameFixes)
+ let title = `Fix all ${ruleId} problems`
+ let command = Command.create(title, CommandIds.applySameFixes)
+ result.get(ruleId).fixAll = CodeAction.create(
+ title,
+ command,
+ CodeActionKind.QuickFix
+ )
+ }
+ })
+ if (all.length > 1) {
+ let allFixes: WorkspaceChange = new WorkspaceChange()
+ let allTextChange = allFixes.getTextEditChange({ uri, version: documentVersion })
+ all.map(createTextEdit).forEach(edit => allTextChange.add(edit))
+ commands.set(CommandIds.applyAllFixes, allFixes)
+ let title = `Fix all auto-fixable problems`
+ let command = Command.create(title, CommandIds.applyAllFixes)
+ result.fixAll = CodeAction.create(
+ title,
+ command,
+ CodeActionKind.SourceFixAll
+ )
+ }
+ }
+ return result.all()
+ })
+ },
+ (params): number => {
+ let document = documents.get(params.textDocument.uri)
+ return document ? document.version : undefined
+ }
+)
+
+messageQueue.registerRequest(
+ ExecuteCommandRequest.type,
+ async params => {
+ let workspaceChange: WorkspaceChange
+ if (params.command === CommandIds.applyAutoFix) {
+ let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
+ if (!identifier.uri.startsWith('file:')) {
+ return {}
+ }
+ let textDocument = documents.get(identifier.uri)
+ let settings = await Promise.resolve(resolveSettings(textDocument))
+ let edits = getAllFixEdits(textDocument, settings)
+ if (edits && edits.length) {
+ workspaceChange = new WorkspaceChange()
+ let textChange = workspaceChange.getTextEditChange(identifier)
+ edits.forEach(edit => textChange.add(edit))
+ }
+ } else {
+ if ([CommandIds.applySingleFix, CommandIds.applyDisableLine, CommandIds.applyDisableFile].indexOf(params.command) !== -1) {
+ let ruleId = params.arguments[0]
+ workspaceChange = commands.get(`${params.command}:${ruleId}`)
+ } else if (params.command === CommandIds.openRuleDoc) {
+ let ruleId = params.arguments[0]
+ let url = ruleDocData.urls.get(ruleId)
+ if (url) {
+ await connection.sendRequest(OpenESLintDocRequest.type, { url })
+ }
+ } else {
+ workspaceChange = commands.get(params.command)
+ }
+ }
+
+ if (!workspaceChange) {
+ return {}
+ }
+ try {
+ let response = await Promise.resolve(connection.workspace.applyEdit(workspaceChange.edit))
+ if (!response.applied) {
+ connection.console.error(`Failed to apply command: ${params.command}`)
+ }
+ } catch (e) {
+ connection.console.error(`Failed to apply command: ${params.command}`)
+ }
+ return {}
+ },
+ (params): number => {
+ if (params.command === CommandIds.applyAutoFix) {
+ let identifier: VersionedTextDocumentIdentifier = params.arguments[0]
+ return identifier.version
+ } else {
+ return undefined
+ }
+ }
+)
+
+connection.tracer.connection.listen()