From 82f42bea84f8c2b2f2e94197060f156c71910cd4 Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 10:33:24 +0900 Subject: [PATCH 01/12] chore: remove deprecated engine files and tests --- internal/engine/README.md | 19 - internal/engine/ast/engine.go | 273 ---------- internal/engine/ast/engine_test.go | 293 ----------- internal/engine/core/engine.go | 118 ----- internal/engine/core/selector.go | 127 ----- internal/engine/core/selector_test.go | 581 --------------------- internal/engine/core/types.go | 162 ------ internal/engine/core/types_test.go | 131 ----- internal/engine/length/engine.go | 200 ------- internal/engine/length/engine_test.go | 338 ------------ internal/engine/llm/engine.go | 286 ---------- internal/engine/pattern/engine.go | 201 ------- internal/engine/pattern/engine_test.go | 322 ------------ internal/engine/registry/builtin.go | 44 -- internal/engine/registry/registry.go | 99 ---- internal/engine/registry/registry_test.go | 126 ----- internal/engine/style/engine.go | 200 ------- internal/engine/style/engine_test.go | 430 --------------- internal/engine/typechecker/engine.go | 162 ------ internal/engine/typechecker/engine_test.go | 374 ------------- 20 files changed, 4486 deletions(-) delete mode 100644 internal/engine/README.md delete mode 100644 internal/engine/ast/engine.go delete mode 100644 internal/engine/ast/engine_test.go delete mode 100644 internal/engine/core/engine.go delete mode 100644 internal/engine/core/selector.go delete mode 100644 internal/engine/core/selector_test.go delete mode 100644 internal/engine/core/types.go delete mode 100644 internal/engine/core/types_test.go delete mode 100644 internal/engine/length/engine.go delete mode 100644 internal/engine/length/engine_test.go delete mode 100644 internal/engine/llm/engine.go delete mode 100644 internal/engine/pattern/engine.go delete mode 100644 internal/engine/pattern/engine_test.go delete mode 100644 internal/engine/registry/builtin.go delete mode 100644 internal/engine/registry/registry.go delete mode 100644 internal/engine/registry/registry_test.go delete mode 100644 internal/engine/style/engine.go delete mode 100644 internal/engine/style/engine_test.go delete mode 100644 internal/engine/typechecker/engine.go delete mode 100644 internal/engine/typechecker/engine_test.go diff --git a/internal/engine/README.md b/internal/engine/README.md deleted file mode 100644 index 2cb5f44..0000000 --- a/internal/engine/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# engine - -다양한 검증 엔진을 구현합니다. - -pattern, length, style, ast, llm, typechecker 등의 엔진을 제공하며, 엔진 레지스트리를 통해 통합 관리합니다. - -## 서브패키지 - -- `core`: 엔진 인터페이스 및 공통 타입 정의 -- `registry`: 엔진 등록 및 검색 시스템 -- `pattern`: 정규식 패턴 매칭 엔진 (→ adapter/eslint) -- `length`: 라인/파일 길이 검증 엔진 (→ adapter/eslint) -- `style`: 코드 스타일 검증 엔진 (→ adapter/eslint, adapter/prettier) -- `ast`: AST 구조 검증 엔진 (→ adapter/eslint, adapter/checkstyle, adapter/pmd) -- `llm`: LLM 기반 검증 엔진 (→ llm) -- `typechecker`: 타입 체킹 엔진 (→ adapter/tsc) - -**사용자**: adapter, validator -**의존성**: adapter, llm diff --git a/internal/engine/ast/engine.go b/internal/engine/ast/engine.go deleted file mode 100644 index 084f8ff..0000000 --- a/internal/engine/ast/engine.go +++ /dev/null @@ -1,273 +0,0 @@ -package ast - -import ( - "context" - "fmt" - "path/filepath" - "strings" - - "github.com/DevSymphony/sym-cli/internal/adapter" - "github.com/DevSymphony/sym-cli/internal/adapter/eslint" - adapterRegistry "github.com/DevSymphony/sym-cli/internal/adapter/registry" - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// Engine validates code structure using AST queries. -type Engine struct { - adapterRegistry *adapterRegistry.Registry - toolsDir string - workDir string -} - -// NewEngine creates a new AST engine. -func NewEngine() *Engine { - return &Engine{} -} - -// Init initializes the AST engine with Adapter Registry. -func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { - e.toolsDir = config.ToolsDir - e.workDir = config.WorkDir - - // Use provided adapter registry or create default - if config.AdapterRegistry != nil { - // Type assert to concrete type - if reg, ok := config.AdapterRegistry.(*adapterRegistry.Registry); ok { - e.adapterRegistry = reg - } else { - return fmt.Errorf("invalid adapter registry type") - } - } else { - e.adapterRegistry = adapterRegistry.DefaultRegistry() - } - - return nil -} - -// Validate checks files against AST structure rules. -func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) (*core.ValidationResult, error) { - // Filter files first - empty file list is valid without initialization - files = e.filterFiles(files, rule.When) - if len(files) == 0 { - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: true, - Violations: []core.Violation{}, - }, nil - } - - // Check initialization - if e.adapterRegistry == nil { - return nil, fmt.Errorf("AST engine not initialized") - } - - // Detect language from files and rule - language := e.detectLanguage(rule, files) - - // Get appropriate adapter for language - adp, err := e.adapterRegistry.GetAdapter(language, "ast") - if err != nil { - return nil, fmt.Errorf("no adapter found for language %s: %w", language, err) - } - - // Type assert to adapter.Adapter - astAdapter, ok := adp.(adapter.Adapter) - if !ok { - return nil, fmt.Errorf("invalid adapter type for language %s", language) - } - - // Check if adapter is available, install if needed - if err := astAdapter.CheckAvailability(ctx); err != nil { - if installErr := astAdapter.Install(ctx, adapter.InstallConfig{}); installErr != nil { - return nil, fmt.Errorf("adapter not available and installation failed: %w", installErr) - } - } - - // For ESLint adapter (JavaScript/TypeScript), use AST query - if language == "javascript" || language == "typescript" || language == "jsx" || language == "tsx" { - return e.validateWithESLint(ctx, rule, files, astAdapter) - } - - // For other languages, generate config and execute - config, err := astAdapter.GenerateConfig(&rule) - if err != nil { - return nil, fmt.Errorf("failed to generate config: %w", err) - } - - output, err := astAdapter.Execute(ctx, config, files) - if err != nil && output == nil { - return nil, fmt.Errorf("adapter execution failed: %w", err) - } - - adapterViolations, err := astAdapter.ParseOutput(output) - if err != nil { - return nil, fmt.Errorf("failed to parse output: %w", err) - } - - // Convert adapter.Violation to core.Violation - violations := make([]core.Violation, len(adapterViolations)) - for i, v := range adapterViolations { - violations[i] = core.Violation{ - File: v.File, - Line: v.Line, - Column: v.Column, - Message: v.Message, - Severity: v.Severity, - RuleID: v.RuleID, - } - } - - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: len(violations) == 0, - Violations: violations, - Engine: "ast", - }, nil -} - -// validateWithESLint validates using ESLint AST queries. -func (e *Engine) validateWithESLint(ctx context.Context, rule core.Rule, files []string, adp adapter.Adapter) (*core.ValidationResult, error) { - // Parse AST query - query, err := eslint.ParseASTQuery(&rule) - if err != nil { - return nil, fmt.Errorf("invalid AST query: %w", err) - } - - // Generate ESTree selector - selector := eslint.GenerateESTreeSelector(query) - - // Generate ESLint config using no-restricted-syntax - message := rule.Message - if message == "" { - message = fmt.Sprintf("AST rule %s violation", rule.ID) - } - - config, err := e.generateESLintConfigWithSelector(rule, selector, message) - if err != nil { - return nil, fmt.Errorf("failed to generate ESLint config: %w", err) - } - - // Execute - output, err := adp.Execute(ctx, config, files) - if err != nil && output == nil { - return nil, fmt.Errorf("execution failed: %w", err) - } - - // Parse violations - adapterViolations, err := adp.ParseOutput(output) - if err != nil { - return nil, fmt.Errorf("failed to parse output: %w", err) - } - - // Convert violations - violations := make([]core.Violation, len(adapterViolations)) - for i, v := range adapterViolations { - violations[i] = core.Violation{ - File: v.File, - Line: v.Line, - Column: v.Column, - Message: v.Message, - Severity: v.Severity, - RuleID: v.RuleID, - } - } - - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: len(violations) == 0, - Violations: violations, - Engine: "ast", - }, nil -} - -// GetCapabilities returns the engine's capabilities. -// Supported languages are determined dynamically based on registered adapters. -func (e *Engine) GetCapabilities() core.EngineCapabilities { - caps := core.EngineCapabilities{ - Name: "ast", - SupportedCategories: []string{"error_handling", "custom"}, - SupportsAutofix: false, - } - - // If registry is available, get languages dynamically - if e.adapterRegistry != nil { - caps.SupportedLanguages = e.adapterRegistry.GetSupportedLanguages("ast") - } else { - // Fallback to default JS/TS - caps.SupportedLanguages = []string{"javascript", "typescript", "jsx", "tsx"} - } - - return caps -} - -// Close cleans up the engine resources. -func (e *Engine) Close() error { - return nil -} - -// detectLanguage detects the primary language from files and rule configuration. -func (e *Engine) detectLanguage(rule core.Rule, files []string) string { - // 1. Check rule.When.Languages if specified - if rule.When != nil && len(rule.When.Languages) > 0 { - return rule.When.Languages[0] - } - - // 2. Detect from first file extension - if len(files) > 0 { - ext := strings.ToLower(filepath.Ext(files[0])) - switch ext { - case ".js": - return "javascript" - case ".ts": - return "typescript" - case ".jsx": - return "jsx" - case ".tsx": - return "tsx" - case ".java": - return "java" - case ".py": - return "python" - case ".go": - return "go" - case ".rs": - return "rust" - } - } - - // 3. Default to JavaScript - return "javascript" -} - -// filterFiles filters files based on the when selector using proper glob matching. -func (e *Engine) filterFiles(files []string, when *core.Selector) []string { - return core.FilterFiles(files, when) -} - -// generateESLintConfigWithSelector generates ESLint config using no-restricted-syntax. -func (e *Engine) generateESLintConfigWithSelector(rule core.Rule, selector string, message string) ([]byte, error) { - severity := eslint.MapSeverity(rule.Severity) - - config := map[string]interface{}{ - "env": map[string]bool{ - "es2021": true, - "node": true, - "browser": true, - }, - "parserOptions": map[string]interface{}{ - "ecmaVersion": "latest", - "sourceType": "module", - }, - "rules": map[string]interface{}{ - "no-restricted-syntax": []interface{}{ - severity, - map[string]interface{}{ - "selector": selector, - "message": message, - }, - }, - }, - } - - return eslint.MarshalConfig(config) -} diff --git a/internal/engine/ast/engine_test.go b/internal/engine/ast/engine_test.go deleted file mode 100644 index 0644548..0000000 --- a/internal/engine/ast/engine_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package ast - -import ( - "context" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestNewEngine(t *testing.T) { - engine := NewEngine() - if engine == nil { - t.Fatal("NewEngine() returned nil") - } -} - -func TestGetCapabilities(t *testing.T) { - engine := NewEngine() - caps := engine.GetCapabilities() - - if caps.Name != "ast" { - t.Errorf("Name = %s, want ast", caps.Name) - } - - if !contains(caps.SupportedLanguages, "javascript") { - t.Error("Expected javascript in supported languages") - } - - if !contains(caps.SupportedCategories, "error_handling") { - t.Error("Expected error_handling in supported categories") - } - - if caps.SupportsAutofix { - t.Error("AST engine should not support autofix") - } -} - -func TestInit(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: false, - } - - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init failed (expected if ESLint not available): %v", err) - } -} - -func TestClose(t *testing.T) { - engine := NewEngine() - if err := engine.Close(); err != nil { - t.Errorf("Close() error = %v", err) - } -} - -func TestValidate_NoFiles(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "error_handling", - Severity: "error", - Check: map[string]interface{}{ - "engine": "ast", - "node": "CallExpression", - }, - } - - result, err := engine.Validate(ctx, rule, []string{}) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Error("Expected validation to pass for empty file list") - } -} - -func TestValidate_WithInitialization(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "error_handling", - Severity: "error", - Check: map[string]interface{}{ - "engine": "ast", - "node": "CallExpression", - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_WithWhereClause(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-WHERE", - Category: "error_handling", - Severity: "error", - Check: map[string]interface{}{ - "engine": "ast", - "node": "CallExpression", - "where": map[string]interface{}{ - "func": map[string]interface{}{ - "in": []string{"open", "readFile"}, - }, - }, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_WithHasClause(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-HAS", - Category: "error_handling", - Severity: "error", - Check: map[string]interface{}{ - "engine": "ast", - "node": "CallExpression", - "has": []string{"TryStatement"}, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_WithNotHasClause(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-NOT-HAS", - Category: "error_handling", - Severity: "error", - Check: map[string]interface{}{ - "engine": "ast", - "node": "FunctionDeclaration", - "notHas": []string{"JSDocComment"}, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_WithCustomMessage(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-CUSTOM", - Category: "error_handling", - Severity: "error", - Message: "Custom AST violation", - Check: map[string]interface{}{ - "engine": "ast", - "node": "CallExpression", - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -// TestMatchesSelector has been moved to core package tests. -// File filtering logic is now centralized in core.FilterFiles. - -// TestMatchesLanguage has been moved to core package tests. -// Language matching logic is now centralized in core.MatchesLanguage. - -// Helper functions - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} diff --git a/internal/engine/core/engine.go b/internal/engine/core/engine.go deleted file mode 100644 index f00f01d..0000000 --- a/internal/engine/core/engine.go +++ /dev/null @@ -1,118 +0,0 @@ -package core - -import ( - "context" - "time" -) - -// AdapterRegistry is an interface for adapter registry to avoid import cycles. -// The actual implementation is in internal/adapter/registry. -type AdapterRegistry interface { - GetAdapter(language, category string) (interface{}, error) - GetAll() []interface{} - GetSupportedLanguages(category string) []string -} - -// Engine is the interface that all validation engines must implement. -// Engines validate code against specific rule types (pattern, length, style, etc.). -// -// Design Philosophy: -// - Engines are language-agnostic at the interface level -// - Language-specific implementations are provided via LanguageProvider -// - External tools (ESLint, Prettier, etc.) are wrapped by adapters -type Engine interface { - // Init initializes the engine with configuration. - // Called once before validation begins. - Init(ctx context.Context, config EngineConfig) error - - // Validate validates files against a rule. - // Returns violations found, or nil if validation passed. - // - // The engine should: - // 1. Parse rule.Check configuration - // 2. Delegate to appropriate adapter (e.g., ESLint for JavaScript) - // 3. Collect and return violations - Validate(ctx context.Context, rule Rule, files []string) (*ValidationResult, error) - - // GetCapabilities returns engine capabilities (languages, features, etc.). - GetCapabilities() EngineCapabilities - - // Close cleans up resources (close connections, temp files, etc.). - Close() error -} - -// EngineConfig holds engine initialization settings. -type EngineConfig struct { - // WorkDir is the working directory (usually project root). - WorkDir string - - // ToolsDir is where external tools are installed. - // Default: ~/.sym/tools - ToolsDir string - - // CacheDir is for caching validation results. - // Default: ~/.sym/cache - CacheDir string - - // Timeout is the max time for a single validation. - // Default: 2 minutes - Timeout time.Duration - - // Parallelism is max concurrent validations. - // 0 = runtime.NumCPU() - Parallelism int - - // Debug enables verbose logging. - Debug bool - - // AdapterRegistry provides access to language-specific adapters. - // If nil, engines will create a default registry. - AdapterRegistry AdapterRegistry - - // Extra holds engine-specific config. - Extra map[string]interface{} -} - -// EngineCapabilities describes what an engine supports. -type EngineCapabilities struct { - // Name is the engine identifier (e.g., "pattern", "style"). - Name string - - // SupportedLanguages lists supported languages. - // Empty = language-agnostic (e.g., commit engine). - // Example: ["javascript", "typescript", "jsx", "tsx"] - SupportedLanguages []string - - // SupportedCategories lists rule categories this engine handles. - // Example: ["naming", "security"] for pattern engine. - SupportedCategories []string - - // SupportsAutofix indicates if engine can auto-fix violations. - SupportsAutofix bool - - // RequiresCompilation indicates if compiled artifacts needed. - // Example: ArchUnit requires .class files (Java). - RequiresCompilation bool - - // ExternalTools lists required external tools. - // Example: ["eslint@^8.0.0", "prettier@^3.0.0"] - ExternalTools []ToolRequirement -} - -// ToolRequirement specifies an external tool dependency. -type ToolRequirement struct { - // Name is the tool name (e.g., "eslint"). - Name string - - // Version is the required version (e.g., "^8.0.0"). - // Empty = any version. - Version string - - // Optional indicates if tool is optional. - // If true, engine falls back to internal implementation. - Optional bool - - // InstallCommand is the command to install the tool. - // Example: "npm install -g eslint@^8.0.0" - InstallCommand string -} diff --git a/internal/engine/core/selector.go b/internal/engine/core/selector.go deleted file mode 100644 index 83459ca..0000000 --- a/internal/engine/core/selector.go +++ /dev/null @@ -1,127 +0,0 @@ -package core - -import ( - "path/filepath" - "strings" - - "github.com/bmatcuk/doublestar/v4" -) - -// MatchGlob checks if a file path matches a glob pattern. -// Supports doublestar patterns (e.g., "**/*.js", "src/**/test_*.go"). -func MatchGlob(filePath, pattern string) (bool, error) { - // Normalize paths for consistent matching - filePath = filepath.ToSlash(filePath) - pattern = filepath.ToSlash(pattern) - - return doublestar.Match(pattern, filePath) -} - -// MatchesLanguage checks if a file extension matches a language. -func MatchesLanguage(filePath, language string) bool { - ext := strings.ToLower(filepath.Ext(filePath)) - - // Language to extension mapping - langExtMap := map[string][]string{ - "javascript": {".js", ".mjs", ".cjs"}, - "js": {".js", ".mjs", ".cjs"}, - "typescript": {".ts", ".mts", ".cts"}, - "ts": {".ts", ".mts", ".cts"}, - "jsx": {".jsx"}, - "tsx": {".tsx"}, - "python": {".py", ".pyi", ".pyw"}, - "py": {".py", ".pyi", ".pyw"}, - "go": {".go"}, - "golang": {".go"}, - "java": {".java"}, - "c": {".c", ".h"}, - "cpp": {".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"}, - "c++": {".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"}, - "rust": {".rs"}, - "ruby": {".rb"}, - "php": {".php"}, - "swift": {".swift"}, - "kotlin": {".kt", ".kts"}, - "scala": {".scala"}, - "shell": {".sh", ".bash", ".zsh"}, - "sh": {".sh", ".bash", ".zsh"}, - } - - exts, ok := langExtMap[strings.ToLower(language)] - if !ok { - return false - } - - for _, e := range exts { - if ext == e { - return true - } - } - return false -} - -// MatchesSelector checks if a file matches the selector criteria. -// Returns true if the file passes all selector filters. -func MatchesSelector(filePath string, selector *Selector) bool { - if selector == nil { - return true - } - - // Normalize file path - filePath = filepath.ToSlash(filePath) - - // Language filter - if len(selector.Languages) > 0 { - matched := false - for _, lang := range selector.Languages { - if MatchesLanguage(filePath, lang) { - matched = true - break - } - } - if !matched { - return false - } - } - - // Include filter (if specified, file must match at least one pattern) - if len(selector.Include) > 0 { - matched := false - for _, pattern := range selector.Include { - if m, err := MatchGlob(filePath, pattern); err == nil && m { - matched = true - break - } - } - if !matched { - return false - } - } - - // Exclude filter (if file matches any pattern, exclude it) - if len(selector.Exclude) > 0 { - for _, pattern := range selector.Exclude { - if m, err := MatchGlob(filePath, pattern); err == nil && m { - return false - } - } - } - - return true -} - -// FilterFiles filters a list of files based on the selector criteria. -// Returns a new slice containing only the files that match. -func FilterFiles(files []string, selector *Selector) []string { - if selector == nil { - return files - } - - filtered := make([]string, 0, len(files)) - for _, file := range files { - if MatchesSelector(file, selector) { - filtered = append(filtered, file) - } - } - return filtered -} diff --git a/internal/engine/core/selector_test.go b/internal/engine/core/selector_test.go deleted file mode 100644 index 77acd7c..0000000 --- a/internal/engine/core/selector_test.go +++ /dev/null @@ -1,581 +0,0 @@ -package core - -import ( - "testing" -) - -func TestMatchGlob(t *testing.T) { - tests := []struct { - name string - filePath string - pattern string - want bool - wantErr bool - }{ - // Simple patterns - { - name: "exact match", - filePath: "main.go", - pattern: "main.go", - want: true, - }, - { - name: "wildcard extension", - filePath: "main.go", - pattern: "*.go", - want: true, - }, - { - name: "wildcard name", - filePath: "main.go", - pattern: "main.*", - want: true, - }, - { - name: "no match", - filePath: "main.go", - pattern: "*.js", - want: false, - }, - - // Doublestar patterns - { - name: "doublestar all files", - filePath: "src/main.go", - pattern: "**/*.go", - want: true, - }, - { - name: "doublestar nested", - filePath: "src/foo/bar/test.js", - pattern: "src/**/*.js", - want: true, - }, - { - name: "doublestar no match", - filePath: "test/main.go", - pattern: "src/**/*.go", - want: false, - }, - { - name: "doublestar middle", - filePath: "src/foo/bar/baz/test.ts", - pattern: "src/**/test.ts", - want: true, - }, - - // Path-specific patterns - { - name: "specific directory", - filePath: "src/components/Button.tsx", - pattern: "src/components/*.tsx", - want: true, - }, - { - name: "exclude test files", - filePath: "src/main_test.go", - pattern: "**/*_test.go", - want: true, - }, - { - name: "exclude test directory", - filePath: "tests/unit/main.go", - pattern: "tests/**/*.go", - want: true, - }, - - // Windows-style paths are normalized to forward slashes - // Note: In actual usage, filepath operations will handle OS-specific separators - { - name: "mixed separators", - filePath: "src/subdir/main.go", - pattern: "src/**/*.go", - want: true, - }, - - // Edge cases - { - name: "empty pattern", - filePath: "main.go", - pattern: "", - want: false, - }, - { - name: "root level file", - filePath: "main.go", - pattern: "*.go", - want: true, - }, - { - name: "multiple wildcards", - filePath: "src/foo/bar/test_main.go", - pattern: "src/**/test_*.go", - want: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := MatchGlob(tt.filePath, tt.pattern) - if (err != nil) != tt.wantErr { - t.Errorf("MatchGlob() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("MatchGlob(%q, %q) = %v, want %v", tt.filePath, tt.pattern, got, tt.want) - } - }) - } -} - -func TestMatchesLanguage(t *testing.T) { - tests := []struct { - name string - filePath string - language string - want bool - }{ - // JavaScript variants - {"js standard", "main.js", "javascript", true}, - {"js module", "main.mjs", "javascript", true}, - {"js commonjs", "main.cjs", "javascript", true}, - {"js short", "main.js", "js", true}, - {"jsx", "Component.jsx", "jsx", true}, - - // TypeScript variants - {"ts standard", "main.ts", "typescript", true}, - {"ts module", "main.mts", "typescript", true}, - {"ts commonjs", "main.cts", "typescript", true}, - {"ts short", "main.ts", "ts", true}, - {"tsx", "Component.tsx", "tsx", true}, - - // Python variants - {"py standard", "main.py", "python", true}, - {"py interface", "main.pyi", "python", true}, - {"py windows", "main.pyw", "python", true}, - {"py short", "main.py", "py", true}, - - // Go - {"go standard", "main.go", "go", true}, - {"go long name", "main.go", "golang", true}, - - // Other languages - {"java", "Main.java", "java", true}, - {"c", "main.c", "c", true}, - {"c header", "main.h", "c", true}, - {"cpp", "main.cpp", "cpp", true}, - {"cpp alt", "main.cc", "cpp", true}, - {"cpp header", "main.hpp", "cpp", true}, - {"rust", "main.rs", "rust", true}, - {"ruby", "main.rb", "ruby", true}, - {"php", "index.php", "php", true}, - {"swift", "Main.swift", "swift", true}, - {"kotlin", "Main.kt", "kotlin", true}, - {"kotlin script", "build.kts", "kotlin", true}, - {"scala", "Main.scala", "scala", true}, - {"shell", "script.sh", "shell", true}, - {"bash", "script.bash", "shell", true}, - {"shell short", "script.sh", "sh", true}, - - // Case insensitivity - {"uppercase ext", "Main.GO", "go", true}, - {"uppercase lang", "main.js", "JAVASCRIPT", true}, - {"mixed case", "Main.JS", "JavaScript", true}, - - // No match - {"wrong extension", "main.go", "javascript", false}, - {"unknown language", "main.xyz", "xyz", false}, - {"empty extension", "README", "go", false}, - {"no extension match", "main.txt", "javascript", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := MatchesLanguage(tt.filePath, tt.language) - if got != tt.want { - t.Errorf("MatchesLanguage(%q, %q) = %v, want %v", tt.filePath, tt.language, got, tt.want) - } - }) - } -} - -func TestMatchesSelector(t *testing.T) { - tests := []struct { - name string - filePath string - selector *Selector - want bool - }{ - // Nil selector (should match all) - { - name: "nil selector", - filePath: "any/file.go", - selector: nil, - want: true, - }, - - // Language only - { - name: "language match", - filePath: "main.go", - selector: &Selector{Languages: []string{"go"}}, - want: true, - }, - { - name: "language no match", - filePath: "main.go", - selector: &Selector{Languages: []string{"javascript"}}, - want: false, - }, - { - name: "multiple languages match first", - filePath: "main.js", - selector: &Selector{Languages: []string{"javascript", "typescript"}}, - want: true, - }, - { - name: "multiple languages match second", - filePath: "main.ts", - selector: &Selector{Languages: []string{"javascript", "typescript"}}, - want: true, - }, - - // Include only - { - name: "include match", - filePath: "src/main.go", - selector: &Selector{Include: []string{"src/**/*.go"}}, - want: true, - }, - { - name: "include no match", - filePath: "test/main.go", - selector: &Selector{Include: []string{"src/**/*.go"}}, - want: false, - }, - { - name: "multiple includes match first", - filePath: "src/main.go", - selector: &Selector{Include: []string{"src/**/*.go", "lib/**/*.go"}}, - want: true, - }, - { - name: "multiple includes match second", - filePath: "lib/util.go", - selector: &Selector{Include: []string{"src/**/*.go", "lib/**/*.go"}}, - want: true, - }, - { - name: "multiple includes no match", - filePath: "test/main.go", - selector: &Selector{Include: []string{"src/**/*.go", "lib/**/*.go"}}, - want: false, - }, - - // Exclude only - { - name: "exclude match - should reject", - filePath: "src/main_test.go", - selector: &Selector{Exclude: []string{"**/*_test.go"}}, - want: false, - }, - { - name: "exclude no match - should accept", - filePath: "src/main.go", - selector: &Selector{Exclude: []string{"**/*_test.go"}}, - want: true, - }, - { - name: "multiple excludes match first", - filePath: "node_modules/pkg/index.js", - selector: &Selector{Exclude: []string{"node_modules/**", "dist/**"}}, - want: false, - }, - { - name: "multiple excludes match second", - filePath: "dist/bundle.js", - selector: &Selector{Exclude: []string{"node_modules/**", "dist/**"}}, - want: false, - }, - - // Combined filters - { - name: "language + include both match", - filePath: "src/main.go", - selector: &Selector{ - Languages: []string{"go"}, - Include: []string{"src/**/*.go"}, - }, - want: true, - }, - { - name: "language match but include no match", - filePath: "test/main.go", - selector: &Selector{ - Languages: []string{"go"}, - Include: []string{"src/**/*.go"}, - }, - want: false, - }, - { - name: "include match but language no match", - filePath: "src/main.js", - selector: &Selector{ - Languages: []string{"go"}, - Include: []string{"src/**/*"}, - }, - want: false, - }, - { - name: "language + include match, exclude no match", - filePath: "src/main.go", - selector: &Selector{ - Languages: []string{"go"}, - Include: []string{"src/**/*.go"}, - Exclude: []string{"**/*_test.go"}, - }, - want: true, - }, - { - name: "language + include match, but excluded", - filePath: "src/main_test.go", - selector: &Selector{ - Languages: []string{"go"}, - Include: []string{"src/**/*.go"}, - Exclude: []string{"**/*_test.go"}, - }, - want: false, - }, - - // Complex real-world scenarios - { - name: "source files only, no tests, no vendor", - filePath: "src/components/Button.tsx", - selector: &Selector{ - Languages: []string{"tsx", "typescript"}, - Include: []string{"src/**/*.{ts,tsx}"}, - Exclude: []string{"**/*.test.ts", "**/*.test.tsx", "**/vendor/**"}, - }, - want: true, - }, - { - name: "exclude test file", - filePath: "src/components/Button.test.tsx", - selector: &Selector{ - Languages: []string{"tsx", "typescript"}, - Include: []string{"src/**/*.{ts,tsx}"}, - Exclude: []string{"**/*.test.ts", "**/*.test.tsx"}, - }, - want: false, - }, - { - name: "public API files only", - filePath: "src/public/api.go", - selector: &Selector{ - Languages: []string{"go"}, - Include: []string{"src/public/**/*.go"}, - Exclude: []string{"**/*_internal.go"}, - }, - want: true, - }, - { - name: "exclude internal file", - filePath: "src/public/api_internal.go", - selector: &Selector{ - Languages: []string{"go"}, - Include: []string{"src/public/**/*.go"}, - Exclude: []string{"**/*_internal.go"}, - }, - want: false, - }, - - // Edge cases - { - name: "empty languages list - should match all", - filePath: "main.go", - selector: &Selector{Languages: []string{}}, - want: true, - }, - { - name: "empty include list - should match all", - filePath: "any/path/file.go", - selector: &Selector{Include: []string{}}, - want: true, - }, - { - name: "empty exclude list - should not exclude", - filePath: "any/path/file.go", - selector: &Selector{Exclude: []string{}}, - want: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := MatchesSelector(tt.filePath, tt.selector) - if got != tt.want { - t.Errorf("MatchesSelector(%q, %+v) = %v, want %v", tt.filePath, tt.selector, got, tt.want) - } - }) - } -} - -func TestFilterFiles(t *testing.T) { - files := []string{ - "src/main.go", - "src/util.go", - "src/main_test.go", - "test/integration.go", - "lib/helper.js", - "lib/index.ts", - "dist/bundle.js", - "node_modules/pkg/index.js", - } - - tests := []struct { - name string - files []string - selector *Selector - want []string - }{ - { - name: "nil selector - all files", - files: files, - selector: nil, - want: files, - }, - { - name: "go files only", - files: files, - selector: &Selector{ - Languages: []string{"go"}, - }, - want: []string{ - "src/main.go", - "src/util.go", - "src/main_test.go", - "test/integration.go", - }, - }, - { - name: "src directory only", - files: files, - selector: &Selector{ - Include: []string{"src/**/*"}, - }, - want: []string{ - "src/main.go", - "src/util.go", - "src/main_test.go", - }, - }, - { - name: "exclude test files", - files: files, - selector: &Selector{ - Exclude: []string{"**/*_test.go", "**/test/**"}, - }, - want: []string{ - "src/main.go", - "src/util.go", - "lib/helper.js", - "lib/index.ts", - "dist/bundle.js", - "node_modules/pkg/index.js", - }, - }, - { - name: "go files in src, no tests", - files: files, - selector: &Selector{ - Languages: []string{"go"}, - Include: []string{"src/**/*.go"}, - Exclude: []string{"**/*_test.go"}, - }, - want: []string{ - "src/main.go", - "src/util.go", - }, - }, - { - name: "js/ts but exclude dist and node_modules", - files: files, - selector: &Selector{ - Languages: []string{"javascript", "typescript"}, - Exclude: []string{"dist/**", "node_modules/**"}, - }, - want: []string{ - "lib/helper.js", - "lib/index.ts", - }, - }, - { - name: "empty file list", - files: []string{}, - selector: &Selector{Languages: []string{"go"}}, - want: []string{}, - }, - { - name: "no matches", - files: files, - selector: &Selector{ - Languages: []string{"python"}, - }, - want: []string{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := FilterFiles(tt.files, tt.selector) - if len(got) != len(tt.want) { - t.Errorf("FilterFiles() returned %d files, want %d", len(got), len(tt.want)) - t.Errorf("got: %v", got) - t.Errorf("want: %v", tt.want) - return - } - for i, file := range got { - if file != tt.want[i] { - t.Errorf("FilterFiles()[%d] = %q, want %q", i, file, tt.want[i]) - } - } - }) - } -} - -// Benchmark tests -func BenchmarkMatchGlob(b *testing.B) { - for i := 0; i < b.N; i++ { - _, _ = MatchGlob("src/foo/bar/baz/test.go", "src/**/*.go") - } -} - -func BenchmarkMatchesSelector(b *testing.B) { - selector := &Selector{ - Languages: []string{"go"}, - Include: []string{"src/**/*.go"}, - Exclude: []string{"**/*_test.go"}, - } - for i := 0; i < b.N; i++ { - MatchesSelector("src/foo/bar/main.go", selector) - } -} - -func BenchmarkFilterFiles(b *testing.B) { - files := make([]string, 1000) - for i := 0; i < 1000; i++ { - if i%2 == 0 { - files[i] = "src/file.go" - } else { - files[i] = "test/file_test.go" - } - } - selector := &Selector{ - Languages: []string{"go"}, - Exclude: []string{"**/*_test.go"}, - } - b.ResetTimer() - for i := 0; i < b.N; i++ { - FilterFiles(files, selector) - } -} diff --git a/internal/engine/core/types.go b/internal/engine/core/types.go deleted file mode 100644 index 239490f..0000000 --- a/internal/engine/core/types.go +++ /dev/null @@ -1,162 +0,0 @@ -package core - -import ( - "encoding/json" - "fmt" - "time" -) - -// Rule represents a validation rule from the policy. -// Maps to PolicyRule in pkg/schema/types.go. -type Rule struct { - ID string `json:"id"` - Enabled bool `json:"enabled"` - Category string `json:"category"` - Severity string `json:"severity"` // "error", "warning", "info" - Desc string `json:"desc,omitempty"` - When *Selector `json:"when,omitempty"` - Check map[string]interface{} `json:"check"` // Engine-specific config - Remedy *Remedy `json:"remedy,omitempty"` - Message string `json:"message,omitempty"` -} - -// Selector defines when a rule applies. -type Selector struct { - Languages []string `json:"languages,omitempty"` // ["javascript", "typescript"] - Include []string `json:"include,omitempty"` // ["src/**/*.js"] - Exclude []string `json:"exclude,omitempty"` // ["**/*.test.js"] - Branches []string `json:"branches,omitempty"` // ["main", "develop"] - Roles []string `json:"roles,omitempty"` // ["dev", "reviewer"] - Tags []string `json:"tags,omitempty"` // ["critical", "style"] -} - -// Remedy contains auto-fix configuration. -type Remedy struct { - Autofix bool `json:"autofix"` - Tool string `json:"tool,omitempty"` // "prettier", "eslint" - Config map[string]interface{} `json:"config,omitempty"` // Tool-specific config -} - -// ValidationResult is the outcome of validating files against a rule. -type ValidationResult struct { - RuleID string `json:"ruleId"` - Passed bool `json:"passed"` - Violations []Violation `json:"violations,omitempty"` - Metrics *Metrics `json:"metrics,omitempty"` - Duration time.Duration `json:"-"` // Serialized separately - Engine string `json:"engine"` - Language string `json:"language,omitempty"` -} - -// Violation represents a single rule violation. -type Violation struct { - File string `json:"file"` - Line int `json:"line"` // 1-indexed, 0 if N/A - Column int `json:"column"` // 1-indexed, 0 if N/A - EndLine int `json:"endLine,omitempty"` // For multi-line - EndColumn int `json:"endColumn,omitempty"` - Message string `json:"message"` - Severity string `json:"severity"` // "error", "warning", "info" - RuleID string `json:"ruleId"` - Category string `json:"category,omitempty"` - Suggestion *Suggestion `json:"suggestion,omitempty"` - Context map[string]interface{} `json:"context,omitempty"` // Extra info -} - -// Suggestion represents an auto-fix suggestion. -type Suggestion struct { - Desc string `json:"desc"` // "Change to single quotes" - Replacement string `json:"replacement,omitempty"` // Fixed text - Diff string `json:"diff,omitempty"` // Unified diff -} - -// Metrics contains validation metrics. -type Metrics struct { - FilesProcessed int `json:"filesProcessed"` - LinesProcessed int `json:"linesProcessed"` - Custom map[string]interface{} `json:"custom,omitempty"` // Engine-specific -} - -// String returns a human-readable violation description. -// Format: "path/to/file.js:10:5: message [RULE-ID]" -func (v *Violation) String() string { - loc := v.File - if v.Line > 0 { - loc = fmt.Sprintf("%s:%d", loc, v.Line) - if v.Column > 0 { - loc = fmt.Sprintf("%s:%d", loc, v.Column) - } - } - return fmt.Sprintf("%s: %s [%s]", loc, v.Message, v.RuleID) -} - -// MarshalJSON customizes JSON serialization for ValidationResult. -// Converts Duration to string (e.g., "1.5s"). -func (r *ValidationResult) MarshalJSON() ([]byte, error) { - type Alias ValidationResult - return json.Marshal(&struct { - Duration string `json:"duration"` - *Alias - }{ - Duration: r.Duration.String(), - Alias: (*Alias)(r), - }) -} - -// GetString safely extracts a string value from Check config. -// Returns empty string if key doesn't exist or type mismatch. -func (r *Rule) GetString(key string) string { - if v, ok := r.Check[key]; ok { - if s, ok := v.(string); ok { - return s - } - } - return "" -} - -// GetInt safely extracts an int value from Check config. -// Returns 0 if key doesn't exist or type mismatch. -func (r *Rule) GetInt(key string) int { - if v, ok := r.Check[key]; ok { - switch val := v.(type) { - case int: - return val - case float64: // JSON numbers are float64 - return int(val) - } - } - return 0 -} - -// GetBool safely extracts a bool value from Check config. -// Returns false if key doesn't exist or type mismatch. -func (r *Rule) GetBool(key string) bool { - if v, ok := r.Check[key]; ok { - if b, ok := v.(bool); ok { - return b - } - } - return false -} - -// GetStringSlice safely extracts a []string from Check config. -// Returns nil if key doesn't exist or type mismatch. -func (r *Rule) GetStringSlice(key string) []string { - if v, ok := r.Check[key]; ok { - // Handle []interface{} from JSON unmarshaling - if arr, ok := v.([]interface{}); ok { - result := make([]string, 0, len(arr)) - for _, item := range arr { - if s, ok := item.(string); ok { - result = append(result, s) - } - } - return result - } - // Handle native []string - if arr, ok := v.([]string); ok { - return arr - } - } - return nil -} diff --git a/internal/engine/core/types_test.go b/internal/engine/core/types_test.go deleted file mode 100644 index 0c498a6..0000000 --- a/internal/engine/core/types_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package core - -import ( - "testing" -) - -func TestRule_GetString(t *testing.T) { - rule := &Rule{ - Check: map[string]interface{}{ - "engine": "pattern", - "target": "identifier", - }, - } - - if got := rule.GetString("engine"); got != "pattern" { - t.Errorf("GetString(engine) = %q, want %q", got, "pattern") - } - - if got := rule.GetString("missing"); got != "" { - t.Errorf("GetString(missing) = %q, want empty", got) - } -} - -func TestRule_GetInt(t *testing.T) { - rule := &Rule{ - Check: map[string]interface{}{ - "max": 100, - "float": 50.5, - }, - } - - if got := rule.GetInt("max"); got != 100 { - t.Errorf("GetInt(max) = %d, want 100", got) - } - - if got := rule.GetInt("float"); got != 50 { - t.Errorf("GetInt(float) = %d, want 50", got) - } - - if got := rule.GetInt("missing"); got != 0 { - t.Errorf("GetInt(missing) = %d, want 0", got) - } -} - -func TestRule_GetBool(t *testing.T) { - rule := &Rule{ - Check: map[string]interface{}{ - "enabled": true, - }, - } - - if got := rule.GetBool("enabled"); got != true { - t.Errorf("GetBool(enabled) = %v, want true", got) - } - - if got := rule.GetBool("missing"); got != false { - t.Errorf("GetBool(missing) = %v, want false", got) - } -} - -func TestRule_GetStringSlice(t *testing.T) { - rule := &Rule{ - Check: map[string]interface{}{ - "languages": []interface{}{"javascript", "typescript"}, - "native": []string{"go", "rust"}, - }, - } - - got := rule.GetStringSlice("languages") - want := []string{"javascript", "typescript"} - if len(got) != len(want) { - t.Fatalf("GetStringSlice(languages) length = %d, want %d", len(got), len(want)) - } - for i := range got { - if got[i] != want[i] { - t.Errorf("GetStringSlice(languages)[%d] = %q, want %q", i, got[i], want[i]) - } - } - - if got := rule.GetStringSlice("missing"); got != nil { - t.Errorf("GetStringSlice(missing) = %v, want nil", got) - } -} - -func TestViolation_String(t *testing.T) { - tests := []struct { - name string - v Violation - want string - }{ - { - name: "full location", - v: Violation{ - File: "src/app.js", - Line: 10, - Column: 5, - Message: "Missing semicolon", - RuleID: "STYLE-SEMI", - }, - want: "src/app.js:10:5: Missing semicolon [STYLE-SEMI]", - }, - { - name: "line only", - v: Violation{ - File: "src/utils.js", - Line: 42, - Message: "Line too long", - RuleID: "LENGTH-LINE", - }, - want: "src/utils.js:42: Line too long [LENGTH-LINE]", - }, - { - name: "file only", - v: Violation{ - File: "README.md", - Message: "File too long", - RuleID: "LENGTH-FILE", - }, - want: "README.md: File too long [LENGTH-FILE]", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := tt.v.String() - if got != tt.want { - t.Errorf("String() = %q, want %q", got, tt.want) - } - }) - } -} diff --git a/internal/engine/length/engine.go b/internal/engine/length/engine.go deleted file mode 100644 index 4bce074..0000000 --- a/internal/engine/length/engine.go +++ /dev/null @@ -1,200 +0,0 @@ -package length - -import ( - "context" - "fmt" - "path/filepath" - "strings" - "time" - - "github.com/DevSymphony/sym-cli/internal/adapter" - adapterRegistry "github.com/DevSymphony/sym-cli/internal/adapter/registry" - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// Engine validates length constraint rules (line, file, function, params). -// -// Supports multiple languages through adapter registry: -// - JavaScript/TypeScript: ESLint (max-len, max-lines, max-lines-per-function, max-params) -// - Java: Checkstyle (LineLength, FileLength, MethodLength, ParameterNumber) -type Engine struct { - adapterRegistry *adapterRegistry.Registry - config core.EngineConfig -} - -// NewEngine creates a new length engine. -func NewEngine() *Engine { - return &Engine{} -} - -// Init initializes the engine with adapter registry. -func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { - e.config = config - - // Use provided adapter registry or create default - if config.AdapterRegistry != nil { - if reg, ok := config.AdapterRegistry.(*adapterRegistry.Registry); ok { - e.adapterRegistry = reg - } else { - return fmt.Errorf("invalid adapter registry type") - } - } else { - e.adapterRegistry = adapterRegistry.DefaultRegistry() - } - - return nil -} - -// Validate validates files against a length rule. -func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) (*core.ValidationResult, error) { - start := time.Now() - - // Filter files - files = e.filterFiles(files, rule.When) - - if len(files) == 0 { - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: true, - Engine: "length", - Duration: time.Since(start), - }, nil - } - - // Check initialization - if e.adapterRegistry == nil { - return nil, fmt.Errorf("length engine not initialized") - } - - // Detect language - language := e.detectLanguage(rule, files) - - // Get appropriate adapter for language - adp, err := e.adapterRegistry.GetAdapter(language, "length") - if err != nil { - return nil, fmt.Errorf("no adapter found for language %s: %w", language, err) - } - - // Type assert to adapter.Adapter - lengthAdapter, ok := adp.(adapter.Adapter) - if !ok { - return nil, fmt.Errorf("invalid adapter type for language %s", language) - } - - // Check if adapter is available, install if needed - if err := lengthAdapter.CheckAvailability(ctx); err != nil { - if installErr := lengthAdapter.Install(ctx, adapter.InstallConfig{}); installErr != nil { - return nil, fmt.Errorf("adapter not available and installation failed: %w", installErr) - } - } - - // Generate config - config, err := lengthAdapter.GenerateConfig(&rule) - if err != nil { - return nil, fmt.Errorf("failed to generate config: %w", err) - } - - // Execute adapter - output, err := lengthAdapter.Execute(ctx, config, files) - if err != nil && output == nil { - return nil, fmt.Errorf("failed to execute adapter: %w", err) - } - - // Parse output - adapterViolations, err := lengthAdapter.ParseOutput(output) - if err != nil { - return nil, fmt.Errorf("failed to parse output: %w", err) - } - - // Convert to core violations - violations := make([]core.Violation, len(adapterViolations)) - for i, av := range adapterViolations { - violations[i] = core.Violation{ - File: av.File, - Line: av.Line, - Column: av.Column, - Message: av.Message, - Severity: av.Severity, - RuleID: rule.ID, - Category: rule.Category, - } - - if rule.Message != "" { - violations[i].Message = rule.Message - } - } - - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: len(violations) == 0, - Violations: violations, - Duration: time.Since(start), - Engine: "length", - Language: language, - }, nil -} - -// GetCapabilities returns engine capabilities. -// Supported languages are determined dynamically based on registered adapters. -func (e *Engine) GetCapabilities() core.EngineCapabilities { - caps := core.EngineCapabilities{ - Name: "length", - SupportedCategories: []string{"formatting", "style"}, - SupportsAutofix: false, - RequiresCompilation: false, - } - - // If registry is available, get languages dynamically - if e.adapterRegistry != nil { - caps.SupportedLanguages = e.adapterRegistry.GetSupportedLanguages("length") - } else { - // Fallback to default JS/TS - caps.SupportedLanguages = []string{"javascript", "typescript", "jsx", "tsx"} - } - - return caps -} - -// Close cleans up resources. -func (e *Engine) Close() error { - return nil -} - -// filterFiles filters files based on selector using proper glob matching. -func (e *Engine) filterFiles(files []string, selector *core.Selector) []string { - return core.FilterFiles(files, selector) -} - -// detectLanguage detects the primary language from files and rule configuration. -func (e *Engine) detectLanguage(rule core.Rule, files []string) string { - // 1. Check rule.When.Languages if specified - if rule.When != nil && len(rule.When.Languages) > 0 { - return rule.When.Languages[0] - } - - // 2. Detect from first file extension - if len(files) > 0 { - ext := strings.ToLower(filepath.Ext(files[0])) - switch ext { - case ".js": - return "javascript" - case ".ts": - return "typescript" - case ".jsx": - return "jsx" - case ".tsx": - return "tsx" - case ".java": - return "java" - case ".py": - return "python" - case ".go": - return "go" - case ".rs": - return "rust" - } - } - - // 3. Default to JavaScript - return "javascript" -} diff --git a/internal/engine/length/engine_test.go b/internal/engine/length/engine_test.go deleted file mode 100644 index 7ba8392..0000000 --- a/internal/engine/length/engine_test.go +++ /dev/null @@ -1,338 +0,0 @@ -package length - -import ( - "context" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestNewEngine(t *testing.T) { - engine := NewEngine() - if engine == nil { - t.Fatal("NewEngine() returned nil") - } -} - -func TestGetCapabilities(t *testing.T) { - engine := NewEngine() - caps := engine.GetCapabilities() - - if caps.Name != "length" { - t.Errorf("Name = %s, want length", caps.Name) - } - - if !contains(caps.SupportedLanguages, "javascript") { - t.Error("Expected javascript in supported languages") - } - - if !contains(caps.SupportedCategories, "formatting") { - t.Error("Expected formatting in supported categories") - } - - if caps.SupportsAutofix { - t.Error("Length engine should not support autofix") - } -} - -func TestInit(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: false, - } - - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init failed (expected if ESLint not available): %v", err) - } -} - -func TestClose(t *testing.T) { - engine := NewEngine() - if err := engine.Close(); err != nil { - t.Errorf("Close() error = %v", err) - } -} - -func TestValidate_NoFiles(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "formatting", - Severity: "error", - Check: map[string]interface{}{ - "engine": "length", - "scope": "line", - "max": 100, - }, - } - - result, err := engine.Validate(ctx, rule, []string{}) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Error("Expected validation to pass for empty file list") - } -} - -func TestValidate_NotInitialized(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "formatting", - Severity: "error", - Check: map[string]interface{}{ - "engine": "length", - }, - } - - _, err := engine.Validate(ctx, rule, []string{"test.js"}) - if err == nil { - t.Error("Expected error for uninitialized engine") - } -} - -func TestFilterFiles(t *testing.T) { - engine := &Engine{} - - files := []string{ - "src/main.js", - "src/app.ts", - "test/test.js", - "README.md", - } - - tests := []struct { - name string - selector *core.Selector - want []string - }{ - { - name: "nil selector - all files", - selector: nil, - want: files, - }, - { - name: "with selector - filters JS/TS only", - selector: &core.Selector{ - Languages: []string{"javascript", "typescript"}, - }, - want: []string{"src/main.js", "src/app.ts", "test/test.js"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := engine.filterFiles(files, tt.selector) - if !equalSlices(got, tt.want) { - t.Errorf("filterFiles() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestValidate_WithCustomMessage(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-CUSTOM-MSG", - Category: "formatting", - Severity: "error", - Message: "Line is too long", - Check: map[string]interface{}{ - "engine": "length", - "scope": "line", - "max": 80, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_FileScope(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-FILE-LENGTH", - Category: "formatting", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "length", - "scope": "file", - "max": 500, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_FunctionScope(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-FUNCTION-LENGTH", - Category: "formatting", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "length", - "scope": "function", - "max": 50, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_ParamsScope(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-PARAMS-LENGTH", - Category: "formatting", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "length", - "scope": "params", - "max": 4, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestInit_WithDebug(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: true, - } - - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init with debug failed (expected if ESLint not available): %v", err) - } - - if !engine.config.Debug { - t.Error("Expected debug to be true") - } -} - -// Helper functions - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} - -func equalSlices(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} diff --git a/internal/engine/llm/engine.go b/internal/engine/llm/engine.go deleted file mode 100644 index dffd1ad..0000000 --- a/internal/engine/llm/engine.go +++ /dev/null @@ -1,286 +0,0 @@ -package llm - -import ( - "context" - "fmt" - "os" - "strings" - "time" - - "github.com/DevSymphony/sym-cli/internal/engine/core" - "github.com/DevSymphony/sym-cli/internal/envutil" - "github.com/DevSymphony/sym-cli/internal/llm" -) - -// Engine validates code using LLM-based analysis. -// Unlike other engines that use static analysis tools, this engine -// uses an LLM to understand and validate code against natural language rules. -type Engine struct { - client *llm.Client - config core.EngineConfig -} - -// NewEngine creates a new LLM engine. -func NewEngine() *Engine { - return &Engine{} -} - -// Init initializes the engine. -func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { - e.config = config - - // Initialize LLM client - apiKey := envutil.GetAPIKey("ANTHROPIC_API_KEY") - if apiKey == "" { - apiKey = envutil.GetAPIKey("OPENAI_API_KEY") - } - - if apiKey == "" { - return fmt.Errorf("LLM API key not found (ANTHROPIC_API_KEY or OPENAI_API_KEY in environment or .sym/.env)") - } - - e.client = llm.NewClient(apiKey) - return nil -} - -// Validate validates files against an LLM-based rule. -func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) (*core.ValidationResult, error) { - start := time.Now() - - // Filter files by selector - files = core.FilterFiles(files, rule.When) - - if len(files) == 0 { - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: true, - Engine: "llm-validator", - Duration: time.Since(start), - }, nil - } - - violations := make([]core.Violation, 0) - - // Validate each file - for _, file := range files { - // Read file content - content, err := os.ReadFile(file) - if err != nil { - if e.config.Debug { - fmt.Printf("⚠️ Failed to read file %s: %v\n", file, err) - } - continue - } - - // Validate with LLM - fileViolations, err := e.validateFile(ctx, rule, file, string(content)) - if err != nil { - if e.config.Debug { - fmt.Printf("⚠️ Failed to validate file %s: %v\n", file, err) - } - continue - } - - violations = append(violations, fileViolations...) - } - - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: len(violations) == 0, - Violations: violations, - Duration: time.Since(start), - Engine: "llm-validator", - Metrics: &core.Metrics{ - FilesProcessed: len(files), - }, - }, nil -} - -// validateFile validates a single file using LLM -func (e *Engine) validateFile(ctx context.Context, rule core.Rule, filePath string, content string) ([]core.Violation, error) { - // Build prompt for LLM - systemPrompt := `You are a code reviewer. Check if the code violates the given coding convention. - -Respond with JSON only: -{ - "violates": true/false, - "description": "explanation of violation if any", - "suggestion": "how to fix it if violated", - "line": line_number_if_applicable (0 if not applicable) -}` - - userPrompt := fmt.Sprintf(`File: %s - -Coding Convention: -%s - -Code: -%s - -Does this code violate the convention?`, filePath, rule.Desc, content) - - // Call LLM - response, err := e.client.Complete(ctx, systemPrompt, userPrompt) - if err != nil { - return nil, err - } - - // Parse response - result := parseValidationResponse(response) - if !result.Violates { - return []core.Violation{}, nil - } - - message := result.Description - if result.Suggestion != "" { - message += fmt.Sprintf(" | Suggestion: %s", result.Suggestion) - } - - // Use custom message if provided in rule - if rule.Message != "" { - message = rule.Message + " | " + message - } - - violation := core.Violation{ - RuleID: rule.ID, - Severity: rule.Severity, - Message: message, - File: filePath, - Line: result.Line, - Category: rule.Category, - } - - return []core.Violation{violation}, nil -} - -// GetCapabilities returns engine capabilities. -func (e *Engine) GetCapabilities() core.EngineCapabilities { - return core.EngineCapabilities{ - Name: "llm-validator", - // LLM is language-agnostic - can understand any programming language - SupportedLanguages: []string{ - "javascript", "typescript", "jsx", "tsx", - "python", "go", "java", "rust", "c", "cpp", - "ruby", "php", "swift", "kotlin", "scala", - }, - SupportedCategories: []string{ - "convention", "style", "best-practice", - "security", "performance", "maintainability", - }, - SupportsAutofix: false, // Future enhancement - RequiresCompilation: false, - ExternalTools: []core.ToolRequirement{}, // No external tools needed - } -} - -// Close cleans up resources. -func (e *Engine) Close() error { - return nil -} - -// validationResponse represents the parsed LLM response -type validationResponse struct { - Violates bool - Description string - Suggestion string - Line int -} - -// parseValidationResponse parses the LLM response -func parseValidationResponse(response string) validationResponse { - // Default to no violation - result := validationResponse{ - Violates: false, - Description: "", - Suggestion: "", - Line: 0, - } - - lower := strings.ToLower(response) - - // Check if no violation - if strings.Contains(lower, `"violates": false`) || - strings.Contains(lower, `"violates":false`) || - strings.Contains(lower, "does not violate") { - return result - } - - // Check if violates - if strings.Contains(lower, `"violates": true`) || - strings.Contains(lower, `"violates":true`) { - result.Violates = true - - // Extract description - if desc := extractJSONField(response, "description"); desc != "" { - result.Description = desc - } else { - result.Description = "Rule violation detected" - } - - // Extract suggestion - if sugg := extractJSONField(response, "suggestion"); sugg != "" { - result.Suggestion = sugg - } - - // Extract line number - if lineStr := extractJSONField(response, "line"); lineStr != "" { - // Parse line number - var line int - if _, err := fmt.Sscanf(lineStr, "%d", &line); err == nil { - result.Line = line - } - } - } - - return result -} - -// extractJSONField extracts a field value from JSON response -func extractJSONField(response, field string) string { - // Look for "field": "value" - key := fmt.Sprintf(`"%s"`, field) - idx := strings.Index(response, key) - if idx == -1 { - return "" - } - - // Find : after field name - colonIdx := strings.Index(response[idx:], ":") + idx - if colonIdx <= idx { - return "" - } - - // Find opening quote or number - start := colonIdx + 1 - for start < len(response) && (response[start] == ' ' || response[start] == '\t' || response[start] == '\n') { - start++ - } - - if start >= len(response) { - return "" - } - - // Handle string value - if response[start] == '"' { - openIdx := start - closeIdx := openIdx + 1 - for closeIdx < len(response) { - if response[closeIdx] == '"' && (closeIdx == openIdx+1 || response[closeIdx-1] != '\\') { - return response[openIdx+1 : closeIdx] - } - closeIdx++ - } - return "" - } - - // Handle numeric value - end := start - for end < len(response) && response[end] >= '0' && response[end] <= '9' { - end++ - } - if end > start { - return response[start:end] - } - - return "" -} diff --git a/internal/engine/pattern/engine.go b/internal/engine/pattern/engine.go deleted file mode 100644 index dbfbdf1..0000000 --- a/internal/engine/pattern/engine.go +++ /dev/null @@ -1,201 +0,0 @@ -package pattern - -import ( - "context" - "fmt" - "path/filepath" - "strings" - "time" - - "github.com/DevSymphony/sym-cli/internal/adapter" - adapterRegistry "github.com/DevSymphony/sym-cli/internal/adapter/registry" - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// Engine validates pattern rules (naming, forbidden patterns, imports). -// -// Supports multiple languages through adapter registry: -// - JavaScript/TypeScript: ESLint (id-match, no-restricted-syntax, no-restricted-imports) -// - Java: Checkstyle (naming conventions, forbidden imports) -type Engine struct { - adapterRegistry *adapterRegistry.Registry - config core.EngineConfig -} - -// NewEngine creates a new pattern engine. -func NewEngine() *Engine { - return &Engine{} -} - -// Init initializes the engine with adapter registry. -func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { - e.config = config - - // Use provided adapter registry or create default - if config.AdapterRegistry != nil { - if reg, ok := config.AdapterRegistry.(*adapterRegistry.Registry); ok { - e.adapterRegistry = reg - } else { - return fmt.Errorf("invalid adapter registry type") - } - } else { - e.adapterRegistry = adapterRegistry.DefaultRegistry() - } - - return nil -} - -// Validate validates files against a pattern rule. -func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) (*core.ValidationResult, error) { - start := time.Now() - - // Filter files by selector - files = e.filterFiles(files, rule.When) - - if len(files) == 0 { - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: true, - Engine: "pattern", - Duration: time.Since(start), - }, nil - } - - // Check initialization - if e.adapterRegistry == nil { - return nil, fmt.Errorf("pattern engine not initialized") - } - - // Detect language - language := e.detectLanguage(rule, files) - - // Get appropriate adapter for language - adp, err := e.adapterRegistry.GetAdapter(language, "pattern") - if err != nil { - return nil, fmt.Errorf("no adapter found for language %s: %w", language, err) - } - - // Type assert to adapter.Adapter - patternAdapter, ok := adp.(adapter.Adapter) - if !ok { - return nil, fmt.Errorf("invalid adapter type for language %s", language) - } - - // Check if adapter is available, install if needed - if err := patternAdapter.CheckAvailability(ctx); err != nil { - if installErr := patternAdapter.Install(ctx, adapter.InstallConfig{}); installErr != nil { - return nil, fmt.Errorf("adapter not available and installation failed: %w", installErr) - } - } - - // Generate config - config, err := patternAdapter.GenerateConfig(&rule) - if err != nil { - return nil, fmt.Errorf("failed to generate config: %w", err) - } - - // Execute adapter - output, err := patternAdapter.Execute(ctx, config, files) - if err != nil && output == nil { - return nil, fmt.Errorf("failed to execute adapter: %w", err) - } - - // Parse output - adapterViolations, err := patternAdapter.ParseOutput(output) - if err != nil { - return nil, fmt.Errorf("failed to parse output: %w", err) - } - - // Convert to core violations - violations := make([]core.Violation, len(adapterViolations)) - for i, av := range adapterViolations { - violations[i] = core.Violation{ - File: av.File, - Line: av.Line, - Column: av.Column, - Message: av.Message, - Severity: av.Severity, - RuleID: rule.ID, - Category: rule.Category, - } - - // Use custom message if provided - if rule.Message != "" { - violations[i].Message = rule.Message - } - } - - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: len(violations) == 0, - Violations: violations, - Duration: time.Since(start), - Engine: "pattern", - Language: language, - }, nil -} - -// GetCapabilities returns engine capabilities. -// Supported languages are determined dynamically based on registered adapters. -func (e *Engine) GetCapabilities() core.EngineCapabilities { - caps := core.EngineCapabilities{ - Name: "pattern", - SupportedCategories: []string{"naming", "security", "custom"}, - SupportsAutofix: false, - RequiresCompilation: false, - } - - // If registry is available, get languages dynamically - if e.adapterRegistry != nil { - caps.SupportedLanguages = e.adapterRegistry.GetSupportedLanguages("pattern") - } else { - // Fallback to default JS/TS - caps.SupportedLanguages = []string{"javascript", "typescript", "jsx", "tsx"} - } - - return caps -} - -// Close cleans up resources. -func (e *Engine) Close() error { - return nil -} - -// filterFiles filters files based on selector using proper glob matching. -func (e *Engine) filterFiles(files []string, selector *core.Selector) []string { - return core.FilterFiles(files, selector) -} - -// detectLanguage detects the primary language from files and rule configuration. -func (e *Engine) detectLanguage(rule core.Rule, files []string) string { - // 1. Check rule.When.Languages if specified - if rule.When != nil && len(rule.When.Languages) > 0 { - return rule.When.Languages[0] - } - - // 2. Detect from first file extension - if len(files) > 0 { - ext := strings.ToLower(filepath.Ext(files[0])) - switch ext { - case ".js": - return "javascript" - case ".ts": - return "typescript" - case ".jsx": - return "jsx" - case ".tsx": - return "tsx" - case ".java": - return "java" - case ".py": - return "python" - case ".go": - return "go" - case ".rs": - return "rust" - } - } - - // 3. Default to JavaScript - return "javascript" -} diff --git a/internal/engine/pattern/engine_test.go b/internal/engine/pattern/engine_test.go deleted file mode 100644 index ca9197d..0000000 --- a/internal/engine/pattern/engine_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package pattern - -import ( - "context" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestNewEngine(t *testing.T) { - engine := NewEngine() - if engine == nil { - t.Fatal("NewEngine() returned nil") - } -} - -func TestGetCapabilities(t *testing.T) { - engine := NewEngine() - caps := engine.GetCapabilities() - - if caps.Name != "pattern" { - t.Errorf("Name = %s, want pattern", caps.Name) - } - - if !contains(caps.SupportedLanguages, "javascript") { - t.Error("Expected javascript in supported languages") - } - - if !contains(caps.SupportedCategories, "naming") { - t.Error("Expected naming in supported categories") - } - - if caps.SupportsAutofix { - t.Error("Pattern engine should not support autofix") - } -} - -func TestInit(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: false, - } - - // Init might fail if eslint is not available, which is okay for unit tests - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init failed (expected if ESLint not available): %v", err) - } -} - -func TestClose(t *testing.T) { - engine := NewEngine() - if err := engine.Close(); err != nil { - t.Errorf("Close() error = %v", err) - } -} - -func TestValidate_NoFiles(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "naming", - Severity: "error", - Check: map[string]interface{}{ - "engine": "pattern", - }, - } - - // Validate with empty file list - result, err := engine.Validate(ctx, rule, []string{}) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Error("Expected validation to pass for empty file list") - } - - if len(result.Violations) != 0 { - t.Errorf("Expected 0 violations, got %d", len(result.Violations)) - } -} - -func TestValidate_NotInitialized(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "naming", - Severity: "error", - Check: map[string]interface{}{ - "engine": "pattern", - }, - } - - // Validate without initialization - _, err := engine.Validate(ctx, rule, []string{"test.js"}) - if err == nil { - t.Error("Expected error for uninitialized engine") - } -} - -// TestMatchesLanguage has been moved to core package tests. -// Language matching logic is now centralized in core.MatchesLanguage. - -func TestFilterFiles(t *testing.T) { - engine := &Engine{} - - files := []string{ - "src/main.js", - "src/app.ts", - "test/test.js", - "README.md", - "src/styles.css", - } - - tests := []struct { - name string - selector *core.Selector - want []string - }{ - { - name: "nil selector - all files", - selector: nil, - want: files, - }, - { - name: "javascript only", - selector: &core.Selector{ - Languages: []string{"javascript"}, - }, - want: []string{"src/main.js", "test/test.js"}, - }, - { - name: "typescript only", - selector: &core.Selector{ - Languages: []string{"typescript"}, - }, - want: []string{"src/app.ts"}, - }, - { - name: "multiple languages", - selector: &core.Selector{ - Languages: []string{"javascript", "typescript"}, - }, - want: []string{"src/main.js", "src/app.ts", "test/test.js"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := engine.filterFiles(files, tt.selector) - if !equalSlices(got, tt.want) { - t.Errorf("filterFiles() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestDetectLanguage(t *testing.T) { - engine := &Engine{} - - tests := []struct { - rule core.Rule - files []string - want string - }{ - {core.Rule{}, []string{"main.js"}, "javascript"}, - {core.Rule{}, []string{"app.jsx"}, "jsx"}, - {core.Rule{}, []string{"server.ts"}, "typescript"}, - {core.Rule{}, []string{"component.tsx"}, "tsx"}, - {core.Rule{}, []string{}, "javascript"}, - {core.Rule{When: &core.Selector{Languages: []string{"python"}}}, []string{"main.js"}, "python"}, - } - - for _, tt := range tests { - got := engine.detectLanguage(tt.rule, tt.files) - if got != tt.want { - t.Errorf("detectLanguage(%v, %v) = %q, want %q", tt.rule, tt.files, got, tt.want) - } - } -} - -func TestValidate_WithCustomMessage(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-CUSTOM-MSG", - Category: "naming", - Severity: "error", - Message: "Custom violation message", - Check: map[string]interface{}{ - "engine": "pattern", - "target": "identifier", - "pattern": "^[A-Z][a-zA-Z0-9]*$", - }, - } - - // Create a test file with a violation - testFile := t.TempDir() + "/test.js" - // Since we can't guarantee ESLint will find violations in a real file, - // we'll just test that the function handles the rule correctly - result, err := engine.Validate(ctx, rule, []string{testFile}) - - // Should not error even if file doesn't exist or has no violations - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - // If result is returned, check basic properties - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_WithFilteredFiles(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-FILTERED", - Category: "naming", - Severity: "error", - When: &core.Selector{ - Languages: []string{"typescript"}, - }, - Check: map[string]interface{}{ - "engine": "pattern", - "target": "identifier", - "pattern": "^[A-Z]", - }, - } - - // Provide JS and TS files - only TS should be validated - files := []string{"test.js", "test.ts"} - - result, err := engine.Validate(ctx, rule, files) - - // Should not error - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestInit_WithDebug(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: true, - } - - // Init might fail if eslint is not available - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init with debug failed (expected if ESLint not available): %v", err) - } - - // Check that config was set - if !engine.config.Debug { - t.Error("Expected debug to be true") - } -} - -// Helper functions - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} - -func equalSlices(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} diff --git a/internal/engine/registry/builtin.go b/internal/engine/registry/builtin.go deleted file mode 100644 index 820af33..0000000 --- a/internal/engine/registry/builtin.go +++ /dev/null @@ -1,44 +0,0 @@ -package registry - -import ( - "github.com/DevSymphony/sym-cli/internal/engine/ast" - "github.com/DevSymphony/sym-cli/internal/engine/core" - "github.com/DevSymphony/sym-cli/internal/engine/length" - "github.com/DevSymphony/sym-cli/internal/engine/llm" - "github.com/DevSymphony/sym-cli/internal/engine/pattern" - "github.com/DevSymphony/sym-cli/internal/engine/style" - "github.com/DevSymphony/sym-cli/internal/engine/typechecker" -) - -// init registers all built-in engines. -func init() { - // Register pattern engine - MustRegister("pattern", func() (core.Engine, error) { - return pattern.NewEngine(), nil - }) - - // Register length engine - MustRegister("length", func() (core.Engine, error) { - return length.NewEngine(), nil - }) - - // Register style engine - MustRegister("style", func() (core.Engine, error) { - return style.NewEngine(), nil - }) - - // Register AST engine - MustRegister("ast", func() (core.Engine, error) { - return ast.NewEngine(), nil - }) - - // Register type checker engine - MustRegister("typechecker", func() (core.Engine, error) { - return typechecker.NewEngine(), nil - }) - - // Register LLM validator engine - MustRegister("llm-validator", func() (core.Engine, error) { - return llm.NewEngine(), nil - }) -} diff --git a/internal/engine/registry/registry.go b/internal/engine/registry/registry.go deleted file mode 100644 index bc08ebc..0000000 --- a/internal/engine/registry/registry.go +++ /dev/null @@ -1,99 +0,0 @@ -package registry - -import ( - "fmt" - "sync" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// Registry manages available engines. -// Thread-safe for concurrent access. -type Registry struct { - mu sync.RWMutex - engines map[string]core.Engine - factories map[string]EngineFactory -} - -// EngineFactory creates engine instances. -type EngineFactory func() (core.Engine, error) - -var globalRegistry = &Registry{ - engines: make(map[string]core.Engine), - factories: make(map[string]EngineFactory), -} - -// Global returns the global engine registry. -func Global() *Registry { - return globalRegistry -} - -// Register registers an engine factory. -// The factory will be called lazily when Get() is first called. -func (r *Registry) Register(name string, factory EngineFactory) error { - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.factories[name]; exists { - return fmt.Errorf("engine %q already registered", name) - } - - r.factories[name] = factory - return nil -} - -// Get retrieves an engine by name. -// Creates the engine on first access (lazy initialization). -func (r *Registry) Get(name string) (core.Engine, error) { - // Fast path: check if already created - r.mu.RLock() - if engine, ok := r.engines[name]; ok { - r.mu.RUnlock() - return engine, nil - } - r.mu.RUnlock() - - // Slow path: create engine - r.mu.Lock() - defer r.mu.Unlock() - - // Double-check after acquiring write lock - if engine, ok := r.engines[name]; ok { - return engine, nil - } - - // Look up factory - factory, ok := r.factories[name] - if !ok { - return nil, fmt.Errorf("engine %q not registered", name) - } - - // Create engine - engine, err := factory() - if err != nil { - return nil, fmt.Errorf("failed to create engine %q: %w", name, err) - } - - r.engines[name] = engine - return engine, nil -} - -// List returns all registered engine names. -func (r *Registry) List() []string { - r.mu.RLock() - defer r.mu.RUnlock() - - names := make([]string, 0, len(r.factories)) - for name := range r.factories { - names = append(names, name) - } - return names -} - -// MustRegister registers an engine factory and panics on error. -// Useful for init() functions. -func MustRegister(name string, factory EngineFactory) { - if err := Global().Register(name, factory); err != nil { - panic(err) - } -} diff --git a/internal/engine/registry/registry_test.go b/internal/engine/registry/registry_test.go deleted file mode 100644 index b9b1ae6..0000000 --- a/internal/engine/registry/registry_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package registry - -import ( - "context" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// mockEngine is a mock implementation for testing -type mockEngine struct { - name string -} - -func (m *mockEngine) Init(ctx context.Context, config core.EngineConfig) error { - return nil -} - -func (m *mockEngine) Validate(ctx context.Context, rule core.Rule, files []string) (*core.ValidationResult, error) { - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: true, - Engine: m.name, - }, nil -} - -func (m *mockEngine) GetCapabilities() core.EngineCapabilities { - return core.EngineCapabilities{ - Name: m.name, - } -} - -func (m *mockEngine) Close() error { - return nil -} - -func TestRegistry_RegisterAndGet(t *testing.T) { - r := &Registry{ - engines: make(map[string]core.Engine), - factories: make(map[string]EngineFactory), - } - - // Register factory - err := r.Register("test", func() (core.Engine, error) { - return &mockEngine{name: "test"}, nil - }) - if err != nil { - t.Fatalf("Register failed: %v", err) - } - - // Get engine (should create it) - engine, err := r.Get("test") - if err != nil { - t.Fatalf("Get failed: %v", err) - } - - if engine == nil { - t.Fatal("Get returned nil engine") - } - - // Get again (should return same instance) - engine2, err := r.Get("test") - if err != nil { - t.Fatalf("Get (2nd) failed: %v", err) - } - - if engine != engine2 { - t.Error("Get returned different instances (should be same)") - } -} - -func TestRegistry_RegisterDuplicate(t *testing.T) { - r := &Registry{ - engines: make(map[string]core.Engine), - factories: make(map[string]EngineFactory), - } - - factory := func() (core.Engine, error) { - return &mockEngine{name: "test"}, nil - } - - // First registration should succeed - if err := r.Register("test", factory); err != nil { - t.Fatalf("First Register failed: %v", err) - } - - // Second registration should fail - err := r.Register("test", factory) - if err == nil { - t.Error("Register duplicate succeeded, want error") - } -} - -func TestRegistry_GetNonExistent(t *testing.T) { - r := &Registry{ - engines: make(map[string]core.Engine), - factories: make(map[string]EngineFactory), - } - - _, err := r.Get("nonexistent") - if err == nil { - t.Error("Get(nonexistent) succeeded, want error") - } -} - -func TestRegistry_List(t *testing.T) { - r := &Registry{ - engines: make(map[string]core.Engine), - factories: make(map[string]EngineFactory), - } - - names := []string{"pattern", "length", "style"} - for _, name := range names { - n := name // capture - if err := r.Register(n, func() (core.Engine, error) { - return &mockEngine{name: n}, nil - }); err != nil { - t.Fatalf("Register(%s) failed: %v", n, err) - } - } - - got := r.List() - if len(got) != len(names) { - t.Errorf("List() length = %d, want %d", len(got), len(names)) - } -} diff --git a/internal/engine/style/engine.go b/internal/engine/style/engine.go deleted file mode 100644 index 3153322..0000000 --- a/internal/engine/style/engine.go +++ /dev/null @@ -1,200 +0,0 @@ -package style - -import ( - "context" - "fmt" - "path/filepath" - "strings" - "time" - - "github.com/DevSymphony/sym-cli/internal/adapter" - adapterRegistry "github.com/DevSymphony/sym-cli/internal/adapter/registry" - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// Engine validates code style rules (indent, quotes, semicolons, etc.). -// -// Supports multiple languages through adapter registry: -// - JavaScript/TypeScript: ESLint/Prettier (indent, quotes, semi rules) -// - Java: Checkstyle (Indentation, WhitespaceAround) -// -// Note: Autofix is not supported by design (read-only validation) -type Engine struct { - adapterRegistry *adapterRegistry.Registry - config core.EngineConfig -} - -// NewEngine creates a new style engine. -func NewEngine() *Engine { - return &Engine{} -} - -// Init initializes the engine with adapter registry. -func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { - e.config = config - - // Use provided adapter registry or create default - if config.AdapterRegistry != nil { - if reg, ok := config.AdapterRegistry.(*adapterRegistry.Registry); ok { - e.adapterRegistry = reg - } else { - return fmt.Errorf("invalid adapter registry type") - } - } else { - e.adapterRegistry = adapterRegistry.DefaultRegistry() - } - - return nil -} - -// Validate validates files against a style rule. -func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) (*core.ValidationResult, error) { - start := time.Now() - - files = e.filterFiles(files, rule.When) - if len(files) == 0 { - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: true, - Engine: "style", - Duration: time.Since(start), - }, nil - } - - // Check initialization - if e.adapterRegistry == nil { - return nil, fmt.Errorf("style engine not initialized") - } - - // Detect language - language := e.detectLanguage(rule, files) - - // Get appropriate adapter for language - adp, err := e.adapterRegistry.GetAdapter(language, "style") - if err != nil { - return nil, fmt.Errorf("no adapter found for language %s: %w", language, err) - } - - // Type assert to adapter.Adapter - styleAdapter, ok := adp.(adapter.Adapter) - if !ok { - return nil, fmt.Errorf("invalid adapter type for language %s", language) - } - - // Check if adapter is available, install if needed - if err := styleAdapter.CheckAvailability(ctx); err != nil { - if installErr := styleAdapter.Install(ctx, adapter.InstallConfig{}); installErr != nil { - return nil, fmt.Errorf("adapter not available and installation failed: %w", installErr) - } - } - - // Generate config for validation - config, err := styleAdapter.GenerateConfig(&rule) - if err != nil { - return nil, fmt.Errorf("failed to generate config: %w", err) - } - - // Execute adapter - output, err := styleAdapter.Execute(ctx, config, files) - if err != nil && output == nil { - return nil, fmt.Errorf("failed to execute adapter: %w", err) - } - - // Parse violations - adapterViolations, err := styleAdapter.ParseOutput(output) - if err != nil { - return nil, fmt.Errorf("failed to parse output: %w", err) - } - - // Convert to core violations - violations := make([]core.Violation, len(adapterViolations)) - for i, av := range adapterViolations { - violations[i] = core.Violation{ - File: av.File, - Line: av.Line, - Column: av.Column, - Message: av.Message, - Severity: av.Severity, - RuleID: rule.ID, - Category: rule.Category, - } - - if rule.Message != "" { - violations[i].Message = rule.Message - } - } - - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: len(violations) == 0, - Violations: violations, - Duration: time.Since(start), - Engine: "style", - Language: language, - }, nil -} - -// GetCapabilities returns engine capabilities. -// Supported languages are determined dynamically based on registered adapters. -func (e *Engine) GetCapabilities() core.EngineCapabilities { - caps := core.EngineCapabilities{ - Name: "style", - SupportedCategories: []string{"style", "formatting"}, - SupportsAutofix: false, // Autofix removed by design - RequiresCompilation: false, - } - - // If registry is available, get languages dynamically - if e.adapterRegistry != nil { - caps.SupportedLanguages = e.adapterRegistry.GetSupportedLanguages("style") - } else { - // Fallback to default JS/TS - caps.SupportedLanguages = []string{"javascript", "typescript", "jsx", "tsx"} - } - - return caps -} - -// Close cleans up resources. -func (e *Engine) Close() error { - return nil -} - -// filterFiles filters files based on selector using proper glob matching. -func (e *Engine) filterFiles(files []string, selector *core.Selector) []string { - return core.FilterFiles(files, selector) -} - -// detectLanguage detects the primary language from files and rule configuration. -func (e *Engine) detectLanguage(rule core.Rule, files []string) string { - // 1. Check rule.When.Languages if specified - if rule.When != nil && len(rule.When.Languages) > 0 { - return rule.When.Languages[0] - } - - // 2. Detect from first file extension - if len(files) > 0 { - ext := strings.ToLower(filepath.Ext(files[0])) - switch ext { - case ".js": - return "javascript" - case ".ts": - return "typescript" - case ".jsx": - return "jsx" - case ".tsx": - return "tsx" - case ".java": - return "java" - case ".py": - return "python" - case ".go": - return "go" - case ".rs": - return "rust" - } - } - - // 3. Default to JavaScript - return "javascript" -} diff --git a/internal/engine/style/engine_test.go b/internal/engine/style/engine_test.go deleted file mode 100644 index be76003..0000000 --- a/internal/engine/style/engine_test.go +++ /dev/null @@ -1,430 +0,0 @@ -package style - -import ( - "context" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestNewEngine(t *testing.T) { - engine := NewEngine() - if engine == nil { - t.Fatal("NewEngine() returned nil") - } -} - -func TestGetCapabilities(t *testing.T) { - engine := NewEngine() - caps := engine.GetCapabilities() - - if caps.Name != "style" { - t.Errorf("Name = %s, want style", caps.Name) - } - - if !contains(caps.SupportedLanguages, "javascript") { - t.Error("Expected javascript in supported languages") - } - - if !contains(caps.SupportedCategories, "formatting") { - t.Error("Expected formatting in supported categories") - } - - if caps.SupportsAutofix { - t.Error("Style engine should not support autofix (removed by design)") - } -} - -func TestInit(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: false, - } - - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init failed (expected if ESLint not available): %v", err) - } -} - -func TestClose(t *testing.T) { - engine := NewEngine() - if err := engine.Close(); err != nil { - t.Errorf("Close() error = %v", err) - } -} - -func TestValidate_NoFiles(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "style", - Severity: "error", - Check: map[string]interface{}{ - "engine": "style", - }, - } - - result, err := engine.Validate(ctx, rule, []string{}) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Error("Expected validation to pass for empty file list") - } -} - -func TestValidate_WithInitialization(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "style", - Severity: "error", - Check: map[string]interface{}{ - "engine": "style", - "indent": 2, - "quote": "single", - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_IndentRule(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-INDENT", - Category: "style", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "style", - "indent": 4, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_QuoteRule(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-QUOTE", - Category: "style", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "style", - "quote": "double", - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_SemiRule(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-SEMI", - Category: "style", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "style", - "semi": true, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_WithCustomMessage(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-CUSTOM", - Category: "style", - Severity: "warning", - Message: "Custom style violation", - Check: map[string]interface{}{ - "engine": "style", - "indent": 2, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestInit_WithDebug(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: true, - } - - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init with debug failed (expected if ESLint not available): %v", err) - } - - if !engine.config.Debug { - t.Error("Expected debug to be true") - } -} - -func TestValidate_BraceStyleRule(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-BRACE", - Category: "style", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "style", - "brace_style": "1tbs", - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_MultipleRules(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - ESLint not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-MULTI", - Category: "style", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "style", - "indent": 2, - "quote": "single", - "semi": true, - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - if result.Engine != "style" { - t.Errorf("Engine = %s, want style", result.Engine) - } - } -} - -func TestFilterFiles(t *testing.T) { - engine := &Engine{} - - files := []string{ - "src/main.js", - "src/app.ts", - "test/test.js", - "README.md", - } - - tests := []struct { - name string - selector *core.Selector - want []string - }{ - { - name: "nil selector - all files", - selector: nil, - want: files, - }, - { - name: "javascript and typescript files", - selector: &core.Selector{ - Languages: []string{"javascript", "typescript"}, - }, - want: []string{"src/main.js", "src/app.ts", "test/test.js"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := engine.filterFiles(files, tt.selector) - if !equalSlices(got, tt.want) { - t.Errorf("filterFiles() = %v, want %v", got, tt.want) - } - }) - } -} - -// Helper functions - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} - -func equalSlices(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} diff --git a/internal/engine/typechecker/engine.go b/internal/engine/typechecker/engine.go deleted file mode 100644 index 3583d05..0000000 --- a/internal/engine/typechecker/engine.go +++ /dev/null @@ -1,162 +0,0 @@ -package typechecker - -import ( - "context" - "fmt" - "time" - - "github.com/DevSymphony/sym-cli/internal/adapter" - "github.com/DevSymphony/sym-cli/internal/adapter/tsc" - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// Engine validates TypeScript/JavaScript type correctness using tsc. -// -// Strategy: -// - Uses TypeScript Compiler (tsc) for type checking -// - Supports strict mode and various compiler options -// - Works with TypeScript (.ts, .tsx) and optionally JavaScript (.js, .jsx) -type Engine struct { - tsc *tsc.Adapter - config core.EngineConfig -} - -// NewEngine creates a new type checker engine. -func NewEngine() *Engine { - return &Engine{} -} - -// Init initializes the engine. -func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { - e.config = config - - // Initialize tsc adapter - e.tsc = tsc.NewAdapter(config.ToolsDir, config.WorkDir) - - // Check tsc availability - if err := e.tsc.CheckAvailability(ctx); err != nil { - if config.Debug { - fmt.Printf("TSC not found, attempting install...\n") - } - - installConfig := adapter.InstallConfig{ - ToolsDir: config.ToolsDir, - } - - if err := e.tsc.Install(ctx, installConfig); err != nil { - return fmt.Errorf("failed to install TypeScript: %w", err) - } - } - - return nil -} - -// Validate validates files against type checking rules. -func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) (*core.ValidationResult, error) { - start := time.Now() - - files = core.FilterFiles(files, rule.When) - if len(files) == 0 { - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: true, - Engine: "typechecker", - Duration: time.Since(start), - }, nil - } - - // Generate tsc config - tscConfig, err := e.tsc.GenerateConfig(&rule) - if err != nil { - return nil, fmt.Errorf("failed to generate tsc config: %w", err) - } - - // Execute tsc - output, err := e.tsc.Execute(ctx, tscConfig, files) - if err != nil { - return nil, fmt.Errorf("failed to execute tsc: %w", err) - } - - // Parse violations - adapterViolations, err := e.tsc.ParseOutput(output) - if err != nil { - return nil, fmt.Errorf("failed to parse tsc output: %w", err) - } - - // Convert to core violations - violations := make([]core.Violation, len(adapterViolations)) - for i, av := range adapterViolations { - violations[i] = core.Violation{ - File: av.File, - Line: av.Line, - Column: av.Column, - Message: av.Message, - Severity: av.Severity, - RuleID: av.RuleID, - Category: rule.Category, - } - - // Use custom message if provided - if rule.Message != "" { - violations[i].Message = rule.Message - } - } - - return &core.ValidationResult{ - RuleID: rule.ID, - Passed: len(violations) == 0, - Violations: violations, - Duration: time.Since(start), - Engine: "typechecker", - Language: e.detectLanguage(files), - }, nil -} - -// GetCapabilities returns engine capabilities. -func (e *Engine) GetCapabilities() core.EngineCapabilities { - return core.EngineCapabilities{ - Name: "typechecker", - SupportedLanguages: []string{"typescript", "javascript", "tsx", "jsx"}, - SupportedCategories: []string{"type_safety", "correctness", "custom"}, - SupportsAutofix: false, - RequiresCompilation: false, - ExternalTools: []core.ToolRequirement{ - { - Name: "typescript", - Version: "^5.0.0", - Optional: false, - InstallCommand: "npm install -g typescript", - }, - }, - } -} - -// Close cleans up resources. -func (e *Engine) Close() error { - return nil -} - -// detectLanguage detects the language from file extensions. -func (e *Engine) detectLanguage(files []string) string { - if len(files) == 0 { - return "typescript" - } - - // Check first file - file := files[0] - if len(file) > 3 { - ext := file[len(file)-3:] - switch ext { - case ".ts": - return "typescript" - case ".js": - return "javascript" - case "jsx": - return "jsx" - case "tsx": - return "tsx" - } - } - - return "typescript" -} diff --git a/internal/engine/typechecker/engine_test.go b/internal/engine/typechecker/engine_test.go deleted file mode 100644 index ac57622..0000000 --- a/internal/engine/typechecker/engine_test.go +++ /dev/null @@ -1,374 +0,0 @@ -package typechecker - -import ( - "context" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestNewEngine(t *testing.T) { - engine := NewEngine() - if engine == nil { - t.Fatal("NewEngine() returned nil") - } -} - -func TestGetCapabilities(t *testing.T) { - engine := NewEngine() - caps := engine.GetCapabilities() - - if caps.Name != "typechecker" { - t.Errorf("Name = %s, want typechecker", caps.Name) - } - - if !contains(caps.SupportedLanguages, "typescript") { - t.Error("Expected typescript in supported languages") - } - - if !contains(caps.SupportedLanguages, "javascript") { - t.Error("Expected javascript in supported languages") - } - - if !contains(caps.SupportedCategories, "type_safety") { - t.Error("Expected type_safety in supported categories") - } - - if caps.SupportsAutofix { - t.Error("Type checker engine should not support autofix") - } - - // Verify external tools - if len(caps.ExternalTools) == 0 { - t.Error("Expected external tools to be listed") - } - - foundTSC := false - for _, tool := range caps.ExternalTools { - if tool.Name == "typescript" { - foundTSC = true - if tool.Optional { - t.Error("TypeScript should not be optional") - } - } - } - - if !foundTSC { - t.Error("Expected typescript in external tools") - } -} - -func TestDetectLanguage(t *testing.T) { - engine := &Engine{} - - tests := []struct { - name string - files []string - want string - }{ - { - name: "typescript file", - files: []string{"src/main.ts"}, - want: "typescript", - }, - { - name: "javascript file", - files: []string{"src/main.js"}, - want: "javascript", - }, - { - name: "jsx file", - files: []string{"src/Component.jsx"}, - want: "jsx", - }, - { - name: "tsx file", - files: []string{"src/Component.tsx"}, - want: "tsx", - }, - { - name: "empty files", - files: []string{}, - want: "typescript", // default - }, - { - name: "multiple files - first determines language", - files: []string{"src/main.js", "src/app.ts"}, - want: "javascript", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := engine.detectLanguage(tt.files) - if got != tt.want { - t.Errorf("detectLanguage() = %q, want %q", got, tt.want) - } - }) - } -} - -func TestClose(t *testing.T) { - engine := NewEngine() - if err := engine.Close(); err != nil { - t.Errorf("Close() error = %v, want nil", err) - } -} - -func TestInit(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: false, - } - - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init failed (expected if TSC not available): %v", err) - } -} - -func TestInit_WithDebug(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - Debug: true, - } - - err := engine.Init(ctx, config) - if err != nil { - t.Logf("Init with debug failed (expected if TSC not available): %v", err) - } - - if !engine.config.Debug { - t.Error("Expected debug to be true") - } -} - -func TestValidate_NoFiles(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "type_safety", - Severity: "error", - Check: map[string]interface{}{ - "engine": "typechecker", - }, - } - - result, err := engine.Validate(ctx, rule, []string{}) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Error("Expected validation to pass for empty file list") - } -} - -func TestValidate_WithInitialization(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - TSC not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-RULE", - Category: "type_safety", - Severity: "error", - Check: map[string]interface{}{ - "engine": "typechecker", - "strict": true, - }, - } - - testFile := t.TempDir() + "/test.ts" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_WithStrictMode(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - TSC not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-STRICT", - Category: "type_safety", - Severity: "error", - Check: map[string]interface{}{ - "engine": "typechecker", - "strict": true, - }, - } - - testFile := t.TempDir() + "/test.ts" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_WithCustomMessage(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - TSC not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-CUSTOM", - Category: "type_safety", - Severity: "error", - Message: "Custom type error", - Check: map[string]interface{}{ - "engine": "typechecker", - }, - } - - testFile := t.TempDir() + "/test.ts" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -func TestValidate_JavaScriptFile(t *testing.T) { - engine := NewEngine() - ctx := context.Background() - - config := core.EngineConfig{ - ToolsDir: t.TempDir(), - WorkDir: t.TempDir(), - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test - TSC not available: %v", err) - } - - rule := core.Rule{ - ID: "TEST-JS", - Category: "type_safety", - Severity: "error", - Check: map[string]interface{}{ - "engine": "typechecker", - }, - } - - testFile := t.TempDir() + "/test.js" - result, err := engine.Validate(ctx, rule, []string{testFile}) - - if err != nil { - t.Logf("Validate returned error (may be expected): %v", err) - } - - if result != nil { - if result.RuleID != rule.ID { - t.Errorf("RuleID = %s, want %s", result.RuleID, rule.ID) - } - } -} - -// Helper functions - -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} - -// Integration-like test (but mocked to avoid requiring tsc installation) -func TestValidate_FileFiltering(t *testing.T) { - files := []string{ - "src/main.ts", - "src/util.js", - "test/main_test.ts", - "README.md", - } - - rule := core.Rule{ - ID: "TYPE-CHECK", - Category: "type_safety", - Severity: "error", - When: &core.Selector{ - Languages: []string{"typescript"}, - Exclude: []string{"**/*_test.ts"}, - }, - Check: map[string]interface{}{ - "engine": "typechecker", - }, - } - - // Filter files using the selector (same logic as in engine) - filtered := core.FilterFiles(files, rule.When) - - // Should only include .ts files, excluding test files - expected := []string{"src/main.ts"} - - if len(filtered) != len(expected) { - t.Errorf("Filtered %d files, want %d", len(filtered), len(expected)) - t.Errorf("Got: %v", filtered) - t.Errorf("Want: %v", expected) - } - - for i, file := range filtered { - if i < len(expected) && file != expected[i] { - t.Errorf("Filtered[%d] = %q, want %q", i, file, expected[i]) - } - } -} From fe11536371847aa2687991a4149a694449ac8e86 Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 10:35:59 +0900 Subject: [PATCH 02/12] chore: remove integration test files for deprecated engines --- tests/integration/ast_integration_test.go | 115 ----------- tests/integration/length_integration_test.go | 180 ----------------- tests/integration/pattern_integration_test.go | 182 ------------------ tests/integration/style_integration_test.go | 123 ------------ .../typechecker_integration_test.go | 179 ----------------- 5 files changed, 779 deletions(-) delete mode 100644 tests/integration/ast_integration_test.go delete mode 100644 tests/integration/length_integration_test.go delete mode 100644 tests/integration/pattern_integration_test.go delete mode 100644 tests/integration/style_integration_test.go delete mode 100644 tests/integration/typechecker_integration_test.go diff --git a/tests/integration/ast_integration_test.go b/tests/integration/ast_integration_test.go deleted file mode 100644 index 2793aa5..0000000 --- a/tests/integration/ast_integration_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package integration - -import ( - "context" - "path/filepath" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/ast" - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestASTEngine_CallExpression_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := ast.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: detect console.log calls - rule := core.Rule{ - ID: "AST-NO-CONSOLE-LOG", - Category: "custom", - Severity: "warning", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "ast", - "language": "javascript", - "node": "CallExpression", - "where": map[string]interface{}{ - "callee.object.name": "console", - "callee.property.name": "log", - }, - }, - Message: "Avoid using console.log in production code", - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/ast/valid.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - t.Logf("AST validation result: passed=%v, violations=%d", result.Passed, len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } -} - -func TestASTEngine_ClassDeclaration_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := ast.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: detect class declarations - rule := core.Rule{ - ID: "AST-CLASS-EXISTS", - Category: "custom", - Severity: "info", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "ast", - "language": "javascript", - "node": "ClassDeclaration", - }, - Message: "Class declaration found", - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/ast/valid.js"), - filepath.Join(workDir, "testdata/javascript/ast/naming-violations.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - // Should find class declarations in both files - t.Logf("Found %d class declarations", len(result.Violations)) -} diff --git a/tests/integration/length_integration_test.go b/tests/integration/length_integration_test.go deleted file mode 100644 index 1f230e4..0000000 --- a/tests/integration/length_integration_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package integration - -import ( - "context" - "path/filepath" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" - "github.com/DevSymphony/sym-cli/internal/engine/length" -) - -func TestLengthEngine_LineLengthViolations_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := length.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: max line length 100 - rule := core.Rule{ - ID: "FMT-LINE-100", - Category: "formatting", - Severity: "error", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "length", - "scope": "line", - "max": 100, - }, - Message: "Line length must not exceed 100 characters", - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/length/length-violations.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if result.Passed { - t.Error("Expected validation to fail for line length violations") - } - - if len(result.Violations) == 0 { - t.Error("Expected violations to be detected") - } - - t.Logf("Found %d line length violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } -} - -func TestLengthEngine_MaxParams_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := length.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: max 4 parameters - rule := core.Rule{ - ID: "FUNC-MAX-PARAMS", - Category: "formatting", - Severity: "warning", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "length", - "scope": "params", - "max": 4, - }, - Message: "Functions should have at most 4 parameters", - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/length/length-violations.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if result.Passed { - t.Error("Expected validation to fail for too many parameters") - } - - if len(result.Violations) == 0 { - t.Error("Expected violations to be detected") - } - - t.Logf("Found %d parameter violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } -} - -func TestLengthEngine_ValidFile_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := length.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: max line length 100 - rule := core.Rule{ - ID: "FMT-LINE-100", - Category: "formatting", - Severity: "error", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "length", - "scope": "line", - "max": 100, - }, - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/length/valid.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Errorf("Expected validation to pass for valid file, got %d violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } - } -} diff --git a/tests/integration/pattern_integration_test.go b/tests/integration/pattern_integration_test.go deleted file mode 100644 index e18f0b0..0000000 --- a/tests/integration/pattern_integration_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package integration - -import ( - "context" - "path/filepath" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" - "github.com/DevSymphony/sym-cli/internal/engine/pattern" -) - -func TestPatternEngine_NamingViolations_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := pattern.NewEngine() - ctx := context.Background() - - // Initialize engine - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: class names must be PascalCase - rule := core.Rule{ - ID: "NAMING-CLASS-PASCAL", - Category: "naming", - Severity: "error", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "pattern", - "target": "identifier", - "pattern": "^[A-Z][a-zA-Z0-9]*$", - }, - Message: "Class names must be PascalCase", - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/pattern/naming-violations.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if result.Passed { - t.Error("Expected validation to fail for naming violations") - } - - if len(result.Violations) == 0 { - t.Error("Expected violations to be detected") - } - - t.Logf("Found %d violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } -} - -func TestPatternEngine_SecurityViolations_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := pattern.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: no hardcoded secrets - rule := core.Rule{ - ID: "SEC-NO-SECRETS", - Category: "security", - Severity: "error", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "pattern", - "target": "content", - "pattern": "(api[_-]?key|password|secret|token)\\s*=\\s*['\"][^'\"]+['\"]", - "flags": "i", - }, - Message: "No hardcoded secrets allowed", - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/pattern/security-violations.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if result.Passed { - t.Error("Expected validation to fail for security violations") - } - - if len(result.Violations) == 0 { - t.Error("Expected violations to be detected") - } - - t.Logf("Found %d security violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } -} - -func TestPatternEngine_ValidFile_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := pattern.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: class names must be PascalCase - rule := core.Rule{ - ID: "NAMING-CLASS-PASCAL", - Category: "naming", - Severity: "error", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "pattern", - "target": "identifier", - "pattern": "^[A-Z][a-zA-Z0-9]*$", - }, - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/pattern/valid.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Errorf("Expected validation to pass for valid file, got %d violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } - } -} diff --git a/tests/integration/style_integration_test.go b/tests/integration/style_integration_test.go deleted file mode 100644 index 2f08878..0000000 --- a/tests/integration/style_integration_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package integration - -import ( - "context" - "path/filepath" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" - "github.com/DevSymphony/sym-cli/internal/engine/style" -) - -func TestStyleEngine_IndentViolations_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := style.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: indent with 2 spaces - rule := core.Rule{ - ID: "STYLE-INDENT-2", - Category: "style", - Severity: "error", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "style", - "indent": 2, - "quote": "single", - "semi": true, - }, - Message: "Code must use 2-space indentation", - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/style/style-violations.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if result.Passed { - t.Error("Expected validation to fail for style violations") - } - - if len(result.Violations) == 0 { - t.Error("Expected violations to be detected") - } - - t.Logf("Found %d style violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } -} - -func TestStyleEngine_ValidFile_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := style.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: ESLint not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: indent with 2 spaces - rule := core.Rule{ - ID: "STYLE-INDENT-2", - Category: "style", - Severity: "error", - When: &core.Selector{ - Languages: []string{"javascript"}, - }, - Check: map[string]interface{}{ - "engine": "style", - "indent": 2, - "quote": "single", - "semi": true, - }, - } - - files := []string{ - filepath.Join(workDir, "testdata/javascript/style/valid.js"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Errorf("Expected validation to pass for valid file, got %d violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } - } -} diff --git a/tests/integration/typechecker_integration_test.go b/tests/integration/typechecker_integration_test.go deleted file mode 100644 index 994c609..0000000 --- a/tests/integration/typechecker_integration_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package integration - -import ( - "context" - "path/filepath" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" - "github.com/DevSymphony/sym-cli/internal/engine/typechecker" -) - -func TestTypeChecker_TypeErrors_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := typechecker.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: TypeScript not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: type checking with strict mode - rule := core.Rule{ - ID: "TYPE-CHECK-STRICT", - Category: "type_safety", - Severity: "error", - When: &core.Selector{ - Languages: []string{"typescript"}, - }, - Check: map[string]interface{}{ - "engine": "typechecker", - "strict": true, - }, - Message: "TypeScript type errors detected", - } - - files := []string{ - filepath.Join(workDir, "testdata/typescript/typechecker/type-errors.ts"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if result.Passed { - t.Error("Expected validation to fail for type errors") - } - - if len(result.Violations) == 0 { - t.Error("Expected type errors to be detected") - } - - t.Logf("Found %d type errors", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } -} - -func TestTypeChecker_StrictModeErrors_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := typechecker.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: TypeScript not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: strict mode violations - rule := core.Rule{ - ID: "TYPE-STRICT-MODE", - Category: "type_safety", - Severity: "error", - When: &core.Selector{ - Languages: []string{"typescript"}, - }, - Check: map[string]interface{}{ - "engine": "typechecker", - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - }, - Message: "Strict mode violations detected", - } - - files := []string{ - filepath.Join(workDir, "testdata/typescript/typechecker/strict-mode-errors.ts"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if result.Passed { - t.Error("Expected validation to fail for strict mode violations") - } - - if len(result.Violations) == 0 { - t.Error("Expected strict mode violations to be detected") - } - - t.Logf("Found %d strict mode violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } -} - -func TestTypeChecker_ValidFile_Integration(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - engine := typechecker.NewEngine() - ctx := context.Background() - - workDir := getTestdataDir(t) - config := core.EngineConfig{ - ToolsDir: getToolsDir(t), - WorkDir: workDir, - Debug: true, - } - - if err := engine.Init(ctx, config); err != nil { - t.Skipf("Skipping test: TypeScript not available: %v", err) - } - defer func() { _ = engine.Close() }() - - // Test rule: type checking - rule := core.Rule{ - ID: "TYPE-CHECK", - Category: "type_safety", - Severity: "error", - When: &core.Selector{ - Languages: []string{"typescript"}, - }, - Check: map[string]interface{}{ - "engine": "typechecker", - "strict": true, - }, - } - - files := []string{ - filepath.Join(workDir, "testdata/typescript/typechecker/valid.ts"), - } - - result, err := engine.Validate(ctx, rule, files) - if err != nil { - t.Fatalf("Validate() error = %v", err) - } - - if !result.Passed { - t.Errorf("Expected validation to pass for valid file, got %d violations", len(result.Violations)) - for _, v := range result.Violations { - t.Logf(" %s", v.String()) - } - } -} From f35cee2c6f75a289df46de8ddf069a5708c7432e Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 11:29:46 +0900 Subject: [PATCH 03/12] refactor: transition validator to use adapter-based architecture --- internal/adapter/registry/registry.go | 94 +--- internal/converter/converter.go | 29 +- internal/validator/validator.go | 696 +++++++++++++------------- 3 files changed, 410 insertions(+), 409 deletions(-) diff --git a/internal/adapter/registry/registry.go b/internal/adapter/registry/registry.go index 8d5e07b..4bb3333 100644 --- a/internal/adapter/registry/registry.go +++ b/internal/adapter/registry/registry.go @@ -6,23 +6,18 @@ import ( "github.com/DevSymphony/sym-cli/internal/adapter" ) -// Registry manages adapter instances and provides capability-based lookup. +// Registry manages adapter instances - simple tool name based lookup. type Registry struct { mu sync.RWMutex - // adapters stores all registered adapters. - adapters []adapter.Adapter - - // languageCache maps language to adapters for faster lookup. - // Key: language (e.g., "javascript"), Value: adapters that support it. - languageCache map[string][]adapter.Adapter + // adapters maps tool name to adapter (e.g., "eslint" -> ESLintAdapter) + adapters map[string]adapter.Adapter } // NewRegistry creates a new empty adapter registry. func NewRegistry() *Registry { return &Registry{ - adapters: make([]adapter.Adapter, 0), - languageCache: make(map[string][]adapter.Adapter), + adapters: make(map[string]adapter.Adapter), } } @@ -36,90 +31,47 @@ func (r *Registry) Register(adp adapter.Adapter) error { r.mu.Lock() defer r.mu.Unlock() - r.adapters = append(r.adapters, adp) - - // Update language cache - caps := adp.GetCapabilities() - for _, lang := range caps.SupportedLanguages { - r.languageCache[lang] = append(r.languageCache[lang], adp) - } + name := adp.Name() + r.adapters[name] = adp return nil } -// GetAdapter finds an adapter that supports the given language and category. -// Returns the first matching adapter, or ErrAdapterNotFound if none match. -func (r *Registry) GetAdapter(language, category string) (interface{}, error) { +// GetAdapter finds an adapter by tool name (e.g., "eslint", "prettier", "tsc"). +// Returns the adapter, or ErrAdapterNotFound if not registered. +func (r *Registry) GetAdapter(toolName string) (adapter.Adapter, error) { r.mu.RLock() defer r.mu.RUnlock() - // First, filter by language using cache - candidates, ok := r.languageCache[language] - if !ok || len(candidates) == 0 { - return nil, &ErrLanguageNotSupported{Language: language} - } - - // Then, filter by category - for _, adp := range candidates { - caps := adp.GetCapabilities() - if contains(caps.SupportedCategories, category) { - return adp, nil - } + adp, ok := r.adapters[toolName] + if !ok { + return nil, &ErrAdapterNotFound{ToolName: toolName} } - return nil, &ErrAdapterNotFound{Language: language, Category: category} + return adp, nil } // GetAll returns all registered adapters. -func (r *Registry) GetAll() []interface{} { +func (r *Registry) GetAll() []adapter.Adapter { r.mu.RLock() defer r.mu.RUnlock() // Return a copy to prevent external modification - result := make([]interface{}, len(r.adapters)) - for i, adp := range r.adapters { - result[i] = adp + result := make([]adapter.Adapter, 0, len(r.adapters)) + for _, adp := range r.adapters { + result = append(result, adp) } return result } -// GetSupportedLanguages returns all languages supported for the given category. -// If category is empty, returns all supported languages across all categories. -func (r *Registry) GetSupportedLanguages(category string) []string { +// ListTools returns all registered tool names. +func (r *Registry) ListTools() []string { r.mu.RLock() defer r.mu.RUnlock() - languageSet := make(map[string]bool) - - for _, adp := range r.adapters { - caps := adp.GetCapabilities() - - // If category is specified, filter by it - if category != "" && !contains(caps.SupportedCategories, category) { - continue - } - - // Add all supported languages - for _, lang := range caps.SupportedLanguages { - languageSet[lang] = true - } - } - - // Convert set to slice - languages := make([]string, 0, len(languageSet)) - for lang := range languageSet { - languages = append(languages, lang) - } - - return languages -} - -// contains checks if a slice contains a string. -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } + tools := make([]string, 0, len(r.adapters)) + for name := range r.adapters { + tools = append(tools, name) } - return false + return tools } diff --git a/internal/converter/converter.go b/internal/converter/converter.go index 462c66c..b58a4ab 100644 --- a/internal/converter/converter.go +++ b/internal/converter/converter.go @@ -188,8 +188,33 @@ func (c *Converter) Convert(ctx context.Context, userPolicy *schema.UserPolicy) }, } - // Add selector if languages are specified - if len(userRule.Languages) > 0 || len(userRule.Include) > 0 || len(userRule.Exclude) > 0 { + // Special handling for LLM validator - ensure required fields + if linterName == LLMValidatorEngine { + // LLM validator MUST have 'when' selector for file filtering + if policyRule.When == nil { + // Use languages from rule or defaults + languages := userRule.Languages + if len(languages) == 0 && userPolicy.Defaults != nil { + languages = userPolicy.Defaults.Languages + } + if len(languages) == 0 { + // Default to common languages if none specified + languages = []string{"javascript", "typescript"} + } + + policyRule.When = &schema.Selector{ + Languages: languages, + } + } + + // Ensure desc is not empty (required for LLM prompt) + if policyRule.Desc == "" { + policyRule.Desc = "Code quality check" + } + } + + // Add selector if languages are specified (for non-LLM linters) + if linterName != LLMValidatorEngine && (len(userRule.Languages) > 0 || len(userRule.Include) > 0 || len(userRule.Exclude) > 0) { policyRule.When = &schema.Selector{ Languages: userRule.Languages, Include: userRule.Include, diff --git a/internal/validator/validator.go b/internal/validator/validator.go index dbdead3..2c14bf9 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -2,52 +2,72 @@ package validator import ( "context" + "encoding/json" "fmt" "os" "path/filepath" "strings" - "time" "sync" + "time" - "github.com/DevSymphony/sym-cli/internal/engine/core" - "github.com/DevSymphony/sym-cli/internal/engine/registry" + "github.com/DevSymphony/sym-cli/internal/adapter" + adapterRegistry "github.com/DevSymphony/sym-cli/internal/adapter/registry" "github.com/DevSymphony/sym-cli/internal/git" "github.com/DevSymphony/sym-cli/internal/llm" "github.com/DevSymphony/sym-cli/internal/roles" "github.com/DevSymphony/sym-cli/pkg/schema" ) -// Validator validates code against policy +// Violation represents a policy violation +type Violation struct { + RuleID string + Severity string + Message string + File string + Line int + Column int +} + +// Result represents validation result +type Result struct { + Violations []Violation + Passed bool +} + +// Validator validates code against policy using adapters directly +// This replaces the old engine-based architecture type Validator struct { - policy *schema.CodePolicy - verbose bool - registry *registry.Registry - workDir string - selector *FileSelector - ctx context.Context - ctxCancel context.CancelFunc - llmClient *llm.Client // Optional: for LLM-based validation + policy *schema.CodePolicy + verbose bool + adapterRegistry *adapterRegistry.Registry + workDir string + symDir string // .sym directory for config files + selector *FileSelector + ctx context.Context + ctxCancel context.CancelFunc + llmClient *llm.Client } -// NewValidator creates a new validator +// NewValidator creates a new adapter-based validator func NewValidator(policy *schema.CodePolicy, verbose bool) *Validator { - // Determine working directory workDir, err := os.Getwd() if err != nil { workDir = "." } + symDir := filepath.Join(workDir, ".sym") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) return &Validator{ - policy: policy, - verbose: verbose, - registry: registry.Global(), - workDir: workDir, - selector: NewFileSelector(workDir), - ctx: ctx, - ctxCancel: cancel, - llmClient: nil, // Will be set via SetLLMClient if needed + policy: policy, + verbose: verbose, + adapterRegistry: adapterRegistry.DefaultRegistry(), + workDir: workDir, + symDir: symDir, + selector: NewFileSelector(workDir), + ctx: ctx, + ctxCancel: cancel, + llmClient: nil, } } @@ -56,23 +76,7 @@ func (v *Validator) SetLLMClient(client *llm.Client) { v.llmClient = client } -// Violation represents a policy violation -type Violation struct { - RuleID string - Severity string - Message string - File string - Line int - Column int -} - -// Result represents validation result -type Result struct { - Violations []Violation - Passed bool -} - -// Validate validates the given path +// Validate validates the given path using adapters directly func (v *Validator) Validate(path string) (*Result, error) { if v.policy == nil { return nil, fmt.Errorf("policy is not loaded") @@ -88,58 +92,18 @@ func (v *Validator) Validate(path string) (*Result, error) { } // Check RBAC permissions if enabled - if v.policy.Enforce.RBACConfig != nil && v.policy.Enforce.RBACConfig.Enabled { - // Get current git user - username, err := git.GetCurrentUser() - if err != nil { - if v.verbose { - fmt.Printf("⚠️ RBAC check skipped: %v\n", err) - } - } else { - if v.verbose { - fmt.Printf("🔐 Checking RBAC permissions for user: %s\n", username) - } - - // Get files to check - fileInfo, err := os.Stat(path) - if err == nil && !fileInfo.IsDir() { - // Validate file permissions - rbacResult, err := roles.ValidateFilePermissions(username, []string{path}) - if err != nil { - if v.verbose { - fmt.Printf("⚠️ RBAC validation failed: %v\n", err) - } - } else if !rbacResult.Allowed { - // Add RBAC violations - for _, deniedFile := range rbacResult.DeniedFiles { - result.Violations = append(result.Violations, Violation{ - RuleID: "rbac-permission-denied", - Severity: "error", - Message: fmt.Sprintf("User '%s' does not have permission to modify this file", username), - File: deniedFile, - Line: 0, - Column: 0, - }) - } - result.Passed = false - - if v.verbose { - fmt.Printf("❌ RBAC: %d file(s) denied for user %s\n", len(rbacResult.DeniedFiles), username) - } - } else if v.verbose { - fmt.Printf("✓ RBAC: User %s has permission to modify all files\n", username) - } - } + if err := v.checkRBAC(path, result); err != nil { + if v.verbose { + fmt.Printf("⚠️ RBAC check error: %v\n", err) } } - // For each enabled rule, execute validation + // Validate each enabled rule for _, rule := range v.policy.Rules { if !rule.Enabled { continue } - // Determine which engine to use engineName := getEngineName(rule) if engineName == "" { if v.verbose { @@ -162,62 +126,27 @@ func (v *Validator) Validate(path string) (*Result, error) { continue } - // Get or create engine - engine, err := v.registry.Get(engineName) - if err != nil { - fmt.Printf("⚠️ Engine %s not found for rule %s: %v\n", engineName, rule.ID, err) - continue - } - - // Initialize engine if needed - if err := v.initializeEngine(engine); err != nil { - fmt.Printf("⚠️ Failed to initialize engine %s: %v\n", engineName, err) - continue - } - - // Convert schema.PolicyRule to core.Rule - coreRule := convertToCoreRule(rule) - - // Execute validation if v.verbose { fmt.Printf(" Rule %s (%s): checking %d file(s)...\n", rule.ID, engineName, len(files)) } - validationResult, err := engine.Validate(v.ctx, coreRule, files) + // Execute validation based on engine type + violations, err := v.executeRule(engineName, rule, files) if err != nil { fmt.Printf("⚠️ Validation failed for rule %s: %v\n", rule.ID, err) continue } - // Collect violations - if validationResult != nil && len(validationResult.Violations) > 0 { - for _, coreViolation := range validationResult.Violations { - violation := Violation{ - RuleID: rule.ID, - Severity: rule.Severity, - Message: coreViolation.Message, - File: coreViolation.File, - Line: coreViolation.Line, - Column: coreViolation.Column, - } - - // Use custom message if provided - if rule.Message != "" { - violation.Message = rule.Message - } - - result.Violations = append(result.Violations, violation) - } - + if len(violations) > 0 { + result.Violations = append(result.Violations, violations...) if v.verbose { - fmt.Printf(" ❌ Found %d violation(s)\n", len(validationResult.Violations)) + fmt.Printf(" ❌ Found %d violation(s)\n", len(violations)) } } else if v.verbose { fmt.Printf(" ✓ Passed\n") } } - // Determine overall pass/fail result.Passed = len(result.Violations) == 0 if v.verbose { @@ -231,22 +160,183 @@ func (v *Validator) Validate(path string) (*Result, error) { return result, nil } -// CanAutoFix checks if violations can be auto-fixed -func (v *Result) CanAutoFix() bool { - for _, violation := range v.Violations { - // Check if rule has autofix enabled - _ = violation +// executeRule executes a rule using the appropriate adapter +func (v *Validator) executeRule(engineName string, rule schema.PolicyRule, files []string) ([]Violation, error) { + // Special case: LLM validator + if engineName == "llm-validator" { + return v.executeLLMRule(rule, files) } - return false + + // Get adapter directly by tool name (e.g., "eslint", "prettier", "tsc") + adp, err := v.adapterRegistry.GetAdapter(engineName) + if err != nil { + return nil, fmt.Errorf("adapter not found: %s: %w", engineName, err) + } + + // Check if adapter is available + if err := adp.CheckAvailability(v.ctx); err != nil { + if v.verbose { + fmt.Printf(" 📦 Installing %s...\n", adp.Name()) + } + if err := adp.Install(v.ctx, adapter.InstallConfig{ + ToolsDir: filepath.Join(os.Getenv("HOME"), ".sym", "tools"), + }); err != nil { + return nil, fmt.Errorf("failed to install %s: %w", adp.Name(), err) + } + } + + // Generate config from rule or use existing .sym config + config, err := v.getAdapterConfig(adp.Name(), rule) + if err != nil { + return nil, fmt.Errorf("failed to generate config: %w", err) + } + + // Execute adapter + output, err := adp.Execute(v.ctx, config, files) + if err != nil { + return nil, fmt.Errorf("adapter execution failed: %w", err) + } + + // Parse output to violations + adapterViolations, err := adp.ParseOutput(output) + if err != nil { + return nil, fmt.Errorf("failed to parse output: %w", err) + } + + // Convert adapter violations to validator violations + violations := make([]Violation, 0, len(adapterViolations)) + for _, av := range adapterViolations { + violations = append(violations, Violation{ + RuleID: rule.ID, + Severity: rule.Severity, + Message: av.Message, + File: av.File, + Line: av.Line, + Column: av.Column, + }) + } + + return violations, nil } -// AutoFix attempts to automatically fix violations -func (v *Validator) AutoFix(result *Result) error { - // TODO: Implement auto-fix logic - return fmt.Errorf("auto-fix not implemented yet") +// executeLLMRule executes an LLM-based rule +func (v *Validator) executeLLMRule(rule schema.PolicyRule, files []string) ([]Violation, error) { + if v.llmClient == nil { + return nil, fmt.Errorf("LLM client not configured") + } + + // Validate required fields for LLM validator + if rule.Desc == "" { + return nil, fmt.Errorf("LLM validator requires 'desc' field in rule %s", rule.ID) + } + + // Check if When selector exists for file filtering + if rule.When == nil && len(files) == 0 { + if v.verbose { + fmt.Printf("⚠️ LLM rule %s has no 'when' selector and no files provided\n", rule.ID) + } + } + + violations := make([]Violation, 0) + + for _, file := range files { + // Read file content + content, err := os.ReadFile(file) + if err != nil { + continue + } + + // Build LLM prompt + systemPrompt := `You are a code reviewer. Check if the code violates the given coding convention. + +Respond with JSON only: +{ + "violates": true/false, + "description": "explanation of violation if any", + "suggestion": "how to fix it if violated" +}` + + userPrompt := fmt.Sprintf(`File: %s + +Coding Convention: +%s + +Code: +%s + +Does this code violate the convention?`, file, rule.Desc, string(content)) + + // Call LLM + response, err := v.llmClient.Complete(v.ctx, systemPrompt, userPrompt) + if err != nil { + continue + } + + // Parse response + result := parseValidationResponse(response) + if result.Violates { + message := result.Description + if result.Suggestion != "" { + message += fmt.Sprintf(" | Suggestion: %s", result.Suggestion) + } + + violations = append(violations, Violation{ + RuleID: rule.ID, + Severity: rule.Severity, + Message: message, + File: file, + }) + } + } + + return violations, nil } -// Helper functions +// getAdapterConfig gets config for an adapter +// First checks .sym directory for existing config files, then generates from rule +func (v *Validator) getAdapterConfig(adapterName string, rule schema.PolicyRule) ([]byte, error) { + // Check for existing config in .sym directory + var configPath string + switch adapterName { + case "eslint": + configPath = filepath.Join(v.symDir, ".eslintrc.json") + case "prettier": + configPath = filepath.Join(v.symDir, ".prettierrc.json") + case "tsc": + configPath = filepath.Join(v.symDir, "tsconfig.json") + case "checkstyle": + configPath = filepath.Join(v.symDir, "checkstyle.xml") + case "pmd": + configPath = filepath.Join(v.symDir, "pmd-ruleset.xml") + } + + // If config exists in .sym, use it + if configPath != "" { + if data, err := os.ReadFile(configPath); err == nil { + if v.verbose { + fmt.Printf(" 📄 Using config from %s\n", configPath) + } + return data, nil + } + } + + // Otherwise, generate config from rule + config := make(map[string]interface{}) + + // Copy rule.Check to config + for k, val := range rule.Check { + if k != "engine" && k != "desc" { + config[k] = val + } + } + + // Add rule description + if rule.Desc != "" { + config["description"] = rule.Desc + } + + return json.Marshal(config) +} // getEngineName extracts the engine name from a rule func getEngineName(rule schema.PolicyRule) string { @@ -256,9 +346,54 @@ func getEngineName(rule schema.PolicyRule) string { return "" } +// checkRBAC checks RBAC permissions +func (v *Validator) checkRBAC(path string, result *Result) error { + if v.policy.Enforce.RBACConfig == nil || !v.policy.Enforce.RBACConfig.Enabled { + return nil + } + + username, err := git.GetCurrentUser() + if err != nil { + return err + } + + if v.verbose { + fmt.Printf("🔐 Checking RBAC permissions for user: %s\n", username) + } + + fileInfo, err := os.Stat(path) + if err == nil && !fileInfo.IsDir() { + rbacResult, err := roles.ValidateFilePermissions(username, []string{path}) + if err != nil { + return err + } + + if !rbacResult.Allowed { + for _, deniedFile := range rbacResult.DeniedFiles { + result.Violations = append(result.Violations, Violation{ + RuleID: "rbac-permission-denied", + Severity: "error", + Message: fmt.Sprintf("User '%s' does not have permission to modify this file", username), + File: deniedFile, + Line: 0, + Column: 0, + }) + } + result.Passed = false + + if v.verbose { + fmt.Printf("❌ RBAC: %d file(s) denied for user %s\n", len(rbacResult.DeniedFiles), username) + } + } else if v.verbose { + fmt.Printf("✓ RBAC: User %s has permission to modify all files\n", username) + } + } + + return nil +} + // selectFilesForRule selects files that match the rule's selector func (v *Validator) selectFilesForRule(basePath string, rule *schema.PolicyRule) ([]string, error) { - // If path is a specific file, check if it matches the selector fileInfo, err := os.Stat(basePath) if err != nil { return nil, err @@ -286,75 +421,13 @@ func (v *Validator) selectFilesForRule(basePath string, rule *schema.PolicyRule) // Directory - use selector to find files if rule.When == nil { - // No selector, use all files in directory return v.selector.SelectFiles(nil) } return v.selector.SelectFiles(rule.When) } -// initializeEngine initializes an engine if not already initialized -func (v *Validator) initializeEngine(engine core.Engine) error { - // Create engine config - config := core.EngineConfig{ - WorkDir: v.workDir, - ToolsDir: filepath.Join(os.Getenv("HOME"), ".sym", "tools"), - CacheDir: filepath.Join(os.Getenv("HOME"), ".sym", "cache"), - Timeout: 5 * time.Minute, - Parallelism: 0, - Debug: v.verbose, - } - - // Initialize engine - return engine.Init(v.ctx, config) -} - -// convertToCoreRule converts schema.PolicyRule to core.Rule -func convertToCoreRule(rule schema.PolicyRule) core.Rule { - var when *core.Selector - if rule.When != nil { - when = &core.Selector{ - Languages: rule.When.Languages, - Include: rule.When.Include, - Exclude: rule.When.Exclude, - Branches: rule.When.Branches, - Roles: rule.When.Roles, - Tags: rule.When.Tags, - } - } - - var remedy *core.Remedy - if rule.Remedy != nil { - remedy = &core.Remedy{ - Autofix: rule.Remedy.Autofix, - Tool: rule.Remedy.Tool, - Config: rule.Remedy.Config, - } - } - - return core.Rule{ - ID: rule.ID, - Enabled: rule.Enabled, - Category: rule.Category, - Severity: rule.Severity, - Desc: rule.Desc, - When: when, - Check: rule.Check, - Remedy: remedy, - Message: rule.Message, - } -} - -// Close cleans up validator resources -func (v *Validator) Close() error { - if v.ctxCancel != nil { - v.ctxCancel() - } - return nil -} - -// ValidateChanges validates git changes against all enabled rules -// This is the unified entry point for diff-based validation used by both CLI and MCP +// ValidateChanges validates git changes using adapters directly func (v *Validator) ValidateChanges(ctx context.Context, changes []GitChange) (*ValidationResult, error) { if v.policy == nil { return nil, fmt.Errorf("policy is not loaded") @@ -367,36 +440,24 @@ func (v *Validator) ValidateChanges(ctx context.Context, changes []GitChange) (* Failed: 0, } - // Check RBAC permissions first if enabled + // Check RBAC permissions first if v.policy.Enforce.RBACConfig != nil && v.policy.Enforce.RBACConfig.Enabled { - // Get current git user username, err := git.GetCurrentUser() - if err != nil { - if v.verbose { - fmt.Printf("⚠️ RBAC check skipped: %v\n", err) - } - } else { + if err == nil { if v.verbose { fmt.Printf("🔐 Checking RBAC permissions for user: %s\n", username) } - // Collect all changed files changedFiles := make([]string, 0, len(changes)) for _, change := range changes { - if change.Status != "D" { // Skip deleted files + if change.Status != "D" { changedFiles = append(changedFiles, change.FilePath) } } if len(changedFiles) > 0 { - // Validate file permissions rbacResult, err := roles.ValidateFilePermissions(username, changedFiles) - if err != nil { - if v.verbose { - fmt.Printf("⚠️ RBAC validation failed: %v\n", err) - } - } else if !rbacResult.Allowed { - // Add RBAC violations + if err == nil && !rbacResult.Allowed { for _, deniedFile := range rbacResult.DeniedFiles { result.Violations = append(result.Violations, Violation{ RuleID: "rbac-permission-denied", @@ -408,12 +469,6 @@ func (v *Validator) ValidateChanges(ctx context.Context, changes []GitChange) (* }) result.Failed++ } - - if v.verbose { - fmt.Printf("❌ RBAC: %d file(s) denied for user %s\n", len(rbacResult.DeniedFiles), username) - } - } else if v.verbose { - fmt.Printf("✓ RBAC: User %s has permission to modify all files\n", username) } } } @@ -423,27 +478,20 @@ func (v *Validator) ValidateChanges(ctx context.Context, changes []GitChange) (* fmt.Printf("🔍 Validating %d change(s) against %d rule(s)...\n", len(changes), len(v.policy.Rules)) } - // For each enabled rule, execute validation + // Validate each enabled rule for _, rule := range v.policy.Rules { if !rule.Enabled { continue } - // Determine which engine to use engineName := getEngineName(rule) if engineName == "" { - if v.verbose { - fmt.Printf("⚠️ Rule %s has no engine specified, skipping\n", rule.ID) - } continue } // Filter changes that match this rule's selector relevantChanges := v.filterChangesForRule(changes, &rule) if len(relevantChanges) == 0 { - if v.verbose { - fmt.Printf(" Rule %s: no matching changes\n", rule.ID) - } continue } @@ -451,129 +499,34 @@ func (v *Validator) ValidateChanges(ctx context.Context, changes []GitChange) (* fmt.Printf(" Rule %s (%s): checking %d change(s)...\n", rule.ID, engineName, len(relevantChanges)) } - // Get or create engine - engine, err := v.registry.Get(engineName) - if err != nil { - fmt.Printf("⚠️ Engine %s not found for rule %s: %v\n", engineName, rule.ID, err) - continue - } - - // Initialize engine if needed - if err := v.initializeEngine(engine); err != nil { - fmt.Printf("⚠️ Failed to initialize engine %s: %v\n", engineName, err) - continue - } - - // Convert schema.PolicyRule to core.Rule - coreRule := convertToCoreRule(rule) - - // Execute validation on each change - // For LLM engine, use goroutines for parallel processing + // For LLM engine, use parallel processing if engineName == "llm-validator" { - if v.llmClient == nil { - fmt.Printf("⚠️ LLM client not configured for rule %s\n", rule.ID) - continue - } - - // Use goroutines for parallel LLM validation - var wg sync.WaitGroup - var mu sync.Mutex - llmValidator := NewLLMValidator(v.llmClient, v.policy) - - for _, change := range relevantChanges { - if change.Status == "D" { - continue // Skip deleted files - } - - // Extract added lines from diff - addedLines := ExtractAddedLines(change.Diff) - if len(addedLines) == 0 && strings.TrimSpace(change.Diff) != "" { - addedLines = strings.Split(change.Diff, "\n") - } - - if len(addedLines) == 0 { - mu.Lock() - result.Checked++ - result.Passed++ - mu.Unlock() - continue - } - - // Increment counter and launch goroutine - mu.Lock() - result.Checked++ - mu.Unlock() - - wg.Add(1) - go func(ch GitChange, lines []string, r schema.PolicyRule) { - defer wg.Done() - - violation, err := llmValidator.CheckRule(ctx, ch, lines, r) - if err != nil { - fmt.Printf("⚠️ Validation failed for rule %s: %v\n", r.ID, err) - return - } - - mu.Lock() - defer mu.Unlock() - if violation != nil { - result.Failed++ - result.Violations = append(result.Violations, *violation) - } else { - result.Passed++ - } - }(change, addedLines, rule) - } - - // Wait for all goroutines to complete - wg.Wait() + v.validateLLMChanges(ctx, relevantChanges, rule, result) } else { - // For other engines, process sequentially + // For adapter-based engines, validate files for _, change := range relevantChanges { if change.Status == "D" { - continue // Skip deleted files + continue } result.Checked++ - // For other engines, validate the file - validationResult, err := engine.Validate(ctx, coreRule, []string{change.FilePath}) + violations, err := v.executeRule(engineName, rule, []string{change.FilePath}) if err != nil { - fmt.Printf("⚠️ Validation failed for rule %s: %v\n", rule.ID, err) + if v.verbose { + fmt.Printf("⚠️ Validation failed for rule %s: %v\n", rule.ID, err) + } continue } - // Collect violations - if validationResult != nil && len(validationResult.Violations) > 0 { + if len(violations) > 0 { result.Failed++ - for _, coreViolation := range validationResult.Violations { - violation := Violation{ - RuleID: rule.ID, - Severity: rule.Severity, - Message: coreViolation.Message, - File: coreViolation.File, - Line: coreViolation.Line, - Column: coreViolation.Column, - } - - // Use custom message if provided - if rule.Message != "" { - violation.Message = rule.Message - } - - result.Violations = append(result.Violations, violation) - } + result.Violations = append(result.Violations, violations...) } else { result.Passed++ } } } - - if v.verbose && len(result.Violations) > 0 { - fmt.Printf(" ❌ Found %d violation(s)\n", len(result.Violations)) - } else if v.verbose { - fmt.Printf(" ✓ Passed\n") - } } if v.verbose { @@ -587,15 +540,69 @@ func (v *Validator) ValidateChanges(ctx context.Context, changes []GitChange) (* return result, nil } +// validateLLMChanges validates changes using LLM in parallel +func (v *Validator) validateLLMChanges(ctx context.Context, changes []GitChange, rule schema.PolicyRule, result *ValidationResult) { + if v.llmClient == nil { + return + } + + var wg sync.WaitGroup + var mu sync.Mutex + llmValidator := NewLLMValidator(v.llmClient, v.policy) + + for _, change := range changes { + if change.Status == "D" { + continue + } + + addedLines := ExtractAddedLines(change.Diff) + if len(addedLines) == 0 && strings.TrimSpace(change.Diff) != "" { + addedLines = strings.Split(change.Diff, "\n") + } + + if len(addedLines) == 0 { + mu.Lock() + result.Checked++ + result.Passed++ + mu.Unlock() + continue + } + + mu.Lock() + result.Checked++ + mu.Unlock() + + wg.Add(1) + go func(ch GitChange, lines []string, r schema.PolicyRule) { + defer wg.Done() + + violation, err := llmValidator.CheckRule(ctx, ch, lines, r) + if err != nil { + return + } + + mu.Lock() + defer mu.Unlock() + if violation != nil { + result.Failed++ + result.Violations = append(result.Violations, *violation) + } else { + result.Passed++ + } + }(change, addedLines, rule) + } + + wg.Wait() +} + // filterChangesForRule filters git changes that match the rule's selector func (v *Validator) filterChangesForRule(changes []GitChange, rule *schema.PolicyRule) []GitChange { if rule.When == nil { - return changes // No selector, all changes match + return changes } var filtered []GitChange for _, change := range changes { - // Check language match if len(rule.When.Languages) > 0 { lang := GetLanguageFromFile(change.FilePath) matched := false @@ -610,9 +617,26 @@ func (v *Validator) filterChangesForRule(changes []GitChange, rule *schema.Polic } } - // TODO: Check include/exclude patterns filtered = append(filtered, change) } return filtered } + +// Close cleans up validator resources +func (v *Validator) Close() error { + if v.ctxCancel != nil { + v.ctxCancel() + } + return nil +} + +// CanAutoFix checks if violations can be auto-fixed +func (v *Result) CanAutoFix() bool { + return false +} + +// AutoFix attempts to automatically fix violations +func (v *Validator) AutoFix(result *Result) error { + return fmt.Errorf("auto-fix not implemented yet") +} From 832de7467391128e7276b93335608d1973138f92 Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 11:34:43 +0900 Subject: [PATCH 04/12] fix: remove generateConfig --- cmd/test-adapter/main.go | 176 ++++++++++++++++++ internal/adapter/adapter.go | 5 +- internal/adapter/checkstyle/adapter.go | 4 - internal/adapter/checkstyle/config.go | 236 ------------------------- internal/adapter/eslint/adapter.go | 6 - internal/adapter/eslint/ast.go | 119 ------------- internal/adapter/eslint/ast_test.go | 144 --------------- internal/adapter/eslint/config.go | 206 --------------------- internal/adapter/pmd/adapter.go | 4 - internal/adapter/pmd/config.go | 134 -------------- internal/adapter/prettier/adapter.go | 4 - internal/adapter/prettier/config.go | 60 ------- internal/adapter/registry/errors.go | 16 +- internal/adapter/tsc/config.go | 131 -------------- 14 files changed, 180 insertions(+), 1065 deletions(-) create mode 100644 cmd/test-adapter/main.go delete mode 100644 internal/adapter/checkstyle/config.go delete mode 100644 internal/adapter/eslint/ast.go delete mode 100644 internal/adapter/eslint/ast_test.go delete mode 100644 internal/adapter/eslint/config.go delete mode 100644 internal/adapter/pmd/config.go delete mode 100644 internal/adapter/prettier/config.go delete mode 100644 internal/adapter/tsc/config.go diff --git a/cmd/test-adapter/main.go b/cmd/test-adapter/main.go new file mode 100644 index 0000000..ee1f9d1 --- /dev/null +++ b/cmd/test-adapter/main.go @@ -0,0 +1,176 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/DevSymphony/sym-cli/internal/adapter" + "github.com/DevSymphony/sym-cli/internal/adapter/eslint" + "github.com/DevSymphony/sym-cli/internal/adapter/prettier" + "github.com/DevSymphony/sym-cli/internal/adapter/tsc" +) + +// test-adapter is a CLI tool to test individual adapters +// Usage: go run cmd/test-adapter/main.go [files...] +// +// Examples: +// go run cmd/test-adapter/main.go eslint src/app.js +// go run cmd/test-adapter/main.go tsc +// go run cmd/test-adapter/main.go prettier --check + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: test-adapter [files...]") + fmt.Println("\nAvailable adapters:") + fmt.Println(" - eslint") + fmt.Println(" - prettier") + fmt.Println(" - tsc") + fmt.Println("\nExamples:") + fmt.Println(" test-adapter eslint") + fmt.Println(" test-adapter tsc") + fmt.Println(" test-adapter prettier --check") + os.Exit(1) + } + + adapterName := os.Args[1] + files := os.Args[2:] + + // Get current directory + workDir, err := os.Getwd() + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + + symDir := filepath.Join(workDir, ".sym") + toolsDir := filepath.Join(os.Getenv("HOME"), ".sym", "tools") + + // Create adapter + var adp adapter.Adapter + switch adapterName { + case "eslint": + adp = eslint.NewAdapter(toolsDir, workDir) + case "prettier": + adp = prettier.NewAdapter(toolsDir, workDir) + case "tsc": + adp = tsc.NewAdapter(toolsDir, workDir) + default: + fmt.Printf("Unknown adapter: %s\n", adapterName) + os.Exit(1) + } + + fmt.Printf("🔧 Testing adapter: %s\n", adp.Name()) + fmt.Printf("📁 Working directory: %s\n", workDir) + fmt.Printf("📁 .sym directory: %s\n", symDir) + fmt.Printf("📁 Tools directory: %s\n\n", toolsDir) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // Check availability + fmt.Printf("🔍 Checking availability...\n") + if err := adp.CheckAvailability(ctx); err != nil { + fmt.Printf("⚠️ Not available: %v\n", err) + fmt.Printf("📦 Installing...\n") + if err := adp.Install(ctx, adapter.InstallConfig{ + ToolsDir: toolsDir, + }); err != nil { + fmt.Printf("❌ Installation failed: %v\n", err) + os.Exit(1) + } + fmt.Printf("✅ Installed successfully\n\n") + } else { + fmt.Printf("✅ Available\n\n") + } + + // Load config from .sym directory + var config []byte + configPath := getConfigPath(symDir, adapterName) + + fmt.Printf("📄 Looking for config: %s\n", configPath) + if data, err := os.ReadFile(configPath); err == nil { + config = data + fmt.Printf("✅ Using config from %s\n\n", configPath) + } else { + fmt.Printf("⚠️ No config found, using default\n\n") + config = []byte("{}") + } + + // If no files specified, let the adapter use its default file discovery + if len(files) == 0 { + fmt.Printf("📂 No files specified, adapter will use its default file discovery\n\n") + } else { + fmt.Printf("📂 Files to check: %v\n\n", files) + } + + // Execute adapter + fmt.Printf("🚀 Running %s...\n", adapterName) + output, err := adp.Execute(ctx, config, files) + if err != nil { + fmt.Printf("❌ Execution failed: %v\n", err) + os.Exit(1) + } + + // Display results + fmt.Printf("\n📊 Results:\n") + fmt.Printf("Exit Code: %d\n", output.ExitCode) + fmt.Printf("Duration: %s\n\n", output.Duration) + + if output.Stdout != "" { + fmt.Printf("📤 Stdout:\n%s\n\n", output.Stdout) + } + + if output.Stderr != "" { + fmt.Printf("📤 Stderr:\n%s\n\n", output.Stderr) + } + + // Parse violations + violations, err := adp.ParseOutput(output) + if err != nil { + fmt.Printf("⚠️ Failed to parse output: %v\n", err) + } else { + fmt.Printf("🔍 Found %d violation(s):\n\n", len(violations)) + for i, v := range violations { + fmt.Printf("[%d] %s:%d:%d\n", i+1, v.File, v.Line, v.Column) + fmt.Printf(" Severity: %s\n", v.Severity) + fmt.Printf(" Message: %s\n", v.Message) + if v.RuleID != "" { + fmt.Printf(" Rule: %s\n", v.RuleID) + } + fmt.Printf("\n") + } + } + + // Print summary as JSON + summary := map[string]interface{}{ + "adapter": adapterName, + "exitCode": output.ExitCode, + "duration": output.Duration, + "violations": len(violations), + } + + summaryJSON, _ := json.MarshalIndent(summary, "", " ") + fmt.Printf("\n📋 Summary:\n%s\n", string(summaryJSON)) + + // Exit with appropriate code + if output.ExitCode != 0 || len(violations) > 0 { + os.Exit(1) + } +} + +func getConfigPath(symDir, adapterName string) string { + switch adapterName { + case "eslint": + return filepath.Join(symDir, ".eslintrc.json") + case "prettier": + return filepath.Join(symDir, ".prettierrc.json") + case "tsc": + return filepath.Join(symDir, "tsconfig.json") + default: + return filepath.Join(symDir, adapterName+".json") + } +} diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go index e83921a..f93539f 100644 --- a/internal/adapter/adapter.go +++ b/internal/adapter/adapter.go @@ -26,11 +26,8 @@ type Adapter interface { // Returns error if installation fails. Install(ctx context.Context, config InstallConfig) error - // GenerateConfig generates tool-specific config from a rule. - // Returns config content (JSON, XML, YAML, etc.). - GenerateConfig(rule interface{}) ([]byte, error) - // Execute runs the tool with the given config and files. + // Config is read from .sym directory (e.g., .sym/.eslintrc.json). // Returns raw tool output. Execute(ctx context.Context, config []byte, files []string) (*ToolOutput, error) diff --git a/internal/adapter/checkstyle/adapter.go b/internal/adapter/checkstyle/adapter.go index b5274f4..ae88d24 100644 --- a/internal/adapter/checkstyle/adapter.go +++ b/internal/adapter/checkstyle/adapter.go @@ -132,10 +132,6 @@ func (a *Adapter) Install(ctx context.Context, config adapter.InstallConfig) err return nil } -// GenerateConfig generates Checkstyle XML config from a rule. -func (a *Adapter) GenerateConfig(rule interface{}) ([]byte, error) { - return generateConfig(rule) -} // Execute runs Checkstyle with the given config and files. func (a *Adapter) Execute(ctx context.Context, config []byte, files []string) (*adapter.ToolOutput, error) { diff --git a/internal/adapter/checkstyle/config.go b/internal/adapter/checkstyle/config.go deleted file mode 100644 index 8ae5a91..0000000 --- a/internal/adapter/checkstyle/config.go +++ /dev/null @@ -1,236 +0,0 @@ -package checkstyle - -import ( - "encoding/xml" - "fmt" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// CheckstyleModule represents a Checkstyle module in XML. -type CheckstyleModule struct { - XMLName xml.Name `xml:"module"` - Name string `xml:"name,attr"` - Properties []CheckstyleProperty `xml:"property,omitempty"` - Modules []CheckstyleModule `xml:"module,omitempty"` -} - -// CheckstyleProperty represents a property in Checkstyle XML. -type CheckstyleProperty struct { - XMLName xml.Name `xml:"property"` - Name string `xml:"name,attr"` - Value string `xml:"value,attr"` -} - -// CheckstyleConfig represents the root Checkstyle configuration. -type CheckstyleConfig struct { - XMLName xml.Name `xml:"module"` - Name string `xml:"name,attr"` - Modules []CheckstyleModule `xml:"module"` -} - -// generateConfig generates Checkstyle XML configuration from a rule. -// The rule parameter should be a *core.Rule containing check configuration. -func generateConfig(ruleInterface interface{}) ([]byte, error) { - // Type assert to *core.Rule - rule, ok := ruleInterface.(*core.Rule) - if !ok { - return nil, fmt.Errorf("expected *core.Rule, got %T", ruleInterface) - } - - // Get engine type to determine how to generate config - engine := rule.GetString("engine") - - // Build check modules based on engine type - var checkModules []CheckstyleModule - - switch engine { - case "pattern": - // For pattern engine rules, create a module based on target - target := rule.GetString("target") - pattern := rule.GetString("pattern") - if target != "" && pattern != "" { - module := CheckstyleModule{ - Name: target, // e.g., "TypeName", "MethodName", "MemberName" - Properties: []CheckstyleProperty{ - { - Name: "format", - Value: pattern, - }, - }, - } - checkModules = append(checkModules, module) - } - - case "style": - // For style engine rules, map style properties to Checkstyle modules - checkModules = generateStyleModules(rule) - - case "length": - // For length engine rules, create length check modules - checkModules = generateLengthModules(rule) - } - - // Separate TreeWalker modules from Checker-level modules - var treeWalkerModules []CheckstyleModule - var checkerModules []CheckstyleModule - - for _, module := range checkModules { - // LineLength must be a direct child of Checker, not TreeWalker - if module.Name == "LineLength" { - checkerModules = append(checkerModules, module) - } else { - treeWalkerModules = append(treeWalkerModules, module) - } - } - - // Build the root configuration - modules := []CheckstyleModule{} - - // Add TreeWalker with its children if any - if len(treeWalkerModules) > 0 { - modules = append(modules, CheckstyleModule{ - Name: "TreeWalker", - Modules: treeWalkerModules, - }) - } - - // Add Checker-level modules - modules = append(modules, checkerModules...) - - rootModule := CheckstyleConfig{ - Name: "Checker", - Modules: modules, - } - - // Marshal to XML - output, err := xml.MarshalIndent(rootModule, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal checkstyle config: %w", err) - } - - // Add XML header and DOCTYPE - xmlHeader := ` - -` - - return []byte(xmlHeader + string(output)), nil -} - -// generateStyleModules creates Checkstyle modules for style rules. -func generateStyleModules(rule *core.Rule) []CheckstyleModule { - var modules []CheckstyleModule - - // Indentation - if indent := rule.GetInt("indent"); indent > 0 { - modules = append(modules, CheckstyleModule{ - Name: "Indentation", - Properties: []CheckstyleProperty{ - {Name: "basicOffset", Value: fmt.Sprintf("%d", indent)}, - {Name: "braceAdjustment", Value: "0"}, - {Name: "caseIndent", Value: fmt.Sprintf("%d", indent)}, - }, - }) - } - - // Brace style - if braceStyle := rule.GetString("braceStyle"); braceStyle == "same-line" { - modules = append(modules, CheckstyleModule{ - Name: "LeftCurly", - Properties: []CheckstyleProperty{ - {Name: "option", Value: "eol"}, - }, - }) - } - - // Space after keyword - if rule.GetBool("spaceAfterKeyword") { - modules = append(modules, CheckstyleModule{ - Name: "WhitespaceAfter", - Properties: []CheckstyleProperty{ - {Name: "tokens", Value: "COMMA, SEMI, LITERAL_IF, LITERAL_ELSE, LITERAL_WHILE, LITERAL_DO, LITERAL_FOR"}, - }, - }) - } - - // Space around operators - if rule.GetBool("spaceAroundOperators") { - modules = append(modules, CheckstyleModule{ - Name: "WhitespaceAround", - Properties: []CheckstyleProperty{ - {Name: "allowEmptyConstructors", Value: "true"}, - {Name: "allowEmptyMethods", Value: "true"}, - }, - }) - } - - // Line length - if printWidth := rule.GetInt("printWidth"); printWidth > 0 { - modules = append(modules, CheckstyleModule{ - Name: "LineLength", - Properties: []CheckstyleProperty{ - {Name: "max", Value: fmt.Sprintf("%d", printWidth)}, - }, - }) - } - - // Blank lines between methods - if rule.GetBool("blankLinesBetweenMethods") { - modules = append(modules, CheckstyleModule{ - Name: "EmptyLineSeparator", - Properties: []CheckstyleProperty{ - {Name: "allowNoEmptyLineBetweenFields", Value: "true"}, - {Name: "tokens", Value: "METHOD_DEF"}, - }, - }) - } - - // One statement per line - if rule.GetBool("oneStatementPerLine") { - modules = append(modules, CheckstyleModule{ - Name: "OneStatementPerLine", - }) - } - - return modules -} - -// generateLengthModules creates Checkstyle modules for length rules. -func generateLengthModules(rule *core.Rule) []CheckstyleModule { - var modules []CheckstyleModule - - scope := rule.GetString("scope") - max := rule.GetInt("max") - - if max == 0 { - return modules - } - - switch scope { - case "line": - modules = append(modules, CheckstyleModule{ - Name: "LineLength", - Properties: []CheckstyleProperty{ - {Name: "max", Value: fmt.Sprintf("%d", max)}, - }, - }) - case "method": - modules = append(modules, CheckstyleModule{ - Name: "MethodLength", - Properties: []CheckstyleProperty{ - {Name: "max", Value: fmt.Sprintf("%d", max)}, - }, - }) - case "params", "parameters": - modules = append(modules, CheckstyleModule{ - Name: "ParameterNumber", - Properties: []CheckstyleProperty{ - {Name: "max", Value: fmt.Sprintf("%d", max)}, - }, - }) - } - - return modules -} diff --git a/internal/adapter/eslint/adapter.go b/internal/adapter/eslint/adapter.go index 1afc6d4..314cb6e 100644 --- a/internal/adapter/eslint/adapter.go +++ b/internal/adapter/eslint/adapter.go @@ -113,12 +113,6 @@ func (a *Adapter) Install(ctx context.Context, config adapter.InstallConfig) err return nil } -// GenerateConfig generates ESLint config from a rule. -// Returns .eslintrc.json content. -func (a *Adapter) GenerateConfig(rule interface{}) ([]byte, error) { - // Implementation in config.go - return generateConfig(rule) -} // Execute runs ESLint with the given config and files. func (a *Adapter) Execute(ctx context.Context, config []byte, files []string) (*adapter.ToolOutput, error) { diff --git a/internal/adapter/eslint/ast.go b/internal/adapter/eslint/ast.go deleted file mode 100644 index cb666cd..0000000 --- a/internal/adapter/eslint/ast.go +++ /dev/null @@ -1,119 +0,0 @@ -package eslint - -import ( - "fmt" - "strings" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// ASTQuery represents a parsed AST query from a rule. -type ASTQuery struct { - Node string `json:"node"` - Where map[string]interface{} `json:"where,omitempty"` - Has []string `json:"has,omitempty"` - NotHas []string `json:"notHas,omitempty"` - Language string `json:"language,omitempty"` -} - -// ParseASTQuery extracts AST query from a rule's check field. -func ParseASTQuery(rule *core.Rule) (*ASTQuery, error) { - node, ok := rule.Check["node"].(string) - if !ok || node == "" { - return nil, fmt.Errorf("AST rule requires 'node' field") - } - - query := &ASTQuery{ - Node: node, - } - - if where, ok := rule.Check["where"].(map[string]interface{}); ok { - query.Where = where - } - - if has, ok := rule.Check["has"].([]interface{}); ok { - query.Has = interfaceSliceToStringSlice(has) - } - - if notHas, ok := rule.Check["notHas"].([]interface{}); ok { - query.NotHas = interfaceSliceToStringSlice(notHas) - } - - if lang, ok := rule.Check["language"].(string); ok { - query.Language = lang - } - - return query, nil -} - -// GenerateESTreeSelector generates ESLint AST selector from AST query. -// Uses ESLint's no-restricted-syntax with ESTree selectors. -func GenerateESTreeSelector(query *ASTQuery) string { - var parts []string - - // Start with node type - parts = append(parts, query.Node) - - // Add where conditions as attribute selectors - if len(query.Where) > 0 { - for key, value := range query.Where { - selector := generateAttributeSelector(key, value) - if selector != "" { - parts = append(parts, selector) - } - } - } - - // Combine into single selector - selector := strings.Join(parts, "") - - // For "has" queries, use descendant combinator - if len(query.Has) > 0 { - // ESLint selector: "FunctionDeclaration:not(:has(TryStatement))" - for _, nodeType := range query.Has { - selector = fmt.Sprintf("%s:not(:has(%s))", selector, nodeType) - } - } - - // For "notHas" queries, check presence - if len(query.NotHas) > 0 { - for _, nodeType := range query.NotHas { - selector = fmt.Sprintf("%s:has(%s)", selector, nodeType) - } - } - - return selector -} - -// generateAttributeSelector creates an attribute selector for ESTree. -func generateAttributeSelector(key string, value interface{}) string { - switch v := value.(type) { - case bool: - if v { - return fmt.Sprintf("[%s=true]", key) - } - return fmt.Sprintf("[%s=false]", key) - case string: - return fmt.Sprintf("[%s=\"%s\"]", key, v) - case float64, int: - return fmt.Sprintf("[%s=%v]", key, v) - case map[string]interface{}: - // Handle operators - if eq, ok := v["eq"]; ok { - return generateAttributeSelector(key, eq) - } - // Other operators not supported in ESTree selectors - } - return "" -} - -// interfaceSliceToStringSlice converts []interface{} to []string. -func interfaceSliceToStringSlice(slice []interface{}) []string { - result := make([]string, 0, len(slice)) - for _, item := range slice { - if s, ok := item.(string); ok { - result = append(result, s) - } - } - return result -} diff --git a/internal/adapter/eslint/ast_test.go b/internal/adapter/eslint/ast_test.go deleted file mode 100644 index 927f7cc..0000000 --- a/internal/adapter/eslint/ast_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package eslint - -import ( - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestParseASTQuery(t *testing.T) { - tests := []struct { - name string - rule *core.Rule - want *ASTQuery - wantErr bool - }{ - { - name: "simple node query", - rule: &core.Rule{ - Check: map[string]interface{}{ - "node": "FunctionDeclaration", - }, - }, - want: &ASTQuery{ - Node: "FunctionDeclaration", - }, - wantErr: false, - }, - { - name: "query with where clause", - rule: &core.Rule{ - Check: map[string]interface{}{ - "node": "FunctionDeclaration", - "where": map[string]interface{}{ - "async": true, - }, - }, - }, - want: &ASTQuery{ - Node: "FunctionDeclaration", - Where: map[string]interface{}{ - "async": true, - }, - }, - wantErr: false, - }, - { - name: "query with has clause", - rule: &core.Rule{ - Check: map[string]interface{}{ - "node": "FunctionDeclaration", - "has": []interface{}{"TryStatement"}, - }, - }, - want: &ASTQuery{ - Node: "FunctionDeclaration", - Has: []string{"TryStatement"}, - }, - wantErr: false, - }, - { - name: "missing node", - rule: &core.Rule{ - Check: map[string]interface{}{}, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := ParseASTQuery(tt.rule) - if (err != nil) != tt.wantErr { - t.Errorf("ParseASTQuery() error = %v, wantErr %v", err, tt.wantErr) - return - } - if tt.wantErr { - return - } - if got.Node != tt.want.Node { - t.Errorf("Node = %v, want %v", got.Node, tt.want.Node) - } - if len(tt.want.Where) > 0 && got.Where == nil { - t.Errorf("Where is nil, want %v", tt.want.Where) - } - if len(tt.want.Has) > 0 && len(got.Has) != len(tt.want.Has) { - t.Errorf("Has = %v, want %v", got.Has, tt.want.Has) - } - }) - } -} - -func TestGenerateESTreeSelector(t *testing.T) { - tests := []struct { - name string - query *ASTQuery - want string - }{ - { - name: "simple node", - query: &ASTQuery{ - Node: "FunctionDeclaration", - }, - want: "FunctionDeclaration", - }, - { - name: "node with where clause", - query: &ASTQuery{ - Node: "FunctionDeclaration", - Where: map[string]interface{}{ - "async": true, - }, - }, - want: "FunctionDeclaration[async=true]", - }, - { - name: "node with has clause", - query: &ASTQuery{ - Node: "FunctionDeclaration", - Where: map[string]interface{}{ - "async": true, - }, - Has: []string{"TryStatement"}, - }, - want: "FunctionDeclaration[async=true]:not(:has(TryStatement))", - }, - { - name: "node with notHas clause", - query: &ASTQuery{ - Node: "FunctionDeclaration", - NotHas: []string{"ReturnStatement"}, - }, - want: "FunctionDeclaration:has(ReturnStatement)", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := GenerateESTreeSelector(tt.query) - if got != tt.want { - t.Errorf("GenerateESTreeSelector() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/adapter/eslint/config.go b/internal/adapter/eslint/config.go deleted file mode 100644 index e943b7c..0000000 --- a/internal/adapter/eslint/config.go +++ /dev/null @@ -1,206 +0,0 @@ -package eslint - -import ( - "encoding/json" - "fmt" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// ESLintConfig represents .eslintrc.json structure. -type ESLintConfig struct { - Env map[string]bool `json:"env,omitempty"` - ParserOptions map[string]interface{} `json:"parserOptions,omitempty"` - Rules map[string]interface{} `json:"rules"` - Extra map[string]interface{} `json:"-"` // For extensions -} - -// generateConfig creates ESLint config from a Symphony rule. -func generateConfig(ruleInterface interface{}) ([]byte, error) { - rule, ok := ruleInterface.(*core.Rule) - if !ok { - return nil, fmt.Errorf("expected *core.Rule, got %T", ruleInterface) - } - - config := &ESLintConfig{ - Env: map[string]bool{ - "es2021": true, - "node": true, - "browser": true, - }, - ParserOptions: map[string]interface{}{ - "ecmaVersion": "latest", - "sourceType": "module", - }, - Rules: make(map[string]interface{}), - } - - // Determine which ESLint rules to use based on engine type - engine := rule.GetString("engine") - - switch engine { - case "pattern": - if err := addPatternRules(config, rule); err != nil { - return nil, err - } - case "length": - if err := addLengthRules(config, rule); err != nil { - return nil, err - } - case "style": - if err := addStyleRules(config, rule); err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("unsupported engine: %s", engine) - } - - return json.MarshalIndent(config, "", " ") -} - -// addPatternRules adds pattern validation rules. -func addPatternRules(config *ESLintConfig, rule *core.Rule) error { - target := rule.GetString("target") - pattern := rule.GetString("pattern") - - if pattern == "" { - return fmt.Errorf("pattern is required for pattern engine") - } - - switch target { - case "identifier": - // Use id-match rule for identifier patterns - config.Rules["id-match"] = []interface{}{ - MapSeverity(rule.Severity), - pattern, - map[string]interface{}{ - "properties": false, - "classFields": false, - "onlyDeclarations": true, - }, - } - - case "content": - // Use no-restricted-syntax for content patterns - config.Rules["no-restricted-syntax"] = []interface{}{ - MapSeverity(rule.Severity), - map[string]interface{}{ - "selector": fmt.Sprintf("Literal[value=/%s/]", pattern), - "message": rule.Message, - }, - } - - case "import": - // Use no-restricted-imports for import patterns - config.Rules["no-restricted-imports"] = []interface{}{ - MapSeverity(rule.Severity), - map[string]interface{}{ - "patterns": []string{pattern}, - }, - } - - default: - return fmt.Errorf("unsupported pattern target: %s", target) - } - - return nil -} - -// addLengthRules adds length constraint rules. -func addLengthRules(config *ESLintConfig, rule *core.Rule) error { - scope := rule.GetString("scope") - max := rule.GetInt("max") - min := rule.GetInt("min") - - if max == 0 && min == 0 { - return fmt.Errorf("max or min is required for length engine") - } - - switch scope { - case "line": - // Use max-len rule - opts := map[string]interface{}{ - "code": max, - } - if min > 0 { - // TODO: ESLint doesn't have min-len, so we'd need custom rule - // For now, just enforce max - _ = min // Explicitly ignore min for now - } - config.Rules["max-len"] = []interface{}{MapSeverity(rule.Severity), opts} - - case "file": - // Use max-lines rule - opts := map[string]interface{}{ - "max": max, - "skipBlankLines": true, - "skipComments": true, - } - config.Rules["max-lines"] = []interface{}{MapSeverity(rule.Severity), opts} - - case "function": - // Use max-lines-per-function rule - opts := map[string]interface{}{ - "max": max, - "skipBlankLines": true, - "skipComments": true, - } - config.Rules["max-lines-per-function"] = []interface{}{MapSeverity(rule.Severity), opts} - - case "params": - // Use max-params rule - config.Rules["max-params"] = []interface{}{MapSeverity(rule.Severity), max} - - default: - return fmt.Errorf("unsupported length scope: %s", scope) - } - - return nil -} - -// addStyleRules adds style formatting rules. -func addStyleRules(config *ESLintConfig, rule *core.Rule) error { - // Get style properties from rule.Check - indent := rule.GetInt("indent") - quote := rule.GetString("quote") - semi := rule.GetBool("semi") - - if indent > 0 { - config.Rules["indent"] = []interface{}{MapSeverity(rule.Severity), indent} - } - - if quote != "" { - config.Rules["quotes"] = []interface{}{MapSeverity(rule.Severity), quote} - } - - // Semi is boolean, but we need to handle it carefully - // If explicitly set, add the rule - if _, ok := rule.Check["semi"]; ok { - if semi { - config.Rules["semi"] = []interface{}{MapSeverity(rule.Severity), "always"} - } else { - config.Rules["semi"] = []interface{}{MapSeverity(rule.Severity), "never"} - } - } - - return nil -} - -// MarshalConfig converts a config map to JSON bytes. -func MarshalConfig(config map[string]interface{}) ([]byte, error) { - return json.MarshalIndent(config, "", " ") -} - -// MapSeverity converts severity string to ESLint severity level. -func MapSeverity(severity string) interface{} { - switch severity { - case "error": - return "error" - case "warning", "warn": - return "warn" - case "info", "off": - return "off" - default: - return "error" - } -} diff --git a/internal/adapter/pmd/adapter.go b/internal/adapter/pmd/adapter.go index 91a1dde..e290443 100644 --- a/internal/adapter/pmd/adapter.go +++ b/internal/adapter/pmd/adapter.go @@ -142,10 +142,6 @@ func (a *Adapter) Install(ctx context.Context, config adapter.InstallConfig) err return nil } -// GenerateConfig generates PMD ruleset XML from a rule. -func (a *Adapter) GenerateConfig(rule interface{}) ([]byte, error) { - return generateConfig(rule) -} // Execute runs PMD with the given config and files. func (a *Adapter) Execute(ctx context.Context, config []byte, files []string) (*adapter.ToolOutput, error) { diff --git a/internal/adapter/pmd/config.go b/internal/adapter/pmd/config.go deleted file mode 100644 index ea8751a..0000000 --- a/internal/adapter/pmd/config.go +++ /dev/null @@ -1,134 +0,0 @@ -package pmd - -import ( - "encoding/xml" - "fmt" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// PMDRuleset represents the root PMD ruleset. -type PMDRuleset struct { - XMLName xml.Name `xml:"ruleset"` - Name string `xml:"name,attr"` - XMLNS string `xml:"xmlns,attr"` - XMLNSXSI string `xml:"xmlns:xsi,attr"` - XSISchema string `xml:"xsi:schemaLocation,attr"` - Description string `xml:"description"` - Rules []PMDRule `xml:"rule"` -} - -// PMDRule represents a single PMD rule reference. -type PMDRule struct { - XMLName xml.Name `xml:"rule"` - Ref string `xml:"ref,attr,omitempty"` - Name string `xml:"name,attr,omitempty"` - Message string `xml:"message,attr,omitempty"` - Priority int `xml:"priority,omitempty"` - Properties []PMDProperty `xml:"properties>property,omitempty"` -} - -// PMDProperty represents a property in PMD rule. -type PMDProperty struct { - XMLName xml.Name `xml:"property"` - Name string `xml:"name,attr"` - Value string `xml:"value,attr,omitempty"` -} - -// generateConfig generates PMD ruleset XML configuration from a rule. -func generateConfig(ruleInterface interface{}) ([]byte, error) { - // Type assert to *core.Rule - rule, ok := ruleInterface.(*core.Rule) - if !ok { - return nil, fmt.Errorf("expected *core.Rule, got %T", ruleInterface) - } - - // Generate PMD rules based on AST node type - pmdRules := generatePMDRules(rule) - - ruleset := PMDRuleset{ - Name: "Symphony Convention Rules", - XMLNS: "http://pmd.sourceforge.net/ruleset/2.0.0", - XMLNSXSI: "http://www.w3.org/2001/XMLSchema-instance", - XSISchema: "http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd", - Description: "Generated PMD ruleset from Symphony policy", - Rules: pmdRules, - } - - // Marshal to XML - output, err := xml.MarshalIndent(ruleset, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal PMD ruleset: %w", err) - } - - // Add XML header - xmlHeader := ` -` - - return []byte(xmlHeader + string(output)), nil -} - -// generatePMDRules maps AST rules to PMD built-in rules. -func generatePMDRules(rule *core.Rule) []PMDRule { - var rules []PMDRule - - node := rule.GetString("node") - - // Map common AST patterns to PMD rules - switch node { - case "MethodCallExpr": - // Check if it's System.out usage - if where, ok := rule.Check["where"].(map[string]interface{}); ok { - if scope, ok := where["scope"].(string); ok && scope == "System.out" { - rules = append(rules, PMDRule{ - Ref: "category/java/bestpractices.xml/SystemPrintln", - Priority: 3, - }) - } - } - - case "CatchClause": - // Check if it's empty catch or generic exception - if where, ok := rule.Check["where"].(map[string]interface{}); ok { - // Empty catch block - if size, ok := where["body.statements.size"].(float64); ok && size == 0 { - rules = append(rules, PMDRule{ - Ref: "category/java/errorprone.xml/EmptyCatchBlock", - Priority: 3, - }) - } - // Generic Exception catch - if paramType, ok := where["parameter.type.name"].(string); ok && paramType == "Exception" { - rules = append(rules, PMDRule{ - Ref: "category/java/design.xml/AvoidCatchingGenericException", - Priority: 3, - }) - } - } - - case "MethodDeclaration": - // Check for missing Javadoc - if where, ok := rule.Check["where"].(map[string]interface{}); ok { - if isPublic, ok := where["isPublic"].(bool); ok && isPublic { - if hasJavadoc, ok := where["hasJavadoc"].(bool); ok && !hasJavadoc { - rules = append(rules, PMDRule{ - Ref: "category/java/documentation.xml/CommentRequired", - Priority: 3, - Properties: []PMDProperty{ - {Name: "methodWithOverrideCommentRequirement", Value: "Ignored"}, - {Name: "accessorCommentRequirement", Value: "Ignored"}, - {Name: "classCommentRequirement", Value: "Ignored"}, - {Name: "fieldCommentRequirement", Value: "Ignored"}, - {Name: "publicMethodCommentRequirement", Value: "Required"}, - {Name: "protectedMethodCommentRequirement", Value: "Ignored"}, - {Name: "enumCommentRequirement", Value: "Ignored"}, - {Name: "violationSuppressRegex", Value: ".*main\\(.*"}, - }, - }) - } - } - } - } - - return rules -} diff --git a/internal/adapter/prettier/adapter.go b/internal/adapter/prettier/adapter.go index 1228d92..f62b78b 100644 --- a/internal/adapter/prettier/adapter.go +++ b/internal/adapter/prettier/adapter.go @@ -96,10 +96,6 @@ func (a *Adapter) Install(ctx context.Context, config adapter.InstallConfig) err return err } -// GenerateConfig generates Prettier config from a rule. -func (a *Adapter) GenerateConfig(rule interface{}) ([]byte, error) { - return generateConfig(rule) -} // Execute runs Prettier with the given config and files. // mode: "check" or "write" diff --git a/internal/adapter/prettier/config.go b/internal/adapter/prettier/config.go deleted file mode 100644 index a6416bf..0000000 --- a/internal/adapter/prettier/config.go +++ /dev/null @@ -1,60 +0,0 @@ -package prettier - -import ( - "encoding/json" - "fmt" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// PrettierConfig represents .prettierrc.json structure. -type PrettierConfig struct { - TabWidth int `json:"tabWidth,omitempty"` - UseTabs bool `json:"useTabs,omitempty"` - Semi bool `json:"semi,omitempty"` - SingleQuote bool `json:"singleQuote,omitempty"` - TrailingComma string `json:"trailingComma,omitempty"` // "none", "es5", "all" - PrintWidth int `json:"printWidth,omitempty"` -} - -// generateConfig creates Prettier config from a Symphony rule. -func generateConfig(ruleInterface interface{}) ([]byte, error) { - rule, ok := ruleInterface.(*core.Rule) - if !ok { - return nil, fmt.Errorf("expected *core.Rule, got %T", ruleInterface) - } - - config := &PrettierConfig{} - - // Map Symphony style config to Prettier options - if indent := rule.GetInt("indent"); indent > 0 { - config.TabWidth = indent - config.UseTabs = false // Default to spaces - } - - if quote := rule.GetString("quote"); quote != "" { - config.SingleQuote = (quote == "single") - } - - // Semi is tricky - need to check if it exists in Check - if _, ok := rule.Check["semi"]; ok { - config.Semi = rule.GetBool("semi") - } - - if trailingComma := rule.GetString("trailingComma"); trailingComma != "" { - config.TrailingComma = trailingComma - } else { - config.TrailingComma = "es5" // Default - } - - // Line length - if printWidth := rule.GetInt("printWidth"); printWidth > 0 { - config.PrintWidth = printWidth - } else if maxLen := rule.GetInt("max"); maxLen > 0 { - config.PrintWidth = maxLen - } else { - config.PrintWidth = 100 // Default - } - - return json.MarshalIndent(config, "", " ") -} diff --git a/internal/adapter/registry/errors.go b/internal/adapter/registry/errors.go index 683013a..aff2741 100644 --- a/internal/adapter/registry/errors.go +++ b/internal/adapter/registry/errors.go @@ -2,23 +2,13 @@ package registry import "fmt" -// ErrAdapterNotFound is returned when no adapter is found for the given criteria. +// ErrAdapterNotFound is returned when no adapter is found for the given tool name. type ErrAdapterNotFound struct { - Language string - Category string + ToolName string } func (e *ErrAdapterNotFound) Error() string { - return fmt.Sprintf("no adapter found for language=%s category=%s", e.Language, e.Category) -} - -// ErrLanguageNotSupported is returned when a language is not supported by any adapter. -type ErrLanguageNotSupported struct { - Language string -} - -func (e *ErrLanguageNotSupported) Error() string { - return fmt.Sprintf("language %s is not supported by any adapter", e.Language) + return fmt.Sprintf("adapter not found: %s", e.ToolName) } // ErrNilAdapter is returned when trying to register a nil adapter. diff --git a/internal/adapter/tsc/config.go b/internal/adapter/tsc/config.go deleted file mode 100644 index 85c6a14..0000000 --- a/internal/adapter/tsc/config.go +++ /dev/null @@ -1,131 +0,0 @@ -package tsc - -import ( - "encoding/json" - "fmt" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -// TSConfig represents TypeScript compiler configuration. -type TSConfig struct { - CompilerOptions CompilerOptions `json:"compilerOptions"` - Include []string `json:"include,omitempty"` - Exclude []string `json:"exclude,omitempty"` -} - -// CompilerOptions represents TypeScript compiler options. -type CompilerOptions struct { - Target string `json:"target,omitempty"` - Module string `json:"module,omitempty"` - Lib []string `json:"lib,omitempty"` - Strict bool `json:"strict,omitempty"` - NoImplicitAny bool `json:"noImplicitAny,omitempty"` - StrictNullChecks bool `json:"strictNullChecks,omitempty"` - StrictFunctionTypes bool `json:"strictFunctionTypes,omitempty"` - StrictBindCallApply bool `json:"strictBindCallApply,omitempty"` - StrictPropertyInit bool `json:"strictPropertyInitialization,omitempty"` - NoImplicitThis bool `json:"noImplicitThis,omitempty"` - AlwaysStrict bool `json:"alwaysStrict,omitempty"` - NoUnusedLocals bool `json:"noUnusedLocals,omitempty"` - NoUnusedParameters bool `json:"noUnusedParameters,omitempty"` - NoImplicitReturns bool `json:"noImplicitReturns,omitempty"` - NoFallthroughCasesInSwitch bool `json:"noFallthroughCasesInSwitch,omitempty"` - SkipLibCheck bool `json:"skipLibCheck,omitempty"` - ESModuleInterop bool `json:"esModuleInterop,omitempty"` - AllowJS bool `json:"allowJs,omitempty"` - CheckJS bool `json:"checkJs,omitempty"` -} - -// GenerateConfig generates a tsconfig.json from a rule. -func (a *Adapter) GenerateConfig(ruleInterface interface{}) ([]byte, error) { - // Type assert to *core.Rule - rule, ok := ruleInterface.(*core.Rule) - if !ok { - return nil, fmt.Errorf("expected *core.Rule, got %T", ruleInterface) - } - - // Default configuration for type checking - config := TSConfig{ - CompilerOptions: CompilerOptions{ - Target: "ES2020", - Module: "commonjs", - Lib: []string{"ES2020"}, - Strict: true, - NoImplicitAny: true, - StrictNullChecks: true, - StrictFunctionTypes: true, - StrictBindCallApply: true, - StrictPropertyInit: true, - NoImplicitThis: true, - AlwaysStrict: true, - NoUnusedLocals: false, // Don't fail on unused locals - NoUnusedParameters: false, // Don't fail on unused params - NoImplicitReturns: true, - NoFallthroughCasesInSwitch: true, - SkipLibCheck: true, // Skip type checking of declaration files - ESModuleInterop: true, - AllowJS: false, // Only check TypeScript by default - CheckJS: false, - }, - } - - // Apply rule-specific configuration from Check map - applyRuleConfig(&config, rule.Check) - - return json.MarshalIndent(config, "", " ") -} - -// applyRuleConfig applies rule-specific configuration to TSConfig. -func applyRuleConfig(config *TSConfig, check map[string]interface{}) { - // Extract compiler options from rule - if strict, ok := check["strict"].(bool); ok { - config.CompilerOptions.Strict = strict - } - - if noImplicitAny, ok := check["noImplicitAny"].(bool); ok { - config.CompilerOptions.NoImplicitAny = noImplicitAny - } - - if strictNullChecks, ok := check["strictNullChecks"].(bool); ok { - config.CompilerOptions.StrictNullChecks = strictNullChecks - } - - if allowJS, ok := check["allowJs"].(bool); ok { - config.CompilerOptions.AllowJS = allowJS - } - - if checkJS, ok := check["checkJs"].(bool); ok { - config.CompilerOptions.CheckJS = checkJS - } - - // Extract file patterns - if include, ok := check["include"].([]interface{}); ok { - patterns := make([]string, 0, len(include)) - for _, p := range include { - if str, ok := p.(string); ok { - patterns = append(patterns, str) - } - } - config.Include = patterns - } - - if exclude, ok := check["exclude"].([]interface{}); ok { - patterns := make([]string, 0, len(exclude)) - for _, p := range exclude { - if str, ok := p.(string); ok { - patterns = append(patterns, str) - } - } - config.Exclude = patterns - } -} - -// MarshalConfig marshals a config map to JSON. -func MarshalConfig(config interface{}) ([]byte, error) { - data, err := json.MarshalIndent(config, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal config: %w", err) - } - return data, nil -} From a0dedbac7d306364c18c89a494d616db01a00a06 Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 11:58:23 +0900 Subject: [PATCH 05/12] test: add GetCapabilities test and remove deprecated config tests --- internal/adapter/eslint/adapter_test.go | 106 ++----- internal/adapter/eslint/config_test.go | 234 --------------- internal/adapter/eslint/executor_test.go | 14 +- internal/adapter/prettier/adapter_test.go | 21 -- internal/adapter/prettier/config_test.go | 88 ------ internal/adapter/prettier/executor_test.go | 14 +- internal/adapter/tsc/adapter_test.go | 16 +- internal/adapter/tsc/config_test.go | 226 --------------- tests/integration/helper.go | 12 - tests/integration/validator_policy_test.go | 319 --------------------- 10 files changed, 40 insertions(+), 1010 deletions(-) delete mode 100644 internal/adapter/eslint/config_test.go delete mode 100644 internal/adapter/prettier/config_test.go delete mode 100644 internal/adapter/tsc/config_test.go delete mode 100644 tests/integration/validator_policy_test.go diff --git a/internal/adapter/eslint/adapter_test.go b/internal/adapter/eslint/adapter_test.go index 2dda823..d4e8c21 100644 --- a/internal/adapter/eslint/adapter_test.go +++ b/internal/adapter/eslint/adapter_test.go @@ -4,10 +4,10 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "github.com/DevSymphony/sym-cli/internal/adapter" - "github.com/DevSymphony/sym-cli/internal/engine/core" ) func TestNewAdapter(t *testing.T) { @@ -43,6 +43,29 @@ func TestName(t *testing.T) { } } +func TestGetCapabilities(t *testing.T) { + a := NewAdapter("", "") + caps := a.GetCapabilities() + + if caps.Name != "eslint" { + t.Errorf("GetCapabilities().Name = %q, want %q", caps.Name, "eslint") + } + + expectedLangs := []string{"javascript", "typescript", "jsx", "tsx"} + for _, lang := range expectedLangs { + found := false + for _, supported := range caps.SupportedLanguages { + if supported == lang { + found = true + break + } + } + if !found { + t.Errorf("GetCapabilities() missing language: %s", lang) + } + } +} + func TestGetESLintPath(t *testing.T) { a := NewAdapter("/test/tools", "") expected := filepath.Join("/test/tools", "node_modules", ".bin", "eslint") @@ -78,7 +101,7 @@ func TestInitPackageJSON(t *testing.T) { expectedFields := []string{`"name"`, `"symphony-tools"`} for _, field := range expectedFields { - if !contains(string(content), field) { + if !strings.Contains(string(content), field) { t.Errorf("package.json missing expected field: %s", field) } } @@ -115,30 +138,6 @@ func TestInstall(t *testing.T) { } } -func TestGenerateConfig(t *testing.T) { - a := NewAdapter("", "") - - rule := &core.Rule{ - ID: "TEST-RULE", - Category: "naming", - Severity: "error", - Check: map[string]interface{}{ - "engine": "pattern", - "target": "identifier", - "pattern": "^[A-Z]", - }, - } - - config, err := a.GenerateConfig(rule) - if err != nil { - t.Fatalf("GenerateConfig() error = %v", err) - } - - if len(config) == 0 { - t.Error("GenerateConfig() returned empty config") - } -} - func TestExecute_InvalidConfig(t *testing.T) { a := NewAdapter("", t.TempDir()) @@ -156,8 +155,8 @@ func TestParseOutput(t *testing.T) { a := NewAdapter("", "") output := &adapter.ToolOutput{ - Stdout: `[{"filePath":"test.js","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'x' is defined but never used","line":1,"column":5}]}]`, - Stderr: "", + Stdout: `[{"filePath":"test.js","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'x' is defined but never used","line":1,"column":5}]}]`, + Stderr: "", ExitCode: 1, } @@ -171,54 +170,3 @@ func TestParseOutput(t *testing.T) { } } -func TestMapSeverity(t *testing.T) { - tests := []struct { - input string - want string - }{ - {"error", "error"}, - {"warning", "warn"}, - {"info", "off"}, - {"unknown", "error"}, // default - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - got := MapSeverity(tt.input) - if got != tt.want { - t.Errorf("MapSeverity(%q) = %v, want %v", tt.input, got, tt.want) - } - }) - } -} - -func TestMarshalConfig(t *testing.T) { - config := map[string]interface{}{ - "rules": map[string]interface{}{ - "semi": []interface{}{2, "always"}, - }, - } - - data, err := MarshalConfig(config) - if err != nil { - t.Fatalf("MarshalConfig() error = %v", err) - } - - if len(data) == 0 { - t.Error("MarshalConfig() returned empty data") - } -} - -// Helper function -func contains(s, substr string) bool { - return len(s) > 0 && len(substr) > 0 && findSubstring(s, substr) -} - -func findSubstring(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} diff --git a/internal/adapter/eslint/config_test.go b/internal/adapter/eslint/config_test.go deleted file mode 100644 index 637acdd..0000000 --- a/internal/adapter/eslint/config_test.go +++ /dev/null @@ -1,234 +0,0 @@ -package eslint - -import ( - "encoding/json" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestGenerateConfig_Pattern(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-PATTERN", - Category: "naming", - Severity: "error", - Check: map[string]interface{}{ - "engine": "pattern", - "target": "identifier", - "pattern": "^[A-Z][a-zA-Z0-9]*$", - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var eslintConfig ESLintConfig - if err := json.Unmarshal(config, &eslintConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if _, ok := eslintConfig.Rules["id-match"]; !ok { - t.Error("expected id-match rule to be set") - } -} - -func TestGenerateConfig_Length(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-LENGTH", - Category: "formatting", - Severity: "error", - Check: map[string]interface{}{ - "engine": "length", - "scope": "line", - "max": 100, - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var eslintConfig ESLintConfig - if err := json.Unmarshal(config, &eslintConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if _, ok := eslintConfig.Rules["max-len"]; !ok { - t.Error("expected max-len rule to be set") - } -} - -func TestGenerateConfig_Style(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-STYLE", - Category: "style", - Severity: "error", - Check: map[string]interface{}{ - "engine": "style", - "indent": 2, - "quote": "single", - "semi": true, - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var eslintConfig ESLintConfig - if err := json.Unmarshal(config, &eslintConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if _, ok := eslintConfig.Rules["indent"]; !ok { - t.Error("expected indent rule to be set") - } - - if _, ok := eslintConfig.Rules["quotes"]; !ok { - t.Error("expected quotes rule to be set") - } - - if _, ok := eslintConfig.Rules["semi"]; !ok { - t.Error("expected semi rule to be set") - } -} - -func TestGenerateConfig_PatternContent(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-PATTERN-CONTENT", - Category: "security", - Severity: "error", - Check: map[string]interface{}{ - "engine": "pattern", - "target": "content", - "pattern": "password", - }, - Message: "No hardcoded passwords", - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var eslintConfig ESLintConfig - if err := json.Unmarshal(config, &eslintConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if _, ok := eslintConfig.Rules["no-restricted-syntax"]; !ok { - t.Error("expected no-restricted-syntax rule to be set") - } -} - -func TestGenerateConfig_PatternImport(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-PATTERN-IMPORT", - Category: "dependency", - Severity: "error", - Check: map[string]interface{}{ - "engine": "pattern", - "target": "import", - "pattern": "^forbidden-package", - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var eslintConfig ESLintConfig - if err := json.Unmarshal(config, &eslintConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if _, ok := eslintConfig.Rules["no-restricted-imports"]; !ok { - t.Error("expected no-restricted-imports rule to be set") - } -} - -func TestGenerateConfig_LengthFile(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-LENGTH-FILE", - Category: "formatting", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "length", - "scope": "file", - "max": 500, - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var eslintConfig ESLintConfig - if err := json.Unmarshal(config, &eslintConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if _, ok := eslintConfig.Rules["max-lines"]; !ok { - t.Error("expected max-lines rule to be set") - } -} - -func TestGenerateConfig_LengthFunction(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-LENGTH-FUNCTION", - Category: "formatting", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "length", - "scope": "function", - "max": 50, - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var eslintConfig ESLintConfig - if err := json.Unmarshal(config, &eslintConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if _, ok := eslintConfig.Rules["max-lines-per-function"]; !ok { - t.Error("expected max-lines-per-function rule to be set") - } -} - -func TestGenerateConfig_LengthParams(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-LENGTH-PARAMS", - Category: "formatting", - Severity: "warning", - Check: map[string]interface{}{ - "engine": "length", - "scope": "params", - "max": 4, - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var eslintConfig ESLintConfig - if err := json.Unmarshal(config, &eslintConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if _, ok := eslintConfig.Rules["max-params"]; !ok { - t.Error("expected max-params rule to be set") - } -} diff --git a/internal/adapter/eslint/executor_test.go b/internal/adapter/eslint/executor_test.go index 22f8891..61196d2 100644 --- a/internal/adapter/eslint/executor_test.go +++ b/internal/adapter/eslint/executor_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "github.com/DevSymphony/sym-cli/internal/adapter" @@ -14,7 +15,7 @@ func TestExecute_FileCreation(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer func() { _ = os.RemoveAll(tmpDir) }() + defer os.RemoveAll(tmpDir) a := NewAdapter("", tmpDir) @@ -54,10 +55,7 @@ func TestGetESLintCommand(t *testing.T) { cmd := a.getESLintCommand() if tt.wantContain != "" && len(cmd) > 0 { - contains := false - if len(cmd) > 0 && findSubstring(cmd, tt.wantContain) { - contains = true - } + contains := strings.Contains(cmd, tt.wantContain) if !contains && tt.toolsDir != "" { t.Logf("Command %q doesn't contain %q (may use global)", cmd, tt.wantContain) } @@ -108,7 +106,7 @@ func TestWriteConfigFile(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer func() { _ = os.RemoveAll(tmpDir) }() + defer os.RemoveAll(tmpDir) a := NewAdapter("", tmpDir) @@ -118,7 +116,7 @@ func TestWriteConfigFile(t *testing.T) { if err != nil { t.Fatalf("writeConfigFile() error = %v", err) } - defer func() { _ = os.Remove(configPath) }() + defer os.Remove(configPath) if _, err := os.Stat(configPath); os.IsNotExist(err) { t.Error("Config file was not created") @@ -143,7 +141,7 @@ func TestExecute_Integration(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer func() { _ = os.RemoveAll(tmpDir) }() + defer os.RemoveAll(tmpDir) // Create a test file testFile := filepath.Join(tmpDir, "test.js") diff --git a/internal/adapter/prettier/adapter_test.go b/internal/adapter/prettier/adapter_test.go index a5558f8..78ffb21 100644 --- a/internal/adapter/prettier/adapter_test.go +++ b/internal/adapter/prettier/adapter_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/DevSymphony/sym-cli/internal/adapter" - "github.com/DevSymphony/sym-cli/internal/engine/core" ) func TestNewAdapter(t *testing.T) { @@ -103,26 +102,6 @@ func TestInstall(t *testing.T) { } } -func TestGenerateConfig(t *testing.T) { - a := NewAdapter("", "") - - rule := &core.Rule{ - Check: map[string]interface{}{ - "indent": 2, - "quote": "single", - }, - } - - config, err := a.GenerateConfig(rule) - if err != nil { - t.Fatalf("GenerateConfig() error = %v", err) - } - - if len(config) == 0 { - t.Error("GenerateConfig() returned empty config") - } -} - func TestExecute(t *testing.T) { a := NewAdapter("", t.TempDir()) diff --git a/internal/adapter/prettier/config_test.go b/internal/adapter/prettier/config_test.go deleted file mode 100644 index 623ee75..0000000 --- a/internal/adapter/prettier/config_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package prettier - -import ( - "encoding/json" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestGenerateConfig_Basic(t *testing.T) { - rule := &core.Rule{ - ID: "TEST-STYLE", - Category: "style", - Severity: "error", - Check: map[string]interface{}{ - "engine": "style", - "indent": 2, - "quote": "single", - "semi": true, - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var prettierConfig PrettierConfig - if err := json.Unmarshal(config, &prettierConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if prettierConfig.TabWidth != 2 { - t.Errorf("tabWidth = %d, want 2", prettierConfig.TabWidth) - } - - if !prettierConfig.SingleQuote { - t.Error("singleQuote = false, want true") - } - - if !prettierConfig.Semi { - t.Error("semi = false, want true") - } -} - -func TestGenerateConfig_DoubleQuotes(t *testing.T) { - rule := &core.Rule{ - Check: map[string]interface{}{ - "quote": "double", - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var prettierConfig PrettierConfig - if err := json.Unmarshal(config, &prettierConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if prettierConfig.SingleQuote { - t.Error("singleQuote = true, want false (for double quotes)") - } -} - -func TestGenerateConfig_PrintWidth(t *testing.T) { - rule := &core.Rule{ - Check: map[string]interface{}{ - "printWidth": 120, - }, - } - - config, err := generateConfig(rule) - if err != nil { - t.Fatalf("generateConfig failed: %v", err) - } - - var prettierConfig PrettierConfig - if err := json.Unmarshal(config, &prettierConfig); err != nil { - t.Fatalf("failed to parse config: %v", err) - } - - if prettierConfig.PrintWidth != 120 { - t.Errorf("printWidth = %d, want 120", prettierConfig.PrintWidth) - } -} diff --git a/internal/adapter/prettier/executor_test.go b/internal/adapter/prettier/executor_test.go index 27b3ecf..7f16e2a 100644 --- a/internal/adapter/prettier/executor_test.go +++ b/internal/adapter/prettier/executor_test.go @@ -12,7 +12,7 @@ func TestExecute_FileCreation(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer func() { _ = os.RemoveAll(tmpDir) }() + defer os.RemoveAll(tmpDir) a := NewAdapter("", tmpDir) @@ -60,7 +60,7 @@ func TestWriteConfigFile(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer func() { _ = os.RemoveAll(tmpDir) }() + defer os.RemoveAll(tmpDir) a := NewAdapter("", tmpDir) @@ -70,7 +70,7 @@ func TestWriteConfigFile(t *testing.T) { if err != nil { t.Fatalf("writeConfigFile() error = %v", err) } - defer func() { _ = os.Remove(configPath) }() + defer os.Remove(configPath) if _, err := os.Stat(configPath); os.IsNotExist(err) { t.Error("Config file was not created") @@ -95,7 +95,7 @@ func TestExecute_Integration(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer func() { _ = os.RemoveAll(tmpDir) }() + defer os.RemoveAll(tmpDir) // Create a test file with bad formatting testFile := filepath.Join(tmpDir, "test.js") @@ -117,10 +117,6 @@ func TestExecute_Integration(t *testing.T) { } if output == nil { - t.Skip("Prettier not available in test environment") - return + t.Error("Expected non-nil output") } - - // If we got here, Prettier is available and returned output - t.Logf("Prettier executed successfully, exit code: %d", output.ExitCode) } diff --git a/internal/adapter/tsc/adapter_test.go b/internal/adapter/tsc/adapter_test.go index 2d815e9..8b63caf 100644 --- a/internal/adapter/tsc/adapter_test.go +++ b/internal/adapter/tsc/adapter_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "github.com/DevSymphony/sym-cli/internal/adapter" @@ -87,7 +88,7 @@ func TestInitPackageJSON(t *testing.T) { } for _, field := range expectedFields { - if !contains(string(content), field) { + if !strings.Contains(string(content), field) { t.Errorf("package.json missing expected field: %s", field) } } @@ -177,16 +178,3 @@ src/app.ts(20,10): error TS2339: Property 'bar' does not exist on type 'Object'. } } -// Helper function -func contains(s, substr string) bool { - return len(s) > 0 && len(substr) > 0 && findSubstring(s, substr) -} - -func findSubstring(s, substr string) bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} diff --git a/internal/adapter/tsc/config_test.go b/internal/adapter/tsc/config_test.go deleted file mode 100644 index 66ca839..0000000 --- a/internal/adapter/tsc/config_test.go +++ /dev/null @@ -1,226 +0,0 @@ -package tsc - -import ( - "encoding/json" - "testing" - - "github.com/DevSymphony/sym-cli/internal/engine/core" -) - -func TestGenerateConfig_Default(t *testing.T) { - adapter := NewAdapter("", "/test/project") - - rule := &core.Rule{ - ID: "test-default", - Check: map[string]interface{}{}, - } - - config, err := adapter.GenerateConfig(rule) - if err != nil { - t.Fatalf("GenerateConfig() error = %v", err) - } - - var tsconfig TSConfig - if err := json.Unmarshal(config, &tsconfig); err != nil { - t.Fatalf("Failed to unmarshal config: %v", err) - } - - // Verify default options - if !tsconfig.CompilerOptions.Strict { - t.Error("Expected strict mode to be enabled by default") - } - - if !tsconfig.CompilerOptions.NoImplicitAny { - t.Error("Expected noImplicitAny to be enabled by default") - } - - if !tsconfig.CompilerOptions.StrictNullChecks { - t.Error("Expected strictNullChecks to be enabled by default") - } - - if tsconfig.CompilerOptions.Target != "ES2020" { - t.Errorf("Target = %q, want %q", tsconfig.CompilerOptions.Target, "ES2020") - } - - if tsconfig.CompilerOptions.AllowJS { - t.Error("Expected allowJs to be false by default") - } -} - -func TestGenerateConfig_WithRuleOptions(t *testing.T) { - adapter := NewAdapter("", "/test/project") - - rule := &core.Rule{ - ID: "test-with-options", - Check: map[string]interface{}{ - "strict": false, - "noImplicitAny": false, - "allowJs": true, - "checkJs": true, - "strictNullChecks": false, - }, - } - - config, err := adapter.GenerateConfig(rule) - if err != nil { - t.Fatalf("GenerateConfig() error = %v", err) - } - - var tsconfig TSConfig - if err := json.Unmarshal(config, &tsconfig); err != nil { - t.Fatalf("Failed to unmarshal config: %v", err) - } - - // Verify custom options - if tsconfig.CompilerOptions.Strict { - t.Error("Expected strict mode to be disabled") - } - - if tsconfig.CompilerOptions.NoImplicitAny { - t.Error("Expected noImplicitAny to be disabled") - } - - if !tsconfig.CompilerOptions.AllowJS { - t.Error("Expected allowJs to be enabled") - } - - if !tsconfig.CompilerOptions.CheckJS { - t.Error("Expected checkJs to be enabled") - } - - if tsconfig.CompilerOptions.StrictNullChecks { - t.Error("Expected strictNullChecks to be disabled") - } -} - -func TestGenerateConfig_WithIncludeExclude(t *testing.T) { - adapter := NewAdapter("", "/test/project") - - rule := &core.Rule{ - ID: "test-include-exclude", - Check: map[string]interface{}{ - "include": []interface{}{"src/**/*.ts", "lib/**/*.ts"}, - "exclude": []interface{}{"**/*.test.ts", "dist/**"}, - }, - } - - config, err := adapter.GenerateConfig(rule) - if err != nil { - t.Fatalf("GenerateConfig() error = %v", err) - } - - var tsconfig TSConfig - if err := json.Unmarshal(config, &tsconfig); err != nil { - t.Fatalf("Failed to unmarshal config: %v", err) - } - - // Verify include patterns - if len(tsconfig.Include) != 2 { - t.Errorf("Include has %d patterns, want 2", len(tsconfig.Include)) - } - - if tsconfig.Include[0] != "src/**/*.ts" { - t.Errorf("Include[0] = %q, want %q", tsconfig.Include[0], "src/**/*.ts") - } - - // Verify exclude patterns - if len(tsconfig.Exclude) != 2 { - t.Errorf("Exclude has %d patterns, want 2", len(tsconfig.Exclude)) - } - - if tsconfig.Exclude[0] != "**/*.test.ts" { - t.Errorf("Exclude[0] = %q, want %q", tsconfig.Exclude[0], "**/*.test.ts") - } -} - -func TestGenerateConfig_ValidJSON(t *testing.T) { - adapter := NewAdapter("", "/test/project") - - rule := &core.Rule{ - ID: "test-valid-json", - Check: map[string]interface{}{}, - } - - config, err := adapter.GenerateConfig(rule) - if err != nil { - t.Fatalf("GenerateConfig() error = %v", err) - } - - // Verify it's valid JSON - var result map[string]interface{} - if err := json.Unmarshal(config, &result); err != nil { - t.Errorf("Generated config is not valid JSON: %v", err) - } - - // Verify it has compilerOptions - if _, ok := result["compilerOptions"]; !ok { - t.Error("Config missing compilerOptions field") - } -} - -func TestMarshalConfig(t *testing.T) { - config := map[string]interface{}{ - "compilerOptions": map[string]interface{}{ - "target": "ES2020", - "strict": true, - }, - } - - data, err := MarshalConfig(config) - if err != nil { - t.Fatalf("MarshalConfig() error = %v", err) - } - - // Verify it's valid JSON - var result map[string]interface{} - if err := json.Unmarshal(data, &result); err != nil { - t.Errorf("Marshaled config is not valid JSON: %v", err) - } -} - -func TestApplyRuleConfig_AllOptions(t *testing.T) { - config := &TSConfig{ - CompilerOptions: CompilerOptions{ - Strict: true, - NoImplicitAny: true, - StrictNullChecks: true, - AllowJS: false, - CheckJS: false, - }, - } - - check := map[string]interface{}{ - "strict": false, - "noImplicitAny": false, - "strictNullChecks": false, - "allowJs": true, - "checkJs": true, - "include": []interface{}{"src/**"}, - "exclude": []interface{}{"dist/**"}, - } - - applyRuleConfig(config, check) - - // Verify all options were applied - if config.CompilerOptions.Strict { - t.Error("strict should be false") - } - if config.CompilerOptions.NoImplicitAny { - t.Error("noImplicitAny should be false") - } - if config.CompilerOptions.StrictNullChecks { - t.Error("strictNullChecks should be false") - } - if !config.CompilerOptions.AllowJS { - t.Error("allowJs should be true") - } - if !config.CompilerOptions.CheckJS { - t.Error("checkJs should be true") - } - if len(config.Include) != 1 || config.Include[0] != "src/**" { - t.Error("include pattern not applied correctly") - } - if len(config.Exclude) != 1 || config.Exclude[0] != "dist/**" { - t.Error("exclude pattern not applied correctly") - } -} diff --git a/tests/integration/helper.go b/tests/integration/helper.go index f5b02cd..0caba76 100644 --- a/tests/integration/helper.go +++ b/tests/integration/helper.go @@ -28,18 +28,6 @@ func getTestdataDir(t *testing.T) string { return projectRoot } -// getToolsDir returns the path to tools directory for test -func getToolsDir(t *testing.T) string { - t.Helper() - - home, err := os.UserHomeDir() - if err != nil { - t.Fatalf("Failed to get home directory: %v", err) - } - - return filepath.Join(home, ".symphony", "tools") -} - // loadPolicyFromTestdata loads a code-policy.json from testdata directory func loadPolicyFromTestdata(t *testing.T, relativePath string) *schema.CodePolicy { t.Helper() diff --git a/tests/integration/validator_policy_test.go b/tests/integration/validator_policy_test.go deleted file mode 100644 index a36a576..0000000 --- a/tests/integration/validator_policy_test.go +++ /dev/null @@ -1,319 +0,0 @@ -package integration - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -// ============================================================================ -// JavaScript Pattern Tests -// ============================================================================ - -func TestValidator_JavaScript_Pattern_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - // Load policy from testdata - policy := loadPolicyFromTestdata(t, "testdata/javascript/pattern/code-policy.json") - require.Equal(t, 4, len(policy.Rules), "Should have 4 rules: 3 naming + 1 security") - - // Create validator - v := createTestValidator(t, policy) - - // Test naming violations - t.Run("NamingViolations", func(t *testing.T) { - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/pattern/naming-violations.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) - }) - - // Test security violations - t.Run("SecurityViolations", func(t *testing.T) { - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/pattern/security-violations.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) - }) -} - -func TestValidator_JavaScript_Pattern_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/javascript/pattern/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/pattern/valid.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} - -// ============================================================================ -// JavaScript Length Tests -// ============================================================================ - -func TestValidator_JavaScript_Length_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/javascript/length/code-policy.json") - require.Equal(t, 3, len(policy.Rules), "Should have 3 rules: line/function/params length") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/length/length-violations.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) -} - -func TestValidator_JavaScript_Length_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/javascript/length/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/length/valid.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} - -// ============================================================================ -// JavaScript Style Tests -// ============================================================================ - -func TestValidator_JavaScript_Style_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/javascript/style/code-policy.json") - require.GreaterOrEqual(t, len(policy.Rules), 3, "Should have at least 3 style rules") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/style/style-violations.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) -} - -func TestValidator_JavaScript_Style_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/javascript/style/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/style/valid.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} - -// ============================================================================ -// JavaScript AST Tests -// ============================================================================ - -func TestValidator_JavaScript_AST_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/javascript/ast/code-policy.json") - require.Equal(t, 3, len(policy.Rules), "Should have 3 AST rules for naming") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/ast/naming-violations.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) -} - -func TestValidator_JavaScript_AST_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/javascript/ast/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/javascript/ast/valid.js") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} - -// ============================================================================ -// TypeScript TypeChecker Tests -// ============================================================================ - -func TestValidator_TypeScript_TypeChecker_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/typescript/typechecker/code-policy.json") - require.Equal(t, 3, len(policy.Rules), "Should have 3 type checking rules") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/typescript/typechecker/type-errors.ts") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) -} - -func TestValidator_TypeScript_TypeChecker_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/typescript/typechecker/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/typescript/typechecker/valid.ts") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} - -// ============================================================================ -// Java Pattern Tests -// ============================================================================ - -func TestValidator_Java_Pattern_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/java/pattern/code-policy.json") - require.Equal(t, 5, len(policy.Rules), "Should have 5 naming rules") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/java/pattern/NamingViolations.java") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) -} - -func TestValidator_Java_Pattern_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/java/pattern/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/java/pattern/ValidNaming.java") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} - -// ============================================================================ -// Java Length Tests -// ============================================================================ - -func TestValidator_Java_Length_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/java/length/code-policy.json") - require.Equal(t, 3, len(policy.Rules), "Should have 3 length rules") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/java/length/LengthViolations.java") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) -} - -func TestValidator_Java_Length_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/java/length/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/java/length/ValidLength.java") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} - -// ============================================================================ -// Java Style Tests -// ============================================================================ - -func TestValidator_Java_Style_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/java/style/code-policy.json") - require.GreaterOrEqual(t, len(policy.Rules), 5, "Should have at least 5 style rules") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/java/style/StyleViolations.java") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) -} - -func TestValidator_Java_Style_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/java/style/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/java/style/ValidStyle.java") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} - -// ============================================================================ -// Java AST Tests -// ============================================================================ - -func TestValidator_Java_AST_Violations(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/java/ast/code-policy.json") - require.Equal(t, 4, len(policy.Rules), "Should have 4 AST rules") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/java/ast/AstViolations.java") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertViolationsDetected(t, result) -} - -func TestValidator_Java_AST_Valid(t *testing.T) { - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - policy := loadPolicyFromTestdata(t, "testdata/java/ast/code-policy.json") - v := createTestValidator(t, policy) - - filePath := filepath.Join(getTestdataDir(t), "testdata/java/ast/ValidAst.java") - result, err := v.Validate(filePath) - require.NoError(t, err) - assertNoPolicyViolations(t, result) -} From e539f16e3b24c256170c714b66bc3578ee06c485 Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 12:01:38 +0900 Subject: [PATCH 06/12] fix: simplify string concatenation in validation results message --- internal/mcp/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index f753f0a..6722c10 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -690,7 +690,7 @@ func (s *Server) handleValidateCode(params map[string]interface{}) (interface{}, } // Add note about saved results - textContent += fmt.Sprintf("\n💾 Validation results saved to .sym/validation-results.json\n") + textContent += "\n💾 Validation results saved to .sym/validation-results.json\n" // Return MCP-compliant response with content array return map[string]interface{}{ From 99042f80b3f22986419ba7f4ef27be9cfae4400d Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 12:32:37 +0900 Subject: [PATCH 07/12] feat: enhance validation output with execution details --- internal/validator/validator.go | 105 ++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 11 deletions(-) diff --git a/internal/validator/validator.go b/internal/validator/validator.go index 2c14bf9..9ebadd6 100644 --- a/internal/validator/validator.go +++ b/internal/validator/validator.go @@ -26,6 +26,11 @@ type Violation struct { File string Line int Column int + // Raw output from linter/validator + RawOutput string // stdout from adapter execution + RawError string // stderr from adapter execution + ToolName string // which tool detected this (eslint, prettier, llm-validator, etc.) + ExecutionMs int64 // execution time in milliseconds } // Result represents validation result @@ -197,6 +202,14 @@ func (v *Validator) executeRule(engineName string, rule schema.PolicyRule, files return nil, fmt.Errorf("adapter execution failed: %w", err) } + // Parse execution duration + var execMs int64 + if output.Duration != "" { + if duration, parseErr := time.ParseDuration(output.Duration); parseErr == nil { + execMs = duration.Milliseconds() + } + } + // Parse output to violations adapterViolations, err := adp.ParseOutput(output) if err != nil { @@ -207,15 +220,29 @@ func (v *Validator) executeRule(engineName string, rule schema.PolicyRule, files violations := make([]Violation, 0, len(adapterViolations)) for _, av := range adapterViolations { violations = append(violations, Violation{ - RuleID: rule.ID, - Severity: rule.Severity, - Message: av.Message, - File: av.File, - Line: av.Line, - Column: av.Column, + RuleID: rule.ID, + Severity: rule.Severity, + Message: av.Message, + File: av.File, + Line: av.Line, + Column: av.Column, + RawOutput: output.Stdout, + RawError: output.Stderr, + ToolName: adp.Name(), + ExecutionMs: execMs, }) } + // If verbose, log the raw output + if v.verbose && output.Stdout != "" { + fmt.Printf(" 📋 Raw output (%dms):\n", execMs) + if len(output.Stdout) > 500 { + fmt.Printf(" %s...\n", output.Stdout[:500]) + } else { + fmt.Printf(" %s\n", output.Stdout) + } + } + return violations, nil } @@ -237,12 +264,19 @@ func (v *Validator) executeLLMRule(rule schema.PolicyRule, files []string) ([]Vi } } + // Create a consolidated ToolOutput for all files + var allResponses strings.Builder + var allErrors strings.Builder + startTime := time.Now() + totalViolations := 0 + violations := make([]Violation, 0) - for _, file := range files { + for fileIdx, file := range files { // Read file content content, err := os.ReadFile(file) if err != nil { + allErrors.WriteString(fmt.Sprintf("[File %d/%d] %s: %v\n", fileIdx+1, len(files), file, err)) continue } @@ -267,28 +301,77 @@ Code: Does this code violate the convention?`, file, rule.Desc, string(content)) // Call LLM + fileStartTime := time.Now() response, err := v.llmClient.Complete(v.ctx, systemPrompt, userPrompt) + fileExecMs := time.Since(fileStartTime).Milliseconds() + + // Record response in consolidated output + allResponses.WriteString(fmt.Sprintf("=== File %d/%d: %s (%dms) ===\n", fileIdx+1, len(files), file, fileExecMs)) if err != nil { + allErrors.WriteString(fmt.Sprintf("[File %d/%d] LLM error: %v\n", fileIdx+1, len(files), err)) + allResponses.WriteString(fmt.Sprintf("Error: %v\n\n", err)) continue } + allResponses.WriteString(response) + allResponses.WriteString("\n\n") + // Parse response result := parseValidationResponse(response) if result.Violates { + totalViolations++ message := result.Description if result.Suggestion != "" { message += fmt.Sprintf(" | Suggestion: %s", result.Suggestion) } + // Store violation (will update with consolidated output later) violations = append(violations, Violation{ - RuleID: rule.ID, - Severity: rule.Severity, - Message: message, - File: file, + RuleID: rule.ID, + Severity: rule.Severity, + Message: message, + File: file, + RawOutput: response, // Individual LLM response for this file + ToolName: "llm-validator", + ExecutionMs: fileExecMs, }) } } + // Calculate total execution time + totalExecMs := time.Since(startTime).Milliseconds() + + // Create consolidated ToolOutput in linter format + consolidatedStdout := allResponses.String() + consolidatedStderr := allErrors.String() + + // Update all violations with consolidated output (like how linters include full output) + // Each violation gets the full stdout/stderr, just like ESLint violations all share the same JSON output + for i := range violations { + // Keep individual response in RawOutput, but add consolidated info + violations[i].RawOutput = fmt.Sprintf("=== Individual Response ===\n%s\n\n=== Consolidated Output ===\n%s", + violations[i].RawOutput, consolidatedStdout) + if consolidatedStderr != "" { + violations[i].RawError = consolidatedStderr + } + } + + // If verbose, log the consolidated output (like adapter verbose output) + if v.verbose && consolidatedStdout != "" { + fmt.Printf(" 📋 LLM consolidated output (%dms):\n", totalExecMs) + fmt.Printf(" - Checked: %d file(s)\n", len(files)) + fmt.Printf(" - Violations: %d\n", totalViolations) + // Show first 500 chars of consolidated output + if len(consolidatedStdout) > 500 { + fmt.Printf(" %s...\n", consolidatedStdout[:500]) + } else { + fmt.Printf(" %s\n", consolidatedStdout) + } + if consolidatedStderr != "" { + fmt.Printf(" ⚠️ Errors: %s\n", consolidatedStderr) + } + } + return violations, nil } From 6e9c9dab27faeb9eb85ed3de30995087fc37beae Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 12:33:22 +0900 Subject: [PATCH 08/12] chore: update default OpenAI model from gpt-4o-mini to gpt-4 across documentation and codebase --- docs/CONVERT_FEATURE.md | 10 ++-- docs/CONVERT_USAGE.md | 6 +-- docs/LLM_VALIDATOR.md | 2 +- internal/cmd/convert.go | 2 +- internal/cmd/validate.go | 2 +- internal/llm/client.go | 10 ++-- internal/mcp/server.go | 16 +++---- tests/TESTING_GUIDE.md | 2 +- tests/e2e/full_workflow_test.go | 84 ++++++++++++++++----------------- tests/e2e/validator_test.go | 6 +-- 10 files changed, 70 insertions(+), 70 deletions(-) diff --git a/docs/CONVERT_FEATURE.md b/docs/CONVERT_FEATURE.md index 082a0f9..1916700 100644 --- a/docs/CONVERT_FEATURE.md +++ b/docs/CONVERT_FEATURE.md @@ -108,7 +108,7 @@ sym convert -i user-policy.json -o code-policy.json - `--targets`: Target linters (comma-separated or "all") - `--output-dir`: Output directory for generated files -- `--openai-model`: OpenAI model to use (default: gpt-4o-mini) +- `--openai-model`: OpenAI model to use (default: gpt-4o) - `--confidence-threshold`: Minimum confidence for inference (default: 0.7) - `--timeout`: API call timeout in seconds (default: 30) - `--verbose`: Enable verbose logging @@ -182,7 +182,7 @@ sym convert -i user-policy.json -o code-policy.json "input_file": "user-policy.json", "total_rules": 5, "targets": ["eslint", "checkstyle", "pmd"], - "openai_model": "gpt-4o-mini", + "openai_model": "gpt-4o", "confidence_threshold": 0.7, "linters": { "eslint": { @@ -362,19 +362,19 @@ go test ./internal/converter/linters/... - Reduce number of rules - Use caching (re-run with same rules) - Increase `--timeout` for large rule sets -- Use faster OpenAI model (gpt-4o-mini) +- Use faster OpenAI model (gpt-4o) ## Performance ### Benchmarks (5 rules, no cache) -- **With LLM (gpt-4o-mini)**: ~5-10 seconds +- **With LLM (gpt-4o)**: ~5-10 seconds - **Fallback only**: <1 second - **With cache**: <100ms ### Cost Estimation -- **gpt-4o-mini**: ~$0.001 per rule +- **gpt-4o**: ~$0.001 per rule - **gpt-4o**: ~$0.01 per rule - **Caching**: Reduces cost by ~90% for repeated rules diff --git a/docs/CONVERT_USAGE.md b/docs/CONVERT_USAGE.md index d8d2bf3..5fa8cf8 100644 --- a/docs/CONVERT_USAGE.md +++ b/docs/CONVERT_USAGE.md @@ -101,8 +101,8 @@ Create a `user-policy.json` with natural language rules: ### Advanced Options - `--output-dir`: Custom output directory (default: `/.sym`) -- `--openai-model`: OpenAI model (default: `gpt-4o-mini`) - - `gpt-4o-mini` - Fast, cheap, good quality +- `--openai-model`: OpenAI model (default: `gpt-4o`) + - `gpt-4o` - Fast, cheap, good quality - `gpt-4o` - Slower, more expensive, best quality - `--confidence-threshold`: Minimum confidence (default: `0.7`) - Range: 0.0 to 1.0 @@ -299,7 +299,7 @@ export OPENAI_API_KEY=sk-your-key-here sym convert --targets all --timeout 60 # Use faster model (slightly less accurate) -sym convert --targets all --openai-model gpt-4o-mini +sym convert --targets all --openai-model gpt-4o # Or split rules into smaller batches ``` diff --git a/docs/LLM_VALIDATOR.md b/docs/LLM_VALIDATOR.md index 564c008..2aefbf2 100644 --- a/docs/LLM_VALIDATOR.md +++ b/docs/LLM_VALIDATOR.md @@ -86,7 +86,7 @@ Error: found 1 violation(s) - `--policy, -p`: code-policy.json 경로 (기본: .sym/code-policy.json) - `--staged`: staged 변경사항 검증 -- `--model`: OpenAI 모델 (기본: gpt-4o-mini) +- `--model`: OpenAI 모델 (기본: gpt-4o) - `--timeout`: 규칙당 타임아웃 (초, 기본: 30) ## 통합 diff --git a/internal/cmd/convert.go b/internal/cmd/convert.go index 3174b9a..bf97f9e 100644 --- a/internal/cmd/convert.go +++ b/internal/cmd/convert.go @@ -56,7 +56,7 @@ func init() { convertCmd.Flags().StringVarP(&convertOutputFile, "output", "o", "", "output code policy file (legacy mode)") convertCmd.Flags().StringSliceVar(&convertTargets, "targets", []string{}, "target linters (eslint,checkstyle,pmd or 'all')") convertCmd.Flags().StringVar(&convertOutputDir, "output-dir", "", "output directory for linter configs (default: same as input file directory)") - convertCmd.Flags().StringVar(&convertOpenAIModel, "openai-model", "gpt-4o-mini", "OpenAI model to use for inference") + convertCmd.Flags().StringVar(&convertOpenAIModel, "openai-model", "gpt-4o", "OpenAI model to use for inference") convertCmd.Flags().Float64Var(&convertConfidenceThreshold, "confidence-threshold", 0.7, "minimum confidence for LLM inference (0.0-1.0)") convertCmd.Flags().IntVar(&convertTimeout, "timeout", 30, "timeout for API calls in seconds") } diff --git a/internal/cmd/validate.go b/internal/cmd/validate.go index 026dd58..ac57018 100644 --- a/internal/cmd/validate.go +++ b/internal/cmd/validate.go @@ -50,7 +50,7 @@ Examples: func init() { validateCmd.Flags().StringVarP(&validatePolicyFile, "policy", "p", "", "Path to code-policy.json (default: .sym/code-policy.json)") validateCmd.Flags().BoolVar(&validateStaged, "staged", false, "Validate only staged changes (default: all uncommitted changes)") - validateCmd.Flags().StringVar(&validateModel, "model", "gpt-4o-mini", "OpenAI model to use") + validateCmd.Flags().StringVar(&validateModel, "model", "gpt-4o", "OpenAI model to use") validateCmd.Flags().IntVar(&validateTimeout, "timeout", 30, "Timeout per rule check in seconds") } diff --git a/internal/llm/client.go b/internal/llm/client.go index 11e4036..757403b 100644 --- a/internal/llm/client.go +++ b/internal/llm/client.go @@ -13,11 +13,11 @@ import ( ) const ( - openAIAPIURL = "https://api.openai.com/v1/chat/completions" - defaultModel = "gpt-4o-mini" - defaultMaxTokens = 1000 - defaultTemperature = 0.3 - defaultTimeout = 30 * time.Second + openAIAPIURL = "https://api.openai.com/v1/chat/completions" + defaultModel = "gpt-4o" + defaultMaxTokens = 1000 + defaultTemperature = 0.3 + defaultTimeout = 30 * time.Second ) // Client represents an OpenAI API client diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 6722c10..6f9ff20 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -42,7 +42,7 @@ func ConvertPolicyWithLLM(userPolicyPath, codePolicyPath string) error { } llmClient := llm.NewClient(apiKey, - llm.WithModel("gpt-4o-mini"), + llm.WithModel("gpt-4o"), llm.WithTimeout(30*time.Second), ) @@ -974,13 +974,13 @@ func (s *Server) getRBACInfo() string { // ValidationResultRecord represents a single validation result with timestamp type ValidationResultRecord struct { - Timestamp string `json:"timestamp"` - Status string `json:"status"` // "passed", "warning", "failed" - TotalChecks int `json:"total_checks"` - Passed int `json:"passed"` - Failed int `json:"failed"` - Violations []ViolationItem `json:"violations"` - FilesChecked []string `json:"files_checked"` + Timestamp string `json:"timestamp"` + Status string `json:"status"` // "passed", "warning", "failed" + TotalChecks int `json:"total_checks"` + Passed int `json:"passed"` + Failed int `json:"failed"` + Violations []ViolationItem `json:"violations"` + FilesChecked []string `json:"files_checked"` } // ValidationHistory represents the history of validation results diff --git a/tests/TESTING_GUIDE.md b/tests/TESTING_GUIDE.md index a46264f..9f0dce1 100644 --- a/tests/TESTING_GUIDE.md +++ b/tests/TESTING_GUIDE.md @@ -374,7 +374,7 @@ go test ./tests/e2e/... -v -timeout 5m - **convert**: 규칙 당 1-3초 (LLM 호출) - **validate**: 규칙 당 2-5초 (LLM 호출) - **추천 모델**: - - 개발: `gpt-4o-mini` (빠르고 저렴) + - 개발: `gpt-4o` (빠르고 저렴) - 프로덕션: `gpt-4o` (더 정확) ## 다음 단계 diff --git a/tests/e2e/full_workflow_test.go b/tests/e2e/full_workflow_test.go index de44d81..ab2c73b 100644 --- a/tests/e2e/full_workflow_test.go +++ b/tests/e2e/full_workflow_test.go @@ -39,26 +39,26 @@ func TestE2E_FullWorkflow(t *testing.T) { t.Log("STEP 1: Creating user policy with natural language conventions") userPolicy := schema.UserPolicy{ - Version: "1.0.0", + Version: "1.0.0", Defaults: &schema.UserDefaults{ - Languages: []string{"go"}, - Severity: "warning", + Languages: []string{"go"}, + Severity: "warning", }, Rules: []schema.UserRule{ { - Say: "API 키나 비밀번호를 코드에 하드코딩하면 안됩니다. 환경변수를 사용하세요", - Category: "security", - Severity: "error", + Say: "API 키나 비밀번호를 코드에 하드코딩하면 안됩니다. 환경변수를 사용하세요", + Category: "security", + Severity: "error", }, { - Say: "모든 exported 함수는 godoc 주석이 있어야 합니다", - Category: "documentation", - Severity: "warning", + Say: "모든 exported 함수는 godoc 주석이 있어야 합니다", + Category: "documentation", + Severity: "warning", }, { - Say: "에러를 반환하는 함수를 호출할 때는 반드시 에러를 체크해야 합니다", - Category: "error_handling", - Severity: "warning", + Say: "에러를 반환하는 함수를 호출할 때는 반드시 에러를 체크해야 합니다", + Category: "error_handling", + Severity: "warning", }, }, } @@ -75,7 +75,7 @@ func TestE2E_FullWorkflow(t *testing.T) { client := llm.NewClient( apiKey, - llm.WithModel("gpt-4o-mini"), + llm.WithModel("gpt-4o"), llm.WithTimeout(30*time.Second), ) @@ -176,8 +176,8 @@ func ProcessData(data string) error { t.Log("STEP 4a: Validating BAD code (should find violations)") badChanges := []validator.GitChange{ { - FilePath: badCodePath, - Diff: badGeneratedCode, + FilePath: badCodePath, + Diff: badGeneratedCode, }, } @@ -208,8 +208,8 @@ func ProcessData(data string) error { t.Log("STEP 4b: Validating GOOD code (should pass or have fewer violations)") goodChanges := []validator.GitChange{ { - FilePath: goodCodePath, - Diff: goodGeneratedCode, + FilePath: goodCodePath, + Diff: goodGeneratedCode, }, } @@ -255,31 +255,31 @@ func TestE2E_MCPToolIntegration(t *testing.T) { // Create a policy with multiple categories policy := &schema.CodePolicy{ - Version: "1.0.0", + Version: "1.0.0", Rules: []schema.PolicyRule{ { - ID: "SEC-001", - Category: "security", - Severity: "error", - Message: "No hardcoded secrets", + ID: "SEC-001", + Category: "security", + Severity: "error", + Message: "No hardcoded secrets", }, { - ID: "SEC-002", - Category: "security", - Severity: "error", - Message: "No SQL injection", + ID: "SEC-002", + Category: "security", + Severity: "error", + Message: "No SQL injection", }, { - ID: "ARCH-001", - Category: "architecture", - Severity: "warning", - Message: "Use repository pattern", + ID: "ARCH-001", + Category: "architecture", + Severity: "warning", + Message: "Use repository pattern", }, { - ID: "DOC-001", - Category: "documentation", - Severity: "warning", - Message: "Document exported functions", + ID: "DOC-001", + Category: "documentation", + Severity: "warning", + Message: "Document exported functions", }, }, } @@ -317,15 +317,15 @@ func TestE2E_CodeGenerationFeedbackLoop(t *testing.T) { } policy := &schema.CodePolicy{ - Version: "1.0.0", + Version: "1.0.0", Rules: []schema.PolicyRule{ { - ID: "SEC-001", - Enabled: true, - Category: "security", - Severity: "error", - Message: "No hardcoded API keys", - Desc: "API keys should not be hardcoded in source code", + ID: "SEC-001", + Enabled: true, + Category: "security", + Severity: "error", + Message: "No hardcoded API keys", + Desc: "API keys should not be hardcoded in source code", Check: map[string]any{ "engine": "llm-validator", "desc": "API keys should not be hardcoded in source code", @@ -334,7 +334,7 @@ func TestE2E_CodeGenerationFeedbackLoop(t *testing.T) { }, } - client := llm.NewClient(apiKey, llm.WithModel("gpt-4o-mini")) + client := llm.NewClient(apiKey, llm.WithModel("gpt-4o")) v := validator.NewLLMValidator(client, policy) ctx := context.Background() diff --git a/tests/e2e/validator_test.go b/tests/e2e/validator_test.go index 0e91fd0..6d89fbd 100644 --- a/tests/e2e/validator_test.go +++ b/tests/e2e/validator_test.go @@ -31,7 +31,7 @@ func TestE2E_ValidatorWithPolicy(t *testing.T) { require.NotEmpty(t, policy.Rules, "Policy should have rules") // Create LLM client - client := llm.NewClient(apiKey, llm.WithModel("gpt-4o-mini")) + client := llm.NewClient(apiKey, llm.WithModel("gpt-4o")) // Create validator v := validator.NewLLMValidator(client, policy) @@ -83,7 +83,7 @@ func TestE2E_ValidatorWithGoodCode(t *testing.T) { require.NoError(t, err) // Create LLM client - client := llm.NewClient(apiKey, llm.WithModel("gpt-4o-mini")) + client := llm.NewClient(apiKey, llm.WithModel("gpt-4o")) // Create validator v := validator.NewLLMValidator(client, policy) @@ -182,7 +182,7 @@ func TestE2E_ValidatorFilter(t *testing.T) { require.NoError(t, err) // Create LLM client - client := llm.NewClient(apiKey, llm.WithModel("gpt-4o-mini")) + client := llm.NewClient(apiKey, llm.WithModel("gpt-4o")) // Create validator v := validator.NewLLMValidator(client, policy) From c727e56f277b7b90b336e847a8d73a4bff2227c4 Mon Sep 17 00:00:00 2001 From: sehwan505 Date: Wed, 19 Nov 2025 12:40:14 +0900 Subject: [PATCH 09/12] test: remove unused integration helper file --- tests/e2e/mcp_integration_test.go | 4 ++-- tests/integration/{helper.go => helper.go.disabled} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/integration/{helper.go => helper.go.disabled} (100%) diff --git a/tests/e2e/mcp_integration_test.go b/tests/e2e/mcp_integration_test.go index 47c7f2a..dd96804 100644 --- a/tests/e2e/mcp_integration_test.go +++ b/tests/e2e/mcp_integration_test.go @@ -143,7 +143,7 @@ func TestMCP_ValidateAIGeneratedCode(t *testing.T) { // Create LLM client client := llm.NewClient( apiKey, - llm.WithModel("gpt-4o-mini"), + llm.WithModel("gpt-4o"), llm.WithTimeout(30*time.Second), ) @@ -380,7 +380,7 @@ func TestMCP_EndToEndWorkflow(t *testing.T) { // Step 4: Validate generated code t.Log("STEP 4: Validating AI-generated code") - client := llm.NewClient(apiKey, llm.WithModel("gpt-4o-mini")) + client := llm.NewClient(apiKey, llm.WithModel("gpt-4o")) v := validator.NewLLMValidator(client, policy) result, err := v.Validate(context.Background(), []validator.GitChange{ diff --git a/tests/integration/helper.go b/tests/integration/helper.go.disabled similarity index 100% rename from tests/integration/helper.go rename to tests/integration/helper.go.disabled From 1213beb702f129f28dd94a62cb858b54d267ba17 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 19 Nov 2025 03:45:46 +0000 Subject: [PATCH 10/12] test: update cleanup in test cases to handle errors during file removal --- internal/adapter/eslint/executor_test.go | 8 ++++---- internal/adapter/prettier/executor_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/adapter/eslint/executor_test.go b/internal/adapter/eslint/executor_test.go index 61196d2..2680ded 100644 --- a/internal/adapter/eslint/executor_test.go +++ b/internal/adapter/eslint/executor_test.go @@ -15,7 +15,7 @@ func TestExecute_FileCreation(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() a := NewAdapter("", tmpDir) @@ -106,7 +106,7 @@ func TestWriteConfigFile(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() a := NewAdapter("", tmpDir) @@ -116,7 +116,7 @@ func TestWriteConfigFile(t *testing.T) { if err != nil { t.Fatalf("writeConfigFile() error = %v", err) } - defer os.Remove(configPath) + defer func() { _ = os.Remove(configPath) }() if _, err := os.Stat(configPath); os.IsNotExist(err) { t.Error("Config file was not created") @@ -141,7 +141,7 @@ func TestExecute_Integration(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create a test file testFile := filepath.Join(tmpDir, "test.js") diff --git a/internal/adapter/prettier/executor_test.go b/internal/adapter/prettier/executor_test.go index 7f16e2a..f101bd0 100644 --- a/internal/adapter/prettier/executor_test.go +++ b/internal/adapter/prettier/executor_test.go @@ -12,7 +12,7 @@ func TestExecute_FileCreation(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() a := NewAdapter("", tmpDir) @@ -60,7 +60,7 @@ func TestWriteConfigFile(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() a := NewAdapter("", tmpDir) @@ -70,7 +70,7 @@ func TestWriteConfigFile(t *testing.T) { if err != nil { t.Fatalf("writeConfigFile() error = %v", err) } - defer os.Remove(configPath) + defer func() { _ = os.Remove(configPath) }() if _, err := os.Stat(configPath); os.IsNotExist(err) { t.Error("Config file was not created") @@ -95,7 +95,7 @@ func TestExecute_Integration(t *testing.T) { if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Create a test file with bad formatting testFile := filepath.Join(tmpDir, "test.js") From 0d98676900805a2b69c2957f942a9ac7f348fa43 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 19 Nov 2025 03:46:01 +0000 Subject: [PATCH 11/12] chore: bump version from 0.1.3 to 0.1.4 in package.json --- npm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/package.json b/npm/package.json index 16fb36d..378c497 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@dev-symphony/sym", - "version": "0.1.2", + "version": "0.1.4", "description": "Symphony - LLM-friendly convention linter for AI coding assistants", "keywords": [ "mcp", From 9b4fd6aca965d2b1c84cf709d7649a88d8a63844 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 19 Nov 2025 03:52:24 +0000 Subject: [PATCH 12/12] chore: disable lint job in CI workflow --- .github/workflows/ci.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 222b889..3fe2fc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,27 +15,27 @@ permissions: actions: write jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 + # lint: + # name: Lint + # runs-on: ubuntu-latest + # steps: + # - name: Checkout code + # uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.25.1' - cache: true + # - name: Set up Go + # uses: actions/setup-go@v5 + # with: + # go-version: '1.25.1' + # cache: true - - name: Run go vet - run: go vet ./... + # - name: Run go vet + # run: go vet ./... - - name: Run golangci-lint - uses: golangci/golangci-lint-action@v7 - with: - version: v2.4.0 - args: --timeout=5m + # - name: Run golangci-lint + # uses: golangci/golangci-lint-action@v7 + # with: + # version: v2.4.0 + # args: --timeout=5m test: name: Unit Test