@allansantos-dev/smart-tool 0.9.0 → 0.9.2
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 +50 -23
- package/NOTICE +32 -32
- package/README.md +117 -116
- package/atomic_io.py +94 -94
- package/bin/smart-tool.mjs +5 -1
- package/client_hooks.py +192 -192
- package/code_graph_java.mjs +49 -49
- package/code_graph_js.cjs +135 -135
- package/duplicates.py +313 -313
- package/embedding_cache.py +99 -99
- package/endpoint_sync.py +179 -179
- package/gateway.py +220 -220
- package/hook_decision.py +96 -96
- package/index_inventory.py +75 -75
- package/indexer.py +878 -878
- package/install.py +113 -11
- package/install_runtime.py +101 -35
- package/integration_metrics.py +82 -82
- package/local_embedder.py +172 -172
- package/model_defaults.py +70 -70
- package/package.json +35 -35
- package/project_insights_ui.py +38 -38
- package/project_monitor.py +204 -204
- package/project_store.py +220 -220
- package/projects_ui.py +146 -146
- package/requirements.txt +8 -8
- package/research_cache.py +171 -171
- package/research_metrics.py +110 -110
- package/router.py +582 -582
- package/secret_store.py +121 -121
- package/setup_ui.py +1011 -986
- package/smart_tool_daemon.py +3604 -3604
- package/version.py +1 -1
- package/web_adapters/browser/fetch_camoufox.py +69 -0
- package/web_adapters/node/ddg-search.mjs +89 -89
- package/web_adapters/node/open-websearch-adapter.mjs +77 -77
- package/web_adapters/node/package.json +12 -12
- package/web_adapters/node/wikipedia-search.mjs +44 -44
- package/web_search_adapters.py +193 -193
- package/web_search_external.py +288 -288
- package/web_search_health.py +88 -88
package/client_hooks.py
CHANGED
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
import secrets
|
|
4
|
-
import shutil
|
|
5
|
-
import sys
|
|
6
|
-
import time
|
|
7
|
-
|
|
8
|
-
import daemon_launcher
|
|
9
|
-
import endpoint_sync
|
|
10
|
-
|
|
11
|
-
HOOK_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hooks", "pretool_router.py")
|
|
12
|
-
MARKER = "pretool_router.py"
|
|
13
|
-
HTTP_PATH = "/hooks/pretool"
|
|
14
|
-
HTTP_TIMEOUT_S = 15
|
|
15
|
-
HOME = os.path.expanduser("~")
|
|
16
|
-
PREVIEW_TTL_S = 600
|
|
17
|
-
CLIENTS = {
|
|
18
|
-
"claude": {"label": "Claude Code", "names": {"claude-code"}, "files": [os.path.join(endpoint_sync.claude_dir(), "settings.json")],
|
|
19
|
-
"docs": "https://code.claude.com/docs/en/hooks", "event": "PreToolUse", "matcher": "Grep|Glob|Read|Bash|WebSearch|WebFetch", "http": True},
|
|
20
|
-
"codex": {"label": "Codex", "prefix": "codex", "files": [os.path.join(HOME, ".codex", "hooks.json"), os.path.join(HOME, ".codex", "config.toml")],
|
|
21
|
-
"docs": "https://learn.chatgpt.com/docs/hooks", "event": "PreToolUse", "matcher": "Bash",
|
|
22
|
-
"after_install": "Codex only runs new hooks after the user approves the definition in /hooks."},
|
|
23
|
-
"cursor": {"label": "Cursor", "prefix": "cursor", "files": [os.path.join(HOME, ".cursor", "hooks.json")],
|
|
24
|
-
"docs": "https://cursor.com/docs/agent/hooks", "event": "preToolUse", "matcher": "Shell|Read|Grep",
|
|
25
|
-
"blocked": "In Cursor, `allow` approves the tool without the user and empty output blocks: there is no documented neutral "
|
|
26
|
-
"response, so the hook is not installed automatically."},
|
|
27
|
-
}
|
|
28
|
-
_PREVIEWS = {}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def identify(client_info):
|
|
32
|
-
name = str((client_info or {}).get("name") or "").casefold()
|
|
33
|
-
return next((key for key, spec in CLIENTS.items()
|
|
34
|
-
if name in spec.get("names", ()) or (spec.get("prefix") and name.startswith(spec["prefix"]))), None)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _our_hook(hook):
|
|
38
|
-
return isinstance(hook, dict) and (MARKER in str(hook.get("command", "")) or HTTP_PATH in str(hook.get("url", "")))
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _ours(group):
|
|
42
|
-
return isinstance(group, dict) and isinstance(group.get("hooks"), list) and any(map(_our_hook, group["hooks"]))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _without_ours(groups):
|
|
46
|
-
"""Drops only our hook entries; a group shared with other hooks keeps them and disappears only when emptied."""
|
|
47
|
-
kept = []
|
|
48
|
-
for group in groups:
|
|
49
|
-
if not _ours(group):
|
|
50
|
-
kept.append(group)
|
|
51
|
-
continue
|
|
52
|
-
others = [hook for hook in group["hooks"] if not _our_hook(hook)]
|
|
53
|
-
if others:
|
|
54
|
-
kept.append({**group, "hooks": others})
|
|
55
|
-
return kept
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def installed(client):
|
|
59
|
-
"""Our hook is present with the current matcher; an entry with an older matcher counts as missing."""
|
|
60
|
-
spec = CLIENTS[client]
|
|
61
|
-
for path in spec["files"]:
|
|
62
|
-
try:
|
|
63
|
-
with open(path, encoding="utf-8") as stream:
|
|
64
|
-
text = stream.read()
|
|
65
|
-
except FileNotFoundError:
|
|
66
|
-
continue
|
|
67
|
-
if MARKER not in text and HTTP_PATH not in text:
|
|
68
|
-
continue
|
|
69
|
-
if not path.endswith(".json"):
|
|
70
|
-
return True
|
|
71
|
-
data = json.loads(text)
|
|
72
|
-
hooks = data.get("hooks") if isinstance(data, dict) else None
|
|
73
|
-
groups = hooks.get(spec["event"]) if isinstance(hooks, dict) else None
|
|
74
|
-
expected = _handler(client)
|
|
75
|
-
return isinstance(groups, list) and any(
|
|
76
|
-
group.get("matcher") == spec["matcher"] and expected in group["hooks"] for group in groups if _ours(group))
|
|
77
|
-
return False
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def status(client_info):
|
|
81
|
-
client = identify(client_info)
|
|
82
|
-
if client is None:
|
|
83
|
-
return {"client": None, "name": (client_info or {}).get("name"), "installed": None, "error": None}
|
|
84
|
-
spec = CLIENTS[client]
|
|
85
|
-
try:
|
|
86
|
-
state, error = installed(client), None
|
|
87
|
-
except (OSError, ValueError) as exc:
|
|
88
|
-
state, error = None, f"{type(exc).__name__}: {exc}"[:200]
|
|
89
|
-
return {"client": client, "name": client_info.get("name"), "installed": state, "error": error,
|
|
90
|
-
"files": spec["files"], "docs": spec["docs"]}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def notice(state):
|
|
94
|
-
if state["client"] is None:
|
|
95
|
-
return (f"Unrecognized MCP client ({state['name'] or 'no clientInfo'}): Smart Tool does not check the hook "
|
|
96
|
-
"that redirects Grep/Read to smart_search and WebSearch/WebFetch to web_search/web_fetch in this client.")
|
|
97
|
-
spec = CLIENTS[state["client"]]
|
|
98
|
-
if state["error"]:
|
|
99
|
-
return f"Could not check the Smart Tool hook in {spec['files'][0]}: {state['error']}"
|
|
100
|
-
if state["installed"]:
|
|
101
|
-
return None
|
|
102
|
-
action = spec.get("blocked") or ("If the user asks, use project_manage action=install_hook_preview, show the "
|
|
103
|
-
"change and confirm with action=install_hook (makes a backup).")
|
|
104
|
-
return (f"Smart Tool hook missing or outdated in {spec['label']} ({spec['files'][0]}): Grep/Read/Bash are not "
|
|
105
|
-
f"redirected to smart_search, nor WebSearch/WebFetch to web_search/web_fetch. {action} Docs: {spec['docs']}")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _shell_safe(path):
|
|
109
|
-
"""Unquoted path that runs the same in cmd, PowerShell and bash (the Codex docs don't name the shell): forward
|
|
110
|
-
slashes (bash eats backslashes), no spaces (8.3 short name), no quotes (PowerShell reads a quoted head as a string)."""
|
|
111
|
-
if " " in path and sys.platform == "win32":
|
|
112
|
-
import ctypes
|
|
113
|
-
buffer = ctypes.create_unicode_buffer(1024)
|
|
114
|
-
if ctypes.windll.kernel32.GetShortPathNameW(path, buffer, len(buffer)):
|
|
115
|
-
path = buffer.value
|
|
116
|
-
if " " in path:
|
|
117
|
-
raise ValueError(f"Path contains a space and has no 8.3 short name ({path}); the hook was not installed.")
|
|
118
|
-
return path.replace("\\", "/")
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _command(client):
|
|
122
|
-
return f"{_shell_safe(sys.executable)} {_shell_safe(HOOK_SCRIPT)} --client {client}"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _handler(client):
|
|
126
|
-
"""Claude Code posts the payload straight to the warm daemon (`type: "http"`, no process per tool call); other
|
|
127
|
-
clients run the thin command that forwards to the same route."""
|
|
128
|
-
if CLIENTS[client].get("http"):
|
|
129
|
-
registry = daemon_launcher.read_registry()
|
|
130
|
-
if not registry or not registry.get("url"):
|
|
131
|
-
raise ValueError("Smart Tool daemon has no registered URL; start the daemon before installing the hook.")
|
|
132
|
-
return {"type": "http", "url": f"{registry['url']}{HTTP_PATH}?client={client}", "timeout": HTTP_TIMEOUT_S}
|
|
133
|
-
return {"type": "command", "command": _command(client), "timeout": HTTP_TIMEOUT_S}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _plan(client_info):
|
|
137
|
-
client = identify(client_info)
|
|
138
|
-
if client is None:
|
|
139
|
-
raise ValueError(f"Unrecognized MCP client ({(client_info or {}).get('name') or 'no clientInfo'}); "
|
|
140
|
-
"the hook is only installed for Claude Code or Codex, from that client's own session.")
|
|
141
|
-
spec = CLIENTS[client]
|
|
142
|
-
if spec.get("blocked"):
|
|
143
|
-
raise ValueError(spec["blocked"] + f" Configure it manually if you want: {spec['docs']}")
|
|
144
|
-
path = os.path.realpath(spec["files"][0])
|
|
145
|
-
try:
|
|
146
|
-
with open(path, encoding="utf-8") as stream:
|
|
147
|
-
data = json.load(stream)
|
|
148
|
-
except FileNotFoundError:
|
|
149
|
-
data = {}
|
|
150
|
-
if not isinstance(data, dict) or not isinstance(data.setdefault("hooks", {}), dict):
|
|
151
|
-
raise ValueError(f"{path} does not have the expected format (object with 'hooks'); nothing was changed.")
|
|
152
|
-
groups = data["hooks"].setdefault(spec["event"], [])
|
|
153
|
-
if not isinstance(groups, list):
|
|
154
|
-
raise ValueError(f"{path}: hooks.{spec['event']} is not a list; nothing was changed.")
|
|
155
|
-
groups[:] = _without_ours(groups)
|
|
156
|
-
entry = {"matcher": spec["matcher"], "hooks": [_handler(client)]}
|
|
157
|
-
return client, spec, path, data, groups, entry
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def preview(client_info):
|
|
161
|
-
client, spec, path, _data, _groups, entry = _plan(client_info)
|
|
162
|
-
if installed(client):
|
|
163
|
-
return {"client": client, "installed": True, "file": path, "change": None}
|
|
164
|
-
token = secrets.token_urlsafe(12)
|
|
165
|
-
_PREVIEWS[token] = {"client": client, "at": time.time()}
|
|
166
|
-
return {"client": client, "installed": False, "file": path, "event": spec["event"], "change": entry,
|
|
167
|
-
"token": token, "expires_s": PREVIEW_TTL_S, "after_install": spec.get("after_install")}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def install(client_info, token, confirm):
|
|
171
|
-
planned = _PREVIEWS.pop(token or "", None)
|
|
172
|
-
client, spec, path, data, groups, entry = _plan(client_info)
|
|
173
|
-
if confirm is not True or not planned or planned["client"] != client or time.time() - planned["at"] > PREVIEW_TTL_S:
|
|
174
|
-
raise ValueError("Installing requires the install_hook_preview token from this session (valid for 10 min) and confirm=true.")
|
|
175
|
-
if installed(client):
|
|
176
|
-
return {"client": client, "installed": True, "changed": False, "file": path}
|
|
177
|
-
groups.append(entry)
|
|
178
|
-
backup = None
|
|
179
|
-
if os.path.exists(path):
|
|
180
|
-
backup = f"{path}.smart-tool-backup-{time.strftime('%Y%m%d-%H%M%S')}"
|
|
181
|
-
shutil.copy2(path, backup)
|
|
182
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
183
|
-
temporary = path + ".smart-tool-tmp"
|
|
184
|
-
with open(temporary, "w", encoding="utf-8") as stream:
|
|
185
|
-
json.dump(data, stream, indent=2, ensure_ascii=False)
|
|
186
|
-
os.replace(temporary, path)
|
|
187
|
-
if not installed(client):
|
|
188
|
-
if backup:
|
|
189
|
-
shutil.copy2(backup, path)
|
|
190
|
-
raise RuntimeError(f"The hook does not appear in {path} after writing; the file was restored.")
|
|
191
|
-
return {"client": client, "installed": True, "changed": True, "file": path, "backup": backup,
|
|
192
|
-
"after_install": spec.get("after_install")}
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import secrets
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import daemon_launcher
|
|
9
|
+
import endpoint_sync
|
|
10
|
+
|
|
11
|
+
HOOK_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "hooks", "pretool_router.py")
|
|
12
|
+
MARKER = "pretool_router.py"
|
|
13
|
+
HTTP_PATH = "/hooks/pretool"
|
|
14
|
+
HTTP_TIMEOUT_S = 15
|
|
15
|
+
HOME = os.path.expanduser("~")
|
|
16
|
+
PREVIEW_TTL_S = 600
|
|
17
|
+
CLIENTS = {
|
|
18
|
+
"claude": {"label": "Claude Code", "names": {"claude-code"}, "files": [os.path.join(endpoint_sync.claude_dir(), "settings.json")],
|
|
19
|
+
"docs": "https://code.claude.com/docs/en/hooks", "event": "PreToolUse", "matcher": "Grep|Glob|Read|Bash|WebSearch|WebFetch", "http": True},
|
|
20
|
+
"codex": {"label": "Codex", "prefix": "codex", "files": [os.path.join(HOME, ".codex", "hooks.json"), os.path.join(HOME, ".codex", "config.toml")],
|
|
21
|
+
"docs": "https://learn.chatgpt.com/docs/hooks", "event": "PreToolUse", "matcher": "Bash",
|
|
22
|
+
"after_install": "Codex only runs new hooks after the user approves the definition in /hooks."},
|
|
23
|
+
"cursor": {"label": "Cursor", "prefix": "cursor", "files": [os.path.join(HOME, ".cursor", "hooks.json")],
|
|
24
|
+
"docs": "https://cursor.com/docs/agent/hooks", "event": "preToolUse", "matcher": "Shell|Read|Grep",
|
|
25
|
+
"blocked": "In Cursor, `allow` approves the tool without the user and empty output blocks: there is no documented neutral "
|
|
26
|
+
"response, so the hook is not installed automatically."},
|
|
27
|
+
}
|
|
28
|
+
_PREVIEWS = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def identify(client_info):
|
|
32
|
+
name = str((client_info or {}).get("name") or "").casefold()
|
|
33
|
+
return next((key for key, spec in CLIENTS.items()
|
|
34
|
+
if name in spec.get("names", ()) or (spec.get("prefix") and name.startswith(spec["prefix"]))), None)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _our_hook(hook):
|
|
38
|
+
return isinstance(hook, dict) and (MARKER in str(hook.get("command", "")) or HTTP_PATH in str(hook.get("url", "")))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _ours(group):
|
|
42
|
+
return isinstance(group, dict) and isinstance(group.get("hooks"), list) and any(map(_our_hook, group["hooks"]))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _without_ours(groups):
|
|
46
|
+
"""Drops only our hook entries; a group shared with other hooks keeps them and disappears only when emptied."""
|
|
47
|
+
kept = []
|
|
48
|
+
for group in groups:
|
|
49
|
+
if not _ours(group):
|
|
50
|
+
kept.append(group)
|
|
51
|
+
continue
|
|
52
|
+
others = [hook for hook in group["hooks"] if not _our_hook(hook)]
|
|
53
|
+
if others:
|
|
54
|
+
kept.append({**group, "hooks": others})
|
|
55
|
+
return kept
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def installed(client):
|
|
59
|
+
"""Our hook is present with the current matcher; an entry with an older matcher counts as missing."""
|
|
60
|
+
spec = CLIENTS[client]
|
|
61
|
+
for path in spec["files"]:
|
|
62
|
+
try:
|
|
63
|
+
with open(path, encoding="utf-8") as stream:
|
|
64
|
+
text = stream.read()
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
continue
|
|
67
|
+
if MARKER not in text and HTTP_PATH not in text:
|
|
68
|
+
continue
|
|
69
|
+
if not path.endswith(".json"):
|
|
70
|
+
return True
|
|
71
|
+
data = json.loads(text)
|
|
72
|
+
hooks = data.get("hooks") if isinstance(data, dict) else None
|
|
73
|
+
groups = hooks.get(spec["event"]) if isinstance(hooks, dict) else None
|
|
74
|
+
expected = _handler(client)
|
|
75
|
+
return isinstance(groups, list) and any(
|
|
76
|
+
group.get("matcher") == spec["matcher"] and expected in group["hooks"] for group in groups if _ours(group))
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def status(client_info):
|
|
81
|
+
client = identify(client_info)
|
|
82
|
+
if client is None:
|
|
83
|
+
return {"client": None, "name": (client_info or {}).get("name"), "installed": None, "error": None}
|
|
84
|
+
spec = CLIENTS[client]
|
|
85
|
+
try:
|
|
86
|
+
state, error = installed(client), None
|
|
87
|
+
except (OSError, ValueError) as exc:
|
|
88
|
+
state, error = None, f"{type(exc).__name__}: {exc}"[:200]
|
|
89
|
+
return {"client": client, "name": client_info.get("name"), "installed": state, "error": error,
|
|
90
|
+
"files": spec["files"], "docs": spec["docs"]}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def notice(state):
|
|
94
|
+
if state["client"] is None:
|
|
95
|
+
return (f"Unrecognized MCP client ({state['name'] or 'no clientInfo'}): Smart Tool does not check the hook "
|
|
96
|
+
"that redirects Grep/Read to smart_search and WebSearch/WebFetch to web_search/web_fetch in this client.")
|
|
97
|
+
spec = CLIENTS[state["client"]]
|
|
98
|
+
if state["error"]:
|
|
99
|
+
return f"Could not check the Smart Tool hook in {spec['files'][0]}: {state['error']}"
|
|
100
|
+
if state["installed"]:
|
|
101
|
+
return None
|
|
102
|
+
action = spec.get("blocked") or ("If the user asks, use project_manage action=install_hook_preview, show the "
|
|
103
|
+
"change and confirm with action=install_hook (makes a backup).")
|
|
104
|
+
return (f"Smart Tool hook missing or outdated in {spec['label']} ({spec['files'][0]}): Grep/Read/Bash are not "
|
|
105
|
+
f"redirected to smart_search, nor WebSearch/WebFetch to web_search/web_fetch. {action} Docs: {spec['docs']}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _shell_safe(path):
|
|
109
|
+
"""Unquoted path that runs the same in cmd, PowerShell and bash (the Codex docs don't name the shell): forward
|
|
110
|
+
slashes (bash eats backslashes), no spaces (8.3 short name), no quotes (PowerShell reads a quoted head as a string)."""
|
|
111
|
+
if " " in path and sys.platform == "win32":
|
|
112
|
+
import ctypes
|
|
113
|
+
buffer = ctypes.create_unicode_buffer(1024)
|
|
114
|
+
if ctypes.windll.kernel32.GetShortPathNameW(path, buffer, len(buffer)):
|
|
115
|
+
path = buffer.value
|
|
116
|
+
if " " in path:
|
|
117
|
+
raise ValueError(f"Path contains a space and has no 8.3 short name ({path}); the hook was not installed.")
|
|
118
|
+
return path.replace("\\", "/")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _command(client):
|
|
122
|
+
return f"{_shell_safe(sys.executable)} {_shell_safe(HOOK_SCRIPT)} --client {client}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _handler(client):
|
|
126
|
+
"""Claude Code posts the payload straight to the warm daemon (`type: "http"`, no process per tool call); other
|
|
127
|
+
clients run the thin command that forwards to the same route."""
|
|
128
|
+
if CLIENTS[client].get("http"):
|
|
129
|
+
registry = daemon_launcher.read_registry()
|
|
130
|
+
if not registry or not registry.get("url"):
|
|
131
|
+
raise ValueError("Smart Tool daemon has no registered URL; start the daemon before installing the hook.")
|
|
132
|
+
return {"type": "http", "url": f"{registry['url']}{HTTP_PATH}?client={client}", "timeout": HTTP_TIMEOUT_S}
|
|
133
|
+
return {"type": "command", "command": _command(client), "timeout": HTTP_TIMEOUT_S}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _plan(client_info):
|
|
137
|
+
client = identify(client_info)
|
|
138
|
+
if client is None:
|
|
139
|
+
raise ValueError(f"Unrecognized MCP client ({(client_info or {}).get('name') or 'no clientInfo'}); "
|
|
140
|
+
"the hook is only installed for Claude Code or Codex, from that client's own session.")
|
|
141
|
+
spec = CLIENTS[client]
|
|
142
|
+
if spec.get("blocked"):
|
|
143
|
+
raise ValueError(spec["blocked"] + f" Configure it manually if you want: {spec['docs']}")
|
|
144
|
+
path = os.path.realpath(spec["files"][0])
|
|
145
|
+
try:
|
|
146
|
+
with open(path, encoding="utf-8") as stream:
|
|
147
|
+
data = json.load(stream)
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
data = {}
|
|
150
|
+
if not isinstance(data, dict) or not isinstance(data.setdefault("hooks", {}), dict):
|
|
151
|
+
raise ValueError(f"{path} does not have the expected format (object with 'hooks'); nothing was changed.")
|
|
152
|
+
groups = data["hooks"].setdefault(spec["event"], [])
|
|
153
|
+
if not isinstance(groups, list):
|
|
154
|
+
raise ValueError(f"{path}: hooks.{spec['event']} is not a list; nothing was changed.")
|
|
155
|
+
groups[:] = _without_ours(groups)
|
|
156
|
+
entry = {"matcher": spec["matcher"], "hooks": [_handler(client)]}
|
|
157
|
+
return client, spec, path, data, groups, entry
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def preview(client_info):
|
|
161
|
+
client, spec, path, _data, _groups, entry = _plan(client_info)
|
|
162
|
+
if installed(client):
|
|
163
|
+
return {"client": client, "installed": True, "file": path, "change": None}
|
|
164
|
+
token = secrets.token_urlsafe(12)
|
|
165
|
+
_PREVIEWS[token] = {"client": client, "at": time.time()}
|
|
166
|
+
return {"client": client, "installed": False, "file": path, "event": spec["event"], "change": entry,
|
|
167
|
+
"token": token, "expires_s": PREVIEW_TTL_S, "after_install": spec.get("after_install")}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def install(client_info, token, confirm):
|
|
171
|
+
planned = _PREVIEWS.pop(token or "", None)
|
|
172
|
+
client, spec, path, data, groups, entry = _plan(client_info)
|
|
173
|
+
if confirm is not True or not planned or planned["client"] != client or time.time() - planned["at"] > PREVIEW_TTL_S:
|
|
174
|
+
raise ValueError("Installing requires the install_hook_preview token from this session (valid for 10 min) and confirm=true.")
|
|
175
|
+
if installed(client):
|
|
176
|
+
return {"client": client, "installed": True, "changed": False, "file": path}
|
|
177
|
+
groups.append(entry)
|
|
178
|
+
backup = None
|
|
179
|
+
if os.path.exists(path):
|
|
180
|
+
backup = f"{path}.smart-tool-backup-{time.strftime('%Y%m%d-%H%M%S')}"
|
|
181
|
+
shutil.copy2(path, backup)
|
|
182
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
183
|
+
temporary = path + ".smart-tool-tmp"
|
|
184
|
+
with open(temporary, "w", encoding="utf-8") as stream:
|
|
185
|
+
json.dump(data, stream, indent=2, ensure_ascii=False)
|
|
186
|
+
os.replace(temporary, path)
|
|
187
|
+
if not installed(client):
|
|
188
|
+
if backup:
|
|
189
|
+
shutil.copy2(backup, path)
|
|
190
|
+
raise RuntimeError(f"The hook does not appear in {path} after writing; the file was restored.")
|
|
191
|
+
return {"client": client, "installed": True, "changed": True, "file": path, "backup": backup,
|
|
192
|
+
"after_install": spec.get("after_install")}
|
package/code_graph_java.mjs
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
// Análise estática conservadora de Java a partir de snapshots; nenhum código é executado.
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import {parse} from './web_adapters/node/node_modules/java-parser/src/index.js';
|
|
4
|
-
const input=JSON.parse(fs.readFileSync(0,'utf8'));
|
|
5
|
-
const symbols=[],dependencies=[],calls=[],unresolved=[],diagnostics=[],units=[],classes=new Map(),methodNodes=new Map();
|
|
6
|
-
const MAX_SYMBOLS=8000,MAX_EDGES=20000;
|
|
7
|
-
function children(n){return n?.children?Object.values(n.children).flat().filter(x=>x.children):[]}
|
|
8
|
-
function all(n,name,stop=false){const result=[];function visit(node){if(node?.name===name){result.push(node);if(stop)return}for(const child of children(node))visit(child)}visit(n);return result}
|
|
9
|
-
function first(n,name){return all(n,name,true)[0]}
|
|
10
|
-
function identifiers(n){const out=[];function visit(node){if(node?.children)for(const [key,items] of Object.entries(node.children))for(const child of items){if(child.children)visit(child);else if(key==='Identifier')out.push(child)}}visit(n);return out.sort((a,b)=>a.startOffset-b.startOffset)}
|
|
11
|
-
function text(u,n){return n?.location?u.text.slice(n.location.startOffset,n.location.endOffset+1):''}
|
|
12
|
-
function addSymbol(u,n,name,kind){if(symbols.length>=MAX_SYMBOLS)return null;const s={id:u.path+'::'+name+'@'+n.location.startLine+':'+n.location.startOffset,path:u.path,name,kind,language:'java',start_line:n.location.startLine,end_line:n.location.endLine};symbols.push(s);return s}
|
|
13
|
-
const classKinds=new Set(['normalClassDeclaration','normalInterfaceDeclaration','enumDeclaration','recordDeclaration']);
|
|
14
|
-
for(const file of input.files){try{const tree=parse(file.text),pkg=first(tree,'packageDeclaration'),unit={...file,tree,package:pkg?identifiers(pkg).map(t=>t.image).join('.'): '',imports:[],classes:[]};
|
|
15
|
-
for(const n of all(tree,'importDeclaration')){const raw=text(unit,n).replace(/^import\s+/,'').replace(/;\s*$/,'').trim(),stat=raw.startsWith('static '),spec=(stat?raw.slice(7):raw).replace(/\s+/g,'');unit.imports.push({spec,static:stat,line:n.location.startLine})}
|
|
16
|
-
function scan(n,parent=null){let current=parent;if(classKinds.has(n.name)){const id=first(n,'typeIdentifier')||first(n,'typeName'),short=identifiers(id||n)[0]?.image;if(!short)return;const local=(parent?parent.local+'.':'')+short,fqn=(unit.package?unit.package+'.':'')+local;const symbol=addSymbol(unit,n,local,n.name==='normalInterfaceDeclaration'?'interface':'class');if(!symbol)return;current={unit,fqn,local,short,symbol,node:n,body:n.children.classBody?.[0]||n.children.interfaceBody?.[0]||n.children.recordBody?.[0]||n.children.enumBody?.[0],methods:[],fields:new Map(),extends:text(unit,n.children.classExtends?.[0]).replace(/^extends\s+/, '').trim()};classes.set(fqn,classes.has(fqn)?null:current);unit.classes.push(current)}
|
|
17
|
-
if(current&&n.name==='classBody'&&n!==current.body)return;
|
|
18
|
-
if(current&&['methodDeclaration','interfaceMethodDeclaration','constructorDeclaration','compactConstructorDeclaration'].includes(n.name)){const declarator=first(n,'methodDeclarator')||first(n,'constructorDeclarator'),ctor=n.name.includes('Constructor')||n.name==='constructorDeclaration',name=ctor?current.short:(declarator?.children.Identifier?.[0]?.image);if(name){const params=all(declarator||n,'formalParameter'),types=params.map(p=>text(unit,first(p,'unannType'))||'?');const signature=name+'('+types.join(',')+')';const sym=addSymbol(unit,n,current.local+'.'+signature,ctor?'constructor':'method');if(sym){sym.signature=signature;const method={symbol:sym,name,arity:params.length,types,node:n,cls:current,params,concrete:!!first(n,'block')||ctor};current.methods.push(method);methodNodes.set(n,method)}}}
|
|
19
|
-
for(const child of children(n))scan(child,current)}scan(tree);units.push(unit)
|
|
20
|
-
}catch{diagnostics.push({path:file.path,reason:'Invalid or fragmented Java, or syntax not supported by the parser.'})}}
|
|
21
|
-
function resolveType(unit,raw,context){let name=raw.replace(/@[\w.]+(?:\([^)]*\))?/g,'').replace(/<.*>/g,'').replace(/\[\]|\.\.\./g,'').trim();if(!name||['void','boolean','byte','short','int','long','float','double','char','var'].includes(name))return null;if(classes.has(name))return classes.get(name);const candidates=new Set();const local=[context?context.fqn+'.'+name:'',unit.package?unit.package+'.'+name:name];for(const fq of local)if(classes.has(fq))candidates.add(fq);for(const item of unit.imports.filter(i=>!i.static)){if(item.spec.endsWith('.'+name)&&classes.has(item.spec))candidates.add(item.spec);if(item.spec.endsWith('.*')){const fq=item.spec.slice(0,-1)+name;if(classes.has(fq))candidates.add(fq)}}return candidates.size===1?classes.get([...candidates][0]):null}
|
|
22
|
-
function lookupMethod(cls,name,arity,visited=new Set()){if(!cls||visited.has(cls.fqn))return null;visited.add(cls.fqn);const methods=cls.methods.filter(m=>m.name===name&&m.arity===arity&&m.concrete);if(methods.length===1)return methods[0].symbol;if(methods.length>1)return null;return cls.extends?lookupMethod(resolveType(cls.unit,cls.extends,cls),name,arity,visited):null}
|
|
23
|
-
function putBinding(map,name,type){if(name)map.set(name,map.has(name)&&map.get(name)!==type?null:type)}
|
|
24
|
-
function scoped(node,name){const out=[];function scan(n){if(n!==node&&(classKinds.has(n.name)||n.name==='lambdaExpression'||n.name==='classBody'))return;if(n.name===name)out.push(n);for(const c of children(n))scan(c)}scan(node);return out}
|
|
25
|
-
function collectVariables(unit,node,map){for(const parentName of ['fieldDeclaration','localVariableDeclaration','variableParaRegularParameter','variableArityParameter'])for(const declaration of scoped(node,parentName)){const type=text(unit,first(declaration,'unannType'));for(const id of all(declaration,'variableDeclaratorId'))putBinding(map,identifiers(id)[0]?.image,type)}}
|
|
26
|
-
for(const unit of units){for(const cl of unit.classes){const fieldNodes=scoped(cl.body,'fieldDeclaration');for(const node of fieldNodes){const type=text(unit,first(node,'unannType'));for(const id of all(node,'variableDeclaratorId'))putBinding(cl.fields,identifiers(id)[0]?.image,type)}}}
|
|
27
|
-
for(const unit of units){
|
|
28
|
-
for(const item of unit.imports){const spec=item.spec;let target=classes.get(item.static?spec.split('.').slice(0,-1).join('.'):spec);dependencies.push({source:unit.path,target:target?.unit.path||null,specifier:spec,line:item.line,kind:item.static?'java_static_import':'import',resolution:target?'resolved':'external'})}
|
|
29
|
-
function walk(n,cls=null,method=null,vars=new Map(),owner=null){if(classKinds.has(n.name))cls=unit.classes.find(c=>c.node===n)||cls;if(cls&&n.name==='classBody'&&n!==cls.body)return;
|
|
30
|
-
if(methodNodes.has(n)){method=methodNodes.get(n);vars=new Map(cls.fields);collectVariables(unit,n,vars);owner=method.symbol}
|
|
31
|
-
if(n.name==='lambdaExpression'){vars=new Map(vars);for(const param of identifiers(n.children.lambdaParameters?.[0]))vars.set(param.image,null);owner=addSymbol(unit,n,(cls?.local||'')+'.lambda@'+n.location.startLine,'function')||owner}
|
|
32
|
-
if(cls&&['unannType','classType','interfaceType'].includes(n.name)){const type=resolveType(unit,text(unit,n),cls);if(type&&type.unit.path!==unit.path&&dependencies.length<MAX_EDGES)dependencies.push({source:unit.path,target:type.unit.path,specifier:type.fqn,line:n.location.startLine,kind:'java_type',resolution:'resolved'})}
|
|
33
|
-
if(cls&&n.name==='primary'){
|
|
34
|
-
const prefix=n.children.primaryPrefix?.[0],prefixText=text(unit,prefix).trim(),suffixes=n.children.primarySuffix||[];let chain=prefixText,fluent=false;
|
|
35
|
-
const newMatch=/^new\s+([\w.$]+)/.exec(prefixText);let constructed=newMatch?resolveType(unit,newMatch[1],cls):null;
|
|
36
|
-
if(constructed&&calls.length<MAX_EDGES)calls.push({source:(owner||cls.symbol).id,target:constructed.symbol.id,line:n.location.startLine,kind:'construct',resolution:'static'});
|
|
37
|
-
for(const suffix of suffixes){const invoke=suffix.children.methodInvocationSuffix?.[0];if(!invoke){chain+=text(unit,suffix);continue}const args=invoke.children.argumentList?.[0],arity=args?(args.children.expression||[]).length:0;const match=/^(?:([\w.$]+)\.)?([\w$]+)$/.exec(chain);let target=null;
|
|
38
|
-
if(match&&!fluent){const receiver=match[1],name=match[2];let type=null;if(!receiver||receiver==='this')type=cls;else if(receiver==='super')type=resolveType(unit,cls.extends,cls);else if(receiver.startsWith('this.'))type=resolveType(unit,cls.fields.get(receiver.slice(5))||'',cls);else if(vars.has(receiver))type=resolveType(unit,vars.get(receiver)||'',cls);else type=resolveType(unit,receiver,cls);target=lookupMethod(type,name,arity);if(!target&&!receiver){const candidates=unit.imports.filter(i=>i.static&&(i.spec.endsWith('.'+name)||i.spec.endsWith('.*'))).map(i=>lookupMethod(classes.get(i.spec.split('.').slice(0,-1).join('.')),name,arity)).filter(Boolean);if(candidates.length===1)target=candidates[0]}}
|
|
39
|
-
if(constructed&&!fluent){const methodName=/\.([\w$]+)$/.exec(chain)?.[1];if(methodName)target=lookupMethod(constructed,methodName,arity)}
|
|
40
|
-
if(target&&calls.length<MAX_EDGES)calls.push({source:(owner||cls.symbol).id,target:target.id,line:invoke.location.startLine,kind:'call',resolution:'static'});
|
|
41
|
-
else if(unresolved.length<1000)unresolved.push({path:unit.path,line:invoke.location.startLine,expression:chain.slice(0,140),reason:'External/dynamic type, interface without implementation, or ambiguous overload in the Java snapshot.'});
|
|
42
|
-
fluent=true;chain+='<result>';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
for(const child of children(n))walk(child,cls,method,vars,owner)
|
|
46
|
-
}walk(unit.tree)
|
|
47
|
-
}
|
|
48
|
-
const ids=new Set(symbols.map(s=>s.id));
|
|
49
|
-
process.stdout.write(JSON.stringify({symbols,dependencies:dependencies.slice(0,MAX_EDGES),calls:calls.filter(c=>ids.has(c.source)&&ids.has(c.target)),unresolved,diagnostics,parsed:units.map(u=>u.path),truncated:symbols.length>=MAX_SYMBOLS||dependencies.length>MAX_EDGES||calls.length>=MAX_EDGES}));
|
|
1
|
+
// Análise estática conservadora de Java a partir de snapshots; nenhum código é executado.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import {parse} from './web_adapters/node/node_modules/java-parser/src/index.js';
|
|
4
|
+
const input=JSON.parse(fs.readFileSync(0,'utf8'));
|
|
5
|
+
const symbols=[],dependencies=[],calls=[],unresolved=[],diagnostics=[],units=[],classes=new Map(),methodNodes=new Map();
|
|
6
|
+
const MAX_SYMBOLS=8000,MAX_EDGES=20000;
|
|
7
|
+
function children(n){return n?.children?Object.values(n.children).flat().filter(x=>x.children):[]}
|
|
8
|
+
function all(n,name,stop=false){const result=[];function visit(node){if(node?.name===name){result.push(node);if(stop)return}for(const child of children(node))visit(child)}visit(n);return result}
|
|
9
|
+
function first(n,name){return all(n,name,true)[0]}
|
|
10
|
+
function identifiers(n){const out=[];function visit(node){if(node?.children)for(const [key,items] of Object.entries(node.children))for(const child of items){if(child.children)visit(child);else if(key==='Identifier')out.push(child)}}visit(n);return out.sort((a,b)=>a.startOffset-b.startOffset)}
|
|
11
|
+
function text(u,n){return n?.location?u.text.slice(n.location.startOffset,n.location.endOffset+1):''}
|
|
12
|
+
function addSymbol(u,n,name,kind){if(symbols.length>=MAX_SYMBOLS)return null;const s={id:u.path+'::'+name+'@'+n.location.startLine+':'+n.location.startOffset,path:u.path,name,kind,language:'java',start_line:n.location.startLine,end_line:n.location.endLine};symbols.push(s);return s}
|
|
13
|
+
const classKinds=new Set(['normalClassDeclaration','normalInterfaceDeclaration','enumDeclaration','recordDeclaration']);
|
|
14
|
+
for(const file of input.files){try{const tree=parse(file.text),pkg=first(tree,'packageDeclaration'),unit={...file,tree,package:pkg?identifiers(pkg).map(t=>t.image).join('.'): '',imports:[],classes:[]};
|
|
15
|
+
for(const n of all(tree,'importDeclaration')){const raw=text(unit,n).replace(/^import\s+/,'').replace(/;\s*$/,'').trim(),stat=raw.startsWith('static '),spec=(stat?raw.slice(7):raw).replace(/\s+/g,'');unit.imports.push({spec,static:stat,line:n.location.startLine})}
|
|
16
|
+
function scan(n,parent=null){let current=parent;if(classKinds.has(n.name)){const id=first(n,'typeIdentifier')||first(n,'typeName'),short=identifiers(id||n)[0]?.image;if(!short)return;const local=(parent?parent.local+'.':'')+short,fqn=(unit.package?unit.package+'.':'')+local;const symbol=addSymbol(unit,n,local,n.name==='normalInterfaceDeclaration'?'interface':'class');if(!symbol)return;current={unit,fqn,local,short,symbol,node:n,body:n.children.classBody?.[0]||n.children.interfaceBody?.[0]||n.children.recordBody?.[0]||n.children.enumBody?.[0],methods:[],fields:new Map(),extends:text(unit,n.children.classExtends?.[0]).replace(/^extends\s+/, '').trim()};classes.set(fqn,classes.has(fqn)?null:current);unit.classes.push(current)}
|
|
17
|
+
if(current&&n.name==='classBody'&&n!==current.body)return;
|
|
18
|
+
if(current&&['methodDeclaration','interfaceMethodDeclaration','constructorDeclaration','compactConstructorDeclaration'].includes(n.name)){const declarator=first(n,'methodDeclarator')||first(n,'constructorDeclarator'),ctor=n.name.includes('Constructor')||n.name==='constructorDeclaration',name=ctor?current.short:(declarator?.children.Identifier?.[0]?.image);if(name){const params=all(declarator||n,'formalParameter'),types=params.map(p=>text(unit,first(p,'unannType'))||'?');const signature=name+'('+types.join(',')+')';const sym=addSymbol(unit,n,current.local+'.'+signature,ctor?'constructor':'method');if(sym){sym.signature=signature;const method={symbol:sym,name,arity:params.length,types,node:n,cls:current,params,concrete:!!first(n,'block')||ctor};current.methods.push(method);methodNodes.set(n,method)}}}
|
|
19
|
+
for(const child of children(n))scan(child,current)}scan(tree);units.push(unit)
|
|
20
|
+
}catch{diagnostics.push({path:file.path,reason:'Invalid or fragmented Java, or syntax not supported by the parser.'})}}
|
|
21
|
+
function resolveType(unit,raw,context){let name=raw.replace(/@[\w.]+(?:\([^)]*\))?/g,'').replace(/<.*>/g,'').replace(/\[\]|\.\.\./g,'').trim();if(!name||['void','boolean','byte','short','int','long','float','double','char','var'].includes(name))return null;if(classes.has(name))return classes.get(name);const candidates=new Set();const local=[context?context.fqn+'.'+name:'',unit.package?unit.package+'.'+name:name];for(const fq of local)if(classes.has(fq))candidates.add(fq);for(const item of unit.imports.filter(i=>!i.static)){if(item.spec.endsWith('.'+name)&&classes.has(item.spec))candidates.add(item.spec);if(item.spec.endsWith('.*')){const fq=item.spec.slice(0,-1)+name;if(classes.has(fq))candidates.add(fq)}}return candidates.size===1?classes.get([...candidates][0]):null}
|
|
22
|
+
function lookupMethod(cls,name,arity,visited=new Set()){if(!cls||visited.has(cls.fqn))return null;visited.add(cls.fqn);const methods=cls.methods.filter(m=>m.name===name&&m.arity===arity&&m.concrete);if(methods.length===1)return methods[0].symbol;if(methods.length>1)return null;return cls.extends?lookupMethod(resolveType(cls.unit,cls.extends,cls),name,arity,visited):null}
|
|
23
|
+
function putBinding(map,name,type){if(name)map.set(name,map.has(name)&&map.get(name)!==type?null:type)}
|
|
24
|
+
function scoped(node,name){const out=[];function scan(n){if(n!==node&&(classKinds.has(n.name)||n.name==='lambdaExpression'||n.name==='classBody'))return;if(n.name===name)out.push(n);for(const c of children(n))scan(c)}scan(node);return out}
|
|
25
|
+
function collectVariables(unit,node,map){for(const parentName of ['fieldDeclaration','localVariableDeclaration','variableParaRegularParameter','variableArityParameter'])for(const declaration of scoped(node,parentName)){const type=text(unit,first(declaration,'unannType'));for(const id of all(declaration,'variableDeclaratorId'))putBinding(map,identifiers(id)[0]?.image,type)}}
|
|
26
|
+
for(const unit of units){for(const cl of unit.classes){const fieldNodes=scoped(cl.body,'fieldDeclaration');for(const node of fieldNodes){const type=text(unit,first(node,'unannType'));for(const id of all(node,'variableDeclaratorId'))putBinding(cl.fields,identifiers(id)[0]?.image,type)}}}
|
|
27
|
+
for(const unit of units){
|
|
28
|
+
for(const item of unit.imports){const spec=item.spec;let target=classes.get(item.static?spec.split('.').slice(0,-1).join('.'):spec);dependencies.push({source:unit.path,target:target?.unit.path||null,specifier:spec,line:item.line,kind:item.static?'java_static_import':'import',resolution:target?'resolved':'external'})}
|
|
29
|
+
function walk(n,cls=null,method=null,vars=new Map(),owner=null){if(classKinds.has(n.name))cls=unit.classes.find(c=>c.node===n)||cls;if(cls&&n.name==='classBody'&&n!==cls.body)return;
|
|
30
|
+
if(methodNodes.has(n)){method=methodNodes.get(n);vars=new Map(cls.fields);collectVariables(unit,n,vars);owner=method.symbol}
|
|
31
|
+
if(n.name==='lambdaExpression'){vars=new Map(vars);for(const param of identifiers(n.children.lambdaParameters?.[0]))vars.set(param.image,null);owner=addSymbol(unit,n,(cls?.local||'')+'.lambda@'+n.location.startLine,'function')||owner}
|
|
32
|
+
if(cls&&['unannType','classType','interfaceType'].includes(n.name)){const type=resolveType(unit,text(unit,n),cls);if(type&&type.unit.path!==unit.path&&dependencies.length<MAX_EDGES)dependencies.push({source:unit.path,target:type.unit.path,specifier:type.fqn,line:n.location.startLine,kind:'java_type',resolution:'resolved'})}
|
|
33
|
+
if(cls&&n.name==='primary'){
|
|
34
|
+
const prefix=n.children.primaryPrefix?.[0],prefixText=text(unit,prefix).trim(),suffixes=n.children.primarySuffix||[];let chain=prefixText,fluent=false;
|
|
35
|
+
const newMatch=/^new\s+([\w.$]+)/.exec(prefixText);let constructed=newMatch?resolveType(unit,newMatch[1],cls):null;
|
|
36
|
+
if(constructed&&calls.length<MAX_EDGES)calls.push({source:(owner||cls.symbol).id,target:constructed.symbol.id,line:n.location.startLine,kind:'construct',resolution:'static'});
|
|
37
|
+
for(const suffix of suffixes){const invoke=suffix.children.methodInvocationSuffix?.[0];if(!invoke){chain+=text(unit,suffix);continue}const args=invoke.children.argumentList?.[0],arity=args?(args.children.expression||[]).length:0;const match=/^(?:([\w.$]+)\.)?([\w$]+)$/.exec(chain);let target=null;
|
|
38
|
+
if(match&&!fluent){const receiver=match[1],name=match[2];let type=null;if(!receiver||receiver==='this')type=cls;else if(receiver==='super')type=resolveType(unit,cls.extends,cls);else if(receiver.startsWith('this.'))type=resolveType(unit,cls.fields.get(receiver.slice(5))||'',cls);else if(vars.has(receiver))type=resolveType(unit,vars.get(receiver)||'',cls);else type=resolveType(unit,receiver,cls);target=lookupMethod(type,name,arity);if(!target&&!receiver){const candidates=unit.imports.filter(i=>i.static&&(i.spec.endsWith('.'+name)||i.spec.endsWith('.*'))).map(i=>lookupMethod(classes.get(i.spec.split('.').slice(0,-1).join('.')),name,arity)).filter(Boolean);if(candidates.length===1)target=candidates[0]}}
|
|
39
|
+
if(constructed&&!fluent){const methodName=/\.([\w$]+)$/.exec(chain)?.[1];if(methodName)target=lookupMethod(constructed,methodName,arity)}
|
|
40
|
+
if(target&&calls.length<MAX_EDGES)calls.push({source:(owner||cls.symbol).id,target:target.id,line:invoke.location.startLine,kind:'call',resolution:'static'});
|
|
41
|
+
else if(unresolved.length<1000)unresolved.push({path:unit.path,line:invoke.location.startLine,expression:chain.slice(0,140),reason:'External/dynamic type, interface without implementation, or ambiguous overload in the Java snapshot.'});
|
|
42
|
+
fluent=true;chain+='<result>';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for(const child of children(n))walk(child,cls,method,vars,owner)
|
|
46
|
+
}walk(unit.tree)
|
|
47
|
+
}
|
|
48
|
+
const ids=new Set(symbols.map(s=>s.id));
|
|
49
|
+
process.stdout.write(JSON.stringify({symbols,dependencies:dependencies.slice(0,MAX_EDGES),calls:calls.filter(c=>ids.has(c.source)&&ids.has(c.target)),unresolved,diagnostics,parsed:units.map(u=>u.path),truncated:symbols.length>=MAX_SYMBOLS||dependencies.length>MAX_EDGES||calls.length>=MAX_EDGES}));
|