Add a "none" linter configuration for the Rexx language to provide structure, while noting that no traditional linter is used and it's primarily for grokkit analyze.
327 lines
7.5 KiB
Go
327 lines
7.5 KiB
Go
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",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "Rexx",
|
|
Extensions: []string{".rx", ".rex", ".rexlib", ".rexx", ".cls"},
|
|
Linters: []Linter{
|
|
{
|
|
Name: "none",
|
|
Command: "none",
|
|
Args: []string{},
|
|
InstallInfo: "No traditional linter; used primarily by `grokkit analyze`",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// 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
|
|
}
|