Add parsers module
This commit is contained in:
341
src/cli/main.py
Normal file
341
src/cli/main.py
Normal 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()
|
||||
Reference in New Issue
Block a user