agentseed-mcp 0.3.0

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/LICENSE +202 -0
  3. package/README.ja.md +320 -0
  4. package/README.md +318 -0
  5. package/README.zh.md +306 -0
  6. package/bin/cli.js +37 -0
  7. package/mcp.json +12 -0
  8. package/package.json +30 -0
  9. package/plugin.json +22 -0
  10. package/server/.agentseed/verification-log.jsonl +2 -0
  11. package/server/__pycache__/guard_cli.cpython-313.pyc +0 -0
  12. package/server/__pycache__/guard_engine.cpython-313.pyc +0 -0
  13. package/server/__pycache__/test_cli.cpython-313-pytest-9.1.1.pyc +0 -0
  14. package/server/__pycache__/test_cli.cpython-313.pyc +0 -0
  15. package/server/__pycache__/test_features.cpython-313-pytest-9.1.1.pyc +0 -0
  16. package/server/__pycache__/test_features.cpython-313.pyc +0 -0
  17. package/server/__pycache__/test_guard.cpython-313-pytest-9.1.1.pyc +0 -0
  18. package/server/__pycache__/test_guard.cpython-313.pyc +0 -0
  19. package/server/__pycache__/test_hook.cpython-313-pytest-9.1.1.pyc +0 -0
  20. package/server/__pycache__/test_hook.cpython-313.pyc +0 -0
  21. package/server/__pycache__/test_manifests.cpython-313-pytest-9.1.1.pyc +0 -0
  22. package/server/__pycache__/test_manifests.cpython-313.pyc +0 -0
  23. package/server/__pycache__/test_server.cpython-313-pytest-9.1.1.pyc +0 -0
  24. package/server/__pycache__/test_server.cpython-313.pyc +0 -0
  25. package/server/engine/__init__.py +64 -0
  26. package/server/engine/__pycache__/__init__.cpython-313.pyc +0 -0
  27. package/server/engine/__pycache__/audit.cpython-313.pyc +0 -0
  28. package/server/engine/__pycache__/config.cpython-313.pyc +0 -0
  29. package/server/engine/__pycache__/hallucination.cpython-313.pyc +0 -0
  30. package/server/engine/__pycache__/imports.cpython-313.pyc +0 -0
  31. package/server/engine/__pycache__/plugin.cpython-313.pyc +0 -0
  32. package/server/engine/__pycache__/sandbox.cpython-313.pyc +0 -0
  33. package/server/engine/__pycache__/schema.cpython-313.pyc +0 -0
  34. package/server/engine/__pycache__/symbols.cpython-313.pyc +0 -0
  35. package/server/engine/__pycache__/version.cpython-313.pyc +0 -0
  36. package/server/engine/audit.py +84 -0
  37. package/server/engine/config.py +131 -0
  38. package/server/engine/hallucination.py +254 -0
  39. package/server/engine/imports.py +136 -0
  40. package/server/engine/plugin.py +367 -0
  41. package/server/engine/sandbox.py +287 -0
  42. package/server/engine/schema.py +193 -0
  43. package/server/engine/symbols.py +984 -0
  44. package/server/engine/version.py +17 -0
  45. package/server/guard_cli.py +455 -0
  46. package/server/guard_engine.py +111 -0
  47. package/server/guard_hook.py +404 -0
  48. package/server/guard_server.py +472 -0
  49. package/server/requirements.txt +7 -0
  50. package/server/test_cli.py +132 -0
  51. package/server/test_features.py +426 -0
  52. package/server/test_guard.py +828 -0
  53. package/server/test_hook.py +331 -0
  54. package/server/test_manifests.py +70 -0
  55. package/server/test_server.py +247 -0
  56. package/skills/verify-before-code/SKILL.ja.md +116 -0
  57. package/skills/verify-before-code/SKILL.md +140 -0
  58. package/skills/verify-before-code/SKILL.zh.md +117 -0
  59. package/skills/verify-before-code/references/DEFAULT-NORMS.md +52 -0
  60. package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.ja.md +121 -0
  61. package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.md +166 -0
  62. package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.zh.md +145 -0
  63. package/skills/verify-before-code/references/PROMPT-POOL.ja.md +248 -0
  64. package/skills/verify-before-code/references/PROMPT-POOL.md +282 -0
  65. package/skills/verify-before-code/references/PROMPT-POOL.zh.md +252 -0
  66. package/skills/verify-before-code/references/SDD-CONTRACT.ja.md +61 -0
  67. package/skills/verify-before-code/references/SDD-CONTRACT.md +66 -0
  68. package/skills/verify-before-code/references/SDD-CONTRACT.zh.md +58 -0
  69. package/skills/verify-before-code/references/VENDOR-SOLUTIONS.ja.md +62 -0
  70. package/skills/verify-before-code/references/VENDOR-SOLUTIONS.md +62 -0
  71. package/skills/verify-before-code/references/VENDOR-SOLUTIONS.zh.md +54 -0
  72. package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.ja.md +68 -0
  73. package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.md +73 -0
  74. package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.zh.md +68 -0
  75. package/skills/verify-before-code/scripts/check.ps1 +52 -0
  76. package/skills/verify-before-code/scripts/check.sh +44 -0
