@eventra_dev/eventra-cli 0.1.6 → 0.1.7
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/dist/commands/check.js +1 -1
- package/dist/commands/sync.js +36 -2
- package/dist/commands/watch.js +2 -2
- package/dist/engine/callResolver.js +58 -5
- package/dist/engine/engine.js +26 -19
- package/dist/engine/graph.js +39 -20
- package/dist/engine/resolver.js +5 -0
- package/dist/engine/scanner.js +163 -62
- package/package.json +1 -1
- package/src/commands/check.ts +1 -1
- package/src/commands/sync.ts +62 -2
- package/src/commands/watch.ts +2 -2
- package/src/engine/callResolver.ts +75 -8
- package/src/engine/engine.ts +30 -21
- package/src/engine/graph.ts +49 -24
- package/src/engine/resolver.ts +10 -0
- package/src/engine/scanner.ts +223 -72
package/dist/commands/check.js
CHANGED
|
@@ -25,7 +25,7 @@ async function check({ fix = false }) {
|
|
|
25
25
|
try {
|
|
26
26
|
const raw = await promises_1.default.readFile(file, "utf-8");
|
|
27
27
|
const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
|
|
28
|
-
engine.scanFile(virtualFile, content);
|
|
28
|
+
engine.scanFile(virtualFile, content, config);
|
|
29
29
|
}
|
|
30
30
|
catch (err) {
|
|
31
31
|
console.log(chalk_1.default.yellow(`Skipped: ${file}`));
|
package/dist/commands/sync.js
CHANGED
|
@@ -23,11 +23,19 @@ async function sync() {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
const engine = new engine_1.EventraEngine(process.cwd());
|
|
26
|
+
const detectedFn = new Set();
|
|
27
|
+
const detectedCmp = new Map();
|
|
26
28
|
for (const file of files) {
|
|
27
29
|
try {
|
|
28
30
|
const raw = await promises_1.default.readFile(file, "utf-8");
|
|
29
31
|
const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
|
|
30
|
-
engine.scanFile(virtualFile, content);
|
|
32
|
+
const res = engine.scanFile(virtualFile, content, config);
|
|
33
|
+
for (const w of res.detectedFunctionWrappers) {
|
|
34
|
+
detectedFn.add(w);
|
|
35
|
+
}
|
|
36
|
+
for (const [k, v] of res.detectedComponentWrappers) {
|
|
37
|
+
detectedCmp.set(k, v);
|
|
38
|
+
}
|
|
31
39
|
}
|
|
32
40
|
catch (err) {
|
|
33
41
|
console.log(chalk_1.default.yellow(`Skipped: ${file}`));
|
|
@@ -36,8 +44,34 @@ async function sync() {
|
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
46
|
}
|
|
47
|
+
// EVENTS
|
|
39
48
|
const events = engine.getAllEvents().sort();
|
|
40
|
-
config.events
|
|
49
|
+
const mergedEvents = new Set(config.events ?? []);
|
|
50
|
+
events.forEach(e => mergedEvents.add(e));
|
|
51
|
+
config.events = [...mergedEvents].sort();
|
|
52
|
+
// FUNCTION WRAPPERS
|
|
53
|
+
config.functionWrappers = mergeUnique(config.functionWrappers ?? [], [...detectedFn].map(name => ({ name })), w => w.name);
|
|
54
|
+
// COMPONENT WRAPPERS
|
|
55
|
+
config.wrappers = mergeUnique(config.wrappers ?? [], [...detectedCmp.entries()].map(([name, prop]) => ({
|
|
56
|
+
name,
|
|
57
|
+
prop,
|
|
58
|
+
})), w => `${w.name}:${w.prop}`);
|
|
41
59
|
await (0, config_1.saveConfig)(config);
|
|
42
60
|
console.log(chalk_1.default.green(`Found ${events.length} events`));
|
|
61
|
+
if (detectedFn.size) {
|
|
62
|
+
console.log(chalk_1.default.blue("\nDetected function wrappers:"));
|
|
63
|
+
detectedFn.forEach(w => console.log(chalk_1.default.gray(`• ${w}`)));
|
|
64
|
+
}
|
|
65
|
+
if (detectedCmp.size) {
|
|
66
|
+
console.log(chalk_1.default.blue("\nDetected component wrappers:"));
|
|
67
|
+
detectedCmp.forEach((prop, name) => console.log(chalk_1.default.gray(`• ${name} (${prop})`)));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// HELPERS
|
|
71
|
+
function mergeUnique(a, b, key) {
|
|
72
|
+
const map = new Map();
|
|
73
|
+
[...b, ...a].forEach(item => {
|
|
74
|
+
map.set(key(item), item);
|
|
75
|
+
});
|
|
76
|
+
return [...map.values()];
|
|
43
77
|
}
|
package/dist/commands/watch.js
CHANGED
|
@@ -24,7 +24,7 @@ async function watch() {
|
|
|
24
24
|
try {
|
|
25
25
|
const raw = await promises_1.default.readFile(file, "utf-8");
|
|
26
26
|
const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
|
|
27
|
-
engine.scanFile(virtualFile, content);
|
|
27
|
+
engine.scanFile(virtualFile, content, config);
|
|
28
28
|
}
|
|
29
29
|
catch {
|
|
30
30
|
console.log(chalk_1.default.gray(`skip: ${file}`));
|
|
@@ -46,7 +46,7 @@ async function watch() {
|
|
|
46
46
|
try {
|
|
47
47
|
const raw = await promises_1.default.readFile(file, "utf-8");
|
|
48
48
|
const { content, virtualFile } = (0, processFile_1.processFile)(file, raw);
|
|
49
|
-
engine.updateFile(virtualFile, content);
|
|
49
|
+
engine.updateFile(virtualFile, content, config);
|
|
50
50
|
}
|
|
51
51
|
catch {
|
|
52
52
|
console.log(chalk_1.default.gray(`skip: ${file}`));
|
|
@@ -5,18 +5,71 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.resolveFunctionFromCall = resolveFunctionFromCall;
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
function isFunctionLike(node) {
|
|
9
|
+
return (typescript_1.default.isFunctionDeclaration(node) ||
|
|
10
|
+
typescript_1.default.isMethodDeclaration(node) ||
|
|
11
|
+
typescript_1.default.isConstructorDeclaration(node) ||
|
|
12
|
+
typescript_1.default.isFunctionExpression(node) ||
|
|
13
|
+
typescript_1.default.isArrowFunction(node));
|
|
14
|
+
}
|
|
15
|
+
function resolveFromSymbol(symbol, checker, visited = new Set()) {
|
|
16
|
+
if (visited.has(symbol))
|
|
17
|
+
return null;
|
|
18
|
+
visited.add(symbol);
|
|
19
|
+
// alias unwrap
|
|
20
|
+
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
21
|
+
const aliased = checker.getAliasedSymbol(symbol);
|
|
22
|
+
return resolveFromSymbol(aliased, checker, visited);
|
|
23
|
+
}
|
|
24
|
+
const decls = symbol.getDeclarations() ?? [];
|
|
25
|
+
for (const decl of decls) {
|
|
26
|
+
if (isFunctionLike(decl)) {
|
|
27
|
+
return decl;
|
|
28
|
+
}
|
|
29
|
+
if (typescript_1.default.isMethodSignature(decl)) {
|
|
30
|
+
const parent = decl.parent;
|
|
31
|
+
if (typescript_1.default.isInterfaceDeclaration(parent) || typescript_1.default.isTypeLiteralNode(parent)) {
|
|
32
|
+
const name = decl.name.getText();
|
|
33
|
+
const type = checker.getTypeAtLocation(parent);
|
|
34
|
+
const props = type.getProperties();
|
|
35
|
+
for (const prop of props) {
|
|
36
|
+
if (prop.name !== name)
|
|
37
|
+
continue;
|
|
38
|
+
const resolved = resolveFromSymbol(prop, checker, visited);
|
|
39
|
+
if (resolved)
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// fallback
|
|
45
|
+
const s = checker.getSymbolAtLocation(decl);
|
|
46
|
+
if (s) {
|
|
47
|
+
const resolved = resolveFromSymbol(s, checker, visited);
|
|
48
|
+
if (resolved)
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
8
54
|
function resolveFunctionFromCall(expr, checker) {
|
|
9
55
|
const type = checker.getTypeAtLocation(expr);
|
|
10
56
|
const signatures = type.getCallSignatures();
|
|
11
57
|
for (const sig of signatures) {
|
|
12
58
|
const decl = sig.getDeclaration();
|
|
13
|
-
if (decl
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
typescript_1.default.isFunctionExpression(decl) ||
|
|
17
|
-
typescript_1.default.isArrowFunction(decl))) {
|
|
59
|
+
if (!decl)
|
|
60
|
+
continue;
|
|
61
|
+
if (isFunctionLike(decl)) {
|
|
18
62
|
return decl;
|
|
19
63
|
}
|
|
64
|
+
// MethodSignature case
|
|
65
|
+
if (typescript_1.default.isMethodSignature(decl)) {
|
|
66
|
+
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
67
|
+
if (symbol) {
|
|
68
|
+
const resolved = resolveFromSymbol(symbol, checker);
|
|
69
|
+
if (resolved)
|
|
70
|
+
return resolved;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
20
73
|
}
|
|
21
74
|
return null;
|
|
22
75
|
}
|
package/dist/engine/engine.js
CHANGED
|
@@ -8,53 +8,60 @@ const hash_1 = require("./hash");
|
|
|
8
8
|
class EventraEngine {
|
|
9
9
|
ts;
|
|
10
10
|
graph;
|
|
11
|
-
|
|
11
|
+
fileResults = new Map();
|
|
12
12
|
fileHash = new Map();
|
|
13
13
|
constructor(root) {
|
|
14
14
|
this.ts = new languageService_1.TSService(root);
|
|
15
15
|
const { baseUrl, paths } = this.ts.getModuleResolutionConfig();
|
|
16
16
|
this.graph = new graph_1.DependencyGraph(baseUrl, paths);
|
|
17
17
|
}
|
|
18
|
-
scanFile(file, content) {
|
|
18
|
+
scanFile(file, content, config) {
|
|
19
19
|
const h = (0, hash_1.hash)(content);
|
|
20
|
-
// cache
|
|
21
20
|
if (this.fileHash.get(file) === h) {
|
|
22
|
-
return this.
|
|
21
|
+
return (this.fileResults.get(file) ?? {
|
|
22
|
+
events: new Set(),
|
|
23
|
+
detectedFunctionWrappers: new Set(),
|
|
24
|
+
detectedComponentWrappers: new Map(),
|
|
25
|
+
});
|
|
23
26
|
}
|
|
24
27
|
this.fileHash.set(file, h);
|
|
25
28
|
this.ts.updateFile(file, content);
|
|
26
29
|
const source = this.ts.getSourceFile(file);
|
|
27
30
|
const checker = this.ts.getChecker();
|
|
28
|
-
if (!source || !checker)
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
+
if (!source || !checker) {
|
|
32
|
+
return {
|
|
33
|
+
events: new Set(),
|
|
34
|
+
detectedFunctionWrappers: new Set(),
|
|
35
|
+
detectedComponentWrappers: new Map(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
31
38
|
this.graph.update(file, source);
|
|
32
|
-
const
|
|
33
|
-
this.
|
|
34
|
-
return
|
|
39
|
+
const res = (0, scanner_1.scanSource)(source, checker, config);
|
|
40
|
+
this.fileResults.set(file, res);
|
|
41
|
+
return res;
|
|
35
42
|
}
|
|
36
|
-
updateFile(file, content) {
|
|
37
|
-
const
|
|
38
|
-
const dependents = this.graph.
|
|
43
|
+
updateFile(file, content, config) {
|
|
44
|
+
const res = this.scanFile(file, content, config);
|
|
45
|
+
const dependents = this.graph.getAllDependentsDeep(file);
|
|
39
46
|
for (const dep of dependents) {
|
|
40
47
|
const source = this.ts.getSourceFile(dep);
|
|
41
48
|
const checker = this.ts.getChecker();
|
|
42
49
|
if (!source || !checker)
|
|
43
50
|
continue;
|
|
44
|
-
const
|
|
45
|
-
this.
|
|
51
|
+
const r = (0, scanner_1.scanSource)(source, checker, config);
|
|
52
|
+
this.fileResults.set(dep, r);
|
|
46
53
|
}
|
|
47
|
-
return
|
|
54
|
+
return res;
|
|
48
55
|
}
|
|
49
56
|
getAllEvents() {
|
|
50
57
|
const all = new Set();
|
|
51
|
-
for (const
|
|
52
|
-
|
|
58
|
+
for (const r of this.fileResults.values()) {
|
|
59
|
+
r.events.forEach(e => all.add(e));
|
|
53
60
|
}
|
|
54
61
|
return [...all];
|
|
55
62
|
}
|
|
56
63
|
removeFile(file) {
|
|
57
|
-
this.
|
|
64
|
+
this.fileResults.delete(file);
|
|
58
65
|
this.fileHash.delete(file);
|
|
59
66
|
this.graph.remove(file);
|
|
60
67
|
}
|
package/dist/engine/graph.js
CHANGED
|
@@ -12,6 +12,7 @@ class DependencyGraph {
|
|
|
12
12
|
paths;
|
|
13
13
|
reverse = new Map();
|
|
14
14
|
exports = new Map();
|
|
15
|
+
fileCache = new Map();
|
|
15
16
|
constructor(baseUrl, paths) {
|
|
16
17
|
this.baseUrl = baseUrl;
|
|
17
18
|
this.paths = paths;
|
|
@@ -19,37 +20,36 @@ class DependencyGraph {
|
|
|
19
20
|
normalize(file) {
|
|
20
21
|
return path_1.default.resolve(file);
|
|
21
22
|
}
|
|
22
|
-
// TS-like resolver
|
|
23
23
|
resolveModule(module, from) {
|
|
24
|
-
// relative
|
|
25
24
|
if (module.startsWith(".")) {
|
|
26
25
|
const abs = path_1.default.resolve(path_1.default.dirname(from), module);
|
|
27
26
|
return this.resolveFile(abs);
|
|
28
27
|
}
|
|
29
|
-
// paths mapping
|
|
30
28
|
for (const key in this.paths) {
|
|
31
|
-
const pattern = key.replace(
|
|
29
|
+
const pattern = key.replace(/\*/g, "(.*)");
|
|
32
30
|
const regex = new RegExp("^" + pattern + "$");
|
|
33
31
|
const match = module.match(regex);
|
|
34
32
|
if (!match)
|
|
35
33
|
continue;
|
|
36
|
-
const wildcard = match
|
|
34
|
+
const wildcard = match.slice(1);
|
|
37
35
|
for (const target of this.paths[key]) {
|
|
38
|
-
|
|
36
|
+
let replaced = target;
|
|
37
|
+
wildcard.forEach(w => {
|
|
38
|
+
replaced = replaced.replace("*", w);
|
|
39
|
+
});
|
|
39
40
|
const abs = path_1.default.resolve(this.baseUrl, replaced);
|
|
40
41
|
const resolved = this.resolveFile(abs);
|
|
41
42
|
if (resolved)
|
|
42
43
|
return resolved;
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
// baseUrl fallback
|
|
46
46
|
const abs = path_1.default.resolve(this.baseUrl, module);
|
|
47
|
-
|
|
48
|
-
if (resolved)
|
|
49
|
-
return resolved;
|
|
50
|
-
return null;
|
|
47
|
+
return this.resolveFile(abs);
|
|
51
48
|
}
|
|
52
49
|
resolveFile(base) {
|
|
50
|
+
if (this.fileCache.has(base)) {
|
|
51
|
+
return this.fileCache.get(base);
|
|
52
|
+
}
|
|
53
53
|
const tryFiles = [
|
|
54
54
|
base,
|
|
55
55
|
base + ".ts",
|
|
@@ -62,16 +62,18 @@ class DependencyGraph {
|
|
|
62
62
|
];
|
|
63
63
|
for (const f of tryFiles) {
|
|
64
64
|
if (fs_1.default.existsSync(f)) {
|
|
65
|
-
|
|
65
|
+
const normalized = this.normalize(f);
|
|
66
|
+
this.fileCache.set(base, normalized);
|
|
67
|
+
return normalized;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
70
|
+
this.fileCache.set(base, null);
|
|
68
71
|
return null;
|
|
69
72
|
}
|
|
70
73
|
update(file, source) {
|
|
71
74
|
const normalizedFile = this.normalize(file);
|
|
72
75
|
this.exports.set(normalizedFile, new Set());
|
|
73
76
|
for (const stmt of source.statements) {
|
|
74
|
-
// IMPORT
|
|
75
77
|
if (typescript_1.default.isImportDeclaration(stmt)) {
|
|
76
78
|
const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
77
79
|
const resolved = this.resolveModule(module, normalizedFile);
|
|
@@ -82,16 +84,12 @@ class DependencyGraph {
|
|
|
82
84
|
}
|
|
83
85
|
this.reverse.get(resolved).add(normalizedFile);
|
|
84
86
|
}
|
|
85
|
-
// EXPORT *
|
|
86
87
|
if (typescript_1.default.isExportDeclaration(stmt) && stmt.moduleSpecifier) {
|
|
87
88
|
const module = stmt.moduleSpecifier.getText().replace(/['"]/g, "");
|
|
88
89
|
const resolved = this.resolveModule(module, normalizedFile);
|
|
89
90
|
if (!resolved)
|
|
90
91
|
continue;
|
|
91
|
-
|
|
92
|
-
this.exports.set(normalizedFile, new Set());
|
|
93
|
-
}
|
|
94
|
-
this.exports.get(normalizedFile).add(resolved);
|
|
92
|
+
this.exports.get(normalizedFile)?.add(resolved);
|
|
95
93
|
if (!this.reverse.has(resolved)) {
|
|
96
94
|
this.reverse.set(resolved, new Set());
|
|
97
95
|
}
|
|
@@ -99,15 +97,36 @@ class DependencyGraph {
|
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
getAllDependentsDeep(file) {
|
|
101
|
+
const start = this.normalize(file);
|
|
102
|
+
const visited = new Set();
|
|
103
|
+
const result = new Set();
|
|
104
|
+
const walk = (f) => {
|
|
105
|
+
if (visited.has(f))
|
|
106
|
+
return;
|
|
107
|
+
visited.add(f);
|
|
108
|
+
const deps = this.reverse.get(f);
|
|
109
|
+
deps?.forEach(d => {
|
|
110
|
+
result.add(d);
|
|
111
|
+
walk(d);
|
|
112
|
+
});
|
|
113
|
+
const ex = this.exports.get(f);
|
|
114
|
+
ex?.forEach(e => walk(e));
|
|
115
|
+
};
|
|
116
|
+
walk(start);
|
|
117
|
+
return [...result];
|
|
104
118
|
}
|
|
105
119
|
remove(file) {
|
|
106
120
|
const normalized = this.normalize(file);
|
|
121
|
+
this.reverse.delete(normalized);
|
|
107
122
|
this.exports.delete(normalized);
|
|
123
|
+
this.fileCache.delete(normalized);
|
|
108
124
|
for (const set of this.reverse.values()) {
|
|
109
125
|
set.delete(normalized);
|
|
110
126
|
}
|
|
127
|
+
for (const set of this.exports.values()) {
|
|
128
|
+
set.delete(normalized);
|
|
129
|
+
}
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
132
|
exports.DependencyGraph = DependencyGraph;
|
package/dist/engine/resolver.js
CHANGED
|
@@ -37,6 +37,11 @@ function resolveNodeValue(node, checker, paramMap, seen = new Set()) {
|
|
|
37
37
|
let symbol = checker.getSymbolAtLocation(node);
|
|
38
38
|
if (!symbol)
|
|
39
39
|
return null;
|
|
40
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
41
|
+
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
42
|
+
return resolveNodeValue(decl.initializer, checker, paramMap, seen);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
40
45
|
symbol = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker) ?? symbol;
|
|
41
46
|
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
42
47
|
symbol = checker.getAliasedSymbol(symbol);
|
package/dist/engine/scanner.js
CHANGED
|
@@ -7,94 +7,195 @@ exports.scanSource = scanSource;
|
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
8
|
const resolver_1 = require("./resolver");
|
|
9
9
|
const callResolver_1 = require("./callResolver");
|
|
10
|
+
const exportResolver_1 = require("./exportResolver");
|
|
10
11
|
const functionAnalyzer_1 = require("./functionAnalyzer");
|
|
12
|
+
const TRACKING_NAMES = new Set(["track", "event", "capture", "send"]);
|
|
11
13
|
function getCallName(expr) {
|
|
12
14
|
if (typescript_1.default.isIdentifier(expr))
|
|
13
15
|
return expr.text;
|
|
14
|
-
if (typescript_1.default.isPropertyAccessExpression(expr))
|
|
16
|
+
if (typescript_1.default.isPropertyAccessExpression(expr))
|
|
15
17
|
return expr.name.text;
|
|
16
|
-
}
|
|
17
18
|
return "";
|
|
18
19
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
function getJsxAttrName(name) {
|
|
21
|
+
if (typescript_1.default.isIdentifier(name))
|
|
22
|
+
return name.text;
|
|
23
|
+
if (typescript_1.default.isJsxNamespacedName(name))
|
|
24
|
+
return name.name.text;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function isValidEvent(v) {
|
|
28
|
+
return (v &&
|
|
29
|
+
v.length > 1 &&
|
|
30
|
+
v.length < 100 &&
|
|
31
|
+
!v.includes(" ") &&
|
|
32
|
+
/^[a-zA-Z0-9._:-]+$/.test(v));
|
|
33
|
+
}
|
|
34
|
+
function isPropUsedInTracking(fn, prop) {
|
|
35
|
+
let found = false;
|
|
36
|
+
function visit(node) {
|
|
37
|
+
if (typescript_1.default.isCallExpression(node)) {
|
|
38
|
+
const name = getCallName(node.expression);
|
|
39
|
+
if (!TRACKING_NAMES.has(name))
|
|
40
|
+
return;
|
|
41
|
+
for (const arg of node.arguments) {
|
|
42
|
+
if ((typescript_1.default.isIdentifier(arg) && arg.text === prop) ||
|
|
43
|
+
(typescript_1.default.isPropertyAccessExpression(arg) &&
|
|
44
|
+
arg.name.text === prop)) {
|
|
45
|
+
found = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
}
|
|
49
|
+
typescript_1.default.forEachChild(node, visit);
|
|
35
50
|
}
|
|
36
|
-
|
|
51
|
+
visit(fn);
|
|
52
|
+
return found;
|
|
37
53
|
}
|
|
38
|
-
function scanSource(source, checker, visited = new Set(), depth = 0) {
|
|
39
|
-
if (depth > 5)
|
|
40
|
-
return new Set();
|
|
54
|
+
function scanSource(source, checker, config, visited = new Set(), depth = 0) {
|
|
41
55
|
const events = new Set();
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
const detectedFunctionWrappers = new Set();
|
|
57
|
+
const detectedComponentWrappers = new Map();
|
|
58
|
+
if (depth > 5 || visited.has(source.fileName)) {
|
|
59
|
+
return { events, detectedFunctionWrappers, detectedComponentWrappers };
|
|
60
|
+
}
|
|
44
61
|
visited.add(source.fileName);
|
|
45
62
|
function visit(node) {
|
|
63
|
+
// CALL EXPRESSIONS
|
|
46
64
|
if (typescript_1.default.isCallExpression(node)) {
|
|
47
65
|
const name = getCallName(node.expression);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
const isKnownWrapper = config.functionWrappers.some(w => w.name === name);
|
|
67
|
+
const fn = (0, callResolver_1.resolveFunctionFromCall)(node.expression, checker);
|
|
68
|
+
const isAutoWrapper = fn && (0, functionAnalyzer_1.findTrackParamIndex)(fn) !== null;
|
|
69
|
+
const isTracking = isKnownWrapper ||
|
|
70
|
+
TRACKING_NAMES.has(name) ||
|
|
71
|
+
isAutoWrapper;
|
|
72
|
+
if (!isTracking)
|
|
73
|
+
return;
|
|
74
|
+
// AUTO DETECT WRAPPER
|
|
75
|
+
if (!isKnownWrapper &&
|
|
76
|
+
fn &&
|
|
77
|
+
!TRACKING_NAMES.has(name)) {
|
|
78
|
+
detectedFunctionWrappers.add(name);
|
|
79
|
+
}
|
|
80
|
+
const paramMap = new Map();
|
|
81
|
+
if (fn && typescript_1.default.isFunctionLike(fn)) {
|
|
82
|
+
fn.parameters.forEach((param, i) => {
|
|
83
|
+
const arg = node.arguments[i];
|
|
84
|
+
if (!arg)
|
|
85
|
+
return;
|
|
86
|
+
if (typescript_1.default.isIdentifier(param.name)) {
|
|
87
|
+
paramMap.set(param.name.text, arg);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
let handled = false;
|
|
92
|
+
if (fn) {
|
|
93
|
+
const idx = (0, functionAnalyzer_1.findTrackParamIndex)(fn);
|
|
94
|
+
if (idx !== null && node.arguments[idx]) {
|
|
95
|
+
const res = (0, resolver_1.resolveNodeValue)(node.arguments[idx], checker, paramMap);
|
|
96
|
+
res?.values.forEach(v => {
|
|
97
|
+
if (isValidEvent(v))
|
|
98
|
+
events.add(v);
|
|
59
99
|
});
|
|
100
|
+
handled = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// fallback
|
|
104
|
+
if (!handled) {
|
|
105
|
+
for (const arg of node.arguments) {
|
|
106
|
+
const res = (0, resolver_1.resolveNodeValue)(arg, checker, paramMap);
|
|
107
|
+
res?.values.forEach(v => {
|
|
108
|
+
if (isValidEvent(v))
|
|
109
|
+
events.add(v);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// cross-file traversal
|
|
114
|
+
if (fn) {
|
|
115
|
+
const sf = fn.getSourceFile();
|
|
116
|
+
if (!visited.has(sf.fileName)) {
|
|
117
|
+
const res = scanSource(sf, checker, config, visited, depth + 1);
|
|
118
|
+
res.events.forEach(e => events.add(e));
|
|
60
119
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// JSX COMPONENTS
|
|
123
|
+
if (typescript_1.default.isJsxSelfClosingElement(node) ||
|
|
124
|
+
typescript_1.default.isJsxOpeningElement(node)) {
|
|
125
|
+
const tagNode = typescript_1.default.isPropertyAccessExpression(node.tagName)
|
|
126
|
+
? node.tagName.name
|
|
127
|
+
: node.tagName;
|
|
128
|
+
let symbol = checker.getSymbolAtLocation(tagNode);
|
|
129
|
+
if (symbol) {
|
|
130
|
+
const resolved = (0, exportResolver_1.resolveExportedSymbol)(symbol, checker);
|
|
131
|
+
if (resolved)
|
|
132
|
+
symbol = resolved;
|
|
133
|
+
}
|
|
134
|
+
if (symbol && (symbol.flags & typescript_1.default.SymbolFlags.Alias)) {
|
|
135
|
+
const aliased = checker.getAliasedSymbol(symbol);
|
|
136
|
+
if (aliased)
|
|
137
|
+
symbol = aliased;
|
|
138
|
+
}
|
|
139
|
+
if (!symbol)
|
|
140
|
+
return;
|
|
141
|
+
const tagName = typescript_1.default.isPropertyAccessExpression(node.tagName)
|
|
142
|
+
? node.tagName.name.getText()
|
|
143
|
+
: node.tagName.getText();
|
|
144
|
+
const declarations = symbol.getDeclarations();
|
|
145
|
+
if (!declarations)
|
|
146
|
+
return;
|
|
147
|
+
for (const decl of declarations) {
|
|
148
|
+
let fn = null;
|
|
149
|
+
if (typescript_1.default.isFunctionDeclaration(decl)) {
|
|
150
|
+
fn = decl;
|
|
151
|
+
}
|
|
152
|
+
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
153
|
+
const init = decl.initializer;
|
|
154
|
+
if (typescript_1.default.isArrowFunction(init) ||
|
|
155
|
+
typescript_1.default.isFunctionExpression(init)) {
|
|
156
|
+
fn = init;
|
|
78
157
|
}
|
|
79
158
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
159
|
+
if (!fn)
|
|
160
|
+
continue;
|
|
161
|
+
const param = fn.parameters[0];
|
|
162
|
+
if (!param)
|
|
163
|
+
continue;
|
|
164
|
+
if (!typescript_1.default.isObjectBindingPattern(param.name))
|
|
165
|
+
continue;
|
|
166
|
+
for (const el of param.name.elements) {
|
|
167
|
+
const prop = el.name.getText();
|
|
168
|
+
const isEvent = isPropUsedInTracking(fn, prop) ||
|
|
169
|
+
config.wrappers.some(w => w.name === tagName && w.prop === prop);
|
|
170
|
+
if (!isEvent)
|
|
171
|
+
continue;
|
|
172
|
+
const attr = node.attributes.properties.find(p => {
|
|
173
|
+
if (!typescript_1.default.isJsxAttribute(p))
|
|
174
|
+
return false;
|
|
175
|
+
return getJsxAttrName(p.name) === prop;
|
|
176
|
+
});
|
|
177
|
+
if (!attr || !attr.initializer)
|
|
178
|
+
continue;
|
|
179
|
+
const expr = typescript_1.default.isJsxExpression(attr.initializer)
|
|
180
|
+
? attr.initializer.expression
|
|
181
|
+
: attr.initializer;
|
|
182
|
+
if (!expr)
|
|
183
|
+
continue;
|
|
184
|
+
const res = (0, resolver_1.resolveNodeValue)(expr, checker);
|
|
84
185
|
res?.values.forEach(v => {
|
|
85
|
-
if (v
|
|
86
|
-
v.length > 1 &&
|
|
87
|
-
v.length < 100 &&
|
|
88
|
-
!v.includes(" ") &&
|
|
89
|
-
/^[a-zA-Z0-9._:-]+$/.test(v)) {
|
|
186
|
+
if (isValidEvent(v))
|
|
90
187
|
events.add(v);
|
|
91
|
-
}
|
|
92
188
|
});
|
|
189
|
+
detectedComponentWrappers.set(tagName, prop);
|
|
93
190
|
}
|
|
94
191
|
}
|
|
95
192
|
}
|
|
96
193
|
typescript_1.default.forEachChild(node, visit);
|
|
97
194
|
}
|
|
98
195
|
visit(source);
|
|
99
|
-
return
|
|
196
|
+
return {
|
|
197
|
+
events,
|
|
198
|
+
detectedFunctionWrappers,
|
|
199
|
+
detectedComponentWrappers,
|
|
200
|
+
};
|
|
100
201
|
}
|
package/package.json
CHANGED
package/src/commands/check.ts
CHANGED
|
@@ -28,7 +28,7 @@ export async function check({ fix = false }: { fix?: boolean }) {
|
|
|
28
28
|
|
|
29
29
|
const { content, virtualFile } = processFile(file, raw);
|
|
30
30
|
|
|
31
|
-
engine.scanFile(virtualFile, content);
|
|
31
|
+
engine.scanFile(virtualFile, content, config);
|
|
32
32
|
|
|
33
33
|
} catch (err) {
|
|
34
34
|
console.log(chalk.yellow(`Skipped: ${file}`));
|