package cc.unitmesh.agent.linter import kotlinx.serialization.Serializable @Serializable data class LintFileResult( val filePath: String, val linterName: String, val errorCount: Int, val warningCount: Int, val infoCount: Int, val issues: List ) /** * UI-friendly lint severity */ @Serializable enum class LintSeverity { ERROR, WARNING, INFO } @Serializable data class LintIssue( val line: Int, val column: Int = 0, val severity: LintSeverity, val message: String, val rule: String? = null, val suggestion: String? = null, val filePath: String? = null ) @Serializable data class LintResult( val filePath: String, val issues: List, val success: Boolean, val errorMessage: String? = null, val linterName: String ) { val hasIssues: Boolean get() = issues.isNotEmpty() val errorCount: Int get() = issues.count { it.severity == LintSeverity.ERROR } val warningCount: Int get() = issues.count { it.severity == LintSeverity.WARNING } } /** * Base interface for all linters */ interface Linter { /** * Name of the linter (e.g., "eslint", "detekt", "ruff") */ val name: String /** * Description of what this linter checks */ val description: String /** * Supported file extensions (e.g., ["kt", "kts"] for Kotlin) */ val supportedExtensions: List /** * Check if this linter is available in the system */ suspend fun isAvailable(): Boolean /** * Lint a single file */ suspend fun lintFile(filePath: String, projectPath: String): LintResult /** * Lint multiple files */ suspend fun lintFiles(filePaths: List, projectPath: String): List { return filePaths.map { lintFile(it, projectPath) } } /** * Get installation instructions if linter is not available */ fun getInstallationInstructions(): String } /** * Per-file lint issue summary */ @Serializable data class FileLintSummary( val filePath: String, val linterName: String, val totalIssues: Int, val errorCount: Int, val warningCount: Int, val infoCount: Int, val topIssues: List, // Top 5 most important issues val hasMoreIssues: Boolean ) /** * Registry for managing available linters */ class LinterRegistry { private val linters = mutableMapOf() /** * Register a linter */ fun register(linter: Linter) { linters[linter.name] = linter } /** * Get linter by name */ fun getLinter(name: String): Linter? { return linters[name] } /** * Get all registered linters */ fun getAllLinters(): List { return linters.values.toList() } fun findLintersForFiles(filePaths: List): List { val extensions = filePaths.map { it.substringAfterLast('.', "") }.toSet() return linters.values.filter { linter -> linter.supportedExtensions.any { ext -> extensions.any { it.equals(ext, ignoreCase = true) } } }.distinctBy { it.name } } suspend fun getLinterSummaryForFiles(filePaths: List, projectPath: String = "."): LinterSummary { if (filePaths.isEmpty()) { return LinterSummary( totalFiles = 0, filesWithIssues = 0, totalIssues = 0, errorCount = 0, warningCount = 0, infoCount = 0, fileIssues = emptyList(), executedLinters = emptyList() ) } val suitableLinters = findLintersForFiles(filePaths) val availableLinters = suitableLinters.filter { it.isAvailable() } if (availableLinters.isEmpty()) { // No linters available, return empty summary return LinterSummary( totalFiles = filePaths.size, filesWithIssues = 0, totalIssues = 0, errorCount = 0, warningCount = 0, infoCount = 0, fileIssues = emptyList(), executedLinters = emptyList() ) } val fileIssues = mutableListOf() val executedLinters = mutableSetOf() var totalIssues = 0 var totalErrors = 0 var totalWarnings = 0 var totalInfo = 0 // Run each available linter on its supported files for (linter in availableLinters) { try { val supportedFiles = filePaths.filter { path -> val ext = path.substringAfterLast('.', "").lowercase() linter.supportedExtensions.any { it.equals(ext, ignoreCase = true) } } if (supportedFiles.isEmpty()) continue // Lint the supported files val results = linter.lintFiles(supportedFiles, projectPath) executedLinters.add(linter.name) // Process results for (result in results) { if (result.issues.isEmpty()) continue val issues = result.issues val errorCount = issues.count { it.severity == LintSeverity.ERROR } val warningCount = issues.count { it.severity == LintSeverity.WARNING } val infoCount = issues.count { it.severity == LintSeverity.INFO } // Take top 10 issues, prioritizing errors > warnings > info val topIssues = issues .sortedWith(compareBy { when (it.severity) { LintSeverity.ERROR -> 0 LintSeverity.WARNING -> 1 LintSeverity.INFO -> 2 } }.thenBy { it.line }) .take(10) fileIssues.add( FileLintSummary( filePath = result.filePath, linterName = linter.name, totalIssues = issues.size, errorCount = errorCount, warningCount = warningCount, infoCount = infoCount, topIssues = topIssues, hasMoreIssues = issues.size > 10 ) ) totalIssues += issues.size totalErrors += errorCount totalWarnings += warningCount totalInfo += infoCount } } catch (e: Exception) { // Log error but continue with other linters println("Warning: Linter ${linter.name} failed: ${e.message}") } } return LinterSummary( totalFiles = filePaths.size, filesWithIssues = fileIssues.size, totalIssues = totalIssues, errorCount = totalErrors, warningCount = totalWarnings, infoCount = totalInfo, fileIssues = fileIssues.sortedByDescending { it.errorCount }, executedLinters = executedLinters.toList() ) } companion object { private var instance: LinterRegistry? = null fun getInstance(): LinterRegistry { if (instance == null) { instance = LinterRegistry() registerPlatformLinters(instance!!) } return instance!! } } } /** * Platform-specific linter registration * This function is implemented in each platform's source set */ expect fun registerPlatformLinters(registry: LinterRegistry)