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,283 @@
1
+ """Go language adapter.
2
+
3
+ Detects Go repos via ``go.mod`` presence. Walks the repo for production
4
+ ``.go`` files (excluding ``_test.go``), scans ``return fmt.Errorf(`` /
5
+ ``errors.New(`` / custom error returns, and detects Go doc comments above
6
+ exported declarations.
7
+
8
+ Go's error model differs from throw-based languages: errors are returned
9
+ values, not thrown exceptions. This adapter maps Go error-creation sites
10
+ (``fmt.Errorf``, ``errors.New``, ``&CustomError{}``) to the ``ThrowSite``
11
+ model. The "exception type" is the error constructor or custom type name.
12
+
13
+ All analysis is regex-based — no AST parsing, stdlib only.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from pathlib import Path
19
+ from typing import Iterable
20
+
21
+ from .base import (
22
+ Declaration,
23
+ LanguageAdapter,
24
+ ThrowSite,
25
+ count_file_loc,
26
+ iter_source_files,
27
+ read_text_safely,
28
+ )
29
+
30
+ _GO_SUFFIX: tuple[str, ...] = (".go",)
31
+ _GO_TEST_SUFFIX: tuple[str, ...] = ("_test.go",)
32
+
33
+ # Error-creation patterns (Go returns errors, doesn't throw them).
34
+ _ERRORS_NEW_PATTERN = re.compile(r"\berrors\.New\s*\(")
35
+ _FMT_ERRORF_PATTERN = re.compile(r"\bfmt\.Errorf\s*\(")
36
+ _CUSTOM_ERROR_PATTERN = re.compile(r"&([A-Z][A-Za-z0-9_]*Error)\s*\{")
37
+
38
+ # Declaration patterns — Go uses capitalization for visibility.
39
+ _FUNC_PATTERN = re.compile(r"^func\s+(?:\([^)]+\)\s+)?([A-Za-z_]\w*)\s*\(")
40
+ _TYPE_PATTERN = re.compile(r"^type\s+([A-Za-z_]\w*)\s+(?:struct|interface|int|string)")
41
+ _CONST_VAR_PATTERN = re.compile(r"^(?:var|const)\s+([A-Za-z_]\w*)\s")
42
+
43
+ # Test function pattern.
44
+ _TEST_FUNC_PATTERN = re.compile(r"^func\s+(Test[A-Z]\w*)\s*\(")
45
+ # Go convention: TestFoo_Bar_Baz or TestFooBar.
46
+ _TEST_NAMING_PATTERN = re.compile(r"^Test[A-Z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)+$")
47
+
48
+ # Generic error constructors (too generic for good agent UX).
49
+ _GENERIC_ERROR_NAMES: frozenset[str] = frozenset({
50
+ "errors.New",
51
+ "fmt.Errorf",
52
+ })
53
+
54
+ _FIX_HINT_KEYWORDS: tuple[str, ...] = (
55
+ "hint:", "fix:", "see ", "try ", "use ", "check ", "did you mean",
56
+ "suggested fix", "to fix", "to resolve", "example:", "ensure",
57
+ "install", "provide", "verify", "configure", "expected",
58
+ )
59
+
60
+ _TEST_DIR_CANDIDATES: tuple[str, ...] = ("test", "tests", "internal/test")
61
+
62
+
63
+ class GoAdapter(LanguageAdapter):
64
+ """Go adapter."""
65
+
66
+ name = "go"
67
+
68
+ # ------------------------------------------------------------------
69
+ # Detection
70
+ # ------------------------------------------------------------------
71
+
72
+ def detect(self, repo_path: Path) -> float:
73
+ if (repo_path / "go.mod").is_file():
74
+ return 1.0
75
+ if any(repo_path.rglob("*.go")):
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
+ all_go = iter_source_files(repo_path, suffixes=_GO_SUFFIX)
85
+ return [f for f in all_go if not _is_test_file(f)]
86
+
87
+ def find_test_files(self, repo_path: Path) -> list[Path]:
88
+ all_go = iter_source_files(repo_path, suffixes=_GO_SUFFIX)
89
+ return [f for f in all_go if _is_test_file(f)]
90
+
91
+ def find_production_modules(self, repo_path: Path) -> list[str]:
92
+ """Return Go package directory names containing production .go files."""
93
+ modules: set[str] = set()
94
+ for f in self.find_production_files(repo_path):
95
+ # Use the parent directory name as the module/package name.
96
+ pkg = f.parent.name
97
+ if pkg and pkg != repo_path.name:
98
+ modules.add(pkg)
99
+ # If all files are at repo root, use the module name from go.mod.
100
+ if not modules:
101
+ mod_file = repo_path / "go.mod"
102
+ if mod_file.is_file():
103
+ text = read_text_safely(mod_file)
104
+ m = re.search(r"^module\s+(\S+)", text, re.MULTILINE)
105
+ if m:
106
+ modules.add(m.group(1).rsplit("/", 1)[-1])
107
+ return sorted(modules)
108
+
109
+ # ------------------------------------------------------------------
110
+ # Throw-site analysis (Go: error-creation sites)
111
+ # ------------------------------------------------------------------
112
+
113
+ def scan_throw_sites(
114
+ self,
115
+ files: Iterable[Path],
116
+ hint_marker: str,
117
+ domain_exception_types: set[str],
118
+ ) -> list[ThrowSite]:
119
+ sites: list[ThrowSite] = []
120
+ for path in files:
121
+ text = read_text_safely(path)
122
+ if not text:
123
+ continue
124
+
125
+ # errors.New() sites
126
+ for match in _ERRORS_NEW_PATTERN.finditer(text):
127
+ context = _extract_context(text, match.start())
128
+ line_no = text.count("\n", 0, match.start()) + 1
129
+ sites.append(
130
+ ThrowSite(
131
+ file=path,
132
+ line=line_no,
133
+ exception_type="errors.New",
134
+ has_fix_hint=_has_fix_hint(context, hint_marker),
135
+ is_user_defined=False,
136
+ is_generic=True,
137
+ )
138
+ )
139
+
140
+ # fmt.Errorf() sites
141
+ for match in _FMT_ERRORF_PATTERN.finditer(text):
142
+ context = _extract_context(text, match.start())
143
+ line_no = text.count("\n", 0, match.start()) + 1
144
+ sites.append(
145
+ ThrowSite(
146
+ file=path,
147
+ line=line_no,
148
+ exception_type="fmt.Errorf",
149
+ has_fix_hint=_has_fix_hint(context, hint_marker),
150
+ is_user_defined=False,
151
+ is_generic=True,
152
+ )
153
+ )
154
+
155
+ # Custom error types: &CustomError{}
156
+ for match in _CUSTOM_ERROR_PATTERN.finditer(text):
157
+ error_type = match.group(1)
158
+ context = _extract_context(text, match.start())
159
+ line_no = text.count("\n", 0, match.start()) + 1
160
+ sites.append(
161
+ ThrowSite(
162
+ file=path,
163
+ line=line_no,
164
+ exception_type=error_type,
165
+ has_fix_hint=_has_fix_hint(context, hint_marker),
166
+ is_user_defined=error_type in domain_exception_types,
167
+ is_generic=False,
168
+ )
169
+ )
170
+ return sites
171
+
172
+ def generic_exception_names(self) -> set[str]:
173
+ return set(_GENERIC_ERROR_NAMES)
174
+
175
+ # ------------------------------------------------------------------
176
+ # Declarations
177
+ # ------------------------------------------------------------------
178
+
179
+ def scan_declarations(self, files: Iterable[Path]) -> list[Declaration]:
180
+ declarations: list[Declaration] = []
181
+ for path in files:
182
+ text = read_text_safely(path)
183
+ if not text:
184
+ continue
185
+ declarations.extend(self._scan_declarations_in_text(path, text))
186
+ return declarations
187
+
188
+ def _scan_declarations_in_text(self, path: Path, text: str) -> list[Declaration]:
189
+ lines = text.splitlines()
190
+ results: list[Declaration] = []
191
+ for i, line in enumerate(lines):
192
+ name = _match_declaration(line)
193
+ if name is None:
194
+ continue
195
+ # Go visibility: uppercase first letter = exported (public).
196
+ visibility = "public" if name[0].isupper() else "internal"
197
+ results.append(
198
+ Declaration(
199
+ file=path,
200
+ line=i + 1,
201
+ name=name,
202
+ visibility=visibility,
203
+ has_doc_comment=_has_preceding_go_doc(lines, i),
204
+ )
205
+ )
206
+ return results
207
+
208
+ # ------------------------------------------------------------------
209
+ # Test methods
210
+ # ------------------------------------------------------------------
211
+
212
+ def find_test_methods(self, files: Iterable[Path]) -> list[tuple[Path, str]]:
213
+ results: list[tuple[Path, str]] = []
214
+ for path in files:
215
+ text = read_text_safely(path)
216
+ if not text:
217
+ continue
218
+ for line in text.splitlines():
219
+ m = _TEST_FUNC_PATTERN.match(line)
220
+ if m:
221
+ results.append((path, m.group(1)))
222
+ return results
223
+
224
+ def test_naming_pattern(self) -> re.Pattern[str]:
225
+ return _TEST_NAMING_PATTERN
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Helpers
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ def _is_test_file(path: Path) -> bool:
234
+ return path.name.endswith("_test.go")
235
+
236
+
237
+ def _extract_context(text: str, start: int, max_chars: int = 500) -> str:
238
+ """Extract text from start through the next closing paren or brace."""
239
+ end = min(start + max_chars, len(text))
240
+ depth = 0
241
+ for i in range(start, end):
242
+ ch = text[i]
243
+ if ch in ("(", "{"):
244
+ depth += 1
245
+ elif ch in (")", "}"):
246
+ depth -= 1
247
+ if depth == 0:
248
+ return text[start:i + 1]
249
+ return text[start:end]
250
+
251
+
252
+ def _has_fix_hint(text: str, hint_marker: str) -> bool:
253
+ lower = text.lower()
254
+ if hint_marker and hint_marker.lower() in lower:
255
+ return True
256
+ for keyword in _FIX_HINT_KEYWORDS:
257
+ if keyword in lower:
258
+ return True
259
+ return False
260
+
261
+
262
+ def _match_declaration(line: str) -> str | None:
263
+ """Return the declaration name if the line declares a func, type, or var/const."""
264
+ for pattern in (_FUNC_PATTERN, _TYPE_PATTERN, _CONST_VAR_PATTERN):
265
+ m = pattern.match(line)
266
+ if m:
267
+ return m.group(1)
268
+ return None
269
+
270
+
271
+ def _has_preceding_go_doc(lines: list[str], index: int) -> bool:
272
+ """Return True if the line above is a Go doc comment (``//`` comment block)."""
273
+ j = index - 1
274
+ while j >= 0:
275
+ stripped = lines[j].strip()
276
+ if stripped == "":
277
+ j -= 1
278
+ continue
279
+ return stripped.startswith("//")
280
+ return False
281
+
282
+
283
+ GoAdapter.count_file_loc = staticmethod(count_file_loc) # type: ignore[attr-defined]
@@ -0,0 +1,244 @@
1
+ """Python language adapter.
2
+
3
+ Detects Python repos via ``pyproject.toml`` / ``setup.py`` presence. Walks
4
+ ``src/`` or top-level packages for production files and ``tests/`` / ``test/``
5
+ for test files. Scans ``raise`` statements, classifies exception types, and
6
+ detects docstring presence above top-level declarations.
7
+
8
+ Deliberately minimal: this adapter is new at launch and not research-backed
9
+ to the same degree as the C# adapter. A "Python adapter validation" doc
10
+ published post-launch will compare scores on 3 popular Python repos.
11
+ """
12
+ from __future__ import annotations
13
+
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
+ _PYTHON_SUFFIX: tuple[str, ...] = (".py",)
28
+
29
+ _RAISE_PATTERN = re.compile(r"\braise\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(|$)")
30
+
31
+ _PUBLIC_DECL_PATTERN = re.compile(r"^(class|def)\s+([A-Za-z][A-Za-z0-9_]*)\s*[\(:]")
32
+ _PRIVATE_DECL_PATTERN = re.compile(r"^(class|def)\s+(_[A-Za-z0-9_]*)\s*[\(:]")
33
+
34
+ _TEST_METHOD_PATTERN = re.compile(r"^\s*def\s+(test_[A-Za-z0-9_]+)\s*\(", re.MULTILINE)
35
+ _TEST_SNAKE_PATTERN = re.compile(r"^test_[a-z][a-z0-9_]*_[a-z0-9_]+_[a-z0-9_]+$")
36
+
37
+ # Stdlib exception types considered "too generic" for good agent UX.
38
+ _GENERIC_EXCEPTION_NAMES: frozenset[str] = frozenset({
39
+ "Exception",
40
+ "BaseException",
41
+ "RuntimeError",
42
+ "ValueError",
43
+ "TypeError",
44
+ })
45
+
46
+ _PROD_DIR_CANDIDATES: tuple[str, ...] = ("src", "lib")
47
+ _TEST_DIR_CANDIDATES: tuple[str, ...] = ("tests", "test")
48
+
49
+
50
+ class PythonAdapter(LanguageAdapter):
51
+ """Python adapter. MVP implementation."""
52
+
53
+ name = "python"
54
+
55
+ # ------------------------------------------------------------------
56
+ # Detection
57
+ # ------------------------------------------------------------------
58
+
59
+ def detect(self, repo_path: Path) -> float:
60
+ if (repo_path / "pyproject.toml").is_file():
61
+ return 1.0
62
+ if (repo_path / "setup.py").is_file():
63
+ return 0.9
64
+ if any(repo_path.rglob("*.py")):
65
+ return 0.6
66
+ return 0.0
67
+
68
+ # ------------------------------------------------------------------
69
+ # File discovery
70
+ # ------------------------------------------------------------------
71
+
72
+ def find_production_files(self, repo_path: Path) -> list[Path]:
73
+ production_roots = self._production_roots(repo_path)
74
+ results: list[Path] = []
75
+ for root in production_roots:
76
+ results.extend(iter_source_files(root, suffixes=_PYTHON_SUFFIX))
77
+ return results
78
+
79
+ def find_test_files(self, repo_path: Path) -> list[Path]:
80
+ results: list[Path] = []
81
+ for name in _TEST_DIR_CANDIDATES:
82
+ candidate = repo_path / name
83
+ if candidate.is_dir():
84
+ results.extend(iter_source_files(candidate, suffixes=_PYTHON_SUFFIX))
85
+ return results
86
+
87
+ def find_production_modules(self, repo_path: Path) -> list[str]:
88
+ """Top-level package directories under src/ or the repo root."""
89
+ modules: set[str] = set()
90
+ for root in self._production_roots(repo_path):
91
+ for entry in root.iterdir():
92
+ if entry.is_dir() and (entry / "__init__.py").is_file():
93
+ modules.add(entry.name)
94
+ return sorted(modules)
95
+
96
+ def _production_roots(self, repo_path: Path) -> list[Path]:
97
+ roots: list[Path] = []
98
+ for name in _PROD_DIR_CANDIDATES:
99
+ candidate = repo_path / name
100
+ if candidate.is_dir():
101
+ roots.append(candidate)
102
+ if not roots:
103
+ # Fall back: top-level packages in the repo root that are NOT tests.
104
+ for entry in repo_path.iterdir():
105
+ if not entry.is_dir():
106
+ continue
107
+ if entry.name in _TEST_DIR_CANDIDATES:
108
+ continue
109
+ if entry.name.startswith("."):
110
+ continue
111
+ if (entry / "__init__.py").is_file():
112
+ roots.append(entry)
113
+ return roots
114
+
115
+ # ------------------------------------------------------------------
116
+ # Throw-site analysis (Python: raise sites)
117
+ # ------------------------------------------------------------------
118
+
119
+ def scan_throw_sites(
120
+ self,
121
+ files: Iterable[Path],
122
+ hint_marker: str,
123
+ domain_exception_types: set[str],
124
+ ) -> list[ThrowSite]:
125
+ sites: list[ThrowSite] = []
126
+ for path in files:
127
+ text = read_text_safely(path)
128
+ if not text:
129
+ continue
130
+ for match in _RAISE_PATTERN.finditer(text):
131
+ exception_type = match.group(1)
132
+ line_no = text.count("\n", 0, match.start()) + 1
133
+ # Lightweight message extraction: take the rest of the line.
134
+ line_start = text.rfind("\n", 0, match.start()) + 1
135
+ line_end = text.find("\n", match.start())
136
+ if line_end == -1:
137
+ line_end = len(text)
138
+ line_text = text[line_start:line_end]
139
+ sites.append(
140
+ ThrowSite(
141
+ file=path,
142
+ line=line_no,
143
+ exception_type=exception_type,
144
+ has_fix_hint=_has_fix_hint(line_text, hint_marker),
145
+ is_user_defined=exception_type in domain_exception_types,
146
+ is_generic=exception_type in _GENERIC_EXCEPTION_NAMES,
147
+ )
148
+ )
149
+ return sites
150
+
151
+ def generic_exception_names(self) -> set[str]:
152
+ return set(_GENERIC_EXCEPTION_NAMES)
153
+
154
+ # ------------------------------------------------------------------
155
+ # Declarations
156
+ # ------------------------------------------------------------------
157
+
158
+ def scan_declarations(self, files: Iterable[Path]) -> list[Declaration]:
159
+ declarations: list[Declaration] = []
160
+ for path in files:
161
+ text = read_text_safely(path)
162
+ if not text:
163
+ continue
164
+ declarations.extend(self._scan_declarations_in_text(path, text))
165
+ return declarations
166
+
167
+ def _scan_declarations_in_text(self, path: Path, text: str) -> list[Declaration]:
168
+ lines = text.splitlines()
169
+ results: list[Declaration] = []
170
+ for i, line in enumerate(lines):
171
+ if line.startswith(" ") or line.startswith("\t"):
172
+ # Only top-level declarations.
173
+ continue
174
+ public_match = _PUBLIC_DECL_PATTERN.match(line)
175
+ if public_match and not public_match.group(2).startswith("_"):
176
+ results.append(
177
+ Declaration(
178
+ file=path,
179
+ line=i + 1,
180
+ name=public_match.group(2),
181
+ visibility="public",
182
+ has_doc_comment=_has_following_docstring(lines, i),
183
+ )
184
+ )
185
+ continue
186
+ private_match = _PRIVATE_DECL_PATTERN.match(line)
187
+ if private_match:
188
+ results.append(
189
+ Declaration(
190
+ file=path,
191
+ line=i + 1,
192
+ name=private_match.group(2),
193
+ visibility="private",
194
+ has_doc_comment=_has_following_docstring(lines, i),
195
+ )
196
+ )
197
+ return results
198
+
199
+ # ------------------------------------------------------------------
200
+ # Test methods
201
+ # ------------------------------------------------------------------
202
+
203
+ def find_test_methods(self, files: Iterable[Path]) -> list[tuple[Path, str]]:
204
+ results: list[tuple[Path, str]] = []
205
+ for path in files:
206
+ text = read_text_safely(path)
207
+ if not text:
208
+ continue
209
+ for match in _TEST_METHOD_PATTERN.finditer(text):
210
+ results.append((path, match.group(1)))
211
+ return results
212
+
213
+ def test_naming_pattern(self) -> re.Pattern[str]:
214
+ return _TEST_SNAKE_PATTERN
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Helpers
219
+ # ---------------------------------------------------------------------------
220
+
221
+
222
+ def _has_fix_hint(text: str, hint_marker: str) -> bool:
223
+ lower = text.lower()
224
+ if hint_marker and hint_marker.lower() in lower:
225
+ return True
226
+ for phrase in ("hint:", "fix:", "see ", "try ", "use ", "check ", "did you mean"):
227
+ if phrase in lower:
228
+ return True
229
+ return False
230
+
231
+
232
+ def _has_following_docstring(lines: list[str], index: int) -> bool:
233
+ """Return True if the next non-blank line after ``index`` starts a docstring."""
234
+ j = index + 1
235
+ while j < len(lines):
236
+ stripped = lines[j].strip()
237
+ if stripped == "":
238
+ j += 1
239
+ continue
240
+ return stripped.startswith('"""') or stripped.startswith("'''")
241
+ return False
242
+
243
+
244
+ PythonAdapter.count_file_loc = staticmethod(count_file_loc) # type: ignore[attr-defined]