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,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