package linter import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "gmgauthier.com/grokkit/internal/logger" ) // Language represents a programming language type Language struct { Name string Extensions []string Linters []Linter } // Linter represents a linting tool type Linter struct { Name string Command string Args []string InstallInfo string } // LintResult contains linting results type LintResult struct { Language string LinterUsed string Output string ExitCode int HasIssues bool LinterExists bool } // Supported languages and their linters var languages = []Language{ { Name: "Go", Extensions: []string{".go"}, Linters: []Linter{ { Name: "golangci-lint", Command: "golangci-lint", Args: []string{"run"}, InstallInfo: "https://golangci-lint.run/usage/install/", }, { Name: "go vet", Command: "go", Args: []string{"vet"}, InstallInfo: "Built-in with Go", }, }, }, { Name: "Python", Extensions: []string{".py"}, Linters: []Linter{ { Name: "pylint", Command: "pylint", Args: []string{}, InstallInfo: "pip install pylint", }, { Name: "flake8", Command: "flake8", Args: []string{}, InstallInfo: "pip install flake8", }, { Name: "ruff", Command: "ruff", Args: []string{"check"}, InstallInfo: "pip install ruff", }, }, }, { Name: "JavaScript", Extensions: []string{".js", ".jsx"}, Linters: []Linter{ { Name: "eslint", Command: "eslint", Args: []string{}, InstallInfo: "npm install -g eslint", }, }, }, { Name: "TypeScript", Extensions: []string{".ts", ".tsx"}, Linters: []Linter{ { Name: "eslint", Command: "eslint", Args: []string{}, InstallInfo: "npm install -g eslint @typescript-eslint/parser", }, { Name: "tsc", Command: "tsc", Args: []string{"--noEmit"}, InstallInfo: "npm install -g typescript", }, }, }, { Name: "Rust", Extensions: []string{".rs"}, Linters: []Linter{ { Name: "clippy", Command: "cargo", Args: []string{"clippy", "--"}, InstallInfo: "rustup component add clippy", }, }, }, { Name: "Ruby", Extensions: []string{".rb"}, Linters: []Linter{ { Name: "rubocop", Command: "rubocop", Args: []string{}, InstallInfo: "gem install rubocop", }, }, }, { Name: "Java", Extensions: []string{".java"}, Linters: []Linter{ { Name: "checkstyle", Command: "checkstyle", Args: []string{"-c", "/google_checks.xml"}, InstallInfo: "https://checkstyle.org/", }, }, }, { Name: "C/C++", Extensions: []string{".c", ".cpp", ".cc", ".cxx", ".h", ".hpp"}, Linters: []Linter{ { Name: "clang-tidy", Command: "clang-tidy", Args: []string{}, InstallInfo: "apt install clang-tidy or brew install llvm", }, }, }, { Name: "Shell", Extensions: []string{".sh", ".bash"}, Linters: []Linter{ { Name: "shellcheck", Command: "shellcheck", Args: []string{}, InstallInfo: "apt install shellcheck or brew install shellcheck", }, }, }, } // DetectLanguage detects the programming language based on file extension func DetectLanguage(filePath string) (*Language, error) { ext := strings.ToLower(filepath.Ext(filePath)) logger.Debug("detecting language", "file", filePath, "extension", ext) for i := range languages { for _, langExt := range languages[i].Extensions { if ext == langExt { logger.Info("language detected", "file", filePath, "language", languages[i].Name) return &languages[i], nil } } } return nil, fmt.Errorf("unsupported file type: %s", ext) } // CheckLinterAvailable checks if a linter command is available func CheckLinterAvailable(linter Linter) bool { _, err := exec.LookPath(linter.Command) available := err == nil logger.Debug("checking linter availability", "linter", linter.Name, "command", linter.Command, "available", available) return available } // FindAvailableLinter finds the first available linter for a language func FindAvailableLinter(lang *Language) (*Linter, error) { logger.Info("searching for available linter", "language", lang.Name, "options", len(lang.Linters)) for i := range lang.Linters { if CheckLinterAvailable(lang.Linters[i]) { logger.Info("found available linter", "language", lang.Name, "linter", lang.Linters[i].Name) return &lang.Linters[i], nil } } // Build install instructions installOptions := make([]string, 0, len(lang.Linters)) for _, linter := range lang.Linters { installOptions = append(installOptions, fmt.Sprintf(" - %s: %s", linter.Name, linter.InstallInfo)) } return nil, fmt.Errorf("no linter found for %s\n\nInstall one of:\n%s", lang.Name, strings.Join(installOptions, "\n")) } // RunLinter executes the linter on the specified file func RunLinter(filePath string, linter Linter) (*LintResult, error) { logger.Info("running linter", "file", filePath, "linter", linter.Name, "command", linter.Command) // Build command arguments linterArgs := append([]string{}, linter.Args...) linterArgs = append(linterArgs, filePath) // Execute linter // nolint:gosec // intentional subprocess for linter cmd := exec.Command(linter.Command, linterArgs...) output, err := cmd.CombinedOutput() result := &LintResult{ LinterUsed: linter.Name, Output: string(output), LinterExists: true, } if err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { result.ExitCode = exitErr.ExitCode() result.HasIssues = true logger.Info("linter found issues", "linter", linter.Name, "exit_code", result.ExitCode, "output_length", len(output)) } else { logger.Error("linter execution failed", "linter", linter.Name, "error", err) return nil, fmt.Errorf("failed to execute %s: %w", linter.Name, err) } } else { result.ExitCode = 0 result.HasIssues = false logger.Info("linter passed with no issues", "linter", linter.Name) } return result, nil } // LintFile detects language and runs appropriate linter func LintFile(filePath string) (*LintResult, error) { // Check file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { return nil, fmt.Errorf("file not found: %s", filePath) } // Detect language lang, err := DetectLanguage(filePath) if err != nil { return nil, err } // Find available linter linter, err := FindAvailableLinter(lang) if err != nil { return &LintResult{ Language: lang.Name, LinterExists: false, Output: err.Error(), }, err } // Run linter result, err := RunLinter(filePath, *linter) if err != nil { return nil, err } result.Language = lang.Name return result, nil } // GetSupportedLanguages returns a list of all supported languages func GetSupportedLanguages() []string { langs := make([]string, 0, len(languages)) for _, lang := range languages { langs = append(langs, fmt.Sprintf("%s (%s)", lang.Name, strings.Join(lang.Extensions, ", "))) } return langs }