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.
- package/CHANGELOG.md +178 -0
- package/LICENSE +202 -0
- package/README.ja.md +320 -0
- package/README.md +318 -0
- package/README.zh.md +306 -0
- package/bin/cli.js +37 -0
- package/mcp.json +12 -0
- package/package.json +30 -0
- package/plugin.json +22 -0
- package/server/.agentseed/verification-log.jsonl +2 -0
- package/server/__pycache__/guard_cli.cpython-313.pyc +0 -0
- package/server/__pycache__/guard_engine.cpython-313.pyc +0 -0
- package/server/__pycache__/test_cli.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_cli.cpython-313.pyc +0 -0
- package/server/__pycache__/test_features.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_features.cpython-313.pyc +0 -0
- package/server/__pycache__/test_guard.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_guard.cpython-313.pyc +0 -0
- package/server/__pycache__/test_hook.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_hook.cpython-313.pyc +0 -0
- package/server/__pycache__/test_manifests.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_manifests.cpython-313.pyc +0 -0
- package/server/__pycache__/test_server.cpython-313-pytest-9.1.1.pyc +0 -0
- package/server/__pycache__/test_server.cpython-313.pyc +0 -0
- package/server/engine/__init__.py +64 -0
- package/server/engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/audit.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/config.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/hallucination.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/imports.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/plugin.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/sandbox.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/schema.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/symbols.cpython-313.pyc +0 -0
- package/server/engine/__pycache__/version.cpython-313.pyc +0 -0
- package/server/engine/audit.py +84 -0
- package/server/engine/config.py +131 -0
- package/server/engine/hallucination.py +254 -0
- package/server/engine/imports.py +136 -0
- package/server/engine/plugin.py +367 -0
- package/server/engine/sandbox.py +287 -0
- package/server/engine/schema.py +193 -0
- package/server/engine/symbols.py +984 -0
- package/server/engine/version.py +17 -0
- package/server/guard_cli.py +455 -0
- package/server/guard_engine.py +111 -0
- package/server/guard_hook.py +404 -0
- package/server/guard_server.py +472 -0
- package/server/requirements.txt +7 -0
- package/server/test_cli.py +132 -0
- package/server/test_features.py +426 -0
- package/server/test_guard.py +828 -0
- package/server/test_hook.py +331 -0
- package/server/test_manifests.py +70 -0
- package/server/test_server.py +247 -0
- package/skills/verify-before-code/SKILL.ja.md +116 -0
- package/skills/verify-before-code/SKILL.md +140 -0
- package/skills/verify-before-code/SKILL.zh.md +117 -0
- package/skills/verify-before-code/references/DEFAULT-NORMS.md +52 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.ja.md +121 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.md +166 -0
- package/skills/verify-before-code/references/HALLUCINATION-PATTERNS.zh.md +145 -0
- package/skills/verify-before-code/references/PROMPT-POOL.ja.md +248 -0
- package/skills/verify-before-code/references/PROMPT-POOL.md +282 -0
- package/skills/verify-before-code/references/PROMPT-POOL.zh.md +252 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.ja.md +61 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.md +66 -0
- package/skills/verify-before-code/references/SDD-CONTRACT.zh.md +58 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.ja.md +62 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.md +62 -0
- package/skills/verify-before-code/references/VENDOR-SOLUTIONS.zh.md +54 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.ja.md +68 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.md +73 -0
- package/skills/verify-before-code/references/VERIFICATION-CHECKLIST.zh.md +68 -0
- package/skills/verify-before-code/scripts/check.ps1 +52 -0
- 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
|