fancy-tree 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fancy_tree-0.1.0/PKG-INFO +60 -0
- fancy_tree-0.1.0/fancy_tree/cli.py +160 -0
- fancy_tree-0.1.0/fancy_tree/config/__init__.py +8 -0
- fancy_tree-0.1.0/fancy_tree/config/languages.yaml +95 -0
- fancy_tree-0.1.0/fancy_tree/core/__init__.py +45 -0
- fancy_tree-0.1.0/fancy_tree/core/config.py +258 -0
- fancy_tree-0.1.0/fancy_tree/core/discovery.py +235 -0
- fancy_tree-0.1.0/fancy_tree/core/extraction.py +271 -0
- fancy_tree-0.1.0/fancy_tree/core/formatter.py +143 -0
- fancy_tree-0.1.0/fancy_tree/extractors/__init__.py +25 -0
- fancy_tree-0.1.0/fancy_tree/extractors/base.py +93 -0
- fancy_tree-0.1.0/fancy_tree/extractors/java.py +104 -0
- fancy_tree-0.1.0/fancy_tree/extractors/python.py +65 -0
- fancy_tree-0.1.0/fancy_tree/extractors/typescript.py +105 -0
- fancy_tree-0.1.0/fancy_tree/main.py +6 -0
- fancy_tree-0.1.0/fancy_tree/schema.py +178 -0
- fancy_tree-0.1.0/fancy_tree.egg-info/PKG-INFO +60 -0
- fancy_tree-0.1.0/fancy_tree.egg-info/SOURCES.txt +27 -0
- fancy_tree-0.1.0/fancy_tree.egg-info/dependency_links.txt +1 -0
- fancy_tree-0.1.0/fancy_tree.egg-info/entry_points.txt +2 -0
- fancy_tree-0.1.0/fancy_tree.egg-info/requires.txt +47 -0
- fancy_tree-0.1.0/fancy_tree.egg-info/top_level.txt +1 -0
- fancy_tree-0.1.0/pyproject.toml +101 -0
- fancy_tree-0.1.0/setup.cfg +4 -0
- fancy_tree-0.1.0/tests/test_extraction.py +217 -0
- fancy_tree-0.1.0/tests/test_phase1_verify_structure.py +41 -0
- fancy_tree-0.1.0/tests/test_phase2.py +336 -0
- fancy_tree-0.1.0/tests/test_phase3_complete.py +299 -0
- fancy_tree-0.1.0/tests/test_sample.py +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fancy-tree
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Git-enabled, cross-language code analysis that makes tree-sitter as easy as the tree command
|
|
5
|
+
Author: Antoine Descamps, Ishan Tiwari
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dant2021/fancy-tree
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/dant2021/fancy-tree/issues
|
|
9
|
+
Project-URL: Source, https://github.com/dant2021/fancy-tree
|
|
10
|
+
Keywords: git,tree-sitter,code-analysis,multi-language,ast
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: typer>=0.9.0
|
|
25
|
+
Requires-Dist: rich>=13.0.0
|
|
26
|
+
Requires-Dist: tree-sitter>=0.20.0
|
|
27
|
+
Requires-Dist: PyYAML>=6.0
|
|
28
|
+
Requires-Dist: tree-sitter-language-pack>=0.9.0
|
|
29
|
+
Provides-Extra: python
|
|
30
|
+
Requires-Dist: tree-sitter-python>=0.20.0; extra == "python"
|
|
31
|
+
Provides-Extra: typescript
|
|
32
|
+
Requires-Dist: tree-sitter-typescript>=0.20.0; extra == "typescript"
|
|
33
|
+
Provides-Extra: java
|
|
34
|
+
Requires-Dist: tree-sitter-java>=0.20.0; extra == "java"
|
|
35
|
+
Provides-Extra: javascript
|
|
36
|
+
Requires-Dist: tree-sitter-javascript>=0.20.0; extra == "javascript"
|
|
37
|
+
Provides-Extra: rust
|
|
38
|
+
Requires-Dist: tree-sitter-rust>=0.20.0; extra == "rust"
|
|
39
|
+
Provides-Extra: go
|
|
40
|
+
Requires-Dist: tree-sitter-go>=0.20.0; extra == "go"
|
|
41
|
+
Provides-Extra: cpp
|
|
42
|
+
Requires-Dist: tree-sitter-cpp>=0.20.0; extra == "cpp"
|
|
43
|
+
Provides-Extra: all-languages
|
|
44
|
+
Requires-Dist: tree-sitter-python>=0.20.0; extra == "all-languages"
|
|
45
|
+
Requires-Dist: tree-sitter-typescript>=0.20.0; extra == "all-languages"
|
|
46
|
+
Requires-Dist: tree-sitter-java>=0.20.0; extra == "all-languages"
|
|
47
|
+
Requires-Dist: tree-sitter-javascript>=0.20.0; extra == "all-languages"
|
|
48
|
+
Requires-Dist: tree-sitter-rust>=0.20.0; extra == "all-languages"
|
|
49
|
+
Requires-Dist: tree-sitter-go>=0.20.0; extra == "all-languages"
|
|
50
|
+
Requires-Dist: tree-sitter-cpp>=0.20.0; extra == "all-languages"
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
53
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
54
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
55
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
56
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
57
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
58
|
+
Provides-Extra: build
|
|
59
|
+
Requires-Dist: build>=0.8.0; extra == "build"
|
|
60
|
+
Requires-Dist: twine>=4.0.0; extra == "build"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Command-line interface for fancy_tree."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
from .core.extraction import process_repository
|
|
13
|
+
from .core.formatter import format_repository_tree
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(name="fancy-tree", help="Git-enabled, cross-language code analysis with tree-sitter")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def scan(
|
|
21
|
+
path: Optional[Path] = typer.Argument(None, help="Repository path to scan (default: current directory)"),
|
|
22
|
+
languages: Optional[List[str]] = typer.Option(None, "--lang", "-l", help="Filter by specific languages"),
|
|
23
|
+
max_files: Optional[int] = typer.Option(None, "--max-files", "-m", help="Maximum number of files to process"),
|
|
24
|
+
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path (default: stdout)"),
|
|
25
|
+
format: str = typer.Option("tree", "--format", "-f", help="Output format: tree, json"),
|
|
26
|
+
group_by_language: bool = typer.Option(True, "--group-by-lang/--group-by-structure", help="Group output by language or directory structure"),
|
|
27
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress progress output"),
|
|
28
|
+
):
|
|
29
|
+
"""Scan a repository and extract code structure."""
|
|
30
|
+
|
|
31
|
+
# Default to current directory
|
|
32
|
+
if path is None:
|
|
33
|
+
path = Path.cwd()
|
|
34
|
+
|
|
35
|
+
# Validate path
|
|
36
|
+
if not path.exists():
|
|
37
|
+
console.print(f"Error: Path '{path}' does not exist", style="red")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
if not path.is_dir():
|
|
41
|
+
console.print(f"Error: Path '{path}' is not a directory", style="red")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
|
|
44
|
+
if not quiet:
|
|
45
|
+
console.print(f"Scanning repository: {path}")
|
|
46
|
+
if languages:
|
|
47
|
+
console.print(f"Language filter: {', '.join(languages)}")
|
|
48
|
+
if max_files:
|
|
49
|
+
console.print(f"Max files: {max_files}")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# Process repository
|
|
53
|
+
repo_summary = process_repository(
|
|
54
|
+
repo_path=path,
|
|
55
|
+
language_filter=languages,
|
|
56
|
+
max_files=max_files
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Format output
|
|
60
|
+
if format == "json":
|
|
61
|
+
output_content = json.dumps(repo_summary.to_dict(), indent=2)
|
|
62
|
+
elif format == "tree":
|
|
63
|
+
output_content = format_repository_tree(
|
|
64
|
+
repo_summary,
|
|
65
|
+
group_by_language=group_by_language
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
console.print(f"Error: Unknown format '{format}'. Use 'tree' or 'json'", style="red")
|
|
69
|
+
raise typer.Exit(1)
|
|
70
|
+
|
|
71
|
+
# Output results
|
|
72
|
+
if output:
|
|
73
|
+
output.write_text(output_content, encoding='utf-8')
|
|
74
|
+
if not quiet:
|
|
75
|
+
console.print(f"Results written to: {output}")
|
|
76
|
+
else:
|
|
77
|
+
console.print(output_content)
|
|
78
|
+
|
|
79
|
+
if not quiet:
|
|
80
|
+
console.print(f"\nProcessed {repo_summary.total_files} files in {len(repo_summary.languages)} languages")
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
console.print(f"Error processing repository: {e}", style="red")
|
|
84
|
+
if not quiet:
|
|
85
|
+
import traceback
|
|
86
|
+
console.print(traceback.format_exc())
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command()
|
|
91
|
+
def languages():
|
|
92
|
+
"""List supported languages and their status."""
|
|
93
|
+
from .extractors import list_supported_languages
|
|
94
|
+
from .core.config import detect_available_languages
|
|
95
|
+
|
|
96
|
+
console.print(Panel("Fancy Tree - Supported Languages", style="bold blue"))
|
|
97
|
+
|
|
98
|
+
# Show implemented extractors
|
|
99
|
+
supported = list_supported_languages()
|
|
100
|
+
console.print(f"\nImplemented extractors: {len(supported)}")
|
|
101
|
+
for lang in sorted(supported):
|
|
102
|
+
console.print(f" ✓ {lang}")
|
|
103
|
+
|
|
104
|
+
# Try to detect language availability in current directory
|
|
105
|
+
try:
|
|
106
|
+
current_path = Path.cwd()
|
|
107
|
+
availability = detect_available_languages(current_path)
|
|
108
|
+
|
|
109
|
+
if availability:
|
|
110
|
+
console.print(f"\nLanguages detected in {current_path}:")
|
|
111
|
+
for lang, info in availability.items():
|
|
112
|
+
status = "AVAILABLE" if info.get("parser_available", False) else "PARSER MISSING"
|
|
113
|
+
file_count = info.get("file_count", 0)
|
|
114
|
+
console.print(f" {lang}: {file_count} files ({status})")
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
console.print(f"\nNote: Could not detect languages in current directory: {e}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@app.command()
|
|
121
|
+
def version():
|
|
122
|
+
"""Show version information."""
|
|
123
|
+
try:
|
|
124
|
+
from . import __version__
|
|
125
|
+
except ImportError:
|
|
126
|
+
__version__ = "1.0.0"
|
|
127
|
+
console.print(f"fancy-tree version {__version__}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command()
|
|
131
|
+
def test(
|
|
132
|
+
path: Optional[Path] = typer.Argument(None, help="Path to test (default: current directory)")
|
|
133
|
+
):
|
|
134
|
+
"""Test fancy_tree functionality on a directory."""
|
|
135
|
+
if path is None:
|
|
136
|
+
path = Path.cwd()
|
|
137
|
+
|
|
138
|
+
console.print(Panel(f"Testing fancy_tree on: {path}", style="bold green"))
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
# Quick test
|
|
142
|
+
repo_summary = process_repository(path, max_files=10)
|
|
143
|
+
|
|
144
|
+
console.print(f"✓ Successfully processed {repo_summary.total_files} files")
|
|
145
|
+
console.print(f"✓ Found {len(repo_summary.languages)} languages: {list(repo_summary.languages.keys())}")
|
|
146
|
+
|
|
147
|
+
# Show supported vs unsupported
|
|
148
|
+
supported_count = sum(1 for supported in repo_summary.supported_languages.values() if supported)
|
|
149
|
+
total_langs = len(repo_summary.supported_languages)
|
|
150
|
+
console.print(f"✓ Language support: {supported_count}/{total_langs} languages supported")
|
|
151
|
+
|
|
152
|
+
console.print("\nTest completed successfully!")
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
console.print(f"✗ Test failed: {e}", style="red")
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
app()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Extended language configuration for fancy_tree
|
|
2
|
+
# Multi-language symbol extraction with tree-sitter
|
|
3
|
+
|
|
4
|
+
python:
|
|
5
|
+
extensions: [".py"]
|
|
6
|
+
function_nodes: ["function_definition"]
|
|
7
|
+
class_nodes: ["class_definition"]
|
|
8
|
+
name_nodes: ["identifier"]
|
|
9
|
+
signature_templates:
|
|
10
|
+
function: "def {name}({params})"
|
|
11
|
+
method: "def {name}({params})"
|
|
12
|
+
class: "class {name}"
|
|
13
|
+
tree_sitter_package: "tree-sitter-python"
|
|
14
|
+
|
|
15
|
+
typescript:
|
|
16
|
+
extensions: [".ts", ".tsx"]
|
|
17
|
+
function_nodes: ["function_declaration", "method_definition", "arrow_function"]
|
|
18
|
+
class_nodes: ["class_declaration"]
|
|
19
|
+
interface_nodes: ["interface_declaration"]
|
|
20
|
+
name_nodes: ["identifier", "type_identifier"]
|
|
21
|
+
signature_templates:
|
|
22
|
+
function: "function {name}({params}): {return_type}"
|
|
23
|
+
method: "{name}({params}): {return_type}"
|
|
24
|
+
class: "class {name}"
|
|
25
|
+
interface: "interface {name}"
|
|
26
|
+
tree_sitter_package: "tree-sitter-typescript"
|
|
27
|
+
|
|
28
|
+
java:
|
|
29
|
+
extensions: [".java"]
|
|
30
|
+
function_nodes: ["method_declaration", "constructor_declaration"]
|
|
31
|
+
class_nodes: ["class_declaration", "interface_declaration", "enum_declaration"]
|
|
32
|
+
name_nodes: ["identifier"]
|
|
33
|
+
signature_templates:
|
|
34
|
+
method: "{visibility} {return_type} {name}({params})"
|
|
35
|
+
constructor: "{visibility} {name}({params})"
|
|
36
|
+
class: "{visibility} class {name}"
|
|
37
|
+
interface: "{visibility} interface {name}"
|
|
38
|
+
enum: "{visibility} enum {name}"
|
|
39
|
+
tree_sitter_package: "tree-sitter-java"
|
|
40
|
+
|
|
41
|
+
# Placeholder configurations for future languages
|
|
42
|
+
javascript:
|
|
43
|
+
extensions: [".js", ".jsx"]
|
|
44
|
+
function_nodes: ["function_declaration", "method_definition", "arrow_function"]
|
|
45
|
+
class_nodes: ["class_declaration"]
|
|
46
|
+
name_nodes: ["identifier"]
|
|
47
|
+
signature_templates:
|
|
48
|
+
function: "function {name}({params})"
|
|
49
|
+
method: "{name}({params})"
|
|
50
|
+
class: "class {name}"
|
|
51
|
+
tree_sitter_package: "tree-sitter-javascript"
|
|
52
|
+
|
|
53
|
+
rust:
|
|
54
|
+
extensions: [".rs"]
|
|
55
|
+
function_nodes: ["function_item"]
|
|
56
|
+
class_nodes: ["struct_item", "impl_item", "trait_item"]
|
|
57
|
+
name_nodes: ["identifier", "type_identifier"]
|
|
58
|
+
signature_templates:
|
|
59
|
+
function: "fn {name}({params}) -> {return_type}"
|
|
60
|
+
struct: "struct {name}"
|
|
61
|
+
trait: "trait {name}"
|
|
62
|
+
tree_sitter_package: "tree-sitter-rust"
|
|
63
|
+
|
|
64
|
+
go:
|
|
65
|
+
extensions: [".go"]
|
|
66
|
+
function_nodes: ["function_declaration", "method_declaration"]
|
|
67
|
+
class_nodes: ["type_declaration"]
|
|
68
|
+
name_nodes: ["identifier"]
|
|
69
|
+
signature_templates:
|
|
70
|
+
function: "func {name}({params}) {return_type}"
|
|
71
|
+
method: "func ({receiver}) {name}({params}) {return_type}"
|
|
72
|
+
type: "type {name}"
|
|
73
|
+
tree_sitter_package: "tree-sitter-go"
|
|
74
|
+
|
|
75
|
+
# Build files and special configurations
|
|
76
|
+
build_files:
|
|
77
|
+
- "build.gradle"
|
|
78
|
+
- "build.gradle.kts"
|
|
79
|
+
- "pom.xml"
|
|
80
|
+
- "package.json"
|
|
81
|
+
- "Cargo.toml"
|
|
82
|
+
- "go.mod"
|
|
83
|
+
- "pyproject.toml"
|
|
84
|
+
- "requirements.txt"
|
|
85
|
+
|
|
86
|
+
# File patterns to ignore
|
|
87
|
+
ignore_patterns:
|
|
88
|
+
- "__pycache__"
|
|
89
|
+
- "node_modules"
|
|
90
|
+
- ".git"
|
|
91
|
+
- "dist"
|
|
92
|
+
- "build"
|
|
93
|
+
- "target"
|
|
94
|
+
- "*.pyc"
|
|
95
|
+
- "*.class"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Core functionality for fancy_tree."""
|
|
2
|
+
|
|
3
|
+
from .config import (
|
|
4
|
+
get_language_config,
|
|
5
|
+
detect_language,
|
|
6
|
+
detect_available_languages,
|
|
7
|
+
show_language_status_and_install
|
|
8
|
+
)
|
|
9
|
+
from .discovery import (
|
|
10
|
+
discover_files,
|
|
11
|
+
classify_files,
|
|
12
|
+
scan_repository,
|
|
13
|
+
get_repository_info
|
|
14
|
+
)
|
|
15
|
+
from .extraction import (
|
|
16
|
+
extract_symbols_generic,
|
|
17
|
+
extract_symbols_from_file,
|
|
18
|
+
process_repository,
|
|
19
|
+
get_parser_for_language
|
|
20
|
+
)
|
|
21
|
+
from .formatter import (
|
|
22
|
+
format_repository_tree,
|
|
23
|
+
EnhancedTreeFormatter
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Config
|
|
28
|
+
"get_language_config",
|
|
29
|
+
"detect_language",
|
|
30
|
+
"detect_available_languages",
|
|
31
|
+
"show_language_status_and_install",
|
|
32
|
+
# Discovery
|
|
33
|
+
"discover_files",
|
|
34
|
+
"classify_files",
|
|
35
|
+
"scan_repository",
|
|
36
|
+
"get_repository_info",
|
|
37
|
+
# Extraction
|
|
38
|
+
"extract_symbols_generic",
|
|
39
|
+
"extract_symbols_from_file",
|
|
40
|
+
"process_repository",
|
|
41
|
+
"get_parser_for_language",
|
|
42
|
+
# Formatting
|
|
43
|
+
"format_repository_tree",
|
|
44
|
+
"EnhancedTreeFormatter"
|
|
45
|
+
]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Configuration management and dynamic dependency detection."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import yaml
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional, Set, Any
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
class LanguageConfig:
|
|
14
|
+
"""Configuration for a single language."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, language: str, config_dict: Dict[str, Any]):
|
|
17
|
+
self.language = language
|
|
18
|
+
self.extensions = config_dict.get("extensions", [])
|
|
19
|
+
self.function_nodes = config_dict.get("function_nodes", [])
|
|
20
|
+
self.class_nodes = config_dict.get("class_nodes", [])
|
|
21
|
+
self.interface_nodes = config_dict.get("interface_nodes", [])
|
|
22
|
+
self.name_nodes = config_dict.get("name_nodes", ["identifier"])
|
|
23
|
+
self.signature_templates = config_dict.get("signature_templates", {})
|
|
24
|
+
self.tree_sitter_package = config_dict.get("tree_sitter_package", f"tree-sitter-{language}")
|
|
25
|
+
self.language_function = config_dict.get("language_function", "language")
|
|
26
|
+
|
|
27
|
+
def get_template(self, symbol_type: str) -> str:
|
|
28
|
+
"""Get signature template for symbol type with fallback."""
|
|
29
|
+
return self.signature_templates.get(symbol_type, f"{symbol_type} {{name}}")
|
|
30
|
+
|
|
31
|
+
def __repr__(self):
|
|
32
|
+
return f"LanguageConfig({self.language}, {len(self.extensions)} extensions)"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConfigManager:
|
|
36
|
+
"""Manages language configurations and dependencies."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, config_path: Optional[Path] = None):
|
|
39
|
+
if config_path is None:
|
|
40
|
+
config_path = Path(__file__).parent.parent / "config" / "languages.yaml"
|
|
41
|
+
|
|
42
|
+
self.config_path = config_path
|
|
43
|
+
self.languages: Dict[str, LanguageConfig] = {}
|
|
44
|
+
self.build_files: List[str] = []
|
|
45
|
+
self.ignore_patterns: List[str] = []
|
|
46
|
+
self._loaded = False
|
|
47
|
+
|
|
48
|
+
def load_config(self) -> None:
|
|
49
|
+
"""Load language configuration from YAML."""
|
|
50
|
+
if self._loaded:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
55
|
+
config = yaml.safe_load(f)
|
|
56
|
+
|
|
57
|
+
# Load language configurations
|
|
58
|
+
for lang_name, lang_config in config.items():
|
|
59
|
+
if lang_name in ['build_files', 'ignore_patterns']:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if isinstance(lang_config, dict) and 'extensions' in lang_config:
|
|
63
|
+
self.languages[lang_name] = LanguageConfig(lang_name, lang_config)
|
|
64
|
+
|
|
65
|
+
# Load special configurations
|
|
66
|
+
self.build_files = config.get('build_files', [])
|
|
67
|
+
self.ignore_patterns = config.get('ignore_patterns', [])
|
|
68
|
+
|
|
69
|
+
self._loaded = True
|
|
70
|
+
|
|
71
|
+
except (FileNotFoundError, yaml.YAMLError) as e:
|
|
72
|
+
console.print(f"Warning: Could not load language config: {e}")
|
|
73
|
+
|
|
74
|
+
def get_language_config(self, language: str) -> Optional[LanguageConfig]:
|
|
75
|
+
"""Get configuration for specific language."""
|
|
76
|
+
self.load_config()
|
|
77
|
+
return self.languages.get(language)
|
|
78
|
+
|
|
79
|
+
def detect_language_from_extension(self, file_path: Path) -> Optional[str]:
|
|
80
|
+
"""Detect language from file extension."""
|
|
81
|
+
self.load_config()
|
|
82
|
+
file_ext = file_path.suffix.lower()
|
|
83
|
+
|
|
84
|
+
for lang_name, lang_config in self.languages.items():
|
|
85
|
+
if file_ext in lang_config.extensions:
|
|
86
|
+
return lang_name
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def scan_file_extensions(self, repo_path: Path) -> Set[str]:
|
|
91
|
+
"""Scan repository for file extensions."""
|
|
92
|
+
extensions = set()
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Use git ls-files if available
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
['git', 'ls-files'],
|
|
98
|
+
cwd=repo_path,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=30
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if result.returncode == 0:
|
|
105
|
+
for file_line in result.stdout.strip().split('\n'):
|
|
106
|
+
if file_line.strip():
|
|
107
|
+
ext = Path(file_line).suffix.lower()
|
|
108
|
+
if ext:
|
|
109
|
+
extensions.add(ext)
|
|
110
|
+
else:
|
|
111
|
+
# Fallback to file system scan
|
|
112
|
+
for file_path in repo_path.rglob('*'):
|
|
113
|
+
if file_path.is_file():
|
|
114
|
+
ext = file_path.suffix.lower()
|
|
115
|
+
if ext:
|
|
116
|
+
extensions.add(ext)
|
|
117
|
+
|
|
118
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, Exception):
|
|
119
|
+
# Fallback to file system scan
|
|
120
|
+
for file_path in repo_path.rglob('*'):
|
|
121
|
+
if file_path.is_file():
|
|
122
|
+
ext = file_path.suffix.lower()
|
|
123
|
+
if ext:
|
|
124
|
+
extensions.add(ext)
|
|
125
|
+
|
|
126
|
+
return extensions
|
|
127
|
+
|
|
128
|
+
def detect_available_languages(self, repo_path: Path) -> Dict[str, Dict[str, Any]]:
|
|
129
|
+
"""Detect which languages exist in repo and which parsers are available."""
|
|
130
|
+
self.load_config()
|
|
131
|
+
file_extensions = self.scan_file_extensions(repo_path)
|
|
132
|
+
language_availability = {}
|
|
133
|
+
|
|
134
|
+
for lang_name, lang_config in self.languages.items():
|
|
135
|
+
# Check if files of this language exist
|
|
136
|
+
has_files = any(ext in file_extensions for ext in lang_config.extensions)
|
|
137
|
+
|
|
138
|
+
if has_files:
|
|
139
|
+
# Check if tree-sitter package is available
|
|
140
|
+
package_name = lang_config.tree_sitter_package
|
|
141
|
+
module_name = package_name.replace('-', '_')
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
__import__(module_name)
|
|
145
|
+
parser_available = True
|
|
146
|
+
except ImportError:
|
|
147
|
+
parser_available = False
|
|
148
|
+
|
|
149
|
+
language_availability[lang_name] = {
|
|
150
|
+
"has_files": has_files,
|
|
151
|
+
"parser_available": parser_available,
|
|
152
|
+
"package_name": package_name,
|
|
153
|
+
"file_count": len([ext for ext in file_extensions if ext in lang_config.extensions])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return language_availability
|
|
157
|
+
|
|
158
|
+
def install_missing_packages(self, missing_packages: List[str]) -> bool:
|
|
159
|
+
"""Attempt auto-installation with user permission."""
|
|
160
|
+
if not missing_packages:
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
console.print(f"Missing tree-sitter packages for optimal support:")
|
|
164
|
+
for package in missing_packages:
|
|
165
|
+
console.print(f" - {package}")
|
|
166
|
+
|
|
167
|
+
install = typer.confirm("Install missing packages automatically?")
|
|
168
|
+
|
|
169
|
+
if install:
|
|
170
|
+
console.print("Installing packages...")
|
|
171
|
+
success_count = 0
|
|
172
|
+
|
|
173
|
+
for package in missing_packages:
|
|
174
|
+
try:
|
|
175
|
+
console.print(f"Installing {package}...")
|
|
176
|
+
result = subprocess.run(
|
|
177
|
+
[sys.executable, "-m", "pip", "install", package],
|
|
178
|
+
capture_output=True,
|
|
179
|
+
text=True,
|
|
180
|
+
timeout=120
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if result.returncode == 0:
|
|
184
|
+
console.print(f"SUCCESS: {package} installed successfully")
|
|
185
|
+
success_count += 1
|
|
186
|
+
else:
|
|
187
|
+
console.print(f"ERROR: Failed to install {package}: {result.stderr}")
|
|
188
|
+
|
|
189
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError) as e:
|
|
190
|
+
console.print(f"ERROR: Error installing {package}: {e}")
|
|
191
|
+
|
|
192
|
+
if success_count == len(missing_packages):
|
|
193
|
+
console.print("SUCCESS: All packages installed successfully!")
|
|
194
|
+
return True
|
|
195
|
+
else:
|
|
196
|
+
console.print(f"WARNING: {success_count}/{len(missing_packages)} packages installed")
|
|
197
|
+
return False
|
|
198
|
+
else:
|
|
199
|
+
console.print("INFO: Continuing with available languages only")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def show_language_status(self, language_availability: Dict[str, Dict[str, Any]]) -> None:
|
|
203
|
+
"""Display language support status."""
|
|
204
|
+
console.print("Language Support Status:")
|
|
205
|
+
|
|
206
|
+
if not language_availability:
|
|
207
|
+
console.print(" No supported languages detected in repository")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
missing_packages = []
|
|
211
|
+
|
|
212
|
+
for lang, info in language_availability.items():
|
|
213
|
+
has_files = info["has_files"]
|
|
214
|
+
parser_available = info["parser_available"]
|
|
215
|
+
file_count = info.get("file_count", 0)
|
|
216
|
+
|
|
217
|
+
if has_files:
|
|
218
|
+
if parser_available:
|
|
219
|
+
console.print(f" SUPPORTED: {lang}: {file_count} files (parser available)")
|
|
220
|
+
else:
|
|
221
|
+
console.print(f" NOT_SUPPORTED: {lang}: {file_count} files (parser missing)")
|
|
222
|
+
missing_packages.append(info["package_name"])
|
|
223
|
+
|
|
224
|
+
if missing_packages:
|
|
225
|
+
console.print(f"\nTIP: To enable full support: pip install {' '.join(missing_packages)}")
|
|
226
|
+
return missing_packages
|
|
227
|
+
else:
|
|
228
|
+
console.print("\nSUCCESS: All detected languages have parser support!")
|
|
229
|
+
return []
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# Global config manager instance
|
|
233
|
+
config_manager = ConfigManager()
|
|
234
|
+
|
|
235
|
+
# Convenience functions
|
|
236
|
+
def get_language_config(language: str) -> Optional[LanguageConfig]:
|
|
237
|
+
"""Get language configuration."""
|
|
238
|
+
return config_manager.get_language_config(language)
|
|
239
|
+
|
|
240
|
+
def detect_language(file_path: Path) -> Optional[str]:
|
|
241
|
+
"""Detect language from file path."""
|
|
242
|
+
return config_manager.detect_language_from_extension(file_path)
|
|
243
|
+
|
|
244
|
+
def detect_available_languages(repo_path: Path) -> Dict[str, Dict[str, Any]]:
|
|
245
|
+
"""Detect available languages in repository."""
|
|
246
|
+
return config_manager.detect_available_languages(repo_path)
|
|
247
|
+
|
|
248
|
+
def show_language_status_and_install(repo_path: Path) -> Dict[str, Dict[str, Any]]:
|
|
249
|
+
"""Show language status and offer to install missing packages."""
|
|
250
|
+
availability = config_manager.detect_available_languages(repo_path)
|
|
251
|
+
missing_packages = config_manager.show_language_status(availability)
|
|
252
|
+
|
|
253
|
+
if missing_packages:
|
|
254
|
+
config_manager.install_missing_packages(missing_packages)
|
|
255
|
+
# Re-check availability after installation
|
|
256
|
+
availability = config_manager.detect_available_languages(repo_path)
|
|
257
|
+
|
|
258
|
+
return availability
|