devlensio 0.4.2 → 0.4.3
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/README.md +2 -2
- package/dist/graph/edges/apiFetchEdges.js +67 -0
- package/dist/parser/extractors/functions.d.ts +5 -0
- package/dist/parser/extractors/functions.js +5 -5
- package/dist/parser/extractors/objectMethods.d.ts +4 -0
- package/dist/parser/extractors/objectMethods.js +141 -0
- package/dist/parser/index.js +3 -1
- package/dist/parser/index.test.js +130 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ Repo path
|
|
|
29
29
|
[2] Filesystem scan extract routes (Next.js app/pages, Express, Fastify, Koa)
|
|
30
30
|
[3] Parse (ts-morph) walk every .ts/.tsx/.js/.jsx → nodes (typed params, return types, prop types)
|
|
31
31
|
[4] Edge detection many detectors → CALLS, IMPORTS, READS_FROM, WRITES_TO, PROP_PASS, EMITS,
|
|
32
|
-
LISTENS, WRAPPED_BY, GUARDS, HANDLES, TESTS, USES
|
|
32
|
+
LISTENS, WRAPPED_BY, GUARDS, HANDLES, TESTS, USES, NEXTJS_API_CALL
|
|
33
33
|
[5] Scoring multi-pass importance scoring + noise filtering (no AI)
|
|
34
34
|
[6] Clustering cohesive cluster assignment
|
|
35
35
|
[7] Summarize (optional) topologically-ordered LLM summaries, checkpoint/resume, MapReduce
|
|
@@ -73,7 +73,7 @@ Also exported: all core types (`CodeNode`, `CodeEdge`, `NodeType`, `EdgeType`,
|
|
|
73
73
|
### Node & edge types
|
|
74
74
|
|
|
75
75
|
- **Node types:** `COMPONENT`, `HOOK`, `FUNCTION`, `STATE_STORE`, `UTILITY`, `FILE`, `ROUTE`, `TEST`, `STORY`, `THIRD_PARTY` (+ internal `GHOST`).
|
|
76
|
-
- **Edge types:** `CALLS`, `IMPORTS`, `READS_FROM`, `WRITES_TO`, `PROP_PASS`, `EMITS`, `LISTENS`, `WRAPPED_BY`, `GUARDS`, `HANDLES`, `TESTS`, `USES
|
|
76
|
+
- **Edge types:** `CALLS`, `IMPORTS`, `READS_FROM`, `WRITES_TO`, `PROP_PASS`, `EMITS`, `LISTENS`, `WRAPPED_BY`, `GUARDS`, `HANDLES`, `TESTS`, `USES`, `NEXTJS_API_CALL` (frontend `fetch`/`axios`/`useSWR` call site → Next.js API route).
|
|
77
77
|
- Each node carries an importance score and (after summarization) a technical summary, a business summary, and a security assessment (`none|low|medium|high` + notes).
|
|
78
78
|
|
|
79
79
|
---
|
|
@@ -207,6 +207,15 @@ function extractUrlFromArg(arg, sourceFile, project) {
|
|
|
207
207
|
// For each call matching CALLER_CONFIG:
|
|
208
208
|
// - infers the HTTP method
|
|
209
209
|
// - extracts and resolves the URL argument
|
|
210
|
+
// It additionally scans for backend routes used as a resource URL:
|
|
211
|
+
// - JSX src/href attributes — tag-agnostic, so it covers native elements
|
|
212
|
+
// (<video src="/api/..">, <iframe src={url}>, <a href={`/api/${id}`}>)
|
|
213
|
+
// AND third-party player components (<HlsPlayer src={url} />,
|
|
214
|
+
// <ReactHlsPlayer src={...} />).
|
|
215
|
+
// - object-literal src/href properties — config-object style usage where a
|
|
216
|
+
// route is handed to a third-party player/function, e.g.
|
|
217
|
+
// loadPlayer({ src: '/api/stream' }).
|
|
218
|
+
// The browser issues a GET for any such resource, so these are emitted as GET.
|
|
210
219
|
// The expensive AST traversal happens once per file here; callers then
|
|
211
220
|
// attribute the resulting calls to nodes by line range (see detectNextjsApiCallEdges).
|
|
212
221
|
// Note: useQuery/useMutation inner fetch/axios calls are captured naturally
|
|
@@ -270,6 +279,64 @@ function extractApiCallsFromFile(sourceFile, project) {
|
|
|
270
279
|
},
|
|
271
280
|
});
|
|
272
281
|
}
|
|
282
|
+
// JSX src/href attributes — tag-agnostic, so native elements (video, iframe,
|
|
283
|
+
// a) and third-party player components (HlsPlayer, ReactHlsPlayer, …) are all
|
|
284
|
+
// covered. The browser fetches the resource with GET.
|
|
285
|
+
for (const attr of sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
|
|
286
|
+
const attrName = attr.getNameNode().getText();
|
|
287
|
+
if (attrName !== "src" && attrName !== "href")
|
|
288
|
+
continue;
|
|
289
|
+
const initializer = attr.getInitializer();
|
|
290
|
+
if (!initializer)
|
|
291
|
+
continue;
|
|
292
|
+
// value is either a bare string (src="/api/x") or a JSX expression
|
|
293
|
+
// container (src={url} / src={`/api/${id}`})
|
|
294
|
+
let valueNode;
|
|
295
|
+
const initKind = initializer.getKind();
|
|
296
|
+
if (initKind === SyntaxKind.StringLiteral) {
|
|
297
|
+
valueNode = initializer;
|
|
298
|
+
}
|
|
299
|
+
else if (initKind === SyntaxKind.JsxExpression) {
|
|
300
|
+
valueNode = initializer.getExpression();
|
|
301
|
+
}
|
|
302
|
+
if (!valueNode)
|
|
303
|
+
continue;
|
|
304
|
+
const urlResult = extractUrlFromArg(valueNode, sourceFile, project);
|
|
305
|
+
if (!urlResult)
|
|
306
|
+
continue;
|
|
307
|
+
results.push({
|
|
308
|
+
startLine: attr.getStartLineNumber(),
|
|
309
|
+
call: {
|
|
310
|
+
callerType: `jsx-${attrName}`,
|
|
311
|
+
rawUrl: urlResult.rawUrl,
|
|
312
|
+
resolvedUrl: urlResult.resolvedUrl,
|
|
313
|
+
method: "GET",
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// Object-literal src/href properties — config-object style usage where a
|
|
318
|
+
// backend route is passed to a third-party player/function, e.g.
|
|
319
|
+
// loadPlayer({ src: '/api/stream' }). Same GET semantics.
|
|
320
|
+
for (const prop of sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
321
|
+
const name = prop.getName();
|
|
322
|
+
if (name !== "src" && name !== "href")
|
|
323
|
+
continue;
|
|
324
|
+
const init = prop.getInitializer();
|
|
325
|
+
if (!init)
|
|
326
|
+
continue;
|
|
327
|
+
const urlResult = extractUrlFromArg(init, sourceFile, project);
|
|
328
|
+
if (!urlResult)
|
|
329
|
+
continue;
|
|
330
|
+
results.push({
|
|
331
|
+
startLine: prop.getStartLineNumber(),
|
|
332
|
+
call: {
|
|
333
|
+
callerType: `prop-${name}`,
|
|
334
|
+
rawUrl: urlResult.rawUrl,
|
|
335
|
+
resolvedUrl: urlResult.resolvedUrl,
|
|
336
|
+
method: "GET",
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
273
340
|
return results;
|
|
274
341
|
}
|
|
275
342
|
//MAIN FUNCTION
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { SourceFile } from "ts-morph";
|
|
2
2
|
import type { CodeNode } from "../../types.js";
|
|
3
3
|
import { type RenderingBoundary } from "../directives.js";
|
|
4
|
+
export declare function extractFunctionCalls(node: any): string[];
|
|
5
|
+
export declare function extractHookCalls(node: any): string[];
|
|
6
|
+
export declare function extractApiCalls(node: any): string[];
|
|
7
|
+
export declare function hasErrorHandling(node: any): boolean;
|
|
8
|
+
export declare function extractThrowStatements(node: any): boolean;
|
|
4
9
|
export declare function extractFunctions(file: SourceFile, fileDirective?: RenderingBoundary): CodeNode[];
|
|
@@ -7,7 +7,7 @@ const HTTP_METHOD_EXPORTS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "H
|
|
|
7
7
|
function makeId(filePath, name) {
|
|
8
8
|
return `${filePath}::${name}`;
|
|
9
9
|
}
|
|
10
|
-
function extractFunctionCalls(node) {
|
|
10
|
+
export function extractFunctionCalls(node) {
|
|
11
11
|
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
12
12
|
const names = [];
|
|
13
13
|
for (const call of calls) {
|
|
@@ -22,7 +22,7 @@ function extractFunctionCalls(node) {
|
|
|
22
22
|
}
|
|
23
23
|
return [...new Set(names)];
|
|
24
24
|
}
|
|
25
|
-
function extractHookCalls(node) {
|
|
25
|
+
export function extractHookCalls(node) {
|
|
26
26
|
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
27
27
|
const hooks = [];
|
|
28
28
|
for (const call of calls) {
|
|
@@ -34,7 +34,7 @@ function extractHookCalls(node) {
|
|
|
34
34
|
}
|
|
35
35
|
return [...new Set(hooks)];
|
|
36
36
|
}
|
|
37
|
-
function extractApiCalls(node) {
|
|
37
|
+
export function extractApiCalls(node) {
|
|
38
38
|
const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
39
39
|
const apiCalls = [];
|
|
40
40
|
for (const call of calls) {
|
|
@@ -75,11 +75,11 @@ function extractApiCalls(node) {
|
|
|
75
75
|
}
|
|
76
76
|
return [...new Set(apiCalls)];
|
|
77
77
|
}
|
|
78
|
-
function hasErrorHandling(node) {
|
|
78
|
+
export function hasErrorHandling(node) {
|
|
79
79
|
const tryCatch = node.getDescendantsOfKind(SyntaxKind.TryStatement);
|
|
80
80
|
return tryCatch.length > 0;
|
|
81
81
|
}
|
|
82
|
-
function extractThrowStatements(node) {
|
|
82
|
+
export function extractThrowStatements(node) {
|
|
83
83
|
const throws = node.getDescendantsOfKind(SyntaxKind.ThrowStatement);
|
|
84
84
|
return throws.length > 0;
|
|
85
85
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { SyntaxKind } from "ts-morph";
|
|
2
|
+
import { detectFunctionDirective } from "../directives.js";
|
|
3
|
+
import { extractFunctionCalls, extractHookCalls, extractApiCalls, hasErrorHandling, extractThrowStatements, } from "./functions.js";
|
|
4
|
+
import { extractParams, extractReturnTypeAnnotation, extractBareTypeNames, extractReferencedInterfaces, } from "../typeUtils.js";
|
|
5
|
+
// Extracts function-valued properties of object literals as FUNCTION nodes.
|
|
6
|
+
//
|
|
7
|
+
// Many React/Next.js modules keep their logic inside an exported object literal
|
|
8
|
+
// (provider adapters, route tables, command maps, strategy/config objects). The
|
|
9
|
+
// other extractors (declarations, var-assigned arrows, components, hooks) never
|
|
10
|
+
// descend into object literals, so those functions had no node to anchor edges
|
|
11
|
+
// to — every CALLS / NEXTJS_API_CALL / READS_FROM edge silently dropped.
|
|
12
|
+
//
|
|
13
|
+
// The node `name` encodes the dotted access path (e.g.
|
|
14
|
+
// "providersConfig.zoro.streamingData"). index.ts builds the id as
|
|
15
|
+
// `${relativePath}::${name}`, so this yields the desired node identity for free.
|
|
16
|
+
function makeId(filePath, name) {
|
|
17
|
+
return `${filePath}::${name}`;
|
|
18
|
+
}
|
|
19
|
+
// Unwraps `as const` / `as Foo` and parenthesized expressions, returning the
|
|
20
|
+
// underlying ObjectLiteralExpression node, or null if it isn't one.
|
|
21
|
+
function asObjectLiteral(node) {
|
|
22
|
+
let n = node;
|
|
23
|
+
while (n &&
|
|
24
|
+
(n.getKind() === SyntaxKind.AsExpression ||
|
|
25
|
+
n.getKind() === SyntaxKind.ParenthesizedExpression)) {
|
|
26
|
+
n = n.getExpression?.();
|
|
27
|
+
}
|
|
28
|
+
return n && n.getKind() === SyntaxKind.ObjectLiteralExpression ? n : null;
|
|
29
|
+
}
|
|
30
|
+
// True for `async (...) => {}`, `async function () {}`, `async key() {}`.
|
|
31
|
+
function isAsyncFn(fnNode) {
|
|
32
|
+
if (typeof fnNode.isAsync === "function")
|
|
33
|
+
return fnNode.isAsync();
|
|
34
|
+
return fnNode.getText().trimStart().startsWith("async");
|
|
35
|
+
}
|
|
36
|
+
// Builds a FUNCTION node from a function-like node + the span node that should
|
|
37
|
+
// own its line range / rawCode. The metadata shape mirrors functions.ts exactly
|
|
38
|
+
// so callEdges / apiFetchEdges treat these like any other FUNCTION node.
|
|
39
|
+
// fnNode — ArrowFunction | FunctionExpression | MethodDeclaration (params/body)
|
|
40
|
+
// spanNode— the PropertyAssignment (`key: fn`) or the MethodDeclaration itself,
|
|
41
|
+
// so the inner call's line falls inside [startLine, endLine].
|
|
42
|
+
function buildNode(file, dottedName, fnNode, spanNode, fileDirective) {
|
|
43
|
+
const filePath = file.getFilePath();
|
|
44
|
+
const typedParams = extractParams(fnNode);
|
|
45
|
+
const calls = extractFunctionCalls(fnNode);
|
|
46
|
+
const hookCalls = extractHookCalls(fnNode);
|
|
47
|
+
const apiCalls = extractApiCalls(fnNode);
|
|
48
|
+
const isAsync = isAsyncFn(fnNode);
|
|
49
|
+
const hasErrors = hasErrorHandling(fnNode);
|
|
50
|
+
const throws = extractThrowStatements(fnNode);
|
|
51
|
+
const renderingBoundary = detectFunctionDirective(fnNode.getBody?.()) ?? fileDirective;
|
|
52
|
+
const returnType = extractReturnTypeAnnotation(fnNode);
|
|
53
|
+
const bareTypeNames = extractBareTypeNames([...typedParams.map((p) => p.type), returnType]);
|
|
54
|
+
const referencedTypes = extractReferencedInterfaces(file, bareTypeNames);
|
|
55
|
+
return {
|
|
56
|
+
id: makeId(filePath, dottedName),
|
|
57
|
+
name: dottedName,
|
|
58
|
+
type: "FUNCTION",
|
|
59
|
+
filePath,
|
|
60
|
+
startLine: spanNode.getStartLineNumber(),
|
|
61
|
+
endLine: spanNode.getEndLineNumber(),
|
|
62
|
+
rawCode: spanNode.getText(),
|
|
63
|
+
metadata: {
|
|
64
|
+
params: typedParams.map((p) => p.name),
|
|
65
|
+
parameters: typedParams,
|
|
66
|
+
returnType,
|
|
67
|
+
referencedTypes,
|
|
68
|
+
calls,
|
|
69
|
+
hookCalls,
|
|
70
|
+
apiCalls,
|
|
71
|
+
isAsync,
|
|
72
|
+
hasErrorHandling: hasErrors,
|
|
73
|
+
throws,
|
|
74
|
+
lineCount: spanNode.getEndLineNumber() - spanNode.getStartLineNumber(),
|
|
75
|
+
isHttpHandler: false,
|
|
76
|
+
httpMethod: undefined,
|
|
77
|
+
...(renderingBoundary !== null && { renderingBoundary }),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Recursively walks an object literal, emitting a node per function-valued
|
|
82
|
+
// property and descending into nested object literals to arbitrary depth.
|
|
83
|
+
function walkObject(file, objLiteral, prefix, fileDirective, out) {
|
|
84
|
+
// getProperties() returns a union (PropertyAssignment | ShorthandPropertyAssignment
|
|
85
|
+
// | SpreadAssignment | MethodDeclaration | Get/SetAccessor) — guard every getter.
|
|
86
|
+
for (const prop of objLiteral.getProperties()) {
|
|
87
|
+
const kind = prop.getKind();
|
|
88
|
+
// Shorthand method: streamingData(args) { ... } (incl. async)
|
|
89
|
+
if (kind === SyntaxKind.MethodDeclaration) {
|
|
90
|
+
const name = prop.getName?.();
|
|
91
|
+
if (!name)
|
|
92
|
+
continue;
|
|
93
|
+
out.push(buildNode(file, `${prefix}.${name}`, prop, prop, fileDirective));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Everything else we handle needs a name + initializer.
|
|
97
|
+
if (kind !== SyntaxKind.PropertyAssignment)
|
|
98
|
+
continue; // skip spread/shorthand/get/set
|
|
99
|
+
const name = prop.getName?.();
|
|
100
|
+
const init = prop.getInitializer?.();
|
|
101
|
+
if (!name || !init)
|
|
102
|
+
continue;
|
|
103
|
+
const ik = init.getKind();
|
|
104
|
+
if (ik === SyntaxKind.ArrowFunction || ik === SyntaxKind.FunctionExpression) {
|
|
105
|
+
// span = the PropertyAssignment so the range wraps `key: <fn>`.
|
|
106
|
+
out.push(buildNode(file, `${prefix}.${name}`, init, prop, fileDirective));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Nested object → recurse (unwrap `as const` / parens).
|
|
110
|
+
const nested = asObjectLiteral(init);
|
|
111
|
+
if (nested) {
|
|
112
|
+
walkObject(file, nested, `${prefix}.${name}`, fileDirective, out);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function extractObjectMethods(file, fileDirective = null) {
|
|
117
|
+
const out = [];
|
|
118
|
+
// Roots: named object literals — `const x = { ... }` (incl. `as const`).
|
|
119
|
+
for (const variable of file.getVariableDeclarations()) {
|
|
120
|
+
const init = variable.getInitializer();
|
|
121
|
+
if (!init)
|
|
122
|
+
continue;
|
|
123
|
+
const obj = asObjectLiteral(init);
|
|
124
|
+
if (!obj)
|
|
125
|
+
continue;
|
|
126
|
+
walkObject(file, obj, variable.getName(), fileDirective, out);
|
|
127
|
+
}
|
|
128
|
+
// Root: `export default { ... }` — fall back to the "default" prefix.
|
|
129
|
+
for (const assign of file.getExportAssignments()) {
|
|
130
|
+
if (assign.isExportEquals?.())
|
|
131
|
+
continue; // skip `export = ...`
|
|
132
|
+
const expr = assign.getExpression?.();
|
|
133
|
+
if (!expr)
|
|
134
|
+
continue;
|
|
135
|
+
const obj = asObjectLiteral(expr);
|
|
136
|
+
if (!obj)
|
|
137
|
+
continue;
|
|
138
|
+
walkObject(file, obj, "default", fileDirective, out);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
package/dist/parser/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { extractComponents } from "./extractors/components.js";
|
|
|
5
5
|
import { extractHooks } from "./extractors/hooks.js";
|
|
6
6
|
import { extractFunctions } from "./extractors/functions.js";
|
|
7
7
|
import { extractStores } from "./extractors/stores.js";
|
|
8
|
+
import { extractObjectMethods } from "./extractors/objectMethods.js";
|
|
8
9
|
import { detectFileDirective } from "./directives.js";
|
|
9
10
|
import { createHash } from "crypto";
|
|
10
11
|
// Directories to skip entirely while walking
|
|
@@ -107,7 +108,8 @@ export function parseRepo(repoPath) {
|
|
|
107
108
|
const hooks = extractHooks(file, fileDirective);
|
|
108
109
|
const functions = extractFunctions(file, fileDirective);
|
|
109
110
|
const stores = extractStores(file);
|
|
110
|
-
const
|
|
111
|
+
const objectMethods = extractObjectMethods(file, fileDirective);
|
|
112
|
+
const extracted = [...components, ...hooks, ...functions, ...stores, ...objectMethods];
|
|
111
113
|
for (const node of extracted) {
|
|
112
114
|
// Normalize all extracted nodes to relative paths so every node in the
|
|
113
115
|
// graph uses the same coordinate system as the FILE nodes.
|
|
@@ -316,4 +316,134 @@ describe("parseRepo", () => {
|
|
|
316
316
|
expect(configNode).toBeUndefined();
|
|
317
317
|
deleteFakeRepo(repoPath);
|
|
318
318
|
});
|
|
319
|
+
// ─── Object-literal Method Extraction ──────────────────────────────────────
|
|
320
|
+
it("should detect an arrow-function value in an object literal", () => {
|
|
321
|
+
const repoPath = createFakeRepo({
|
|
322
|
+
"src/cfg.ts": `
|
|
323
|
+
export const cfg = {
|
|
324
|
+
load: () => doThing(),
|
|
325
|
+
};
|
|
326
|
+
`,
|
|
327
|
+
});
|
|
328
|
+
const result = parseRepo(repoPath);
|
|
329
|
+
const fn = result.nodes.find((n) => n.name === "cfg.load");
|
|
330
|
+
expect(fn).toBeDefined();
|
|
331
|
+
expect(fn?.type).toBe("FUNCTION");
|
|
332
|
+
expect(fn?.startLine).toBeGreaterThanOrEqual(1);
|
|
333
|
+
expect(fn?.endLine).toBeGreaterThanOrEqual(fn.startLine);
|
|
334
|
+
expect(fn?.metadata.calls).toContain("doThing");
|
|
335
|
+
deleteFakeRepo(repoPath);
|
|
336
|
+
});
|
|
337
|
+
it("should detect an async arrow value calling axios (nested object)", () => {
|
|
338
|
+
const repoPath = createFakeRepo({
|
|
339
|
+
"src/providersConfig.js": `
|
|
340
|
+
export const providersConfig = {
|
|
341
|
+
zoro: {
|
|
342
|
+
streamingData: async (episodeId) => {
|
|
343
|
+
const response = await axios.get('/api/v2/zoro/watch/' + episodeId);
|
|
344
|
+
return response.data;
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
`,
|
|
349
|
+
});
|
|
350
|
+
const result = parseRepo(repoPath);
|
|
351
|
+
const fn = result.nodes.find((n) => n.name === "providersConfig.zoro.streamingData");
|
|
352
|
+
expect(fn).toBeDefined();
|
|
353
|
+
expect(fn?.type).toBe("FUNCTION");
|
|
354
|
+
expect(fn?.metadata.isAsync).toBe(true);
|
|
355
|
+
expect((fn?.metadata.apiCalls).length).toBeGreaterThan(0);
|
|
356
|
+
deleteFakeRepo(repoPath);
|
|
357
|
+
});
|
|
358
|
+
it("should detect a function-expression value in an object literal", () => {
|
|
359
|
+
const repoPath = createFakeRepo({
|
|
360
|
+
"src/cfg.ts": `
|
|
361
|
+
export const cfg = {
|
|
362
|
+
run: function (x) { return helper(x); },
|
|
363
|
+
};
|
|
364
|
+
`,
|
|
365
|
+
});
|
|
366
|
+
const result = parseRepo(repoPath);
|
|
367
|
+
const fn = result.nodes.find((n) => n.name === "cfg.run");
|
|
368
|
+
expect(fn).toBeDefined();
|
|
369
|
+
expect(fn?.type).toBe("FUNCTION");
|
|
370
|
+
expect(fn?.metadata.params).toContain("x");
|
|
371
|
+
deleteFakeRepo(repoPath);
|
|
372
|
+
});
|
|
373
|
+
it("should detect a shorthand method in an object literal", () => {
|
|
374
|
+
const repoPath = createFakeRepo({
|
|
375
|
+
"src/cfg.ts": `
|
|
376
|
+
export const cfg = {
|
|
377
|
+
fetchData(url) { return fetch(url); },
|
|
378
|
+
};
|
|
379
|
+
`,
|
|
380
|
+
});
|
|
381
|
+
const result = parseRepo(repoPath);
|
|
382
|
+
const fn = result.nodes.find((n) => n.name === "cfg.fetchData");
|
|
383
|
+
expect(fn).toBeDefined();
|
|
384
|
+
expect(fn?.type).toBe("FUNCTION");
|
|
385
|
+
deleteFakeRepo(repoPath);
|
|
386
|
+
});
|
|
387
|
+
it("should detect a concise-body arrow value in an object literal", () => {
|
|
388
|
+
const repoPath = createFakeRepo({
|
|
389
|
+
"src/cfg.ts": `
|
|
390
|
+
export const cfg = {
|
|
391
|
+
pick: (u) => u.id,
|
|
392
|
+
};
|
|
393
|
+
`,
|
|
394
|
+
});
|
|
395
|
+
const result = parseRepo(repoPath);
|
|
396
|
+
const fn = result.nodes.find((n) => n.name === "cfg.pick");
|
|
397
|
+
expect(fn).toBeDefined();
|
|
398
|
+
expect(fn?.type).toBe("FUNCTION");
|
|
399
|
+
deleteFakeRepo(repoPath);
|
|
400
|
+
});
|
|
401
|
+
it("should detect a function in a two-level nested object", () => {
|
|
402
|
+
const repoPath = createFakeRepo({
|
|
403
|
+
"src/api.ts": `
|
|
404
|
+
export const api = {
|
|
405
|
+
v2: {
|
|
406
|
+
users: {
|
|
407
|
+
list: async () => fetch('/api/users'),
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
`,
|
|
412
|
+
});
|
|
413
|
+
const result = parseRepo(repoPath);
|
|
414
|
+
const fn = result.nodes.find((n) => n.name === "api.v2.users.list");
|
|
415
|
+
expect(fn).toBeDefined();
|
|
416
|
+
expect(fn?.type).toBe("FUNCTION");
|
|
417
|
+
deleteFakeRepo(repoPath);
|
|
418
|
+
});
|
|
419
|
+
it("should detect a function in an export-default object", () => {
|
|
420
|
+
const repoPath = createFakeRepo({
|
|
421
|
+
"src/handler.ts": `
|
|
422
|
+
export default {
|
|
423
|
+
handler: async () => fetch('/api/x'),
|
|
424
|
+
};
|
|
425
|
+
`,
|
|
426
|
+
});
|
|
427
|
+
const result = parseRepo(repoPath);
|
|
428
|
+
const fn = result.nodes.find((n) => n.name === "default.handler");
|
|
429
|
+
expect(fn).toBeDefined();
|
|
430
|
+
expect(fn?.type).toBe("FUNCTION");
|
|
431
|
+
deleteFakeRepo(repoPath);
|
|
432
|
+
});
|
|
433
|
+
it("should not double-extract or collide with top-level hooks", () => {
|
|
434
|
+
const repoPath = createFakeRepo({
|
|
435
|
+
"src/mix.ts": `
|
|
436
|
+
export const useThing = () => { return 1; };
|
|
437
|
+
export const cfg = {
|
|
438
|
+
a: () => doA(),
|
|
439
|
+
};
|
|
440
|
+
`,
|
|
441
|
+
});
|
|
442
|
+
const result = parseRepo(repoPath);
|
|
443
|
+
const objMethods = result.nodes.filter((n) => n.name === "cfg.a");
|
|
444
|
+
expect(objMethods.length).toBe(1);
|
|
445
|
+
const hook = result.nodes.find((n) => n.name === "useThing");
|
|
446
|
+
expect(hook?.type).toBe("HOOK");
|
|
447
|
+
deleteFakeRepo(repoPath);
|
|
448
|
+
});
|
|
319
449
|
});
|