agentrepocoach 0.2.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,304 @@
1
+ """Rust language adapter.
2
+
3
+ Detects Rust repos via ``Cargo.toml`` presence. Walks ``src/`` for production
4
+ ``.rs`` files and scans for error-creation patterns. Rust uses ``Result<T, E>``
5
+ rather than exceptions — this adapter maps error-creation sites (``panic!``,
6
+ custom error types, ``anyhow!``, ``bail!``) to the ``ThrowSite`` model.
7
+
8
+ Declarations are detected via ``pub fn``, ``pub struct``, ``pub enum``, etc.
9
+ Doc comments use ``///`` (outer) or ``//!`` (inner) prefixes.
10
+
11
+ All analysis is regex-based — no AST parsing, stdlib only.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from pathlib import Path
17
+ from typing import Iterable
18
+
19
+ from .base import (
20
+ Declaration,
21
+ LanguageAdapter,
22
+ ThrowSite,
23
+ count_file_loc,
24
+ iter_source_files,
25
+ read_text_safely,
26
+ )
27
+
28
+ _RUST_SUFFIX: tuple[str, ...] = (".rs",)
29
+
30
+ # Error-creation patterns.
31
+ _PANIC_PATTERN = re.compile(r"\bpanic!\s*\(")
32
+ _ANYHOW_PATTERN = re.compile(r"\b(?:anyhow!|bail!)\s*\(")
33
+ _CUSTOM_ERROR_RETURN = re.compile(r"\bErr\s*\(\s*([A-Z][A-Za-z0-9_]*(?:::[A-Z]\w*)?)\s*[({]")
34
+
35
+ # Declaration patterns.
36
+ _PUB_DECL_PATTERN = re.compile(
37
+ r"^pub(?:\s*\(crate\))?\s+(?:async\s+)?(?:fn|struct|enum|trait|type|const|static|mod)\s+([A-Za-z_]\w*)"
38
+ )
39
+ _PRIVATE_DECL_PATTERN = re.compile(
40
+ r"^(?:async\s+)?(?:fn|struct|enum|trait|type|const|static)\s+([A-Za-z_]\w*)"
41
+ )
42
+
43
+ # Test function pattern.
44
+ _TEST_FN_PATTERN = re.compile(r"^\s*(?:async\s+)?fn\s+(test_\w+)\s*\(")
45
+ _TEST_ATTR_PATTERN = re.compile(r"^\s*#\[test\]")
46
+ # Rust test naming: test_something_does_something.
47
+ _TEST_NAMING_PATTERN = re.compile(r"^test_[a-z][a-z0-9_]*_[a-z0-9_]+$")
48
+
49
+ # Generic panic types.
50
+ _GENERIC_ERROR_NAMES: frozenset[str] = frozenset({
51
+ "panic!",
52
+ "anyhow!",
53
+ "bail!",
54
+ })
55
+
56
+ _FIX_HINT_KEYWORDS: tuple[str, ...] = (
57
+ "hint:", "fix:", "see ", "try ", "use ", "check ", "did you mean",
58
+ "suggested fix", "to fix", "to resolve", "example:", "ensure",
59
+ "expected", "must be", "should be",
60
+ )
61
+
62
+
63
+ class RustAdapter(LanguageAdapter):
64
+ """Rust adapter."""
65
+
66
+ name = "rust"
67
+
68
+ # ------------------------------------------------------------------
69
+ # Detection
70
+ # ------------------------------------------------------------------
71
+
72
+ def detect(self, repo_path: Path) -> float:
73
+ if (repo_path / "Cargo.toml").is_file():
74
+ return 1.0
75
+ if any(repo_path.rglob("*.rs")):
76
+ return 0.5
77
+ return 0.0
78
+
79
+ # ------------------------------------------------------------------
80
+ # File discovery
81
+ # ------------------------------------------------------------------
82
+
83
+ def find_production_files(self, repo_path: Path) -> list[Path]:
84
+ src_dir = repo_path / "src"
85
+ if src_dir.is_dir():
86
+ return iter_source_files(src_dir, suffixes=_RUST_SUFFIX)
87
+ return iter_source_files(repo_path, suffixes=_RUST_SUFFIX)
88
+
89
+ def find_test_files(self, repo_path: Path) -> list[Path]:
90
+ results: list[Path] = []
91
+ tests_dir = repo_path / "tests"
92
+ if tests_dir.is_dir():
93
+ results.extend(iter_source_files(tests_dir, suffixes=_RUST_SUFFIX))
94
+ # In Rust, test modules are often inline (#[cfg(test)]) in prod files.
95
+ # We also look for dedicated test files in src/.
96
+ return results
97
+
98
+ def find_production_modules(self, repo_path: Path) -> list[str]:
99
+ """Return crate name from Cargo.toml and top-level module names."""
100
+ modules: set[str] = set()
101
+ cargo = repo_path / "Cargo.toml"
102
+ if cargo.is_file():
103
+ text = read_text_safely(cargo)
104
+ m = re.search(r'name\s*=\s*"([^"]+)"', text)
105
+ if m:
106
+ modules.add(m.group(1))
107
+ src_dir = repo_path / "src"
108
+ if src_dir.is_dir():
109
+ for entry in src_dir.iterdir():
110
+ if entry.is_dir() and any(entry.rglob("*.rs")):
111
+ modules.add(entry.name)
112
+ elif entry.is_file() and entry.suffix == ".rs" and entry.name not in ("main.rs", "lib.rs"):
113
+ modules.add(entry.stem)
114
+ return sorted(modules)
115
+
116
+ # ------------------------------------------------------------------
117
+ # Throw-site analysis (Rust: panic!, bail!, Err(CustomError))
118
+ # ------------------------------------------------------------------
119
+
120
+ def scan_throw_sites(
121
+ self,
122
+ files: Iterable[Path],
123
+ hint_marker: str,
124
+ domain_exception_types: set[str],
125
+ ) -> list[ThrowSite]:
126
+ sites: list[ThrowSite] = []
127
+ for path in files:
128
+ text = read_text_safely(path)
129
+ if not text:
130
+ continue
131
+
132
+ # panic!() sites
133
+ for match in _PANIC_PATTERN.finditer(text):
134
+ context = _extract_context(text, match.start())
135
+ line_no = text.count("\n", 0, match.start()) + 1
136
+ sites.append(
137
+ ThrowSite(
138
+ file=path,
139
+ line=line_no,
140
+ exception_type="panic!",
141
+ has_fix_hint=_has_fix_hint(context, hint_marker),
142
+ is_user_defined=False,
143
+ is_generic=True,
144
+ )
145
+ )
146
+
147
+ # anyhow!/bail! sites
148
+ for match in _ANYHOW_PATTERN.finditer(text):
149
+ context = _extract_context(text, match.start())
150
+ line_no = text.count("\n", 0, match.start()) + 1
151
+ err_type = "anyhow!" if "anyhow!" in match.group() else "bail!"
152
+ sites.append(
153
+ ThrowSite(
154
+ file=path,
155
+ line=line_no,
156
+ exception_type=err_type,
157
+ has_fix_hint=_has_fix_hint(context, hint_marker),
158
+ is_user_defined=False,
159
+ is_generic=True,
160
+ )
161
+ )
162
+
163
+ # Err(CustomError{}) or Err(CustomError(...))
164
+ for match in _CUSTOM_ERROR_RETURN.finditer(text):
165
+ error_type = match.group(1)
166
+ context = _extract_context(text, match.start())
167
+ line_no = text.count("\n", 0, match.start()) + 1
168
+ sites.append(
169
+ ThrowSite(
170
+ file=path,
171
+ line=line_no,
172
+ exception_type=error_type,
173
+ has_fix_hint=_has_fix_hint(context, hint_marker),
174
+ is_user_defined=error_type in domain_exception_types,
175
+ is_generic=False,
176
+ )
177
+ )
178
+ return sites
179
+
180
+ def generic_exception_names(self) -> set[str]:
181
+ return set(_GENERIC_ERROR_NAMES)
182
+
183
+ # ------------------------------------------------------------------
184
+ # Declarations
185
+ # ------------------------------------------------------------------
186
+
187
+ def scan_declarations(self, files: Iterable[Path]) -> list[Declaration]:
188
+ declarations: list[Declaration] = []
189
+ for path in files:
190
+ text = read_text_safely(path)
191
+ if not text:
192
+ continue
193
+ declarations.extend(self._scan_declarations_in_text(path, text))
194
+ return declarations
195
+
196
+ def _scan_declarations_in_text(self, path: Path, text: str) -> list[Declaration]:
197
+ lines = text.splitlines()
198
+ results: list[Declaration] = []
199
+ for i, line in enumerate(lines):
200
+ # Try pub declarations first.
201
+ m = _PUB_DECL_PATTERN.match(line)
202
+ if m:
203
+ visibility = "internal" if "pub(crate)" in line else "public"
204
+ results.append(
205
+ Declaration(
206
+ file=path,
207
+ line=i + 1,
208
+ name=m.group(1),
209
+ visibility=visibility,
210
+ has_doc_comment=_has_preceding_doc_comment(lines, i),
211
+ )
212
+ )
213
+ continue
214
+ # Try private (no pub) declarations — only at column 0.
215
+ if not line.startswith(" ") and not line.startswith("\t"):
216
+ m = _PRIVATE_DECL_PATTERN.match(line)
217
+ if m:
218
+ results.append(
219
+ Declaration(
220
+ file=path,
221
+ line=i + 1,
222
+ name=m.group(1),
223
+ visibility="private",
224
+ has_doc_comment=_has_preceding_doc_comment(lines, i),
225
+ )
226
+ )
227
+ return results
228
+
229
+ # ------------------------------------------------------------------
230
+ # Test methods
231
+ # ------------------------------------------------------------------
232
+
233
+ def find_test_methods(self, files: Iterable[Path]) -> list[tuple[Path, str]]:
234
+ """Find test functions in both dedicated test files and inline #[test] modules."""
235
+ results: list[tuple[Path, str]] = []
236
+ all_files = list(files)
237
+ # Also scan production files for inline tests.
238
+ for path in all_files:
239
+ text = read_text_safely(path)
240
+ if not text:
241
+ continue
242
+ lines = text.splitlines()
243
+ for i, line in enumerate(lines):
244
+ m = _TEST_FN_PATTERN.match(line)
245
+ if m:
246
+ results.append((path, m.group(1)))
247
+ continue
248
+ # Also detect functions preceded by #[test] attribute.
249
+ if _TEST_ATTR_PATTERN.match(line):
250
+ # Next non-blank, non-attribute line should be the fn.
251
+ for j in range(i + 1, min(i + 5, len(lines))):
252
+ fn_match = re.match(r"^\s*(?:async\s+)?fn\s+(\w+)\s*\(", lines[j])
253
+ if fn_match:
254
+ results.append((path, fn_match.group(1)))
255
+ break
256
+ return results
257
+
258
+ def test_naming_pattern(self) -> re.Pattern[str]:
259
+ return _TEST_NAMING_PATTERN
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Helpers
264
+ # ---------------------------------------------------------------------------
265
+
266
+
267
+ def _extract_context(text: str, start: int, max_chars: int = 500) -> str:
268
+ """Extract text from start through the next closing paren/brace."""
269
+ end = min(start + max_chars, len(text))
270
+ depth = 0
271
+ for i in range(start, end):
272
+ ch = text[i]
273
+ if ch in ("(", "{"):
274
+ depth += 1
275
+ elif ch in (")", "}"):
276
+ depth -= 1
277
+ if depth == 0:
278
+ return text[start:i + 1]
279
+ return text[start:end]
280
+
281
+
282
+ def _has_fix_hint(text: str, hint_marker: str) -> bool:
283
+ lower = text.lower()
284
+ if hint_marker and hint_marker.lower() in lower:
285
+ return True
286
+ for keyword in _FIX_HINT_KEYWORDS:
287
+ if keyword in lower:
288
+ return True
289
+ return False
290
+
291
+
292
+ def _has_preceding_doc_comment(lines: list[str], index: int) -> bool:
293
+ """Return True if the line above is a Rust doc comment (``///`` or ``//!``)."""
294
+ j = index - 1
295
+ while j >= 0:
296
+ stripped = lines[j].strip()
297
+ if stripped == "":
298
+ j -= 1
299
+ continue
300
+ return stripped.startswith("///") or stripped.startswith("//!")
301
+ return False
302
+
303
+
304
+ RustAdapter.count_file_loc = staticmethod(count_file_loc) # type: ignore[attr-defined]
@@ -0,0 +1,351 @@
1
+ """TypeScript language adapter.
2
+
3
+ Detects TypeScript repos via ``tsconfig.json`` / ``package.json`` presence.
4
+ Walks ``src/`` for production files and ``tests/`` / ``test/`` / ``__tests__/``
5
+ for test files. Scans ``throw new`` statements, classifies error types, and
6
+ detects JSDoc presence above top-level declarations.
7
+
8
+ All analysis is regex-based against file text — no AST parsing, no runtime
9
+ dependencies beyond the Python 3.11+ standard library.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ from pathlib import Path
16
+ from typing import Iterable
17
+
18
+ from .base import (
19
+ Declaration,
20
+ LanguageAdapter,
21
+ ThrowSite,
22
+ count_file_loc,
23
+ iter_source_files,
24
+ read_text_safely,
25
+ )
26
+
27
+ _TS_SUFFIX: tuple[str, ...] = (".ts", ".tsx")
28
+ _TS_EXCLUDE_SUFFIXES: tuple[str, ...] = (".d.ts",)
29
+ _TS_TEST_SUFFIXES: tuple[str, ...] = (".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx")
30
+
31
+ # Throw-site scanning.
32
+ _THROW_PATTERN = re.compile(r"\bthrow\s+new\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(")
33
+
34
+ # Declaration scanning — exported (public).
35
+ _EXPORT_CLASS_PATTERN = re.compile(
36
+ r"^export\s+(?:default\s+|abstract\s+)*(?:class|interface)\s+([A-Za-z_]\w*)"
37
+ )
38
+ _EXPORT_FUNCTION_PATTERN = re.compile(
39
+ r"^export\s+(?:default\s+|async\s+)*function\s+([A-Za-z_]\w*)"
40
+ )
41
+ _EXPORT_ENUM_PATTERN = re.compile(
42
+ r"^export\s+(?:const\s+)?enum\s+([A-Za-z_]\w*)"
43
+ )
44
+ _EXPORT_TYPE_PATTERN = re.compile(
45
+ r"^export\s+type\s+([A-Za-z_]\w*)\s*[=<{]"
46
+ )
47
+ _EXPORT_CONST_PATTERN = re.compile(
48
+ r"^export\s+const\s+([A-Za-z_]\w*)\s*[=:]"
49
+ )
50
+
51
+ # Non-exported (internal) declarations.
52
+ _INTERNAL_CLASS_PATTERN = re.compile(
53
+ r"^(?:abstract\s+)?class\s+([A-Za-z_]\w*)"
54
+ )
55
+ _INTERNAL_FUNCTION_PATTERN = re.compile(
56
+ r"^(?:async\s+)?function\s+([A-Za-z_]\w*)"
57
+ )
58
+ _INTERNAL_CONST_PATTERN = re.compile(
59
+ r"^const\s+([A-Za-z_]\w*)\s*[=:]"
60
+ )
61
+
62
+ # Test method patterns (Jest/Vitest/Mocha).
63
+ _TEST_IT_PATTERN = re.compile(r"""\bit\s*\(\s*(['"`])(.+?)\1""")
64
+ _TEST_TEST_PATTERN = re.compile(r"""\btest\s*\(\s*(['"`])(.+?)\1""")
65
+
66
+ # Test naming: descriptive string — we accept anything with at least 3 words.
67
+ _TEST_DESCRIPTIVE_PATTERN = re.compile(r"^.+\s.+\s.+")
68
+
69
+ # Language-stdlib error types considered "too generic" for good agent UX.
70
+ _GENERIC_EXCEPTION_NAMES: frozenset[str] = frozenset({
71
+ "Error",
72
+ "TypeError",
73
+ "RangeError",
74
+ "ReferenceError",
75
+ "SyntaxError",
76
+ })
77
+
78
+ _FIX_HINT_KEYWORDS: tuple[str, ...] = (
79
+ "hint:", "fix:", "see ", "try ", "use ", "check ", "did you mean",
80
+ "suggested fix", "to fix", "to resolve", "example:", "ensure",
81
+ "install", "provide", "verify", "configure",
82
+ )
83
+
84
+ _PROD_DIR_CANDIDATES: tuple[str, ...] = ("src", "lib")
85
+ _TEST_DIR_CANDIDATES: tuple[str, ...] = ("tests", "test", "__tests__")
86
+
87
+
88
+ class TypeScriptAdapter(LanguageAdapter):
89
+ """TypeScript / JavaScript adapter."""
90
+
91
+ name = "typescript"
92
+
93
+ # ------------------------------------------------------------------
94
+ # Detection
95
+ # ------------------------------------------------------------------
96
+
97
+ def detect(self, repo_path: Path) -> float:
98
+ if (repo_path / "tsconfig.json").is_file():
99
+ return 1.0
100
+ pkg_json = repo_path / "package.json"
101
+ if pkg_json.is_file():
102
+ try:
103
+ data = json.loads(read_text_safely(pkg_json))
104
+ deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
105
+ if "typescript" in deps:
106
+ return 0.8
107
+ except (json.JSONDecodeError, TypeError):
108
+ pass
109
+ return 0.3
110
+ return 0.0
111
+
112
+ # ------------------------------------------------------------------
113
+ # File discovery
114
+ # ------------------------------------------------------------------
115
+
116
+ def find_production_files(self, repo_path: Path) -> list[Path]:
117
+ production_roots = self._production_roots(repo_path)
118
+ results: list[Path] = []
119
+ for root in production_roots:
120
+ for f in iter_source_files(
121
+ root,
122
+ suffixes=_TS_SUFFIX,
123
+ exclude_suffixes=_TS_EXCLUDE_SUFFIXES,
124
+ ):
125
+ if not _is_test_file(f):
126
+ results.append(f)
127
+ return results
128
+
129
+ def find_test_files(self, repo_path: Path) -> list[Path]:
130
+ results: list[Path] = []
131
+ # Conventional test directories.
132
+ for name in _TEST_DIR_CANDIDATES:
133
+ candidate = repo_path / name
134
+ if candidate.is_dir():
135
+ results.extend(
136
+ iter_source_files(candidate, suffixes=_TS_SUFFIX, exclude_suffixes=_TS_EXCLUDE_SUFFIXES)
137
+ )
138
+ # Also pick up co-located test files in production directories.
139
+ for root in self._production_roots(repo_path):
140
+ for f in iter_source_files(root, suffixes=_TS_SUFFIX, exclude_suffixes=_TS_EXCLUDE_SUFFIXES):
141
+ if _is_test_file(f) and f not in results:
142
+ results.append(f)
143
+ return results
144
+
145
+ def find_production_modules(self, repo_path: Path) -> list[str]:
146
+ """Top-level directories under src/ that contain .ts files."""
147
+ modules: set[str] = set()
148
+ for root in self._production_roots(repo_path):
149
+ for entry in root.iterdir():
150
+ if entry.is_dir() and any(entry.rglob("*.ts")):
151
+ modules.add(entry.name)
152
+ # If src/ has direct .ts files but no subdirectories, use the root name.
153
+ if not modules:
154
+ ts_files = list(root.glob("*.ts"))
155
+ if ts_files:
156
+ modules.add(root.name)
157
+ return sorted(modules)
158
+
159
+ def _production_roots(self, repo_path: Path) -> list[Path]:
160
+ roots: list[Path] = []
161
+ for name in _PROD_DIR_CANDIDATES:
162
+ candidate = repo_path / name
163
+ if candidate.is_dir():
164
+ roots.append(candidate)
165
+ return roots
166
+
167
+ # ------------------------------------------------------------------
168
+ # Throw-site analysis
169
+ # ------------------------------------------------------------------
170
+
171
+ def scan_throw_sites(
172
+ self,
173
+ files: Iterable[Path],
174
+ hint_marker: str,
175
+ domain_exception_types: set[str],
176
+ ) -> list[ThrowSite]:
177
+ sites: list[ThrowSite] = []
178
+ for path in files:
179
+ text = read_text_safely(path)
180
+ if not text:
181
+ continue
182
+ for match in _THROW_PATTERN.finditer(text):
183
+ exception_type = match.group(1)
184
+ line_no = text.count("\n", 0, match.start()) + 1
185
+ # Extract text from throw to closing paren/semicolon for
186
+ # fix-hint detection (message often spans multiple lines).
187
+ context_text = _extract_throw_context(text, match.start())
188
+ sites.append(
189
+ ThrowSite(
190
+ file=path,
191
+ line=line_no,
192
+ exception_type=exception_type,
193
+ has_fix_hint=_has_fix_hint(context_text, hint_marker),
194
+ is_user_defined=exception_type in domain_exception_types,
195
+ is_generic=exception_type in _GENERIC_EXCEPTION_NAMES,
196
+ )
197
+ )
198
+ return sites
199
+
200
+ def generic_exception_names(self) -> set[str]:
201
+ return set(_GENERIC_EXCEPTION_NAMES)
202
+
203
+ # ------------------------------------------------------------------
204
+ # Declarations
205
+ # ------------------------------------------------------------------
206
+
207
+ def scan_declarations(self, files: Iterable[Path]) -> list[Declaration]:
208
+ declarations: list[Declaration] = []
209
+ for path in files:
210
+ text = read_text_safely(path)
211
+ if not text:
212
+ continue
213
+ declarations.extend(self._scan_declarations_in_text(path, text))
214
+ return declarations
215
+
216
+ def _scan_declarations_in_text(self, path: Path, text: str) -> list[Declaration]:
217
+ lines = text.splitlines()
218
+ results: list[Declaration] = []
219
+ for i, line in enumerate(lines):
220
+ stripped = line.strip()
221
+
222
+ # Try exported (public) patterns first.
223
+ name = _match_exported(stripped)
224
+ if name is not None:
225
+ results.append(
226
+ Declaration(
227
+ file=path,
228
+ line=i + 1,
229
+ name=name,
230
+ visibility="public",
231
+ has_doc_comment=_has_preceding_jsdoc(lines, i),
232
+ )
233
+ )
234
+ continue
235
+
236
+ # Try non-exported (internal) patterns — only at column 0.
237
+ if line and not line[0].isspace():
238
+ name = _match_internal(stripped)
239
+ if name is not None:
240
+ results.append(
241
+ Declaration(
242
+ file=path,
243
+ line=i + 1,
244
+ name=name,
245
+ visibility="internal",
246
+ has_doc_comment=_has_preceding_jsdoc(lines, i),
247
+ )
248
+ )
249
+ return results
250
+
251
+ # ------------------------------------------------------------------
252
+ # Test methods
253
+ # ------------------------------------------------------------------
254
+
255
+ def find_test_methods(self, files: Iterable[Path]) -> list[tuple[Path, str]]:
256
+ results: list[tuple[Path, str]] = []
257
+ for path in files:
258
+ text = read_text_safely(path)
259
+ if not text:
260
+ continue
261
+ for match in _TEST_IT_PATTERN.finditer(text):
262
+ results.append((path, match.group(2)))
263
+ for match in _TEST_TEST_PATTERN.finditer(text):
264
+ results.append((path, match.group(2)))
265
+ return results
266
+
267
+ def test_naming_pattern(self) -> re.Pattern[str]:
268
+ return _TEST_DESCRIPTIVE_PATTERN
269
+
270
+
271
+ # ---------------------------------------------------------------------------
272
+ # Helpers
273
+ # ---------------------------------------------------------------------------
274
+
275
+
276
+ def _extract_throw_context(text: str, start: int, max_chars: int = 500) -> str:
277
+ """Extract text from a throw statement through the closing paren.
278
+
279
+ Walks forward from ``start`` up to ``max_chars`` or the matching close
280
+ paren, whichever comes first. This captures multi-line throw messages.
281
+ """
282
+ end = min(start + max_chars, len(text))
283
+ depth = 0
284
+ for i in range(start, end):
285
+ if text[i] == "(":
286
+ depth += 1
287
+ elif text[i] == ")":
288
+ depth -= 1
289
+ if depth == 0:
290
+ return text[start:i + 1]
291
+ return text[start:end]
292
+
293
+
294
+ def _is_test_file(path: Path) -> bool:
295
+ name = path.name
296
+ return any(name.endswith(sfx) for sfx in _TS_TEST_SUFFIXES)
297
+
298
+
299
+ def _has_fix_hint(text: str, hint_marker: str) -> bool:
300
+ lower = text.lower()
301
+ if hint_marker and hint_marker.lower() in lower:
302
+ return True
303
+ for keyword in _FIX_HINT_KEYWORDS:
304
+ if keyword in lower:
305
+ return True
306
+ return False
307
+
308
+
309
+ def _match_exported(line: str) -> str | None:
310
+ """Return the declaration name if line is an export declaration, else None."""
311
+ for pattern in (
312
+ _EXPORT_CLASS_PATTERN,
313
+ _EXPORT_FUNCTION_PATTERN,
314
+ _EXPORT_ENUM_PATTERN,
315
+ _EXPORT_TYPE_PATTERN,
316
+ _EXPORT_CONST_PATTERN,
317
+ ):
318
+ m = pattern.match(line)
319
+ if m:
320
+ return m.group(1)
321
+ return None
322
+
323
+
324
+ def _match_internal(line: str) -> str | None:
325
+ """Return the declaration name if line is a non-exported top-level declaration."""
326
+ for pattern in (
327
+ _INTERNAL_CLASS_PATTERN,
328
+ _INTERNAL_FUNCTION_PATTERN,
329
+ _INTERNAL_CONST_PATTERN,
330
+ ):
331
+ m = pattern.match(line)
332
+ if m:
333
+ return m.group(1)
334
+ return None
335
+
336
+
337
+ def _has_preceding_jsdoc(lines: list[str], index: int) -> bool:
338
+ """Walk backwards from ``index``. Return True if the first non-blank line
339
+ above ends with ``*/`` (closing a JSDoc block).
340
+ """
341
+ j = index - 1
342
+ while j >= 0:
343
+ stripped = lines[j].strip()
344
+ if stripped == "":
345
+ j -= 1
346
+ continue
347
+ return stripped.endswith("*/")
348
+ return False
349
+
350
+
351
+ TypeScriptAdapter.count_file_loc = staticmethod(count_file_loc) # type: ignore[attr-defined]