diff --git a/README.md b/README.md index fc5d517..c4b1f13 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,19 @@ sym-cli/ ├── scripts/ # 빌드 스크립트 ├── examples/ # 예제 파일 ├── tests/ # 테스트 +├── testdata/ # 통합 테스트 데이터 +│ ├── javascript/ # JavaScript 테스트 파일 +│ │ ├── pattern/ # 패턴 매칭 테스트 +│ │ ├── length/ # 길이 제한 테스트 +│ │ ├── style/ # 코드 스타일 테스트 +│ │ └── ast/ # AST 구조 테스트 +│ ├── typescript/ # TypeScript 테스트 파일 +│ │ └── typechecker/ # 타입 체킹 테스트 +│ └── java/ # Java 테스트 파일 +│ ├── pattern/ # 패턴 매칭 테스트 +│ ├── length/ # 길이 제한 테스트 +│ ├── style/ # 코드 스타일 테스트 +│ └── ast/ # AST 구조 테스트 ├── .sym/ # 정책 및 역할 파일 (gitignore) ├── Makefile └── README.md @@ -331,10 +344,18 @@ make test # 특정 패키지 테스트 go test ./internal/engine/pattern/... -v -# 통합 테스트 +# 통합 테스트 (JavaScript, TypeScript, Java) go test ./tests/integration/... -v ``` +**통합 테스트 데이터**: +- `testdata/javascript/`: JavaScript 엔진 테스트 (pattern, length, style, ast) +- `testdata/typescript/`: TypeScript 타입체커 테스트 +- `testdata/java/`: Java 엔진 테스트 (Checkstyle, PMD 검증) + +각 디렉토리는 위반 케이스와 정상 케이스를 포함하여 검증 엔진의 정확성을 보장합니다. +자세한 내용은 [testdata/README.md](testdata/README.md)를 참고하세요. + 테스트 커버리지 리포트는 [여기](https://devsymphony.github.io/sym-cli/coverage.html)에서 확인할 수 있습니다. ### 코드 품질 @@ -453,3 +474,212 @@ Contributions are welcome! Please feel free to submit a Pull Request. --- **Note:** 코드 검증 기능 (`convert`, `validate`, `export`)은 현재 개발 중입니다. + +## 📊 패키지 구조 및 의존성 + +```mermaid +graph TB + subgraph "메인 진입점" + main[cmd/sym
main] + end + + subgraph "CLI 계층" + cmd[internal/cmd
Cobra Commands] + end + + subgraph "중앙 데이터 구조" + schema[pkg/schema
Types] + end + + subgraph "기본 유틸리티" + config[internal/config] + git[internal/git] + github[internal/github] + llm[internal/llm] + end + + subgraph "도메인 계층" + auth[internal/auth] + + subgraph converter_group["internal/converter"] + converter[converter] + conv_linters[linters] + end + + policy[internal/policy] + end + + subgraph "비즈니스 로직" + roles[internal/roles] + + subgraph adapter_group["internal/adapter"] + adapter[adapter] + adapter_eslint[eslint] + adapter_prettier[prettier] + adapter_tsc[tsc] + adapter_checkstyle[checkstyle] + adapter_pmd[pmd] + adapter_registry[registry] + end + + subgraph engine_group["internal/engine"] + engine[engine] + engine_core[core] + engine_registry[registry] + engine_pattern[pattern] + engine_length[length] + engine_style[style] + engine_ast[ast] + engine_llm[llm engine] + engine_typechecker[typechecker] + end + + validator[internal/validator] + end + + subgraph "통합 계층" + mcp[internal/mcp] + server[internal/server] + end + + %% main 의존성 + main --> cmd + + %% cmd 의존성 + cmd --> auth + cmd --> config + cmd --> converter + cmd --> git + cmd --> github + cmd --> llm + cmd --> mcp + cmd --> policy + cmd --> roles + cmd --> server + cmd --> validator + cmd --> schema + + %% auth 의존성 + auth --> config + auth --> github + + %% converter 의존성 + converter --> llm + converter --> schema + conv_linters --> converter + + %% policy 의존성 + policy --> git + policy --> schema + + %% roles 의존성 + roles --> git + roles --> policy + roles --> schema + + %% adapter 서브패키지 + adapter_eslint --> adapter + adapter_prettier --> adapter + adapter_tsc --> adapter + adapter_checkstyle --> adapter + adapter_pmd --> adapter + adapter_registry --> adapter + adapter --> engine_core + + %% engine 서브패키지 + engine_pattern --> engine_core + engine_pattern --> adapter_eslint + engine_length --> engine_core + engine_length --> adapter_eslint + engine_style --> engine_core + engine_style --> adapter_eslint + engine_style --> adapter_prettier + engine_ast --> engine_core + engine_ast --> adapter_eslint + engine_ast --> adapter_checkstyle + engine_ast --> adapter_pmd + engine_llm --> engine_core + engine_llm --> llm + engine_typechecker --> engine_core + engine_typechecker --> adapter_tsc + engine_registry --> engine_core + engine --> engine_registry + + %% validator 의존성 + validator --> engine + validator --> llm + validator --> roles + validator --> git + validator --> schema + + %% mcp 의존성 + mcp --> converter + mcp --> git + mcp --> llm + mcp --> policy + mcp --> validator + mcp --> schema + + %% server 의존성 + server --> config + server --> git + server --> github + server --> policy + server --> roles + server --> schema + + %% llm의 schema 의존성 + llm --> schema + + classDef mainEntry fill:#e03131,stroke:#a61e4d,color:#fff,stroke-width:3px + classDef cliLayer fill:#ff6b6b,stroke:#c92a2a,color:#fff + classDef core fill:#20c997,stroke:#087f5b,color:#fff + classDef leaf fill:#51cf66,stroke:#2f9e44,color:#fff + classDef domain fill:#74c0fc,stroke:#1971c2,color:#fff + classDef business fill:#ffd43b,stroke:#f08c00,color:#000 + classDef integration fill:#da77f2,stroke:#9c36b5,color:#fff + classDef subpkg fill:#f8f9fa,stroke:#868e96,color:#000 + + class main mainEntry + class cmd cliLayer + class schema core + class config,git,github,llm leaf + class auth,converter,policy domain + class roles,adapter,engine,validator business + class mcp,server integration + class adapter_eslint,adapter_prettier,adapter_tsc,adapter_checkstyle,adapter_pmd,adapter_registry,conv_linters subpkg + class engine_core,engine_registry,engine_pattern,engine_length,engine_style,engine_ast,engine_llm,engine_typechecker subpkg +``` + +### 패키지 계층 구조 + +**메인 진입점** +- `cmd/sym`: main 패키지 (→ internal/cmd) + +**CLI 계층** +- `internal/cmd`: Cobra 기반 CLI 커맨드 구현 (→ 모든 internal 패키지) + +**중앙 데이터 구조** +- `pkg/schema`: UserPolicy(A Schema) 및 CodePolicy(B Schema) 타입 정의 + +**Tier 0: 기본 유틸리티** (의존성 없음) +- `internal/config`: 전역 설정 및 토큰 관리 +- `internal/git`: Git 저장소 작업 +- `internal/github`: GitHub API 클라이언트 +- `internal/llm`: OpenAI API 클라이언트 (→ schema) + +**Tier 1: 도메인 계층** +- `internal/auth`: GitHub OAuth 인증 (→ config, github) +- `internal/converter`: 정책 변환 (→ llm, schema) +- `internal/policy`: 정책 파일 관리 (→ git, schema) + +**Tier 2: 비즈니스 로직** +- `internal/roles`: RBAC 구현 (→ git, policy, schema) +- `internal/adapter` ↔ `internal/engine`: 검증 도구 어댑터 및 엔진 (순환 의존성) + - Adapters: ESLint, Prettier, TSC, Checkstyle, PMD + - Engines: Pattern, Length, Style, AST, LLM, TypeChecker +- `internal/validator`: 검증 오케스트레이터 (→ engine, llm, roles, git, schema) + +**Tier 3: 통합 계층** +- `internal/mcp`: MCP 서버 (→ converter, git, llm, policy, validator, schema) +- `internal/server`: 웹 대시보드 (→ config, git, github, policy, roles, schema) diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..2bcbd83 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,150 @@ +# Setup Guide + +This document describes the development environment setup and external tool requirements for the Symphony CLI project. + +## Prerequisites + +### Required Tools + +#### 1. Go (Required) +- **Version**: Go 1.21 or higher +- **Purpose**: Build and run the CLI +- **Installation**: https://go.dev/doc/install + +#### 2. Node.js & npm (Required for JavaScript/TypeScript validation) +- **Version**: Node.js 18+ with npm +- **Purpose**: Run ESLint, Prettier, TSC adapters +- **Installation**: https://nodejs.org/ +- **Used by engines**: Pattern, Length, Style, AST, TypeChecker + +#### 3. Java JDK (Required for Java validation) +- **Version**: Java 21 or higher (OpenJDK recommended) +- **Purpose**: Run Checkstyle and PMD adapters +- **Installation**: + ```bash + # Ubuntu/Debian + sudo apt-get update + sudo apt-get install -y default-jdk + + # macOS (Homebrew) + brew install openjdk@21 + + # Verify installation + java -version + ``` +- **Used by engines**: Pattern (Java), Length (Java), Style (Java), AST (Java) +- **Note**: Java was installed on 2025-11-16 for integration testing + +### Optional Tools + +#### Git +- Required for running integration tests that involve git operations +- Most systems have git pre-installed + +--- + +## External Tool Installation + +The CLI automatically installs external validation tools when first needed: + +### JavaScript/TypeScript Tools +- **ESLint**: Installed to `~/.symphony/tools/node_modules/eslint` via npm +- **Prettier**: Installed to `~/.symphony/tools/node_modules/prettier` via npm +- **TypeScript**: Installed to `~/.symphony/tools/node_modules/typescript` via npm + +### Java Tools +- **Checkstyle**: Downloaded to `~/.symphony/tools/checkstyle-10.26.1.jar` from Maven Central +- **PMD**: Downloaded to `~/.symphony/tools/pmd-/` from GitHub Releases + +**Auto-installation happens when:** +1. A validation rule is executed for the first time +2. The engine checks if the adapter is available (`CheckAvailability()`) +3. If not found, the engine calls `Install()` to download and set up the tool + +--- + +## Running Tests + +### Unit Tests +```bash +# Run all unit tests +go test ./... + +# Run with coverage +go test -cover ./... +``` + +### Integration Tests +```bash +# Run all integration tests +go test ./tests/integration/... + +# Run specific test suite +go test ./tests/integration/validator_policy_test.go ./tests/integration/helper.go -v + +# Skip integration tests in short mode +go test -short ./... +``` + +**Note**: Integration tests require: +- Node.js and npm (for JavaScript/TypeScript tests) +- Java JDK (for Java tests) + +--- + +## Troubleshooting + +### Java Tests Fail with "java not found" +```bash +# Install Java JDK +sudo apt-get install -y default-jdk + +# Verify installation +java -version # Should show: openjdk version "21.0.8" +``` + +### ESLint/Prettier Installation Fails +```bash +# Ensure npm is available +npm --version + +# Manually install tools +cd ~/.symphony/tools +npm install eslint@^8.0.0 prettier@latest +``` + +### Checkstyle Download Fails (HTTP 404) +- The Checkstyle version was updated to 10.26.1 on 2025-11-16 +- If you see 404 errors, check `/workspace/internal/adapter/checkstyle/adapter.go` for the correct version +- Maven Central URL: https://repo1.maven.org/maven2/com/puppycrawl/tools/checkstyle/ + +--- + +## Development Environment + +### Recommended VS Code Extensions +- Go (golang.go) +- YAML (redhat.vscode-yaml) +- JSON (vscode.json-language-features) + +### Directory Structure +``` +~/.symphony/tools/ # Auto-installed external tools + ├── node_modules/ # JavaScript/TypeScript tools + │ ├── eslint/ + │ ├── prettier/ + │ └── typescript/ + ├── checkstyle-10.26.1.jar # Java style checker + └── pmd-/ # Java static analyzer +``` + +--- + +## Installation History + +### 2025-11-16: Java JDK Installation +- **Installed**: OpenJDK 21.0.8 +- **Reason**: Required for Java validation integration tests +- **Method**: `sudo apt-get install -y default-jdk` +- **Verification**: `java -version` shows OpenJDK 21.0.8+9 +- **Impact**: Enables Checkstyle and PMD adapters for Java code validation diff --git a/docs/LINTER_VALIDATION.md b/docs/LINTER_VALIDATION.md index 76b1e87..c425461 100644 --- a/docs/LINTER_VALIDATION.md +++ b/docs/LINTER_VALIDATION.md @@ -86,3 +86,45 @@ Generated `code-policy.json` contains: ``` Rules with `engine: "llm-validator"` cannot be checked by traditional linters and require custom LLM-based validation. + +## Testing + +### Integration Test Data + +Validation engines are tested using structured test data in `testdata/`: + +``` +testdata/ +├── javascript/ # ESLint-based validation tests +│ ├── pattern/ # Naming conventions, regex patterns +│ ├── length/ # Line/function length limits +│ ├── style/ # Code formatting +│ └── ast/ # AST structure validation +├── typescript/ # TSC-based validation tests +│ └── typechecker/ # Type checking tests +└── java/ # Checkstyle/PMD-based validation tests + ├── pattern/ # Naming conventions (PascalCase, camelCase) + ├── length/ # Line/method/parameter length limits + ├── style/ # Java formatting conventions + └── ast/ # Code structure (exception handling, etc.) +``` + +Each directory contains: +- **Violation files**: Code that violates conventions (e.g., `NamingViolations.java`) +- **Valid files**: Code that complies with conventions (e.g., `ValidNaming.java`) + +### Running Integration Tests + +```bash +# All integration tests +go test ./tests/integration/... -v + +# Specific engine tests +go test ./tests/integration/... -v -run TestPatternEngine +go test ./tests/integration/... -v -run TestLengthEngine +go test ./tests/integration/... -v -run TestStyleEngine +go test ./tests/integration/... -v -run TestASTEngine +go test ./tests/integration/... -v -run TestTypeChecker +``` + +For detailed test data structure, see [testdata/README.md](../testdata/README.md). diff --git a/internal/adapter/README.md b/internal/adapter/README.md new file mode 100644 index 0000000..e00460a --- /dev/null +++ b/internal/adapter/README.md @@ -0,0 +1,17 @@ +# adapter + +외부 검증 도구를 표준 인터페이스로 통합하는 어댑터 레이어입니다. + +ESLint, Prettier, TypeScript Compiler(TSC), Checkstyle, PMD 등의 도구를 subprocess로 실행하고 결과를 파싱합니다. + +## 서브패키지 + +- `eslint`: JavaScript/TypeScript 린터 어댑터 +- `prettier`: 코드 포매터 어댑터 +- `tsc`: TypeScript 타입 체커 어댑터 +- `checkstyle`: Java 스타일 체커 어댑터 +- `pmd`: Java 정적 분석 도구 어댑터 +- `registry`: 어댑터 등록 및 검색 시스템 + +**사용자**: engine +**의존성**: engine/core diff --git a/internal/adapter/adapter.go b/internal/adapter/adapter.go index d34a21c..e83921a 100644 --- a/internal/adapter/adapter.go +++ b/internal/adapter/adapter.go @@ -14,6 +14,10 @@ type Adapter interface { // Name returns the adapter name (e.g., "eslint", "prettier"). Name() string + // GetCapabilities returns the adapter's capabilities. + // This includes supported languages, categories, and version info. + GetCapabilities() AdapterCapabilities + // CheckAvailability checks if the tool is installed and usable. // Returns nil if available, error with details if not. CheckAvailability(ctx context.Context) error @@ -34,6 +38,23 @@ type Adapter interface { ParseOutput(output *ToolOutput) ([]Violation, error) } +// AdapterCapabilities describes what an adapter can do. +type AdapterCapabilities struct { + // Name is the adapter identifier (e.g., "eslint", "checkstyle"). + Name string + + // SupportedLanguages lists languages this adapter can validate. + // Examples: ["javascript", "typescript", "java"] + SupportedLanguages []string + + // SupportedCategories lists rule categories this adapter can handle. + // Examples: ["pattern", "length", "style", "ast", "complexity"] + SupportedCategories []string + + // Version is the tool version (e.g., "8.0.0", "10.12.0"). + Version string +} + // InstallConfig holds tool installation settings. type InstallConfig struct { // ToolsDir is where to install the tool. diff --git a/internal/adapter/checkstyle/adapter.go b/internal/adapter/checkstyle/adapter.go new file mode 100644 index 0000000..b5274f4 --- /dev/null +++ b/internal/adapter/checkstyle/adapter.go @@ -0,0 +1,193 @@ +package checkstyle + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + + "github.com/DevSymphony/sym-cli/internal/adapter" +) + +const ( + // DefaultVersion is the default Checkstyle version. + DefaultVersion = "10.26.1" + + // GitHubReleaseURL is the GitHub Releases base URL for Checkstyle. + GitHubReleaseURL = "https://github.com/checkstyle/checkstyle/releases/download" +) + +// Adapter wraps Checkstyle for Java validation. +// +// Checkstyle handles: +// - Pattern rules: naming conventions, regex patterns +// - Length rules: line length, file length +// - Style rules: indentation, whitespace +// - Naming rules: class names, method names, variable names +type Adapter struct { + // ToolsDir is where Checkstyle JAR is stored. + // Default: ~/.sym/tools + ToolsDir string + + // WorkDir is the project root. + WorkDir string + + // JavaPath is the path to java executable. + // Empty = use system java + JavaPath string + + // executor runs subprocess + executor *adapter.SubprocessExecutor +} + +// NewAdapter creates a new Checkstyle adapter. +func NewAdapter(toolsDir, workDir string) *Adapter { + if toolsDir == "" { + home, _ := os.UserHomeDir() + toolsDir = filepath.Join(home, ".sym", "tools") + } + + javaPath, _ := exec.LookPath("java") + + return &Adapter{ + ToolsDir: toolsDir, + WorkDir: workDir, + JavaPath: javaPath, + executor: adapter.NewSubprocessExecutor(), + } +} + +// Name returns the adapter name. +func (a *Adapter) Name() string { + return "checkstyle" +} + +// GetCapabilities returns the Checkstyle adapter capabilities. +func (a *Adapter) GetCapabilities() adapter.AdapterCapabilities { + return adapter.AdapterCapabilities{ + Name: "checkstyle", + SupportedLanguages: []string{"java"}, + SupportedCategories: []string{"pattern", "length", "style", "naming"}, + Version: DefaultVersion, + } +} + +// CheckAvailability checks if Java and Checkstyle JAR are available. +func (a *Adapter) CheckAvailability(ctx context.Context) error { + // Check Java + if a.JavaPath == "" { + return fmt.Errorf("java not found: please install Java") + } + + // Verify Java version + cmd := exec.CommandContext(ctx, a.JavaPath, "-version") + if err := cmd.Run(); err != nil { + return fmt.Errorf("java execution failed: %w", err) + } + + // Check Checkstyle JAR + jarPath := a.getJARPath() + if _, err := os.Stat(jarPath); os.IsNotExist(err) { + return fmt.Errorf("checkstyle JAR not found at %s: run Install first", jarPath) + } + + return nil +} + +// Install downloads Checkstyle JAR from Maven Central. +func (a *Adapter) Install(ctx context.Context, config adapter.InstallConfig) error { + // Ensure tools directory exists + if err := os.MkdirAll(a.ToolsDir, 0755); err != nil { + return fmt.Errorf("failed to create tools dir: %w", err) + } + + // Determine version + version := config.Version + if version == "" { + version = DefaultVersion + } + + // Download URL - use GitHub Releases for -all.jar + jarName := fmt.Sprintf("checkstyle-%s-all.jar", version) + url := fmt.Sprintf("%s/checkstyle-%s/%s", GitHubReleaseURL, version, jarName) + + // Destination path + jarPath := filepath.Join(a.ToolsDir, jarName) + + // Check if already exists and not forcing reinstall + if !config.Force { + if _, err := os.Stat(jarPath); err == nil { + return nil // Already installed + } + } + + // Download + if err := a.downloadFile(ctx, url, jarPath); err != nil { + return fmt.Errorf("failed to download checkstyle: %w", 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) { + return a.execute(ctx, config, files) +} + +// ParseOutput converts Checkstyle JSON output to violations. +func (a *Adapter) ParseOutput(output *adapter.ToolOutput) ([]adapter.Violation, error) { + return parseOutput(output) +} + +// getJARPath returns the path to Checkstyle JAR. +func (a *Adapter) getJARPath() string { + return filepath.Join(a.ToolsDir, fmt.Sprintf("checkstyle-%s-all.jar", DefaultVersion)) +} + +// downloadFile downloads a file from URL to destPath. +func (a *Adapter) downloadFile(ctx context.Context, url, destPath string) error { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download failed: HTTP %d", resp.StatusCode) + } + + // Create temp file + tempFile := destPath + ".tmp" + out, err := os.Create(tempFile) + if err != nil { + return err + } + defer func() { _ = out.Close() }() + + // Copy content + if _, err := io.Copy(out, resp.Body); err != nil { + _ = os.Remove(tempFile) + return err + } + + // Rename temp to final + if err := os.Rename(tempFile, destPath); err != nil { + _ = os.Remove(tempFile) + return err + } + + return nil +} diff --git a/internal/adapter/checkstyle/config.go b/internal/adapter/checkstyle/config.go new file mode 100644 index 0000000..8ae5a91 --- /dev/null +++ b/internal/adapter/checkstyle/config.go @@ -0,0 +1,236 @@ +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/checkstyle/executor.go b/internal/adapter/checkstyle/executor.go new file mode 100644 index 0000000..b33f3f6 --- /dev/null +++ b/internal/adapter/checkstyle/executor.go @@ -0,0 +1,84 @@ +package checkstyle + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/DevSymphony/sym-cli/internal/adapter" +) + +// execute runs Checkstyle with the given config and files. +func (a *Adapter) execute(ctx context.Context, config []byte, files []string) (*adapter.ToolOutput, error) { + if len(files) == 0 { + return &adapter.ToolOutput{ + Stdout: "", + Stderr: "", + ExitCode: 0, + Duration: "0s", + }, nil + } + + // Create temp config file + configFile, err := a.createTempConfig(config) + if err != nil { + return nil, fmt.Errorf("failed to create temp config: %w", err) + } + defer func() { _ = os.Remove(configFile) }() + + // Build command + jarPath := a.getJARPath() + + args := []string{ + "-jar", jarPath, + "-c", configFile, + "-f", "xml", // XML output format + } + args = append(args, files...) + + // Execute + start := time.Now() + a.executor.WorkDir = a.WorkDir + + output, err := a.executor.Execute(ctx, a.JavaPath, args...) + duration := time.Since(start) + + if output == nil { + output = &adapter.ToolOutput{ + Stdout: "", + Stderr: "", + ExitCode: 1, + Duration: duration.String(), + } + if err != nil { + output.Stderr = err.Error() + } + } else { + output.Duration = duration.String() + } + + // Checkstyle returns non-zero exit code when violations are found + // This is expected, not an error + if err != nil && output.ExitCode != 0 { + // Only return error if it's not a violations-found error + if output.Stdout == "" && output.Stderr != "" { + return output, fmt.Errorf("checkstyle execution failed: %w", err) + } + } + + return output, nil +} + +// createTempConfig creates a temporary config file. +func (a *Adapter) createTempConfig(config []byte) (string, error) { + // Create temp file in tools directory + tempFile := filepath.Join(a.ToolsDir, "checkstyle-config-temp.xml") + + if err := os.WriteFile(tempFile, config, 0644); err != nil { + return "", err + } + + return tempFile, nil +} diff --git a/internal/adapter/checkstyle/parser.go b/internal/adapter/checkstyle/parser.go new file mode 100644 index 0000000..7e2b3f7 --- /dev/null +++ b/internal/adapter/checkstyle/parser.go @@ -0,0 +1,105 @@ +package checkstyle + +import ( + "encoding/xml" + "fmt" + "strings" + + "github.com/DevSymphony/sym-cli/internal/adapter" +) + +// CheckstyleOutput represents the XML output from Checkstyle. +type CheckstyleOutput struct { + XMLName xml.Name `xml:"checkstyle"` + Files []CheckstyleFile `xml:"file"` +} + +// CheckstyleFile represents a file with violations in Checkstyle output. +type CheckstyleFile struct { + Name string `xml:"name,attr"` + Errors []CheckstyleError `xml:"error"` +} + +// CheckstyleError represents a single violation in Checkstyle output. +type CheckstyleError struct { + Line int `xml:"line,attr"` + Column int `xml:"column,attr"` + Severity string `xml:"severity,attr"` + Message string `xml:"message,attr"` + Source string `xml:"source,attr"` +} + +// parseOutput converts Checkstyle XML output to violations. +func parseOutput(output *adapter.ToolOutput) ([]adapter.Violation, error) { + if output == nil { + return nil, fmt.Errorf("output is nil") + } + + // If no output and exit code 0, no violations + if output.Stdout == "" && output.ExitCode == 0 { + return []adapter.Violation{}, nil + } + + // Parse XML output + var result CheckstyleOutput + if err := xml.Unmarshal([]byte(output.Stdout), &result); err != nil { + // If XML parsing fails, try to extract errors from stderr + if output.Stderr != "" { + return nil, fmt.Errorf("checkstyle failed: %s", output.Stderr) + } + return nil, fmt.Errorf("failed to parse checkstyle output: %w", err) + } + + // Convert to violations + violations := make([]adapter.Violation, 0) + + for _, file := range result.Files { + for _, err := range file.Errors { + violations = append(violations, adapter.Violation{ + File: file.Name, + Line: err.Line, + Column: err.Column, + Message: err.Message, + Severity: mapSeverity(err.Severity), + RuleID: extractRuleID(err.Source), + }) + } + } + + return violations, nil +} + +// mapSeverity maps Checkstyle severity to standard severity. +func mapSeverity(severity string) string { + switch strings.ToLower(severity) { + case "error": + return "error" + case "warning", "warn": + return "warning" + case "info": + return "info" + default: + return "warning" + } +} + +// extractRuleID extracts the rule ID from Checkstyle source string. +// Example: "com.puppycrawl.tools.checkstyle.checks.naming.TypeNameCheck" -> "TypeName" +func extractRuleID(source string) string { + if source == "" { + return "unknown" + } + + // Split by dots and get the last part + parts := strings.Split(source, ".") + if len(parts) == 0 { + return source + } + + lastPart := parts[len(parts)-1] + + // Remove "Check" suffix if present + lastPart = strings.TrimSuffix(lastPart, "Check") + + return lastPart +} diff --git a/internal/adapter/eslint/adapter.go b/internal/adapter/eslint/adapter.go index 0e5de17..1afc6d4 100644 --- a/internal/adapter/eslint/adapter.go +++ b/internal/adapter/eslint/adapter.go @@ -50,6 +50,16 @@ func (a *Adapter) Name() string { return "eslint" } +// GetCapabilities returns the ESLint adapter capabilities. +func (a *Adapter) GetCapabilities() adapter.AdapterCapabilities { + return adapter.AdapterCapabilities{ + Name: "eslint", + SupportedLanguages: []string{"javascript", "typescript", "jsx", "tsx"}, + SupportedCategories: []string{"pattern", "length", "style", "ast"}, + Version: "^8.0.0", + } +} + // CheckAvailability checks if ESLint is installed. func (a *Adapter) CheckAvailability(ctx context.Context) error { // Try local installation first diff --git a/internal/adapter/pmd/adapter.go b/internal/adapter/pmd/adapter.go new file mode 100644 index 0000000..91a1dde --- /dev/null +++ b/internal/adapter/pmd/adapter.go @@ -0,0 +1,215 @@ +package pmd + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/DevSymphony/sym-cli/internal/adapter" +) + +const ( + // DefaultVersion is the default PMD version. + DefaultVersion = "7.0.0" + + // GitHubReleaseURL is the GitHub releases base URL. + GitHubReleaseURL = "https://github.com/pmd/pmd/releases/download" +) + +// Adapter wraps PMD for Java validation. +// +// PMD handles: +// - Pattern rules: custom XPath rules +// - Complexity rules: cyclomatic complexity, nesting depth +// - Performance rules: inefficient code patterns +// - Security rules: hardcoded credentials, SQL injection +// - Error handling rules: empty catch blocks, exception handling +type Adapter struct { + // ToolsDir is where PMD is installed. + // Default: ~/.sym/tools + ToolsDir string + + // WorkDir is the project root. + WorkDir string + + // PMDPath is the path to pmd executable. + // Empty = use default location + PMDPath string + + // executor runs subprocess + executor *adapter.SubprocessExecutor +} + +// NewAdapter creates a new PMD adapter. +func NewAdapter(toolsDir, workDir string) *Adapter { + if toolsDir == "" { + home, _ := os.UserHomeDir() + toolsDir = filepath.Join(home, ".sym", "tools") + } + + return &Adapter{ + ToolsDir: toolsDir, + WorkDir: workDir, + executor: adapter.NewSubprocessExecutor(), + } +} + +// Name returns the adapter name. +func (a *Adapter) Name() string { + return "pmd" +} + +// GetCapabilities returns the PMD adapter capabilities. +func (a *Adapter) GetCapabilities() adapter.AdapterCapabilities { + return adapter.AdapterCapabilities{ + Name: "pmd", + SupportedLanguages: []string{"java"}, + SupportedCategories: []string{"pattern", "complexity", "performance", "security", "error_handling", "ast"}, + Version: DefaultVersion, + } +} + +// CheckAvailability checks if PMD is available. +func (a *Adapter) CheckAvailability(ctx context.Context) error { + pmdPath := a.getPMDPath() + + // Check if PMD binary exists + if _, err := os.Stat(pmdPath); os.IsNotExist(err) { + return fmt.Errorf("pmd not found at %s: run Install first", pmdPath) + } + + // Try to run PMD version check + cmd := exec.CommandContext(ctx, pmdPath, "--version") + if err := cmd.Run(); err != nil { + return fmt.Errorf("pmd execution failed: %w", err) + } + + return nil +} + +// Install downloads and extracts PMD from GitHub releases. +func (a *Adapter) Install(ctx context.Context, config adapter.InstallConfig) error { + // Ensure tools directory exists + if err := os.MkdirAll(a.ToolsDir, 0755); err != nil { + return fmt.Errorf("failed to create tools dir: %w", err) + } + + // Determine version + version := config.Version + if version == "" { + version = DefaultVersion + } + + // PMD distribution filename + distName := fmt.Sprintf("pmd-dist-%s-bin.zip", version) + url := fmt.Sprintf("%s/pmd_releases%%2F%s/%s", GitHubReleaseURL, version, distName) + + // Destination paths + zipPath := filepath.Join(a.ToolsDir, distName) + extractDir := filepath.Join(a.ToolsDir, fmt.Sprintf("pmd-bin-%s", version)) + + // Check if already exists + if !config.Force { + if _, err := os.Stat(extractDir); err == nil { + return nil // Already installed + } + } + + // Download + if err := a.downloadFile(ctx, url, zipPath); err != nil { + return fmt.Errorf("failed to download PMD: %w", err) + } + defer func() { _ = os.Remove(zipPath) }() + + // Extract (simplified - in production use archive/zip) + // For now, assume unzip command is available + cmd := exec.CommandContext(ctx, "unzip", "-q", "-o", zipPath, "-d", a.ToolsDir) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to extract PMD: %w (try installing unzip)", err) + } + + // Make PMD binary executable + pmdBin := a.getPMDPath() + if err := os.Chmod(pmdBin, 0755); err != nil { + return fmt.Errorf("failed to make PMD executable: %w", 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) { + return a.execute(ctx, config, files) +} + +// ParseOutput converts PMD JSON output to violations. +func (a *Adapter) ParseOutput(output *adapter.ToolOutput) ([]adapter.Violation, error) { + return parseOutput(output) +} + +// getPMDPath returns the path to PMD binary. +func (a *Adapter) getPMDPath() string { + if a.PMDPath != "" { + return a.PMDPath + } + + pmdDir := filepath.Join(a.ToolsDir, fmt.Sprintf("pmd-bin-%s", DefaultVersion)) + + // PMD binary name depends on OS + binName := "pmd" + if runtime.GOOS == "windows" { + binName = "pmd.bat" + } + + return filepath.Join(pmdDir, "bin", binName) +} + +// downloadFile downloads a file from URL to destPath. +func (a *Adapter) downloadFile(ctx context.Context, url, destPath string) error { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download failed: HTTP %d", resp.StatusCode) + } + + // Create temp file + tempFile := destPath + ".tmp" + out, err := os.Create(tempFile) + if err != nil { + return err + } + defer func() { _ = out.Close() }() + + // Copy content + if _, err := io.Copy(out, resp.Body); err != nil { + _ = os.Remove(tempFile) + return err + } + + // Rename temp to final + if err := os.Rename(tempFile, destPath); err != nil { + _ = os.Remove(tempFile) + return err + } + + return nil +} diff --git a/internal/adapter/pmd/config.go b/internal/adapter/pmd/config.go new file mode 100644 index 0000000..ea8751a --- /dev/null +++ b/internal/adapter/pmd/config.go @@ -0,0 +1,134 @@ +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/pmd/executor.go b/internal/adapter/pmd/executor.go new file mode 100644 index 0000000..25c14c3 --- /dev/null +++ b/internal/adapter/pmd/executor.go @@ -0,0 +1,90 @@ +package pmd + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/DevSymphony/sym-cli/internal/adapter" +) + +// execute runs PMD with the given config and files. +func (a *Adapter) execute(ctx context.Context, config []byte, files []string) (*adapter.ToolOutput, error) { + if len(files) == 0 { + return &adapter.ToolOutput{ + Stdout: "", + Stderr: "", + ExitCode: 0, + Duration: "0s", + }, nil + } + + // Create temp ruleset file + rulesetFile, err := a.createTempRuleset(config) + if err != nil { + return nil, fmt.Errorf("failed to create temp ruleset: %w", err) + } + defer func() { _ = os.Remove(rulesetFile) }() + + // Build command + pmdPath := a.getPMDPath() + + // PMD command format: pmd check -d -R -f json + args := []string{ + "check", + "-d", strings.Join(files, ","), // Comma-separated file list + "-R", rulesetFile, + "-f", "json", // JSON output format + "--no-cache", // Disable cache for consistent results + } + + // Execute + start := time.Now() + a.executor.WorkDir = a.WorkDir + + output, err := a.executor.Execute(ctx, pmdPath, args...) + duration := time.Since(start) + + if output == nil { + output = &adapter.ToolOutput{ + Stdout: "", + Stderr: "", + ExitCode: 1, + Duration: duration.String(), + } + if err != nil { + output.Stderr = err.Error() + } + } else { + output.Duration = duration.String() + } + + // PMD returns exit code 4 when violations are found + // This is expected, not an error + if err != nil && (output.ExitCode == 4 || output.ExitCode == 0) { + // Not an actual error, just violations found + err = nil + } + + // Only return error if it's a real execution error + if err != nil && output.Stdout == "" && output.Stderr != "" { + return output, fmt.Errorf("PMD execution failed: %w", err) + } + + return output, nil +} + +// createTempRuleset creates a temporary ruleset file. +func (a *Adapter) createTempRuleset(config []byte) (string, error) { + // Create temp file in tools directory + tempFile := filepath.Join(a.ToolsDir, "pmd-ruleset-temp.xml") + + if err := os.WriteFile(tempFile, config, 0644); err != nil { + return "", err + } + + return tempFile, nil +} diff --git a/internal/adapter/pmd/parser.go b/internal/adapter/pmd/parser.go new file mode 100644 index 0000000..3c25023 --- /dev/null +++ b/internal/adapter/pmd/parser.go @@ -0,0 +1,108 @@ +package pmd + +import ( + "encoding/json" + "fmt" + + "github.com/DevSymphony/sym-cli/internal/adapter" +) + +// PMDOutput represents the JSON output from PMD. +type PMDOutput struct { + FormatVersion int `json:"formatVersion"` + PMDVersion string `json:"pmdVersion"` + Files []PMDFile `json:"files"` + ProcessingErrors []PMDProcessingError `json:"processingErrors"` +} + +// PMDFile represents a file with violations in PMD output. +type PMDFile struct { + Filename string `json:"filename"` + Violations []PMDViolation `json:"violations"` +} + +// PMDViolation represents a single violation in PMD output. +type PMDViolation struct { + BeginLine int `json:"beginLine"` + BeginColumn int `json:"beginColumn"` + EndLine int `json:"endLine"` + EndColumn int `json:"endColumn"` + Description string `json:"description"` + Rule string `json:"rule"` + RuleSet string `json:"ruleSet"` + Priority int `json:"priority"` + ExternalInfo string `json:"externalInfoUrl"` +} + +// PMDProcessingError represents an error during PMD analysis. +type PMDProcessingError struct { + Filename string `json:"filename"` + Message string `json:"message"` +} + +// parseOutput converts PMD JSON output to violations. +func parseOutput(output *adapter.ToolOutput) ([]adapter.Violation, error) { + if output == nil { + return nil, fmt.Errorf("output is nil") + } + + // If no output and exit code 0, no violations + if output.Stdout == "" && output.ExitCode == 0 { + return []adapter.Violation{}, nil + } + + // Parse JSON output + var result PMDOutput + if err := json.Unmarshal([]byte(output.Stdout), &result); err != nil { + // If JSON parsing fails, try to extract errors from stderr + if output.Stderr != "" { + return nil, fmt.Errorf("PMD failed: %s", output.Stderr) + } + return nil, fmt.Errorf("failed to parse PMD output: %w", err) + } + + // Convert to violations + violations := make([]adapter.Violation, 0) + + for _, file := range result.Files { + for _, v := range file.Violations { + violations = append(violations, adapter.Violation{ + File: file.Filename, + Line: v.BeginLine, + Column: v.BeginColumn, + Message: v.Description, + Severity: mapPriority(v.Priority), + RuleID: v.Rule, + }) + } + } + + // Add processing errors as violations + for _, err := range result.ProcessingErrors { + violations = append(violations, adapter.Violation{ + File: err.Filename, + Line: 0, + Column: 0, + Message: fmt.Sprintf("Processing error: %s", err.Message), + Severity: "error", + RuleID: "PMDProcessingError", + }) + } + + return violations, nil +} + +// mapPriority maps PMD priority to standard severity. +// PMD priority: 1 (high) to 5 (low) +func mapPriority(priority int) string { + switch priority { + case 1, 2: + return "error" + case 3: + return "warning" + case 4, 5: + return "info" + default: + return "warning" + } +} diff --git a/internal/adapter/prettier/adapter.go b/internal/adapter/prettier/adapter.go index cef88d8..1228d92 100644 --- a/internal/adapter/prettier/adapter.go +++ b/internal/adapter/prettier/adapter.go @@ -43,6 +43,16 @@ func (a *Adapter) Name() string { return "prettier" } +// GetCapabilities returns the Prettier adapter capabilities. +func (a *Adapter) GetCapabilities() adapter.AdapterCapabilities { + return adapter.AdapterCapabilities{ + Name: "prettier", + SupportedLanguages: []string{"javascript", "typescript", "jsx", "tsx", "json", "yaml", "css", "html", "markdown"}, + SupportedCategories: []string{"style"}, + Version: "^3.0.0", + } +} + // CheckAvailability checks if Prettier is installed. func (a *Adapter) CheckAvailability(ctx context.Context) error { prettierPath := a.getPrettierPath() diff --git a/internal/adapter/registry/errors.go b/internal/adapter/registry/errors.go new file mode 100644 index 0000000..683013a --- /dev/null +++ b/internal/adapter/registry/errors.go @@ -0,0 +1,25 @@ +package registry + +import "fmt" + +// ErrAdapterNotFound is returned when no adapter is found for the given criteria. +type ErrAdapterNotFound struct { + Language string + Category 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) +} + +// ErrNilAdapter is returned when trying to register a nil adapter. +var ErrNilAdapter = fmt.Errorf("cannot register nil adapter") diff --git a/internal/adapter/registry/init.go b/internal/adapter/registry/init.go new file mode 100644 index 0000000..f19c5b6 --- /dev/null +++ b/internal/adapter/registry/init.go @@ -0,0 +1,44 @@ +package registry + +import ( + "os" + "path/filepath" + + "github.com/DevSymphony/sym-cli/internal/adapter/checkstyle" + "github.com/DevSymphony/sym-cli/internal/adapter/eslint" + "github.com/DevSymphony/sym-cli/internal/adapter/pmd" + "github.com/DevSymphony/sym-cli/internal/adapter/prettier" + "github.com/DevSymphony/sym-cli/internal/adapter/tsc" +) + +// DefaultRegistry creates and populates a registry with all available adapters. +func DefaultRegistry() *Registry { + reg := NewRegistry() + + // Determine tools directory + toolsDir := getToolsDir() + workDir := getWorkDir() + + // Register JavaScript/TypeScript adapters + _ = reg.Register(eslint.NewAdapter(toolsDir, workDir)) + _ = reg.Register(prettier.NewAdapter(toolsDir, workDir)) + _ = reg.Register(tsc.NewAdapter(toolsDir, workDir)) + + // Register Java adapters + _ = reg.Register(checkstyle.NewAdapter(toolsDir, workDir)) + _ = reg.Register(pmd.NewAdapter(toolsDir, workDir)) + + return reg +} + +// getToolsDir returns the default tools directory. +func getToolsDir() string { + home, _ := os.UserHomeDir() + return filepath.Join(home, ".sym", "tools") +} + +// getWorkDir returns the current working directory. +func getWorkDir() string { + wd, _ := os.Getwd() + return wd +} diff --git a/internal/adapter/registry/registry.go b/internal/adapter/registry/registry.go new file mode 100644 index 0000000..8d5e07b --- /dev/null +++ b/internal/adapter/registry/registry.go @@ -0,0 +1,125 @@ +package registry + +import ( + "sync" + + "github.com/DevSymphony/sym-cli/internal/adapter" +) + +// Registry manages adapter instances and provides capability-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 +} + +// NewRegistry creates a new empty adapter registry. +func NewRegistry() *Registry { + return &Registry{ + adapters: make([]adapter.Adapter, 0), + languageCache: make(map[string][]adapter.Adapter), + } +} + +// Register adds an adapter to the registry. +// Returns ErrNilAdapter if the adapter is nil. +func (r *Registry) Register(adp adapter.Adapter) error { + if adp == nil { + return ErrNilAdapter + } + + 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) + } + + 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) { + 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 + } + } + + return nil, &ErrAdapterNotFound{Language: language, Category: category} +} + +// GetAll returns all registered adapters. +func (r *Registry) GetAll() []interface{} { + 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 + } + 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 { + 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 + } + } + return false +} diff --git a/internal/adapter/tsc/adapter.go b/internal/adapter/tsc/adapter.go index 037f696..a7b72d4 100644 --- a/internal/adapter/tsc/adapter.go +++ b/internal/adapter/tsc/adapter.go @@ -48,6 +48,16 @@ func (a *Adapter) Name() string { return "tsc" } +// GetCapabilities returns the TSC adapter capabilities. +func (a *Adapter) GetCapabilities() adapter.AdapterCapabilities { + return adapter.AdapterCapabilities{ + Name: "tsc", + SupportedLanguages: []string{"typescript"}, + SupportedCategories: []string{"typechecker"}, + Version: "^5.0.0", + } +} + // CheckAvailability checks if tsc is installed. func (a *Adapter) CheckAvailability(ctx context.Context) error { // Try local installation first @@ -101,6 +111,7 @@ func (a *Adapter) Install(ctx context.Context, config adapter.InstallConfig) err return nil } + // Execute runs tsc with the given config and files. // Returns type checking results. func (a *Adapter) Execute(ctx context.Context, config []byte, files []string) (*adapter.ToolOutput, error) { diff --git a/internal/adapter/tsc/config.go b/internal/adapter/tsc/config.go index 42c655f..85c6a14 100644 --- a/internal/adapter/tsc/config.go +++ b/internal/adapter/tsc/config.go @@ -3,6 +3,8 @@ package tsc import ( "encoding/json" "fmt" + + "github.com/DevSymphony/sym-cli/internal/engine/core" ) // TSConfig represents TypeScript compiler configuration. @@ -36,7 +38,13 @@ type CompilerOptions struct { } // GenerateConfig generates a tsconfig.json from a rule. -func (a *Adapter) GenerateConfig(rule interface{}) ([]byte, error) { +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{ @@ -62,12 +70,8 @@ func (a *Adapter) GenerateConfig(rule interface{}) ([]byte, error) { }, } - // If rule is a map, extract type checking options - if ruleMap, ok := rule.(map[string]interface{}); ok { - if check, ok := ruleMap["check"].(map[string]interface{}); ok { - applyRuleConfig(&config, check) - } - } + // Apply rule-specific configuration from Check map + applyRuleConfig(&config, rule.Check) return json.MarshalIndent(config, "", " ") } diff --git a/internal/adapter/tsc/config_test.go b/internal/adapter/tsc/config_test.go index 278d212..66ca839 100644 --- a/internal/adapter/tsc/config_test.go +++ b/internal/adapter/tsc/config_test.go @@ -3,12 +3,19 @@ package tsc import ( "encoding/json" "testing" + + "github.com/DevSymphony/sym-cli/internal/engine/core" ) func TestGenerateConfig_Default(t *testing.T) { adapter := NewAdapter("", "/test/project") - config, err := adapter.GenerateConfig(nil) + rule := &core.Rule{ + ID: "test-default", + Check: map[string]interface{}{}, + } + + config, err := adapter.GenerateConfig(rule) if err != nil { t.Fatalf("GenerateConfig() error = %v", err) } @@ -43,8 +50,9 @@ func TestGenerateConfig_Default(t *testing.T) { func TestGenerateConfig_WithRuleOptions(t *testing.T) { adapter := NewAdapter("", "/test/project") - rule := map[string]interface{}{ - "check": map[string]interface{}{ + rule := &core.Rule{ + ID: "test-with-options", + Check: map[string]interface{}{ "strict": false, "noImplicitAny": false, "allowJs": true, @@ -88,8 +96,9 @@ func TestGenerateConfig_WithRuleOptions(t *testing.T) { func TestGenerateConfig_WithIncludeExclude(t *testing.T) { adapter := NewAdapter("", "/test/project") - rule := map[string]interface{}{ - "check": map[string]interface{}{ + rule := &core.Rule{ + ID: "test-include-exclude", + Check: map[string]interface{}{ "include": []interface{}{"src/**/*.ts", "lib/**/*.ts"}, "exclude": []interface{}{"**/*.test.ts", "dist/**"}, }, @@ -127,7 +136,12 @@ func TestGenerateConfig_WithIncludeExclude(t *testing.T) { func TestGenerateConfig_ValidJSON(t *testing.T) { adapter := NewAdapter("", "/test/project") - config, err := adapter.GenerateConfig(nil) + 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) } diff --git a/internal/adapter/tsc/executor.go b/internal/adapter/tsc/executor.go index 8c4fd74..718b4f9 100644 --- a/internal/adapter/tsc/executor.go +++ b/internal/adapter/tsc/executor.go @@ -2,6 +2,7 @@ package tsc import ( "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -11,9 +12,26 @@ import ( // execute runs tsc with the given configuration. func (a *Adapter) execute(ctx context.Context, config []byte, files []string) (*adapter.ToolOutput, error) { + // Parse config and add files to check + var tsconfig map[string]interface{} + if err := json.Unmarshal(config, &tsconfig); err != nil { + return nil, fmt.Errorf("failed to parse tsconfig: %w", err) + } + + // Add files to tsconfig if specific files are provided + if len(files) > 0 { + tsconfig["files"] = files + } + + // Marshal updated config + updatedConfig, err := json.MarshalIndent(tsconfig, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal updated tsconfig: %w", err) + } + // Write tsconfig.json to a temporary location configPath := filepath.Join(a.WorkDir, ".symphony-tsconfig.json") - if err := os.WriteFile(configPath, config, 0644); err != nil { + if err := os.WriteFile(configPath, updatedConfig, 0644); err != nil { return nil, fmt.Errorf("failed to write tsconfig: %w", err) } defer func() { _ = os.Remove(configPath) }() @@ -26,19 +44,13 @@ func (a *Adapter) execute(ctx context.Context, config []byte, files []string) (* } // Build tsc command - // Use --noEmit to only check types without generating output - // Use --pretty false to get machine-readable output + // Use --project to read config, --noEmit to only check types, --pretty false for machine-readable output args := []string{ "--project", configPath, "--noEmit", "--pretty", "false", } - // If specific files are provided, add them - if len(files) > 0 { - args = append(args, files...) - } - // Execute tsc a.executor.WorkDir = a.WorkDir output, err := a.executor.Execute(ctx, tscPath, args...) diff --git a/internal/auth/README.md b/internal/auth/README.md new file mode 100644 index 0000000..c860b9f --- /dev/null +++ b/internal/auth/README.md @@ -0,0 +1,8 @@ +# auth + +GitHub OAuth 인증 플로우를 관리합니다. + +로컬 콜백 서버를 실행하여 OAuth 인증 과정을 처리하고 액세스 토큰을 발급받습니다. + +**사용자**: cmd +**의존성**: config, github diff --git a/internal/cmd/README.md b/internal/cmd/README.md new file mode 100644 index 0000000..6b72757 --- /dev/null +++ b/internal/cmd/README.md @@ -0,0 +1,8 @@ +# cmd + +sym CLI 커맨드를 구현합니다. + +Cobra 기반으로 validate, convert, policy, login, dashboard 등의 명령어를 제공합니다. + +**사용자**: 없음 (진입점) +**의존성**: auth, config, converter, git, github, llm, mcp, policy, roles, server, validator diff --git a/internal/config/README.md b/internal/config/README.md new file mode 100644 index 0000000..cd57d9d --- /dev/null +++ b/internal/config/README.md @@ -0,0 +1,8 @@ +# config + +전역 설정 및 인증 토큰을 관리합니다. + +`~/.config/sym/` 디렉토리에 설정 파일과 GitHub 액세스 토큰을 저장하고 로드합니다. + +**사용자**: auth, cmd, server +**의존성**: 없음 diff --git a/internal/converter/README.md b/internal/converter/README.md new file mode 100644 index 0000000..4e04926 --- /dev/null +++ b/internal/converter/README.md @@ -0,0 +1,13 @@ +# converter + +UserPolicy(A Schema)를 CodePolicy(B Schema)로 변환합니다. + +자연어 규칙을 구조화된 검증 규칙으로 변환하고 ESLint, Prettier, Checkstyle, PMD 등의 linter 설정 파일을 자동 생성합니다. + +## 서브패키지 + +- `linters`: 각 linter별 설정 파일 생성기 (ESLint, Prettier, Checkstyle, PMD) +- `linters/registry`: Linter 변환기 등록 및 검색 시스템 + +**사용자**: cmd, mcp +**의존성**: llm, schema diff --git a/internal/engine/README.md b/internal/engine/README.md new file mode 100644 index 0000000..2cb5f44 --- /dev/null +++ b/internal/engine/README.md @@ -0,0 +1,19 @@ +# 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 index 4d5cfdf..084f8ff 100644 --- a/internal/engine/ast/engine.go +++ b/internal/engine/ast/engine.go @@ -3,17 +3,20 @@ 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 { - eslint *eslint.Adapter - toolsDir string - workDir string + adapterRegistry *adapterRegistry.Registry + toolsDir string + workDir string } // NewEngine creates a new AST engine. @@ -21,19 +24,21 @@ func NewEngine() *Engine { return &Engine{} } -// Init initializes the AST engine with ESLint adapter. +// 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 - e.eslint = eslint.NewAdapter(e.toolsDir, e.workDir) - - // Check ESLint availability - if err := e.eslint.CheckAvailability(ctx); err != nil { - // Try to install - if installErr := e.eslint.Install(ctx, adapter.InstallConfig{}); installErr != nil { - return fmt.Errorf("eslint not available and installation failed: %w", installErr) + // 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 @@ -51,11 +56,77 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( }, nil } - // Check initialization only when we have files to process - if e.eslint == 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 { @@ -76,19 +147,19 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( return nil, fmt.Errorf("failed to generate ESLint config: %w", err) } - // Execute ESLint - output, err := e.eslint.Execute(ctx, config, files) + // Execute + output, err := adp.Execute(ctx, config, files) if err != nil && output == nil { - return nil, fmt.Errorf("eslint execution failed: %w", err) + return nil, fmt.Errorf("execution failed: %w", err) } // Parse violations - adapterViolations, err := e.eslint.ParseOutput(output) + adapterViolations, err := adp.ParseOutput(output) if err != nil { - return nil, fmt.Errorf("failed to parse ESLint output: %w", err) + return nil, fmt.Errorf("failed to parse output: %w", err) } - // Convert adapter.Violation to core.Violation + // Convert violations violations := make([]core.Violation, len(adapterViolations)) for i, v := range adapterViolations { violations[i] = core.Violation{ @@ -96,7 +167,7 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( Line: v.Line, Column: v.Column, Message: v.Message, - Severity: v.Severity, + Severity: v.Severity, RuleID: v.RuleID, } } @@ -110,13 +181,23 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( } // GetCapabilities returns the engine's capabilities. +// Supported languages are determined dynamically based on registered adapters. func (e *Engine) GetCapabilities() core.EngineCapabilities { - return core.EngineCapabilities{ + caps := core.EngineCapabilities{ Name: "ast", - SupportedLanguages: []string{"javascript", "typescript", "jsx", "tsx"}, 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. @@ -124,6 +205,40 @@ 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) diff --git a/internal/engine/core/engine.go b/internal/engine/core/engine.go index 9c067a9..f00f01d 100644 --- a/internal/engine/core/engine.go +++ b/internal/engine/core/engine.go @@ -5,6 +5,14 @@ import ( "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.). // @@ -57,6 +65,10 @@ type EngineConfig struct { // 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{} } diff --git a/internal/engine/length/engine.go b/internal/engine/length/engine.go index 892662d..4bce074 100644 --- a/internal/engine/length/engine.go +++ b/internal/engine/length/engine.go @@ -3,22 +3,23 @@ package length import ( "context" "fmt" + "path/filepath" + "strings" "time" - "github.com/DevSymphony/sym-cli/internal/adapter/eslint" + "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). // -// For JavaScript/TypeScript: -// - Uses ESLint max-len for line length -// - Uses ESLint max-lines for file length -// - Uses ESLint max-lines-per-function for function length -// - Uses ESLint max-params for parameter count +// 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 { - eslint *eslint.Adapter - config core.EngineConfig + adapterRegistry *adapterRegistry.Registry + config core.EngineConfig } // NewEngine creates a new length engine. @@ -26,32 +27,19 @@ func NewEngine() *Engine { return &Engine{} } -// Init initializes the engine. +// Init initializes the engine with adapter registry. func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { e.config = config - // Initialize ESLint adapter - e.eslint = eslint.NewAdapter(config.ToolsDir, config.WorkDir) - - // Check availability (same as pattern engine) - if err := e.eslint.CheckAvailability(ctx); err != nil { - if config.Debug { - fmt.Printf("ESLint not found, attempting install...\n") - } - - installConfig := struct { - ToolsDir string - Version string - Force bool - }{ - ToolsDir: config.ToolsDir, - Version: "", - Force: false, - } - - if err := e.eslint.Install(ctx, installConfig); err != nil { - return fmt.Errorf("failed to install ESLint: %w", err) + // 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 @@ -73,20 +61,47 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( }, nil } - // Generate ESLint config - config, err := e.eslint.GenerateConfig(&rule) + // 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("failed to generate config: %w", err) + 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) + } } - // Execute ESLint - output, err := e.eslint.Execute(ctx, config, files) + // Generate config + config, err := lengthAdapter.GenerateConfig(&rule) if err != nil { - return nil, fmt.Errorf("failed to execute ESLint: %w", err) + 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 := e.eslint.ParseOutput(output) + adapterViolations, err := lengthAdapter.ParseOutput(output) if err != nil { return nil, fmt.Errorf("failed to parse output: %w", err) } @@ -115,27 +130,29 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( Violations: violations, Duration: time.Since(start), Engine: "length", - Language: "javascript", + Language: language, }, nil } // GetCapabilities returns engine capabilities. +// Supported languages are determined dynamically based on registered adapters. func (e *Engine) GetCapabilities() core.EngineCapabilities { - return core.EngineCapabilities{ + caps := core.EngineCapabilities{ Name: "length", - SupportedLanguages: []string{"javascript", "typescript", "jsx", "tsx"}, SupportedCategories: []string{"formatting", "style"}, SupportsAutofix: false, RequiresCompilation: false, - ExternalTools: []core.ToolRequirement{ - { - Name: "eslint", - Version: "^8.0.0", - Optional: false, - InstallCommand: "npm install -g eslint", - }, - }, } + + // 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. @@ -147,3 +164,37 @@ func (e *Engine) Close() error { 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.go b/internal/engine/pattern/engine.go index e3ccff6..dbfbdf1 100644 --- a/internal/engine/pattern/engine.go +++ b/internal/engine/pattern/engine.go @@ -3,21 +3,23 @@ package pattern import ( "context" "fmt" + "path/filepath" + "strings" "time" - "github.com/DevSymphony/sym-cli/internal/adapter/eslint" + "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). // -// For JavaScript/TypeScript: -// - Uses ESLint id-match for identifier patterns -// - Uses ESLint no-restricted-syntax for content patterns -// - Uses ESLint no-restricted-imports for import patterns +// 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 { - eslint *eslint.Adapter - config core.EngineConfig + adapterRegistry *adapterRegistry.Registry + config core.EngineConfig } // NewEngine creates a new pattern engine. @@ -25,35 +27,19 @@ func NewEngine() *Engine { return &Engine{} } -// Init initializes the engine. +// Init initializes the engine with adapter registry. func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { e.config = config - // Initialize ESLint adapter - e.eslint = eslint.NewAdapter(config.ToolsDir, config.WorkDir) - - // Check if ESLint is available - if err := e.eslint.CheckAvailability(ctx); err != nil { - // Try to install - if config.Debug { - fmt.Printf("ESLint not found, attempting install...\n") - } - - installConfig := struct { - ToolsDir string - Version string - Force bool - }{ - ToolsDir: config.ToolsDir, - Version: "", - Force: false, - } - - // Convert to adapter.InstallConfig - // (Note: we'd need to import adapter package, but for now let's inline) - if err := e.eslint.Install(ctx, installConfig); err != nil { - return fmt.Errorf("failed to install ESLint: %w", err) + // 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 @@ -75,20 +61,47 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( }, nil } - // Generate ESLint config - config, err := e.eslint.GenerateConfig(&rule) + // 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("failed to generate config: %w", err) + 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) } - // Execute ESLint - output, err := e.eslint.Execute(ctx, config, files) + // 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 execute ESLint: %w", err) + 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 := e.eslint.ParseOutput(output) + adapterViolations, err := patternAdapter.ParseOutput(output) if err != nil { return nil, fmt.Errorf("failed to parse output: %w", err) } @@ -118,27 +131,29 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( Violations: violations, Duration: time.Since(start), Engine: "pattern", - Language: e.detectLanguage(files), + Language: language, }, nil } // GetCapabilities returns engine capabilities. +// Supported languages are determined dynamically based on registered adapters. func (e *Engine) GetCapabilities() core.EngineCapabilities { - return core.EngineCapabilities{ + caps := core.EngineCapabilities{ Name: "pattern", - SupportedLanguages: []string{"javascript", "typescript", "jsx", "tsx"}, SupportedCategories: []string{"naming", "security", "custom"}, SupportsAutofix: false, RequiresCompilation: false, - ExternalTools: []core.ToolRequirement{ - { - Name: "eslint", - Version: "^8.0.0", - Optional: false, - InstallCommand: "npm install -g eslint", - }, - }, } + + // 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. @@ -151,25 +166,36 @@ func (e *Engine) filterFiles(files []string, selector *core.Selector) []string { return core.FilterFiles(files, selector) } -// detectLanguage detects the language from file extensions. -func (e *Engine) detectLanguage(files []string) string { - if len(files) == 0 { - return "javascript" +// 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] } - // Check first file - file := files[0] - if len(file) > 3 { - ext := file[len(file)-3:] + // 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": + case ".jsx": return "jsx" - case "tsx": + 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 index f76ff28..ca9197d 100644 --- a/internal/engine/pattern/engine_test.go +++ b/internal/engine/pattern/engine_test.go @@ -168,20 +168,22 @@ func TestDetectLanguage(t *testing.T) { engine := &Engine{} tests := []struct { + rule core.Rule files []string want string }{ - {[]string{"main.js"}, "javascript"}, - {[]string{"app.jsx"}, "jsx"}, - {[]string{"server.ts"}, "typescript"}, - {[]string{"component.tsx"}, "tsx"}, - {[]string{}, "javascript"}, + {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.files) + got := engine.detectLanguage(tt.rule, tt.files) if got != tt.want { - t.Errorf("detectLanguage(%v) = %q, want %q", tt.files, got, tt.want) + t.Errorf("detectLanguage(%v, %v) = %q, want %q", tt.rule, tt.files, got, tt.want) } } } diff --git a/internal/engine/style/engine.go b/internal/engine/style/engine.go index ba397f4..3153322 100644 --- a/internal/engine/style/engine.go +++ b/internal/engine/style/engine.go @@ -3,20 +3,25 @@ package style import ( "context" "fmt" + "path/filepath" + "strings" "time" - "github.com/DevSymphony/sym-cli/internal/adapter/eslint" + "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.). // -// Strategy: -// - Validation: Use ESLint (indent, quotes, semi rules) -// - Autofix is not supported (removed by design) +// 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 { - eslint *eslint.Adapter - config core.EngineConfig + adapterRegistry *adapterRegistry.Registry + config core.EngineConfig } // NewEngine creates a new style engine. @@ -24,28 +29,19 @@ func NewEngine() *Engine { return &Engine{} } -// Init initializes the engine. +// Init initializes the engine with adapter registry. func (e *Engine) Init(ctx context.Context, config core.EngineConfig) error { e.config = config - // Initialize ESLint adapter - e.eslint = eslint.NewAdapter(config.ToolsDir, config.WorkDir) - - // Check ESLint availability - if err := e.eslint.CheckAvailability(ctx); err != nil { - if config.Debug { - fmt.Printf("ESLint not found, attempting install...\n") - } - - installConfig := struct { - ToolsDir string - Version string - Force bool - }{ToolsDir: config.ToolsDir} - - if err := e.eslint.Install(ctx, installConfig); err != nil { - return fmt.Errorf("failed to install ESLint: %w", err) + // 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 @@ -65,22 +61,49 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( }, nil } - // Generate ESLint config for validation - eslintConfig, err := e.eslint.GenerateConfig(&rule) + // 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("failed to generate ESLint config: %w", err) + 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) + } } - // Execute ESLint - output, err := e.eslint.Execute(ctx, eslintConfig, files) + // Generate config for validation + config, err := styleAdapter.GenerateConfig(&rule) if err != nil { - return nil, fmt.Errorf("failed to execute ESLint: %w", err) + 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 := e.eslint.ParseOutput(output) + adapterViolations, err := styleAdapter.ParseOutput(output) if err != nil { - return nil, fmt.Errorf("failed to parse ESLint output: %w", err) + return nil, fmt.Errorf("failed to parse output: %w", err) } // Convert to core violations @@ -107,27 +130,29 @@ func (e *Engine) Validate(ctx context.Context, rule core.Rule, files []string) ( Violations: violations, Duration: time.Since(start), Engine: "style", - Language: "javascript", + Language: language, }, nil } // GetCapabilities returns engine capabilities. +// Supported languages are determined dynamically based on registered adapters. func (e *Engine) GetCapabilities() core.EngineCapabilities { - return core.EngineCapabilities{ + caps := core.EngineCapabilities{ Name: "style", - SupportedLanguages: []string{"javascript", "typescript", "jsx", "tsx"}, SupportedCategories: []string{"style", "formatting"}, SupportsAutofix: false, // Autofix removed by design RequiresCompilation: false, - ExternalTools: []core.ToolRequirement{ - { - Name: "eslint", - Version: "^8.0.0", - Optional: false, - InstallCommand: "npm install -g eslint", - }, - }, } + + // 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. @@ -139,3 +164,37 @@ func (e *Engine) Close() error { 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/git/README.md b/internal/git/README.md new file mode 100644 index 0000000..b4d5337 --- /dev/null +++ b/internal/git/README.md @@ -0,0 +1,8 @@ +# git + +Git 저장소 작업을 위한 유틸리티를 제공합니다. + +저장소 루트 디렉토리 탐색 및 Git 상태 조회 기능을 제공합니다. + +**사용자**: cmd, mcp, policy, roles, server +**의존성**: 없음 diff --git a/internal/github/README.md b/internal/github/README.md new file mode 100644 index 0000000..5e67fe0 --- /dev/null +++ b/internal/github/README.md @@ -0,0 +1,8 @@ +# github + +GitHub API 클라이언트를 제공합니다. + +OAuth 인증 URL 생성, 사용자 정보 조회, 리포지토리 정보 조회 등의 기능을 제공합니다. + +**사용자**: auth, cmd, server +**의존성**: 없음 diff --git a/internal/llm/README.md b/internal/llm/README.md new file mode 100644 index 0000000..94bcc6f --- /dev/null +++ b/internal/llm/README.md @@ -0,0 +1,8 @@ +# llm + +OpenAI API 클라이언트를 제공합니다. + +LLM 기반 추론 및 검증을 지원하며 정책 변환 시 자연어 규칙 해석에 활용됩니다. + +**사용자**: cmd, converter, engine, mcp, validator +**의존성**: 없음 diff --git a/internal/mcp/README.md b/internal/mcp/README.md new file mode 100644 index 0000000..0fbc308 --- /dev/null +++ b/internal/mcp/README.md @@ -0,0 +1,8 @@ +# mcp + +Model Context Protocol 서버를 구현합니다. + +바이브코딩 도구와의 통신 인터페이스를 제공하며 정책 변환 및 검증 기능을 노출합니다. + +**사용자**: cmd +**의존성**: converter, git, llm, policy, validator diff --git a/internal/policy/README.md b/internal/policy/README.md new file mode 100644 index 0000000..d29a715 --- /dev/null +++ b/internal/policy/README.md @@ -0,0 +1,8 @@ +# policy + +정책 파일을 관리합니다. + +UserPolicy 및 CodePolicy 로드, 저장, 변경 이력 추적 기능을 제공하며 다양한 프레임워크 템플릿을 포함합니다. + +**사용자**: cmd, mcp, roles, server +**의존성**: git, schema diff --git a/internal/roles/README.md b/internal/roles/README.md new file mode 100644 index 0000000..98fd914 --- /dev/null +++ b/internal/roles/README.md @@ -0,0 +1,8 @@ +# roles + +RBAC(Role-Based Access Control)을 구현합니다. + +역할별 파일 접근 권한을 검증하며 glob 패턴 기반 읽기/쓰기/실행 권한 관리를 제공합니다. + +**사용자**: cmd, server +**의존성**: git, policy diff --git a/internal/server/README.md b/internal/server/README.md new file mode 100644 index 0000000..5405e11 --- /dev/null +++ b/internal/server/README.md @@ -0,0 +1,8 @@ +# server + +웹 대시보드 HTTP 서버를 제공합니다. + +정책 관리를 위한 REST API 및 정적 파일 제공 기능을 포함합니다. + +**사용자**: cmd +**의존성**: config, git, github, policy, roles diff --git a/internal/validator/README.md b/internal/validator/README.md new file mode 100644 index 0000000..82e8ef1 --- /dev/null +++ b/internal/validator/README.md @@ -0,0 +1,8 @@ +# validator + +코드 검증 오케스트레이터입니다. + +정책에 정의된 규칙을 바탕으로 검증 엔진을 조율하고 검증 결과를 수집하여 위반 사항을 보고합니다. RBAC 권한 검증도 통합되어 파일 접근 권한을 확인합니다. + +**사용자**: cmd, mcp +**의존성**: engine, llm, roles, git diff --git a/pkg/schema/README.md b/pkg/schema/README.md new file mode 100644 index 0000000..2deb0d2 --- /dev/null +++ b/pkg/schema/README.md @@ -0,0 +1,8 @@ +# schema + +중앙 데이터 구조를 정의합니다. + +UserPolicy(A Schema) 및 CodePolicy(B Schema) 타입을 정의하며, 전체 시스템의 데이터 흐름을 담당합니다. + +**사용자**: cmd, converter, llm, mcp, policy, roles, server, validator +**의존성**: 없음 diff --git a/testdata/README.md b/testdata/README.md index a30fe25..0267f2d 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -1,65 +1,111 @@ -# Testdata Directory +# Test Data Directory -This directory contains test files for integration and unit tests. +This directory contains test data files for integration testing of the validation engines. -## Structure +## Directory Structure ``` testdata/ -├── javascript/ # JavaScript test files -├── typescript/ # TypeScript test files -└── mixed/ # JSX/TSX test files +├── javascript/ +│ ├── pattern/ # Pattern matching and naming convention tests +│ ├── length/ # Line, file, and function length tests +│ ├── style/ # Code formatting and style tests +│ └── ast/ # AST-based structural tests +├── typescript/ +│ └── typechecker/ # Type checking tests +└── java/ + ├── pattern/ # Pattern matching and naming convention tests + ├── length/ # Line, method, and parameter length tests + ├── style/ # Code formatting and style tests + └── ast/ # AST-based structural tests ``` -## JavaScript Test Files +## Engine Types -### Naming Violations -- `naming-violations.js` - Contains snake_case, lowercase class names -- `bad-naming.js` - Additional naming convention violations +### Pattern Engine +Tests regex-based pattern matching and naming conventions. -### Style Violations -- `style-violations.js` - Indentation, quote, semicolon issues -- `bad-style.js` - Additional style violations +**JavaScript Files:** +- `naming-violations.js` - Snake_case and incorrect naming patterns +- `security-violations.js` - Hardcoded secrets and security issues +- `valid.js` - Correct naming conventions -### Length Violations -- `length-violations.js` - General length violations -- `long-lines.js` - Lines exceeding max length -- `long-function.js` - Functions exceeding max lines -- `long-file.js` - Files exceeding max lines -- `many-params.js` - Functions with too many parameters +**Java Files:** +- `NamingViolations.java` - Invalid class, method, variable names +- `ValidNaming.java` - Correct PascalCase and camelCase usage -### Security Violations -- `security-violations.js` - Hardcoded credentials -- `hardcoded-secrets.js` - API keys, passwords +### Length Engine +Tests line length, file length, and parameter count limits. -### AST/Error Handling -- `async-with-try.js` - Async code with proper try/catch -- `async-without-try.js` - Async code without error handling +**JavaScript Files:** +- `length-violations.js` - Long lines, long functions, too many parameters +- `valid.js` - Proper length constraints -### Import Violations -- `bad-imports.js` - Restricted import patterns +**Java Files:** +- `LengthViolations.java` - Long lines, long methods, too many parameters +- `ValidLength.java` - Proper length constraints -### Valid Code -- `valid.js` - Well-formatted, compliant code -- `good-code.js` - Additional valid examples -- `good-style.js` - Properly styled code +### Style Engine +Tests code formatting and style conventions. -## TypeScript Test Files +**JavaScript Files:** +- `style-violations.js` - Bad indentation, spacing, quotes +- `valid.js` - Proper formatting -- `type-errors.ts` - Type assignment errors +**Java Files:** +- `StyleViolations.java` - Inconsistent indentation, brace placement, spacing +- `ValidStyle.java` - Standard Java formatting + +### AST Engine +Tests structural patterns via Abstract Syntax Tree analysis. + +**JavaScript Files:** +- `naming-violations.js` - AST-level naming issues +- `valid.js` - Valid AST structure + +**Java Files:** +- `AstViolations.java` - Empty catch blocks, System.out usage, missing docs +- `ValidAst.java` - Proper exception handling and structure + +### TypeChecker Engine +Tests type safety and TypeScript-specific checks. + +**TypeScript Files:** +- `type-errors.ts` - Type mismatches and errors - `strict-mode-errors.ts` - Strict mode violations -- `valid.ts` - Valid TypeScript code +- `valid.ts` - Correct type usage -## Mixed Files +## File Naming Conventions -- `component.jsx` - React JSX component -- `component.tsx` - React TypeScript component +- **Violations**: Files containing rule violations are named `*-violations.*` or `*Violations.*` +- **Valid**: Files with compliant code are named `valid.*` or `Valid*.*` +- **Specific**: Files testing specific issues use descriptive names (e.g., `security-violations.js`) -## Usage +## Adding New Test Data -Integration tests reference these files via: -```go -filepath.Join(workDir, "testdata/javascript/naming-violations.js") -``` +When adding new test data: + +1. Choose the appropriate engine directory (`pattern`, `length`, `style`, `ast`, `typechecker`) +2. Create both violation and valid files for comprehensive testing +3. Add clear comments explaining what each violation tests +4. Update integration tests to reference new files +5. Ensure files compile/parse correctly for their language + +## Integration Test Usage + +These files are referenced by integration tests in `tests/integration/*_integration_test.go`: + +- `pattern_integration_test.go` - Uses pattern engine test data +- `length_integration_test.go` - Uses length engine test data +- `style_integration_test.go` - Uses style engine test data +- `ast_integration_test.go` - Uses ast engine test data +- `typechecker_integration_test.go` - Uses typechecker engine test data + +## Validation Engines + +Each engine uses specific adapters: + +- **JavaScript/TypeScript**: ESLint, Prettier, TSC +- **Java**: Checkstyle, PMD -Where `workDir` is the project root directory. +Test data files should reflect the validation capabilities of these underlying tools. diff --git a/testdata/java/ast/AstViolations.java b/testdata/java/ast/AstViolations.java new file mode 100644 index 0000000..43962ec --- /dev/null +++ b/testdata/java/ast/AstViolations.java @@ -0,0 +1,59 @@ +/** + * Test file with AST-level violations + * Contains structural issues detectable via AST analysis + */ +package com.example; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; + +public class AstViolations { + + // VIOLATION: System.out usage in production code + public void debugPrint(String message) { + System.out.println("Debug: " + message); + } + + // VIOLATION: File I/O without try-catch + public String readFileUnsafe(String path) { + FileReader reader = new FileReader(path); + return "content"; + } + + // VIOLATION: Empty catch block + public void emptyCatch() { + try { + riskyOperation(); + } catch (Exception e) { + // Empty catch - swallows exception + } + } + + // VIOLATION: Generic exception catch + public void catchGeneric() { + try { + riskyOperation(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // VIOLATION: Missing method documentation + public int calculate(int a, int b, int c) { + return a + b * c; + } + + private void riskyOperation() throws IOException { + File file = new File("test.txt"); + if (!file.exists()) { + throw new IOException("File not found"); + } + } + + public static void main(String[] args) { + AstViolations obj = new AstViolations(); + obj.debugPrint("test"); + obj.emptyCatch(); + } +} diff --git a/testdata/java/ast/ValidAst.java b/testdata/java/ast/ValidAst.java new file mode 100644 index 0000000..004b1cd --- /dev/null +++ b/testdata/java/ast/ValidAst.java @@ -0,0 +1,77 @@ +/** + * Test file with valid AST structure + * Demonstrates proper exception handling and code structure + */ +package com.example; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.logging.Logger; + +public class ValidAst { + + private static final Logger LOGGER = Logger.getLogger(ValidAst.class.getName()); + + /** + * Reads file content safely with proper exception handling + * + * @param path the file path to read + * @return the file content + * @throws IOException if file cannot be read + */ + public String readFileSafe(String path) throws IOException { + try (FileReader reader = new FileReader(path)) { + return "content"; + } catch (IOException e) { + LOGGER.severe("Failed to read file: " + path); + throw e; + } + } + + /** + * Performs calculation with proper error handling + * + * @param a first operand + * @param b second operand + * @param c third operand + * @return calculated result + */ + public int calculate(int a, int b, int c) { + return a + b * c; + } + + /** + * Processes data with specific exception handling + */ + public void processWithSpecificCatch() { + try { + riskyOperation(); + } catch (IOException e) { + LOGGER.warning("I/O error during processing: " + e.getMessage()); + handleError(e); + } + } + + private void riskyOperation() throws IOException { + File file = new File("test.txt"); + if (!file.exists()) { + throw new IOException("File not found"); + } + } + + private void handleError(Exception e) { + LOGGER.severe("Error handled: " + e.getMessage()); + } + + /** + * Main entry point for the application. + * @param args command line arguments + */ + public static void main(String[] args) { + ValidAst obj = new ValidAst(); + obj.processWithSpecificCatch(); + int result = obj.calculate(1, 2, 3); + LOGGER.info("Result: " + result); + } +} diff --git a/testdata/java/ast/code-policy.json b/testdata/java/ast/code-policy.json new file mode 100644 index 0000000..147ed81 --- /dev/null +++ b/testdata/java/ast/code-policy.json @@ -0,0 +1,87 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "JAVA-AST-NO-SYSTEM-OUT", + "enabled": true, + "category": "error_handling", + "severity": "error", + "desc": "Avoid using System.out for logging", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "ast", + "language": "java", + "node": "MethodCallExpr", + "where": { + "scope": "System.out", + "name": "println" + } + }, + "message": "Use a proper logging framework instead of System.out" + }, + { + "id": "JAVA-AST-EMPTY-CATCH", + "enabled": true, + "category": "error_handling", + "severity": "error", + "desc": "Empty catch blocks are not allowed", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "ast", + "language": "java", + "node": "CatchClause", + "where": { + "body.statements.size": 0 + } + }, + "message": "Catch block must handle the exception or at least log it" + }, + { + "id": "JAVA-AST-GENERIC-EXCEPTION", + "enabled": true, + "category": "error_handling", + "severity": "error", + "desc": "Avoid catching generic Exception", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "ast", + "language": "java", + "node": "CatchClause", + "where": { + "parameter.type.name": "Exception" + } + }, + "message": "Catch specific exception types instead of generic Exception" + }, + { + "id": "JAVA-AST-MISSING-JAVADOC", + "enabled": true, + "category": "documentation", + "severity": "error", + "desc": "Public methods must have Javadoc", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "ast", + "language": "java", + "node": "MethodDeclaration", + "where": { + "isPublic": true, + "hasJavadoc": false + } + }, + "message": "Public methods must have Javadoc documentation" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/java/length/LengthViolations.java b/testdata/java/length/LengthViolations.java new file mode 100644 index 0000000..ef9b0f2 --- /dev/null +++ b/testdata/java/length/LengthViolations.java @@ -0,0 +1,77 @@ +/** + * Test file with length violations + * Contains violations for line length, method length, and parameter count + */ +package com.example; + +public class LengthViolations { + + // VIOLATION: Line length exceeds 100 characters + private static final String VERY_LONG_CONSTANT_NAME_THAT_EXCEEDS_THE_MAXIMUM_LINE_LENGTH_AND_SHOULD_BE_FLAGGED = "test-value"; + + // VIOLATION: Too many parameters (more than 4) + public String processData(String firstName, String lastName, String email, String phone, String address, String city) { + return firstName + " " + lastName + " - " + email; + } + + // VIOLATION: Method is too long (more than 50 lines) + public void veryLongMethod() { + int line1 = 1; + int line2 = 2; + int line3 = 3; + int line4 = 4; + int line5 = 5; + int line6 = 6; + int line7 = 7; + int line8 = 8; + int line9 = 9; + int line10 = 10; + int line11 = 11; + int line12 = 12; + int line13 = 13; + int line14 = 14; + int line15 = 15; + int line16 = 16; + int line17 = 17; + int line18 = 18; + int line19 = 19; + int line20 = 20; + int line21 = 21; + int line22 = 22; + int line23 = 23; + int line24 = 24; + int line25 = 25; + int line26 = 26; + int line27 = 27; + int line28 = 28; + int line29 = 29; + int line30 = 30; + int line31 = 31; + int line32 = 32; + int line33 = 33; + int line34 = 34; + int line35 = 35; + int line36 = 36; + int line37 = 37; + int line38 = 38; + int line39 = 39; + int line40 = 40; + int line41 = 41; + int line42 = 42; + int line43 = 43; + int line44 = 44; + int line45 = 45; + int line46 = 46; + int line47 = 47; + int line48 = 48; + int line49 = 49; + int line50 = 50; + int line51 = 51; + System.out.println("Line: " + line51); + } + + public static void main(String[] args) { + LengthViolations obj = new LengthViolations(); + obj.veryLongMethod(); + } +} diff --git a/testdata/java/length/ValidLength.java b/testdata/java/length/ValidLength.java new file mode 100644 index 0000000..c8e3996 --- /dev/null +++ b/testdata/java/length/ValidLength.java @@ -0,0 +1,52 @@ +/** + * Test file with valid length constraints + * All lines, methods, and parameter counts are within limits + */ +package com.example; + +public class ValidLength { + + private static final String CONFIG = "config-value"; + + // Correct: 4 parameters or fewer + public String formatUser(String name, String email, String role) { + return name + " (" + email + ") - " + role; + } + + // Correct: Short, focused method + public int add(int a, int b) { + return a + b; + } + + // Correct: Method within reasonable length + public void processRequest() { + String input = readInput(); + String validated = validate(input); + String result = transform(validated); + save(result); + } + + private String readInput() { + return "input"; + } + + private String validate(String input) { + if (input == null || input.isEmpty()) { + throw new IllegalArgumentException("Invalid input"); + } + return input; + } + + private String transform(String data) { + return data.toUpperCase(); + } + + private void save(String data) { + System.out.println("Saved: " + data); + } + + public static void main(String[] args) { + ValidLength obj = new ValidLength(); + obj.processRequest(); + } +} diff --git a/testdata/java/length/code-policy.json b/testdata/java/length/code-policy.json new file mode 100644 index 0000000..42dd961 --- /dev/null +++ b/testdata/java/length/code-policy.json @@ -0,0 +1,60 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "JAVA-LENGTH-MAX-LINE", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Line length must not exceed 100 characters", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "length", + "language": "java", + "scope": "line", + "max": 100 + }, + "message": "Line length must not exceed 100 characters" + }, + { + "id": "JAVA-LENGTH-MAX-METHOD", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Method length must not exceed 50 lines", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "length", + "language": "java", + "scope": "method", + "max": 50 + }, + "message": "Method length must not exceed 50 lines" + }, + { + "id": "JAVA-LENGTH-MAX-PARAMS", + "enabled": true, + "category": "formatting", + "severity": "warning", + "desc": "Methods should have at most 4 parameters", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "length", + "language": "java", + "scope": "params", + "max": 4 + }, + "message": "Methods should have at most 4 parameters" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/java/pattern/NamingViolations.java b/testdata/java/pattern/NamingViolations.java new file mode 100644 index 0000000..e021999 --- /dev/null +++ b/testdata/java/pattern/NamingViolations.java @@ -0,0 +1,31 @@ +/** + * Test file with naming convention violations + * Violates Checkstyle naming rules + */ +package com.example; + +// VIOLATION: Class name should start with uppercase (PascalCase) +public class invalidClassName { + + // VIOLATION: Constant should be UPPER_SNAKE_CASE + private static final String apiKey = "sk-1234567890"; + + // VIOLATION: Variable name using UPPER_SNAKE_CASE (should be camelCase) + private int BAD_VARIABLE = 100; + + // VIOLATION: Method name starts with uppercase (should be camelCase) + public void BadMethod() { + System.out.println("This method has bad naming"); + } + + // VIOLATION: Method parameter uses snake_case + public String processData(String user_name) { + return "Hello " + user_name; + } + + // VIOLATION: Multiple naming issues + public static void MAIN(String[] args) { + invalidClassName obj = new invalidClassName(); + obj.BadMethod(); + } +} diff --git a/testdata/java/pattern/ValidNaming.java b/testdata/java/pattern/ValidNaming.java new file mode 100644 index 0000000..995cfec --- /dev/null +++ b/testdata/java/pattern/ValidNaming.java @@ -0,0 +1,30 @@ +/** + * Test file with correct naming conventions + * Complies with Checkstyle naming rules + */ +package com.example; + +public class ValidNaming { + + // Correct: Constant in UPPER_SNAKE_CASE + private static final String API_KEY = "from-environment"; + + // Correct: Variable in camelCase + private int goodVariable = 100; + + // Correct: Method in camelCase + public void goodMethod() { + System.out.println("This method has good naming"); + } + + // Correct: Parameter in camelCase + public String processData(String userName) { + return "Hello " + userName; + } + + // Correct: Main method + public static void main(String[] args) { + ValidNaming obj = new ValidNaming(); + obj.goodMethod(); + } +} diff --git a/testdata/java/pattern/code-policy.json b/testdata/java/pattern/code-policy.json new file mode 100644 index 0000000..6f4a174 --- /dev/null +++ b/testdata/java/pattern/code-policy.json @@ -0,0 +1,94 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "JAVA-PATTERN-CLASS-PASCAL", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Class names must be PascalCase", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "pattern", + "language": "java", + "target": "TypeName", + "pattern": "^[A-Z][a-zA-Z0-9]*$" + }, + "message": "Class names must be PascalCase (e.g., ValidNaming)" + }, + { + "id": "JAVA-PATTERN-METHOD-CAMEL", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Method names must be camelCase", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "pattern", + "language": "java", + "target": "MethodName", + "pattern": "^[a-z][a-zA-Z0-9]*$" + }, + "message": "Method names must be camelCase (e.g., goodMethod)" + }, + { + "id": "JAVA-PATTERN-VAR-CAMEL", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Variable names must be camelCase", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "pattern", + "language": "java", + "target": "MemberName", + "pattern": "^[a-z][a-zA-Z0-9]*$" + }, + "message": "Variable names must be camelCase (e.g., goodVariable)" + }, + { + "id": "JAVA-PATTERN-CONST-UPPER", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Constants must be UPPER_SNAKE_CASE", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "pattern", + "language": "java", + "target": "ConstantName", + "pattern": "^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$" + }, + "message": "Constants must be UPPER_SNAKE_CASE (e.g., API_KEY)" + }, + { + "id": "JAVA-PATTERN-PARAM-CAMEL", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Parameter names must be camelCase", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "pattern", + "language": "java", + "target": "ParameterName", + "pattern": "^[a-z][a-zA-Z0-9]*$" + }, + "message": "Parameter names must be camelCase (e.g., userName)" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/java/style/StyleViolations.java b/testdata/java/style/StyleViolations.java new file mode 100644 index 0000000..97d8f7b --- /dev/null +++ b/testdata/java/style/StyleViolations.java @@ -0,0 +1,54 @@ +/** + * Test file with style violations + * Contains violations for indentation, spacing, and formatting + */ +package com.example; + +public class StyleViolations { + +// VIOLATION: Missing indentation for class member +private String name; + + // VIOLATION: Inconsistent indentation + public void badIndentation() { + int x = 1; + int y = 2; + int z = 3; + System.out.println(x + y + z); + } + + // VIOLATION: Opening brace on next line (Java convention: same line) + public void badBracePlacement() + { + System.out.println("Bad brace placement"); + } + + // VIOLATION: Multiple statements on one line + public void multipleStatements() { int a = 1; int b = 2; System.out.println(a + b); } + + // VIOLATION: No space after if/for keywords + public void noSpaceAfterKeyword() { + if(true){ + for(int i=0;i<10;i++){ + System.out.println(i); + } + } + } + + // VIOLATION: Inconsistent spacing around operators + public int badOperatorSpacing() { + int result=10+20*30/5-2; + return result; + } + + // VIOLATION: Long line exceeding typical style guide limit + private static final String EXTREMELY_LONG_LINE_THAT_EXCEEDS_REASONABLE_LENGTH_LIMITS_AND_SHOULD_BE_WRAPPED_OR_REFACTORED = "value"; + + // VIOLATION: Missing blank line before method + public void missingBlankLine() { + System.out.println("No blank line above"); + } + public void anotherMethod() { + System.out.println("Methods too close together"); + } +} diff --git a/testdata/java/style/ValidStyle.java b/testdata/java/style/ValidStyle.java new file mode 100644 index 0000000..2b2b5db --- /dev/null +++ b/testdata/java/style/ValidStyle.java @@ -0,0 +1,63 @@ +/** + * Test file with valid Java style + * Follows standard Java formatting conventions + */ +package com.example; + +public class ValidStyle { + + private String name; + private int value; + + public ValidStyle() { + this.name = "default"; + this.value = 0; + } + + public void properIndentation() { + int x = 1; + int y = 2; + int z = 3; + System.out.println(x + y + z); + } + + public void properBracePlacement() { + System.out.println("Correct brace placement"); + } + + public void properSpacing() { + if (true) { + for (int i = 0; i < 10; i++) { + System.out.println(i); + } + } + } + + public int properOperatorSpacing() { + int result = 10 + 20 * 30 / 5 - 2; + return result; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public static void main(String[] args) { + ValidStyle obj = new ValidStyle(); + obj.properIndentation(); + obj.properSpacing(); + System.out.println("Result: " + obj.properOperatorSpacing()); + } +} diff --git a/testdata/java/style/code-policy.json b/testdata/java/style/code-policy.json new file mode 100644 index 0000000..afd629f --- /dev/null +++ b/testdata/java/style/code-policy.json @@ -0,0 +1,122 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "JAVA-STYLE-INDENT", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Use consistent 4-space indentation", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "style", + "language": "java", + "indent": 4, + "indentStyle": "space" + }, + "message": "Use 4 spaces for indentation" + }, + { + "id": "JAVA-STYLE-BRACE-SAME-LINE", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Opening brace must be on same line", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "style", + "language": "java", + "braceStyle": "same-line" + }, + "message": "Opening brace should be on the same line" + }, + { + "id": "JAVA-STYLE-SPACE-AFTER-KEYWORD", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Require space after control keywords", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "style", + "language": "java", + "spaceAfterKeyword": true + }, + "message": "Add space after control keywords (if, for, while, etc.)" + }, + { + "id": "JAVA-STYLE-OPERATOR-SPACING", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Require spacing around operators", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "style", + "language": "java", + "spaceAroundOperators": true + }, + "message": "Add spaces around operators" + }, + { + "id": "JAVA-STYLE-MAX-LINE", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Maximum line length of 120 characters", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "style", + "language": "java", + "printWidth": 120 + }, + "message": "Line length must not exceed 120 characters" + }, + { + "id": "JAVA-STYLE-BLANK-LINE-SEPARATOR", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Require blank lines between methods", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "style", + "language": "java", + "blankLinesBetweenMethods": true + }, + "message": "Add blank line between method declarations" + }, + { + "id": "JAVA-STYLE-ONE-STATEMENT-PER-LINE", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Only one statement per line", + "when": { + "languages": ["java"] + }, + "check": { + "engine": "style", + "language": "java", + "oneStatementPerLine": true + }, + "message": "Place each statement on its own line" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/javascript/ast/code-policy.json b/testdata/javascript/ast/code-policy.json new file mode 100644 index 0000000..aa5171d --- /dev/null +++ b/testdata/javascript/ast/code-policy.json @@ -0,0 +1,63 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "JS-AST-CLASS-NAMING", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Class declarations must be PascalCase", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "pattern", + "language": "javascript", + "target": "identifier", + "context": "class", + "pattern": "^[A-Z][a-zA-Z0-9]*$" + }, + "message": "Class names must be PascalCase" + }, + { + "id": "JS-AST-FUNCTION-NAMING", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Function declarations must be camelCase", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "pattern", + "language": "javascript", + "target": "identifier", + "context": "function", + "pattern": "^[a-z][a-zA-Z0-9]*$" + }, + "message": "Function names must be camelCase" + }, + { + "id": "JS-AST-VAR-NAMING", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Variable declarations must be camelCase", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "pattern", + "language": "javascript", + "target": "identifier", + "context": "variable", + "pattern": "^[a-z][a-zA-Z0-9]*$" + }, + "message": "Variable names must be camelCase" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/javascript/naming-violations.js b/testdata/javascript/ast/naming-violations.js similarity index 100% rename from testdata/javascript/naming-violations.js rename to testdata/javascript/ast/naming-violations.js diff --git a/testdata/javascript/valid.js b/testdata/javascript/ast/valid.js similarity index 100% rename from testdata/javascript/valid.js rename to testdata/javascript/ast/valid.js diff --git a/testdata/javascript/async-with-try.js b/testdata/javascript/async-with-try.js deleted file mode 100644 index 0eb2c13..0000000 --- a/testdata/javascript/async-with-try.js +++ /dev/null @@ -1,24 +0,0 @@ -// Good: Async function with try-catch - -async function fetchData() { - try { - const response = await fetch("https://api.example.com/data"); - const data = await response.json(); - return data; - } catch (error) { - console.error("Failed to fetch data:", error); - throw error; - } -} - -async function processFile(filename) { - try { - const content = await readFile(filename); - return JSON.parse(content); - } catch (error) { - console.error("Failed to process file:", error); - return null; - } -} - -module.exports = { fetchData, processFile }; diff --git a/testdata/javascript/async-without-try.js b/testdata/javascript/async-without-try.js deleted file mode 100644 index be996e1..0000000 --- a/testdata/javascript/async-without-try.js +++ /dev/null @@ -1,20 +0,0 @@ -// Bad: Async functions without try-catch - -async function fetchData() { - const response = await fetch("https://api.example.com/data"); - const data = await response.json(); - return data; -} - -async function processFile(filename) { - const content = await readFile(filename); - return JSON.parse(content); -} - -// This one is also bad - no try-catch -async function saveData(data) { - await writeFile("output.json", JSON.stringify(data)); - console.log("Data saved"); -} - -module.exports = { fetchData, processFile, saveData }; diff --git a/testdata/javascript/bad-imports.js b/testdata/javascript/bad-imports.js deleted file mode 100644 index 5e93b7b..0000000 --- a/testdata/javascript/bad-imports.js +++ /dev/null @@ -1,13 +0,0 @@ -// Bad: Importing from restricted modules - -import { something } from 'lodash'; -import _ from 'lodash'; -const moment = require('moment'); - -// These should be caught by no-restricted-imports -import React from 'react'; -import { useState } from 'react'; - -export default function Component() { - return null; -} diff --git a/testdata/javascript/bad-naming.js b/testdata/javascript/bad-naming.js deleted file mode 100644 index 51b7dad..0000000 --- a/testdata/javascript/bad-naming.js +++ /dev/null @@ -1,31 +0,0 @@ -// This file contains intentional violations for testing - -// ❌ Class name should be PascalCase -class myClass { - constructor() { - this.value = 42; - } -} - -// ❌ Function name should be camelCase -function MyFunction() { - return "hello"; -} - -// ❌ Variable with underscore (if pattern requires camelCase) -const my_variable = 123; - -// ❌ Line exceeds 100 characters -const veryLongLine = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore"; - -// ❌ Function with too many parameters (>5) -function tooManyParams(a, b, c, d, e, f, g) { - return a + b + c + d + e + f + g; -} - -// ✅ Good naming -class User { - getName() { - return "John"; - } -} diff --git a/testdata/javascript/bad-style.js b/testdata/javascript/bad-style.js deleted file mode 100644 index a1b4522..0000000 --- a/testdata/javascript/bad-style.js +++ /dev/null @@ -1,21 +0,0 @@ -// This file has bad style (no prettier formatting) - -const x={a:1,b:2,c:3}; - -function foo(a,b,c){ -return a+b+c -} - -const longObject = {name:"John",age:30,city:"New York",country:"USA",occupation:"Developer"}; - -class MyClass{ -constructor(){ -this.value=42; -} - -getValue(){return this.value;} -} - -const arr=[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]; - -module.exports={MyClass,foo,longObject}; diff --git a/testdata/javascript/good-code.js b/testdata/javascript/good-code.js deleted file mode 100644 index f35170a..0000000 --- a/testdata/javascript/good-code.js +++ /dev/null @@ -1,23 +0,0 @@ -// This file follows all conventions - -class UserService { - constructor() { - this.users = []; - } - - addUser(user) { - this.users.push(user); - } - - getUsers() { - return this.users; - } -} - -function calculateTotal(items) { - return items.reduce((sum, item) => sum + item.price, 0); -} - -const apiEndpoint = "https://api.example.com"; - -module.exports = { UserService, calculateTotal }; diff --git a/testdata/javascript/good-style.js b/testdata/javascript/good-style.js deleted file mode 100644 index 2aaf57d..0000000 --- a/testdata/javascript/good-style.js +++ /dev/null @@ -1,29 +0,0 @@ -// This file has good style (prettier formatted) - -const x = { a: 1, b: 2, c: 3 }; - -function foo(a, b, c) { - return a + b + c; -} - -const longObject = { - name: "John", - age: 30, - city: "New York", - country: "USA", - occupation: "Developer", -}; - -class MyClass { - constructor() { - this.value = 42; - } - - getValue() { - return this.value; - } -} - -const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; - -module.exports = { MyClass, foo, longObject }; diff --git a/testdata/javascript/hardcoded-secrets.js b/testdata/javascript/hardcoded-secrets.js deleted file mode 100644 index e7522fe..0000000 --- a/testdata/javascript/hardcoded-secrets.js +++ /dev/null @@ -1,15 +0,0 @@ -// Bad: Hardcoded secrets (for content pattern testing) - -const API_KEY = "sk-1234567890abcdef"; -const apiKey = "my-secret-key-12345"; -const password = "admin123"; - -const config = { - secret: "my-secret-token", - apiKey: "hardcoded-api-key" -}; - -// This should also be caught -const SECRET_TOKEN = "secret_abc123"; - -module.exports = { API_KEY, config }; diff --git a/testdata/javascript/length/code-policy.json b/testdata/javascript/length/code-policy.json new file mode 100644 index 0000000..a1487bc --- /dev/null +++ b/testdata/javascript/length/code-policy.json @@ -0,0 +1,60 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "JS-LENGTH-MAX-LINE", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Line length must not exceed 100 characters", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "length", + "language": "javascript", + "scope": "line", + "max": 100 + }, + "message": "Line length must not exceed 100 characters" + }, + { + "id": "JS-LENGTH-MAX-FUNCTION", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Function length must not exceed 50 lines", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "length", + "language": "javascript", + "scope": "function", + "max": 50 + }, + "message": "Function length must not exceed 50 lines" + }, + { + "id": "JS-LENGTH-MAX-PARAMS", + "enabled": true, + "category": "formatting", + "severity": "warning", + "desc": "Functions should have at most 4 parameters", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "length", + "language": "javascript", + "scope": "params", + "max": 4 + }, + "message": "Functions should have at most 4 parameters" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/javascript/length-violations.js b/testdata/javascript/length/length-violations.js similarity index 100% rename from testdata/javascript/length-violations.js rename to testdata/javascript/length/length-violations.js diff --git a/testdata/javascript/length/valid.js b/testdata/javascript/length/valid.js new file mode 100644 index 0000000..edebf0f --- /dev/null +++ b/testdata/javascript/length/valid.js @@ -0,0 +1,28 @@ +// Valid JavaScript file for testing + +class Calculator { + constructor() { + this.result = 0; + } + + add(a, b) { + return a + b; + } + + subtract(a, b) { + return a - b; + } + + multiply(a, b) { + return a * b; + } + + divide(a, b) { + if (b === 0) { + throw new Error('Division by zero'); + } + return a / b; + } +} + +module.exports = Calculator; diff --git a/testdata/javascript/long-file.js b/testdata/javascript/long-file.js deleted file mode 100644 index 7b4001c..0000000 --- a/testdata/javascript/long-file.js +++ /dev/null @@ -1,64 +0,0 @@ -// File with too many lines for testing max-lines - -function func1() { return 1; } -function func2() { return 2; } -function func3() { return 3; } -function func4() { return 4; } -function func5() { return 5; } -function func6() { return 6; } -function func7() { return 7; } -function func8() { return 8; } -function func9() { return 9; } -function func10() { return 10; } -function func11() { return 11; } -function func12() { return 12; } -function func13() { return 13; } -function func14() { return 14; } -function func15() { return 15; } -function func16() { return 16; } -function func17() { return 17; } -function func18() { return 18; } -function func19() { return 19; } -function func20() { return 20; } -function func21() { return 21; } -function func22() { return 22; } -function func23() { return 23; } -function func24() { return 24; } -function func25() { return 25; } -function func26() { return 26; } -function func27() { return 27; } -function func28() { return 28; } -function func29() { return 29; } -function func30() { return 30; } -function func31() { return 31; } -function func32() { return 32; } -function func33() { return 33; } -function func34() { return 34; } -function func35() { return 35; } -function func36() { return 36; } -function func37() { return 37; } -function func38() { return 38; } -function func39() { return 39; } -function func40() { return 40; } -function func41() { return 41; } -function func42() { return 42; } -function func43() { return 43; } -function func44() { return 44; } -function func45() { return 45; } -function func46() { return 46; } -function func47() { return 47; } -function func48() { return 48; } -function func49() { return 49; } -function func50() { return 50; } -function func51() { return 51; } -function func52() { return 52; } -function func53() { return 53; } -function func54() { return 54; } -function func55() { return 55; } -function func56() { return 56; } -function func57() { return 57; } -function func58() { return 58; } -function func59() { return 59; } -function func60() { return 60; } - -module.exports = { func1, func60 }; diff --git a/testdata/javascript/long-function.js b/testdata/javascript/long-function.js deleted file mode 100644 index ce5c3f6..0000000 --- a/testdata/javascript/long-function.js +++ /dev/null @@ -1,58 +0,0 @@ -// File with a very long function for testing max-lines-per-function - -function veryLongFunction() { - const line1 = 1; - const line2 = 2; - const line3 = 3; - const line4 = 4; - const line5 = 5; - const line6 = 6; - const line7 = 7; - const line8 = 8; - const line9 = 9; - const line10 = 10; - const line11 = 11; - const line12 = 12; - const line13 = 13; - const line14 = 14; - const line15 = 15; - const line16 = 16; - const line17 = 17; - const line18 = 18; - const line19 = 19; - const line20 = 20; - const line21 = 21; - const line22 = 22; - const line23 = 23; - const line24 = 24; - const line25 = 25; - const line26 = 26; - const line27 = 27; - const line28 = 28; - const line29 = 29; - const line30 = 30; - const line31 = 31; - const line32 = 32; - const line33 = 33; - const line34 = 34; - const line35 = 35; - const line36 = 36; - const line37 = 37; - const line38 = 38; - const line39 = 39; - const line40 = 40; - const line41 = 41; - const line42 = 42; - const line43 = 43; - const line44 = 44; - const line45 = 45; - const line46 = 46; - const line47 = 47; - const line48 = 48; - const line49 = 49; - const line50 = 50; - - return line50; -} - -module.exports = { veryLongFunction }; diff --git a/testdata/javascript/long-lines.js b/testdata/javascript/long-lines.js deleted file mode 100644 index 8eed5a9..0000000 --- a/testdata/javascript/long-lines.js +++ /dev/null @@ -1,12 +0,0 @@ -// File with long lines for testing max-len - -const shortLine = "ok"; - -const veryLongLine = "This is a very long line that exceeds 80 characters and should be flagged by the max-len rule for ESLint validation purposes."; - -function example() { - const anotherVeryLongLineHereThatDefinitelyExceedsTheMaximumLengthAndShouldBeFlaggedByOurLengthEngine = true; - return anotherVeryLongLineHereThatDefinitelyExceedsTheMaximumLengthAndShouldBeFlaggedByOurLengthEngine; -} - -module.exports = { example }; diff --git a/testdata/javascript/many-params.js b/testdata/javascript/many-params.js deleted file mode 100644 index 599b8c1..0000000 --- a/testdata/javascript/many-params.js +++ /dev/null @@ -1,15 +0,0 @@ -// File with functions that have too many parameters - -function tooManyParams(a, b, c, d, e, f, g) { - return a + b + c + d + e + f + g; -} - -const arrowWithManyParams = (x1, x2, x3, x4, x5, x6, x7, x8) => { - return x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8; -}; - -function goodFunction(a, b, c) { - return a + b + c; -} - -module.exports = { tooManyParams, arrowWithManyParams, goodFunction }; diff --git a/testdata/javascript/pattern/code-policy.json b/testdata/javascript/pattern/code-policy.json new file mode 100644 index 0000000..2e29e08 --- /dev/null +++ b/testdata/javascript/pattern/code-policy.json @@ -0,0 +1,81 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "JS-PATTERN-CLASS-PASCAL", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Class names must be PascalCase", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "pattern", + "language": "javascript", + "target": "identifier", + "pattern": "^[A-Z][a-zA-Z0-9]*$", + "scope": "class" + }, + "message": "Class names must be PascalCase (e.g., Calculator)" + }, + { + "id": "JS-PATTERN-FUNCTION-CAMEL", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Function names must be camelCase", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "pattern", + "language": "javascript", + "target": "identifier", + "pattern": "^[a-z][a-zA-Z0-9]*$", + "scope": "function" + }, + "message": "Function names must be camelCase (e.g., goodFunctionName)" + }, + { + "id": "JS-PATTERN-VAR-CAMEL", + "enabled": true, + "category": "naming", + "severity": "error", + "desc": "Variable names must be camelCase", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "pattern", + "language": "javascript", + "target": "identifier", + "pattern": "^[a-z][a-zA-Z0-9]*$", + "scope": "variable" + }, + "message": "Variable names must be camelCase (e.g., goodVariable)" + }, + { + "id": "JS-PATTERN-NO-SECRETS", + "enabled": true, + "category": "security", + "severity": "error", + "desc": "No hardcoded secrets allowed", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "pattern", + "language": "javascript", + "target": "content", + "pattern": "(api[_-]?key|password|secret|token)\\s*=\\s*['\"][^'\"]+['\"]", + "flags": "i" + }, + "message": "Do not hardcode secrets. Use environment variables (process.env)" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/javascript/pattern/naming-violations.js b/testdata/javascript/pattern/naming-violations.js new file mode 100644 index 0000000..196df75 --- /dev/null +++ b/testdata/javascript/pattern/naming-violations.js @@ -0,0 +1,29 @@ +// File with naming convention violations + +// Bad: snake_case function name (should be camelCase) +function bad_function_name() { + return 'test'; +} + +// Bad: lowercase class name (should be PascalCase) +class lowercase_class { + constructor() { + this.value = 0; + } +} + +// Bad: variable with uppercase +var BAD_VARIABLE = 'test'; + +// Good examples for comparison +function goodFunctionName() { + return 'test'; +} + +class GoodClassName { + constructor() { + this.value = 0; + } +} + +const goodVariable = 'test'; diff --git a/testdata/javascript/security-violations.js b/testdata/javascript/pattern/security-violations.js similarity index 100% rename from testdata/javascript/security-violations.js rename to testdata/javascript/pattern/security-violations.js diff --git a/testdata/javascript/pattern/valid.js b/testdata/javascript/pattern/valid.js new file mode 100644 index 0000000..edebf0f --- /dev/null +++ b/testdata/javascript/pattern/valid.js @@ -0,0 +1,28 @@ +// Valid JavaScript file for testing + +class Calculator { + constructor() { + this.result = 0; + } + + add(a, b) { + return a + b; + } + + subtract(a, b) { + return a - b; + } + + multiply(a, b) { + return a * b; + } + + divide(a, b) { + if (b === 0) { + throw new Error('Division by zero'); + } + return a / b; + } +} + +module.exports = Calculator; diff --git a/testdata/javascript/style/code-policy.json b/testdata/javascript/style/code-policy.json new file mode 100644 index 0000000..987ecc4 --- /dev/null +++ b/testdata/javascript/style/code-policy.json @@ -0,0 +1,74 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "JS-STYLE-INDENT", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Use consistent 2-space indentation", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "style", + "language": "javascript", + "indent": 2, + "indentStyle": "space" + }, + "message": "Use 2 spaces for indentation" + }, + { + "id": "JS-STYLE-QUOTES", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Use single quotes for strings", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "style", + "language": "javascript", + "quotes": "single" + }, + "message": "Use single quotes for strings" + }, + { + "id": "JS-STYLE-SEMI", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Require semicolons", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "style", + "language": "javascript", + "semi": true + }, + "message": "Statements must end with semicolons" + }, + { + "id": "JS-STYLE-MAX-LINE", + "enabled": true, + "category": "formatting", + "severity": "error", + "desc": "Maximum line length of 100 characters", + "when": { + "languages": ["javascript"] + }, + "check": { + "engine": "style", + "language": "javascript", + "printWidth": 100 + }, + "message": "Line length must not exceed 100 characters" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/javascript/style-violations.js b/testdata/javascript/style/style-violations.js similarity index 100% rename from testdata/javascript/style-violations.js rename to testdata/javascript/style/style-violations.js diff --git a/testdata/javascript/style/valid.js b/testdata/javascript/style/valid.js new file mode 100644 index 0000000..edebf0f --- /dev/null +++ b/testdata/javascript/style/valid.js @@ -0,0 +1,28 @@ +// Valid JavaScript file for testing + +class Calculator { + constructor() { + this.result = 0; + } + + add(a, b) { + return a + b; + } + + subtract(a, b) { + return a - b; + } + + multiply(a, b) { + return a * b; + } + + divide(a, b) { + if (b === 0) { + throw new Error('Division by zero'); + } + return a / b; + } +} + +module.exports = Calculator; diff --git a/testdata/mixed/component.jsx b/testdata/mixed/component.jsx deleted file mode 100644 index ba795ec..0000000 --- a/testdata/mixed/component.jsx +++ /dev/null @@ -1,36 +0,0 @@ -// React component for testing JSX - -import React from 'react'; - -class Button extends React.Component { - constructor(props) { - super(props); - this.state = { - clicked: false - }; - } - - handleClick = () => { - this.setState({ clicked: true }); - if (this.props.onClick) { - this.props.onClick(); - } - } - - render() { - const { label, disabled } = this.props; - const { clicked } = this.state; - - return ( - - ); - } -} - -export default Button; diff --git a/testdata/mixed/component.tsx b/testdata/mixed/component.tsx deleted file mode 100644 index e6cb76e..0000000 --- a/testdata/mixed/component.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// TypeScript React component for testing TSX - -import React, { Component } from 'react'; - -interface ButtonProps { - label: string; - onClick?: () => void; - disabled?: boolean; -} - -interface ButtonState { - clicked: boolean; -} - -class Button extends Component { - constructor(props: ButtonProps) { - super(props); - this.state = { - clicked: false - }; - } - - handleClick = (): void => { - this.setState({ clicked: true }); - if (this.props.onClick) { - this.props.onClick(); - } - } - - render() { - const { label, disabled } = this.props; - const { clicked } = this.state; - - return ( - - ); - } -} - -export default Button; diff --git a/testdata/typescript/typechecker/code-policy.json b/testdata/typescript/typechecker/code-policy.json new file mode 100644 index 0000000..12498ea --- /dev/null +++ b/testdata/typescript/typechecker/code-policy.json @@ -0,0 +1,60 @@ +{ + "version": "1.0.0", + "rules": [ + { + "id": "TS-TYPE-STRICT", + "enabled": true, + "category": "typing", + "severity": "error", + "desc": "Enable strict type checking", + "when": { + "languages": ["typescript"] + }, + "check": { + "engine": "typechecker", + "language": "typescript", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true + }, + "message": "Strict type checking enabled - all type errors must be resolved" + }, + { + "id": "TS-TYPE-NO-IMPLICIT-ANY", + "enabled": true, + "category": "typing", + "severity": "error", + "desc": "Disallow implicit any types", + "when": { + "languages": ["typescript"] + }, + "check": { + "engine": "typechecker", + "language": "typescript", + "noImplicitAny": true + }, + "message": "Variables and parameters must have explicit type annotations" + }, + { + "id": "TS-TYPE-RETURN-TYPE", + "enabled": true, + "category": "typing", + "severity": "error", + "desc": "Functions must have explicit return types", + "when": { + "languages": ["typescript"] + }, + "check": { + "engine": "typechecker", + "language": "typescript", + "noImplicitReturns": true + }, + "message": "Functions must have explicit return type annotations" + } + ], + "enforce": { + "stages": ["pre-commit"], + "fail_on": ["error"] + } +} diff --git a/testdata/typescript/strict-mode-errors.ts b/testdata/typescript/typechecker/strict-mode-errors.ts similarity index 100% rename from testdata/typescript/strict-mode-errors.ts rename to testdata/typescript/typechecker/strict-mode-errors.ts diff --git a/testdata/typescript/type-errors.ts b/testdata/typescript/typechecker/type-errors.ts similarity index 100% rename from testdata/typescript/type-errors.ts rename to testdata/typescript/typechecker/type-errors.ts diff --git a/testdata/typescript/valid.ts b/testdata/typescript/typechecker/valid.ts similarity index 100% rename from testdata/typescript/valid.ts rename to testdata/typescript/typechecker/valid.ts diff --git a/tests/TESTING_GUIDE.md b/tests/TESTING_GUIDE.md index 5d2d447..a46264f 100644 --- a/tests/TESTING_GUIDE.md +++ b/tests/TESTING_GUIDE.md @@ -139,7 +139,62 @@ go test -v ./tests/e2e/... -timeout 5m - [ ] 위반사항 보고서 생성 - [ ] 종료 코드 설정 (위반 시 1) -## 테스트 데이터 +## 통합 테스트 데이터 구조 + +### testdata 디렉토리 + +검증 엔진의 정확성을 보장하기 위한 테스트 데이터는 `testdata/` 디렉토리에 엔진별, 언어별로 구조화되어 있습니다: + +``` +testdata/ +├── javascript/ +│ ├── pattern/ # 패턴 매칭 및 네이밍 컨벤션 테스트 +│ │ ├── naming-violations.js +│ │ ├── security-violations.js +│ │ └── valid.js +│ ├── length/ # 라인/함수 길이 제한 테스트 +│ │ ├── length-violations.js +│ │ └── valid.js +│ ├── style/ # 코드 스타일 및 포맷팅 테스트 +│ │ ├── style-violations.js +│ │ └── valid.js +│ └── ast/ # AST 구조 검증 테스트 +│ ├── naming-violations.js +│ └── valid.js +├── typescript/ +│ └── typechecker/ # 타입 체킹 테스트 +│ ├── type-errors.ts +│ ├── strict-mode-errors.ts +│ └── valid.ts +└── java/ + ├── pattern/ # Checkstyle 패턴 테스트 + │ ├── NamingViolations.java + │ └── ValidNaming.java + ├── length/ # Checkstyle 길이 제한 테스트 + │ ├── LengthViolations.java + │ └── ValidLength.java + ├── style/ # Checkstyle 스타일 테스트 + │ ├── StyleViolations.java + │ └── ValidStyle.java + └── ast/ # PMD AST 검증 테스트 + ├── AstViolations.java + └── ValidAst.java +``` + +**파일 네이밍 컨벤션**: +- `*-violations.*` / `*Violations.*`: 규칙 위반 케이스 +- `valid.*` / `Valid*.*`: 규칙 준수 케이스 + +각 엔진은 해당 언어의 testdata를 사용하여 통합 테스트를 실행합니다: +- Pattern Engine: 정규식 패턴 검증 (ESLint/Checkstyle) +- Length Engine: 길이 제한 검증 (ESLint/Checkstyle) +- Style Engine: 포맷팅 검증 (Prettier/Checkstyle) +- AST Engine: 구조 검증 (ESLint/PMD) +- TypeChecker Engine: 타입 검증 (TSC) + +자세한 내용은 [testdata/README.md](../testdata/README.md)를 참고하세요. + +## E2E 테스트 데이터 ### 자연어 컨벤션 예시 diff --git a/tests/integration/ast_integration_test.go b/tests/integration/ast_integration_test.go index 02c6736..2793aa5 100644 --- a/tests/integration/ast_integration_test.go +++ b/tests/integration/ast_integration_test.go @@ -50,7 +50,7 @@ func TestASTEngine_CallExpression_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/valid.js"), + filepath.Join(workDir, "testdata/javascript/ast/valid.js"), } result, err := engine.Validate(ctx, rule, files) @@ -101,8 +101,8 @@ func TestASTEngine_ClassDeclaration_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/valid.js"), - filepath.Join(workDir, "testdata/javascript/naming-violations.js"), + filepath.Join(workDir, "testdata/javascript/ast/valid.js"), + filepath.Join(workDir, "testdata/javascript/ast/naming-violations.js"), } result, err := engine.Validate(ctx, rule, files) diff --git a/tests/integration/helper.go b/tests/integration/helper.go index 6fe22a4..f5b02cd 100644 --- a/tests/integration/helper.go +++ b/tests/integration/helper.go @@ -4,6 +4,12 @@ import ( "os" "path/filepath" "testing" + + "github.com/DevSymphony/sym-cli/internal/policy" + "github.com/DevSymphony/sym-cli/internal/validator" + "github.com/DevSymphony/sym-cli/pkg/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // getTestdataDir returns the path to the testdata directory @@ -33,3 +39,59 @@ func getToolsDir(t *testing.T) string { 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() + loader := policy.NewLoader(false) + policyPath := filepath.Join(getTestdataDir(t), relativePath) + pol, err := loader.LoadCodePolicy(policyPath) + require.NoError(t, err, "Failed to load policy from %s", relativePath) + require.NotNil(t, pol, "Policy should not be nil") + return pol +} + +// createTestValidator creates a validator with given policy and registers cleanup +func createTestValidator(t *testing.T, pol *schema.CodePolicy) *validator.Validator { + t.Helper() + v := validator.NewValidator(pol, testing.Verbose()) + t.Cleanup(func() { + if err := v.Close(); err != nil { + t.Logf("Warning: failed to close validator: %v", err) + } + }) + return v +} + +// assertViolationsDetected asserts that violations are found and logs them +func assertViolationsDetected(t *testing.T, result *validator.Result) { + t.Helper() + assert.False(t, result.Passed, "Should detect violations") + assert.Greater(t, len(result.Violations), 0, "Should have violations") + + // Log violations for debugging + if len(result.Violations) > 0 { + t.Logf("Found %d violation(s):", len(result.Violations)) + for i, v := range result.Violations { + t.Logf(" %d. [%s] %s at %s:%d:%d (severity: %s)", + i+1, v.RuleID, v.Message, v.File, v.Line, v.Column, v.Severity) + } + } +} + +// assertNoPolicyViolations asserts that no violations are found +func assertNoPolicyViolations(t *testing.T, result *validator.Result) { + t.Helper() + if !result.Passed || len(result.Violations) > 0 { + // Log violations if any for debugging + if len(result.Violations) > 0 { + t.Logf("Unexpected violations found:") + for i, v := range result.Violations { + t.Logf(" %d. [%s] %s at %s:%d:%d", + i+1, v.RuleID, v.Message, v.File, v.Line, v.Column) + } + } + } + assert.True(t, result.Passed, "Should pass validation") + assert.Equal(t, 0, len(result.Violations), "Should have no violations") +} diff --git a/tests/integration/length_integration_test.go b/tests/integration/length_integration_test.go index 3747af4..1f230e4 100644 --- a/tests/integration/length_integration_test.go +++ b/tests/integration/length_integration_test.go @@ -46,7 +46,7 @@ func TestLengthEngine_LineLengthViolations_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/length-violations.js"), + filepath.Join(workDir, "testdata/javascript/length/length-violations.js"), } result, err := engine.Validate(ctx, rule, files) @@ -105,7 +105,7 @@ func TestLengthEngine_MaxParams_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/length-violations.js"), + filepath.Join(workDir, "testdata/javascript/length/length-violations.js"), } result, err := engine.Validate(ctx, rule, files) @@ -163,7 +163,7 @@ func TestLengthEngine_ValidFile_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/valid.js"), + filepath.Join(workDir, "testdata/javascript/length/valid.js"), } result, err := engine.Validate(ctx, rule, files) diff --git a/tests/integration/pattern_integration_test.go b/tests/integration/pattern_integration_test.go index 241d5db..e18f0b0 100644 --- a/tests/integration/pattern_integration_test.go +++ b/tests/integration/pattern_integration_test.go @@ -47,7 +47,7 @@ func TestPatternEngine_NamingViolations_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/naming-violations.js"), + filepath.Join(workDir, "testdata/javascript/pattern/naming-violations.js"), } result, err := engine.Validate(ctx, rule, files) @@ -107,7 +107,7 @@ func TestPatternEngine_SecurityViolations_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/security-violations.js"), + filepath.Join(workDir, "testdata/javascript/pattern/security-violations.js"), } result, err := engine.Validate(ctx, rule, files) @@ -165,7 +165,7 @@ func TestPatternEngine_ValidFile_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/valid.js"), + filepath.Join(workDir, "testdata/javascript/pattern/valid.js"), } result, err := engine.Validate(ctx, rule, files) diff --git a/tests/integration/style_integration_test.go b/tests/integration/style_integration_test.go index dcc001c..2f08878 100644 --- a/tests/integration/style_integration_test.go +++ b/tests/integration/style_integration_test.go @@ -47,7 +47,7 @@ func TestStyleEngine_IndentViolations_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/style-violations.js"), + filepath.Join(workDir, "testdata/javascript/style/style-violations.js"), } result, err := engine.Validate(ctx, rule, files) @@ -106,7 +106,7 @@ func TestStyleEngine_ValidFile_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/javascript/valid.js"), + filepath.Join(workDir, "testdata/javascript/style/valid.js"), } result, err := engine.Validate(ctx, rule, files) diff --git a/tests/integration/typechecker_integration_test.go b/tests/integration/typechecker_integration_test.go index 37f93e6..994c609 100644 --- a/tests/integration/typechecker_integration_test.go +++ b/tests/integration/typechecker_integration_test.go @@ -45,7 +45,7 @@ func TestTypeChecker_TypeErrors_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/typescript/type-errors.ts"), + filepath.Join(workDir, "testdata/typescript/typechecker/type-errors.ts"), } result, err := engine.Validate(ctx, rule, files) @@ -105,7 +105,7 @@ func TestTypeChecker_StrictModeErrors_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/typescript/strict-mode-errors.ts"), + filepath.Join(workDir, "testdata/typescript/typechecker/strict-mode-errors.ts"), } result, err := engine.Validate(ctx, rule, files) @@ -162,7 +162,7 @@ func TestTypeChecker_ValidFile_Integration(t *testing.T) { } files := []string{ - filepath.Join(workDir, "testdata/typescript/valid.ts"), + filepath.Join(workDir, "testdata/typescript/typechecker/valid.ts"), } result, err := engine.Validate(ctx, rule, files) diff --git a/tests/integration/validator_policy_test.go b/tests/integration/validator_policy_test.go new file mode 100644 index 0000000..a36a576 --- /dev/null +++ b/tests/integration/validator_policy_test.go @@ -0,0 +1,319 @@ +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) +}