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.
@@ -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)