@flisk/analyze-tracking 0.8.7 → 0.9.0
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 +84 -19
- package/package.json +5 -3
- package/src/analyze/index.js +20 -13
- package/src/analyze/javascript/detectors/analytics-source.js +25 -14
- package/src/analyze/javascript/extractors/event-extractor.js +24 -1
- package/src/analyze/javascript/parser.js +23 -13
- package/src/analyze/javascript/utils/import-resolver.js +233 -0
- package/src/analyze/swift/constants.js +61 -0
- package/src/analyze/swift/custom.js +36 -0
- package/src/analyze/swift/index.js +708 -0
- package/src/analyze/swift/providers.js +51 -0
- package/src/analyze/swift/runtime.js +46 -0
- package/src/analyze/swift/utils.js +75 -0
- package/src/analyze/typescript/detectors/analytics-source.js +47 -10
- package/src/analyze/utils/customFunctionParser.js +20 -8
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ts = require('typescript');
|
|
4
|
+
const acorn = require('acorn');
|
|
5
|
+
const jsx = require('acorn-jsx');
|
|
6
|
+
const { PARSER_OPTIONS, NODE_TYPES } = require('../constants');
|
|
7
|
+
|
|
8
|
+
const JS_EXTENSIONS = ['.js', '.mjs', '.cjs', '.jsx'];
|
|
9
|
+
const TS_EXTENSIONS = ['.ts', '.tsx'];
|
|
10
|
+
const ALL_EXTENSIONS = [...JS_EXTENSIONS, ...TS_EXTENSIONS, '.json'];
|
|
11
|
+
|
|
12
|
+
function tryFileWithExtensions(basePath) {
|
|
13
|
+
for (const ext of ALL_EXTENSIONS) {
|
|
14
|
+
const full = basePath + ext;
|
|
15
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile()) return full;
|
|
16
|
+
}
|
|
17
|
+
// index.* inside directory
|
|
18
|
+
if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {
|
|
19
|
+
for (const ext of ALL_EXTENSIONS) {
|
|
20
|
+
const idx = path.join(basePath, 'index' + ext);
|
|
21
|
+
if (fs.existsSync(idx) && fs.statSync(idx).isFile()) return idx;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveModulePath(specifier, fromDir) {
|
|
28
|
+
// Relative or absolute
|
|
29
|
+
if (specifier.startsWith('.') || specifier.startsWith('/')) {
|
|
30
|
+
const abs = path.resolve(fromDir, specifier);
|
|
31
|
+
const direct = tryFileWithExtensions(abs);
|
|
32
|
+
if (direct) return direct;
|
|
33
|
+
} else {
|
|
34
|
+
// Bare or aliased import – search upwards for a matching folder for first segment
|
|
35
|
+
const parts = specifier.split('/');
|
|
36
|
+
let current = fromDir;
|
|
37
|
+
while (true) {
|
|
38
|
+
const candidateRoot = path.join(current, parts[0]);
|
|
39
|
+
if (fs.existsSync(candidateRoot) && fs.statSync(candidateRoot).isDirectory()) {
|
|
40
|
+
const fullBase = path.join(current, specifier);
|
|
41
|
+
const resolved = tryFileWithExtensions(fullBase);
|
|
42
|
+
if (resolved) return resolved;
|
|
43
|
+
}
|
|
44
|
+
const parent = path.dirname(current);
|
|
45
|
+
if (parent === current) break;
|
|
46
|
+
current = parent;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extracts exported constant object maps from a JS/TS file.
|
|
53
|
+
// Returns: { ExportName: { KEY: 'value', ... }, ... }
|
|
54
|
+
function extractExportedConstStringMap(filePath) {
|
|
55
|
+
const code = fs.readFileSync(filePath, 'utf8');
|
|
56
|
+
const map = {};
|
|
57
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
58
|
+
|
|
59
|
+
if (TS_EXTENSIONS.includes(ext)) {
|
|
60
|
+
const sourceFile = ts.createSourceFile(filePath, code, ts.ScriptTarget.Latest, true);
|
|
61
|
+
for (const stmt of sourceFile.statements) {
|
|
62
|
+
if (ts.isVariableStatement(stmt) && stmt.modifiers && stmt.modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
63
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
64
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
65
|
+
const exportName = decl.name.escapedText;
|
|
66
|
+
const obj = unwrapFreezeToObjectLiteral(decl.initializer);
|
|
67
|
+
if (obj) {
|
|
68
|
+
const entries = objectLiteralToStringMapTS(obj);
|
|
69
|
+
if (Object.keys(entries).length > 0) {
|
|
70
|
+
map[exportName] = entries;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return map;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// JS/JSX – parse with acorn + JSX
|
|
81
|
+
const parser = acorn.Parser.extend(jsx());
|
|
82
|
+
let ast;
|
|
83
|
+
try {
|
|
84
|
+
ast = parser.parse(code, { ...PARSER_OPTIONS, sourceType: 'module' });
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return map;
|
|
87
|
+
}
|
|
88
|
+
// Look for export named declarations with const object or Object.freeze
|
|
89
|
+
ast.body.forEach(node => {
|
|
90
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration && node.declaration.type === 'VariableDeclaration') {
|
|
91
|
+
node.declaration.declarations.forEach(decl => {
|
|
92
|
+
if (decl.id && decl.id.type === NODE_TYPES.IDENTIFIER && decl.init) {
|
|
93
|
+
const exportName = decl.id.name;
|
|
94
|
+
const obj = unwrapFreezeToObjectLiteralJS(decl.init);
|
|
95
|
+
if (obj && obj.type === NODE_TYPES.OBJECT_EXPRESSION) {
|
|
96
|
+
const entries = objectExpressionToStringMapJS(obj);
|
|
97
|
+
if (Object.keys(entries).length > 0) {
|
|
98
|
+
map[exportName] = entries;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return map;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function unwrapFreezeToObjectLiteral(initializer) {
|
|
110
|
+
if (ts.isObjectLiteralExpression(initializer)) return initializer;
|
|
111
|
+
if (ts.isCallExpression(initializer)) {
|
|
112
|
+
const callee = initializer.expression;
|
|
113
|
+
if (
|
|
114
|
+
ts.isPropertyAccessExpression(callee) &&
|
|
115
|
+
ts.isIdentifier(callee.expression) && callee.expression.escapedText === 'Object' &&
|
|
116
|
+
callee.name.escapedText === 'freeze' &&
|
|
117
|
+
initializer.arguments.length > 0 && ts.isObjectLiteralExpression(initializer.arguments[0])
|
|
118
|
+
) {
|
|
119
|
+
return initializer.arguments[0];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function objectLiteralToStringMapTS(obj) {
|
|
126
|
+
const out = {};
|
|
127
|
+
for (const prop of obj.properties) {
|
|
128
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
129
|
+
const key = ts.isIdentifier(prop.name) ? prop.name.escapedText : ts.isStringLiteral(prop.name) ? prop.name.text : null;
|
|
130
|
+
if (!key) continue;
|
|
131
|
+
if (prop.initializer && ts.isStringLiteral(prop.initializer)) {
|
|
132
|
+
out[key] = prop.initializer.text;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function unwrapFreezeToObjectLiteralJS(init) {
|
|
139
|
+
if (init.type === NODE_TYPES.OBJECT_EXPRESSION) return init;
|
|
140
|
+
if (init.type === 'CallExpression') {
|
|
141
|
+
const callee = init.callee;
|
|
142
|
+
if (
|
|
143
|
+
callee && callee.type === NODE_TYPES.MEMBER_EXPRESSION &&
|
|
144
|
+
callee.object && callee.object.type === NODE_TYPES.IDENTIFIER && callee.object.name === 'Object' &&
|
|
145
|
+
callee.property && callee.property.type === NODE_TYPES.IDENTIFIER && callee.property.name === 'freeze' &&
|
|
146
|
+
init.arguments && init.arguments.length > 0 && init.arguments[0].type === NODE_TYPES.OBJECT_EXPRESSION
|
|
147
|
+
) {
|
|
148
|
+
return init.arguments[0];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function objectExpressionToStringMapJS(obj) {
|
|
155
|
+
const out = {};
|
|
156
|
+
obj.properties.forEach(prop => {
|
|
157
|
+
if (!prop.key || !prop.value) return;
|
|
158
|
+
const key = prop.key.name || prop.key.value;
|
|
159
|
+
if (prop.value.type === NODE_TYPES.LITERAL && typeof prop.value.value === 'string') {
|
|
160
|
+
out[key] = prop.value.value;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Collect imported constant string maps used by this file
|
|
167
|
+
function collectImportedConstantStringMap(filePath, ast) {
|
|
168
|
+
const fromDir = path.dirname(filePath);
|
|
169
|
+
const imports = [];
|
|
170
|
+
|
|
171
|
+
// ES imports
|
|
172
|
+
ast.body.forEach(node => {
|
|
173
|
+
if (node.type === 'ImportDeclaration' && node.source && typeof node.source.value === 'string') {
|
|
174
|
+
const spec = node.source.value;
|
|
175
|
+
node.specifiers.forEach(s => {
|
|
176
|
+
if (s.type === 'ImportSpecifier' && s.imported && s.local) {
|
|
177
|
+
imports.push({ local: s.local.name, exported: s.imported.name, spec });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// CommonJS requires: const { X } = require('mod')
|
|
184
|
+
ast.body.forEach(node => {
|
|
185
|
+
if (node.type === 'VariableDeclaration') {
|
|
186
|
+
node.declarations.forEach(decl => {
|
|
187
|
+
if (
|
|
188
|
+
decl.init && decl.init.type === 'CallExpression' &&
|
|
189
|
+
decl.init.callee && decl.init.callee.type === NODE_TYPES.IDENTIFIER && decl.init.callee.name === 'require' &&
|
|
190
|
+
decl.init.arguments && decl.init.arguments[0] && decl.init.arguments[0].type === NODE_TYPES.LITERAL
|
|
191
|
+
) {
|
|
192
|
+
const spec = String(decl.init.arguments[0].value);
|
|
193
|
+
if (decl.id && decl.id.type === 'ObjectPattern') {
|
|
194
|
+
decl.id.properties.forEach(p => {
|
|
195
|
+
if (p.key && p.value && p.key.type === NODE_TYPES.IDENTIFIER && p.value.type === NODE_TYPES.IDENTIFIER) {
|
|
196
|
+
imports.push({ local: p.value.name, exported: p.key.name, spec });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const constantMap = {};
|
|
206
|
+
const bySpec = new Map();
|
|
207
|
+
for (const imp of imports) {
|
|
208
|
+
let resolved = bySpec.get(imp.spec);
|
|
209
|
+
if (!resolved) {
|
|
210
|
+
const file = resolveModulePath(imp.spec, fromDir);
|
|
211
|
+
bySpec.set(imp.spec, file || null);
|
|
212
|
+
resolved = file || null;
|
|
213
|
+
}
|
|
214
|
+
if (!resolved) continue;
|
|
215
|
+
try {
|
|
216
|
+
const exported = extractExportedConstStringMap(resolved);
|
|
217
|
+
const entries = exported[imp.exported];
|
|
218
|
+
if (entries && typeof entries === 'object') {
|
|
219
|
+
constantMap[imp.local] = entries;
|
|
220
|
+
}
|
|
221
|
+
} catch (_) { /* ignore resolution errors */ }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return constantMap;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
resolveModulePath,
|
|
229
|
+
extractExportedConstStringMap,
|
|
230
|
+
collectImportedConstantStringMap
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants and helper-return collectors for Swift fixtures
|
|
3
|
+
* - Scans directory for top-level lets and enum/struct static lets
|
|
4
|
+
* - Extracts simple function returns for dict/array builders
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
function buildCrossFileConstMap(dir) {
|
|
11
|
+
const map = {};
|
|
12
|
+
try {
|
|
13
|
+
const entries = fs.readdirSync(dir).filter(f => f.endsWith('.swift'));
|
|
14
|
+
for (const f of entries) {
|
|
15
|
+
const fp = path.join(dir, f);
|
|
16
|
+
const content = fs.readFileSync(fp, 'utf8');
|
|
17
|
+
// Top-level: let NAME = "..."
|
|
18
|
+
for (const m of content.matchAll(/\blet\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
|
|
19
|
+
map[m[1]] = m[2];
|
|
20
|
+
}
|
|
21
|
+
// Enum/struct blocks: capture namespace and all static lets inside
|
|
22
|
+
let idx = 0;
|
|
23
|
+
while (idx < content.length) {
|
|
24
|
+
const head = content.slice(idx);
|
|
25
|
+
const mm = /\b(enum|struct)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/m.exec(head);
|
|
26
|
+
if (!mm) break;
|
|
27
|
+
const ns = mm[2];
|
|
28
|
+
const blockStart = idx + mm.index + mm[0].length - 1; // position at '{'
|
|
29
|
+
// Find matching closing brace
|
|
30
|
+
let depth = 0; let end = -1;
|
|
31
|
+
for (let i = blockStart; i < content.length; i++) {
|
|
32
|
+
const ch = content[i];
|
|
33
|
+
if (ch === '{') depth++;
|
|
34
|
+
else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
35
|
+
}
|
|
36
|
+
if (end === -1) break;
|
|
37
|
+
const block = content.slice(blockStart + 1, end);
|
|
38
|
+
for (const sm of block.matchAll(/\bstatic\s+let\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
|
|
39
|
+
const key = sm[1];
|
|
40
|
+
const val = sm[2];
|
|
41
|
+
map[`${ns}.${key}`] = val;
|
|
42
|
+
}
|
|
43
|
+
idx = end + 1;
|
|
44
|
+
}
|
|
45
|
+
// Capture very simple helper returns
|
|
46
|
+
// func makeAddress() -> [String: Any] { return [ ... ] }
|
|
47
|
+
for (const m of content.matchAll(/func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*->\s*\[[^\]]+\][^{]*\{[\s\S]*?return\s*(\[[\s\S]*?\])[\s\S]*?\}/g)) {
|
|
48
|
+
map.__dictFuncs = map.__dictFuncs || {};
|
|
49
|
+
map.__dictFuncs[m[1]] = { kind: 'dict', text: m[2] };
|
|
50
|
+
}
|
|
51
|
+
// func makeProducts() -> [[String: Any]] { return [ ... ] } (array)
|
|
52
|
+
for (const m of content.matchAll(/func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*->\s*\[\[[^\]]+\]\][^{]*\{[\s\S]*?return\s*(\[[\s\S]*?\])[\s\S]*?\}/g)) {
|
|
53
|
+
map.__dictFuncs = map.__dictFuncs || {};
|
|
54
|
+
map.__dictFuncs[m[1]] = { kind: 'array', text: m[2] };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
return map;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { buildCrossFileConstMap };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom function detection for Swift
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { normalizeChainPart, endsWithChain, extractStringLiteral, isIdentifier } = require('./utils');
|
|
6
|
+
|
|
7
|
+
function matchCustomSignature(call, customFunctionSignatures) {
|
|
8
|
+
if (!Array.isArray(customFunctionSignatures) || customFunctionSignatures.length === 0) return null;
|
|
9
|
+
const chain = Array.isArray(call.calleeChain) ? call.calleeChain.map(normalizeChainPart) : [];
|
|
10
|
+
|
|
11
|
+
for (const cfg of customFunctionSignatures) {
|
|
12
|
+
if (!cfg || !cfg.functionName) continue;
|
|
13
|
+
const sigParts = cfg.functionName.split('.').map(normalizeChainPart).filter(Boolean);
|
|
14
|
+
if (sigParts.length === 0) continue;
|
|
15
|
+
if (endsWithChain(chain, sigParts)) return cfg;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function matchImplicitCustom(call) {
|
|
21
|
+
const chain = Array.isArray(call.calleeChain) ? call.calleeChain.map(normalizeChainPart) : [];
|
|
22
|
+
const last = chain[chain.length - 1] || '';
|
|
23
|
+
if (last === 'module' || last === 'func') {
|
|
24
|
+
return { functionName: chain.join('.'), eventIndex: 0, propertiesIndex: 1, extraParams: [] };
|
|
25
|
+
}
|
|
26
|
+
const name = call.name || '';
|
|
27
|
+
if (/^customTrackFunction\d*$/.test(name)) {
|
|
28
|
+
return { functionName: name, eventIndex: 0, propertiesIndex: 1, extraParams: [] };
|
|
29
|
+
}
|
|
30
|
+
if (name === 'customTrackNoProps') {
|
|
31
|
+
return { functionName: name, eventIndex: 0, propertiesIndex: 9999, extraParams: [] };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { matchCustomSignature, matchImplicitCustom };
|