codeabc 0.1.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.
- backend/__init__.py +0 -0
- backend/app.py +95 -0
- backend/desktop_server.py +46 -0
- backend/models.py +629 -0
- backend/prompts/__init__.py +0 -0
- backend/prompts/annotate.py +44 -0
- backend/prompts/edit.py +62 -0
- backend/prompts/overview.py +57 -0
- backend/prompts/qa.py +45 -0
- backend/routers/__init__.py +0 -0
- backend/routers/analyze.py +271 -0
- backend/routers/project.py +872 -0
- backend/services/__init__.py +0 -0
- backend/services/action_plan.py +366 -0
- backend/services/activity.py +313 -0
- backend/services/apimap.py +391 -0
- backend/services/async_surface.py +123 -0
- backend/services/cache.py +183 -0
- backend/services/churn.py +244 -0
- backend/services/ci_checks.py +317 -0
- backend/services/codemap_export.py +136 -0
- backend/services/commands.py +276 -0
- backend/services/complexity.py +111 -0
- backend/services/config_files.py +190 -0
- backend/services/contributing.py +308 -0
- backend/services/coverage.py +283 -0
- backend/services/datamodels.py +190 -0
- backend/services/deep_nesting.py +110 -0
- backend/services/dependencies.py +270 -0
- backend/services/docs.py +255 -0
- backend/services/docstrings.py +163 -0
- backend/services/duplicate_code.py +165 -0
- backend/services/entrypoints.py +152 -0
- backend/services/envscan.py +167 -0
- backend/services/error_handling.py +239 -0
- backend/services/filenames.py +261 -0
- backend/services/github_clone.py +129 -0
- backend/services/glossary.py +96 -0
- backend/services/health_score.py +381 -0
- backend/services/importgraph.py +986 -0
- backend/services/integrations.py +263 -0
- backend/services/licenses.py +505 -0
- backend/services/llm.py +117 -0
- backend/services/long_functions.py +101 -0
- backend/services/ownership.py +194 -0
- backend/services/package_purposes.py +112 -0
- backend/services/pr_reading.py +181 -0
- backend/services/release_map.py +402 -0
- backend/services/report_export.py +232 -0
- backend/services/risk.py +88 -0
- backend/services/scanner.py +546 -0
- backend/services/schedules.py +369 -0
- backend/services/security.py +445 -0
- backend/services/settings_map.py +234 -0
- backend/services/symbols.py +605 -0
- backend/services/techdebt.py +124 -0
- backend/services/too_many_params.py +102 -0
- backend/services/typing_coverage.py +184 -0
- codeabc-0.1.0.dist-info/METADATA +283 -0
- codeabc-0.1.0.dist-info/RECORD +62 -0
- codeabc-0.1.0.dist-info/WHEEL +4 -0
- codeabc-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
"""Definition index — where each name in the code is actually defined.
|
|
2
|
+
|
|
3
|
+
Reading unfamiliar code means constantly hitting a name and wondering "where
|
|
4
|
+
does this come from?". A call to ``parse_config``, a ``class Scanner``, an
|
|
5
|
+
imported helper — the reader wants the file and line that defines it, not a web
|
|
6
|
+
search. This module builds that index from the source CodeABC already loaded,
|
|
7
|
+
with no LLM and no language server: it scans for top-level definitions (and one
|
|
8
|
+
level of class methods) and records where each one lives, so the UI can offer
|
|
9
|
+
"jump to definition" for any name a reader clicks.
|
|
10
|
+
|
|
11
|
+
It covers the two languages CodeABC annotates today:
|
|
12
|
+
|
|
13
|
+
Python ``def`` / ``async def`` / ``class`` at module level, plus methods
|
|
14
|
+
one indent inside a class (recorded with their enclosing class).
|
|
15
|
+
JS / TS ``function`` / ``async function`` / ``export function`` / ``class``,
|
|
16
|
+
``const name = (...) =>`` / ``const name = function`` assignments, and
|
|
17
|
+
methods one indent inside a class (recorded with their class).
|
|
18
|
+
|
|
19
|
+
Each entry carries the name, its kind (function / class / method), the
|
|
20
|
+
enclosing class for a method, the file, and a 1-based line number.
|
|
21
|
+
|
|
22
|
+
Limitations (kept honest on purpose):
|
|
23
|
+
|
|
24
|
+
* Matching is regex over indentation, not a real parser. Decorators that
|
|
25
|
+
rename, ``setattr`` / ``globals()`` tricks, conditional re-exports, and
|
|
26
|
+
names built at runtime are out of scope.
|
|
27
|
+
* A name defined in several files yields one entry per file; the caller
|
|
28
|
+
decides which is meant.
|
|
29
|
+
|
|
30
|
+
The companion lookup :func:`find_references` answers the other half — where a
|
|
31
|
+
name is *used*, not just where it is born — by whole-word text search across the
|
|
32
|
+
same source files, flagging the occurrence that is the declaration itself.
|
|
33
|
+
|
|
34
|
+
Every entry also carries an ``exported`` flag — public by Python's underscore
|
|
35
|
+
convention or a JS/TS ``export`` — and :func:`public_api` filters the index down
|
|
36
|
+
to just that surface, so a reader can see a project's interface before its
|
|
37
|
+
internals.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import re
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Shared helpers
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
_IDENT = r"[A-Za-z_$][\w$]*"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _lang(path: str) -> str:
|
|
52
|
+
ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
|
|
53
|
+
if ext == "py":
|
|
54
|
+
return "python"
|
|
55
|
+
if ext in ("js", "ts", "jsx", "tsx", "mjs", "cjs"):
|
|
56
|
+
return "js"
|
|
57
|
+
return ext
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _indent_width(line: str) -> int:
|
|
61
|
+
expanded = line.expandtabs(4)
|
|
62
|
+
return len(expanded) - len(expanded.lstrip())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _count_phrase(n: int, kind: str) -> str:
|
|
66
|
+
"""'1 class', '2 classes', '1 function' — grammatical for a plain-language note."""
|
|
67
|
+
if kind == "class":
|
|
68
|
+
word = "class" if n == 1 else "classes"
|
|
69
|
+
else:
|
|
70
|
+
word = kind if n == 1 else f"{kind}s"
|
|
71
|
+
return f"{n} {word}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Python
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
_PY_DEF_RE = re.compile(r"^(?P<indent>\s*)(?:async\s+)?def\s+(?P<name>\w+)\s*\(")
|
|
79
|
+
_PY_CLASS_RE = re.compile(r"^(?P<indent>\s*)class\s+(?P<name>\w+)\s*[:(]")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _py_definitions(path: str, content: str) -> list[dict]:
|
|
83
|
+
"""Top-level functions/classes plus direct (and nested-class) methods.
|
|
84
|
+
|
|
85
|
+
A stack of open ``class`` / ``def`` blocks, keyed by indentation, tells us
|
|
86
|
+
what encloses each definition. A ``def`` whose nearest enclosing block is a
|
|
87
|
+
class is a method; a ``def`` nested inside another ``def`` is a local helper
|
|
88
|
+
and is skipped (it is not part of the public reading surface).
|
|
89
|
+
"""
|
|
90
|
+
out: list[dict] = []
|
|
91
|
+
# frames: (indent, kind, name, exported) for currently open class/def blocks
|
|
92
|
+
frames: list[tuple[int, str, str, bool]] = []
|
|
93
|
+
|
|
94
|
+
for lineno, raw in enumerate(content.splitlines(), start=1):
|
|
95
|
+
stripped = raw.strip()
|
|
96
|
+
if not stripped or stripped.startswith("#"):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
class_match = _PY_CLASS_RE.match(raw)
|
|
100
|
+
def_match = None if class_match else _PY_DEF_RE.match(raw)
|
|
101
|
+
if class_match is None and def_match is None:
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
indent = _indent_width(raw)
|
|
105
|
+
# close every block that this line is not nested inside
|
|
106
|
+
while frames and frames[-1][0] >= indent:
|
|
107
|
+
frames.pop()
|
|
108
|
+
enclosing = frames[-1] if frames else None
|
|
109
|
+
# A name is part of the public surface only when its container is and it
|
|
110
|
+
# follows the convention: no leading underscore.
|
|
111
|
+
parent_exported = _enclosing_exported(enclosing)
|
|
112
|
+
|
|
113
|
+
if class_match is not None:
|
|
114
|
+
name = class_match.group("name")
|
|
115
|
+
parent = enclosing[2] if enclosing and enclosing[1] == "class" else None
|
|
116
|
+
exported = parent_exported and not name.startswith("_")
|
|
117
|
+
out.append(_entry(path, "python", name, "class", parent, lineno, exported))
|
|
118
|
+
frames.append((indent, "class", name, exported))
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
assert def_match is not None # narrowing: class_match is None here
|
|
122
|
+
name = def_match.group("name")
|
|
123
|
+
exported = parent_exported and not name.startswith("_")
|
|
124
|
+
if enclosing is None:
|
|
125
|
+
out.append(_entry(path, "python", name, "function", None, lineno, exported))
|
|
126
|
+
frames.append((indent, "def", name, exported))
|
|
127
|
+
elif enclosing[1] == "class":
|
|
128
|
+
out.append(_entry(path, "python", name, "method", enclosing[2], lineno, exported))
|
|
129
|
+
frames.append((indent, "def", name, exported))
|
|
130
|
+
else:
|
|
131
|
+
# nested inside another def: a local helper — track it so its own
|
|
132
|
+
# nested blocks resolve correctly, but do not index it.
|
|
133
|
+
frames.append((indent, "def", name, False))
|
|
134
|
+
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# JavaScript / TypeScript
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
_JS_FUNC_RE = re.compile(
|
|
143
|
+
r"^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(" + _IDENT + r")\s*\("
|
|
144
|
+
)
|
|
145
|
+
_JS_CLASS_RE = re.compile(
|
|
146
|
+
r"^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(" + _IDENT + r")\b"
|
|
147
|
+
)
|
|
148
|
+
# const/let/var name = (...) => ... or = async (...) => or = function
|
|
149
|
+
_JS_ASSIGN_RE = re.compile(
|
|
150
|
+
r"^\s*(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+(" + _IDENT + r")\s*=\s*"
|
|
151
|
+
r"(?:async\s+)?(?:function\b|\*?\s*\([^)]*\)\s*=>|" + _IDENT + r"\s*=>)"
|
|
152
|
+
)
|
|
153
|
+
# A method *declaration* inside a class body: optional TS modifiers, an optional
|
|
154
|
+
# get/set or generator marker, the name, a parameter list, an optional TS return
|
|
155
|
+
# annotation, and the opening brace on the same line. The trailing ``{`` is what
|
|
156
|
+
# separates a declaration (``render() {``) from a call (``render();``).
|
|
157
|
+
_JS_METHOD_RE = re.compile(
|
|
158
|
+
r"^\s*(?:(?:public|private|protected|readonly|static|async|override|abstract)\s+)*"
|
|
159
|
+
r"(?:(?:get|set)\s+)?\*?\s*"
|
|
160
|
+
r"(?P<name>" + _IDENT + r")\s*\([^;]*\)\s*(?::[^={};]+)?\{"
|
|
161
|
+
)
|
|
162
|
+
# Control-flow heads also read as ``word (...) {``; never treat them as methods.
|
|
163
|
+
_JS_NON_METHODS = frozenset(
|
|
164
|
+
{"if", "for", "while", "switch", "catch", "do", "with", "return", "function", "else"}
|
|
165
|
+
)
|
|
166
|
+
# A top-level declaration is part of the public surface only when it is exported.
|
|
167
|
+
_JS_EXPORT_RE = re.compile(r"^\s*export\b")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _js_line_exported(raw: str) -> bool:
|
|
171
|
+
"""True when a declaration line begins with ``export`` (``export default`` too)."""
|
|
172
|
+
return bool(_JS_EXPORT_RE.match(raw))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _js_method_private(raw: str) -> bool:
|
|
176
|
+
"""True when a method carries a ``private`` / ``protected`` TS modifier.
|
|
177
|
+
|
|
178
|
+
Modifiers sit before the name and parameter list, so the words ahead of the
|
|
179
|
+
first ``(`` are enough to decide; ``#``-prefixed private fields never reach
|
|
180
|
+
here because they are not matched as methods in the first place.
|
|
181
|
+
"""
|
|
182
|
+
prefix = raw.strip().split("(", 1)[0]
|
|
183
|
+
tokens = prefix.split()
|
|
184
|
+
return "private" in tokens or "protected" in tokens
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _js_definitions(path: str, content: str) -> list[dict]:
|
|
188
|
+
out: list[dict] = []
|
|
189
|
+
# frames: (indent, kind, name, exported) for currently open class/block scopes,
|
|
190
|
+
# mirroring the Python pass so a method is only read inside a class body and a
|
|
191
|
+
# method's own body is not re-scanned for nested "methods".
|
|
192
|
+
frames: list[tuple[int, str, str, bool]] = []
|
|
193
|
+
|
|
194
|
+
for lineno, raw in enumerate(content.splitlines(), start=1):
|
|
195
|
+
stripped = raw.strip()
|
|
196
|
+
if not stripped or stripped.startswith("//"):
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
indent = _indent_width(raw)
|
|
200
|
+
while frames and frames[-1][0] >= indent:
|
|
201
|
+
frames.pop()
|
|
202
|
+
enclosing = frames[-1] if frames else None
|
|
203
|
+
# In JS/TS the public surface is what the module exports, not a naming
|
|
204
|
+
# convention; a member is only reachable when its container is too.
|
|
205
|
+
parent_exported = _enclosing_exported(enclosing)
|
|
206
|
+
|
|
207
|
+
class_match = _JS_CLASS_RE.match(raw)
|
|
208
|
+
if class_match:
|
|
209
|
+
name = class_match.group(1)
|
|
210
|
+
parent = enclosing[2] if enclosing and enclosing[1] == "class" else None
|
|
211
|
+
exported = parent_exported and _js_line_exported(raw)
|
|
212
|
+
out.append(_entry(path, "js", name, "class", parent, lineno, exported))
|
|
213
|
+
frames.append((indent, "class", name, exported))
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
func_match = _JS_FUNC_RE.match(raw)
|
|
217
|
+
if func_match:
|
|
218
|
+
name = func_match.group(1)
|
|
219
|
+
exported = parent_exported and _js_line_exported(raw)
|
|
220
|
+
out.append(_entry(path, "js", name, "function", None, lineno, exported))
|
|
221
|
+
frames.append((indent, "block", name, exported))
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
assign_match = _JS_ASSIGN_RE.match(raw)
|
|
225
|
+
if assign_match:
|
|
226
|
+
name = assign_match.group(1)
|
|
227
|
+
exported = parent_exported and _js_line_exported(raw)
|
|
228
|
+
out.append(_entry(path, "js", name, "function", None, lineno, exported))
|
|
229
|
+
frames.append((indent, "block", name, exported))
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if enclosing is not None and enclosing[1] == "class":
|
|
233
|
+
method_match = _JS_METHOD_RE.match(raw)
|
|
234
|
+
if method_match:
|
|
235
|
+
name = method_match.group("name")
|
|
236
|
+
if name not in _JS_NON_METHODS:
|
|
237
|
+
exported = parent_exported and not _js_method_private(raw)
|
|
238
|
+
out.append(_entry(path, "js", name, "method", enclosing[2], lineno, exported))
|
|
239
|
+
frames.append((indent, "block", name, exported))
|
|
240
|
+
|
|
241
|
+
return out
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Assembly
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _entry(
|
|
250
|
+
path: str,
|
|
251
|
+
lang: str,
|
|
252
|
+
name: str,
|
|
253
|
+
kind: str,
|
|
254
|
+
parent: str | None,
|
|
255
|
+
line: int,
|
|
256
|
+
exported: bool,
|
|
257
|
+
) -> dict:
|
|
258
|
+
qualname = f"{parent}.{name}" if parent else name
|
|
259
|
+
return {
|
|
260
|
+
"name": name,
|
|
261
|
+
"qualname": qualname,
|
|
262
|
+
"kind": kind,
|
|
263
|
+
"parent": parent,
|
|
264
|
+
"lang": lang,
|
|
265
|
+
"file": path,
|
|
266
|
+
"line": line,
|
|
267
|
+
"exported": exported,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _enclosing_exported(enclosing: tuple | None) -> bool:
|
|
272
|
+
"""Whether a definition directly inside ``enclosing`` can be public.
|
|
273
|
+
|
|
274
|
+
Module level (no enclosing frame) and the body of a public class can hold
|
|
275
|
+
public names. The body of a function — or of a non-public class — cannot,
|
|
276
|
+
so a name born there is internal no matter what it is called.
|
|
277
|
+
"""
|
|
278
|
+
if enclosing is None:
|
|
279
|
+
return True
|
|
280
|
+
if enclosing[1] == "class":
|
|
281
|
+
return bool(enclosing[3])
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _build_notes(definitions: list[dict], file_contents: dict[str, str]) -> list[str]:
|
|
286
|
+
notes: list[str] = []
|
|
287
|
+
scanned = sum(1 for p in file_contents if _lang(p) in ("python", "js"))
|
|
288
|
+
if scanned == 0:
|
|
289
|
+
notes.append("No Python or JS/TS files were found to index.")
|
|
290
|
+
return notes
|
|
291
|
+
|
|
292
|
+
by_kind: dict[str, int] = {}
|
|
293
|
+
for d in definitions:
|
|
294
|
+
by_kind[d["kind"]] = by_kind.get(d["kind"], 0) + 1
|
|
295
|
+
parts = [_count_phrase(by_kind[k], k) for k in sorted(by_kind)]
|
|
296
|
+
if parts:
|
|
297
|
+
notes.append(f"Indexed {', '.join(parts)} across {scanned} source file(s).")
|
|
298
|
+
|
|
299
|
+
duplicates = _duplicate_names(definitions)
|
|
300
|
+
if duplicates:
|
|
301
|
+
sample = ", ".join(duplicates[:5])
|
|
302
|
+
notes.append(
|
|
303
|
+
f"{len(duplicates)} name(s) are defined in more than one place "
|
|
304
|
+
f"(e.g. {sample}); a lookup returns every match."
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
notes.append(
|
|
308
|
+
"Definitions only: this index shows where a name is declared, not "
|
|
309
|
+
"every place it is used. Matching is regex-based, so runtime-built or "
|
|
310
|
+
"re-exported names may be missed."
|
|
311
|
+
)
|
|
312
|
+
return notes
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _duplicate_names(definitions: list[dict]) -> list[str]:
|
|
316
|
+
seen: dict[str, int] = {}
|
|
317
|
+
for d in definitions:
|
|
318
|
+
seen[d["name"]] = seen.get(d["name"], 0) + 1
|
|
319
|
+
return sorted(name for name, count in seen.items() if count > 1)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def build_definition_index(file_contents: dict[str, str], *, limit: int = 2000) -> dict:
|
|
323
|
+
"""Index every top-level definition (and class method) in the project.
|
|
324
|
+
|
|
325
|
+
Returns ``{"total", "definitions", "notes"}`` where ``definitions`` is a
|
|
326
|
+
deterministic, alphabetically sorted list of entries (see :func:`_entry`),
|
|
327
|
+
truncated to ``limit``.
|
|
328
|
+
"""
|
|
329
|
+
definitions: list[dict] = []
|
|
330
|
+
for path in sorted(file_contents):
|
|
331
|
+
lang = _lang(path)
|
|
332
|
+
content = file_contents[path]
|
|
333
|
+
if lang == "python":
|
|
334
|
+
definitions.extend(_py_definitions(path, content))
|
|
335
|
+
elif lang == "js":
|
|
336
|
+
definitions.extend(_js_definitions(path, content))
|
|
337
|
+
|
|
338
|
+
definitions.sort(key=lambda d: (d["name"].lower(), d["file"], d["line"]))
|
|
339
|
+
notes = _build_notes(definitions, file_contents)
|
|
340
|
+
total = len(definitions)
|
|
341
|
+
return {"total": total, "definitions": definitions[:limit], "notes": notes}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def find_definition(file_contents: dict[str, str], name: str, *, limit: int = 50) -> list[dict]:
|
|
345
|
+
"""Return the places that define ``name``.
|
|
346
|
+
|
|
347
|
+
Tries an exact match first, then falls back to a case-insensitive match so
|
|
348
|
+
a reader who types ``scanner`` still finds ``Scanner``. Results are ordered
|
|
349
|
+
by file then line.
|
|
350
|
+
"""
|
|
351
|
+
target = (name or "").strip()
|
|
352
|
+
if not target:
|
|
353
|
+
return []
|
|
354
|
+
|
|
355
|
+
index = build_definition_index(file_contents, limit=100_000)
|
|
356
|
+
matches = [d for d in index["definitions"] if d["name"] == target]
|
|
357
|
+
if not matches:
|
|
358
|
+
lowered = target.lower()
|
|
359
|
+
matches = [d for d in index["definitions"] if d["name"].lower() == lowered]
|
|
360
|
+
|
|
361
|
+
matches.sort(key=lambda d: (d["file"], d["line"]))
|
|
362
|
+
return matches[:limit]
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
# Public surface — the names a project means to expose, not its internals
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def public_api(file_contents: dict[str, str], *, limit: int = 2000) -> dict:
|
|
371
|
+
"""Return the project's public surface — the names other code is meant to call.
|
|
372
|
+
|
|
373
|
+
Where :func:`build_definition_index` lists *every* definition, this keeps
|
|
374
|
+
only the ones a project exposes on purpose, so a reader sees the interface
|
|
375
|
+
before the internals. The rule follows each language's own convention:
|
|
376
|
+
|
|
377
|
+
Python a name is public unless it starts with an underscore, and a
|
|
378
|
+
method is public only when its class is too — so ``_helper`` and
|
|
379
|
+
``Scanner.__init__`` are internal.
|
|
380
|
+
JS / TS a name is public when it is ``export``-ed, and a method is public
|
|
381
|
+
when its class is exported and it is not declared ``private`` /
|
|
382
|
+
``protected``.
|
|
383
|
+
|
|
384
|
+
Returns ``{"total", "definitions", "notes"}`` with the same entry shape as
|
|
385
|
+
the definition index (each entry already carries an ``exported`` flag),
|
|
386
|
+
alphabetically sorted and truncated to ``limit``.
|
|
387
|
+
"""
|
|
388
|
+
index = build_definition_index(file_contents, limit=100_000)
|
|
389
|
+
public = [d for d in index["definitions"] if d.get("exported")]
|
|
390
|
+
total = len(public)
|
|
391
|
+
notes = _public_api_notes(public, total, file_contents)
|
|
392
|
+
return {"total": total, "definitions": public[:limit], "notes": notes}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _public_api_notes(public: list[dict], total: int, file_contents: dict[str, str]) -> list[str]:
|
|
396
|
+
langs = {_lang(p) for p in file_contents}
|
|
397
|
+
has_py = "python" in langs
|
|
398
|
+
has_js = "js" in langs
|
|
399
|
+
if not (has_py or has_js):
|
|
400
|
+
return ["No Python or JS/TS files were found to index."]
|
|
401
|
+
if total == 0:
|
|
402
|
+
return [
|
|
403
|
+
"No public names were found: every definition is underscore-prefixed "
|
|
404
|
+
"(Python) or not exported (JS/TS)."
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
by_kind: dict[str, int] = {}
|
|
408
|
+
for d in public:
|
|
409
|
+
by_kind[d["kind"]] = by_kind.get(d["kind"], 0) + 1
|
|
410
|
+
parts = [_count_phrase(by_kind[k], k) for k in sorted(by_kind)]
|
|
411
|
+
notes = [f"{total} public name(s) make up the surface: {', '.join(parts)}."]
|
|
412
|
+
|
|
413
|
+
rules: list[str] = []
|
|
414
|
+
if has_py:
|
|
415
|
+
rules.append("Python counts a name as public when it has no leading underscore")
|
|
416
|
+
if has_js:
|
|
417
|
+
rules.append("JS/TS counts a name as public when it is exported")
|
|
418
|
+
notes.append(f"{'; '.join(rules)}.")
|
|
419
|
+
|
|
420
|
+
notes.append(
|
|
421
|
+
"Convention-based: a name shown here may still be internal (exported only "
|
|
422
|
+
"for tests, say), and __all__ or re-export lists are not consulted."
|
|
423
|
+
)
|
|
424
|
+
return notes
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
# ---------------------------------------------------------------------------
|
|
428
|
+
# References — where a name is used, not just where it is defined
|
|
429
|
+
# ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def find_references(file_contents: dict[str, str], name: str, *, limit: int = 200) -> dict:
|
|
433
|
+
"""Return every place ``name`` appears as a whole word — its use sites.
|
|
434
|
+
|
|
435
|
+
This is the other half of :func:`find_definition`: where that points at the
|
|
436
|
+
one line a name is born, this lists every line it shows up on, with a
|
|
437
|
+
trimmed one-line preview and an ``is_definition`` flag so the UI can tell the
|
|
438
|
+
declaration apart from the call sites. Matching is whole-word and
|
|
439
|
+
case-sensitive (``scan`` does not match ``scanner``, ``rescan`` or
|
|
440
|
+
``my_scan``), and an attribute access like ``self.scan`` counts as a use.
|
|
441
|
+
|
|
442
|
+
Returns ``{"name", "total", "files", "references", "notes"}`` where
|
|
443
|
+
``references`` is ordered by file then line and truncated to ``limit`` while
|
|
444
|
+
``total`` and ``files`` count every match.
|
|
445
|
+
|
|
446
|
+
Limitations (kept honest, same as the definition index): it is text search,
|
|
447
|
+
not a parser. A use hidden inside a string literal or a comment may be
|
|
448
|
+
missed, and a same-named attribute on an unrelated object may be counted.
|
|
449
|
+
"""
|
|
450
|
+
target = (name or "").strip()
|
|
451
|
+
if not target:
|
|
452
|
+
return {"name": name, "total": 0, "files": 0, "references": [], "notes": []}
|
|
453
|
+
|
|
454
|
+
pattern = re.compile(r"(?<![\w$])" + re.escape(target) + r"(?![\w$])")
|
|
455
|
+
|
|
456
|
+
index = build_definition_index(file_contents, limit=100_000)
|
|
457
|
+
def_sites = {(d["file"], d["line"]) for d in index["definitions"] if d["name"] == target}
|
|
458
|
+
|
|
459
|
+
references: list[dict] = []
|
|
460
|
+
for path in sorted(file_contents):
|
|
461
|
+
if _lang(path) not in ("python", "js"):
|
|
462
|
+
continue
|
|
463
|
+
for lineno, raw in enumerate(file_contents[path].splitlines(), start=1):
|
|
464
|
+
stripped = raw.strip()
|
|
465
|
+
if not stripped or stripped.startswith("#") or stripped.startswith("//"):
|
|
466
|
+
continue
|
|
467
|
+
if not pattern.search(raw):
|
|
468
|
+
continue
|
|
469
|
+
references.append(
|
|
470
|
+
{
|
|
471
|
+
"name": target,
|
|
472
|
+
"file": path,
|
|
473
|
+
"line": lineno,
|
|
474
|
+
"text": stripped,
|
|
475
|
+
"is_definition": (path, lineno) in def_sites,
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
references.sort(key=lambda r: (r["file"], r["line"]))
|
|
480
|
+
total = len(references)
|
|
481
|
+
files = len({r["file"] for r in references})
|
|
482
|
+
notes = _reference_notes(target, references, total, files, file_contents)
|
|
483
|
+
return {
|
|
484
|
+
"name": target,
|
|
485
|
+
"total": total,
|
|
486
|
+
"files": files,
|
|
487
|
+
"references": references[:limit],
|
|
488
|
+
"notes": notes,
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _reference_notes(
|
|
493
|
+
target: str, references: list[dict], total: int, files: int, file_contents: dict[str, str]
|
|
494
|
+
) -> list[str]:
|
|
495
|
+
scanned = sum(1 for p in file_contents if _lang(p) in ("python", "js"))
|
|
496
|
+
if scanned == 0:
|
|
497
|
+
return ["No Python or JS/TS files were found to search."]
|
|
498
|
+
if total == 0:
|
|
499
|
+
return [
|
|
500
|
+
f"'{target}' is not used in any Python or JS/TS file "
|
|
501
|
+
"(or it only appears inside strings or comments)."
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
notes = [f"Found {total} reference(s) to '{target}' across {files} file(s)."]
|
|
505
|
+
def_count = sum(1 for r in references if r["is_definition"])
|
|
506
|
+
if def_count:
|
|
507
|
+
where = "is where" if def_count == 1 else "are where"
|
|
508
|
+
notes.append(f"{def_count} of these {where} '{target}' is defined; the rest are uses.")
|
|
509
|
+
notes.append(
|
|
510
|
+
"Whole-word text search: a use hidden inside a string or comment may be "
|
|
511
|
+
"missed, and a same-named field on an unrelated object may be counted."
|
|
512
|
+
)
|
|
513
|
+
return notes
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# ---------------------------------------------------------------------------
|
|
517
|
+
# File outline — one file's structure, in the order it is written
|
|
518
|
+
# ---------------------------------------------------------------------------
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def file_outline(file_contents: dict[str, str], path: str, *, limit: int = 1000) -> dict:
|
|
522
|
+
"""Return one file's table of contents, in source order.
|
|
523
|
+
|
|
524
|
+
Where :func:`build_definition_index` flattens the whole project
|
|
525
|
+
alphabetically, this answers the question a reader has the moment they open
|
|
526
|
+
an unfamiliar file: "what is in here, top to bottom?". Top-level functions
|
|
527
|
+
and classes are listed in the order they appear, and a class carries its
|
|
528
|
+
methods nested underneath (also in source order), so the shape of the file
|
|
529
|
+
is visible before any of its detail.
|
|
530
|
+
|
|
531
|
+
Returns ``{"file", "lang", "total", "outline", "notes"}``. ``outline`` is a
|
|
532
|
+
list of entries (see :func:`_entry`) in line order; an entry that can hold
|
|
533
|
+
nested definitions — a class — gains a ``children`` list with its methods
|
|
534
|
+
(and any nested classes). ``total`` counts every definition, nested ones
|
|
535
|
+
included, before the ``limit`` truncation.
|
|
536
|
+
"""
|
|
537
|
+
content = file_contents.get(path)
|
|
538
|
+
if content is None:
|
|
539
|
+
return {
|
|
540
|
+
"file": path,
|
|
541
|
+
"lang": _lang(path),
|
|
542
|
+
"total": 0,
|
|
543
|
+
"outline": [],
|
|
544
|
+
"notes": [f"'{path}' is not among the analyzed files."],
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
lang = _lang(path)
|
|
548
|
+
if lang == "python":
|
|
549
|
+
defs = _py_definitions(path, content)
|
|
550
|
+
elif lang == "js":
|
|
551
|
+
defs = _js_definitions(path, content)
|
|
552
|
+
else:
|
|
553
|
+
return {
|
|
554
|
+
"file": path,
|
|
555
|
+
"lang": lang,
|
|
556
|
+
"total": 0,
|
|
557
|
+
"outline": [],
|
|
558
|
+
"notes": ["The outline covers Python and JS/TS files; this file is neither."],
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
total = len(defs)
|
|
562
|
+
classes_by_name: dict[str, dict] = {}
|
|
563
|
+
outline: list[dict] = []
|
|
564
|
+
# Walk in source order so a class is always seen before its own members.
|
|
565
|
+
for d in sorted(defs, key=lambda e: e["line"]):
|
|
566
|
+
node = dict(d)
|
|
567
|
+
parent_node = classes_by_name.get(d["parent"]) if d["parent"] else None
|
|
568
|
+
if parent_node is not None:
|
|
569
|
+
parent_node["children"].append(node)
|
|
570
|
+
else:
|
|
571
|
+
# A method whose class is not itself indexed (an orphan) still gets
|
|
572
|
+
# surfaced at the top level rather than silently dropped.
|
|
573
|
+
outline.append(node)
|
|
574
|
+
if d["kind"] == "class":
|
|
575
|
+
node["children"] = []
|
|
576
|
+
classes_by_name[d["name"]] = node
|
|
577
|
+
|
|
578
|
+
notes = _outline_notes(path, lang, defs, total)
|
|
579
|
+
return {
|
|
580
|
+
"file": path,
|
|
581
|
+
"lang": lang,
|
|
582
|
+
"total": total,
|
|
583
|
+
"outline": outline[:limit],
|
|
584
|
+
"notes": notes,
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _outline_notes(path: str, lang: str, defs: list[dict], total: int) -> list[str]:
|
|
589
|
+
name = path.rsplit("/", 1)[-1]
|
|
590
|
+
if total == 0:
|
|
591
|
+
return [f"No top-level functions or classes were found in {name}."]
|
|
592
|
+
|
|
593
|
+
by_kind: dict[str, int] = {}
|
|
594
|
+
for d in defs:
|
|
595
|
+
by_kind[d["kind"]] = by_kind.get(d["kind"], 0) + 1
|
|
596
|
+
parts = [_count_phrase(by_kind[k], k) for k in sorted(by_kind)]
|
|
597
|
+
notes = [f"{name} defines {', '.join(parts)}, listed in the order they appear."]
|
|
598
|
+
if lang == "js":
|
|
599
|
+
notes.append(
|
|
600
|
+
"For JS/TS, class methods are broken out when the class body is "
|
|
601
|
+
"conventionally indented; arrow-function fields and computed names "
|
|
602
|
+
"are still listed under the class as a whole."
|
|
603
|
+
)
|
|
604
|
+
notes.append("Regex-based outline: runtime-built or re-exported names may be missed.")
|
|
605
|
+
return notes
|