@eventra_dev/eventra-cli 2.0.7 → 2.0.9
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 +28 -3
- package/dist/analysis/extractor/extractor.js +1 -1
- package/dist/analysis/extractor/propagationExtractor.js +5 -2
- package/dist/analysis/resolver/resolver.js +159 -2
- package/dist/analysis/scanner/jsxEventAttribute.js +58 -0
- package/dist/analysis/scanner/scanner.js +8 -0
- package/dist/analysis/shared/dynamicOccurrence.js +9 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli"><img alt="npm version" src="https://img.shields.io/npm/v/@eventra_dev/eventra-cli.svg?style=flat-square&color=blue"></a>
|
|
9
9
|
<a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli"><img alt="npm downloads" src="https://img.shields.io/npm/dm/@eventra_dev/eventra-cli.svg?style=flat-square&color=blue"></a>
|
|
10
10
|
<a href="https://github.com/and-1991/eventra-cli/actions/workflows/test.yml"><img alt="tests" src="https://img.shields.io/github/actions/workflow/status/and-1991/eventra-cli/test.yml?branch=main&label=tests&style=flat-square&logo=vitest&logoColor=white"></a>
|
|
11
|
-
<img alt="unit tests" src="https://img.shields.io/badge/unit-
|
|
11
|
+
<img alt="unit tests" src="https://img.shields.io/badge/unit-445%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white">
|
|
12
12
|
<img alt="e2e fixtures" src="https://img.shields.io/badge/e2e-12%20fixtures-brightgreen?style=flat-square">
|
|
13
13
|
<img alt="coverage" src="https://img.shields.io/badge/coverage-100%25%20(unit)-brightgreen?style=flat-square&logo=vitest&logoColor=white">
|
|
14
14
|
<img alt="node" src="https://img.shields.io/node/v/@eventra_dev/eventra-cli?style=flat-square&color=darkgreen&logo=node.js&logoColor=white">
|
|
@@ -24,7 +24,7 @@ Eventra CLI statically discovers analytics events from [**@eventra_dev/eventra-s
|
|
|
24
24
|
|
|
25
25
|
The CLI scans TypeScript and JavaScript with the TypeScript compiler API and extracts **only** calls to `Eventra.prototype.track()` on instances of `Eventra` imported from `@eventra_dev/eventra-sdk`.
|
|
26
26
|
|
|
27
|
-
It does **not** detect generic `track()`, Segment, Google Analytics, or other libraries out of the box. Framework files (e.g. `.vue`, `.svelte`, `.astro`) require an optional [plugin](#plugins).
|
|
27
|
+
It does **not** detect generic `track()`, Segment, Google Analytics, or other libraries out of the box. Framework files (e.g. `.vue`, `.svelte`, `.astro`, Angular's `.html` templates) require an optional [plugin](#plugins).
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
@@ -272,6 +272,31 @@ The plugin:
|
|
|
272
272
|
|
|
273
273
|
`sync.include` does **not** need `**/*.astro` manually. See [`@eventra_dev/cli-plugin-astro`](https://www.npmjs.com/package/@eventra_dev/cli-plugin-astro) for details.
|
|
274
274
|
|
|
275
|
+
### Angular (`.html`, via `templateUrl`)
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
pnpm add -D @eventra_dev/cli-plugin-angular
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```json
|
|
282
|
+
{
|
|
283
|
+
"plugins": ["@eventra_dev/cli-plugin-angular"],
|
|
284
|
+
"sync": {
|
|
285
|
+
"include": ["**/*.{ts,tsx,js,jsx}"],
|
|
286
|
+
"exclude": ["node_modules", "dist", ".angular", ".git"]
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The plugin:
|
|
292
|
+
|
|
293
|
+
- parses each component template with the real Angular compiler (`@angular/compiler`) into a virtual TypeScript module, paired with its component class (`foo.component.html` ↔ `foo.component.ts`, Angular CLI's default naming)
|
|
294
|
+
- extracts `track()` calls from the component class exactly like a regular `.ts` file (direct calls, wrappers, propagation) - no plugin needed for that part
|
|
295
|
+
- detects `event="feature_name"` attributes on any tag, including `*ngIf`/`*ngFor` and the `@if`/`@for`/`@switch` block syntax - both literal and dynamic; a dynamic binding resolves against the component instance (`this.field`, including getters), or surfaces as a dynamic occurrence if unresolvable
|
|
296
|
+
- use `[attr.event]="expr"`, not `[event]="expr"`, for a dynamic binding on a plain HTML element - `[event]` is a property binding that Angular's real compiler rejects (`NG8002`) unless the element is a component that actually declares `@Input() event`; both forms are detected identically by the plugin, only `[attr.event]` is guaranteed to compile
|
|
297
|
+
|
|
298
|
+
`sync.include` does **not** need `**/*.html` manually. Only `templateUrl`-based components are covered - an inline `template: "..."` isn't yet. Requires Node `^20.19.0 || ^22.12.0 || >=24.0.0` (higher than the other plugins' `>=18` floor - `@angular/compiler`'s own requirement). See [`@eventra_dev/cli-plugin-angular`](https://www.npmjs.com/package/@eventra_dev/cli-plugin-angular) for details.
|
|
299
|
+
|
|
275
300
|
### Before publishing / local development
|
|
276
301
|
|
|
277
302
|
Plugins are resolved from your project's `node_modules` (same as any dependency):
|
|
@@ -357,7 +382,7 @@ No runtime execution. No monkey-patching.
|
|
|
357
382
|
|
|
358
383
|
## Test Coverage
|
|
359
384
|
|
|
360
|
-
Two test layers, **
|
|
385
|
+
Two test layers, **445 unit tests + 12 e2e fixtures + 3 `check` exit-code scenarios + 1 `watch` scenario**.
|
|
361
386
|
|
|
362
387
|
**100% statement/branch/function/line coverage** (v8 provider, `pnpm test:coverage`), enforced via a `coverage.thresholds` block in `vitest.config.ts` so a regression fails the build instead of silently slipping under 100%. This measures the vitest unit layer only; the e2e/`check`/`watch` fixtures run against the compiled `dist/` binary via `tsx tests/run.ts` and aren't coverage-instrumented. A handful of genuinely unreachable defensive branches (TS-internal invariants — e.g. an aliased import symbol is always fully resolved before its own declarations are ever inspected — that don't hold a second outcome in practice) are marked with `/* v8 ignore */`, with an inline comment explaining why, rather than tested.
|
|
363
388
|
|
|
@@ -24,7 +24,7 @@ function extractEvents(index, checker, _config, evaluationCache, exportCache, re
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
if (resolved.dynamic) {
|
|
27
|
-
(0, dynamicOccurrence_1.recordDynamicOccurrence)(call.node, resolved.values, dynamicOccurrences);
|
|
27
|
+
(0, dynamicOccurrence_1.recordDynamicOccurrence)(call.node, call.calleeText, resolved.values, dynamicOccurrences);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -68,7 +68,10 @@ function extractPropagationEvents(call, checker, wrapperRegistry, resolvedCallCa
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
function extractPropagation(propagation, call, checker, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache, events, dynamicOccurrences) {
|
|
71
|
-
|
|
71
|
+
// Call site can omit the argument and rely on the parameter's own default
|
|
72
|
+
// value (`function f(event = "x") {}` called as `f()`) - that default is a
|
|
73
|
+
// real static literal, so fall back to it instead of dropping the call site.
|
|
74
|
+
const argument = call.arguments[propagation.sourceParameterIndex] ?? propagation.sourceParameter.initializer;
|
|
72
75
|
if (!argument) {
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
@@ -116,6 +119,6 @@ function extractPropagation(propagation, call, checker, evaluationCache, resolve
|
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
if (resolved.dynamic) {
|
|
119
|
-
(0, dynamicOccurrence_1.recordDynamicOccurrence)(call, resolved.values, dynamicOccurrences);
|
|
122
|
+
(0, dynamicOccurrence_1.recordDynamicOccurrence)(call, call.expression.getText(), resolved.values, dynamicOccurrences);
|
|
120
123
|
}
|
|
121
124
|
}
|
|
@@ -71,6 +71,54 @@ function resolveBoundObjectNode(rawNode, checker, context) {
|
|
|
71
71
|
}
|
|
72
72
|
return null;
|
|
73
73
|
}
|
|
74
|
+
// Walks up from a `this` reference to the class it's lexically inside of, so
|
|
75
|
+
// `this.EVENT_NAME` can be resolved against that class's own property
|
|
76
|
+
// declarations - the same idea as resolving a plain identifier against its
|
|
77
|
+
// variable declaration, just rooted at the enclosing class instead of a
|
|
78
|
+
// module-scope symbol (`this` itself has no symbol/declaration of its own to
|
|
79
|
+
// look up via the checker).
|
|
80
|
+
function findEnclosingClass(node) {
|
|
81
|
+
let current = node;
|
|
82
|
+
while (current) {
|
|
83
|
+
if (typescript_1.default.isClassDeclaration(current) || typescript_1.default.isClassExpression(current)) {
|
|
84
|
+
return current;
|
|
85
|
+
}
|
|
86
|
+
current = current.parent;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function isFunctionLikeBoundary(node) {
|
|
91
|
+
return (typescript_1.default.isFunctionDeclaration(node) ||
|
|
92
|
+
typescript_1.default.isFunctionExpression(node) ||
|
|
93
|
+
typescript_1.default.isArrowFunction(node) ||
|
|
94
|
+
typescript_1.default.isMethodDeclaration(node) ||
|
|
95
|
+
typescript_1.default.isGetAccessorDeclaration(node) ||
|
|
96
|
+
typescript_1.default.isSetAccessorDeclaration(node));
|
|
97
|
+
}
|
|
98
|
+
// Collects every `return <expr>;` belonging directly to `getter` - i.e. not
|
|
99
|
+
// one that belongs to a function/method/accessor nested inside its body.
|
|
100
|
+
// A getter with conditional returns is legitimately multi-valued (which
|
|
101
|
+
// branch runs depends on runtime state), so the caller unions more than one
|
|
102
|
+
// of these the same way it already does for a reassigned `let`.
|
|
103
|
+
function collectReturnExpressions(getter) {
|
|
104
|
+
const expressions = [];
|
|
105
|
+
function visit(node) {
|
|
106
|
+
if (node !== getter && isFunctionLikeBoundary(node)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (typescript_1.default.isReturnStatement(node) && node.expression) {
|
|
110
|
+
expressions.push(node.expression);
|
|
111
|
+
}
|
|
112
|
+
typescript_1.default.forEachChild(node, visit);
|
|
113
|
+
}
|
|
114
|
+
if (getter.body) {
|
|
115
|
+
visit(getter.body);
|
|
116
|
+
}
|
|
117
|
+
return expressions;
|
|
118
|
+
}
|
|
119
|
+
function getMemberName(member) {
|
|
120
|
+
return typescript_1.default.isIdentifier(member.name) ? member.name.text : typescript_1.default.isStringLiteral(member.name) ? member.name.text : null;
|
|
121
|
+
}
|
|
74
122
|
function resolvePropertyAccess(node, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache) {
|
|
75
123
|
const propertyName = (0, utils_1.getPropertyName)(node);
|
|
76
124
|
if (!propertyName) {
|
|
@@ -96,6 +144,40 @@ function resolvePropertyAccess(node, checker, context, visited, evaluationCache,
|
|
|
96
144
|
}
|
|
97
145
|
}
|
|
98
146
|
}
|
|
147
|
+
// this.EVENT_NAME
|
|
148
|
+
// class Foo { EVENT_NAME = "checkout"; bar() { track(this.EVENT_NAME); } }
|
|
149
|
+
//
|
|
150
|
+
// this.computedEventName (getter)
|
|
151
|
+
// class Foo { get computedEventName() { return "checkout"; } }
|
|
152
|
+
if (targetExpression.kind === typescript_1.default.SyntaxKind.ThisKeyword) {
|
|
153
|
+
const enclosingClass = findEnclosingClass(node);
|
|
154
|
+
if (enclosingClass) {
|
|
155
|
+
for (const member of enclosingClass.members) {
|
|
156
|
+
if (typescript_1.default.isPropertyDeclaration(member) && member.initializer && getMemberName(member) === propertyName) {
|
|
157
|
+
return resolveNodeValue(member.initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
158
|
+
}
|
|
159
|
+
if (typescript_1.default.isGetAccessorDeclaration(member) && getMemberName(member) === propertyName) {
|
|
160
|
+
const returnExpressions = collectReturnExpressions(member);
|
|
161
|
+
if (returnExpressions.length === 0) {
|
|
162
|
+
return empty();
|
|
163
|
+
}
|
|
164
|
+
if (returnExpressions.length === 1) {
|
|
165
|
+
return resolveNodeValue(returnExpressions[0], checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
166
|
+
}
|
|
167
|
+
const values = [];
|
|
168
|
+
for (const expression of returnExpressions) {
|
|
169
|
+
const resolved = resolveNodeValue(expression, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
170
|
+
values.push(...resolved.values);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
values: unique(values),
|
|
174
|
+
dynamic: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return empty();
|
|
180
|
+
}
|
|
99
181
|
if (typescript_1.default.isIdentifier(targetExpression)) {
|
|
100
182
|
let symbol = checker.getSymbolAtLocation(targetExpression);
|
|
101
183
|
if (symbol) {
|
|
@@ -157,6 +239,63 @@ function resolvePropertyAccess(node, checker, context, visited, evaluationCache,
|
|
|
157
239
|
}
|
|
158
240
|
return empty();
|
|
159
241
|
}
|
|
242
|
+
function isConstDeclaration(declaration) {
|
|
243
|
+
return (typescript_1.default.getCombinedNodeFlags(declaration) & typescript_1.default.NodeFlags.Const) !== 0;
|
|
244
|
+
}
|
|
245
|
+
// `ts.isAssignmentOperator` exists on the compiled module at runtime but
|
|
246
|
+
// isn't part of the public .d.ts (an internal-only export), so the operator
|
|
247
|
+
// set is spelled out here instead of depending on an undocumented API.
|
|
248
|
+
const ASSIGNMENT_OPERATOR_KINDS = new Set([
|
|
249
|
+
typescript_1.default.SyntaxKind.EqualsToken,
|
|
250
|
+
typescript_1.default.SyntaxKind.PlusEqualsToken,
|
|
251
|
+
typescript_1.default.SyntaxKind.MinusEqualsToken,
|
|
252
|
+
typescript_1.default.SyntaxKind.AsteriskEqualsToken,
|
|
253
|
+
typescript_1.default.SyntaxKind.AsteriskAsteriskEqualsToken,
|
|
254
|
+
typescript_1.default.SyntaxKind.SlashEqualsToken,
|
|
255
|
+
typescript_1.default.SyntaxKind.PercentEqualsToken,
|
|
256
|
+
typescript_1.default.SyntaxKind.LessThanLessThanEqualsToken,
|
|
257
|
+
typescript_1.default.SyntaxKind.GreaterThanGreaterThanEqualsToken,
|
|
258
|
+
typescript_1.default.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
|
|
259
|
+
typescript_1.default.SyntaxKind.AmpersandEqualsToken,
|
|
260
|
+
typescript_1.default.SyntaxKind.BarEqualsToken,
|
|
261
|
+
typescript_1.default.SyntaxKind.CaretEqualsToken,
|
|
262
|
+
typescript_1.default.SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
263
|
+
typescript_1.default.SyntaxKind.BarBarEqualsToken,
|
|
264
|
+
typescript_1.default.SyntaxKind.QuestionQuestionEqualsToken,
|
|
265
|
+
]);
|
|
266
|
+
// A `let`/`var` binding can be reassigned after its declaration - resolving
|
|
267
|
+
// only the initializer would confidently report a value that a later
|
|
268
|
+
// assignment (still just as static - a literal, another variable, a
|
|
269
|
+
// template, etc.) overwrites before any sink ever sees it. This walks the
|
|
270
|
+
// declaring file(s) for every assignment to `symbol` so the caller can treat
|
|
271
|
+
// the binding as multi-valued (or at least uncertain) instead of silently
|
|
272
|
+
// trusting the first value it was ever given.
|
|
273
|
+
function collectReassignments(symbol, checker) {
|
|
274
|
+
const values = [];
|
|
275
|
+
let hasUnresolvedReassignment = false;
|
|
276
|
+
// See the identical `?? []` elsewhere in this file: a real ts.Symbol here
|
|
277
|
+
// always has a declarations array.
|
|
278
|
+
/* v8 ignore next */
|
|
279
|
+
const sourceFiles = new Set((symbol.getDeclarations() ?? []).map(declaration => declaration.getSourceFile()));
|
|
280
|
+
for (const sourceFile of sourceFiles) {
|
|
281
|
+
const visit = (node) => {
|
|
282
|
+
if (typescript_1.default.isBinaryExpression(node) &&
|
|
283
|
+
ASSIGNMENT_OPERATOR_KINDS.has(node.operatorToken.kind) &&
|
|
284
|
+
typescript_1.default.isIdentifier(node.left) &&
|
|
285
|
+
checker.getSymbolAtLocation(node.left) === symbol) {
|
|
286
|
+
if (node.operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken) {
|
|
287
|
+
values.push(node.right);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
hasUnresolvedReassignment = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
typescript_1.default.forEachChild(node, visit);
|
|
294
|
+
};
|
|
295
|
+
visit(sourceFile);
|
|
296
|
+
}
|
|
297
|
+
return { values, hasUnresolvedReassignment };
|
|
298
|
+
}
|
|
160
299
|
function resolveIdentifier(node, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache) {
|
|
161
300
|
const boundSymbol = checker.getSymbolAtLocation(node);
|
|
162
301
|
if (boundSymbol && context.parameterBindings.has(boundSymbol)) {
|
|
@@ -186,7 +325,21 @@ function resolveIdentifier(node, checker, context, visited, evaluationCache, res
|
|
|
186
325
|
const declarations = symbol.getDeclarations() ?? [];
|
|
187
326
|
for (const declaration of declarations) {
|
|
188
327
|
if (typescript_1.default.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
189
|
-
const
|
|
328
|
+
const reassignment = isConstDeclaration(declaration) ? null : collectReassignments(symbol, checker);
|
|
329
|
+
if (!reassignment || (reassignment.values.length === 0 && !reassignment.hasUnresolvedReassignment)) {
|
|
330
|
+
const resolved = resolveNodeValue(declaration.initializer, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
331
|
+
evaluationCache.set(symbol, resolved);
|
|
332
|
+
return resolved;
|
|
333
|
+
}
|
|
334
|
+
const values = [];
|
|
335
|
+
for (const expression of [declaration.initializer, ...reassignment.values]) {
|
|
336
|
+
const resolvedValue = resolveNodeValue(expression, checker, context, visited, evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
337
|
+
values.push(...resolvedValue.values);
|
|
338
|
+
}
|
|
339
|
+
const resolved = {
|
|
340
|
+
values: unique(values),
|
|
341
|
+
dynamic: true,
|
|
342
|
+
};
|
|
190
343
|
evaluationCache.set(symbol, resolved);
|
|
191
344
|
return resolved;
|
|
192
345
|
}
|
|
@@ -213,7 +366,11 @@ function resolveCallExpression(node, checker, context, visited, evaluationCache,
|
|
|
213
366
|
}
|
|
214
367
|
const values = [];
|
|
215
368
|
for (const propagation of semantic.propagations) {
|
|
216
|
-
|
|
369
|
+
// Call site can omit the argument and rely on the parameter's own default
|
|
370
|
+
// value (`function build(name = "x") {}` called as `build()`) - that
|
|
371
|
+
// default is a real static literal, so fall back to it instead of
|
|
372
|
+
// dropping the call site.
|
|
373
|
+
const argument = node.arguments[propagation.parameterIndex] ?? propagation.parameter.initializer;
|
|
217
374
|
if (!argument) {
|
|
218
375
|
continue;
|
|
219
376
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.detectJsxEventAttribute = detectJsxEventAttribute;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const EVENT_ATTRIBUTE_NAME = "event";
|
|
9
|
+
/**
|
|
10
|
+
* Detects a JSX `event="literal"` / `event={expr}` attribute - the same
|
|
11
|
+
* declarative-tracking convention the Vue/Svelte/Astro/Angular CLI plugins
|
|
12
|
+
* already use for their own templates, recognized on any tag (plain
|
|
13
|
+
* elements and components alike). No plugin or preprocessing is needed for
|
|
14
|
+
* React: JSX is already part of the TypeScript AST the core walks natively,
|
|
15
|
+
* so this is a plain node-kind check alongside the existing CallExpression
|
|
16
|
+
* sink detection, not a separate virtual-module bridge.
|
|
17
|
+
*
|
|
18
|
+
* Reported through the exact same `TrackCall` shape as a real track() call:
|
|
19
|
+
* a literal value resolves directly, and a dynamic expression is resolved
|
|
20
|
+
* through the same checker-based pipeline as any other TS expression,
|
|
21
|
+
* falling back to a dynamic occurrence (not silently dropped) when it
|
|
22
|
+
* doesn't resolve to a literal.
|
|
23
|
+
*/
|
|
24
|
+
function detectJsxEventAttribute(node) {
|
|
25
|
+
if (!typescript_1.default.isJsxAttribute(node)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (!typescript_1.default.isIdentifier(node.name) || node.name.text !== EVENT_ATTRIBUTE_NAME) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const initializer = node.initializer;
|
|
32
|
+
if (!initializer) {
|
|
33
|
+
// Boolean shorthand `<Button event />` - no name to extract, same as
|
|
34
|
+
// the other plugins' own boolean-shorthand `event` handling.
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
let value;
|
|
38
|
+
if (typescript_1.default.isStringLiteral(initializer)) {
|
|
39
|
+
// event="literal"
|
|
40
|
+
value = initializer;
|
|
41
|
+
}
|
|
42
|
+
else if (typescript_1.default.isJsxExpression(initializer)) {
|
|
43
|
+
// event={expr} - an empty event={} has no expression to extract.
|
|
44
|
+
if (!initializer.expression) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
value = initializer.expression;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
node,
|
|
54
|
+
sourceFile: node.getSourceFile(),
|
|
55
|
+
trackedArguments: [value],
|
|
56
|
+
calleeText: node.parent.parent.tagName.getText(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.scanSource = scanSource;
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
8
|
const sinkDetector_1 = require("./sinkDetector");
|
|
9
|
+
const jsxEventAttribute_1 = require("./jsxEventAttribute");
|
|
9
10
|
const propagationAnalyzer_1 = require("./analyzer/propagationAnalyzer");
|
|
10
11
|
function scanSource(source, checker, wrapperRegistry, plugins) {
|
|
11
12
|
const sinks = [];
|
|
@@ -51,9 +52,16 @@ function scanSource(source, checker, wrapperRegistry, plugins) {
|
|
|
51
52
|
node,
|
|
52
53
|
sourceFile: source,
|
|
53
54
|
trackedArguments,
|
|
55
|
+
calleeText: node.expression.getText(source),
|
|
54
56
|
});
|
|
55
57
|
}
|
|
56
58
|
}
|
|
59
|
+
// JSX `event="..."`/`event={expr}` attribute detection (React/JSX files
|
|
60
|
+
// only - a plain .ts/.js file never contains a JsxAttribute node).
|
|
61
|
+
const jsxEventAttribute = (0, jsxEventAttribute_1.detectJsxEventAttribute)(node);
|
|
62
|
+
if (jsxEventAttribute) {
|
|
63
|
+
trackCalls.push(jsxEventAttribute);
|
|
64
|
+
}
|
|
57
65
|
typescript_1.default.forEachChild(node, visit);
|
|
58
66
|
}
|
|
59
67
|
visit(source);
|
|
@@ -7,15 +7,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.recordDynamicOccurrence = recordDynamicOccurrence;
|
|
8
8
|
const typescript_1 = __importDefault(require("typescript"));
|
|
9
9
|
const CALL_TEXT_MAX_LENGTH = 120;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// `node` is a CallExpression for a real track() call, or a JsxAttribute for
|
|
11
|
+
// a JSX `event="..."`/`event={expr}` binding (see jsxEventAttribute.ts) -
|
|
12
|
+
// `calleeText` is supplied by the caller since it's derived differently per
|
|
13
|
+
// node kind (a call's callee text vs. a JSX element's tag name).
|
|
14
|
+
function recordDynamicOccurrence(node, calleeText, resolvedValues, out) {
|
|
15
|
+
const sourceFile = node.getSourceFile();
|
|
16
|
+
const { line, character } = typescript_1.default.getLineAndCharacterOfPosition(sourceFile, node.getStart(sourceFile));
|
|
17
|
+
const callText = node.getText(sourceFile);
|
|
14
18
|
out.push({
|
|
15
19
|
fileName: sourceFile.fileName,
|
|
16
20
|
line: line + 1,
|
|
17
21
|
character,
|
|
18
|
-
calleeText
|
|
22
|
+
calleeText,
|
|
19
23
|
callText: callText.length > CALL_TEXT_MAX_LENGTH ? `${callText.slice(0, CALL_TEXT_MAX_LENGTH)}…` : callText,
|
|
20
24
|
resolvedValues,
|
|
21
25
|
});
|