- Expand .golangci.yml with more linters (bodyclose, errcheck, etc.), settings for govet, revive, gocritic, gosec - Add // nolint:gosec comments for intentional file operations and subprocesses - Change file write permissions from 0644 to 0600 for better security - Refactor loops, error handling, and test parallelism with t.Parallel() - Minor fixes: ignore unused args, use errors.Is, adjust mkdir permissions to 0750
315 lines
7.2 KiB
Go
315 lines
7.2 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",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// 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
|
|
}
|