flowfig 0.6.0 → 0.8.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 (56) hide show
  1. package/README.md +28 -8
  2. package/dist/browser.d.ts +1 -3
  3. package/dist/browser.js +67 -59
  4. package/dist/check.d.ts +5 -3
  5. package/dist/check.js +24 -17
  6. package/dist/cli.js +43 -49
  7. package/dist/code.d.ts +26 -0
  8. package/dist/code.js +568 -0
  9. package/dist/diff.js +0 -4
  10. package/dist/draw.d.ts +1 -3
  11. package/dist/draw.js +3 -7
  12. package/dist/edges.d.ts +7 -0
  13. package/dist/edges.js +510 -0
  14. package/dist/geometry.d.ts +6 -8
  15. package/dist/geometry.js +75 -82
  16. package/dist/gif.d.ts +1 -2
  17. package/dist/gif.js +115 -42
  18. package/dist/guide.d.ts +1 -4
  19. package/dist/guide.js +10 -11
  20. package/dist/imports.d.ts +11 -0
  21. package/dist/imports.js +235 -0
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +20 -78
  24. package/dist/init.d.ts +3 -13
  25. package/dist/init.js +13 -18
  26. package/dist/load.d.ts +1 -3
  27. package/dist/load.js +2 -5
  28. package/dist/mcp.d.ts +1 -2
  29. package/dist/mcp.js +15 -15
  30. package/dist/model.d.ts +57 -109
  31. package/dist/model.js +20 -89
  32. package/dist/open.d.ts +2 -5
  33. package/dist/open.js +6 -11
  34. package/dist/page.d.ts +0 -1
  35. package/dist/page.js +1 -3
  36. package/dist/picker.d.ts +2 -4
  37. package/dist/picker.js +11 -14
  38. package/dist/png.js +9 -10
  39. package/dist/rail.d.ts +1 -3
  40. package/dist/rail.js +2 -16
  41. package/dist/scene.d.ts +8 -8
  42. package/dist/source.d.ts +1 -2
  43. package/dist/source.js +5 -5
  44. package/dist/svg.d.ts +5 -8
  45. package/dist/svg.js +50 -112
  46. package/dist/text.d.ts +0 -2
  47. package/dist/text.js +1 -4
  48. package/dist/type-index.d.ts +55 -0
  49. package/dist/type-index.js +383 -0
  50. package/dist/types.d.ts +32 -0
  51. package/dist/types.js +733 -0
  52. package/dist/verify.d.ts +26 -4
  53. package/dist/verify.js +68 -26
  54. package/dist/version.d.ts +1 -2
  55. package/dist/version.js +2 -2
  56. package/package.json +4 -2
