import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import { ESLint } from 'eslint'; import js from '@eslint/js'; import ts from 'typescript-eslint'; import type { LinterAdapter, AdapterResult, LinterDiagnostic } from './adapter'; import { getEslintConfigPath } from '../config'; import { t } from '../i18n/messages'; import { eslintExtraRules, eslintExtraTsRules } from '../rules/builtin-rules'; const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts']; const PROJECT_CONFIG_FILES = [ 'eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', 'eslint.config.ts', 'eslint.config.mts', 'eslint.config.cts', ]; const LEGACY_CONFIG_FILES = [ '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json', '.eslintrc.yaml', '.eslintrc.yml', '.eslintrc', ]; function findConfigFile(dir: string, names: string[]): string | null { for (const name of names) { const p = path.join(dir, name); if (fs.existsSync(p)) { return p; } } return null; } type EslintConfigResult = | { kind: 'use'; config: { overrideConfigFile?: string | true; overrideConfig?: any[] } } | { kind: 'legacy'; path: string }; function resolveEslintConfig(workingDir: string): EslintConfigResult { const globalPath = getEslintConfigPath(); if (globalPath && globalPath.trim() !== '') { const abs = path.isAbsolute(globalPath) ? globalPath : path.resolve(workingDir, globalPath); if (fs.existsSync(abs)) { return { kind: 'use', config: { overrideConfigFile: abs } }; } } const projectConfig = findConfigFile(workingDir, PROJECT_CONFIG_FILES); if (projectConfig) { return { kind: 'use', config: { overrideConfigFile: projectConfig } }; } const legacyConfig = findConfigFile(workingDir, LEGACY_CONFIG_FILES); if (legacyConfig) { return { kind: 'legacy', path: legacyConfig }; } return { kind: 'use', config: { overrideConfigFile: true, overrideConfig: ESLintAdapter.getDefaultConfig() } }; } export class ESLintAdapter implements LinterAdapter { id = 'eslint'; supportedLanguages = ['javascript', 'typescript']; private static defaultConfig: any[] | null = null; public static getDefaultConfig(): any[] { if (!ESLintAdapter.defaultConfig) { ESLintAdapter.defaultConfig = [ js.configs.recommended, ...ts.configs.recommended, { rules: eslintExtraRules }, { files: TS_FILES, rules: eslintExtraTsRules }, ]; } return ESLintAdapter.defaultConfig; } isAvailable(): boolean { return true; } async check(document: vscode.TextDocument, workingDir: string): Promise { try { const resolved = resolveEslintConfig(workingDir); if (resolved.kind === 'legacy') { return { diagnostics: [], status: 'execution-failed', errorMessage: t('adapter.eslintLegacyConfig', { 0: resolved.path }), }; } const engine = new ESLint({ cwd: workingDir, ...resolved.config, }); const ext = document.languageId === 'typescript' ? 'ts' : 'js'; const isVirtual = document.uri.scheme === 'untitled'; const results = await engine.lintText(document.getText(), { filePath: isVirtual ? `untitled.${ext}` : document.fileName, }); const diagnostics: LinterDiagnostic[] = []; for (const result of results) { for (const msg of result.messages) { if (msg.ruleId === null) { continue; } diagnostics.push({ severity: msg.severity === 2 ? 'error' : 'warning', ruleId: `eslint:${msg.ruleId}`, message: msg.message, range: new vscode.Range( msg.line - 1, msg.column - 1, (msg.endLine ?? msg.line) - 1, (msg.endColumn ?? msg.column) - 1 ), suggestion: msg.fix?.text, }); } } return { diagnostics, status: 'ok' }; } catch (error) { return { diagnostics: [], status: 'execution-failed', errorMessage: error instanceof Error ? error.message : String(error), }; } } }