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