Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ make setup
echo "Installing global npm packages..."
npm install -g @google/gemini-cli @openai/codex

echo "Installing Python venv package for Pylint adapter..."
sudo apt-get update -qq && sudo apt-get install -y python3-venv

echo "Setup completed successfully!"
3 changes: 3 additions & 0 deletions cmd/sym/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package main

import (
"github.com/DevSymphony/sym-cli/internal/cmd"

// Bootstrap: register all adapters
_ "github.com/DevSymphony/sym-cli/internal/bootstrap"
)

// Version is set by build -ldflags "-X main.Version=x.y.z"
Expand Down
32 changes: 32 additions & 0 deletions internal/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package adapter

import (
"context"

"github.com/DevSymphony/sym-cli/internal/llm"
"github.com/DevSymphony/sym-cli/pkg/schema"
)

// Adapter wraps external tools (ESLint, Prettier, etc.) for use by engines.
Expand Down Expand Up @@ -92,3 +95,32 @@ type Violation struct {
Severity string // "error", "warning", "info"
RuleID string
}

// LinterConverter converts user rules to native linter configuration using LLM.
// This interface is implemented by each linter's converter (e.g., ESLintConverter).
type LinterConverter interface {
// Name returns the linter name (e.g., "eslint", "checkstyle", "pmd")
Name() string

// SupportedLanguages returns the languages this linter supports
SupportedLanguages() []string

// GetLLMDescription returns a description of the linter's capabilities for LLM routing.
// This is used in the LLM prompt to help route rules to appropriate linters.
GetLLMDescription() string

// GetRoutingHints returns routing rules for LLM to decide when to use this linter.
// Each hint is a rule like "For Java naming rules → ALWAYS use checkstyle".
// These hints are collected and included in the LLM prompt for rule routing.
GetRoutingHints() []string

// ConvertRules converts user rules to native linter configuration using LLM
ConvertRules(ctx context.Context, rules []schema.UserRule, llmClient *llm.Client) (*LinterConfig, error)
}

