Introduce new `grokkit lint` command for automatic language detection, linting, and AI-suggested fixes. Supports 9 languages including Go, Python, JavaScript, TypeScript, Rust, Ruby, Java, C/C++, and Shell. - Add cmd/lint.go for command implementation - Create internal/linter package with detection and execution logic - Update README.md with usage examples and workflows - Enhance docs/ARCHITECTURE.md and docs/TROUBLESHOOTING.md - Add comprehensive tests for linter functionality
335 lines
8.6 KiB
Go
335 lines
8.6 KiB
Go
package linter
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestDetectLanguage(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
filePath string
|
|
wantLang string
|
|
wantErr bool
|
|
}{
|
|
{"Go file", "main.go", "Go", false},
|
|
{"Python file", "script.py", "Python", false},
|
|
{"JavaScript file", "app.js", "JavaScript", false},
|
|
{"JSX file", "component.jsx", "JavaScript", false},
|
|
{"TypeScript file", "app.ts", "TypeScript", false},
|
|
{"TSX file", "component.tsx", "TypeScript", false},
|
|
{"Rust file", "main.rs", "Rust", false},
|
|
{"Ruby file", "script.rb", "Ruby", false},
|
|
{"Java file", "Main.java", "Java", false},
|
|
{"C file", "program.c", "C/C++", false},
|
|
{"C++ file", "program.cpp", "C/C++", false},
|
|
{"Header file", "header.h", "C/C++", false},
|
|
{"Shell script", "script.sh", "Shell", false},
|
|
{"Bash script", "script.bash", "Shell", false},
|
|
{"Unsupported file", "file.txt", "", true},
|
|
{"No extension", "Makefile", "", true},
|
|
{"Case insensitive", "MAIN.GO", "Go", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
lang, err := DetectLanguage(tt.filePath)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Errorf("DetectLanguage(%q) expected error, got nil", tt.filePath)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Errorf("DetectLanguage(%q) unexpected error: %v", tt.filePath, err)
|
|
return
|
|
}
|
|
if lang.Name != tt.wantLang {
|
|
t.Errorf("DetectLanguage(%q) = %q, want %q", tt.filePath, lang.Name, tt.wantLang)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckLinterAvailable(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
linter Linter
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "go command should be available",
|
|
linter: Linter{
|
|
Name: "go",
|
|
Command: "go",
|
|
},
|
|
wantMsg: "go should be available on system with Go installed",
|
|
},
|
|
{
|
|
name: "nonexistent command",
|
|
linter: Linter{
|
|
Name: "nonexistent",
|
|
Command: "this-command-does-not-exist-12345",
|
|
},
|
|
wantMsg: "nonexistent command should not be available",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
available := CheckLinterAvailable(tt.linter)
|
|
// We can't assert exact true/false since it depends on the system
|
|
// Just verify the function runs without panic
|
|
t.Logf("%s: available=%v", tt.wantMsg, available)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFindAvailableLinter(t *testing.T) {
|
|
// Test with Go language since 'go' command should be available
|
|
// (we're running tests with Go installed)
|
|
t.Run("Go language should find a linter", func(t *testing.T) {
|
|
goLang := &Language{
|
|
Name: "Go",
|
|
Extensions: []string{".go"},
|
|
Linters: []Linter{
|
|
{
|
|
Name: "go vet",
|
|
Command: "go",
|
|
Args: []string{"vet"},
|
|
InstallInfo: "Built-in with Go",
|
|
},
|
|
},
|
|
}
|
|
|
|
linter, err := FindAvailableLinter(goLang)
|
|
if err != nil {
|
|
t.Errorf("FindAvailableLinter(Go) expected to find 'go', got error: %v", err)
|
|
}
|
|
if linter == nil {
|
|
t.Errorf("FindAvailableLinter(Go) returned nil linter")
|
|
} else if linter.Command != "go" {
|
|
t.Errorf("FindAvailableLinter(Go) = %q, want 'go'", linter.Command)
|
|
}
|
|
})
|
|
|
|
t.Run("Language with no available linters", func(t *testing.T) {
|
|
fakeLang := &Language{
|
|
Name: "FakeLang",
|
|
Extensions: []string{".fake"},
|
|
Linters: []Linter{
|
|
{
|
|
Name: "fakelint",
|
|
Command: "this-does-not-exist-12345",
|
|
InstallInfo: "not installable",
|
|
},
|
|
},
|
|
}
|
|
|
|
linter, err := FindAvailableLinter(fakeLang)
|
|
if err == nil {
|
|
t.Errorf("FindAvailableLinter(FakeLang) expected error, got linter: %v", linter)
|
|
}
|
|
if linter != nil {
|
|
t.Errorf("FindAvailableLinter(FakeLang) expected nil linter, got: %v", linter)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRunLinter(t *testing.T) {
|
|
// Create a temporary Go file with a simple error
|
|
tmpDir := t.TempDir()
|
|
testFile := filepath.Join(tmpDir, "test.go")
|
|
|
|
// Valid Go code
|
|
validCode := `package main
|
|
|
|
import "fmt"
|
|
|
|
func main() {
|
|
fmt.Println("Hello, World!")
|
|
}
|
|
`
|
|
if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
t.Run("Run go vet on valid file", func(t *testing.T) {
|
|
linter := Linter{
|
|
Name: "go vet",
|
|
Command: "go",
|
|
Args: []string{"vet"},
|
|
}
|
|
|
|
result, err := RunLinter(testFile, linter)
|
|
if err != nil {
|
|
t.Errorf("RunLinter() unexpected error: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("RunLinter() returned nil result")
|
|
}
|
|
if !result.LinterExists {
|
|
t.Errorf("RunLinter() LinterExists = false, want true")
|
|
}
|
|
if result.LinterUsed != "go vet" {
|
|
t.Errorf("RunLinter() LinterUsed = %q, want 'go vet'", result.LinterUsed)
|
|
}
|
|
// Note: go vet might find issues or not, so we don't assert HasIssues
|
|
t.Logf("go vet result: ExitCode=%d, HasIssues=%v, Output=%q",
|
|
result.ExitCode, result.HasIssues, result.Output)
|
|
})
|
|
|
|
t.Run("Run nonexistent linter", func(t *testing.T) {
|
|
linter := Linter{
|
|
Name: "fake",
|
|
Command: "this-does-not-exist-12345",
|
|
Args: []string{},
|
|
}
|
|
|
|
result, err := RunLinter(testFile, linter)
|
|
if err == nil {
|
|
t.Errorf("RunLinter() expected error for nonexistent command, got result: %v", result)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLintFile(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
t.Run("Lint valid Go file", func(t *testing.T) {
|
|
testFile := filepath.Join(tmpDir, "valid.go")
|
|
validCode := `package main
|
|
|
|
import "fmt"
|
|
|
|
func main() {
|
|
fmt.Println("Hello, World!")
|
|
}
|
|
`
|
|
if err := os.WriteFile(testFile, []byte(validCode), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
result, err := LintFile(testFile)
|
|
if err != nil {
|
|
t.Errorf("LintFile() unexpected error: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("LintFile() returned nil result")
|
|
}
|
|
if result.Language != "Go" {
|
|
t.Errorf("LintFile() Language = %q, want 'Go'", result.Language)
|
|
}
|
|
if !result.LinterExists {
|
|
t.Errorf("LintFile() LinterExists = false, want true")
|
|
}
|
|
})
|
|
|
|
t.Run("Lint nonexistent file", func(t *testing.T) {
|
|
testFile := filepath.Join(tmpDir, "nonexistent.go")
|
|
|
|
result, err := LintFile(testFile)
|
|
if err == nil {
|
|
t.Errorf("LintFile() expected error for nonexistent file, got result: %v", result)
|
|
}
|
|
})
|
|
|
|
t.Run("Lint unsupported file type", func(t *testing.T) {
|
|
testFile := filepath.Join(tmpDir, "test.txt")
|
|
if err := os.WriteFile(testFile, []byte("hello"), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
|
|
result, err := LintFile(testFile)
|
|
if err == nil {
|
|
t.Errorf("LintFile() expected error for unsupported file type, got result: %v", result)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetSupportedLanguages(t *testing.T) {
|
|
langs := GetSupportedLanguages()
|
|
|
|
if len(langs) == 0 {
|
|
t.Errorf("GetSupportedLanguages() returned empty list")
|
|
}
|
|
|
|
// Check that we have some expected languages
|
|
expectedLangs := []string{"Go", "Python", "JavaScript", "TypeScript"}
|
|
for _, expected := range expectedLangs {
|
|
found := false
|
|
for _, lang := range langs {
|
|
if contains(lang, expected) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("GetSupportedLanguages() missing expected language: %s", expected)
|
|
}
|
|
}
|
|
|
|
// Verify format includes extensions
|
|
for _, lang := range langs {
|
|
if !contains(lang, "(") || !contains(lang, ")") {
|
|
t.Errorf("GetSupportedLanguages() entry missing format 'Name (extensions)': %s", lang)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLanguageStructure(t *testing.T) {
|
|
// Verify the languages slice is properly structured
|
|
if len(languages) == 0 {
|
|
t.Fatal("languages slice is empty")
|
|
}
|
|
|
|
for _, lang := range languages {
|
|
if lang.Name == "" {
|
|
t.Errorf("Language with empty Name found")
|
|
}
|
|
if len(lang.Extensions) == 0 {
|
|
t.Errorf("Language %q has no extensions", lang.Name)
|
|
}
|
|
if len(lang.Linters) == 0 {
|
|
t.Errorf("Language %q has no linters", lang.Name)
|
|
}
|
|
|
|
// Check extensions start with dot
|
|
for _, ext := range lang.Extensions {
|
|
if ext == "" || ext[0] != '.' {
|
|
t.Errorf("Language %q has invalid extension: %q", lang.Name, ext)
|
|
}
|
|
}
|
|
|
|
// Check linters have required fields
|
|
for _, linter := range lang.Linters {
|
|
if linter.Name == "" {
|
|
t.Errorf("Language %q has linter with empty Name", lang.Name)
|
|
}
|
|
if linter.Command == "" {
|
|
t.Errorf("Language %q has linter %q with empty Command", lang.Name, linter.Name)
|
|
}
|
|
if linter.InstallInfo == "" {
|
|
t.Errorf("Language %q has linter %q with empty InstallInfo", lang.Name, linter.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function
|
|
func contains(s, substr string) bool {
|
|
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
|
|
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
|
|
containsMiddle(s, substr)))
|
|
}
|
|
|
|
func containsMiddle(s, substr string) bool {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|