Skip to content
Draft
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
21 changes: 20 additions & 1 deletion src/codegen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@
# from codegen.extensions.index.file_index import FileIndex
# from codegen.extensions.langchain.agent import create_agent_with_tools, create_codebase_agent
from codegen.sdk.core.codebase import Codebase
from codegen.sdk.extensions.code_smells.detector import CodeSmellDetector
from codegen.sdk.extensions.code_smells.refactorer import CodeSmellRefactorer
from codegen.sdk.extensions.code_smells.smells import (
CodeSmell,
CodeSmellCategory,
CodeSmellSeverity,
)
from codegen.shared.enums.programming_language import ProgrammingLanguage

__all__ = ["CodeAgent", "Codebase", "CodegenApp", "Function", "ProgrammingLanguage", "function"]
__all__ = [
"CodeAgent",
"CodeSmell",
"CodeSmellCategory",
"CodeSmellDetector",
"CodeSmellRefactorer",
"CodeSmellSeverity",
"Codebase",
"CodegenApp",
"Function",
"ProgrammingLanguage",
"function",
]
2 changes: 2 additions & 0 deletions src/codegen/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from rich.traceback import install

from codegen.cli.commands.agent.main import agent_command
from codegen.cli.commands.code_smells.main import code_smells_command
from codegen.cli.commands.config.main import config_command
from codegen.cli.commands.create.main import create_command
from codegen.cli.commands.deploy.main import deploy_command
Expand Down Expand Up @@ -33,6 +34,7 @@ def main():

# Wrap commands with error handler
main.add_command(agent_command)
main.add_command(code_smells_command)
main.add_command(init_command)
main.add_command(logout_command)
main.add_command(login_command)
Expand Down
5 changes: 5 additions & 0 deletions src/codegen/cli/commands/code_smells/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Code smells detection and refactoring CLI commands."""

from codegen.cli.commands.code_smells.code_smells import code_smells

__all__ = ["code_smells"]
249 changes: 249 additions & 0 deletions src/codegen/cli/commands/code_smells/code_smells.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
"""CLI command for detecting and refactoring code smells."""

import json
from typing import Optional

import click
from rich.console import Console
from rich.tree import Tree

from codegen.cli.sdk.decorator import command
from codegen.sdk.core.codebase import Codebase
from codegen.sdk.extensions.code_smells.detector import CodeSmellDetector, DetectionConfig
from codegen.sdk.extensions.code_smells.refactorer import CodeSmellRefactorer
from codegen.sdk.extensions.code_smells.smells import (
CodeSmell,
CodeSmellCategory,
CodeSmellSeverity,
)
from codegen.shared.enums.programming_language import ProgrammingLanguage
from codegen.shared.logging.get_logger import get_logger

logger = get_logger(__name__)
console = Console()


@command(help="Detect and refactor code smells in your codebase")
@click.option(
"--path",
"-p",
type=click.Path(exists=True, file_okay=False, dir_okay=True),
default=".",
help="Path to the codebase to analyze",
)
@click.option(
"--language",
"-l",
type=click.Choice(["python", "typescript", "auto"]),
default="auto",
help="Programming language of the codebase",
)
@click.option(
"--severity",
"-s",
type=click.Choice(["low", "medium", "high", "critical", "all"]),
default="all",
help="Minimum severity level of code smells to detect",
)
@click.option(
"--category",
"-c",
type=click.Choice(["bloaters", "object_orientation_abusers", "change_preventers", "dispensables", "couplers", "all"]),
default="all",
help="Category of code smells to detect",
)
@click.option(
"--refactor",
"-r",
is_flag=True,
help="Automatically refactor detected code smells when possible",
)
@click.option(
"--output",
"-o",
type=click.Path(file_okay=True, dir_okay=False),
help="Path to output JSON report",
)
@click.option(
"--long-function-lines",
type=int,
default=50,
help="Threshold for long function detection (lines)",
)
@click.option(
"--long-parameter-list",
type=int,
default=5,
help="Threshold for long parameter list detection",
)
@click.option(
"--duplicate-code-min-lines",
type=int,
default=6,
help="Minimum lines for duplicate code detection",
)
def code_smells(
path: str,
language: str,
severity: str,
category: str,
refactor: bool,
output: Optional[str],
long_function_lines: int,
long_parameter_list: int,
duplicate_code_min_lines: int,
) -> None:
"""Detect and optionally refactor code smells in a codebase.

This command analyzes a codebase for common code smells like long functions,
duplicate code, dead code, etc. It can also automatically refactor some of
these issues.