// LinterConfig represents a generated configuration file.
type LinterConfig struct {
Filename string // e.g., ".eslintrc.json", "checkstyle.xml"
Content []byte // File content
Format string // "json", "xml", "yaml"
}
9 changes: 4 additions & 5 deletions internal/adapter/checkstyle/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ const (
// - Length rules: line length, file length
// - Style rules: indentation, whitespace
// - Naming rules: class names, method names, variable names
//
// Note: Adapter is goroutine-safe and stateless. WorkDir is determined
// by CWD at execution time, not stored in the adapter.
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
Expand All @@ -44,7 +44,7 @@ type Adapter struct {
}

// NewAdapter creates a new Checkstyle adapter.
func NewAdapter(toolsDir, workDir string) *Adapter {
func NewAdapter(toolsDir string) *Adapter {
if toolsDir == "" {
home, _ := os.UserHomeDir()
toolsDir = filepath.Join(home, ".sym", "tools")
Expand All @@ -54,7 +54,6 @@ func NewAdapter(toolsDir, workDir string) *Adapter {

return &Adapter{
ToolsDir: toolsDir,
WorkDir: workDir,
JavaPath: javaPath,
executor: adapter.NewSubprocessExecutor(),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package linters
package checkstyle

import (
"context"
Expand All @@ -8,52 +8,64 @@ import (
"strings"
"sync"

"github.com/DevSymphony/sym-cli/internal/adapter"
"github.com/DevSymphony/sym-cli/internal/llm"
"github.com/DevSymphony/sym-cli/pkg/schema"
)

// CheckstyleLinterConverter converts rules to Checkstyle XML using LLM
type CheckstyleLinterConverter struct{}
// Converter converts rules to Checkstyle XML configuration using LLM
type Converter struct{}

// NewCheckstyleLinterConverter creates a new Checkstyle converter
func NewCheckstyleLinterConverter() *CheckstyleLinterConverter {
return &CheckstyleLinterConverter{}
// NewConverter creates a new Checkstyle converter
func NewConverter() *Converter {
return &Converter{}
}

// Name returns the linter name
func (c *CheckstyleLinterConverter) Name() string {
func (c *Converter) Name() string {
return "checkstyle"
}

// SupportedLanguages returns supported languages
func (c *CheckstyleLinterConverter) SupportedLanguages() []string {
func (c *Converter) SupportedLanguages() []string {
return []string{"java"}
}

// checkstyleModule represents a Checkstyle module
// GetLLMDescription returns a description of Checkstyle's capabilities for LLM routing
func (c *Converter) GetLLMDescription() string {
return `Java style checks (naming, whitespace, imports, line length, complexity)
- CAN: Class/method/variable naming, line/method length, indentation, import checks, cyclomatic complexity, JavaDoc
- CANNOT: Runtime behavior, business logic, security vulnerabilities, advanced design patterns`
}

// GetRoutingHints returns routing rules for LLM to decide when to use Checkstyle
func (c *Converter) GetRoutingHints() []string {
return []string{
"For Java naming rules (class names, variable names, method names) → ALWAYS use checkstyle",
"For Java formatting rules (line length, indentation, whitespace) → use checkstyle",
"For Java import rules (star imports, unused imports) → use checkstyle",
}
}

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
type checkstyleProperty struct {
XMLName xml.Name `xml:"property"`
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}

// checkstyleConfig represents root configuration
type checkstyleConfig struct {
XMLName xml.Name `xml:"module"`
Name string `xml:"name,attr"`
Modules []checkstyleModule `xml:"module"`
}

// ConvertRules converts user rules to Checkstyle configuration using LLM
func (c *CheckstyleLinterConverter) ConvertRules(ctx context.Context, rules []schema.UserRule, llmClient *llm.Client) (*LinterConfig, error) {
func (c *Converter) ConvertRules(ctx context.Context, rules []schema.UserRule, llmClient *llm.Client) (*adapter.LinterConfig, error) {
if llmClient == nil {
return nil, fmt.Errorf("LLM client is required")
}
Expand Down Expand Up @@ -106,15 +118,45 @@ func (c *CheckstyleLinterConverter) ConvertRules(ctx context.Context, rules []sc
return nil, fmt.Errorf("no rules converted: %v", errors)
}

// Separate modules into Checker-level and TreeWalker-level
// Checker-level modules (NOT under TreeWalker)
checkerLevelModules := map[string]bool{
"LineLength": true,
"FileLength": true,
"FileTabCharacter": true,
"NewlineAtEndOfFile": true,
"UniqueProperties": true,
"OrderedProperties": true,
"Translation": true,
"SuppressWarningsFilter": true,
"BeforeExecutionExclusionFileFilter": true,
"SuppressionFilter": true,
"SuppressionCommentFilter": true,
}

var checkerModules []checkstyleModule
var treeWalkerModules []checkstyleModule

for _, module := range modules {
if checkerLevelModules[module.Name] {
checkerModules = append(checkerModules, module)
} else {
treeWalkerModules = append(treeWalkerModules, module)
}
}

// Build Checkstyle configuration
// TreeWalker contains TreeWalker-level modules
treeWalker := checkstyleModule{
Name: "TreeWalker",
Modules: modules,
Modules: treeWalkerModules,
}

// Checker contains Checker-level modules + TreeWalker
allModules := append(checkerModules, treeWalker)
config := checkstyleConfig{
Name: "Checker",
Modules: []checkstyleModule{treeWalker},
Modules: allModules,
}

// Marshal to XML
Expand All @@ -131,15 +173,15 @@ func (c *CheckstyleLinterConverter) ConvertRules(ctx context.Context, rules []sc
`
fullContent := []byte(xmlHeader + string(content))

return &LinterConfig{
return &adapter.LinterConfig{
Filename: "checkstyle.xml",
Content: fullContent,
Format: "xml",
}, nil
}

// convertSingleRule converts a single rule using LLM
func (c *CheckstyleLinterConverter) convertSingleRule(ctx context.Context, rule schema.UserRule, llmClient *llm.Client) (*checkstyleModule, error) {
func (c *Converter) convertSingleRule(ctx context.Context, rule schema.UserRule, llmClient *llm.Client) (*checkstyleModule, error) {
systemPrompt := `You are a Checkstyle configuration expert. Convert natural language Java coding rules to Checkstyle modules.

Return ONLY a JSON object (no markdown fences):
Expand All @@ -150,13 +192,18 @@ Return ONLY a JSON object (no markdown fences):
}

Common Checkstyle modules:
- Naming: TypeName, MethodName, ParameterName, LocalVariableName, ConstantName
- Naming: TypeName, MethodName, MemberName, ParameterName, LocalVariableName, StaticVariableName, ConstantName
- Length: LineLength, MethodLength, ParameterNumber, FileLength
- Style: Indentation, WhitespaceAround, NeedBraces, LeftCurly, RightCurly
- Imports: AvoidStarImport, IllegalImport, UnusedImports
- Complexity: CyclomaticComplexity, NPathComplexity
- JavaDoc: JavadocMethod, JavadocType, MissingJavadocMethod

IMPORTANT - Use MemberName for class fields (instance variables), NOT LocalVariableName:
- MemberName: private/protected/public instance variables (class fields)
- LocalVariableName: variables declared inside methods (local scope only)
- StaticVariableName: static non-final variables

If cannot convert, return:
{
"module_name": "",
Expand All @@ -180,6 +227,30 @@ Output:
"module_name": "LocalVariableName",
"severity": "error",
"properties": {"format": "^[a-z][a-zA-Z0-9]*$"}
}

Input: "Private member variables must start with m_"
Output:
{
"module_name": "MemberName",
"severity": "error",
"properties": {"format": "^m_[a-z][a-zA-Z0-9]*$"}
}

Input: "Class names must be PascalCase"
Output:
{
"module_name": "TypeName",
"severity": "error",
"properties": {"format": "^[A-Z][a-zA-Z0-9]*$"}
}

Input: "Method names must be camelCase"
Output:
{
"module_name": "MethodName",
"severity": "error",
"properties": {"format": "^[a-z][a-zA-Z0-9]*$"}
}`

userPrompt := fmt.Sprintf("Convert this Java rule to Checkstyle module:\n\n%s", rule.Say)
Expand All @@ -196,14 +267,18 @@ Output:
response = strings.TrimSuffix(response, "```")
response = strings.TrimSpace(response)

if response == "" {
return nil, fmt.Errorf("LLM returned empty response")
}

var result struct {
ModuleName string `json:"module_name"`
Severity string `json:"severity"`
Properties map[string]string `json:"properties"`
}

if err := json.Unmarshal([]byte(response), &result); err != nil {
return nil, fmt.Errorf("failed to parse LLM response: %w", err)
return nil, fmt.Errorf("failed to parse LLM response: %w (response: %.100s)", err, response)
}

if result.ModuleName == "" {
Expand Down
3 changes: 1 addition & 2 deletions internal/adapter/checkstyle/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ func (a *Adapter) execute(ctx context.Context, config []byte, files []string) (*
}
args = append(args, files...)

// Execute
// Execute (uses CWD by default)
start := time.Now()
a.executor.WorkDir = a.WorkDir

output, err := a.executor.Execute(ctx, a.JavaPath, args...)
duration := time.Since(start)
Expand Down
14 changes: 14 additions & 0 deletions internal/adapter/checkstyle/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package checkstyle

import (
"github.com/DevSymphony/sym-cli/internal/adapter"
"github.com/DevSymphony/sym-cli/internal/adapter/registry"
)

func init() {
_ = registry.Global().RegisterTool(
NewAdapter(adapter.DefaultToolsDir()),
NewConverter(),
"checkstyle.xml",
)
}
13 changes: 13 additions & 0 deletions internal/adapter/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package adapter

import (
"os"
"path/filepath"
)

// DefaultToolsDir returns the standard tools directory (~/.sym/tools).
// Used by all adapters for consistent tool installation location.
func DefaultToolsDir() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".sym", "tools")
}
12 changes: 5 additions & 7 deletions internal/adapter/eslint/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,27 @@ import (
// - Length rules: max-len, max-lines, max-params, max-lines-per-function
// - Style rules: indent, quotes, semi, comma-dangle
// - AST rules: Custom rule generation
//
// Note: Adapter is goroutine-safe and stateless. WorkDir is determined
// by CWD at execution time, not stored in the adapter.
type Adapter struct {
// ToolsDir is where ESLint is installed
// Default: ~/.sym/tools/node_modules
// Default: ~/.sym/tools
ToolsDir string

// WorkDir is the project root
WorkDir string

// executor runs ESLint subprocess
executor *adapter.SubprocessExecutor
}

// NewAdapter creates a new ESLint adapter.
func NewAdapter(toolsDir, workDir string) *Adapter {
func NewAdapter(toolsDir string) *Adapter {
if toolsDir == "" {
home, _ := os.UserHomeDir()
// symphonyclient integration: .symphony → .sym directory
toolsDir = filepath.Join(home, ".sym", "tools")
}

return &Adapter{
ToolsDir: toolsDir,
WorkDir: workDir,
executor: adapter.NewSubprocessExecutor(),
}
}
Expand Down
Loading