package/dist/edges.js ADDED
@@ -0,0 +1,510 @@
1
+ import { dirname } from 'node:path';
2
+ import { codeFile, esc, langOf, locate, matchClose, outside, readFile } from './code.js';
3
+ import { importsOf, leadsTo } from './imports.js';
4
+ import { ancestors, declIn, defines, findType, inTypePosition, isUpper, localType, makeCtx, normType, receiver, receiverChain, reaches, rustMod, rustUses, } from './types.js';
5
+ import { parseSource } from './source.js';
6
+ export const EDGE_LANGS = new Set(['ts', 'py', 'go', 'java', 'cs', 'rs']);
7
+ const word = (name) => new RegExp(`(?<![\\w$])${esc(name)}(?![\\w$])`);
8
+ const viaToken = (t) => new RegExp(`${/^[\w$]/.test(t) ? '(?<![\\w$])' : ''}${esc(t)}${/[\w$]$/.test(t) ? '(?![\\w$])' : ''}`, 'g');
9
+ function viaHits(via, keep, code) {
10
+ const tail = /[\w$]$/.test(via);
11
+ return [...keep.matchAll(viaToken(via))].map((m) => ({
12
+ at: m.index,
13
+ near: tail && (code == null || code[m.index] !== keep[m.index]) && /^[./-]/.test(keep.slice(m.index + m[0].length)),
14
+ }));
15
+ }
16
+ const IMPORT_LINE = {
17
+ ts: /^\s*(?:(?:import|export)\b[^'"]*\bfrom\s*|import\s*|\}\s*from\s*|(?:const|let|var)\s+[^=]+=\s*require\(\s*)['"][^'"]*['"]\s*\)?\s*;?\s*$/,
18
+ py: /^\s*(?:import\s+[\w.]+(?:\s+as\s+\w+)?(?:\s*,\s*[\w.]+(?:\s+as\s+\w+)?)*|from\s+[\w.]+\s+import\s+[^=()]+)\s*$/,
19
+ go: /^\s*(?:import\s*(?:\w+\s+)?"[^"]*"|(?:[\w.]+\s+)?"[^"]*")\s*$/,
20
+ java: /^\s*import\s+(?:static\s+)?[\w.*]+\s*;\s*$/,
21
+ cs: /^\s*(?:global\s+)?using\s+(?:static\s+)?[\w.]+\s*;\s*$/,
22
+ rs: /^\s*(?:pub(?:\([^)]*\))?\s+)?use\s+[^;]*;\s*$/,
23
+ };
24
+ const LOG_CALL = /\b(?:log|logger|console|logging)\s*\.\s*\w+\s*\(|(?<![\w.])(?:print|println|printf)!?\s*\(/;
25
+ const FUNCS = {
26
+ ts: /\bfunction\b|=>|^[ \t]*(?:(?:public|private|protected|static|async|get|set|override)\s+)*(?!(?:if|for|while|switch|catch)\b)[\w$]+\s*\([^()]*\)\s*(?::[^{;]*)?\{/gm,
27
+ py: /\bdef\s|\blambda\b/g,
28
+ go: /\bfunc\b/g,
29
+ rs: /\bfn\s/g,
30
+ java: /->|^[ \t]*(?:[\w<>[\],.?]+[ \t]+)+(?!(?:if|for|while|switch|catch|new|return)\b)\w+\s*\([^;{]*\)\s*(?:throws[^{;]*)?\{/gm,
31
+ cs: /=>|^[ \t]*(?:[\w<>[\],.?]+[ \t]+)+(?!(?:if|for|while|switch|catch|new|return|using|lock)\b)\w+\s*\([^;{]*\)\s*\{/gm,
32
+ };
33
+ const CONSTS = {
34
+ ts: /^(?:export\s+)?const\s+([\w$]+)\s*(?::\s*string\s*)?=\s*(['"`])([^'"`\n]*)\2/gm,
35
+ py: /^([A-Za-z_]\w*)\s*(?::\s*str\s*)?=\s*(['"])([^'"\n]*)\2/gm,
36
+ go: /^const\s+(\w+)\s*(?:string\s*)?=\s*(["`])([^"`\n]*)\2/gm,
37
+ java: /^[ \t]*(?:(?:public|private|protected)\s+)?static\s+final\s+String\s+(\w+)\s*=\s*(")([^"\n]*)"/gm,
38
+ cs: /^[ \t]*(?:(?:public|private|protected|internal)\s+)?const\s+string\s+(\w+)\s*=\s*(")([^"\n]*)"/gm,
39
+ rs: /^(?:pub(?:\([^)]*\))?\s+)?const\s+(\w+)\s*:\s*&(?:'static\s+)?str\s*=\s*(")([^"\n]*)"/gm,
40
+ };
41
+ const IMPORT_LINES = /\b(?:import|export)\s+(?:type\s+)?[\w*\s,{}$]*?\bfrom\b|^[ \t]*(?:from|use|using|package|import)\b.*|.*\brequire\(.*/gm;
42
+ const MODIFIERS = new Set([
43
+ 'mut',
44
+ 'final',
45
+ 'const',
46
+ 'readonly',
47
+ 'public',
48
+ 'private',
49
+ 'protected',
50
+ 'static',
51
+ 'ref',
52
+ 'out',
53
+ 'in',
54
+ 'this',
55
+ 'params',
56
+ ]);
57
+ const KEYWORD = /^(return|else|throw|new|await|yield|case|do|in|is|as)\b/;
58
+ function typedName(body, n) {
59
+ for (const m of body.matchAll(new RegExp(`\\s+${n}\\s*(?:=(?![=>])|;)`, 'g'))) {
60
+ let s = m.index;
61
+ while (s > 0 && /[\w<>[\],.?]/.test(body[s - 1]))
62
+ s--;
63
+ for (let p = s; p < m.index; p++)
64
+ if ((p === s ? body[p - 1] !== '$' : '<>[],?'.includes(body[p - 1])) && !KEYWORD.test(body.slice(p, m.index)))
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+ function params(body, from) {
70
+ const open = body.indexOf('(', from ? Math.max(0, body.indexOf(from)) : 0);
71
+ if (open < 0)
72
+ return [];
73
+ const text = body.slice(open + 1, matchClose(body, open));
74
+ const parts = [];
75
+ let depth = 0;
76
+ let last = 0;
77
+ for (let i = 0; i < text.length; i++) {
78
+ if ('<([{'.includes(text[i]))
79
+ depth++;
80
+ else if ('>)]}'.includes(text[i]) && text[i - 1] !== '=')
81
+ depth--;
82
+ else if (text[i] === ',' && depth === 0) {
83
+ parts.push(text.slice(last, i));
84
+ last = i + 1;
85
+ }
86
+ }
87
+ return [...parts, text.slice(last)];
88
+ }
89
+ function shadows(body, name, lang, fromName, whole) {
90
+ const n = esc(name);
91
+ if (!whole)
92
+ for (const p of params(body, fromName)) {
93
+ const tokens = (p.split('=')[0].match(/[\w$]+/g) ?? []).filter((t) => !MODIFIERS.has(t));
94
+ if ((lang === 'java' || lang === 'cs' ? tokens.at(-1) : tokens[0]) === name)
95
+ return true;
96
+ }
97
+ return ([`\\b(let|const|var|val|mut)\\s+${n}(?![\\w$])`, `(?<![\\w$.])${n}\\s*:=`, `(^|[\\n;{])[ \\t]*${n}\\s*(:[^=\\n]+)?=(?![=>])`].some((p) => new RegExp(p).test(body)) || typedName(body, n));
98
+ }
99
+ const defaultIs = (ti, symbol) => new RegExp(`export\\s+default\\s+(async\\s+)?(function\\s*\\*?\\s*|class\\s+)?${esc(symbol.split('.')[0])}\\b`).test(ti.code);
100
+ const label = (s) => s.symbol ?? s.path;
101
+ function pyBindings(code, n, limit) {
102
+ let count = 0;
103
+ for (const m of code.matchAll(new RegExp(`^([ \\t]*)(?:(?:(?:async\\s+)?def|class)\\s+${esc(n)}\\b|${esc(n)}\\s*(?::[^=\\n]*)?=(?!=))`, 'gm'))) {
104
+ let ind = m[1].length;
105
+ let line = m.index;
106
+ while (ind > 0 && line > 0) {
107
+ line = code.lastIndexOf('\n', line - 2) + 1;
108
+ const head = /^([ \t]*)(\S.*)/.exec(code.slice(line, code.indexOf('\n', line)));
109
+ if (!head || head[1].length >= ind)
110
+ continue;
111
+ if (/^(?:async\s+)?(?:def|class)\b/.test(head[2]))
112
+ break;
113
+ ind = head[1].length;
114
+ }
115
+ if (ind === 0 && ++count >= limit)
116
+ break;
117
+ }
118
+ return count;
119
+ }
120
+ const twice = (fi, imps, n) => imps.filter((i) => i.local === n).length + (fi.lang === 'py' ? pyBindings(fi.code, n, 2) : 0) > 1;
121
+ function javaScopes(code, pos) {
122
+ const out = [];
123
+ for (let i = pos, d = 0; i >= 0; i--) {
124
+ if (code[i] === '}')
125
+ d++;
126
+ else if (code[i] === '{' && d-- === 0) {
127
+ d = 0;
128
+ const head = code.slice(Math.max(0, i - 300), i).trimEnd();
129
+ const named = /\b(?:class|interface|enum|record)\s+(\w+)[^;{}]*$/.exec(head);
130
+ if (named)
131
+ out.push(named[1]);
132
+ else if (i > 300 && !/[;{}]/.test(head)) {
133
+ out.push(null);
134
+ break;
135
+ }
136
+ else if (head.endsWith(')')) {
137
+ let k = head.length - 1;
138
+ for (let p = 0; k >= 0; k--)
139
+ if (head[k] === ')')
140
+ p++;
141
+ else if (head[k] === '(' && --p === 0)
142
+ break;
143
+ if (/\bnew\s+[\w.<>[\]]+\s*$/.test(head.slice(0, k)))
144
+ out.push(null);
145
+ }
146
+ }
147
+ }
148
+ return out;
149
+ }
150
+ function viaResult(root, from, to, via, read, cache) {
151
+ const longer = (where) => ({ result: 'unsure', reason: `"${via}" is in ${where} only as part of a longer name` });
152
+ const fi = codeFile(root, from.path, read, cache);
153
+ const at = fi && (from.symbol ? locate(fi, from.symbol) : { start: 0, end: fi.keep.length });
154
+ if (!fi || !at)
155
+ return { result: 'not-checked', reason: `${label(from)} is not in code that verify reads` };
156
+ const own = viaHits(via, fi.keep, fi.code).filter((h) => h.at >= at.start && h.at < at.end);
157
+ if (!own.some((h) => !h.near)) {
158
+ const code = fi.code.slice(at.start, at.end);
159
+ const fromName = from.symbol?.split('.').at(-1);
160
+ let near = own.length > 0;
161
+ const names = [];
162
+ for (const m of fi.keep.matchAll(CONSTS[fi.lang])) {
163
+ const hs = word(m[1]).test(code) ? viaHits(via, m[3], null) : [];
164
+ if (hs.some((h) => !h.near))
165
+ names.push(m[1]);
166
+ else if (hs.length)
167
+ near = true;
168
+ }
169
+ if (!names.length)
170
+ return near ? longer(label(from)) : { result: 'not-found', reason: `"${via}" is not in ${label(from)}` };
171
+ const local = names.find((n) => shadows(code, n, fi.lang, fromName, !from.symbol) || new RegExp(`\\.\\s*${esc(n)}(?![\\w$])`).test(code));
172
+ if (local)
173
+ return { result: 'unsure', reason: `${label(from)} has a local or member named ${local}` };
174
+ }
175
+ if (!to)
176
+ return { result: 'found', reason: `"${via}" is in ${label(from)}` };
177
+ const ti = codeFile(root, to.path, read, cache);
178
+ const tb = ti && (to.symbol ? locate(ti, to.symbol) : { start: 0, end: ti.keep.length });
179
+ if (!ti || !tb)
180
+ return { result: 'not-checked', reason: `${label(to)} is not in code that verify reads` };
181
+ const lineAt = (k) => ti.keep.slice(ti.keep.lastIndexOf('\n', k) + 1, ti.keep.indexOf('\n', k) < 0 ? undefined : ti.keep.indexOf('\n', k));
182
+ const raw = viaHits(via, ti.keep, ti.code);
183
+ const all = raw.filter((h) => !IMPORT_LINE[ti.lang].test(lineAt(h.at)) && !LOG_CALL.test(lineAt(h.at)));
184
+ const hits = all.filter((h) => !h.near).map((h) => h.at);
185
+ if (!hits.length && all.length)
186
+ return longer(to.path);
187
+ if (!hits.length && raw.length)
188
+ return { result: 'unsure', reason: `"${via}" is in ${to.path} only on an import or log line` };
189
+ if (!hits.length)
190
+ return { result: 'not-found', reason: `"${via}" is not in ${to.path}` };
191
+ const yes = { result: 'found', reason: `"${via}" is in ${label(from)} and in ${label(to)}` };
192
+ if (hits.some((k) => k >= tb.start && k < tb.end))
193
+ return yes;
194
+ if (hits.some((k) => ti.code[k] !== ti.keep[k]) && [...ti.code.matchAll(FUNCS[ti.lang])].length <= 1)
195
+ return yes;
196
+ return { result: 'unsure', reason: `"${via}" is in ${to.path} outside ${label(to)}` };
197
+ }
198
+ function fileResult(root, fi, ti, body, whole, from, to, read, cache) {
199
+ const fromName = from.symbol?.split('.').at(-1);
200
+ const used = (n) => word(n).test(body) && !shadows(body, n, fi.lang, fromName, whole);
201
+ const names = [];
202
+ let open;
203
+ const imps = importsOf(root, fi, read);
204
+ for (const i of imps) {
205
+ if (i.local === '*' || i.local === '.') {
206
+ const top = [
207
+ ...ti.code.matchAll(/^(?:(?:async\s+)?def|class|func|type|var|const)\s+([\p{L}_][\p{L}\p{N}_]*)|^([\p{L}_][\p{L}\p{N}_]*)\s*=(?!=)/gmu),
208
+ ]
209
+ .map((m) => m[1] ?? m[2])
210
+ .filter(used);
211
+ if (fi.lang === 'py' && top.length)
212
+ return { result: 'unsure', reason: `${top[0]} can come from a star import` };
213
+ if (i.path == null)
214
+ open ??= top.length ? i.local : undefined;
215
+ else
216
+ names.push(...top.filter((n) => leadsTo(root, i.path, to.path, n, read, cache) != null));
217
+ continue;
218
+ }
219
+ if (!used(i.local))
220
+ continue;
221
+ if (i.path == null) {
222
+ open ??= i.local;
223
+ continue;
224
+ }
225
+ const members = i.name === '*' && langOf(i.path) != null
226
+ ? [...body.matchAll(new RegExp(`(?<![\\w$.])${esc(i.local)}\\s*\\.\\s*([\\w$]+)`, 'g'))].map((m) => m[1])
227
+ : [i.name];
228
+ const leads = members.map((m) => leadsTo(root, i.path, to.path, m, read, cache));
229
+ if (leads.some((r) => r != null && r !== '?'))
230
+ names.push(i.local);
231
+ else if (leads.includes('?'))
232
+ open ??= i.local;
233
+ }
234
+ if (fi.lang === 'go' && dirname(from.path) === dirname(to.path) && from.path !== to.path)
235
+ for (const m of ti.code.matchAll(/^(?:func|type|var|const)\s+([\p{L}_][\p{L}\p{N}_]*)/gmu))
236
+ names.push(m[1]);
237
+ const hit = names.find(used);
238
+ const who = label(from);
239
+ if (hit && twice(fi, imps, hit))
240
+ return { result: 'unsure', reason: `${hit} has two bindings` };
241
+ if (hit)
242
+ return { result: 'found', reason: `${who} uses ${hit} from ${to.path}` };
243
+ if (open)
244
+ return { result: 'unsure', reason: `the import of ${open} is not read` };
245
+ return { result: 'not-found', reason: `${who} does not use ${to.path}` };
246
+ }
247
+ export function edgeResult(root, caller, callee, via, read = readFile, cache = new Map()) {
248
+ const skip = (reason) => ({ result: 'not-checked', reason });
249
+ const from = parseSource(caller);
250
+ const to = callee == null ? undefined : (parseSource(callee) ?? undefined);
251
+ if (!from)
252
+ return skip('the caller has no source');
253
+ if (callee === caller)
254
+ return skip('both boxes have the same source');
255
+ if (via != null && typeof via !== 'string')
256
+ return skip('via is not a string');
257
+ if (outside(root, from.path) || (to && outside(root, to.path)))
258
+ return skip('a file is outside the root');
259
+ if (via)
260
+ return viaResult(root, from, to, via, read, cache);
261
+ if (!to)
262
+ return skip('the callee has no source');
263
+ const supported = (p) => {
264
+ const lang = langOf(p.path);
265
+ return lang != null && EDGE_LANGS.has(lang);
266
+ };
267
+ if (!supported(from) || !supported(to))
268
+ return skip('the language is not checked');
269
+ const fi = codeFile(root, from.path, read, cache);
270
+ const ti = codeFile(root, to.path, read, cache);
271
+ if (!fi || !ti)
272
+ return skip('a file is missing');
273
+ const at = from.symbol ? locate(fi, from.symbol) : { start: 0, end: fi.code.length };
274
+ if (!at)
275
+ return skip(`${from.symbol} is not defined`);
276
+ if (to.symbol && !locate(ti, to.symbol))
277
+ return skip(`${to.symbol} is not defined`);
278
+ const whole = !from.symbol;
279
+ const hint = () => (fi.lang !== 'ts' && fi.lang !== 'py') ||
280
+ from.path === to.path ||
281
+ importsOf(root, fi, read).some((i) => i.path != null && leadsTo(root, i.path, to.path, i.name, read, cache) != null)
282
+ ? ''
283
+ : '; if this edge crosses a process, add via';
284
+ let body = fi.code.slice(at.start, at.end);
285
+ if (whole)
286
+ body = body.replace(IMPORT_LINES, (m) => m.replace(/[^\n]/g, ' '));
287
+ if (!to.symbol) {
288
+ const r = fileResult(root, fi, ti, body, whole, from, to, read, cache);
289
+ return r.result === 'not-found' ? { ...r, reason: r.reason + hint() } : r;
290
+ }
291
+ const toParts = to.symbol.split('.');
292
+ const toName = toParts.at(-1);
293
+ const owner = toParts.length > 1 ? toParts.at(-2) : null;
294
+ const fromParts = from.symbol?.split('.') ?? [];
295
+ const fromName = fromParts.at(-1);
296
+ const fromContainer = fromParts.length > 1 ? fromParts.at(-2) : null;
297
+ const cx = makeCtx(root, read, cache);
298
+ const lang = fi.lang;
299
+ const fromDecl = fromContainer ? (declIn(fi, fromContainer) ?? (lang === 'go' ? findType(cx, fi, fromContainer) : null)) : null;
300
+ const ownerDecl = owner ? (declIn(ti, owner) ?? (ti.lang === 'go' ? findType(cx, ti, owner) : null)) : null;
301
+ const tb = locate(ti, to.symbol);
302
+ const extType = ti.lang === 'cs' ? normType(/^[^{;]*?\(\s*this\s+([\w.<>?]+)/.exec(ti.code.slice(tb.start, tb.end))?.[1], 'cs')?.name : null;
303
+ const imps = importsOf(root, fi, read);
304
+ const byLocal = new Map(imps.map((i) => [i.local, i]));
305
+ const pyStar = lang === 'py' && imps.some((i) => i.local === '*');
306
+ const isDefault = defaultIs(ti, to.symbol);
307
+ const wrapped = !isDefault && new RegExp(`\\bexport\\s+default\\b[^;]*${word(toName).source}`).test(ti.code);
308
+ const reach = (path, name) => (path == null ? null : leadsTo(root, path, to.path, name, read, cache));
309
+ const isCallee = (r) => r != null && (r === '*' || r === toName || (r === 'default' && isDefault));
310
+ const names = new Set([toName]);
311
+ for (const i of imps)
312
+ if (i.name !== '*' &&
313
+ word(i.local).test(body) &&
314
+ (i.path == null
315
+ ? i.name === toName || (i.name === 'default' && isDefault)
316
+ : isCallee(reach(i.path, i.name)) || reach(i.path, i.name) === '?' || (wrapped && reach(i.path, i.name) === 'default')))
317
+ names.add(i.local);
318
+ const same = from.path === to.path || ((lang === 'go' || lang === 'java') && dirname(from.path) === dirname(to.path));
319
+ const who = label(from);
320
+ const found = (how) => ({ res: 'found', why: `${who} calls ${toName} ${how}` });
321
+ const not = (why) => ({ res: 'not', why });
322
+ const unsure = (why) => ({ res: 'unsure', why });
323
+ const ignore = { res: 'skip', why: '' };
324
+ const byReach = (r, how) => r === 'yes' ? found(how) : r === 'no' ? not(`${who} does not call ${toName}`) : unsure(`the type of the receiver is not read`);
325
+ const judge = (n, idx, shadowed) => {
326
+ const rc = receiverChain(body, idx, lang);
327
+ if (!rc) {
328
+ if (inTypePosition(body, idx, lang))
329
+ return ignore;
330
+ if (owner && n === owner && /\bnew\s+$/.test(body.slice(Math.max(0, idx - 10), idx)))
331
+ return byReach(reaches(cx, { kind: 'type', name: n, ctx: fi }, ownerDecl, owner, toName, to.path), 'with new');
332
+ if ((lang === 'java' || lang === 'cs') && owner && /^\s*[(<]/.test(body.slice(idx + n.length))) {
333
+ const st = new RegExp(`^\\s*(?:import|using)\\s+static\\s+([\\w.]+?)(?:\\.(${esc(n)}|\\*))?\\s*;`, 'gm');
334
+ const statics = [...fi.keep.matchAll(st)].filter((m) => lang === 'cs' || m[2]);
335
+ const exact = statics.filter((m) => m[2] === n);
336
+ const named = (m) => lang === 'java' && m[1].split('.').at(-1) === owner && to.path.endsWith(`${m[1].replace(/\./g, '/')}.java`);
337
+ if (lang === 'java') {
338
+ const scopes = javaScopes(fi.code, at.start + idx);
339
+ if (!scopes.length)
340
+ return unsure(`the class of ${who} is not known`);
341
+ for (const s of scopes) {
342
+ const d = s == null ? null : declIn(fi, s);
343
+ if (!d)
344
+ return unsure(`the class around ${n} is not read`);
345
+ const up = ancestors(cx, d);
346
+ if ([d, ...up.flatMap((a) => (a.d ? [a.d] : []))].some((x) => defines(cx, x, n)))
347
+ return byReach(reaches(cx, { kind: 'type', name: d.name, ctx: fi }, ownerDecl, owner, toName, to.path), `on ${d.name}`);
348
+ if (up.some((a) => !a.d))
349
+ return unsure(`a base of ${d.name} is not read`);
350
+ }
351
+ if (!statics.length)
352
+ return not(`${who} does not call ${toName}`);
353
+ return exact.length === 1 && named(exact[0]) ? found('through a static import') : unsure(`the static import of ${n} is not read`);
354
+ }
355
+ if (!fromContainer)
356
+ return unsure(`the class of ${who} is not known`);
357
+ const r = reaches(cx, { kind: 'type', name: fromContainer, ctx: fi }, ownerDecl, owner, toName, to.path);
358
+ if (r === 'yes')
359
+ return found('on this');
360
+ if (!statics.length)
361
+ return byReach(r, 'on this');
362
+ const scope = fromParts.slice(0, -1).map((c) => declIn(fi, c));
363
+ const clear = scope.every((d) => {
364
+ const up = d && ancestors(cx, d);
365
+ return up && up.every((a) => a.d) && [d, ...up.map((a) => a.d)].every((x) => !defines(cx, x, n));
366
+ });
367
+ if (exact.length === 1 && named(exact[0]) && clear)
368
+ return found('through a static import');
369
+ return unsure(`the static import of ${n} is not read`);
370
+ }
371
+ if (lang === 'java' && owner && n !== owner)
372
+ return ignore;
373
+ if ((lang === 'java' || lang === 'cs' || lang === 'rs') && !owner && isUpper(n)) {
374
+ const d = findType(cx, fi, n);
375
+ if (d)
376
+ return d.info.path === to.path ? found('as a type') : ignore;
377
+ if (lang === 'rs')
378
+ return unsure(`the type ${n} is not read`);
379
+ }
380
+ if (owner && (lang === 'ts' || lang === 'py' || lang === 'go' || lang === 'rs')) {
381
+ if (lang === 'ts' && new RegExp(`\\{[^}]*\\b${esc(n)}\\b[^}]*\\}\\s*=\\s*this\\b`).test(body)) {
382
+ if (!fromContainer)
383
+ return unsure(`the class of ${who} is not known`);
384
+ return byReach(reaches(cx, { kind: 'type', name: fromContainer, ctx: fi }, ownerDecl, owner, toName, to.path), 'through this');
385
+ }
386
+ if (lang !== 'rs' || !same || n !== toName || new RegExp(`^(pub\\s+)?fn\\s+${esc(n)}\\b`, 'm').test(fi.code))
387
+ return ignore;
388
+ return unsure(`${n} has no path to ${owner}`);
389
+ }
390
+ const dynamic = new RegExp(`\\{[^}]*\\b${esc(n)}\\b[^}]*\\}\\s*=\\s*(?:await\\s+)?(?:import|require)\\(`).test(body);
391
+ if ((shadowed || (n === toName && !dynamic && localType(cx, fi, body, n, lang, !whole))) && n !== fromName)
392
+ return ignore;
393
+ if (pyStar)
394
+ return unsure(`${n} can come from a star import`);
395
+ const bind = byLocal.get(n);
396
+ if (bind && twice(fi, imps, n))
397
+ return unsure(`${n} has two bindings`);
398
+ if (bind?.path === null)
399
+ return unsure(`the import of ${n} is not read`);
400
+ if (bind) {
401
+ const r = reach(bind.path, bind.name);
402
+ if (r === '?')
403
+ return unsure(`${n} can come from a star import`);
404
+ if (r === 'default' && wrapped)
405
+ return unsure(`the default export wraps ${toName}`);
406
+ if (!isCallee(r))
407
+ return ignore;
408
+ if (r === '*') {
409
+ const member = /^\s*\.\s*([\w$]+)/.exec(body.slice(idx + n.length))?.[1];
410
+ if (member == null)
411
+ return unsure(`the use of ${n} is not read`);
412
+ if (member !== toName)
413
+ return ignore;
414
+ }
415
+ return found(n === toName ? 'through an import' : 'through an alias');
416
+ }
417
+ if (same)
418
+ return found('in the same file');
419
+ const use = lang === 'rs' ? rustUses(fi, n, to.path) : 'no';
420
+ if (use === 'yes')
421
+ return found('through a use item');
422
+ if (use === 'outside')
423
+ return unsure(`the use of ${n} is not read`);
424
+ const dots = imps.filter((i) => i.local === '.');
425
+ if (dots.some((i) => isCallee(reach(i.path, n))))
426
+ return found('through a dot import');
427
+ if (dots.some((i) => i.path == null))
428
+ return unsure(`the dot import of ${n} is not read`);
429
+ return (lang === 'java' || lang === 'cs') && !owner ? unsure(`the import of ${n} is not read`) : ignore;
430
+ }
431
+ if (rc.complex)
432
+ return unsure(rc.chain.length ? 'the receiver chain has a call' : 'the receiver is an expression');
433
+ const chain = rc.chain;
434
+ if (pyStar && !shadows(body, chain[0], lang, fromName, whole))
435
+ return unsure(`${chain[0]} can come from a star import`);
436
+ if (owner && chain.length === 1 && chain[0] === owner)
437
+ return byReach(reaches(cx, { kind: 'type', name: owner, ctx: fi }, ownerDecl, owner, toName, to.path), 'through its class');
438
+ const rt = receiver(cx, fi, body, chain, fromContainer, fromDecl);
439
+ if ((rt.kind === 'module' || rt.kind === 'value') && twice(fi, imps, chain[0]))
440
+ return unsure(`${chain[0]} has two bindings`);
441
+ if (rt.kind === 'rsmod') {
442
+ if (owner && ownerDecl)
443
+ return not(`${who} uses a module path for a method`);
444
+ const use = rustUses(fi, chain[0], to.path);
445
+ if (use === 'outside')
446
+ return unsure(`the use of ${chain[0]} is not read`);
447
+ return chain.at(-1) === rustMod(to.path) || (chain.length === 1 && use === 'yes')
448
+ ? found('through a module path')
449
+ : not(`${who} uses another module`);
450
+ }
451
+ if (rt.kind === 'module') {
452
+ if (!rt.mod)
453
+ return unsure(`the module of ${chain[0]} is not read`);
454
+ if (owner && ownerDecl)
455
+ return not(`${who} uses a module member for a method`);
456
+ const r = reach(rt.mod, toName);
457
+ if (r === '?')
458
+ return unsure(`${toName} can come from a star import`);
459
+ return isCallee(r) ? found('through a module') : not(`${who} uses another module`);
460
+ }
461
+ if (rt.kind === 'value') {
462
+ if (owner)
463
+ return unsure(`the type of ${chain.join('.')} is not read`);
464
+ if (rt.mod == null)
465
+ return unsure(`the module of ${chain[0]} is not read`);
466
+ const r = reach(rt.mod, rt.name);
467
+ if (r === '?')
468
+ return unsure(`${rt.name} can come from a star import`);
469
+ return r != null ? found('through an imported value') : not(`${who} uses another value`);
470
+ }
471
+ if (rt.kind === 'super')
472
+ return fromDecl && ancestors(cx, fromDecl).some((x) => x.name === owner)
473
+ ? found('through super')
474
+ : not(`${who} calls super of another class`);
475
+ if (rt.kind !== 'type' || rt.name === '#top')
476
+ return unsure(`the type of ${chain.join('.')} is not known`);
477
+ if (!owner)
478
+ return not(`${who} calls a free function on a typed receiver`);
479
+ if (extType) {
480
+ if (rt.name === extType)
481
+ return found('as an extension');
482
+ const rd = findType(cx, rt.ctx, rt.name, rt.qual);
483
+ if (rd && ancestors(cx, rd).some((x) => x.name === extType))
484
+ return found('as an extension');
485
+ return rd || rt.name === '#external'
486
+ ? not(`${who} calls an extension of another type`)
487
+ : unsure(`the type of ${chain.join('.')} is not read`);
488
+ }
489
+ return byReach(reaches(cx, rt, ownerDecl, owner, toName, to.path), `on ${rt.name}`);
490
+ };
491
+ const verdicts = [];
492
+ let skippedDef = false;
493
+ for (const n of names) {
494
+ const shadowed = shadows(body, n, lang, fromName, whole);
495
+ const re = new RegExp(word(n).source, 'g');
496
+ let m;
497
+ while ((m = re.exec(body)) && verdicts.at(-1)?.res !== 'found') {
498
+ const before = m.index < 200 ? body.slice(0, m.index) : '';
499
+ if (!skippedDef && fromName === n && m.index < 200 && before.trim().length < 60 && !/(\.|::)\s*$/.test(before)) {
500
+ skippedDef = true;
501
+ continue;
502
+ }
503
+ verdicts.push(judge(n, m.index, shadowed));
504
+ }
505
+ }
506
+ const pick = verdicts.find((v) => v.res === 'found') ?? verdicts.find((v) => v.res === 'unsure');
507
+ if (pick)
508
+ return { result: pick.res === 'found' ? 'found' : 'unsure', reason: pick.why };
509
+ return { result: 'not-found', reason: `${who} does not call ${toName}${hint()}` };
510
+ }
@@ -9,8 +9,7 @@ export type Pt = {
9
9
  x: number;
10
10
  y: number;
11
11
  };
12
- /** A cross-block edge of wrapped lanes: the two drawn stubs, their pills, and their points for `check`. `d` then holds both stubs,
13
- * so a packet runs the source stub and jumps to the target stub. */
12
+ /** A cross-lane edge as two stubs, their pills and their points. */
14
13
  export type Stub = {
15
14
  parts: [string, string];
16
15
  pills: [Rect, Rect];
@@ -18,7 +17,7 @@ export type Stub = {
18
17
  short?: true;
19
18
  tight: [boolean, boolean];
20
19
  };
21
- /** `elbow` holds the corners of an elbow path, so `check` can test its straight runs. */
20
+ /** `elbow` holds the corners of an elbow path for `check`. */
22
21
  export type Routed = {
23
22
  id: string;
24
23
  d: string;
@@ -27,11 +26,12 @@ export type Routed = {
27
26
  stub?: Stub;
28
27
  elbow?: Pt[];
29
28
  };
30
- /** A rect that a route keeps clear of. `box` marks a box: an elbow's horizontal runs keep clear of a box, not of a label. */
29
+ /** A rect that a route keeps clear of. `box` marks a box. */
31
30
  export type Avoid = Rect & {
32
31
  box?: boolean;
33
32
  };
34
33
  type Around = 'above' | 'below';
34
+ /** Routes edges between measured boxes as curved SVG paths. `elbow` draws right-angle paths; `stub` splits a cross-lane edge in two. */
35
35
  export declare function route(edges: {
36
36
  id: string;
37
37
  from: string;
@@ -42,12 +42,10 @@ export declare function route(edges: {
42
42
  stub?: number[];
43
43
  bands?: [Rect | undefined, Rect | undefined];
44
44
  labelW?: number;
45
- }[], rects: Record<string, Rect>, tips?: Set<string>, avoid?: Avoid[], // what an elbow's vertical run and a stub pill must not cross (the timeline's outside labels; the boxes and gutters of lanes)
46
- area?: Rect, // where a stub pill may sit: the lanes right of the gutter
47
- lanes?: {
45
+ }[], rects: Record<string, Rect>, tips?: Set<string>, avoid?: Avoid[], area?: Rect, lanes?: {
48
46
  bands: Rect[];
49
47
  boxes: Rect[];
50
48
  }): Routed[];
51
- /** True if the segment a-b crosses the segment c-d. The ends of a-b are trimmed by 1.5 px, so a touch at a box side does not count. */
49
+ /** True if segment a-b crosses c-d. The ends of a-b are trimmed by 1.5 px. */
52
50
  export declare function crosses(a: Pt, b: Pt, c: Pt, d: Pt): boolean;
53
51
  export {};