Args:
path: Path to the codebase to analyze
language: Programming language of the codebase
severity: Minimum severity level of code smells to detect
category: Category of code smells to detect
refactor: Whether to automatically refactor detected code smells
output: Path to output JSON report
long_function_lines: Threshold for long function detection
long_parameter_list: Threshold for long parameter list detection
duplicate_code_min_lines: Minimum lines for duplicate code detection
"""
# Determine the programming language
prog_language = None
if language != "auto":
prog_language = ProgrammingLanguage(language.upper())

# Initialize the codebase
console.print(f"[bold blue]Analyzing codebase at [cyan]{path}[/cyan]...[/bold blue]")
codebase = Codebase(path, language=prog_language)

# Configure the detector
config = DetectionConfig(
long_function_lines=long_function_lines,
long_parameter_list_threshold=long_parameter_list,
duplicate_code_min_lines=duplicate_code_min_lines,
)

# Initialize the detector and refactorer
detector = CodeSmellDetector(codebase, config)
refactorer = CodeSmellRefactorer(codebase)

# Detect code smells
console.print("[bold blue]Detecting code smells...[/bold blue]")
with console.status("[bold green]Analyzing code...[/bold green]"):
all_smells = detector.detect_all()

# Filter by severity
if severity != "all":
severity_level = CodeSmellSeverity[severity.upper()]
all_smells = [smell for smell in all_smells if smell.severity.value >= severity_level.value]

# Filter by category
if category != "all":
category_map = {
"bloaters": CodeSmellCategory.BLOATERS,
"object_orientation_abusers": CodeSmellCategory.OBJECT_ORIENTATION_ABUSERS,
"change_preventers": CodeSmellCategory.CHANGE_PREVENTERS,
"dispensables": CodeSmellCategory.DISPENSABLES,
"couplers": CodeSmellCategory.COUPLERS,
}
category_enum = category_map[category]
all_smells = [smell for smell in all_smells if smell.category == category_enum]

# Display results
if not all_smells:
console.print("[bold green]No code smells detected![/bold green]")
return

console.print(f"[bold yellow]Detected {len(all_smells)} code smells:[/bold yellow]")

# Group by category
smells_by_category: dict[CodeSmellCategory, list[CodeSmell]] = {}
for smell in all_smells:
if smell.category not in smells_by_category:
smells_by_category[smell.category] = []
smells_by_category[smell.category].append(smell)

# Create a tree view of the results
tree = Tree("[bold]Code Smells by Category[/bold]")
for category, smells in smells_by_category.items():
category_node = tree.add(f"[bold]{category.name}[/bold] ({len(smells)} issues)")

# Group by severity within each category
smells_by_severity: dict[CodeSmellSeverity, list[CodeSmell]] = {}
for smell in smells:
if smell.severity not in smells_by_severity:
smells_by_severity[smell.severity] = []
smells_by_severity[smell.severity].append(smell)

# Add severity nodes
for severity, severity_smells in sorted(smells_by_severity.items(), key=lambda x: x[0].value, reverse=True):
severity_color = {
CodeSmellSeverity.LOW: "green",
CodeSmellSeverity.MEDIUM: "yellow",
CodeSmellSeverity.HIGH: "orange",
CodeSmellSeverity.CRITICAL: "red",
}[severity]

severity_node = category_node.add(f"[bold {severity_color}]{severity.name}[/bold {severity_color}] ({len(severity_smells)} issues)")

# Add individual smells
for smell in severity_smells:
refactorable = " [bold green](auto-refactorable)[/bold green]" if refactorer.can_refactor(smell) else ""
severity_node.add(f"{smell.symbol.name}: {smell.description}{refactorable}")

console.print(tree)

# Refactor if requested
if refactor:
refactorable_smells = [smell for smell in all_smells if refactorer.can_refactor(smell)]

if not refactorable_smells:
console.print("[bold yellow]No automatically refactorable code smells found.[/bold yellow]")
else:
console.print(f"[bold blue]Refactoring {len(refactorable_smells)} code smells...[/bold blue]")

with console.status("[bold green]Refactoring code...[/bold green]"):
results = refactorer.refactor_all(refactorable_smells)

# Display refactoring results
success_count = sum(1 for success in results.values() if success)
console.print(f"[bold green]Successfully refactored {success_count}/{len(results)} code smells.[/bold green]")

if success_count < len(results):
console.print("[bold yellow]Some refactorings failed. See details below:[/bold yellow]")
for smell, success in results.items():
if not success:
console.print(f"[bold red]Failed to refactor:[/bold red] {smell}")

# Output JSON report if requested
if output:
report = {
"summary": {
"total_smells": len(all_smells),
"by_category": {category.name: len(smells) for category, smells in smells_by_category.items()},
"by_severity": {severity.name: len([s for s in all_smells if s.severity == severity]) for severity in CodeSmellSeverity},
"refactorable": len([s for s in all_smells if refactorer.can_refactor(s)]),
},
"smells": [
{
"name": smell.name,
"description": smell.description,
"category": smell.category.name,
"severity": smell.severity.name,
"symbol": smell.symbol.name,
"file": smell.symbol.file.path if hasattr(smell.symbol, "file") and smell.symbol.file else None,
"refactoring_suggestions": smell.refactoring_suggestions,
"can_auto_refactor": refactorer.can_refactor(smell),
}
for smell in all_smells
],
}

# Write the report
with open(output, "w") as f:
json.dump(report, f, indent=2)

console.print(f"[bold blue]Report written to [cyan]{output}[/cyan][/bold blue]")
14 changes: 14 additions & 0 deletions src/codegen/cli/commands/code_smells/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Main entry point for the code_smells command."""

import click

from codegen.cli.commands.code_smells import code_smells


@click.group(name="code-smells", help="Detect and refactor code smells in your codebase")
def code_smells_command():
"""Detect and refactor code smells in your codebase."""
pass


code_smells_command.add_command(code_smells, name="detect")
29 changes: 29 additions & 0 deletions src/codegen/sdk/extensions/code_smells/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Code smell detection and refactoring module for Codegen.

This module provides tools to automatically detect and refactor common code smells
in Python and TypeScript codebases.
"""

from codegen.sdk.extensions.code_smells.detector import CodeSmellDetector
from codegen.sdk.extensions.code_smells.refactorer import CodeSmellRefactorer
from codegen.sdk.extensions.code_smells.smells import (
CodeSmell,
ComplexConditional,
DataClump,
DeadCode,
DuplicateCode,
LongFunction,
LongParameterList,
)

__all__ = [
"CodeSmell",
"CodeSmellDetector",
"CodeSmellRefactorer",
"ComplexConditional",
"DataClump",
"DeadCode",
"DuplicateCode",
"LongFunction",
"LongParameterList",
]
Loading
Loading