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,323 @@
|
|
|
1
|
+
"""Framework detection and pattern extraction for PHP projects."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from .parser import ParseResult
|
|
8
|
+
|
|
9
|
+
FrameworkType = Literal["thinkphp", "laravel", "unknown"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RouteInfo:
|
|
14
|
+
"""Information about a route."""
|
|
15
|
+
url: str
|
|
16
|
+
controller: str
|
|
17
|
+
action: str
|
|
18
|
+
method_signature: str = ""
|
|
19
|
+
line_number: int = 0 # Line number where method is defined
|
|
20
|
+
file_path: str = "" # File path relative to project root
|
|
21
|
+
description: str = "" # Method description (from docstring/comment)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def location(self) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Format location as file:line for easy navigation.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
file:line if line_number > 0, otherwise just file path
|
|
30
|
+
"""
|
|
31
|
+
if not self.file_path:
|
|
32
|
+
return ""
|
|
33
|
+
if self.line_number > 0:
|
|
34
|
+
return f"{self.file_path}:{self.line_number}"
|
|
35
|
+
return self.file_path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ModelInfo:
|
|
40
|
+
"""Information about a model."""
|
|
41
|
+
name: str
|
|
42
|
+
table: str # Inferred table name
|
|
43
|
+
file_path: Path | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class FrameworkInfo:
|
|
48
|
+
"""Detected framework information."""
|
|
49
|
+
framework: FrameworkType
|
|
50
|
+
version: str = ""
|
|
51
|
+
modules: list[str] = field(default_factory=list)
|
|
52
|
+
routes: list[RouteInfo] = field(default_factory=list)
|
|
53
|
+
models: list[ModelInfo] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def detect_framework(root: Path) -> FrameworkType:
|
|
57
|
+
"""
|
|
58
|
+
Detect which PHP framework is used in the project.
|
|
59
|
+
|
|
60
|
+
Detection rules:
|
|
61
|
+
- ThinkPHP: Has Application/ directory with Controller subdirs
|
|
62
|
+
- Laravel: Has app/Http/Controllers and artisan file
|
|
63
|
+
- Unknown: No recognized pattern
|
|
64
|
+
"""
|
|
65
|
+
# Check for ThinkPHP
|
|
66
|
+
app_dir = root / "Application"
|
|
67
|
+
if app_dir.exists():
|
|
68
|
+
# Look for Controller directories inside modules
|
|
69
|
+
for module_dir in app_dir.iterdir():
|
|
70
|
+
if module_dir.is_dir():
|
|
71
|
+
controller_dir = module_dir / "Controller"
|
|
72
|
+
if controller_dir.exists():
|
|
73
|
+
return "thinkphp"
|
|
74
|
+
|
|
75
|
+
# Check for Laravel
|
|
76
|
+
if (root / "artisan").exists() and (root / "app" / "Http" / "Controllers").exists():
|
|
77
|
+
return "laravel"
|
|
78
|
+
|
|
79
|
+
# Check composer.json for framework hints
|
|
80
|
+
composer_file = root / "composer.json"
|
|
81
|
+
if composer_file.exists():
|
|
82
|
+
try:
|
|
83
|
+
import json
|
|
84
|
+
with open(composer_file) as f:
|
|
85
|
+
data = json.load(f)
|
|
86
|
+
require = data.get("require", {})
|
|
87
|
+
if "topthink/framework" in require or "topthink/think" in require:
|
|
88
|
+
return "thinkphp"
|
|
89
|
+
if "laravel/framework" in require:
|
|
90
|
+
return "laravel"
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
return "unknown"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_thinkphp_routes(
|
|
98
|
+
parse_results: list[ParseResult],
|
|
99
|
+
module_name: str,
|
|
100
|
+
) -> list[RouteInfo]:
|
|
101
|
+
"""
|
|
102
|
+
Extract routes from ThinkPHP controllers.
|
|
103
|
+
|
|
104
|
+
ThinkPHP routing convention:
|
|
105
|
+
- URL: /module/controller/action
|
|
106
|
+
- Example: /admin/index/home -> Admin\\Controller\\IndexController::home()
|
|
107
|
+
"""
|
|
108
|
+
routes = []
|
|
109
|
+
|
|
110
|
+
for result in parse_results:
|
|
111
|
+
if result.error:
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# Find controller class
|
|
115
|
+
controller_class = None
|
|
116
|
+
for symbol in result.symbols:
|
|
117
|
+
if symbol.kind == "class" and symbol.name.endswith("Controller"):
|
|
118
|
+
controller_class = symbol.name
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
if not controller_class:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Extract controller name (remove "Controller" suffix)
|
|
125
|
+
controller_name = controller_class.replace("Controller", "").lower()
|
|
126
|
+
|
|
127
|
+
# Find public methods (actions)
|
|
128
|
+
for symbol in result.symbols:
|
|
129
|
+
if symbol.kind != "method":
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Only public methods are routes
|
|
133
|
+
if "public" not in symbol.signature.lower():
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Skip magic methods and internal methods
|
|
137
|
+
method_name = symbol.name.split("::")[-1]
|
|
138
|
+
if method_name.startswith("_") or method_name.startswith("__"):
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Build route URL
|
|
142
|
+
url = f"/{module_name.lower()}/{controller_name}/{method_name}"
|
|
143
|
+
|
|
144
|
+
routes.append(
|
|
145
|
+
RouteInfo(
|
|
146
|
+
url=url,
|
|
147
|
+
controller=controller_class,
|
|
148
|
+
action=method_name,
|
|
149
|
+
method_signature=symbol.signature,
|
|
150
|
+
line_number=symbol.line_start, # Epic 6, P1: Line number support
|
|
151
|
+
file_path=result.path.name, # File location
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return routes
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def extract_thinkphp_models(
|
|
159
|
+
parse_results: list[ParseResult],
|
|
160
|
+
) -> list[ModelInfo]:
|
|
161
|
+
"""
|
|
162
|
+
Extract model information from ThinkPHP models.
|
|
163
|
+
|
|
164
|
+
ThinkPHP model convention:
|
|
165
|
+
- Model name: UserModel -> table: tp_user (with prefix)
|
|
166
|
+
- Or uses $tableName property
|
|
167
|
+
"""
|
|
168
|
+
models = []
|
|
169
|
+
|
|
170
|
+
for result in parse_results:
|
|
171
|
+
if result.error:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
for symbol in result.symbols:
|
|
175
|
+
if symbol.kind != "class":
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
# Check if it's a model class
|
|
179
|
+
if not symbol.name.endswith("Model"):
|
|
180
|
+
# Also check if extends Model
|
|
181
|
+
is_model_subclass = (
|
|
182
|
+
"extends Model" in symbol.signature
|
|
183
|
+
or "extends BaseModel" in symbol.signature
|
|
184
|
+
)
|
|
185
|
+
if not is_model_subclass:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# Extract model name
|
|
189
|
+
model_name = symbol.name.replace("Model", "")
|
|
190
|
+
|
|
191
|
+
# Infer table name (ThinkPHP convention: lowercase + underscore)
|
|
192
|
+
# e.g., UserOrder -> user_order
|
|
193
|
+
table_name = ""
|
|
194
|
+
for i, char in enumerate(model_name):
|
|
195
|
+
if char.isupper() and i > 0:
|
|
196
|
+
table_name += "_"
|
|
197
|
+
table_name += char.lower()
|
|
198
|
+
|
|
199
|
+
models.append(ModelInfo(
|
|
200
|
+
name=symbol.name,
|
|
201
|
+
table=table_name,
|
|
202
|
+
file_path=result.path,
|
|
203
|
+
))
|
|
204
|
+
|
|
205
|
+
return models
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def analyze_thinkphp_project(
|
|
209
|
+
root: Path,
|
|
210
|
+
parse_results_by_dir: dict[Path, list[ParseResult]],
|
|
211
|
+
) -> FrameworkInfo:
|
|
212
|
+
"""
|
|
213
|
+
Analyze a ThinkPHP project and extract framework-specific information.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
root: Project root directory
|
|
217
|
+
parse_results_by_dir: Parse results grouped by directory
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
FrameworkInfo with routes, models, and module information
|
|
221
|
+
"""
|
|
222
|
+
info = FrameworkInfo(framework="thinkphp")
|
|
223
|
+
|
|
224
|
+
app_dir = root / "Application"
|
|
225
|
+
if not app_dir.exists():
|
|
226
|
+
return info
|
|
227
|
+
|
|
228
|
+
# Find modules
|
|
229
|
+
for module_dir in sorted(app_dir.iterdir()):
|
|
230
|
+
if not module_dir.is_dir():
|
|
231
|
+
continue
|
|
232
|
+
if module_dir.name.startswith("."):
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
info.modules.append(module_dir.name)
|
|
236
|
+
|
|
237
|
+
# Extract routes from Controller directory
|
|
238
|
+
controller_dir = module_dir / "Controller"
|
|
239
|
+
if controller_dir in parse_results_by_dir:
|
|
240
|
+
routes = extract_thinkphp_routes(
|
|
241
|
+
parse_results_by_dir[controller_dir],
|
|
242
|
+
module_dir.name,
|
|
243
|
+
)
|
|
244
|
+
info.routes.extend(routes)
|
|
245
|
+
|
|
246
|
+
# Extract models from Model directory
|
|
247
|
+
model_dir = module_dir / "Model"
|
|
248
|
+
if model_dir in parse_results_by_dir:
|
|
249
|
+
models = extract_thinkphp_models(parse_results_by_dir[model_dir])
|
|
250
|
+
info.models.extend(models)
|
|
251
|
+
|
|
252
|
+
return info
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def format_framework_info(info: FrameworkInfo, max_routes: int = 20) -> str:
|
|
256
|
+
"""
|
|
257
|
+
Format framework information for README output.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
info: Framework information
|
|
261
|
+
max_routes: Maximum routes to display per module
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Markdown formatted string
|
|
265
|
+
"""
|
|
266
|
+
if info.framework == "unknown":
|
|
267
|
+
return ""
|
|
268
|
+
|
|
269
|
+
lines = [
|
|
270
|
+
f"## Framework: {info.framework.title()}",
|
|
271
|
+
"",
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
if info.modules:
|
|
275
|
+
lines.append(f"**Modules**: {', '.join(info.modules)}")
|
|
276
|
+
lines.append("")
|
|
277
|
+
|
|
278
|
+
# Routes table (grouped by module)
|
|
279
|
+
if info.routes:
|
|
280
|
+
lines.append("### Routes")
|
|
281
|
+
lines.append("")
|
|
282
|
+
lines.append("| URL | Controller | Action |")
|
|
283
|
+
lines.append("|-----|------------|--------|")
|
|
284
|
+
|
|
285
|
+
# Group by module (first part of URL)
|
|
286
|
+
from collections import defaultdict
|
|
287
|
+
by_module = defaultdict(list)
|
|
288
|
+
for route in info.routes:
|
|
289
|
+
module = route.url.split("/")[1] if "/" in route.url else "default"
|
|
290
|
+
by_module[module].append(route)
|
|
291
|
+
|
|
292
|
+
shown = 0
|
|
293
|
+
for module, routes in sorted(by_module.items()):
|
|
294
|
+
for route in routes[:max_routes]:
|
|
295
|
+
lines.append(f"| `{route.url}` | {route.controller} | {route.action} |")
|
|
296
|
+
shown += 1
|
|
297
|
+
if shown >= max_routes * 3: # Limit total
|
|
298
|
+
break
|
|
299
|
+
if shown >= max_routes * 3:
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
total = len(info.routes)
|
|
303
|
+
if shown < total:
|
|
304
|
+
lines.append(f"| ... | _{total - shown} more routes_ | |")
|
|
305
|
+
|
|
306
|
+
lines.append("")
|
|
307
|
+
|
|
308
|
+
# Models table
|
|
309
|
+
if info.models:
|
|
310
|
+
lines.append("### Models")
|
|
311
|
+
lines.append("")
|
|
312
|
+
lines.append("| Model | Table |")
|
|
313
|
+
lines.append("|-------|-------|")
|
|
314
|
+
|
|
315
|
+
for model in sorted(info.models, key=lambda m: m.name)[:30]:
|
|
316
|
+
lines.append(f"| {model.name} | `{model.table}` |")
|
|
317
|
+
|
|
318
|
+
if len(info.models) > 30:
|
|
319
|
+
lines.append(f"| ... | _{len(info.models) - 30} more models_ |")
|
|
320
|
+
|
|
321
|
+
lines.append("")
|
|
322
|
+
|
|
323
|
+
return "\n".join(lines)
|