@eventra_dev/eventra-cli 2.0.0 → 2.0.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/README.md +14 -0
- package/dist/analysis/extractor/propagationExtractor.js +27 -13
- package/dist/analysis/resolver/resolver.js +73 -23
- package/dist/analysis/scanner/analyzer/propagationAnalyzer.js +20 -9
- package/dist/analysis/shared/utils.js +27 -0
- package/dist/plugin/builtins/eventra-sdk.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,20 @@ function trackFeature(name: string) {
|
|
|
80
80
|
trackFeature("purchase");
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
The event name can also come from a property on the wrapper's parameter — plain access, optional chaining, element access, or destructuring (including aliased and nested):
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
function trackWrapper(payload: { event: string }) {
|
|
87
|
+
sdk.track(payload.event); // also: payload?.event, payload["event"]
|
|
88
|
+
}
|
|
89
|
+
trackWrapper({ event: "checkout" });
|
|
90
|
+
|
|
91
|
+
function trackDestructured({ event: name }: { event: string }) {
|
|
92
|
+
sdk.track(name);
|
|
93
|
+
}
|
|
94
|
+
trackDestructured({ event: "checkout" });
|
|
95
|
+
```
|
|
96
|
+
|
|
83
97
|
### Variables, templates, conditionals
|
|
84
98
|
|
|
85
99
|
```ts
|
|
@@ -62,26 +62,40 @@ function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCa
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
function extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events, dynamicOccurrences) {
|
|
65
|
-
|
|
65
|
+
const argument = call.arguments[propagation.sourceParameterIndex];
|
|
66
66
|
if (!argument) {
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
69
|
+
const context = (0, evaluationContext_1.createEvaluationContext)();
|
|
70
|
+
if (typescript_1.default.isIdentifier(propagation.sourceParameter.name)) {
|
|
71
|
+
// sourceParameter is a plain identifier (`function f(payload)`), so
|
|
72
|
+
// propagation.targetNode — `payload`, `payload.event`, or any expression
|
|
73
|
+
// a plugin builds from it (e.g. a template literal) — can resolve its own
|
|
74
|
+
// propertyPath by evaluating targetNode with `payload` substituted for
|
|
75
|
+
// the *raw*, un-narrowed call-site argument.
|
|
76
|
+
const parameterSymbol = checker.getSymbolAtLocation(propagation.sourceParameter.name);
|
|
77
|
+
if (parameterSymbol) {
|
|
78
|
+
context.parameterBindings.set(parameterSymbol, argument);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// sourceParameter is a destructuring pattern (`function f({ event })`),
|
|
83
|
+
// which has no symbol of its own — `getSymbolAtLocation` on the pattern
|
|
84
|
+
// returns nothing. Narrow to the leaf value ourselves via propertyPath,
|
|
85
|
+
// then bind it to the destructured binding's own symbol (targetNode is
|
|
86
|
+
// that binding's reference, e.g. `event` in `sdk.track(event)`).
|
|
87
|
+
if (propagation.propertyPath.length === 0 || !typescript_1.default.isIdentifier(propagation.targetNode)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
75
90
|
const resolvedPath = resolveObjectPath(argument, propagation.propertyPath);
|
|
76
91
|
if (!resolvedPath) {
|
|
77
92
|
return;
|
|
78
93
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
context.parameterBindings.set(parameterSymbol, argument);
|
|
94
|
+
const bindingSymbol = checker.getSymbolAtLocation(propagation.targetNode);
|
|
95
|
+
if (!bindingSymbol) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
context.parameterBindings.set(bindingSymbol, resolvedPath);
|
|
85
99
|
}
|
|
86
100
|
const resolved = (0, resolver_1.resolveNodeValue)(propagation.targetNode, checker, context, new Set(), evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
87
101
|
for (const value of resolved.values) {
|
|
@@ -9,6 +9,7 @@ const exportResolver_1 = require("./exportResolver");
|
|
|
9
9
|
const returnPropagationAnalyzer_1 = require("../scanner/analyzer/returnPropagationAnalyzer");
|
|
10
10
|
const callResolver_1 = require("./callResolver");
|
|
11
11
|
const resolvedCallCache_1 = require("../cache/resolvedCallCache");
|
|
12
|
+
const utils_1 = require("../shared/utils");
|
|
12
13
|
function unique(values) {
|
|
13
14
|
return [
|
|
14
15
|
...new Set(values),
|
|
@@ -29,39 +30,87 @@ function concat(left, right) {
|
|
|
29
30
|
}
|
|
30
31
|
return unique(result);
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Walks a chain rooted at a parameter-bound identifier — e.g. `payload.nested`
|
|
34
|
+
// inside `payload.nested.event` — down to the object literal it refers to, so
|
|
35
|
+
// resolvePropertyAccess can pick the final property off a nested payload, not
|
|
36
|
+
// just a one-level-deep one.
|
|
37
|
+
function resolveBoundObjectNode(rawNode, checker, context) {
|
|
38
|
+
const node = (0, utils_1.unwrapExpression)(rawNode);
|
|
39
|
+
if (typescript_1.default.isIdentifier(node)) {
|
|
40
|
+
const symbol = checker.getSymbolAtLocation(node);
|
|
41
|
+
if (!symbol || !context.parameterBindings.has(symbol)) {
|
|
35
42
|
return null;
|
|
36
43
|
}
|
|
37
|
-
return
|
|
44
|
+
return context.parameterBindings.get(symbol) ?? null;
|
|
38
45
|
}
|
|
39
|
-
|
|
46
|
+
if (typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isPropertyAccessChain(node) || typescript_1.default.isElementAccessExpression(node)) {
|
|
47
|
+
const propertyName = (0, utils_1.getPropertyName)(node);
|
|
48
|
+
if (!propertyName) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const base = resolveBoundObjectNode(node.expression, checker, context);
|
|
52
|
+
if (!base || !typescript_1.default.isObjectLiteralExpression(base)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
for (const property of base.properties) {
|
|
56
|
+
if (!typescript_1.default.isPropertyAssignment(property) && !typescript_1.default.isShorthandPropertyAssignment(property)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const objectPropertyName = typescript_1.default.isIdentifier(property.name) ? property.name.text : property.name.getText();
|
|
60
|
+
if (objectPropertyName !== propertyName) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
return typescript_1.default.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
40
68
|
}
|
|
41
69
|
function resolvePropertyAccess(node, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache) {
|
|
42
|
-
const propertyName = getPropertyName(node);
|
|
70
|
+
const propertyName = (0, utils_1.getPropertyName)(node);
|
|
43
71
|
if (!propertyName) {
|
|
44
72
|
return empty();
|
|
45
73
|
}
|
|
46
74
|
// payload.event
|
|
47
75
|
// payload["event"]
|
|
76
|
+
// payload.nested.event
|
|
48
77
|
const targetExpression = typescript_1.default.isElementAccessExpression(node) ? node.expression : node.expression;
|
|
78
|
+
{
|
|
79
|
+
const bound = resolveBoundObjectNode(targetExpression, checker, context);
|
|
80
|
+
if (bound && typescript_1.default.isObjectLiteralExpression(bound)) {
|
|
81
|
+
for (const property of bound.properties) {
|
|
82
|
+
if (!typescript_1.default.isPropertyAssignment(property) && !typescript_1.default.isShorthandPropertyAssignment(property)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const objectPropertyName = typescript_1.default.isIdentifier(property.name) ? property.name.text : property.name.getText();
|
|
86
|
+
if (objectPropertyName !== propertyName) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const initializer = typescript_1.default.isShorthandPropertyAssignment(property) ? property.name : property.initializer;
|
|
90
|
+
return resolveNodeValue(initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
49
94
|
if (typescript_1.default.isIdentifier(targetExpression)) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
let symbol = checker.getSymbolAtLocation(targetExpression);
|
|
96
|
+
if (symbol) {
|
|
97
|
+
symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker, exportCache) ?? symbol;
|
|
98
|
+
}
|
|
99
|
+
// enum EVENTS {
|
|
100
|
+
// LOGIN = "login"
|
|
101
|
+
// }
|
|
102
|
+
if (symbol) {
|
|
103
|
+
const declarations = symbol.getDeclarations() ?? [];
|
|
104
|
+
for (const declaration of declarations) {
|
|
105
|
+
if (!typescript_1.default.isEnumDeclaration(declaration)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const member of declaration.members) {
|
|
109
|
+
const memberName = typescript_1.default.isIdentifier(member.name) ? member.name.text : typescript_1.default.isStringLiteral(member.name) ? member.name.text : null;
|
|
110
|
+
if (memberName !== propertyName || !member.initializer) {
|
|
61
111
|
continue;
|
|
62
112
|
}
|
|
63
|
-
|
|
64
|
-
return resolveNodeValue(initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
113
|
+
return resolveNodeValue(member.initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
65
114
|
}
|
|
66
115
|
}
|
|
67
116
|
}
|
|
@@ -221,10 +270,11 @@ function resolveNodeValue(node, checker, context, visited, evaluationCache, reso
|
|
|
221
270
|
dynamic: true,
|
|
222
271
|
};
|
|
223
272
|
}
|
|
224
|
-
if (typescript_1.default.isParenthesizedExpression(node)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
273
|
+
if (typescript_1.default.isParenthesizedExpression(node) ||
|
|
274
|
+
typescript_1.default.isAsExpression(node) ||
|
|
275
|
+
typescript_1.default.isTypeAssertionExpression(node) ||
|
|
276
|
+
typescript_1.default.isNonNullExpression(node) ||
|
|
277
|
+
typescript_1.default.isSatisfiesExpression(node)) {
|
|
228
278
|
return resolveNodeValue(node.expression, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
229
279
|
}
|
|
230
280
|
if (typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isPropertyAccessChain(node) || typescript_1.default.isElementAccessExpression(node)) {
|
|
@@ -6,12 +6,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.analyzeWrapperPropagation = analyzeWrapperPropagation;
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
8
|
const symbolUtils_1 = require("../../symbols/symbolUtils");
|
|
9
|
+
const utils_1 = require("../../shared/utils");
|
|
10
|
+
function isPropertyAccessLike(node) {
|
|
11
|
+
return typescript_1.default.isPropertyAccessExpression(node) || typescript_1.default.isPropertyAccessChain(node) || typescript_1.default.isElementAccessExpression(node);
|
|
12
|
+
}
|
|
9
13
|
function extractPropertyPath(expression) {
|
|
10
14
|
const result = [];
|
|
11
|
-
let current = expression;
|
|
12
|
-
while (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
let current = (0, utils_1.unwrapExpression)(expression);
|
|
16
|
+
while (isPropertyAccessLike(current)) {
|
|
17
|
+
const propertyName = (0, utils_1.getPropertyName)(current);
|
|
18
|
+
if (!propertyName) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
result.unshift(propertyName);
|
|
22
|
+
current = (0, utils_1.unwrapExpression)(current.expression);
|
|
15
23
|
}
|
|
16
24
|
if (!typescript_1.default.isIdentifier(current)) {
|
|
17
25
|
return null;
|
|
@@ -77,15 +85,18 @@ function collectPropagations(fn, sinks) {
|
|
|
77
85
|
const result = [];
|
|
78
86
|
for (const sink of sinks) {
|
|
79
87
|
for (const tracked of sink.trackedArguments) {
|
|
80
|
-
const
|
|
81
|
-
if (!
|
|
88
|
+
const rawArgument = sink.call.arguments[tracked.index];
|
|
89
|
+
if (!rawArgument) {
|
|
82
90
|
continue;
|
|
83
91
|
}
|
|
92
|
+
const argument = (0, utils_1.unwrapExpression)(rawArgument);
|
|
84
93
|
// direct parameter
|
|
85
94
|
// track(name)
|
|
86
95
|
// track(props.event)
|
|
87
96
|
// track(props?.event)
|
|
88
|
-
|
|
97
|
+
// track(props?.event as string)
|
|
98
|
+
// track(props["event"])
|
|
99
|
+
if (typescript_1.default.isIdentifier(argument) || isPropertyAccessLike(argument)) {
|
|
89
100
|
let identifier = null;
|
|
90
101
|
let propertyPath = [];
|
|
91
102
|
if (typescript_1.default.isIdentifier(argument)) {
|
|
@@ -94,8 +105,8 @@ function collectPropagations(fn, sinks) {
|
|
|
94
105
|
else {
|
|
95
106
|
propertyPath = extractPropertyPath(argument) ?? [];
|
|
96
107
|
let root = argument;
|
|
97
|
-
while (
|
|
98
|
-
root = root.expression;
|
|
108
|
+
while (isPropertyAccessLike(root)) {
|
|
109
|
+
root = (0, utils_1.unwrapExpression)(root.expression);
|
|
99
110
|
}
|
|
100
111
|
if (typescript_1.default.isIdentifier(root)) {
|
|
101
112
|
identifier = root;
|
|
@@ -4,9 +4,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.unwrapExpression = unwrapExpression;
|
|
8
|
+
exports.getPropertyName = getPropertyName;
|
|
7
9
|
exports.getCallName = getCallName;
|
|
8
10
|
exports.getFunctionName = getFunctionName;
|
|
9
11
|
const typescript_1 = __importDefault(require("typescript"));
|
|
12
|
+
// track(payload?.event as string)
|
|
13
|
+
// track((payload.event))
|
|
14
|
+
// track(payload!.event)
|
|
15
|
+
function unwrapExpression(expression) {
|
|
16
|
+
let current = expression;
|
|
17
|
+
while (typescript_1.default.isAsExpression(current) ||
|
|
18
|
+
typescript_1.default.isTypeAssertionExpression(current) ||
|
|
19
|
+
typescript_1.default.isNonNullExpression(current) ||
|
|
20
|
+
typescript_1.default.isParenthesizedExpression(current) ||
|
|
21
|
+
typescript_1.default.isSatisfiesExpression(current)) {
|
|
22
|
+
current = current.expression;
|
|
23
|
+
}
|
|
24
|
+
return current;
|
|
25
|
+
}
|
|
26
|
+
// payload.event -> "event"
|
|
27
|
+
// payload["event"] -> "event"
|
|
28
|
+
function getPropertyName(node) {
|
|
29
|
+
if (typescript_1.default.isElementAccessExpression(node)) {
|
|
30
|
+
if (!node.argumentExpression || !typescript_1.default.isStringLiteral(node.argumentExpression)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return node.argumentExpression.text;
|
|
34
|
+
}
|
|
35
|
+
return node.name.text;
|
|
36
|
+
}
|
|
10
37
|
function getCallName(expression) {
|
|
11
38
|
//
|
|
12
39
|
// track()
|
|
@@ -6,13 +6,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.registerEventraSdkPlugin = registerEventraSdkPlugin;
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
8
|
const isEventraTrackCall_1 = require("../../analysis/sdk/isEventraTrackCall");
|
|
9
|
+
const utils_1 = require("../../analysis/shared/utils");
|
|
9
10
|
/** SDK: track(name: string, options?) — event name is always the first argument. */
|
|
10
11
|
function extractTrackedArguments(call) {
|
|
11
12
|
if (call.arguments.length === 0) {
|
|
12
13
|
return [];
|
|
13
14
|
}
|
|
14
15
|
const first = call.arguments[0];
|
|
15
|
-
|
|
16
|
+
// A cast/non-null/parens around a misused object literal (e.g.
|
|
17
|
+
// `track({ event: "x" } as any)`) must not defeat this guard — it's still
|
|
18
|
+
// the wrong API shape for `track(name: string, options?)`.
|
|
19
|
+
if (typescript_1.default.isObjectLiteralExpression((0, utils_1.unwrapExpression)(first))) {
|
|
16
20
|
return [];
|
|
17
21
|
}
|
|
18
22
|
return [{ index: 0, propertyPath: [] }];
|