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)
+}