ai-codeindex 0.7.0__py3-none-any.whl
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.
- ai_codeindex-0.7.0.dist-info/METADATA +966 -0
- ai_codeindex-0.7.0.dist-info/RECORD +41 -0
- ai_codeindex-0.7.0.dist-info/WHEEL +4 -0
- ai_codeindex-0.7.0.dist-info/entry_points.txt +2 -0
- ai_codeindex-0.7.0.dist-info/licenses/LICENSE +21 -0
- codeindex/README_AI.md +767 -0
- codeindex/__init__.py +11 -0
- codeindex/adaptive_config.py +83 -0
- codeindex/adaptive_selector.py +171 -0
- codeindex/ai_helper.py +48 -0
- codeindex/cli.py +40 -0
- codeindex/cli_common.py +10 -0
- codeindex/cli_config.py +97 -0
- codeindex/cli_docs.py +66 -0
- codeindex/cli_hooks.py +765 -0
- codeindex/cli_scan.py +562 -0
- codeindex/cli_symbols.py +295 -0
- codeindex/cli_tech_debt.py +238 -0
- codeindex/config.py +479 -0
- codeindex/directory_tree.py +229 -0
- codeindex/docstring_processor.py +342 -0
- codeindex/errors.py +62 -0
- codeindex/extractors/__init__.py +9 -0
- codeindex/extractors/thinkphp.py +132 -0
- codeindex/file_classifier.py +148 -0
- codeindex/framework_detect.py +323 -0
- codeindex/hierarchical.py +428 -0
- codeindex/incremental.py +278 -0
- codeindex/invoker.py +260 -0
- codeindex/parallel.py +155 -0
- codeindex/parser.py +740 -0
- codeindex/route_extractor.py +98 -0
- codeindex/route_registry.py +77 -0
- codeindex/scanner.py +167 -0
- codeindex/semantic_extractor.py +408 -0
- codeindex/smart_writer.py +737 -0
- codeindex/symbol_index.py +199 -0
- codeindex/symbol_scorer.py +283 -0
- codeindex/tech_debt.py +619 -0
- codeindex/tech_debt_formatters.py +234 -0
- codeindex/writer.py +164 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Route extraction framework for multi-framework support.
|
|
2
|
+
|
|
3
|
+
This module provides the abstract base class for route extractors and
|
|
4
|
+
the extraction context data structure.
|
|
5
|
+
|
|
6
|
+
Epic 6: Framework-agnostic route extraction
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .framework_detect import RouteInfo
|
|
14
|
+
from .parser import ParseResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ExtractionContext:
|
|
19
|
+
"""
|
|
20
|
+
Context for route extraction.
|
|
21
|
+
|
|
22
|
+
Provides all necessary information for a route extractor to analyze
|
|
23
|
+
and extract routes from code.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
root_path: Path
|
|
27
|
+
"""Project root directory"""
|
|
28
|
+
|
|
29
|
+
current_dir: Path
|
|
30
|
+
"""Current directory being analyzed"""
|
|
31
|
+
|
|
32
|
+
parse_results: list[ParseResult]
|
|
33
|
+
"""Parsed code symbols from the current directory"""
|
|
34
|
+
|
|
35
|
+
framework_version: str = ""
|
|
36
|
+
"""Framework version (optional, for version-specific extraction)"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RouteExtractor(ABC):
|
|
40
|
+
"""
|
|
41
|
+
Abstract base class for framework-specific route extractors.
|
|
42
|
+
|
|
43
|
+
Each framework (ThinkPHP, Laravel, Django, FastAPI, etc.) should
|
|
44
|
+
implement this interface to provide route extraction capabilities.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
class ThinkPHPRouteExtractor(RouteExtractor):
|
|
48
|
+
@property
|
|
49
|
+
def framework_name(self) -> str:
|
|
50
|
+
return "thinkphp"
|
|
51
|
+
|
|
52
|
+
def can_extract(self, context: ExtractionContext) -> bool:
|
|
53
|
+
return context.current_dir.name == "Controller"
|
|
54
|
+
|
|
55
|
+
def extract_routes(self, context: ExtractionContext) -> list[RouteInfo]:
|
|
56
|
+
# Implementation here
|
|
57
|
+
return routes
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def framework_name(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Return the framework name (e.g., "thinkphp", "laravel", "django").
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Framework identifier in lowercase
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def can_extract(self, context: ExtractionContext) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Check if this extractor can extract routes from the given context.
|
|
75
|
+
|
|
76
|
+
This method is called to determine if the current directory is
|
|
77
|
+
relevant for this framework's route extraction.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
context: Extraction context with directory and parse results
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if this extractor should process this context
|
|
84
|
+
"""
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def extract_routes(self, context: ExtractionContext) -> list[RouteInfo]:
|
|
89
|
+
"""
|
|
90
|
+
Extract route information from the given context.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
context: Extraction context with directory and parse results
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List of RouteInfo objects representing discovered routes
|
|
97
|
+
"""
|
|
98
|
+
pass
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Route extractor registry for framework-agnostic route extraction.
|
|
2
|
+
|
|
3
|
+
This module provides a registry to store and retrieve route extractors
|
|
4
|
+
for different frameworks.
|
|
5
|
+
|
|
6
|
+
Epic 6: Framework-agnostic route extraction
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .route_extractor import RouteExtractor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RouteExtractorRegistry:
|
|
13
|
+
"""
|
|
14
|
+
Registry for route extractors.
|
|
15
|
+
|
|
16
|
+
Stores and manages route extractors for different frameworks.
|
|
17
|
+
Each extractor is registered by its framework name.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
registry = RouteExtractorRegistry()
|
|
21
|
+
registry.register(ThinkPHPRouteExtractor())
|
|
22
|
+
registry.register(LaravelRouteExtractor())
|
|
23
|
+
|
|
24
|
+
extractor = registry.get("thinkphp")
|
|
25
|
+
if extractor:
|
|
26
|
+
routes = extractor.extract_routes(context)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initialize an empty registry."""
|
|
31
|
+
self._extractors: dict[str, RouteExtractor] = {}
|
|
32
|
+
|
|
33
|
+
def register(self, extractor: RouteExtractor) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Register a route extractor.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
extractor: RouteExtractor instance to register
|
|
39
|
+
|
|
40
|
+
Note:
|
|
41
|
+
If an extractor with the same framework_name already exists,
|
|
42
|
+
it will be overwritten.
|
|
43
|
+
"""
|
|
44
|
+
self._extractors[extractor.framework_name] = extractor
|
|
45
|
+
|
|
46
|
+
def get(self, framework: str) -> RouteExtractor | None:
|
|
47
|
+
"""
|
|
48
|
+
Get a route extractor by framework name.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
framework: Framework name (e.g., "thinkphp", "laravel")
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
RouteExtractor instance if found, None otherwise
|
|
55
|
+
"""
|
|
56
|
+
return self._extractors.get(framework)
|
|
57
|
+
|
|
58
|
+
def has_extractor(self, framework: str) -> bool:
|
|
59
|
+
"""
|
|
60
|
+
Check if an extractor is registered for a framework.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
framework: Framework name to check
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if extractor is registered, False otherwise
|
|
67
|
+
"""
|
|
68
|
+
return framework in self._extractors
|
|
69
|
+
|
|
70
|
+
def list_frameworks(self) -> list[str]:
|
|
71
|
+
"""
|
|
72
|
+
List all registered framework names.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of framework names (sorted alphabetically)
|
|
76
|
+
"""
|
|
77
|
+
return sorted(self._extractors.keys())
|
codeindex/scanner.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Directory scanner for codeindex."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .config import Config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ScanResult:
|
|
12
|
+
"""Result of scanning a directory."""
|
|
13
|
+
|
|
14
|
+
path: Path
|
|
15
|
+
files: list[Path]
|
|
16
|
+
subdirs: list[Path]
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def indexable_files(self) -> list[Path]:
|
|
20
|
+
"""Get all indexable files (Python, PHP, etc.)."""
|
|
21
|
+
return self.files
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def python_files(self) -> list[Path]:
|
|
25
|
+
"""Get Python files only."""
|
|
26
|
+
return [f for f in self.files if f.suffix == ".py"]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def php_files(self) -> list[Path]:
|
|
30
|
+
"""Get PHP files only."""
|
|
31
|
+
return [f for f in self.files if f.suffix in (".php", ".phtml")]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
LANGUAGE_EXTENSIONS = {
|
|
35
|
+
"python": [".py"],
|
|
36
|
+
"php": [".php", ".phtml"],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_language_extensions(languages: list[str]) -> set[str]:
|
|
41
|
+
"""Get file extensions for specified languages."""
|
|
42
|
+
extensions = set()
|
|
43
|
+
for lang in languages:
|
|
44
|
+
extensions.update(LANGUAGE_EXTENSIONS.get(lang, []))
|
|
45
|
+
return extensions
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def should_exclude(path: Path, exclude_patterns: list[str], base_path: Path) -> bool:
|
|
49
|
+
"""Check if path matches any exclude pattern."""
|
|
50
|
+
# Resolve both paths to handle symlinks (e.g., /var -> /private/var on macOS)
|
|
51
|
+
rel_path = str(path.resolve().relative_to(base_path.resolve()))
|
|
52
|
+
|
|
53
|
+
for pattern in exclude_patterns:
|
|
54
|
+
if fnmatch.fnmatch(rel_path, pattern):
|
|
55
|
+
return True
|
|
56
|
+
if fnmatch.fnmatch(str(path), pattern):
|
|
57
|
+
return True
|
|
58
|
+
# Check if any parent matches
|
|
59
|
+
if "**" in pattern:
|
|
60
|
+
simple_pattern = pattern.replace("**", "*")
|
|
61
|
+
if fnmatch.fnmatch(rel_path, simple_pattern):
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def scan_directory(
|
|
68
|
+
path: Path,
|
|
69
|
+
config: Config,
|
|
70
|
+
base_path: Path | None = None,
|
|
71
|
+
recursive: bool = True
|
|
72
|
+
) -> ScanResult:
|
|
73
|
+
"""
|
|
74
|
+
Scan a directory and return its contents.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
path: Directory to scan
|
|
78
|
+
config: Configuration object
|
|
79
|
+
base_path: Base path for relative pattern matching
|
|
80
|
+
recursive: Whether to scan subdirectories recursively
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
ScanResult with files and subdirectories
|
|
84
|
+
"""
|
|
85
|
+
if base_path is None:
|
|
86
|
+
base_path = path
|
|
87
|
+
|
|
88
|
+
files: list[Path] = []
|
|
89
|
+
subdirs: list[Path] = []
|
|
90
|
+
|
|
91
|
+
if not path.exists() or not path.is_dir():
|
|
92
|
+
return ScanResult(path=path, files=[], subdirs=[])
|
|
93
|
+
|
|
94
|
+
for item in sorted(path.iterdir()):
|
|
95
|
+
# Skip excluded paths
|
|
96
|
+
if should_exclude(item, config.exclude, base_path):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if item.is_file():
|
|
100
|
+
# Filter by language/extension
|
|
101
|
+
if item.suffix == ".py" and "python" in config.languages:
|
|
102
|
+
files.append(item)
|
|
103
|
+
elif item.suffix in (".php", ".phtml") and "php" in config.languages:
|
|
104
|
+
files.append(item)
|
|
105
|
+
# Add more language support here in V2
|
|
106
|
+
elif item.is_dir() and recursive:
|
|
107
|
+
# Recursively scan subdirectories
|
|
108
|
+
sub_result = scan_directory(item, config, base_path, recursive)
|
|
109
|
+
files.extend(sub_result.files)
|
|
110
|
+
subdirs.extend(sub_result.subdirs)
|
|
111
|
+
subdirs.append(item) # Track the subdirectory itself
|
|
112
|
+
|
|
113
|
+
return ScanResult(path=path, files=files, subdirs=subdirs)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def find_all_directories(root: Path, config: Config) -> list[Path]:
|
|
117
|
+
"""
|
|
118
|
+
Find all directories that should be indexed.
|
|
119
|
+
|
|
120
|
+
If config.include is specified, recursively finds all subdirectories
|
|
121
|
+
with indexable files under those paths.
|
|
122
|
+
Otherwise, walks the entire directory tree.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
root: Root directory to start from
|
|
126
|
+
config: Configuration object
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of directory paths to index
|
|
130
|
+
"""
|
|
131
|
+
dirs_to_index: list[Path] = []
|
|
132
|
+
|
|
133
|
+
def walk_directory(current: Path):
|
|
134
|
+
"""Recursively walk a directory and collect all dirs with files."""
|
|
135
|
+
if should_exclude(current, config.exclude, root):
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Check if this directory has indexable files (non-recursive scan)
|
|
139
|
+
has_files = False
|
|
140
|
+
for item in current.iterdir():
|
|
141
|
+
if item.is_file():
|
|
142
|
+
if item.suffix == ".py" and "python" in config.languages:
|
|
143
|
+
has_files = True
|
|
144
|
+
break
|
|
145
|
+
elif item.suffix in (".php", ".phtml") and "php" in config.languages:
|
|
146
|
+
has_files = True
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
if has_files:
|
|
150
|
+
dirs_to_index.append(current)
|
|
151
|
+
|
|
152
|
+
# Recurse into subdirectories
|
|
153
|
+
for item in sorted(current.iterdir()):
|
|
154
|
+
if item.is_dir() and not should_exclude(item, config.exclude, root):
|
|
155
|
+
walk_directory(item)
|
|
156
|
+
|
|
157
|
+
# If include paths are specified, walk each one recursively
|
|
158
|
+
if config.include:
|
|
159
|
+
for include_path in config.include:
|
|
160
|
+
full_path = root / include_path
|
|
161
|
+
if full_path.exists() and full_path.is_dir():
|
|
162
|
+
walk_directory(full_path)
|
|
163
|
+
return dirs_to_index
|
|
164
|
+
|
|
165
|
+
# Otherwise, walk the entire directory tree from root
|
|
166
|
+
walk_directory(root)
|
|
167
|
+
return dirs_to_index
|