@eventra_dev/eventra-cli 0.1.25 → 0.1.27
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 +62 -50
- package/dist/commands/watch.js +41 -23
- package/dist/engine/callResolver.js +52 -4
- package/dist/engine/engine.js +63 -31
- package/dist/engine/exportResolver.js +16 -4
- package/dist/engine/functionAnalyzer.js +66 -13
- package/dist/engine/graph.js +40 -4
- package/dist/engine/languageService.js +83 -20
- package/dist/engine/resolver.js +29 -21
- package/dist/engine/scanner.js +57 -46
- package/dist/engine/trackingDetector.js +68 -0
- package/dist/utils/processFile.js +0 -9
- package/dist/utils/universal.js +25 -87
- package/package.json +1 -1
- package/src/commands/watch.ts +45 -25
- package/src/engine/callResolver.ts +77 -6
- package/src/engine/engine.ts +77 -32
- package/src/engine/exportResolver.ts +22 -5
- package/src/engine/functionAnalyzer.ts +76 -16
- package/src/engine/graph.ts +45 -6
- package/src/engine/languageService.ts +100 -29
- package/src/engine/resolver.ts +36 -27
- package/src/engine/scanner.ts +72 -50
- package/src/engine/trackingDetector.ts +94 -0
- package/src/types.ts +0 -3
- package/src/utils/processFile.ts +0 -9
- package/src/utils/universal.ts +26 -102
|
@@ -7,9 +7,12 @@ export class TSService {
|
|
|
7
7
|
private fileNames = new Set<string>();
|
|
8
8
|
private service: ts.LanguageService;
|
|
9
9
|
|
|
10
|
-
private
|
|
11
|
-
private
|
|
12
|
-
private
|
|
10
|
+
private compilerOptions: ts.CompilerOptions;
|
|
11
|
+
private baseUrl: string;
|
|
12
|
+
private paths: Record<string, string[]>;
|
|
13
|
+
private usage = new Map<string, number>();
|
|
14
|
+
private tick = 0;
|
|
15
|
+
private MAX_FILES = 2000;
|
|
13
16
|
|
|
14
17
|
constructor(private root: string) {
|
|
15
18
|
const configPath = ts.findConfigFile(
|
|
@@ -20,13 +23,18 @@ export class TSService {
|
|
|
20
23
|
|
|
21
24
|
let compilerOptions: ts.CompilerOptions = {
|
|
22
25
|
allowJs: true,
|
|
23
|
-
jsx: ts.JsxEmit.
|
|
26
|
+
jsx: ts.JsxEmit.React,
|
|
27
|
+
jsxImportSource: "react",
|
|
24
28
|
moduleResolution: ts.ModuleResolutionKind.Node16,
|
|
25
29
|
target: ts.ScriptTarget.ESNext,
|
|
26
30
|
esModuleInterop: true,
|
|
27
31
|
allowSyntheticDefaultImports: true,
|
|
32
|
+
skipLibCheck: true,
|
|
28
33
|
};
|
|
29
34
|
|
|
35
|
+
let baseUrl = root;
|
|
36
|
+
let paths: Record<string, string[]> = {};
|
|
37
|
+
|
|
30
38
|
if (configPath) {
|
|
31
39
|
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
32
40
|
|
|
@@ -41,56 +49,108 @@ export class TSService {
|
|
|
41
49
|
...parsed.options,
|
|
42
50
|
};
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
paths = parsed.options.paths || {};
|
|
53
|
+
baseUrl = parsed.options.baseUrl
|
|
46
54
|
? path.resolve(root, parsed.options.baseUrl)
|
|
47
55
|
: root;
|
|
48
|
-
} else {
|
|
49
|
-
this.baseUrl = root;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
this.compilerOptions = compilerOptions;
|
|
59
|
+
this.baseUrl = baseUrl;
|
|
60
|
+
this.paths = paths;
|
|
53
61
|
|
|
54
62
|
const host: ts.LanguageServiceHost = {
|
|
55
|
-
getScriptFileNames: () =>
|
|
56
|
-
return [...new Set(this.fileNames)].sort((a, b) =>
|
|
57
|
-
a.localeCompare(b)
|
|
58
|
-
);
|
|
59
|
-
},
|
|
63
|
+
getScriptFileNames: () => Array.from(this.fileNames),
|
|
60
64
|
|
|
61
|
-
getScriptVersion: (file) =>
|
|
62
|
-
this.
|
|
65
|
+
getScriptVersion: (file) => {
|
|
66
|
+
file = this.normalize(file);
|
|
67
|
+
return this.files.get(file)?.version.toString() ?? "0";
|
|
68
|
+
},
|
|
63
69
|
|
|
64
70
|
getScriptSnapshot: (file) => {
|
|
65
|
-
|
|
71
|
+
file = this.normalize(file);
|
|
66
72
|
|
|
73
|
+
const f = this.files.get(file);
|
|
67
74
|
if (f) {
|
|
75
|
+
this.touch(file);
|
|
68
76
|
return ts.ScriptSnapshot.fromString(f.content);
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
fs.readFileSync(file, "utf-8")
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
try {
|
|
80
|
+
if (fs.existsSync(file)) {
|
|
81
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
82
|
+
|
|
83
|
+
this.files.set(file, {
|
|
84
|
+
content,
|
|
85
|
+
version: 1,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.fileNames.add(file);
|
|
89
|
+
this.touch(file);
|
|
90
|
+
this.evictIfNeeded();
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
return ts.ScriptSnapshot.fromString(content);
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
// fallback
|
|
97
|
+
return ts.ScriptSnapshot.fromString("");
|
|
78
98
|
},
|
|
79
99
|
|
|
80
100
|
getCurrentDirectory: () => this.root,
|
|
81
101
|
getCompilationSettings: () => this.compilerOptions,
|
|
82
102
|
getDefaultLibFileName: (opts) =>
|
|
83
103
|
ts.getDefaultLibFilePath(opts),
|
|
84
|
-
|
|
85
|
-
readFile: ts.sys.readFile,
|
|
86
104
|
fileExists: ts.sys.fileExists,
|
|
105
|
+
readFile: ts.sys.readFile,
|
|
87
106
|
readDirectory: ts.sys.readDirectory,
|
|
88
107
|
};
|
|
89
108
|
|
|
90
109
|
this.service = ts.createLanguageService(host);
|
|
91
110
|
}
|
|
92
111
|
|
|
112
|
+
// INTERNAL
|
|
113
|
+
private normalize(file: string) {
|
|
114
|
+
return path.resolve(file).replace(/\\/g, "/");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private touch(file: string) {
|
|
118
|
+
this.tick++;
|
|
119
|
+
this.usage.set(file, this.tick);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private evictIfNeeded() {
|
|
123
|
+
if (this.fileNames.size <= this.MAX_FILES) return;
|
|
124
|
+
|
|
125
|
+
let oldestFile: string | null = null;
|
|
126
|
+
let oldestTick = Infinity;
|
|
127
|
+
|
|
128
|
+
for (const f of this.fileNames) {
|
|
129
|
+
const t = this.usage.get(f) ?? 0;
|
|
130
|
+
|
|
131
|
+
if (t < oldestTick) {
|
|
132
|
+
oldestTick = t;
|
|
133
|
+
oldestFile = f;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (oldestFile) {
|
|
138
|
+
this.files.delete(oldestFile);
|
|
139
|
+
this.fileNames.delete(oldestFile);
|
|
140
|
+
this.usage.delete(oldestFile);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private ensureProgram() {
|
|
145
|
+
try {
|
|
146
|
+
this.service.getProgram();
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// PUBLIC API
|
|
93
151
|
updateFile(file: string, content: string) {
|
|
152
|
+
file = this.normalize(file);
|
|
153
|
+
|
|
94
154
|
const prev = this.files.get(file);
|
|
95
155
|
|
|
96
156
|
this.files.set(file, {
|
|
@@ -99,6 +159,20 @@ export class TSService {
|
|
|
99
159
|
});
|
|
100
160
|
|
|
101
161
|
this.fileNames.add(file);
|
|
162
|
+
this.touch(file);
|
|
163
|
+
this.evictIfNeeded();
|
|
164
|
+
|
|
165
|
+
this.ensureProgram();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
removeFile(file: string) {
|
|
169
|
+
file = this.normalize(file);
|
|
170
|
+
|
|
171
|
+
this.files.delete(file);
|
|
172
|
+
this.fileNames.delete(file);
|
|
173
|
+
this.usage.delete(file);
|
|
174
|
+
|
|
175
|
+
this.ensureProgram();
|
|
102
176
|
}
|
|
103
177
|
|
|
104
178
|
getProgram() {
|
|
@@ -110,18 +184,15 @@ export class TSService {
|
|
|
110
184
|
}
|
|
111
185
|
|
|
112
186
|
getSourceFile(file: string) {
|
|
187
|
+
file = this.normalize(file);
|
|
113
188
|
return this.getProgram()?.getSourceFile(file);
|
|
114
189
|
}
|
|
115
190
|
|
|
116
191
|
getFileContent(file: string) {
|
|
192
|
+
file = this.normalize(file);
|
|
117
193
|
return this.files.get(file)?.content;
|
|
118
194
|
}
|
|
119
195
|
|
|
120
|
-
removeFile(file: string) {
|
|
121
|
-
this.files.delete(file);
|
|
122
|
-
this.fileNames.delete(file);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
196
|
getModuleResolutionConfig() {
|
|
126
197
|
return {
|
|
127
198
|
baseUrl: this.baseUrl,
|
package/src/engine/resolver.ts
CHANGED
|
@@ -12,12 +12,13 @@ export function resolveNodeValue(
|
|
|
12
12
|
paramMap?: Map<string, ts.Expression>,
|
|
13
13
|
seen = new Set<ts.Node>()
|
|
14
14
|
): ResolveResult {
|
|
15
|
+
|
|
15
16
|
if (seen.has(node)) {
|
|
16
17
|
return { values: [], dynamic: true };
|
|
17
18
|
}
|
|
18
19
|
seen.add(node);
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
+
// PARAM MAPPING
|
|
21
22
|
if (ts.isIdentifier(node) && paramMap?.has(node.text)) {
|
|
22
23
|
return resolveNodeValue(
|
|
23
24
|
paramMap.get(node.text)!,
|
|
@@ -43,12 +44,15 @@ export function resolveNodeValue(
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
return {
|
|
47
|
+
return {
|
|
48
|
+
values: [...new Set(values)],
|
|
49
|
+
dynamic: true
|
|
50
|
+
};
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
// TEMPLATE
|
|
50
54
|
if (ts.isTemplateExpression(node)) {
|
|
51
|
-
let results = [node.head.text];
|
|
55
|
+
let results = node.head.text ? [node.head.text] : [""];
|
|
52
56
|
|
|
53
57
|
for (const span of node.templateSpans) {
|
|
54
58
|
const res = resolveNodeValue(span.expression, checker, paramMap, seen);
|
|
@@ -83,14 +87,7 @@ export function resolveNodeValue(
|
|
|
83
87
|
let symbol = checker.getSymbolAtLocation(node);
|
|
84
88
|
if (!symbol) return { values: [], dynamic: true };
|
|
85
89
|
|
|
86
|
-
//
|
|
87
|
-
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
88
|
-
|
|
89
|
-
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
90
|
-
symbol = checker.getAliasedSymbol(symbol);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// inline variable
|
|
90
|
+
// INLINE FIRST
|
|
94
91
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
95
92
|
if (ts.isVariableDeclaration(decl) && decl.initializer) {
|
|
96
93
|
return resolveNodeValue(
|
|
@@ -100,7 +97,16 @@ export function resolveNodeValue(
|
|
|
100
97
|
seen
|
|
101
98
|
);
|
|
102
99
|
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// unwrap export
|
|
103
|
+
symbol = resolveExportedSymbol(symbol, checker) ?? symbol;
|
|
104
|
+
|
|
105
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
106
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
107
|
+
}
|
|
103
108
|
|
|
109
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
104
110
|
if (ts.isEnumMember(decl) && decl.initializer) {
|
|
105
111
|
if (ts.isStringLiteral(decl.initializer)) {
|
|
106
112
|
return {
|
|
@@ -111,15 +117,15 @@ export function resolveNodeValue(
|
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
// UNION TYPES
|
|
115
120
|
const type = checker.getTypeAtLocation(node);
|
|
116
121
|
|
|
122
|
+
// UNION
|
|
117
123
|
if (type.isUnion()) {
|
|
118
124
|
const values: string[] = [];
|
|
119
125
|
|
|
120
126
|
for (const t of type.types) {
|
|
121
|
-
if (
|
|
122
|
-
values.push((t as
|
|
127
|
+
if (t.flags & ts.TypeFlags.StringLiteral) {
|
|
128
|
+
values.push((t as ts.StringLiteralType).value);
|
|
123
129
|
}
|
|
124
130
|
}
|
|
125
131
|
|
|
@@ -128,15 +134,18 @@ export function resolveNodeValue(
|
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
//
|
|
132
|
-
if (
|
|
133
|
-
return {
|
|
137
|
+
// STRING LITERAL TYPE
|
|
138
|
+
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
139
|
+
return {
|
|
140
|
+
values: [(type as ts.StringLiteralType).value],
|
|
141
|
+
dynamic: false
|
|
142
|
+
};
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
return { values: [], dynamic: true };
|
|
137
146
|
}
|
|
138
147
|
|
|
139
|
-
// PROPERTY ACCESS
|
|
148
|
+
// PROPERTY ACCESS
|
|
140
149
|
if (ts.isPropertyAccessExpression(node)) {
|
|
141
150
|
|
|
142
151
|
const symbol = checker.getSymbolAtLocation(node);
|
|
@@ -160,18 +169,16 @@ export function resolveNodeValue(
|
|
|
160
169
|
}
|
|
161
170
|
}
|
|
162
171
|
|
|
163
|
-
// constant enum / readonly
|
|
164
172
|
const constantValue = checker.getConstantValue(node as any);
|
|
165
173
|
if (typeof constantValue === "string") {
|
|
166
174
|
return { values: [constantValue], dynamic: false };
|
|
167
175
|
}
|
|
168
176
|
|
|
169
|
-
// type fallback
|
|
170
177
|
const type = checker.getTypeAtLocation(node);
|
|
171
178
|
|
|
172
|
-
if (
|
|
179
|
+
if (type.flags & ts.TypeFlags.StringLiteral) {
|
|
173
180
|
return {
|
|
174
|
-
values: [(type as
|
|
181
|
+
values: [(type as ts.StringLiteralType).value],
|
|
175
182
|
dynamic: false,
|
|
176
183
|
};
|
|
177
184
|
}
|
|
@@ -206,10 +213,11 @@ export function resolveNodeValue(
|
|
|
206
213
|
}
|
|
207
214
|
}
|
|
208
215
|
|
|
209
|
-
|
|
216
|
+
// fallback
|
|
217
|
+
return resolveNodeValue(node.name, checker, paramMap, seen);
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
// ARRAY ACCESS
|
|
220
|
+
// ARRAY ACCESS
|
|
213
221
|
if (ts.isElementAccessExpression(node)) {
|
|
214
222
|
const obj = resolveNodeValue(node.expression, checker, paramMap, seen);
|
|
215
223
|
|
|
@@ -224,7 +232,6 @@ export function resolveNodeValue(
|
|
|
224
232
|
seen
|
|
225
233
|
);
|
|
226
234
|
|
|
227
|
-
// if index known
|
|
228
235
|
if (index.values.length === 1) {
|
|
229
236
|
const i = Number(index.values[0]);
|
|
230
237
|
|
|
@@ -236,7 +243,6 @@ export function resolveNodeValue(
|
|
|
236
243
|
}
|
|
237
244
|
}
|
|
238
245
|
|
|
239
|
-
// fallback → dynamic
|
|
240
246
|
return obj.values.length
|
|
241
247
|
? { values: obj.values, dynamic: true }
|
|
242
248
|
: { values: [], dynamic: true };
|
|
@@ -257,7 +263,10 @@ export function resolveNodeValue(
|
|
|
257
263
|
}
|
|
258
264
|
}
|
|
259
265
|
|
|
260
|
-
return {
|
|
266
|
+
return {
|
|
267
|
+
values: [...new Set(values)],
|
|
268
|
+
dynamic: true
|
|
269
|
+
};
|
|
261
270
|
}
|
|
262
271
|
|
|
263
272
|
// BINARY ("btn_" + name)
|
package/src/engine/scanner.ts
CHANGED
|
@@ -5,8 +5,7 @@ import { resolveExportedSymbol } from "./exportResolver";
|
|
|
5
5
|
import { findTrackParamIndex } from "./functionAnalyzer";
|
|
6
6
|
import { EventraConfig } from "../types";
|
|
7
7
|
import { isExternalFile } from "./boundary";
|
|
8
|
-
|
|
9
|
-
const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
|
|
8
|
+
import { isTrackingCall } from "./trackingDetector";
|
|
10
9
|
|
|
11
10
|
function getCallName(expr: ts.Expression): string {
|
|
12
11
|
if (ts.isIdentifier(expr)) return expr.text;
|
|
@@ -20,6 +19,19 @@ function getJsxAttrName(name: ts.JsxAttributeName): string | null {
|
|
|
20
19
|
return null;
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
// unwrap
|
|
23
|
+
function unwrap(expr: ts.Expression): ts.Expression {
|
|
24
|
+
while (
|
|
25
|
+
ts.isAsExpression(expr) ||
|
|
26
|
+
ts.isParenthesizedExpression(expr) ||
|
|
27
|
+
ts.isTypeAssertionExpression(expr) ||
|
|
28
|
+
ts.isNonNullExpression(expr)
|
|
29
|
+
) {
|
|
30
|
+
expr = expr.expression;
|
|
31
|
+
}
|
|
32
|
+
return expr;
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
function isValidEvent(v: string) {
|
|
24
36
|
return (
|
|
25
37
|
v &&
|
|
@@ -32,24 +44,25 @@ function isValidEvent(v: string) {
|
|
|
32
44
|
|
|
33
45
|
function isPropUsedInTracking(
|
|
34
46
|
fn: ts.FunctionLikeDeclaration,
|
|
47
|
+
checker: ts.TypeChecker,
|
|
35
48
|
prop: string
|
|
36
49
|
) {
|
|
37
50
|
let found = false;
|
|
38
51
|
|
|
39
52
|
function visit(node: ts.Node) {
|
|
40
53
|
if (ts.isCallExpression(node)) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (!TRACKING_NAMES.has(name)) {
|
|
54
|
+
if (!isTrackingCall(node, checker)) {
|
|
44
55
|
ts.forEachChild(node, visit);
|
|
45
56
|
return;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
for (const arg of node.arguments) {
|
|
60
|
+
const a = unwrap(arg as ts.Expression);
|
|
61
|
+
|
|
49
62
|
if (
|
|
50
|
-
(ts.isIdentifier(
|
|
51
|
-
(ts.isPropertyAccessExpression(
|
|
52
|
-
|
|
63
|
+
(ts.isIdentifier(a) && a.text === prop) ||
|
|
64
|
+
(ts.isPropertyAccessExpression(a) &&
|
|
65
|
+
a.name.text === prop)
|
|
53
66
|
) {
|
|
54
67
|
found = true;
|
|
55
68
|
}
|
|
@@ -59,7 +72,10 @@ function isPropUsedInTracking(
|
|
|
59
72
|
ts.forEachChild(node, visit);
|
|
60
73
|
}
|
|
61
74
|
|
|
62
|
-
|
|
75
|
+
if (fn.body) {
|
|
76
|
+
visit(fn.body);
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
return found;
|
|
64
80
|
}
|
|
65
81
|
|
|
@@ -81,7 +97,11 @@ export function scanSource(
|
|
|
81
97
|
const detectedFunctionWrappers = new Set<string>();
|
|
82
98
|
const detectedComponentWrappers = new Map<string, string>();
|
|
83
99
|
|
|
84
|
-
if (
|
|
100
|
+
if (
|
|
101
|
+
visited.has(source.fileName) ||
|
|
102
|
+
depth > 10 ||
|
|
103
|
+
visited.size > 200
|
|
104
|
+
) {
|
|
85
105
|
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
86
106
|
}
|
|
87
107
|
|
|
@@ -90,53 +110,55 @@ export function scanSource(
|
|
|
90
110
|
function visit(node: ts.Node) {
|
|
91
111
|
// CALL EXPRESSIONS
|
|
92
112
|
if (ts.isCallExpression(node)) {
|
|
93
|
-
const
|
|
113
|
+
const expr = unwrap(node.expression);
|
|
114
|
+
const name = getCallName(expr);
|
|
94
115
|
|
|
95
116
|
const isKnownWrapper = config.functionWrappers.some(
|
|
96
117
|
w => w.name === name
|
|
97
118
|
);
|
|
98
119
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// fallback: tracker.track(...)
|
|
102
|
-
if (!resolvedFn && ts.isPropertyAccessExpression(node.expression)) {
|
|
103
|
-
const symbol = checker.getSymbolAtLocation(node.expression.name);
|
|
104
|
-
|
|
105
|
-
if (symbol) {
|
|
106
|
-
const decls = symbol.getDeclarations();
|
|
120
|
+
const isTrackingByType = isTrackingCall(node, checker);
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
const d = decls[0];
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
ts.isMethodDeclaration(d) ||
|
|
113
|
-
ts.isFunctionDeclaration(d) ||
|
|
114
|
-
ts.isFunctionExpression(d) ||
|
|
115
|
-
ts.isArrowFunction(d)
|
|
116
|
-
) {
|
|
117
|
-
resolvedFn = d;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
+
let resolvedFn = resolveFunctionFromCall(expr, checker);
|
|
122
123
|
|
|
123
124
|
const isAutoWrapper =
|
|
124
125
|
resolvedFn && findTrackParamIndex(resolvedFn) !== null;
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
let isTracking =
|
|
127
128
|
isKnownWrapper ||
|
|
128
|
-
|
|
129
|
-
name.startsWith("$") ||
|
|
129
|
+
isTrackingByType ||
|
|
130
|
+
name.startsWith("$") ||
|
|
130
131
|
(isAutoWrapper &&
|
|
131
132
|
resolvedFn &&
|
|
132
133
|
!isExternalFile(resolvedFn.getSourceFile().fileName));
|
|
133
134
|
|
|
135
|
+
// fallback
|
|
136
|
+
if (!isTracking) {
|
|
137
|
+
if (
|
|
138
|
+
ts.isPropertyAccessExpression(expr) &&
|
|
139
|
+
expr.name.text === "track"
|
|
140
|
+
) {
|
|
141
|
+
isTracking = true;
|
|
142
|
+
} else if (
|
|
143
|
+
ts.isIdentifier(expr) &&
|
|
144
|
+
expr.text === "track"
|
|
145
|
+
) {
|
|
146
|
+
isTracking = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
134
150
|
if (!isTracking) {
|
|
135
151
|
ts.forEachChild(node, visit);
|
|
136
152
|
return;
|
|
137
153
|
}
|
|
138
154
|
|
|
139
|
-
|
|
155
|
+
// detect wrapper
|
|
156
|
+
if (
|
|
157
|
+
!isKnownWrapper &&
|
|
158
|
+
isAutoWrapper &&
|
|
159
|
+
!isTrackingByType &&
|
|
160
|
+
!name.startsWith("track")
|
|
161
|
+
) {
|
|
140
162
|
detectedFunctionWrappers.add(name);
|
|
141
163
|
}
|
|
142
164
|
|
|
@@ -161,16 +183,14 @@ export function scanSource(
|
|
|
161
183
|
|
|
162
184
|
if (idx !== null && node.arguments[idx]) {
|
|
163
185
|
const res = resolveNodeValue(
|
|
164
|
-
node.arguments[idx],
|
|
186
|
+
unwrap(node.arguments[idx]),
|
|
165
187
|
checker,
|
|
166
188
|
paramMap
|
|
167
189
|
);
|
|
168
190
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
});
|
|
173
|
-
}
|
|
191
|
+
res.values.forEach(v => {
|
|
192
|
+
if (isValidEvent(v)) events.add(v);
|
|
193
|
+
});
|
|
174
194
|
|
|
175
195
|
handled = true;
|
|
176
196
|
}
|
|
@@ -179,13 +199,15 @@ export function scanSource(
|
|
|
179
199
|
// FALLBACK
|
|
180
200
|
if (!handled) {
|
|
181
201
|
for (const arg of node.arguments) {
|
|
182
|
-
const res = resolveNodeValue(
|
|
202
|
+
const res = resolveNodeValue(
|
|
203
|
+
unwrap(arg),
|
|
204
|
+
checker,
|
|
205
|
+
paramMap
|
|
206
|
+
);
|
|
183
207
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
}
|
|
208
|
+
res.values.forEach(v => {
|
|
209
|
+
if (isValidEvent(v)) events.add(v);
|
|
210
|
+
});
|
|
189
211
|
}
|
|
190
212
|
}
|
|
191
213
|
|
|
@@ -263,7 +285,7 @@ export function scanSource(
|
|
|
263
285
|
const prop = el.name.getText();
|
|
264
286
|
|
|
265
287
|
const isEvent =
|
|
266
|
-
isPropUsedInTracking(fn, prop) ||
|
|
288
|
+
isPropUsedInTracking(fn, checker, prop) ||
|
|
267
289
|
config.wrappers.some(
|
|
268
290
|
w => w.name === tagName && w.prop === prop
|
|
269
291
|
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
function isStringLike(type: ts.Type) {
|
|
4
|
+
return (
|
|
5
|
+
(type.flags & ts.TypeFlags.StringLike) !== 0 ||
|
|
6
|
+
(type.flags & ts.TypeFlags.StringLiteral) !== 0
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function hasTrackLikeSignature(
|
|
11
|
+
type: ts.Type,
|
|
12
|
+
checker: ts.TypeChecker
|
|
13
|
+
): boolean {
|
|
14
|
+
const sigs = type.getCallSignatures();
|
|
15
|
+
|
|
16
|
+
for (const sig of sigs) {
|
|
17
|
+
const params = sig.getParameters();
|
|
18
|
+
if (!params.length) continue;
|
|
19
|
+
|
|
20
|
+
const t = checker.getTypeOfSymbolAtLocation(
|
|
21
|
+
params[0],
|
|
22
|
+
params[0].valueDeclaration!
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (isStringLike(t)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isEventraLikeType(
|
|
34
|
+
type: ts.Type,
|
|
35
|
+
checker: ts.TypeChecker
|
|
36
|
+
): boolean {
|
|
37
|
+
const props = type.getProperties();
|
|
38
|
+
|
|
39
|
+
for (const prop of props) {
|
|
40
|
+
const decl = prop.valueDeclaration ?? prop.declarations?.[0];
|
|
41
|
+
if (!decl) continue;
|
|
42
|
+
|
|
43
|
+
const t = checker.getTypeOfSymbolAtLocation(prop, decl);
|
|
44
|
+
|
|
45
|
+
if (hasTrackLikeSignature(t, checker)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isTrackingCall(
|
|
54
|
+
node: ts.CallExpression,
|
|
55
|
+
checker: ts.TypeChecker
|
|
56
|
+
): boolean {
|
|
57
|
+
const expr = node.expression;
|
|
58
|
+
|
|
59
|
+
// fn(...)
|
|
60
|
+
if (ts.isIdentifier(expr)) {
|
|
61
|
+
const type = checker.getTypeAtLocation(expr);
|
|
62
|
+
if (hasTrackLikeSignature(type, checker)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// obj.method(...)
|
|
68
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
69
|
+
const objType = checker.getTypeAtLocation(expr.expression);
|
|
70
|
+
|
|
71
|
+
// объект похож на tracker
|
|
72
|
+
if (isEventraLikeType(objType, checker)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// fallback
|
|
77
|
+
const methodType = checker.getTypeAtLocation(expr.name);
|
|
78
|
+
|
|
79
|
+
if (hasTrackLikeSignature(methodType, checker)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// obj["track"](...)
|
|
85
|
+
if (ts.isElementAccessExpression(expr)) {
|
|
86
|
+
const objType = checker.getTypeAtLocation(expr.expression);
|
|
87
|
+
|
|
88
|
+
if (isEventraLikeType(objType, checker)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
}
|