@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.
@@ -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 };