@eventra_dev/eventra-cli 2.3.0 → 2.3.1

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.
@@ -8,6 +8,7 @@ const typescript_1 = __importDefault(require("typescript"));
8
8
  const resolver_1 = require("../resolver/resolver");
9
9
  const evaluationContext_1 = require("../shared/evaluationContext");
10
10
  const propagationExtractor_1 = require("./propagationExtractor");
11
+ const symbolUtils_1 = require("../symbols/symbolUtils");
11
12
  const eventValidation_1 = require("../shared/eventValidation");
12
13
  const dynamicOccurrence_1 = require("../shared/dynamicOccurrence");
13
14
  function extractEvents(index, checker, _config, evaluationCache, exportCache, resolvedCallCache, returnPropagationCache, wrapperRegistry) {
@@ -38,7 +39,7 @@ function extractEvents(index, checker, _config, evaluationCache, exportCache, re
38
39
  visit(index.sourceFile);
39
40
  return {
40
41
  events,
41
- detectedFunctionWrappers: new Set(index.wrappers.map(wrapper => wrapper.symbol.getName())),
42
+ detectedFunctionWrappers: new Set(index.wrappers.map(wrapper => (0, symbolUtils_1.wrapperName)(wrapper.declaration, wrapper.symbol))),
42
43
  dynamicOccurrences,
43
44
  };
44
45
  }
@@ -9,6 +9,59 @@ const exportResolver_1 = require("./exportResolver");
9
9
  function isFunctionLike(node) {
10
10
  return (typescript_1.default.isFunctionDeclaration(node) || typescript_1.default.isMethodDeclaration(node) || typescript_1.default.isArrowFunction(node) || typescript_1.default.isFunctionExpression(node));
11
11
  }
12
+ /** `return expr` statements of a function body, not looking into functions nested in it. */
13
+ function returnedExpressions(fn) {
14
+ if (!fn.body) {
15
+ return [];
16
+ }
17
+ if (!typescript_1.default.isBlock(fn.body)) {
18
+ return [fn.body];
19
+ }
20
+ const found = [];
21
+ const visit = (node) => {
22
+ if (typescript_1.default.isFunctionLike(node)) {
23
+ return;
24
+ }
25
+ if (typescript_1.default.isReturnStatement(node) && node.expression) {
26
+ found.push(node.expression);
27
+ }
28
+ typescript_1.default.forEachChild(node, visit);
29
+ };
30
+ visit(fn.body);
31
+ return found;
32
+ }
33
+ const MAX_RETURN_HOPS = 4;
34
+ /**
35
+ * The function a call hands back: `useTracker()` for `function useTracker() { return (n) => t.track(n) }`,
36
+ * where the returned value is a function expression or a name bound to one.
37
+ */
38
+ function resolveReturnedFunction(call, checker, callCache, exportCache, hops) {
39
+ if (hops > MAX_RETURN_HOPS) {
40
+ return null;
41
+ }
42
+ const factory = resolveFunctionFromCall(call.expression, checker, callCache, exportCache, hops + 1);
43
+ if (!factory) {
44
+ return null;
45
+ }
46
+ for (const returned of returnedExpressions(factory)) {
47
+ let value = returned;
48
+ while (typescript_1.default.isParenthesizedExpression(value) || typescript_1.default.isAsExpression(value) || typescript_1.default.isNonNullExpression(value)) {
49
+ value = value.expression;
50
+ }
51
+ if (isFunctionLike(value)) {
52
+ return value;
53
+ }
54
+ if (typescript_1.default.isIdentifier(value)) {
55
+ for (const next of checker.getSymbolAtLocation(value)?.getDeclarations() ?? []) {
56
+ const resolved = resolveDeclaration(next, checker);
57
+ if (resolved) {
58
+ return resolved;
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return null;
64
+ }
12
65
  function resolveDeclaration(declaration, checker) {
13
66
  // function foo() {}
14
67
  if (isFunctionLike(declaration)) {
@@ -64,7 +117,15 @@ function getExpressionSymbol(expression, checker) {
64
117
  // track()
65
118
  return checker.getSymbolAtLocation(expression);
66
119
  }
67
- function resolveFunctionFromCall(expression, checker, callCache, exportCache) {
120
+ function resolveFunctionFromCall(expression, checker, callCache, exportCache, hops = 0) {
121
+ // useTracker()("x")
122
+ let callee = expression;
123
+ while (typescript_1.default.isParenthesizedExpression(callee)) {
124
+ callee = callee.expression;
125
+ }
126
+ if (typescript_1.default.isCallExpression(callee)) {
127
+ return resolveReturnedFunction(callee, checker, callCache, exportCache, hops);
128
+ }
68
129
  let symbol = getExpressionSymbol(expression, checker);
69
130
  if (!symbol) {
70
131
  return null;
@@ -83,7 +144,10 @@ function resolveFunctionFromCall(expression, checker, callCache, exportCache) {
83
144
  // declaration resolution
84
145
  const declarations = symbol.getDeclarations() ?? [];
85
146
  for (const declaration of declarations) {
86
- const resolved = resolveDeclaration(declaration, checker);
147
+ // const track = useTracker(); track("x")
148
+ const resolved = resolveDeclaration(declaration, checker) ?? (typescript_1.default.isVariableDeclaration(declaration) && declaration.initializer && typescript_1.default.isCallExpression(declaration.initializer)
149
+ ? resolveReturnedFunction(declaration.initializer, checker, callCache, exportCache, hops)
150
+ : null);
87
151
  if (!resolved) {
88
152
  continue;
89
153
  }
@@ -91,7 +91,12 @@ function isEventraNewExpression(node, checker) {
91
91
  return false;
92
92
  }
93
93
  const symbol = checker.getSymbolAtLocation(expr);
94
- return symbol ? isEventraSymbol(symbol, checker) : false;
94
+ if (symbol && isEventraSymbol(symbol, checker)) {
95
+ return true;
96
+ }
97
+ // `const { Eventra } = await import("@eventra_dev/eventra-sdk")`: the binding is not an import, but
98
+ // the instance it builds is still typed as the SDK's class
99
+ return typeLooksLikeEventra(checker.getTypeAtLocation(node), checker);
95
100
  }
96
101
  function typeLooksLikeEventra(type, checker, depth = 0) {
97
102
  // cycle guard - TypeScript flattens union/intersection types (a literal
@@ -115,7 +120,63 @@ function typeLooksLikeEventra(type, checker, depth = 0) {
115
120
  }
116
121
  return false;
117
122
  }
118
- function isEventraReceiverExpression(receiver, checker) {
123
+ const MAX_RECEIVER_HOPS = 6;
124
+ /** `return expr` statements of a function body, not looking into functions nested in it. */
125
+ function returnedExpressions(fn) {
126
+ if (!fn.body) {
127
+ return [];
128
+ }
129
+ if (!typescript_1.default.isBlock(fn.body)) {
130
+ return [fn.body];
131
+ }
132
+ const found = [];
133
+ const visit = (node) => {
134
+ if (typescript_1.default.isFunctionLike(node)) {
135
+ return;
136
+ }
137
+ if (typescript_1.default.isReturnStatement(node) && node.expression) {
138
+ found.push(node.expression);
139
+ }
140
+ typescript_1.default.forEachChild(node, visit);
141
+ };
142
+ visit(fn.body);
143
+ return found;
144
+ }
145
+ function functionOf(decl) {
146
+ if (typescript_1.default.isFunctionDeclaration(decl) || typescript_1.default.isMethodDeclaration(decl)) {
147
+ return decl;
148
+ }
149
+ if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer && (typescript_1.default.isArrowFunction(decl.initializer) || typescript_1.default.isFunctionExpression(decl.initializer))) {
150
+ return decl.initializer;
151
+ }
152
+ return undefined;
153
+ }
154
+ /** `t = new Eventra(...)` anywhere in the file of a variable's declaration: a lazily filled `let t: any`. */
155
+ function assignedValues(symbol, checker) {
156
+ const values = [];
157
+ const files = new Set((symbol.getDeclarations() ?? []).map((decl) => decl.getSourceFile()));
158
+ for (const file of files) {
159
+ const visit = (node) => {
160
+ if (typescript_1.default.isBinaryExpression(node) &&
161
+ node.operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken &&
162
+ typescript_1.default.isIdentifier(node.left) &&
163
+ checker.getSymbolAtLocation(node.left) === symbol) {
164
+ values.push(node.right);
165
+ }
166
+ typescript_1.default.forEachChild(node, visit);
167
+ };
168
+ visit(file);
169
+ }
170
+ return values;
171
+ }
172
+ function isEventraReceiverExpression(receiver, checker, hops = 0) {
173
+ if (hops > MAX_RECEIVER_HOPS) {
174
+ return false;
175
+ }
176
+ // `(await getTracker()).track(...)`, `(tracker as Eventra).track(...)`, `tracker!.track(...)`
177
+ if (typescript_1.default.isParenthesizedExpression(receiver) || typescript_1.default.isAwaitExpression(receiver) || typescript_1.default.isNonNullExpression(receiver) || typescript_1.default.isAsExpression(receiver)) {
178
+ return isEventraReceiverExpression(receiver.expression, checker, hops + 1);
179
+ }
119
180
  if (typescript_1.default.isNewExpression(receiver)) {
120
181
  return isEventraNewExpression(receiver, checker);
121
182
  }
@@ -126,6 +187,18 @@ function isEventraReceiverExpression(receiver, checker) {
126
187
  if (typeLooksLikeEventra(type, checker)) {
127
188
  return true;
128
189
  }
190
+ // `getTracker().track(...)` where the getter's type is lost (`any`) but what it returns is a tracker
191
+ if (typescript_1.default.isCallExpression(receiver)) {
192
+ const callee = typescript_1.default.isIdentifier(receiver.expression) || typescript_1.default.isPropertyAccessExpression(receiver.expression)
193
+ ? checker.getSymbolAtLocation(typescript_1.default.isIdentifier(receiver.expression) ? receiver.expression : receiver.expression.name)
194
+ : undefined;
195
+ for (const decl of callee?.getDeclarations() ?? []) {
196
+ const fn = functionOf(decl);
197
+ if (fn && returnedExpressions(fn).some((returned) => isEventraReceiverExpression(returned, checker, hops + 1))) {
198
+ return true;
199
+ }
200
+ }
201
+ }
129
202
  if (typescript_1.default.isIdentifier(receiver)) {
130
203
  const symbol = checker.getSymbolAtLocation(receiver);
131
204
  if (!symbol) {
@@ -134,6 +207,9 @@ function isEventraReceiverExpression(receiver, checker) {
134
207
  // See the identical `?? []` above isEventraSymbol's declaration loop: a
135
208
  // real ts.Symbol here always has a declarations array.
136
209
  /* v8 ignore next */
210
+ if (assignedValues(symbol, checker).some((value) => isEventraReceiverExpression(value, checker, hops + 1))) {
211
+ return true;
212
+ }
137
213
  for (const decl of symbol.getDeclarations() ?? []) {
138
214
  if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
139
215
  if (typescript_1.default.isNewExpression(decl.initializer) && isEventraNewExpression(decl.initializer, checker)) {
@@ -167,7 +243,7 @@ function isEventraReceiverExpression(receiver, checker) {
167
243
  }
168
244
  }
169
245
  if (typescript_1.default.isPropertyAccessExpression(receiver) || typescript_1.default.isPropertyAccessChain(receiver)) {
170
- return isEventraReceiverExpression(receiver.expression, checker);
246
+ return isEventraReceiverExpression(receiver.expression, checker, hops + 1);
171
247
  }
172
248
  return false;
173
249
  }
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getValueSymbol = getValueSymbol;
7
7
  exports.getFunctionSymbol = getFunctionSymbol;
8
+ exports.isReturnedFunction = isReturnedFunction;
9
+ exports.wrapperName = wrapperName;
8
10
  const typescript_1 = __importDefault(require("typescript"));
9
11
  /**
10
12
  * Symbol an identifier *refers to*. For the name in a shorthand property (`{ event }`) a plain
@@ -53,5 +55,44 @@ function getFunctionSymbol(node, checker) {
53
55
  if (typescript_1.default.isExportAssignment(parent)) {
54
56
  return checker.getSymbolAtLocation(node);
55
57
  }
58
+ // `return (name) => ...` / `() => (name) => ...`: a function a hook or factory hands back has no
59
+ // name of its own, but the binder still gives it a symbol, and that is what a call of the result
60
+ // (`const track = useTracker(); track("x")`) is matched against.
61
+ if (isReturnedFunction(node)) {
62
+ return node.symbol;
63
+ }
56
64
  return undefined;
57
65
  }
66
+ /** An arrow/function expression that is the value of a `return`, or the concise body of an arrow. */
67
+ function isReturnedFunction(node) {
68
+ if (!typescript_1.default.isArrowFunction(node) && !typescript_1.default.isFunctionExpression(node)) {
69
+ return false;
70
+ }
71
+ let outer = node.parent;
72
+ while (typescript_1.default.isParenthesizedExpression(outer)) {
73
+ outer = outer.parent;
74
+ }
75
+ return typescript_1.default.isReturnStatement(outer) || (typescript_1.default.isArrowFunction(outer) && unparen(outer.body) === node);
76
+ }
77
+ function unparen(node) {
78
+ while (typescript_1.default.isParenthesizedExpression(node)) {
79
+ node = node.expression;
80
+ }
81
+ return node;
82
+ }
83
+ /**
84
+ * The name a wrapper is reported under: its own, or for an anonymous returned function the hook that
85
+ * returns it (`useTracker` for `function useTracker() { return (n) => t.track(n); }`).
86
+ */
87
+ function wrapperName(declaration, symbol) {
88
+ if (!isReturnedFunction(declaration)) {
89
+ return symbol.getName();
90
+ }
91
+ const owner = typescript_1.default.findAncestor(declaration.parent, (n) => typescript_1.default.isFunctionLike(n));
92
+ const ownerName = owner && owner.name;
93
+ if (ownerName && typescript_1.default.isIdentifier(ownerName)) {
94
+ return ownerName.text;
95
+ }
96
+ const holder = owner?.parent;
97
+ return holder && typescript_1.default.isVariableDeclaration(holder) && typescript_1.default.isIdentifier(holder.name) ? holder.name.text : symbol.getName();
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
5
5
  "license": "MIT",
6
6
  "author": {