flowfig 0.7.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.
@@ -0,0 +1,383 @@
1
+ import { basename, dirname, posix, resolve } from 'node:path';
2
+ import { codeFile, esc, locate, matchClose, pyBody, readFile } from './code.js';
3
+ import { importsOf, resolveTs } from './imports.js';
4
+ const indexes = new WeakMap();
5
+ export function makeCtx(root, read = readFile, cache = new Map()) {
6
+ let ix = indexes.get(cache);
7
+ if (!ix)
8
+ indexes.set(cache, (ix = { types: new Map(), binds: new Map(), repo: null, globals: new Map() }));
9
+ return { root, read, cache, ix };
10
+ }
11
+ export const lastSeg = (s) => s.split(/\.|::/).at(-1);
12
+ export const file = (cx, path) => codeFile(cx.root, path, cx.read, cx.cache);
13
+ export function bindings(cx, f) {
14
+ let out = cx.ix.binds.get(f.path);
15
+ if (!out) {
16
+ out = new Map(importsOf(cx.root, f, cx.read).map((i) => [i.local, { mod: i.path, imported: i.name, dir: f.lang === 'go' }]));
17
+ cx.ix.binds.set(f.path, out);
18
+ }
19
+ return out;
20
+ }
21
+ const SKIP = new Set(['target', 'bin', 'obj', 'build', 'dist']);
22
+ function repoFiles(cx) {
23
+ return (cx.ix.repo ??= (cx.read.files ?? readFile.files)(cx.root));
24
+ }
25
+ export function goDirFiles(cx, dir) {
26
+ return repoFiles(cx)
27
+ .filter((f) => f.endsWith('.go') && !f.endsWith('_test.go') && posix.dirname(f) === dir)
28
+ .map((f) => file(cx, f))
29
+ .filter((f) => f != null);
30
+ }
31
+ const project = (g, path) => g.projects.find((p) => `/${path}`.startsWith(p === './' ? '/' : `/${p}`)) ?? '';
32
+ export function global(cx, lang) {
33
+ const hit = cx.ix.globals.get(lang);
34
+ if (hit)
35
+ return hit;
36
+ const ext = { java: '.java', cs: '.cs', rs: '.rs' }[lang];
37
+ const g = {
38
+ types: new Map(),
39
+ impls: new Map(),
40
+ aliases: new Map(),
41
+ usings: new Set(),
42
+ projects: repoFiles(cx)
43
+ .filter((f) => f.endsWith('.csproj'))
44
+ .map((f) => `${posix.dirname(f)}/`)
45
+ .sort((a, b) => b.length - a.length),
46
+ };
47
+ for (const rel of repoFiles(cx)) {
48
+ if (!rel.endsWith(ext) || rel.split('/').some((s) => SKIP.has(s)))
49
+ continue;
50
+ const src = cx.read(resolve(cx.root, rel));
51
+ if (src == null)
52
+ continue;
53
+ for (const m of src.matchAll(/\b(class|interface|record|struct|enum|trait)\s+([A-Za-z_]\w*)/g)) {
54
+ const list = g.types.get(m[2]) ?? [];
55
+ if (!list.includes(rel))
56
+ list.push(rel);
57
+ g.types.set(m[2], list);
58
+ }
59
+ if (lang === 'cs')
60
+ for (const m of src.matchAll(/^\s*global\s+using\s+([\w.]+)\s*;/gm))
61
+ g.usings.add(`${project(g, rel)}|${m[1]}`);
62
+ if (lang !== 'rs')
63
+ continue;
64
+ for (const m of src.matchAll(/\btype\s+(\w+)\s*(?:<[^>]*>)?\s*=\s*([^;]+);/g))
65
+ g.aliases.set(m[1], { to: m[2], path: rel });
66
+ for (const m of src.matchAll(/\bimpl\b\s*(?:<[^>]*>)?\s*([\w:]+)(?:<[^>]*>)?\s+for\s+(?:&\s*)?([\w:]+)/g)) {
67
+ const ty = lastSeg(m[2]);
68
+ g.impls.set(ty, (g.impls.get(ty) ?? new Set()).add(lastSeg(m[1])));
69
+ }
70
+ }
71
+ cx.ix.globals.set(lang, g);
72
+ return g;
73
+ }
74
+ const DECL = (lang, n) => ({
75
+ ts: new RegExp(`\\b(class|interface)\\s+${esc(n)}\\b`, 'g'),
76
+ py: new RegExp(`(^|\\n)[ \\t]*class\\s+${esc(n)}\\b`, 'g'),
77
+ go: new RegExp(`\\btype\\s+${esc(n)}(\\[[^\\]]*\\])?\\s+(struct\\b|interface\\b|[\\w.*[])`, 'g'),
78
+ java: new RegExp(`\\b(class|interface|record|enum)\\s+${esc(n)}\\b`, 'g'),
79
+ cs: new RegExp(`\\b(class|interface|record|struct)\\s+${esc(n)}\\b`, 'g'),
80
+ rs: new RegExp(`\\b(struct|trait|enum)\\s+${esc(n)}\\b`, 'g'),
81
+ })[lang];
82
+ export function declIn(info, name) {
83
+ if (!info)
84
+ return null;
85
+ const m = DECL(info.lang, name).exec(info.code);
86
+ if (!m)
87
+ return null;
88
+ const start = m.index + (m[1] === '\n' ? 1 : 0);
89
+ let end;
90
+ if (info.lang === 'py')
91
+ end = pyBody(info.code, start)[1];
92
+ else if (info.lang === 'go' && !/(struct|interface)$/.test(m[0])) {
93
+ const nl = info.code.indexOf('\n', m.index);
94
+ end = nl < 0 ? info.code.length : nl;
95
+ }
96
+ else {
97
+ const unit = info.lang === 'rs' && /struct/.test(m[0]) && /^[^{]*;/.test(info.code.slice(m.index, m.index + 200));
98
+ const open = info.code.indexOf(unit ? ';' : '{', m.index);
99
+ end = open < 0 ? info.code.length : info.code[open] === ';' ? open : matchClose(info.code, open) + 1;
100
+ }
101
+ return { info, start, end, name, kind: /interface|trait/.test(m[0]) ? 'interface' : 'class' };
102
+ }
103
+ export function findType(cx, ctx, name, qual) {
104
+ if (!name || name.startsWith('#'))
105
+ return null;
106
+ const key = `${ctx.path}|${qual ?? ''}|${name}`;
107
+ if (cx.ix.types.has(key))
108
+ return cx.ix.types.get(key);
109
+ cx.ix.types.set(key, null);
110
+ const r = findType0(cx, ctx, name, qual);
111
+ cx.ix.types.set(key, r);
112
+ return r;
113
+ }
114
+ function findType0(cx, ctx, name, qual) {
115
+ const lang = ctx.lang;
116
+ if (lang === 'go') {
117
+ let dir = dirname(ctx.path);
118
+ if (qual) {
119
+ const b = bindings(cx, ctx).get(qual);
120
+ if (!b?.mod)
121
+ return null;
122
+ dir = b.mod;
123
+ }
124
+ for (const f of goDirFiles(cx, dir)) {
125
+ const d = declIn(f, name);
126
+ if (d)
127
+ return { ...d, dir };
128
+ }
129
+ return null;
130
+ }
131
+ if (!qual) {
132
+ const d = declIn(ctx, name);
133
+ if (d)
134
+ return d;
135
+ }
136
+ if (lang === 'ts' || lang === 'py') {
137
+ const b = bindings(cx, ctx).get(qual ? qual.split('.')[0] : name);
138
+ if (!b?.mod)
139
+ return null;
140
+ return followExport(cx, b.mod, qual ? name : b.imported === 'default' || b.imported === '*' ? name : b.imported, lang, 0);
141
+ }
142
+ if (lang === 'java') {
143
+ const outer = qual ? findType(cx, ctx, lastSeg(qual)) : null;
144
+ if (outer)
145
+ return declIn(outer.info, name);
146
+ const imp = new RegExp(`^\\s*import\\s+([\\w.]+)\\.${esc(name)}\\s*;`, 'm').exec(ctx.keep);
147
+ const g = global(cx, 'java').types.get(name) ?? [];
148
+ const pick = imp
149
+ ? g.find((p) => p.replace(/\.java$/, '').endsWith(`${imp[1].replace(/\./g, '/')}/${name}`))
150
+ : (g.find((p) => dirname(p) === dirname(ctx.path)) ?? wild(g, ctx));
151
+ return pick ? declIn(file(cx, pick), name) : null;
152
+ }
153
+ if (lang === 'rs') {
154
+ const d = rustFollow(cx, ctx, qual ? [qual.replace(/\./g, '::')] : rustUsePaths(ctx, name), name, 0);
155
+ return d === 'absent' ? null : d;
156
+ }
157
+ const all = (global(cx, lang).types.get(name) ?? []).map((p) => declIn(file(cx, p), name)).filter((d) => d != null);
158
+ const ns = (f) => /\bnamespace\s+([\w.]+)/.exec(f.code)?.[1] ?? '';
159
+ const usings = new Set([...ctx.code.matchAll(/^\s*using\s+([\w.]+)\s*;/gm)].map((m) => m[1]));
160
+ const g = global(cx, 'cs');
161
+ const tiers = [
162
+ (n) => n === ns(ctx) || ns(ctx).startsWith(`${n}.`),
163
+ (n) => usings.has(n),
164
+ (n) => !n || g.usings.has(`${project(g, ctx.path)}|${n}`),
165
+ ];
166
+ for (const tier of tiers) {
167
+ const hits = all.filter((d) => tier(ns(d.info)));
168
+ if (hits.length)
169
+ return hits.length === 1 ? hits[0] : null;
170
+ }
171
+ return null;
172
+ }
173
+ function rustModFile(cx, from, path) {
174
+ const own = /^(mod|lib|main)\.rs$/.test(basename(from.path)) ? dirname(from.path) : from.path.slice(0, -3);
175
+ const segs = path.split('::');
176
+ let dir;
177
+ if (segs[0] === 'crate') {
178
+ dir = dirname(from.path);
179
+ while (!['lib.rs', 'main.rs'].some((f) => file(cx, posix.join(dir, f))) && dir !== '.' && dir !== '/')
180
+ dir = dirname(dir);
181
+ }
182
+ else if (segs[0] === 'self')
183
+ dir = own;
184
+ else
185
+ dir = dirname(own);
186
+ for (const s of segs.slice(1)) {
187
+ if (s === 'super')
188
+ dir = dirname(dir);
189
+ else
190
+ dir = posix.join(dir, s);
191
+ }
192
+ if (segs.length === 1 && segs[0] === 'crate')
193
+ return file(cx, posix.join(dir, 'lib.rs')) ?? file(cx, posix.join(dir, 'main.rs'));
194
+ if (segs.length === 1 && segs[0] === 'self')
195
+ return from;
196
+ return file(cx, `${dir}.rs`) ?? file(cx, posix.join(dir, 'mod.rs'));
197
+ }
198
+ function rustOuter(code) {
199
+ let out = code;
200
+ for (const m of code.matchAll(/\bmod\s+\w+\s*\{/g)) {
201
+ const open = m.index + m[0].length - 1;
202
+ const close = matchClose(code, open);
203
+ out = out.slice(0, open + 1) + out.slice(open + 1, close).replace(/[^\n]/g, ' ') + out.slice(close);
204
+ }
205
+ return out;
206
+ }
207
+ function rustFollow(cx, from, mods, name, depth) {
208
+ if (!mods.length || depth > 3 || mods.some((m) => !/^(crate|self|super)\b/.test(m)))
209
+ return null;
210
+ const hits = [];
211
+ for (const m of mods) {
212
+ const f = rustModFile(cx, from, m);
213
+ if (!f)
214
+ return null;
215
+ const top = rustOuter(f.code);
216
+ const N = esc(name);
217
+ let r;
218
+ if (new RegExp(`\\b(?:struct|enum|trait)\\s+${N}\\b`).test(top)) {
219
+ const d = declIn({ ...f, code: top }, name);
220
+ r = d && { ...d, info: f };
221
+ }
222
+ else if (new RegExp(`\\b(?:struct|enum|trait|type|mod|fn|const|static|union)\\s+${N}\\b`).test(top))
223
+ r = null;
224
+ else {
225
+ const next = rustUsePaths(f, name, true);
226
+ if (next.length)
227
+ r = rustFollow(cx, f, next, name, depth + 1);
228
+ else
229
+ r = new RegExp(`\\buse\\b[^;]*\\b${N}\\b`).test(top) ? null : 'absent';
230
+ }
231
+ if (r === null)
232
+ return null;
233
+ if (r !== 'absent')
234
+ hits.push(r);
235
+ }
236
+ return hits.length === 1 ? hits[0] : hits.length ? null : 'absent';
237
+ }
238
+ function wild(g, ctx) {
239
+ const pkgs = [...ctx.keep.matchAll(/^\s*import\s+([\w.]+)\.\*\s*;/gm)].map((m) => `/${m[1].replace(/\./g, '/')}`);
240
+ const hits = g.filter((p) => pkgs.some((k) => `/${dirname(p)}`.endsWith(k)));
241
+ return hits.length === 1 ? hits[0] : null;
242
+ }
243
+ function followExport(cx, mod, name, lang, depth) {
244
+ const info = file(cx, mod);
245
+ if (!info)
246
+ return null;
247
+ const d = declIn(info, name);
248
+ if (d)
249
+ return d;
250
+ if (depth > 1)
251
+ return null;
252
+ const sub = bindings(cx, info).get(name);
253
+ if (sub?.mod && sub.mod !== mod)
254
+ return followExport(cx, sub.mod, sub.imported === 'default' || sub.imported === '*' ? name : sub.imported, lang, depth + 1);
255
+ if (lang === 'ts')
256
+ for (const m of info.keep.matchAll(/\bexport\s+(\*|\{[^}]*\})\s+from\s+(["'])([^"']+)\2/g)) {
257
+ if (m[1] !== '*' && !new RegExp(`\\b${esc(name)}\\b`).test(m[1]))
258
+ continue;
259
+ const p = resolveTs(cx.root, mod, m[3], cx.read);
260
+ const r = p && followExport(cx, p, name, lang, depth + 1);
261
+ if (r)
262
+ return r;
263
+ }
264
+ return null;
265
+ }
266
+ function basesOf(cx, d) {
267
+ const { info, start, name } = d;
268
+ const lang = info.lang;
269
+ let head = info.code.slice(start, info.code.indexOf(lang === 'py' ? ':' : '{', start + name.length) + 1 || start + 300);
270
+ const at = head.indexOf(name) + name.length;
271
+ if (head[at] === '<') {
272
+ let depth = 0;
273
+ let j = at;
274
+ for (; j < head.length; j++) {
275
+ if (head[j] === '<')
276
+ depth++;
277
+ else if (head[j] === '>' && --depth === 0)
278
+ break;
279
+ }
280
+ head = head.slice(0, at) + head.slice(j + 1);
281
+ }
282
+ const split = (s) => s
283
+ .replace(/<[^<>]*(<[^<>]*>[^<>]*)*>/g, '')
284
+ .split(',')
285
+ .map((x) => x.trim().split(/\s+/)[0])
286
+ .filter(Boolean)
287
+ .map(lastSeg);
288
+ let out = [];
289
+ if (lang === 'ts' || lang === 'java') {
290
+ const e = /\bextends\s+([^{]*?)(\bimplements\b|\{|$)/.exec(head);
291
+ if (e)
292
+ out.push(...split(e[1]));
293
+ const i = /\bimplements\s+([^{]*)/.exec(head);
294
+ if (i)
295
+ out.push(...split(i[1]));
296
+ }
297
+ else if (lang === 'py') {
298
+ const m = new RegExp(`class\\s+${esc(name)}\\s*\\(([^)]*)\\)`).exec(head);
299
+ if (m)
300
+ out = split(m[1].replace(/\w+\s*=\s*[\w.]+/g, ''));
301
+ }
302
+ else if (lang === 'cs') {
303
+ const h = head.replace(new RegExp(`^[\\s\\S]*?\\b${esc(name)}\\s*(<[^>]*>)?\\s*(\\([^)]*\\))?`), '');
304
+ const m = /^\s*:\s*([^{]*?)(\bwhere\b|\{|$)/.exec(h);
305
+ if (m)
306
+ out = split(m[1].replace(/\([^)]*\)/g, ''));
307
+ }
308
+ else if (lang === 'go') {
309
+ for (const m of info.code.slice(start, d.end).matchAll(/\n[ \t]*\*?([\w.]+)[ \t]*(?=\n)/g))
310
+ out.push(lastSeg(m[1]));
311
+ }
312
+ else if (lang === 'rs')
313
+ out = [...(global(cx, 'rs').impls.get(name) ?? [])];
314
+ return out.filter((x) => x && x !== name);
315
+ }
316
+ export function ancestors(cx, d) {
317
+ const out = [];
318
+ const seen = new Set([d.name]);
319
+ const todo = [d];
320
+ while (todo.length && out.length < 30) {
321
+ const c = todo.shift();
322
+ for (const b of basesOf(cx, c)) {
323
+ if (seen.has(b))
324
+ continue;
325
+ seen.add(b);
326
+ const bd = c.info.lang === 'rs' ? null : findType(cx, c.info, b);
327
+ out.push({ name: b, d: bd });
328
+ if (bd)
329
+ todo.push(bd);
330
+ }
331
+ }
332
+ return out;
333
+ }
334
+ export function goMethods(cx, d) {
335
+ const out = new Set();
336
+ for (const f of goDirFiles(cx, dirname(d.info.path)))
337
+ for (const m of f.code.matchAll(new RegExp(`\\bfunc\\s*\\(\\s*\\w*\\s*\\*?\\s*${esc(d.name)}(\\[[^\\]]*\\])?\\s*\\)\\s*(\\w+)`, 'g')))
338
+ out.add(m[2]);
339
+ return out;
340
+ }
341
+ export const goIfaceMethods = (d) => [...d.info.code.slice(d.start, d.end).matchAll(/\n[ \t]*([A-Za-z_]\w*)\s*\(/g)].map((m) => m[1]);
342
+ export function defines(cx, d, n) {
343
+ if (d.info.lang === 'go')
344
+ return (goMethods(cx, d).has(n) ||
345
+ (d.kind === 'interface' && new RegExp(`\\n[ \\t]*${esc(n)}\\s*\\(`).test(d.info.code.slice(d.start, d.end))));
346
+ return locate(d.info, `${d.name}.${n}`) != null;
347
+ }
348
+ export function rustUses(fi, n, tp) {
349
+ const mods = rustUsePaths(fi, n);
350
+ if (mods.some((m) => !/^(crate|self|super)\b/.test(m)))
351
+ return 'outside';
352
+ return mods.some((m) => lastSeg(m) === rustMod(tp)) ? 'yes' : 'no';
353
+ }
354
+ function expandUse(item) {
355
+ const open = item.indexOf('{');
356
+ if (open < 0)
357
+ return [item.trim()];
358
+ const close = matchClose(item, open);
359
+ const parts = [];
360
+ let depth = 0;
361
+ let last = open + 1;
362
+ for (let i = open + 1; i < close; i++) {
363
+ if (item[i] === '{')
364
+ depth++;
365
+ else if (item[i] === '}')
366
+ depth--;
367
+ else if (item[i] === ',' && depth === 0) {
368
+ parts.push(item.slice(last, i));
369
+ last = i + 1;
370
+ }
371
+ }
372
+ parts.push(item.slice(last, close));
373
+ return parts.filter((p) => p.trim()).flatMap((p) => expandUse(item.slice(0, open) + p.trim()));
374
+ }
375
+ function rustUsePaths(fi, n, pubOnly = false) {
376
+ const items = pubOnly ? /\bpub(?:\([^)]*\))?\s+use\s+([^;]+);/g : /\buse\s+([^;]+);/g;
377
+ const paths = [...rustOuter(fi.code).matchAll(items)].flatMap((m) => expandUse(m[1].replace(/\s+/g, ' ')));
378
+ const named = paths.filter((p) => (/\sas\s+(\w+)$/.exec(p)?.[1] ?? lastSeg(p)) === n);
379
+ if (named.length)
380
+ return named.map((p) => (/\sas\s/.test(p) ? '#alias' : p.split('::').slice(0, -1).join('::')));
381
+ return paths.filter((p) => p.endsWith('::*')).map((p) => p.slice(0, -3));
382
+ }
383
+ export const rustMod = (tp) => (basename(tp) === 'mod.rs' ? basename(dirname(tp)) : basename(tp, '.rs'));
@@ -0,0 +1,32 @@
1
+ import { type CodeFile, type Lang } from './code.ts';
2
+ import { type Ctx, type Decl } from './type-index.ts';
3
+ export { makeCtx, bindings, declIn, defines, ancestors, file, findType, rustMod, rustUses, type Ctx, type Decl } from './type-index.ts';
4
+ export type Reach = 'yes' | 'no' | 'unsure';
5
+ export type Rt = {
6
+ kind: 'unknown' | 'type' | 'module' | 'value' | 'super' | 'rsmod';
7
+ name?: string;
8
+ qual?: string | null;
9
+ ctx?: CodeFile;
10
+ decl?: Decl | null;
11
+ mod?: string | null;
12
+ dir?: boolean;
13
+ };
14
+ type Local = {
15
+ chain?: string[];
16
+ type?: string;
17
+ ctor?: string;
18
+ declared?: boolean;
19
+ };
20
+ export declare const isUpper: (s: string) => boolean;
21
+ export declare function receiverChain(body: string, idx: number, lang: Lang): {
22
+ chain: string[];
23
+ complex: boolean;
24
+ } | null;
25
+ export declare function normType(raw: string | undefined, lang: Lang): {
26
+ name: string;
27
+ qual?: string | null;
28
+ } | null;
29
+ export declare function localType(cx: Ctx, info: CodeFile, text: string, n: string, lang: Lang, hasHead?: boolean): Local | null;
30
+ export declare function receiver(cx: Ctx, fi: CodeFile, body: string, chain: string[], fromContainer: string | null, fromDecl: Decl | null, depth?: number): Rt;
31
+ export declare function reaches(cx: Ctx, rt: Rt, ownerDecl: Decl | null, owner: string, method: string, ownerPath: string): Reach;
32
+ export declare function inTypePosition(body: string, idx: number, lang: Lang): boolean;