@@ -0,0 +1,984 @@
1
+ """AgentSeed undefined symbol detection.
2
+
3
+ Static analysis to flag symbols the model may have hallucinated
4
+ (called/used but never defined or imported). Supports python (AST)
5
+ and typescript/javascript (lexical regex pass).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import builtins
12
+ import re
13
+
14
+ # Optional: pyflakes for more accurate Python undefined-name detection (F821).
15
+ # When available its scope-aware analysis is merged in (e.g. ``del x`` on an
16
+ # undefined name, which the hand-rolled walk's Store/Load contexts miss).
17
+ # When unavailable, the zero-dep fallback applies unchanged.
18
+ _HAS_PYFLAKES = False
19
+ try:
20
+ from pyflakes.checker import Checker as _PyflakesChecker
21
+ from pyflakes.messages import UndefinedName as _PyflakesUndefinedName
22
+
23
+ _HAS_PYFLAKES = True
24
+ except ImportError: # pragma: no cover
25
+ _PyflakesChecker = None
26
+ _PyflakesUndefinedName = None
27
+
28
+
29
+ def _pyflakes_undefined(source: str) -> list[tuple[str, int]] | None:
30
+ """Undefined-name findings via optional pyflakes, or None when pyflakes
31
+ is unavailable or the source cannot be compiled (caller already handled
32
+ SyntaxError separately). Returns (name, lineno) pairs."""
33
+ if not _HAS_PYFLAKES:
34
+ return None
35
+ try:
36
+ tree = compile(source, "<agentseed>", "exec", ast.PyCF_ONLY_AST)
37
+ checker = _PyflakesChecker(tree, "<agentseed>")
38
+ except (SyntaxError, ValueError, TypeError, RecursionError):
39
+ return None
40
+ out: list[tuple[str, int]] = []
41
+ for msg in checker.messages:
42
+ if isinstance(msg, _PyflakesUndefinedName) and msg.message_args:
43
+ out.append((str(msg.message_args[0]), getattr(msg, "lineno", 0)))
44
+ return out
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # TypeScript lightweight static analysis (zero-dependency regex pass).
49
+ # ---------------------------------------------------------------------------
50
+
51
+ TS_GLOBALS = {
52
+ "console",
53
+ "Math",
54
+ "JSON",
55
+ "Object",
56
+ "Array",
57
+ "String",
58
+ "Number",
59
+ "Boolean",
60
+ "Date",
61
+ "Promise",
62
+ "RegExp",
63
+ "Error",
64
+ "Set",
65
+ "Map",
66
+ "Symbol",
67
+ "BigInt",
68
+ "process",
69
+ "global",
70
+ "window",
71
+ "document",
72
+ "module",
73
+ "exports",
74
+ "require",
75
+ "fetch",
76
+ "setTimeout",
77
+ "setInterval",
78
+ "clearTimeout",
79
+ "clearInterval",
80
+ "parseInt",
81
+ "parseFloat",
82
+ "isNaN",
83
+ "isFinite",
84
+ "encodeURIComponent",
85
+ "decodeURIComponent",
86
+ "undefined",
87
+ "NaN",
88
+ "Infinity",
89
+ }
90
+
91
+ TS_KEYWORDS = {
92
+ "if",
93
+ "for",
94
+ "while",
95
+ "switch",
96
+ "catch",
97
+ "return",
98
+ "typeof",
99
+ "instanceof",
100
+ "function",
101
+ "class",
102
+ "interface",
103
+ "import",
104
+ "export",
105
+ "const",
106
+ "let",
107
+ "var",
108
+ "new",
109
+ "delete",
110
+ "in",
111
+ "of",
112
+ "await",
113
+ "yield",
114
+ "throw",
115
+ "try",
116
+ "do",
117
+ "case",
118
+ "default",
119
+ "else",
120
+ "this",
121
+ "super",
122
+ "void",
123
+ "break",
124
+ "continue",
125
+ "as",
126
+ "from",
127
+ "type",
128
+ "extends",
129
+ "implements",
130
+ "public",
131
+ "private",
132
+ "protected",
133
+ "readonly",
134
+ "static",
135
+ "async",
136
+ "keyof",
137
+ "never",
138
+ "unknown",
139
+ "any",
140
+ }
141
+
142
+ _TS_IDENT = r"[A-Za-z_$][A-Za-z0-9_$]*"
143
+
144
+
145
+ def _deduplicate(items: list[str]) -> list[str]:
146
+ """Remove duplicates while preserving order."""
147
+ seen: set[str] = set()
148
+ out: list[str] = []
149
+ for item in items:
150
+ if item not in seen:
151
+ seen.add(item)
152
+ out.append(item)
153
+ return out
154
+
155
+
156
+ def _ts_defined_symbols(source: str) -> set[str]:
157
+ """Collect identifiers defined or imported in a TS/JS source (lexical pass)."""
158
+ defined: set[str] = set(TS_GLOBALS)
159
+ # import { a, b as c } from '...'
160
+ for m in re.finditer(r"\bimport\s*\{([^}]*)\}\s*from", source):
161
+ for part in m.group(1).split(","):
162
+ p = part.strip()
163
+ if not p:
164
+ continue
165
+ alias = re.search(r"\bas\s+(" + _TS_IDENT + r")\s*$", p)
166
+ defined.add(alias.group(1) if alias else p.split(":")[-1].strip())
167
+ # import a / import * as a / import a = require()
168
+ for m in re.finditer(r"\bimport\s+(?:\*\s+as\s+)?(" + _TS_IDENT + r")\s*(?:from|=)", source):
169
+ defined.add(m.group(1))
170
+ # const { a, b: c } = require(...) / import(...) / destructuring
171
+ for m in re.finditer(
172
+ r"\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(?:require|import)\s*\(", source
173
+ ):
174
+ for part in m.group(1).split(","):
175
+ p = part.strip()
176
+ if not p:
177
+ continue
178
+ alias = re.search(r":\s*(" + _TS_IDENT + r")\s*$", p)
179
+ defined.add(alias.group(1) if alias else p.split(":")[0].strip())
180
+ # function/class/interface/type declarations
181
+ for m in re.finditer(
182
+ r"\b(?:async\s+)?(?:function|class|interface|type|enum)\s+(" + _TS_IDENT + r")",
183
+ source,
184
+ ):
185
+ defined.add(m.group(1))
186
+ # const/let/var declarations
187
+ for m in re.finditer(r"\b(?:const|let|var)\s+([^;\n]+)", source):
188
+ for part in m.group(1).split(","):
189
+ decl = re.match(r"\s*(" + _TS_IDENT + r")(?:\s*[:=]|\s*$)", part)
190
+ if decl:
191
+ defined.add(decl.group(1))
192
+
193
+ def _add_params(body: str) -> None:
194
+ for part in re.split(r",", body):
195
+ p = part.strip()
196
+ if not p:
197
+ continue
198
+ p = re.sub(r":.*$", "", p)
199
+ p = re.sub(r"^\.\.\.", "", p)
200
+ p = re.sub(r"^\{|\}$", "", p)
201
+ p = re.sub(r"^\[|\]$", "", p)
202
+ if re.fullmatch(_TS_IDENT, p):
203
+ defined.add(p)
204
+
205
+ for m in re.finditer(r"\bfunction\s+(?:" + _TS_IDENT + r"\s*)?\(([^)]*)\)", source):
206
+ _add_params(m.group(1))
207
+ for m in re.finditer(
208
+ r"\b(?:const|let|var)\s+" + _TS_IDENT + r"\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>",
209
+ source,
210
+ ):
211
+ _add_params(m.group(1))
212
+ for m in re.finditer(
213
+ r"\b(?:const|let|var)\s+" + _TS_IDENT + r"\s*=\s*(?:async\s*)?(" + _TS_IDENT + r")\s*=>",
214
+ source,
215
+ ):
216
+ _add_params(m.group(1))
217
+ return defined
218
+
219
+
220
+ def _detect_ts_undefined(source: str) -> tuple[list[str], str]:
221
+ """Lexical pass: calls/new-expressions whose callee is never defined."""
222
+ defined = _ts_defined_symbols(source)
223
+ suspects: list[str] = []
224
+ for m in re.finditer(r"\bnew\s+(" + _TS_IDENT + r")\s*\(", source):
225
+ name = m.group(1)
226
+ if name not in defined and name not in TS_KEYWORDS:
227
+ suspects.append(name)
228
+ for m in re.finditer(r"(?<![\w$.])(" + _TS_IDENT + r")\s*\(", source):
229
+ name = m.group(1)
230
+ if name not in defined and name not in TS_KEYWORDS:
231
+ suspects.append(name)
232
+ note = (
233
+ "Lexical regex pass, not a type checker; may miss destructured "
234
+ "imports or produce false positives on dynamic/global references."
235
+ )
236
+ return _deduplicate(suspects), note
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # Generic multi-language lexical verification (config-driven language
241
+ # registry). Every registered language is analyzed by the SAME engine:
242
+ # comments/strings are masked, defined symbols are collected from the
243
+ # per-language patterns, then bare calls / `new` expressions are checked.
244
+ # Adding a language is a LangSpec config change, not a code change — this
245
+ # is how verify_code scales to "any language" without a parser per language.
246
+ # ---------------------------------------------------------------------------
247
+
248
+ from dataclasses import dataclass
249
+
250
+
251
+ @dataclass(frozen=True)
252
+ class LangSpec:
253
+ name: str
254
+ aliases: tuple[str, ...] = ()
255
+ line_comments: tuple[str, ...] = ()
256
+ block_comments: tuple[tuple[str, str], ...] = ()
257
+ strings: tuple[str, ...] = ()
258
+ ident: str = r"[A-Za-z_][A-Za-z0-9_]*"
259
+ keywords: frozenset[str] = frozenset()
260
+ globals_: frozenset[str] = frozenset()
261
+ # Each pattern must have exactly ONE capturing group: the defined name(s),
262
+ # which may be comma-separated (e.g. `var a, b = ...`).
263
+ defn_patterns: tuple[str, ...] = ()
264
+ import_patterns: tuple[str, ...] = ()
265
+ # Patterns whose group(1) is a function parameter list body, e.g. "a int, b string".
266
+ param_patterns: tuple[str, ...] = ()
267
+ # How to pick the parameter NAME from one comma-part:
268
+ # "last" -> `Type name` (C/Java/C#/C++/PHP)
269
+ # "first" -> `name Type` (Go) or bare `name` (Ruby)
270
+ # "before_colon" -> `name: Type` (Rust/Kotlin/Swift)
271
+ param_mode: str = "last"
272
+ # Characters masked to spaces before analysis (e.g. PHP "$" variable sigil).
273
+ strip_chars: str = ""
274
+ # Ruby-style languages allow bare method calls without parentheses;
275
+ # standalone undefined identifiers are then flagged as calls too.
276
+ bare_calls: bool = False
277
+
278
+
279
+ _LANG_REGISTRY: dict[str, LangSpec] = {}
280
+ _LANG_ALIASES: dict[str, str] = {}
281
+
282
+
283
+ def _register_lang(spec: LangSpec) -> None:
284
+ _LANG_REGISTRY[spec.name] = spec
285
+ _LANG_ALIASES[spec.name] = spec.name
286
+ for alias in spec.aliases:
287
+ _LANG_ALIASES[alias] = spec.name
288
+
289
+
290
+ def resolve_language(name: str) -> LangSpec | None:
291
+ """Map a user-supplied language name to a registered spec (or None)."""
292
+ if not name:
293
+ return None
294
+ canonical = _LANG_ALIASES.get(name.strip().lower())
295
+ return _LANG_REGISTRY.get(canonical) if canonical else None
296
+
297
+
298
+ def _merge_spans(spans: list[tuple[int, int]]) -> list[tuple[int, int]]:
299
+ """Sort and merge overlapping [start, end) spans."""
300
+ if not spans:
301
+ return []
302
+ ordered = sorted(spans)
303
+ merged = [ordered[0]]
304
+ for s, e in ordered[1:]:
305
+ if s <= merged[-1][1]:
306
+ merged[-1] = (merged[-1][0], max(merged[-1][1], e))
307
+ else:
308
+ merged.append((s, e))
309
+ return merged
310
+
311
+
312
+ def _mask_source(source: str, spec: LangSpec) -> str:
313
+ """Blank out strings and comments so patterns only see real code."""
314
+ spans: list[tuple[int, int]] = []
315
+ for pat in spec.strings:
316
+ for m in re.finditer(pat, source):
317
+ spans.append((m.start(), m.end()))
318
+ for lc in spec.line_comments:
319
+ for m in re.finditer(re.escape(lc) + r"[^\r\n]*", source):
320
+ spans.append((m.start(), m.end()))
321
+ for start, end in spec.block_comments:
322
+ for m in re.finditer(re.escape(start) + r".*?" + re.escape(end), source, re.DOTALL):
323
+ spans.append((m.start(), m.end()))
324
+ buf = list(source)
325
+ for s, e in _merge_spans(spans):
326
+ for i in range(s, min(e, len(buf))):
327
+ if buf[i] not in "\r\n": # keep line structure for readability
328
+ buf[i] = " "
329
+ out = "".join(buf)
330
+ for ch in spec.strip_chars:
331
+ out = out.replace(ch, " ")
332
+ return out
333
+
334
+
335
+ def _collect_names(m: re.Match, spec: LangSpec) -> list[str]:
336
+ """Names captured by a defn/import pattern (group 1 may be comma-separated)."""
337
+ out: list[str] = []
338
+ for g in m.groups():
339
+ if not g:
340
+ continue
341
+ for part in g.split(","):
342
+ name = part.strip()
343
+ if name and re.fullmatch(spec.ident, name):
344
+ out.append(name)
345
+ return out
346
+
347
+
348
+ def _params_from(body: str, spec: LangSpec) -> list[str]:
349
+ """Pick parameter names out of one comma-part per the spec's param_mode."""
350
+ if spec.param_mode == "before_colon":
351
+ m = re.search(r"([A-Za-z_]\w*)\s*:", body)
352
+ return [m.group(1)] if m else []
353
+ if spec.param_mode == "first":
354
+ idents = re.findall(r"[A-Za-z_][A-Za-z0-9_]*", body)
355
+ return idents[:1]
356
+ # "last": drop default-value initializers (`int x = foo()`) first
357
+ head = body.split("=", 1)[0]
358
+ idents = re.findall(r"[A-Za-z_][A-Za-z0-9_]*", head)
359
+ return idents[-1:] if idents else []
360
+
361
+
362
+ def _generic_defined(source: str, spec: LangSpec) -> set[str]:
363
+ """Collect everything a generic language source defines or imports."""
364
+ defined = set(spec.globals_)
365
+ for pat in spec.import_patterns:
366
+ for m in re.finditer(pat, source):
367
+ defined.update(_collect_names(m, spec))
368
+ for pat in spec.defn_patterns:
369
+ for m in re.finditer(pat, source):
370
+ defined.update(_collect_names(m, spec))
371
+ for pat in spec.param_patterns:
372
+ for m in re.finditer(pat, source):
373
+ for name in _params_from(m.group(1), spec):
374
+ defined.add(name)
375
+ return defined
376
+
377
+
378
+ def _generic_detect_undefined(source: str, spec: LangSpec) -> tuple[list[str], str]:
379
+ """Lexical pass shared by every registered language (see LangSpec)."""
380
+ masked = _mask_source(source, spec)
381
+ defined = _generic_defined(masked, spec)
382
+ suspects: list[str] = []
383
+ ident = spec.ident
384
+ for m in re.finditer(r"\bnew\s+(" + ident + r")\s*\(", masked):
385
+ name = m.group(1)
386
+ if name not in defined and name not in spec.keywords:
387
+ suspects.append(name)
388
+ # lookbehind blocks attribute (`obj.m()`, `obj->m()`), path (`a::b()`),
389
+ # and `$`-prefixed calls so only bare calls are checked
390
+ for m in re.finditer(r"(?<![\w$.>:-])(" + ident + r")\s*\(", masked):
391
+ name = m.group(1)
392
+ if name not in defined and name not in spec.keywords:
393
+ suspects.append(name)
394
+ # languages that allow paren-less method calls (Ruby): flag standalone
395
+ # undefined identifiers, while excluding attribute/symbol/definition sites
396
+ if spec.bare_calls:
397
+ for m in re.finditer(r"(?<![\w$.@:>-])([A-Za-z_]\w*)(?![\w$.@:(=])", masked):
398
+ name = m.group(1)
399
+ if name not in defined and name not in spec.keywords:
400
+ suspects.append(name)
401
+ note = (
402
+ f"Generic lexical pass for {spec.name} (config-driven registry); "
403
+ "not a type checker — attribute calls, macros, and cross-file "
404
+ "symbols are not analyzed."
405
+ )
406
+ return _deduplicate(suspects), note
407
+
408
+
409
+ _register_lang(
410
+ LangSpec(
411
+ name="go",
412
+ aliases=("golang",),
413
+ line_comments=("//",),
414
+ block_comments=(("/*", "*/"),),
415
+ strings=(r'"(?:[^"\\]|\\.)*"', r"`[^`]*`", r"'(?:[^'\\]|\\.)*'"),
416
+ keywords=frozenset(
417
+ {
418
+ "package", "import", "func", "var", "const", "type", "struct",
419
+ "interface", "map", "chan", "go", "defer", "select", "range",
420
+ "return", "if", "else", "for", "switch", "case", "default",
421
+ "break", "continue", "fallthrough", "goto", "panic", "recover",
422
+ "nil", "true", "false", "iota", "len", "cap", "make", "new",
423
+ "append", "copy", "delete", "close", "complex", "real", "imag",
424
+ "min", "max", "print", "println", "error", "byte", "rune",
425
+ "int", "int8", "int16", "int32", "int64", "uint", "uint8",
426
+ "uint16", "uint32", "uint64", "uintptr", "float32", "float64",
427
+ "string", "bool", "any",
428
+ }
429
+ ),
430
+ defn_patterns=(
431
+ r"\bfunc\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)\s*\(",
432
+ r"\btype\s+([A-Za-z_]\w*)",
433
+ r"\bvar\s+([A-Za-z_]\w*)",
434
+ r"\bconst\s+([A-Za-z_]\w*)",
435
+ r"([A-Za-z_]\w*)\s*:=",
436
+ ),
437
+ param_patterns=(r"\bfunc\s+(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\(([^)]*)\)",),
438
+ param_mode="first",
439
+ )
440
+ )
441
+
442
+ _register_lang(
443
+ LangSpec(
444
+ name="rust",
445
+ aliases=("rs",),
446
+ line_comments=("//",),
447
+ block_comments=(("/*", "*/"),),
448
+ strings=(r'"(?:[^"\\]|\\.)*"', r"'(?:[^'\\]|\\.)*'"),
449
+ keywords=frozenset(
450
+ {
451
+ "fn", "let", "mut", "const", "static", "struct", "enum",
452
+ "trait", "impl", "use", "mod", "pub", "crate", "self", "Self",
453
+ "super", "if", "else", "match", "while", "loop", "for", "in",
454
+ "return", "break", "continue", "move", "ref", "type", "where",
455
+ "async", "await", "dyn", "unsafe", "extern", "as", "true",
456
+ "false", "Option", "Result", "Some", "None", "Ok", "Err",
457
+ "Vec", "String", "Box", "Rc", "Arc", "i8", "i16", "i32",
458
+ "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128",
459
+ "usize", "f32", "f64", "bool", "char", "str", "println",
460
+ "print", "eprintln", "eprint", "panic", "assert", "assert_eq",
461
+ "assert_ne", "vec", "format", "dbg", "todo", "unimplemented",
462
+ "unreachable", "macro_rules",
463
+ }
464
+ ),
465
+ defn_patterns=(
466
+ r"\bfn\s+([A-Za-z_]\w*)\s*\(",
467
+ r"\bstruct\s+([A-Za-z_]\w*)",
468
+ r"\benum\s+([A-Za-z_]\w*)",
469
+ r"\btrait\s+([A-Za-z_]\w*)",
470
+ r"\bconst\s+([A-Za-z_]\w*)\s*:",
471
+ r"\bstatic\s+(?:mut\s+)?([A-Za-z_]\w*)\s*:",
472
+ r"\blet\s+(?:mut\s+)?([A-Za-z_]\w*)\s*[=:]",
473
+ ),
474
+ import_patterns=(
475
+ r"\buse\s+(?:[A-Za-z_]\w*::)*([A-Za-z_]\w*)\s*;",
476
+ r"\buse\s+(?:[A-Za-z_]\w*::)*\{([^}]*)\}",
477
+ ),
478
+ param_patterns=(r"\bfn\s+[A-Za-z_]\w*\s*\(([^)]*)\)",),
479
+ param_mode="before_colon",
480
+ )
481
+ )
482
+
483
+ _register_lang(
484
+ LangSpec(
485
+ name="java",
486
+ aliases=(),
487
+ line_comments=("//",),
488
+ block_comments=(("/*", "*/"),),
489
+ strings=(r'"(?:[^"\\]|\\.)*"', r"'(?:[^'\\]|\\.)*'"),
490
+ keywords=frozenset(
491
+ {
492
+ "public", "private", "protected", "static", "final", "void",
493
+ "int", "long", "float", "double", "boolean", "char", "byte",
494
+ "short", "class", "interface", "enum", "extends", "implements",
495
+ "import", "package", "new", "return", "if", "else", "for",
496
+ "while", "do", "switch", "case", "default", "break",
497
+ "continue", "try", "catch", "finally", "throw", "throws",
498
+ "this", "super", "abstract", "synchronized", "native",
499
+ "transient", "volatile", "instanceof", "true", "false", "null",
500
+ "String", "System", "Math", "Object", "Integer", "Long",
501
+ "Double", "Boolean", "Character", "List", "Map", "Set",
502
+ "ArrayList", "HashMap", "HashSet", "Optional", "StringBuilder",
503
+ "Thread", "Exception", "RuntimeException", "Iterable",
504
+ "Collection", "Stream",
505
+ }
506
+ ),
507
+ defn_patterns=(
508
+ r"\bclass\s+([A-Za-z_]\w*)",
509
+ r"\binterface\s+([A-Za-z_]\w*)",
510
+ r"\benum\s+([A-Za-z_]\w*)",
511
+ r"(?:(?:public|private|protected|static|final|abstract|synchronized|native)\s+)*(?:[A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\(",
512
+ r"(?:(?:public|private|protected|static|final|volatile|transient)\s+)*(?:[A-Za-z_]\w*)(?:\[\])?\s+([A-Za-z_]\w*)\s*(?:=|;)",
513
+ ),
514
+ import_patterns=(
515
+ r"\bimport\s+(?:static\s+)?(?:[A-Za-z_]\w*\.)*([A-Za-z_]\w*)\s*;",
516
+ ),
517
+ param_patterns=(
518
+ r"(?:[A-Za-z_]\w*)(?:\[\])?\s+[A-Za-z_]\w*\s*\(([^)]*)\)",
519
+ ),
520
+ param_mode="last",
521
+ )
522
+ )
523
+
524
+ _register_lang(
525
+ LangSpec(
526
+ name="c",
527
+ aliases=(),
528
+ line_comments=("//",),
529
+ block_comments=(("/*", "*/"),),
530
+ strings=(r'"(?:[^"\\]|\\.)*"', r"'(?:[^'\\]|\\.)*'"),
531
+ keywords=frozenset(
532
+ {
533
+ "auto", "break", "case", "char", "const", "continue",
534
+ "default", "do", "double", "else", "enum", "extern", "float",
535
+ "for", "goto", "if", "inline", "int", "long", "register",
536
+ "restrict", "return", "short", "signed", "sizeof", "static",
537
+ "struct", "switch", "typedef", "union", "unsigned", "void",
538
+ "volatile", "while", "NULL", "size_t", "printf", "scanf",
539
+ "malloc", "calloc", "free", "realloc", "memcpy", "memset",
540
+ "strlen", "strcmp", "strcpy", "strncpy", "strcat", "sprintf",
541
+ "snprintf", "fopen", "fclose", "fprintf", "fgets", "fputs",
542
+ "exit", "assert", "getchar", "putchar", "puts",
543
+ }
544
+ ),
545
+ defn_patterns=(
546
+ r"\b(?:void|int|char|long|float|double|short|unsigned|signed|size_t|ssize_t|[A-Za-z_]\w*_t)\s+([A-Za-z_]\w*)\s*\(",
547
+ r"\bstruct\s+([A-Za-z_]\w*)",
548
+ r"\btypedef\s+(?:[A-Za-z_]\w*\s+)+([A-Za-z_]\w*)\s*;",
549
+ r"#define\s+([A-Za-z_]\w*)",
550
+ ),
551
+ import_patterns=(r"#include\s*[<\"]([A-Za-z_]\w*)[>\"]",),
552
+ param_patterns=(
553
+ r"\b(?:void|int|char|long|float|double|short|unsigned|signed|size_t|ssize_t|[A-Za-z_]\w*_t)\s+[A-Za-z_]\w*\s*\(([^)]*)\)",
554
+ ),
555
+ param_mode="last",
556
+ )
557
+ )
558
+
559
+ _register_lang(
560
+ LangSpec(
561
+ name="cpp",
562
+ aliases=("c++", "cc", "cxx"),
563
+ line_comments=("//",),
564
+ block_comments=(("/*", "*/"),),
565
+ strings=(r'"(?:[^"\\]|\\.)*"', r"'(?:[^'\\]|\\.)*'"),
566
+ keywords=frozenset(
567
+ {
568
+ "auto", "break", "case", "char", "class", "const", "constexpr",
569
+ "continue", "default", "delete", "do", "double", "else",
570
+ "enum", "explicit", "extern", "float", "for", "friend", "goto",
571
+ "if", "inline", "int", "long", "namespace", "new", "noexcept",
572
+ "nullptr", "operator", "private", "protected", "public",
573
+ "register", "return", "short", "signed", "sizeof", "static",
574
+ "static_cast", "dynamic_cast", "reinterpret_cast", "const_cast",
575
+ "struct", "switch", "template", "typename", "typedef", "union",
576
+ "unsigned", "using", "virtual", "override", "void", "volatile",
577
+ "while", "true", "false", "this", "NULL", "nullptr", "size_t",
578
+ "std", "cout", "cin", "endl", "vector", "string", "map",
579
+ "set", "shared_ptr", "unique_ptr", "make_shared", "make_unique",
580
+ "printf", "malloc", "calloc", "free", "realloc", "memcpy",
581
+ "memset", "strlen", "strcmp", "strcpy", "assert",
582
+ }
583
+ ),
584
+ defn_patterns=(
585
+ r"\b(?:[A-Za-z_]\w*)(?:::\s*[A-Za-z_]\w*)?\s+([A-Za-z_]\w*)\s*\(",
586
+ r"\b(?:[A-Za-z_]\w*)(?:::\s*[A-Za-z_]\w*)?\s+([A-Za-z_]\w*)\s*(?:=|;)",
587
+ r"\bclass\s+([A-Za-z_]\w*)",
588
+ r"\bstruct\s+([A-Za-z_]\w*)",
589
+ r"\bnamespace\s+([A-Za-z_]\w*)",
590
+ r"\btypedef\s+(?:[A-Za-z_]\w*\s+)+([A-Za-z_]\w*)\s*;",
591
+ r"#define\s+([A-Za-z_]\w*)",
592
+ ),
593
+ import_patterns=(r"#include\s*[<\"]([A-Za-z_]\w*)[>\"]",),
594
+ param_patterns=(
595
+ r"\b(?:void|int|char|long|float|double|short|unsigned|signed|size_t|ssize_t|bool|[A-Za-z_]\w*_t)\s+[A-Za-z_]\w*\s*\(([^)]*)\)",
596
+ ),
597
+ param_mode="last",
598
+ )
599
+ )
600
+
601
+ _register_lang(
602
+ LangSpec(
603
+ name="csharp",
604
+ aliases=("cs", "c#"),
605
+ line_comments=("//",),
606
+ block_comments=(("/*", "*/"),),
607
+ strings=(r'@"(?:[^"]|"")*"', r'"(?:[^"\\]|\\.)*"', r"'(?:[^'\\]|\\.)*'"),
608
+ keywords=frozenset(
609
+ {
610
+ "using", "namespace", "class", "interface", "enum", "struct",
611
+ "public", "private", "protected", "internal", "static",
612
+ "readonly", "const", "void", "int", "string", "bool",
613
+ "double", "float", "long", "short", "byte", "char", "object",
614
+ "var", "new", "return", "if", "else", "for", "foreach",
615
+ "while", "do", "switch", "case", "default", "break",
616
+ "continue", "try", "catch", "finally", "throw", "this",
617
+ "base", "async", "await", "task", "delegate", "event",
618
+ "override", "virtual", "abstract", "sealed", "partial",
619
+ "get", "set", "null", "true", "false", "Console", "String",
620
+ "Math", "List", "Dictionary", "IEnumerable", "Task",
621
+ "Exception", "dynamic", "lock",
622
+ }
623
+ ),
624
+ defn_patterns=(
625
+ r"(?:(?:public|private|protected|internal|static|readonly|virtual|override|abstract|async|sealed|partial)\s+)*(?:void|int|string|bool|double|float|long|short|byte|char|object|var|[A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\(",
626
+ r"\bclass\s+([A-Za-z_]\w*)",
627
+ r"\binterface\s+([A-Za-z_]\w*)",
628
+ r"\benum\s+([A-Za-z_]\w*)",
629
+ r"\bstruct\s+([A-Za-z_]\w*)",
630
+ r"\bnamespace\s+([A-Za-z_]\w*)",
631
+ r"(?:(?:public|private|protected|internal|static|readonly|const)\s+)*(?:int|string|bool|double|float|long|short|byte|char|object|var|[A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*(?:=|;)",
632
+ ),
633
+ import_patterns=(r"\busing\s+(?:[A-Za-z_]\w*\.)*([A-Za-z_]\w*)\s*;",),
634
+ param_patterns=(
635
+ r"(?:(?:public|private|protected|internal|static|readonly|virtual|override|abstract|async|sealed|partial)\s+)*(?:void|int|string|bool|double|float|long|short|byte|char|object|var|[A-Za-z_]\w*)\s+[A-Za-z_]\w*\s*\(([^)]*)\)",
636
+ ),
637
+ param_mode="last",
638
+ )
639
+ )
640
+
641
+ _register_lang(
642
+ LangSpec(
643
+ name="php",
644
+ aliases=(),
645
+ line_comments=("//", "#"),
646
+ block_comments=(("/*", "*/"),),
647
+ strings=(r'"(?:[^"\\]|\\.)*"', r"'(?:[^'\\]|\\.)*'", r"`[^`]*`"),
648
+ keywords=frozenset(
649
+ {
650
+ "function", "class", "interface", "trait", "enum", "public",
651
+ "private", "protected", "static", "final", "abstract",
652
+ "extends", "implements", "use", "namespace", "new", "return",
653
+ "if", "else", "elseif", "for", "foreach", "while", "do",
654
+ "switch", "case", "default", "break", "continue", "try",
655
+ "catch", "finally", "throw", "this", "self", "parent", "echo",
656
+ "print", "isset", "empty", "unset", "die", "exit", "require",
657
+ "include", "require_once", "include_once", "global", "const",
658
+ "var", "true", "false", "null", "array", "list", "match",
659
+ "fn", "and", "or", "xor", "not", "as", "instanceof",
660
+ "yield", "from", "clone",
661
+ }
662
+ ),
663
+ defn_patterns=(
664
+ r"\bfunction\s+&?\s*([A-Za-z_]\w*)\s*\(",
665
+ r"\bclass\s+([A-Za-z_]\w*)",
666
+ r"\binterface\s+([A-Za-z_]\w*)",
667
+ r"\btrait\s+([A-Za-z_]\w*)",
668
+ r"\benum\s+([A-Za-z_]\w*)",
669
+ r"\bnamespace\s+([A-Za-z_]\w*)",
670
+ r"(?:^|[;\s{}])([A-Za-z_]\w*)\s*=",
671
+ ),
672
+ import_patterns=(r"\buse\s+(?:[A-Za-z_]\w*\\)*([A-Za-z_]\w*)\s*;",),
673
+ param_patterns=(r"\bfunction\s+(?:&?\s*[A-Za-z_]\w*\s*)?\(([^)]*)\)",),
674
+ param_mode="last",
675
+ strip_chars="$",
676
+ )
677
+ )
678
+
679
+ _register_lang(
680
+ LangSpec(
681
+ name="ruby",
682
+ aliases=("rb",),
683
+ line_comments=("#",),
684
+ block_comments=(("=begin", "=end"),),
685
+ strings=(r'"(?:[^"\\]|\\.)*"', r"'(?:[^'\\]|\\.)*'", r"`[^`]*`"),
686
+ keywords=frozenset(
687
+ {
688
+ "def", "class", "module", "end", "if", "elsif", "else",
689
+ "unless", "while", "until", "for", "in", "do", "case",
690
+ "when", "then", "return", "break", "next", "redo", "retry",
691
+ "yield", "raise", "rescue", "ensure", "begin", "require",
692
+ "include", "extend", "attr_accessor", "attr_reader",
693
+ "attr_writer", "new", "self", "super", "true", "false",
694
+ "nil", "and", "or", "not", "lambda", "proc", "puts", "print",
695
+ "p", "gets", "require_relative", "alias", "undef", "defined",
696
+ "loop",
697
+ }
698
+ ),
699
+ defn_patterns=(
700
+ r"\bdef\s+(?:self\.)?([A-Za-z_]\w*)",
701
+ r"\bclass\s+([A-Za-z_]\w*)",
702
+ r"\bmodule\s+([A-Za-z_]\w*)",
703
+ ),
704
+ import_patterns=(r"\brequire\s+['\"]([A-Za-z_]\w*)['\"]",),
705
+ param_patterns=(r"\bdef\s+(?:self\.)?[A-Za-z_]\w*\s*\(([^)]*)\)",),
706
+ param_mode="first",
707
+ bare_calls=True,
708
+ )
709
+ )
710
+
711
+ _register_lang(
712
+ LangSpec(
713
+ name="kotlin",
714
+ aliases=("kt",),
715
+ line_comments=("//",),
716
+ block_comments=(("/*", "*/"),),
717
+ strings=(r'"(?:[^"\\]|\\.)*"', r'"""[\s\S]*?"""', r"'(?:[^'\\]|\\.)*'"),
718
+ keywords=frozenset(
719
+ {
720
+ "fun", "val", "var", "class", "interface", "object", "enum",
721
+ "data", "sealed", "abstract", "open", "override", "internal",
722
+ "public", "private", "protected", "companion", "init",
723
+ "constructor", "import", "package", "if", "else", "when",
724
+ "for", "while", "do", "return", "break", "continue", "try",
725
+ "catch", "finally", "throw", "this", "super", "null", "true",
726
+ "false", "by", "as", "is", "in", "out", "reified", "inline",
727
+ "noinline", "crossinline", "suspend", "operator", "infix",
728
+ "tailrec", "lateinit", "lazy", "List", "MutableList", "Map",
729
+ "Set", "String", "Int", "Double", "Boolean", "Long", "Array",
730
+ "Any", "Unit", "Nothing", "println", "print", "listOf",
731
+ "mapOf", "setOf", "mutableListOf", "mutableMapOf",
732
+ }
733
+ ),
734
+ defn_patterns=(
735
+ r"\bfun\s+(?:[A-Za-z_]\w*\.)?([A-Za-z_]\w*)\s*\(",
736
+ r"\bclass\s+([A-Za-z_]\w*)",
737
+ r"\binterface\s+([A-Za-z_]\w*)",
738
+ r"\benum\s+(?:class\s+)?([A-Za-z_]\w*)",
739
+ r"\bobject\s+([A-Za-z_]\w*)",
740
+ r"\bval\s+([A-Za-z_]\w*)\s*[:=]",
741
+ r"\bvar\s+([A-Za-z_]\w*)\s*[:=]",
742
+ r"\bconst\s+val\s+([A-Za-z_]\w*)",
743
+ ),
744
+ import_patterns=(r"\bimport\s+(?:[A-Za-z_]\w*\.)*([A-Za-z_]\w*)",),
745
+ param_patterns=(r"\bfun\s+(?:[A-Za-z_]\w*\.)?[A-Za-z_]\w*\s*\(([^)]*)\)",),
746
+ param_mode="before_colon",
747
+ )
748
+ )
749
+
750
+ _register_lang(
751
+ LangSpec(
752
+ name="swift",
753
+ aliases=(),
754
+ line_comments=("//",),
755
+ block_comments=(("/*", "*/"),),
756
+ strings=(r'"(?:[^"\\]|\\.)*"', r'"""[\s\S]*?"""'),
757
+ keywords=frozenset(
758
+ {
759
+ "func", "class", "struct", "enum", "protocol", "extension",
760
+ "let", "var", "import", "return", "if", "else", "guard",
761
+ "for", "while", "repeat", "switch", "case", "default",
762
+ "break", "continue", "try", "catch", "throw", "throws", "do",
763
+ "in", "as", "is", "nil", "true", "false", "self", "super",
764
+ "init", "deinit", "public", "private", "internal",
765
+ "fileprivate", "open", "static", "final", "override", "lazy",
766
+ "weak", "unowned", "where", "String", "Int", "Double",
767
+ "Bool", "Array", "Dictionary", "Set", "Optional", "print",
768
+ "count", "map", "filter", "reduce", "sorted", "first",
769
+ "last", "isEmpty",
770
+ }
771
+ ),
772
+ defn_patterns=(
773
+ r"\bfunc\s+([A-Za-z_]\w*)\s*\(",
774
+ r"\bclass\s+([A-Za-z_]\w*)",
775
+ r"\bstruct\s+([A-Za-z_]\w*)",
776
+ r"\benum\s+([A-Za-z_]\w*)",
777
+ r"\bprotocol\s+([A-Za-z_]\w*)",
778
+ r"\bextension\s+([A-Za-z_]\w*)",
779
+ r"\blet\s+([A-Za-z_]\w*)\s*[:=]",
780
+ r"\bvar\s+([A-Za-z_]\w*)\s*[:=]",
781
+ r"\bstatic\s+let\s+([A-Za-z_]\w*)",
782
+ ),
783
+ import_patterns=(r"\bimport\s+(?:[A-Za-z_]\w*\.)*([A-Za-z_]\w*)",),
784
+ param_patterns=(r"\bfunc\s+[A-Za-z_]\w*\s*\(([^)]*)\)",),
785
+ param_mode="before_colon",
786
+ )
787
+ )
788
+
789
+ # Public surface for CLI/schema: canonical python + ts + every registered alias.
790
+ SUPPORTED_LANGUAGES: tuple[str, ...] = (
791
+ "python",
792
+ "typescript",
793
+ "javascript",
794
+ "ts",
795
+ "js",
796
+ ) + tuple(sorted(_LANG_ALIASES))
797
+
798
+
799
+ # Match-statement node types exist only on Python 3.10+; resolve once so
800
+ # older interpreters skip these branches instead of raising AttributeError.
801
+ _MATCH_AS = getattr(ast, "MatchAs", None)
802
+ _MATCH_STAR = getattr(ast, "MatchStar", None)
803
+ _MATCH_MAPPING = getattr(ast, "MatchMapping", None)
804
+
805
+
806
+ def _python_defined_symbols(tree: ast.AST) -> set[str]:
807
+ """Names defined or imported by a parsed Python module (for ``defined_symbols``)."""
808
+ defined: set[str] = set(dir(builtins))
809
+ defined |= {
810
+ "__file__",
811
+ "__doc__",
812
+ "__package__",
813
+ "__spec__",
814
+ "__loader__",
815
+ "__main__",
816
+ "__dict__",
817
+ "__builtins__",
818
+ "__cached__",
819
+ }
820
+ imported: set[str] = set()
821
+
822
+ for node in ast.walk(tree):
823
+ if isinstance(node, ast.Import):
824
+ for alias in node.names:
825
+ imported.add(alias.asname or alias.name.split(".")[0])
826
+ elif isinstance(node, ast.ImportFrom):
827
+ for alias in node.names:
828
+ imported.add(alias.asname or alias.name)
829
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
830
+ defined.add(node.name)
831
+ elif isinstance(node, ast.arg):
832
+ defined.add(node.arg)
833
+ elif isinstance(node, ast.Global) or isinstance(node, ast.Nonlocal):
834
+ defined.update(node.names)
835
+ elif isinstance(node, ast.ExceptHandler) and node.name:
836
+ defined.add(node.name)
837
+ elif _MATCH_AS is not None and isinstance(node, _MATCH_AS) and node.name:
838
+ defined.add(node.name) # case pattern capture, py3.10+
839
+ elif _MATCH_STAR is not None and isinstance(node, _MATCH_STAR) and node.name:
840
+ defined.add(node.name)
841
+ elif _MATCH_MAPPING is not None and isinstance(node, _MATCH_MAPPING) and node.rest:
842
+ defined.add(node.rest)
843
+
844
+ for node in ast.walk(tree):
845
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
846
+ defined.add(node.id)
847
+
848
+ defined |= imported
849
+ return defined
850
+
851
+
852
+ def defined_symbols(source: str, language: str = "python") -> list[str]:
853
+ """Names the source defines or imports (sorted). Used by ``check_contract``
854
+ to verify that ``requires`` symbols actually exist in the module.
855
+
856
+ Same language support as ``detect_undefined_symbols``: Python (AST),
857
+ TypeScript/JavaScript (lexical), and the config-driven generic registry.
858
+ Returns [] for unsupported languages or unparseable Python.
859
+ """
860
+ lang = (language or "python").strip().lower()
861
+ if lang in ("typescript", "ts", "javascript", "js"):
862
+ return sorted(_ts_defined_symbols(source))
863
+ spec = resolve_language(lang)
864
+ if spec is not None:
865
+ return sorted(_generic_defined(_mask_source(source, spec), spec))
866
+ if lang == "python":
867
+ try:
868
+ return sorted(_python_defined_symbols(ast.parse(source)))
869
+ except SyntaxError:
870
+ return []
871
+ return []
872
+
873
+
874
+ def detect_undefined_symbols(
875
+ source: str, language: str = "python", suppress: list[str] | None = None
876
+ ) -> dict:
877
+ """Parse source and return symbols that look hallucinated
878
+ (used/called but never defined or imported).
879
+
880
+ Supports: python (AST), typescript/javascript (lexical), plus a
881
+ config-driven generic lexical pass for go, rust, java, c, c++, c#,
882
+ php, ruby, kotlin, swift — extensible via the language registry.
883
+ ``suppress`` removes exact symbol names from the findings (config:
884
+ ``suppress_symbols``); suppressed names are reported separately so the
885
+ omission stays visible.
886
+
887
+ Returns:
888
+ {"language": ..., "suspects": ["foo", "Bar"],
889
+ "suspects_detail": [{"name": "foo", "line": 12}, ...],
890
+ "suppressed": ["bar"], "note": "..."}
891
+ """
892
+ if language in ("typescript", "ts", "javascript", "js"):
893
+ suspects, note = _detect_ts_undefined(source)
894
+ return _apply_suppress({"language": language, "suspects": suspects, "note": note}, suppress)
895
+ spec = resolve_language(language)
896
+ if spec is not None:
897
+ suspects, note = _generic_detect_undefined(source, spec)
898
+ return _apply_suppress(
899
+ {"language": spec.name, "suspects": suspects, "note": note}, suppress
900
+ )
901
+ if language != "python":
902
+ return {
903
+ "language": language,
904
+ "suspects": [],
905
+ "note": "Unsupported language. Supported: " + ", ".join(SUPPORTED_LANGUAGES) + ".",
906
+ }
907
+
908
+ try:
909
+ tree = ast.parse(source)
910
+ except SyntaxError as exc:
911
+ return {
912
+ "language": "python",
913
+ "suspects": [],
914
+ "note": f"Cannot parse (syntax error): {exc}",
915
+ }
916
+
917
+ defined = _python_defined_symbols(tree)
918
+
919
+ seen: set[str] = set()
920
+ suspects: list[str] = []
921
+ detail: list[dict] = []
922
+ for node in ast.walk(tree):
923
+ name = None
924
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
925
+ name = node.func.id
926
+ elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
927
+ name = node.id
928
+ elif isinstance(node, ast.Delete):
929
+ # `del x` on a never-defined name raises NameError at runtime;
930
+ # the Load/Call contexts above never visit Del targets, so this
931
+ # class needs its own check (works without pyflakes too).
932
+ for target in node.targets:
933
+ if (
934
+ isinstance(target, ast.Name)
935
+ and target.id not in defined
936
+ and target.id not in seen
937
+ ):
938
+ seen.add(target.id)
939
+ suspects.append(target.id)
940
+ detail.append({"name": target.id, "line": getattr(target, "lineno", 0)})
941
+ if name is not None and name not in defined and name not in seen:
942
+ seen.add(name)
943
+ suspects.append(name)
944
+ detail.append({"name": name, "line": getattr(node, "lineno", 0)})
945
+
946
+ pyfindings = _pyflakes_undefined(source)
947
+ note = (
948
+ "Static scope analysis only; no runtime; attribute calls "
949
+ "(foo.bar) are not expanded and may cause false negatives."
950
+ )
951
+ if pyfindings is not None:
952
+ for name, line in pyfindings:
953
+ if name in defined or name in seen:
954
+ continue
955
+ seen.add(name)
956
+ suspects.append(name)
957
+ detail.append({"name": name, "line": line})
958
+ note += " Merged with pyflakes F821 scope analysis."
959
+
960
+ return _apply_suppress(
961
+ {
962
+ "language": "python",
963
+ "suspects": suspects,
964
+ "suspects_detail": detail,
965
+ "note": note,
966
+ },
967
+ suppress,
968
+ )
969
+
970
+
971
+ def _apply_suppress(result: dict, suppress: list[str] | None) -> dict:
972
+ """Filter suppressed symbol names out of a detection result (visible)."""
973
+ if not suppress:
974
+ result.setdefault("suppressed", [])
975
+ return result
976
+ drop = {s for s in suppress if isinstance(s, str)}
977
+ kept = [s for s in result["suspects"] if s not in drop]
978
+ removed = [s for s in result["suspects"] if s in drop]
979
+ out = dict(result)
980
+ out["suspects"] = kept
981
+ out["suppressed"] = removed
982
+ if "suspects_detail" in out:
983
+ out["suspects_detail"] = [d for d in out["suspects_detail"] if d["name"] not in drop]
984
+ return out