Add parsers module
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled

This commit is contained in:
2026-02-02 02:38:26 +00:00
parent 932985241b
commit 41e0239ff2

341
src/cli/main.py Normal file
View File

@@ -0,0 +1,341 @@
from pathlib import Path
from typing import Optional
import click
from rich.console import Console
from rich.table import Table
from rich.tree import Tree
from rich.panel import Panel
from rich.text import Text
from src.parsers.base import BaseParser
from src.parsers.python import PythonParser
from src.parsers.javascript import JavaScriptParser
from src.parsers.go import GoParser
from src.parsers.rust import RustParser
from src.graph.builder import GraphBuilder, GraphType
from src.analyzers.dependencies import DependencyAnalyzer
from src.analyzers.complexity import ComplexityCalculator
from src.exporters.dot import DOTExporter
from src.exporters.json_exporter import JSONExporter
from src.exporters.png import PNGExporter
console = Console()
@click.group()
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
@click.option("--config", "-c", type=click.Path(), help="Path to config file")
@click.pass_context
def cli(ctx: click.Context, verbose: bool, config: Optional[str]):
ctx.ensure_object(dict)
ctx.obj["verbose"] = verbose
ctx.obj["config"] = config
@cli.command()
@click.argument("path", type=click.Path(exists=True, file_okay=False, dir_okay=True))
@click.option("--language", "-l", type=click.Choice(["python", "javascript", "go", "rust", "auto"]), default="auto")
@click.option("--output", "-o", type=click.Path(), help="Output file path")
@click.option("--format", "-f", type=click.Choice(["dot", "json", "png"]), default="json")
@click.option("--include-files/--no-include-files", default=True, help="Include file nodes")
@click.option("--include-functions/--no-include-functions", default=True, help="Include function nodes")
@click.option("--include-classes/--no-include-classes", default=True, help="Include class nodes")
@click.pass_context
def analyze(
ctx: click.Context,
path: str,
language: str,
output: Optional[str],
format: str,
include_files: bool,
include_functions: bool,
include_classes: bool,
):
verbose = ctx.obj.get("verbose", False)
path_obj = Path(path)
console.print(f"[bold]Analyzing codebase: {path_obj}[/bold]")
parser = _get_parser(language, path_obj)
if verbose:
console.print(f"Using parser: {parser.__class__.__name__}")
results = _parse_files(path_obj, parser, verbose)
if verbose:
console.print(f"Parsed {len(results)} files")
graph_builder = GraphBuilder(GraphType.DIRECTED)
graph_builder.build_from_parser_results(results)
if verbose:
nodes = graph_builder.get_nodes()
console.print(f"Graph contains {len(nodes)} nodes")
if format == "dot":
exporter = DOTExporter(graph_builder)
content = exporter.get_string()
if output:
Path(output).write_text(content)
console.print(f"Exported DOT to: {output}")
else:
console.print(content)
elif format == "json":
exporter = JSONExporter(graph_builder)
content = exporter.get_string()
if output:
Path(output).write_text(content)
console.print(f"Exported JSON to: {output}")
else:
console.print(content)
elif format == "png":
if output:
exporter = PNGExporter(graph_builder)
exporter.export(Path(output))
console.print(f"Exported PNG to: {output}")
else:
console.print("[red]Error: PNG format requires output file path[/red]")
_display_summary(graph_builder, results, verbose)
@cli.command()
@click.argument("path", type=click.Path(exists=True, file_okay=False, dir_okay=True))
@click.option("--language", "-l", type=click.Choice(["python", "javascript", "go", "rust", "auto"]), default="auto")
@click.pass_context
def visualize(ctx: click.Context, path: str, language: str):
path_obj = Path(path)
console.print(f"[bold]Generating visualization: {path_obj}[/bold]")
parser = _get_parser(language, path_obj)
results = _parse_files(path_obj, parser, False)
graph_builder = GraphBuilder(GraphType.DIRECTED)
graph_builder.build_from_parser_results(results)
tree = Tree(f"[bold]{path_obj.name}[/bold]")
for result in results:
file_branch = tree.add(f"[cyan]{result.file_path.name}[/cyan]")
for entity in result.entities:
if entity.entity_type.value in ["function", "method"]:
file_branch.add(f"[green]f: {entity.name}()[/green]")
elif entity.entity_type.value == "class":
class_branch = file_branch.add(f"[yellow]c: {entity.name}[/yellow]")
for child in entity.children:
class_branch.add(f"[green]m: {child.name}()[/green]")
console.print(tree)
@cli.command()
@click.argument("path", type=click.Path(exists=True, file_okay=False, dir_okay=True))
@click.option("--language", "-l", type=click.Choice(["python", "javascript", "go", "rust", "auto"]), default="auto")
@click.pass_context
def complexity(ctx: click.Context, path: str, language: str):
path_obj = Path(path)
console.print(f"[bold]Analyzing complexity: {path_obj}[/bold]")
parser = _get_parser(language, path_obj)
results = _parse_files(path_obj, parser, False)
calculator = ComplexityCalculator()
all_entities = []
for result in results:
all_entities.extend(result.entities)
complexity_report = calculator.calculate_project_complexity(all_entities)
table = Table(title="Complexity Analysis")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="magenta")
table.add_row("Total Functions", str(complexity_report["total_functions"]))
table.add_row("Total Classes", str(complexity_report["total_classes"]))
table.add_row("Total Complexity", str(complexity_report["total_cyclomatic_complexity"]))
table.add_row("Average Complexity", str(complexity_report["average_complexity"]))
table.add_row("High Complexity Functions", str(complexity_report["high_complexity_count"]))
console.print(table)
dist = complexity_report["complexity_distribution"]
dist_table = Table(title="Complexity Distribution")
dist_table.add_column("Level", style="cyan")
dist_table.add_column("Count", style="magenta")
dist_table.add_row("Low (1-5)", str(dist.get("low", 0)))
dist_table.add_row("Medium (6-10)", str(dist.get("medium", 0)))
dist_table.add_row("High (11-20)", str(dist.get("high", 0)))
dist_table.add_row("Very High (21+)", str(dist.get("very_high", 0)))
console.print(dist_table)
if complexity_report["high_complexity_count"] > 0:
console.print("\n[bold red]High Complexity Functions:[/bold red]")
for func in complexity_report["functions"]:
if func["is_complex"]:
console.print(
f" - {func['name']} (score: {func['complexity_score']}, "
f"lines: {func['lines_of_code']})"
)
@cli.command()
@click.argument("path", type=click.Path(exists=True, file_okay=False, dir_okay=True))
@click.option("--language", "-l", type=click.Choice(["python", "javascript", "go", "rust", "auto"]), default="auto")
@click.pass_context
def deps(ctx: click.Context, path: str, language: str):
path_obj = Path(path)
console.print(f"[bold]Analyzing dependencies: {path_obj}[/bold]")
parser = _get_parser(language, path_obj)
results = _parse_files(path_obj, parser, False)
graph_builder = GraphBuilder(GraphType.DIRECTED)
graph_builder.build_from_parser_results(results)
analyzer = DependencyAnalyzer(graph_builder)
report = analyzer.analyze()
table = Table(title="Dependency Analysis")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="magenta")
table.add_row("Total Files", str(report.total_files))
table.add_row("Total Functions", str(report.total_functions))
table.add_row("Total Classes", str(report.total_classes))
console.print(table)
if report.circular_dependencies:
console.print("\n[bold red]Circular Dependencies Found:[/bold red]")
for i, cycle in enumerate(report.circular_dependencies, 1):
console.print(f" Cycle {i}: {' -> '.join(cycle)}")
if report.orphan_files:
console.print("\n[bold yellow]Orphan Files (no dependencies):[/bold yellow]")
for orphan in report.orphan_files[:10]:
console.print(f" - {orphan}")
if len(report.orphan_files) > 10:
console.print(f" ... and {len(report.orphan_files) - 10} more")
layers = analyzer.get_architecture_layers()
if layers:
console.print("\n[bold]Architecture Layers:[/bold]")
for layer, files in layers.items():
console.print(f" {layer}: {len(files)} files")
@cli.command()
@click.argument("path", type=click.Path(exists=True, file_okay=False, dir_okay=True))
@click.option("--language", "-l", type=click.Choice(["python", "javascript", "go", "rust", "auto"]), default="auto")
@click.option("--output", "-o", type=click.Path(), required=True)
@click.pass_context
def export(ctx: click.Context, path: str, language: str, output: str):
path_obj = Path(path)
output_path = Path(output)
console.print(f"[bold]Exporting graph: {path_obj} -> {output_path}[/bold]")
parser = _get_parser(language, path_obj)
results = _parse_files(path_obj, parser, False)
graph_builder = GraphBuilder(GraphType.DIRECTED)
graph_builder.build_from_parser_results(results)
if output_path.suffix == ".dot":
exporter = DOTExporter(graph_builder)
exporter.export(output_path)
elif output_path.suffix == ".json":
exporter = JSONExporter(graph_builder)
exporter.export(output_path)
elif output_path.suffix in [".png", ".svg"]:
exporter = PNGExporter(graph_builder)
exporter.export(output_path, format=output_path.suffix[1:])
else:
console.print(f"[red]Unsupported output format: {output_path.suffix}[/red]")
return
console.print(f"[green]Exported successfully to: {output_path}[/green]")
def _get_parser(language: str, path: Path) -> BaseParser:
if language == "python":
return PythonParser()
elif language == "javascript":
return JavaScriptParser()
elif language == "go":
return GoParser()
elif language == "rust":
return RustParser()
else:
for ext in [".py", ".pyi"]:
if path.glob(f"**/{ext}"):
return PythonParser()
for ext in [".js", ".jsx"]:
if path.glob(f"**/{ext}"):
return JavaScriptParser()
for ext in [".go"]:
if path.glob(f"**/{ext}"):
return GoParser()
for ext in [".rs"]:
if path.glob(f"**/{ext}"):
return RustParser()
return PythonParser()
def _parse_files(path: Path, parser: BaseParser, verbose: bool):
results = []
extensions = parser.SUPPORTED_EXTENSIONS
for ext in extensions:
for file_path in path.rglob(f"*{ext}"):
if "test_" in file_path.name or "_test." in file_path.name:
continue
try:
content = file_path.read_text(encoding="utf-8")
result = parser.parse(file_path, content)
if result.errors and verbose:
console.print(f"[yellow]Warning: {result.errors}[/yellow]")
if result.entities:
results.append(result)
except Exception as e:
if verbose:
console.print(f"[red]Error parsing {file_path}: {e}[/red]")
return results
def _display_summary(graph_builder: GraphBuilder, results: list, verbose: bool):
nodes = graph_builder.get_nodes()
file_count = len([n for n in nodes if n.node_type.value == "file"])
func_count = len([n for n in nodes if n.node_type.value == "function"])
class_count = len([n for n in nodes if n.node_type.value == "class"])
summary = Panel(
Text(
f"Files: {file_count}\n"
f"Functions: {func_count}\n"
f"Classes: {class_count}\n"
f"Total Nodes: {len(nodes)}",
justify="left"
),
title="Analysis Summary",
style="blue"
)
console.print(summary)
def main():
cli(obj={})
if __name__ == "__main__":
main()