codegraph-engine 2.3.0__py3-none-any.whl → 3.0.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.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +172 -2
- codegraph/agent_capabilities.py +320 -3
- codegraph/agent_rules.py +1 -1
- codegraph/api_drift.py +455 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/change_impact.py +320 -110
- codegraph/cli.py +776 -25
- codegraph/cli_output.py +17 -2
- codegraph/context.py +38 -11
- codegraph/context_freshness.py +2 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +7 -0
- codegraph/evidence_contract.py +21 -0
- codegraph/frameworks.py +171 -6
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +2 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/indexer.py +291 -38
- codegraph/indexing/models.py +6 -0
- codegraph/indexing/parser.py +239 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +6 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +128 -20
- codegraph/mcp/server.py +190 -28
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/optimizer.py +42 -5
- codegraph/process_lifecycle.py +31 -13
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +403 -0
- codegraph/resolver.py +1202 -159
- codegraph/retrieval_policy.py +6 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +19 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +715 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +150 -38
- codegraph_engine-3.0.0.dist-info/RECORD +129 -0
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
codegraph/search/hybrid.py
CHANGED
|
@@ -623,6 +623,11 @@ def search_code(
|
|
|
623
623
|
|
|
624
624
|
config_exts = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf"}
|
|
625
625
|
filtered: list[dict[str, object]] = []
|
|
626
|
+
seen_file_lines: set[tuple[str, int]] = set()
|
|
627
|
+
files_with_exact_matches: set[str] = set()
|
|
628
|
+
|
|
629
|
+
query_terms = _safe_fts5_terms(clean_query)
|
|
630
|
+
query_terms_lower = [t.lower() for t in query_terms]
|
|
626
631
|
|
|
627
632
|
for item in raw_hits:
|
|
628
633
|
rel_path = item.path or item.file
|
|
@@ -659,6 +664,27 @@ def search_code(
|
|
|
659
664
|
d["match_type"] = "case_insensitive"
|
|
660
665
|
else:
|
|
661
666
|
d["match_type"] = "token"
|
|
667
|
+
|
|
668
|
+
hit_line = item.line
|
|
669
|
+
file_line_key = (rel_path, hit_line)
|
|
670
|
+
if file_line_key in seen_file_lines:
|
|
671
|
+
continue
|
|
672
|
+
|
|
673
|
+
# If multi-word query and this hit is only a token match:
|
|
674
|
+
# verify that all query terms appear in the snippet or matched text
|
|
675
|
+
if len(query_terms_lower) >= 2 and d["match_type"] == "token":
|
|
676
|
+
haystack = (str(d.get("snippet", "")) + " " + str(d.get("matched_text", ""))).lower()
|
|
677
|
+
if not all(t in haystack for t in query_terms_lower):
|
|
678
|
+
continue
|
|
679
|
+
|
|
680
|
+
# If a file already had an exact/case-insensitive match, discard subsequent loose token matches
|
|
681
|
+
if rel_path in files_with_exact_matches and d["match_type"] == "token":
|
|
682
|
+
continue
|
|
683
|
+
|
|
684
|
+
if d["match_type"] in ("exact", "case_insensitive"):
|
|
685
|
+
files_with_exact_matches.add(rel_path)
|
|
686
|
+
|
|
687
|
+
seen_file_lines.add(file_line_key)
|
|
662
688
|
filtered.append(d)
|
|
663
689
|
if len(filtered) >= effective_limit:
|
|
664
690
|
break
|
codegraph/semantic_decorators.py
CHANGED
|
@@ -66,6 +66,31 @@ _RECOGNIZED_DECORATOR_SPECS: tuple[SemanticDecoratorSpec, ...] = (
|
|
|
66
66
|
semantic_role="PROVIDES",
|
|
67
67
|
patterns=("provide", "di.provide", "provider", "singleton", "di.singleton"),
|
|
68
68
|
),
|
|
69
|
+
SemanticDecoratorSpec(
|
|
70
|
+
framework="langchain",
|
|
71
|
+
semantic_role="TOOL_HANDLER",
|
|
72
|
+
patterns=("tool", "langchain.tools.tool", "langchain_core.tools.tool", "agents.tool"),
|
|
73
|
+
),
|
|
74
|
+
SemanticDecoratorSpec(
|
|
75
|
+
framework="llama_index",
|
|
76
|
+
semantic_role="TOOL_HANDLER",
|
|
77
|
+
patterns=("function_tool", "llama_index.core.tools.function_tool"),
|
|
78
|
+
),
|
|
79
|
+
SemanticDecoratorSpec(
|
|
80
|
+
framework="ray",
|
|
81
|
+
semantic_role="TASK_HANDLER",
|
|
82
|
+
patterns=("ray.remote", "remote"),
|
|
83
|
+
),
|
|
84
|
+
SemanticDecoratorSpec(
|
|
85
|
+
framework="prefect",
|
|
86
|
+
semantic_role="TASK_HANDLER",
|
|
87
|
+
patterns=("prefect.task", "prefect.flow", "task", "flow"),
|
|
88
|
+
),
|
|
89
|
+
SemanticDecoratorSpec(
|
|
90
|
+
framework="zenml",
|
|
91
|
+
semantic_role="TASK_HANDLER",
|
|
92
|
+
patterns=("step", "pipeline"),
|
|
93
|
+
),
|
|
69
94
|
)
|
|
70
95
|
|
|
71
96
|
|
|
@@ -132,12 +157,12 @@ def analyze_symbol_decorators(
|
|
|
132
157
|
if "." in raw_expr:
|
|
133
158
|
base = raw_expr.split(".")[0]
|
|
134
159
|
# Verify base is imported or standard framework name
|
|
135
|
-
if base in import_map or base in ("app", "celery", "click", "django", "typer", "di", "inject"):
|
|
160
|
+
if base in import_map or base in ("app", "celery", "click", "django", "typer", "di", "inject", "ray", "langchain", "llama_index", "prefect", "zenml"):
|
|
136
161
|
grounded_name = raw_expr
|
|
137
162
|
matched_spec = spec
|
|
138
163
|
break
|
|
139
164
|
else:
|
|
140
|
-
# Bare name (e.g. shared_task, receiver, command, inject, provide, singleton)
|
|
165
|
+
# Bare name (e.g. shared_task, receiver, command, inject, provide, singleton, tool)
|
|
141
166
|
# Verify it was imported from the framework or known pattern
|
|
142
167
|
if raw_expr in import_map:
|
|
143
168
|
imp_mod, imp_orig = import_map[raw_expr]
|
|
@@ -146,7 +171,7 @@ def analyze_symbol_decorators(
|
|
|
146
171
|
grounded_name = f"{imp_mod}.{orig_or_raw}"
|
|
147
172
|
matched_spec = spec
|
|
148
173
|
break
|
|
149
|
-
elif raw_expr in ("inject", "provide", "singleton"):
|
|
174
|
+
elif raw_expr in ("inject", "provide", "singleton", "receiver", "tool", "function_tool", "remote", "step", "flow"):
|
|
150
175
|
grounded_name = raw_expr
|
|
151
176
|
matched_spec = spec
|
|
152
177
|
break
|
|
@@ -242,6 +267,12 @@ def analyze_symbol_decorators(
|
|
|
242
267
|
dyn_reason = "dynamic_event_name"
|
|
243
268
|
if event_names:
|
|
244
269
|
metadata["events"] = event_names
|
|
270
|
+
for kw in kw_nodes:
|
|
271
|
+
if kw.arg == "sender":
|
|
272
|
+
if isinstance(kw.value, (ast.Name, ast.Attribute)) and hasattr(ast, "unparse"):
|
|
273
|
+
metadata["sender"] = ast.unparse(kw.value)
|
|
274
|
+
elif isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
|
|
275
|
+
metadata["sender"] = kw.value.value
|
|
245
276
|
|
|
246
277
|
elif matched_spec.semantic_role == "COMMAND_HANDLER":
|
|
247
278
|
# Extract command name
|
codegraph/structural_diff.py
CHANGED
|
@@ -152,6 +152,8 @@ def _run_git(repository: Path, *args: str, timeout: int = 15) -> str | None:
|
|
|
152
152
|
cwd=str(repository),
|
|
153
153
|
capture_output=True,
|
|
154
154
|
text=True,
|
|
155
|
+
encoding="utf-8",
|
|
156
|
+
errors="replace",
|
|
155
157
|
timeout=timeout,
|
|
156
158
|
)
|
|
157
159
|
return res.stdout if res.returncode == 0 else None
|
codegraph/styles.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""CSS, SCSS, and CSS Modules Architecture and Selector Analyzer (Pillar 4).
|
|
2
|
+
|
|
3
|
+
Extracts deterministic facts from stylesheets:
|
|
4
|
+
- CSS class selectors (.btn-primary, .invoice-card, .header_title)
|
|
5
|
+
- CSS custom properties/variables (--primary-color, --spacing-md)
|
|
6
|
+
- CSS module boundaries
|
|
7
|
+
- Emits Symbol entities for style search and cross-component impact analysis.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from codegraph.indexing.models import Symbol
|
|
15
|
+
|
|
16
|
+
# CSS Class Selector pattern: .className { or .className:hover or .className.active
|
|
17
|
+
# Excludes decimals like .5s, .75em, .0
|
|
18
|
+
_CSS_CLASS_RE = re.compile(
|
|
19
|
+
r"""(?<![\w\.\-])\.([A-Za-z_][A-Za-z0-9_-]*)(?=[ \t\r\n\.:,\{>+~])"""
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# CSS Custom Property / Variable declaration: --primary-color: #0070f3;
|
|
23
|
+
_CSS_VAR_RE = re.compile(
|
|
24
|
+
r"""(--[A-Za-z_][A-Za-z0-9_-]*)\s*:"""
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Hex color detector to ignore #fff, #123456 as ID selectors
|
|
28
|
+
_HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class CssClassDef:
|
|
33
|
+
name: str
|
|
34
|
+
file_path: str
|
|
35
|
+
line: int
|
|
36
|
+
is_module: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class CssVariableDef:
|
|
41
|
+
name: str
|
|
42
|
+
file_path: str
|
|
43
|
+
line: int
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class StyleFileResult:
|
|
48
|
+
classes: tuple[CssClassDef, ...] = ()
|
|
49
|
+
variables: tuple[CssVariableDef, ...] = ()
|
|
50
|
+
symbols: tuple[Symbol, ...] = ()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_style_file(file_path: str) -> bool:
|
|
54
|
+
"""Check if file is a CSS, SCSS, Sass, or Less stylesheet."""
|
|
55
|
+
lower = file_path.lower()
|
|
56
|
+
return lower.endswith((".css", ".scss", ".sass", ".less"))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def parse_style_file(content: str, file_path: str) -> StyleFileResult:
|
|
60
|
+
"""Extract CSS classes, custom properties, and symbols from stylesheet."""
|
|
61
|
+
if not is_style_file(file_path):
|
|
62
|
+
return StyleFileResult()
|
|
63
|
+
|
|
64
|
+
is_module = ".module." in file_path.lower()
|
|
65
|
+
classes: list[CssClassDef] = []
|
|
66
|
+
variables: list[CssVariableDef] = []
|
|
67
|
+
symbols: list[Symbol] = []
|
|
68
|
+
|
|
69
|
+
seen_classes: set[str] = set()
|
|
70
|
+
seen_vars: set[str] = set()
|
|
71
|
+
|
|
72
|
+
clean_file = file_path.replace("\\", "/").lstrip("./")
|
|
73
|
+
|
|
74
|
+
for idx, line in enumerate(content.splitlines()):
|
|
75
|
+
lineno = idx + 1
|
|
76
|
+
stripped = line.strip()
|
|
77
|
+
|
|
78
|
+
# Skip comment-only lines
|
|
79
|
+
if stripped.startswith("/*") or stripped.startswith("*") or stripped.startswith("//"):
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Extract CSS variables
|
|
83
|
+
for m_var in _CSS_VAR_RE.finditer(line):
|
|
84
|
+
v_name = m_var.group(1)
|
|
85
|
+
if v_name not in seen_vars:
|
|
86
|
+
seen_vars.add(v_name)
|
|
87
|
+
variables.append(CssVariableDef(name=v_name, file_path=clean_file, line=lineno))
|
|
88
|
+
canon_id = f"{clean_file}::{v_name}"
|
|
89
|
+
symbols.append(
|
|
90
|
+
Symbol(
|
|
91
|
+
id=canon_id,
|
|
92
|
+
canonical_id=canon_id,
|
|
93
|
+
name=v_name,
|
|
94
|
+
qualified_name=v_name,
|
|
95
|
+
kind="style_var",
|
|
96
|
+
language="css",
|
|
97
|
+
module=clean_file,
|
|
98
|
+
path=clean_file,
|
|
99
|
+
file_path=clean_file,
|
|
100
|
+
scope="",
|
|
101
|
+
start_line=lineno,
|
|
102
|
+
end_line=lineno,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Extract CSS classes
|
|
107
|
+
for m_cls in _CSS_CLASS_RE.finditer(line):
|
|
108
|
+
c_name = m_cls.group(1)
|
|
109
|
+
# Avoid pseudo-elements and animations
|
|
110
|
+
if c_name in ("hover", "focus", "active", "visited", "disabled", "first-child", "last-child"):
|
|
111
|
+
continue
|
|
112
|
+
if c_name not in seen_classes:
|
|
113
|
+
seen_classes.add(c_name)
|
|
114
|
+
classes.append(
|
|
115
|
+
CssClassDef(
|
|
116
|
+
name=c_name,
|
|
117
|
+
file_path=clean_file,
|
|
118
|
+
line=lineno,
|
|
119
|
+
is_module=is_module,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
canon_id = f"{clean_file}::{c_name}"
|
|
123
|
+
symbols.append(
|
|
124
|
+
Symbol(
|
|
125
|
+
id=canon_id,
|
|
126
|
+
canonical_id=canon_id,
|
|
127
|
+
name=f".{c_name}",
|
|
128
|
+
qualified_name=f".{c_name}",
|
|
129
|
+
kind="style_class",
|
|
130
|
+
language="css",
|
|
131
|
+
module=clean_file,
|
|
132
|
+
path=clean_file,
|
|
133
|
+
file_path=clean_file,
|
|
134
|
+
scope="",
|
|
135
|
+
start_line=lineno,
|
|
136
|
+
end_line=lineno,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return StyleFileResult(
|
|
141
|
+
classes=tuple(classes),
|
|
142
|
+
variables=tuple(variables),
|
|
143
|
+
symbols=tuple(symbols),
|
|
144
|
+
)
|
codegraph/symbol_history.py
CHANGED
codegraph/tsconfig.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""TypeScript and JavaScript Path Alias Resolution (tsconfig.json / jsconfig.json).
|
|
2
|
+
|
|
3
|
+
Provides deterministic resolution of compilerOptions.paths and baseUrl across
|
|
4
|
+
monorepos, Next.js, Vite, and React projects without external dependencies.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import posixpath
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def strip_json_comments(text: str) -> str:
|
|
16
|
+
"""Strip single-line and multi-line comments and trailing commas from tsconfig/jsconfig JSON."""
|
|
17
|
+
# Remove block comments /* ... */
|
|
18
|
+
text = re.sub(r"/\*[\s\S]*?\*/", "", text)
|
|
19
|
+
# Remove line comments // ... (not inside quotes)
|
|
20
|
+
lines: list[str] = []
|
|
21
|
+
for line in text.splitlines():
|
|
22
|
+
# Match // not preceded by a quote mark
|
|
23
|
+
cleaned_line = re.sub(r'(?<![:"\'\\])//.*$', '', line)
|
|
24
|
+
lines.append(cleaned_line)
|
|
25
|
+
cleaned = "\n".join(lines)
|
|
26
|
+
# Remove trailing commas before } or ]
|
|
27
|
+
cleaned = re.sub(r",\s*([}\]])", r"\1", cleaned)
|
|
28
|
+
return cleaned
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class TsConfig:
|
|
33
|
+
config_path: str
|
|
34
|
+
base_dir: str
|
|
35
|
+
base_url: str
|
|
36
|
+
paths: dict[str, tuple[str, ...]] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TsConfigResolver:
|
|
40
|
+
"""Manages tsconfig.json and jsconfig.json path mappings across workspaces."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, configs: list[TsConfig] | None = None) -> None:
|
|
43
|
+
self.configs: list[TsConfig] = configs or []
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def load_from_repo(cls, repo_root: Path) -> TsConfigResolver:
|
|
47
|
+
"""Scan workspace for tsconfig.json, jsconfig.json, and vite.config.* files."""
|
|
48
|
+
configs: list[TsConfig] = []
|
|
49
|
+
candidates = (
|
|
50
|
+
list(repo_root.glob("**/tsconfig*.json"))
|
|
51
|
+
+ list(repo_root.glob("**/jsconfig*.json"))
|
|
52
|
+
+ list(repo_root.glob("**/vite.config.*"))
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
for c_path in candidates:
|
|
56
|
+
# Skip node_modules or build outputs
|
|
57
|
+
parts = c_path.parts
|
|
58
|
+
if any(p in ("node_modules", ".git", "dist", "build", ".next", ".cache") for p in parts):
|
|
59
|
+
continue
|
|
60
|
+
if c_path.name.startswith("vite.config."):
|
|
61
|
+
cfg = cls._parse_vite_config(c_path, repo_root)
|
|
62
|
+
else:
|
|
63
|
+
cfg = cls._parse_single_config(c_path, repo_root)
|
|
64
|
+
if cfg:
|
|
65
|
+
configs.append(cfg)
|
|
66
|
+
|
|
67
|
+
# Sort configs: deepest path first for nearest-config resolution
|
|
68
|
+
configs.sort(key=lambda c: len(c.base_dir), reverse=True)
|
|
69
|
+
return cls(configs)
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _parse_vite_config(file_path: Path, repo_root: Path) -> TsConfig | None:
|
|
73
|
+
try:
|
|
74
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
75
|
+
parsed_paths: dict[str, tuple[str, ...]] = {}
|
|
76
|
+
|
|
77
|
+
# 1. Object alias: alias: { '@': path.resolve(__dirname, './src'), ... }
|
|
78
|
+
alias_match = re.search(r"alias\s*:\s*\{([^}]+)\}", content)
|
|
79
|
+
if alias_match:
|
|
80
|
+
entries = re.findall(
|
|
81
|
+
r"""['"]([^'"]+)['"]\s*:\s*(?:(?:path\.)?resolve\([^)]*['"]([^'"]+)['"]\)|['"]([^'"]+)['"])""",
|
|
82
|
+
alias_match.group(1),
|
|
83
|
+
)
|
|
84
|
+
for key, target1, target2 in entries:
|
|
85
|
+
raw_target = (target1 or target2 or "").strip().replace("\\", "/").lstrip("./")
|
|
86
|
+
if raw_target:
|
|
87
|
+
clean_key = key.rstrip("/")
|
|
88
|
+
parsed_paths[f"{clean_key}/*"] = (f"{raw_target}/*",)
|
|
89
|
+
parsed_paths[clean_key] = (raw_target,)
|
|
90
|
+
|
|
91
|
+
# 2. Array alias: alias: [ { find: '@', replacement: path.resolve(__dirname, './src') } ]
|
|
92
|
+
array_entries = re.findall(
|
|
93
|
+
r"""find\s*:\s*['"]([^'"]+)['"][\s\S]*?replacement\s*:\s*(?:(?:path\.)?resolve\([^)]*['"]([^'"]+)['"]\)|['"]([^'"]+)['"])""",
|
|
94
|
+
content,
|
|
95
|
+
)
|
|
96
|
+
for key, target1, target2 in array_entries:
|
|
97
|
+
raw_target = (target1 or target2 or "").strip().replace("\\", "/").lstrip("./")
|
|
98
|
+
if raw_target:
|
|
99
|
+
clean_key = key.rstrip("/")
|
|
100
|
+
parsed_paths[f"{clean_key}/*"] = (f"{raw_target}/*",)
|
|
101
|
+
parsed_paths[clean_key] = (raw_target,)
|
|
102
|
+
|
|
103
|
+
if not parsed_paths:
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
base_dir_path = file_path.parent
|
|
107
|
+
rel_base_dir = posixpath.normpath(base_dir_path.relative_to(repo_root).as_posix())
|
|
108
|
+
if rel_base_dir == ".":
|
|
109
|
+
rel_base_dir = ""
|
|
110
|
+
|
|
111
|
+
return TsConfig(
|
|
112
|
+
config_path=posixpath.normpath(file_path.relative_to(repo_root).as_posix()),
|
|
113
|
+
base_dir=rel_base_dir,
|
|
114
|
+
base_url=".",
|
|
115
|
+
paths=parsed_paths,
|
|
116
|
+
)
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _parse_single_config(file_path: Path, repo_root: Path) -> TsConfig | None:
|
|
122
|
+
try:
|
|
123
|
+
raw = file_path.read_text(encoding="utf-8", errors="replace")
|
|
124
|
+
cleaned = strip_json_comments(raw)
|
|
125
|
+
data = json.loads(cleaned)
|
|
126
|
+
compiler_opts = data.get("compilerOptions", {})
|
|
127
|
+
|
|
128
|
+
base_dir_path = file_path.parent
|
|
129
|
+
rel_base_dir = posixpath.normpath(base_dir_path.relative_to(repo_root).as_posix())
|
|
130
|
+
if rel_base_dir == ".":
|
|
131
|
+
rel_base_dir = ""
|
|
132
|
+
|
|
133
|
+
base_url = compiler_opts.get("baseUrl", ".")
|
|
134
|
+
raw_paths = compiler_opts.get("paths", {})
|
|
135
|
+
|
|
136
|
+
parsed_paths: dict[str, tuple[str, ...]] = {}
|
|
137
|
+
for pattern, targets in raw_paths.items():
|
|
138
|
+
if isinstance(targets, list):
|
|
139
|
+
parsed_paths[pattern] = tuple(str(t) for t in targets)
|
|
140
|
+
elif isinstance(targets, str):
|
|
141
|
+
parsed_paths[pattern] = (targets,)
|
|
142
|
+
|
|
143
|
+
if not parsed_paths and base_url in (".", ""):
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
return TsConfig(
|
|
147
|
+
config_path=posixpath.normpath(file_path.relative_to(repo_root).as_posix()),
|
|
148
|
+
base_dir=rel_base_dir,
|
|
149
|
+
base_url=base_url,
|
|
150
|
+
paths=parsed_paths,
|
|
151
|
+
)
|
|
152
|
+
except Exception:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
def resolve_alias(
|
|
156
|
+
self,
|
|
157
|
+
imported_module: str,
|
|
158
|
+
source_file: str,
|
|
159
|
+
known_files: set[str],
|
|
160
|
+
) -> str | None:
|
|
161
|
+
"""Resolve an aliased module (e.g. '@/components/Button') to a file path in known_files."""
|
|
162
|
+
if not imported_module or imported_module.startswith("."):
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
clean_src = source_file.replace("\\", "/").lstrip("./")
|
|
166
|
+
|
|
167
|
+
# Pick matching config: deepest base_dir matching source_file, or fallback to root config
|
|
168
|
+
chosen_config: TsConfig | None = None
|
|
169
|
+
for cfg in self.configs:
|
|
170
|
+
if not cfg.base_dir or clean_src.startswith(f"{cfg.base_dir}/"):
|
|
171
|
+
chosen_config = cfg
|
|
172
|
+
break
|
|
173
|
+
if not chosen_config and self.configs:
|
|
174
|
+
chosen_config = self.configs[-1]
|
|
175
|
+
|
|
176
|
+
if not chosen_config:
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
# Check paths mappings
|
|
180
|
+
for pattern, targets in chosen_config.paths.items():
|
|
181
|
+
if "*" in pattern:
|
|
182
|
+
star_idx = pattern.index("*")
|
|
183
|
+
prefix = pattern[:star_idx]
|
|
184
|
+
suffix = pattern[star_idx + 1:]
|
|
185
|
+
if imported_module.startswith(prefix) and (not suffix or imported_module.endswith(suffix)):
|
|
186
|
+
wildcard = imported_module[len(prefix):len(imported_module) - len(suffix) if suffix else None]
|
|
187
|
+
for target_tmpl in targets:
|
|
188
|
+
target = target_tmpl.replace("*", wildcard)
|
|
189
|
+
resolved = self._test_target(
|
|
190
|
+
chosen_config.base_dir, chosen_config.base_url, target, known_files
|
|
191
|
+
)
|
|
192
|
+
if resolved:
|
|
193
|
+
return resolved
|
|
194
|
+
else:
|
|
195
|
+
# Exact match pattern (e.g. "@lib": ["src/lib"])
|
|
196
|
+
if imported_module == pattern:
|
|
197
|
+
for target in targets:
|
|
198
|
+
resolved = self._test_target(
|
|
199
|
+
chosen_config.base_dir, chosen_config.base_url, target, known_files
|
|
200
|
+
)
|
|
201
|
+
if resolved:
|
|
202
|
+
return resolved
|
|
203
|
+
|
|
204
|
+
# Fallback to baseUrl-relative imports if baseUrl is configured
|
|
205
|
+
if chosen_config.base_url and chosen_config.base_url not in (".", ""):
|
|
206
|
+
resolved = self._test_target(
|
|
207
|
+
chosen_config.base_dir, chosen_config.base_url, imported_module, known_files
|
|
208
|
+
)
|
|
209
|
+
if resolved:
|
|
210
|
+
return resolved
|
|
211
|
+
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def _test_target(
|
|
216
|
+
base_dir: str,
|
|
217
|
+
base_url: str,
|
|
218
|
+
target_path: str,
|
|
219
|
+
known_files: set[str],
|
|
220
|
+
) -> str | None:
|
|
221
|
+
"""Check candidate target path against known repository files with extensions and index files."""
|
|
222
|
+
# Join base_dir, base_url, and target_path in posix style
|
|
223
|
+
parts = [p for p in (base_dir, base_url, target_path) if p and p not in (".", "")]
|
|
224
|
+
base = posixpath.normpath("/".join(parts)).lstrip("./")
|
|
225
|
+
|
|
226
|
+
candidates = [base]
|
|
227
|
+
for ext in (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".css", ".scss"):
|
|
228
|
+
candidates.append(f"{base}{ext}")
|
|
229
|
+
for idx in ("index.ts", "index.tsx", "index.js", "index.jsx"):
|
|
230
|
+
candidates.append(posixpath.join(base, idx))
|
|
231
|
+
|
|
232
|
+
for cand in candidates:
|
|
233
|
+
cand_norm = cand.lstrip("./")
|
|
234
|
+
if cand_norm in known_files:
|
|
235
|
+
return cand_norm
|
|
236
|
+
return None
|