311 lines
7.0 KiB
Go
311 lines
7.0 KiB
Go
|
|
package linter
|
||
|
|
|
||
|
|
import (
|
||
|
|
"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
|
||
|
|
var installOptions []string
|
||
|
|
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
|
||
|
|
args := append(linter.Args, filePath)
|
||
|
|
|
||
|
|
// Execute linter
|
||
|
|
cmd := exec.Command(linter.Command, args...)
|
||
|
|
output, err := cmd.CombinedOutput()
|
||
|
|
|
||
|
|
result := &LintResult{
|
||
|
|
LinterUsed: linter.Name,
|
||
|
|
Output: string(output),
|
||
|
|
LinterExists: true,
|
||
|
|
}
|
||
|
|
|
||
|
|
if err != nil {
|
||
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||
|
|
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 {
|
||
|
|
var langs []string
|
||
|
|
for _, lang := range languages {
|
||
|
|
langs = append(langs, fmt.Sprintf("%s (%s)", lang.Name, strings.Join(lang.Extensions, ", ")))
|
||
|
|
}
|
||
|
|
return langs
|
||
|
|
}
|