Skip to content

Commit cc177e4

Browse files
cursoragentskarim
andcommitted
Add Swift analytics tracking support with comprehensive parsing
Co-authored-by: sameenkarim <[email protected]>
1 parent c2cbbc0 commit cc177e4

File tree

14 files changed

+2963
-0
lines changed

14 files changed

+2963
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"test:python": "node --experimental-vm-modules --test tests/analyzePython.test.js",
1414
"test:ruby": "node --experimental-vm-modules --test tests/analyzeRuby.test.js",
1515
"test:go": "node --test tests/analyzeGo.test.js",
16+
"test:swift": "node --experimental-vm-modules --test tests/analyzeSwift.test.js",
1617
"test:cli": "node --test tests/cli.test.js",
1718
"test:schema": "node --test tests/schema.test.js",
1819
"test:generateDescriptions": "node --test tests/generateDescriptions.test.js",

src/analyze/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { analyzeTsFiles } = require('./typescript');
1313
const { analyzePythonFile } = require('./python');
1414
const { analyzeRubyFile, prebuildConstantMaps } = require('./ruby');
1515
const { analyzeGoFile } = require('./go');
16+
const { analyzeSwiftFile } = require('./swift');
1617

1718
/**
1819
* Analyzes a single file for analytics tracking calls
@@ -28,6 +29,7 @@ async function analyzeFile(file, customFunctionSignatures) {
2829
if (/\.py$/.test(file)) return analyzePythonFile(file, customFunctionSignatures)
2930
if (/\.rb$/.test(file)) return analyzeRubyFile(file, customFunctionSignatures)
3031
if (/\.go$/.test(file)) return analyzeGoFile(file, customFunctionSignatures)
32+
if (/\.swift$/.test(file)) return analyzeSwiftFile(file, customFunctionSignatures)
3133
return []
3234
}
3335

src/analyze/swift/constants.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* @fileoverview Constants and configurations for Swift analytics tracking providers
3+
* @module analyze/swift/constants
4+
*/
5+
6+
/**
7+
* Analytics provider configurations for Swift
8+
* @typedef {Object} SwiftProviderConfig
9+
* @property {string} name - Provider display name
10+
* @property {string} objectName - Object name in Swift
11+
* @property {string} methodName - Method name for tracking
12+
* @property {string} type - Type of detection (method|function|struct)
13+
* @property {Array<string>} [objectNames] - Alternative object names
14+
*/
15+
16+
/**
17+
* Supported analytics providers and their detection patterns for Swift
18+
* @type {Object.<string, SwiftProviderConfig>}
19+
*/
20+
const ANALYTICS_PROVIDERS = {
21+
SEGMENT: {
22+
name: 'segment',
23+
objectName: 'Analytics',
24+
methodName: 'track',
25+
type: 'method',
26+
chainMethods: ['shared()']
27+
},
28+
MIXPANEL: {
29+
name: 'mixpanel',
30+
objectName: 'Mixpanel',
31+
methodName: 'track',
32+
type: 'method',
33+
chainMethods: ['mainInstance()']
34+
},
35+
AMPLITUDE: {
36+
name: 'amplitude',
37+
objectName: 'amplitude',
38+
methodName: 'track',
39+
type: 'method'
40+
},
41+
RUDDERSTACK: {
42+
name: 'rudderstack',
43+
objectName: 'RSClient',
44+
methodName: 'track',
45+
type: 'method',
46+
chainMethods: ['sharedInstance()']
47+
},
48+
MPARTICLE: {
49+
name: 'mparticle',
50+
objectName: 'MParticle',
51+
methodName: 'logEvent',
52+
type: 'method',
53+
chainMethods: ['sharedInstance()']
54+
},
55+
POSTHOG: {
56+
name: 'posthog',
57+
objectName: 'PostHogSDK',
58+
methodName: 'capture',
59+
type: 'method',
60+
property: 'shared'
61+
},
62+
PENDO: {
63+
name: 'pendo',
64+
objectName: 'PendoManager',
65+
methodName: 'track',
66+
type: 'method',
67+
chainMethods: ['shared()']
68+
},
69+
HEAP: {
70+
name: 'heap',
71+
objectName: 'Heap',
72+
methodName: 'track',
73+
type: 'method',
74+
property: 'shared'
75+
},
76+
SNOWPLOW: {
77+
name: 'snowplow',
78+
objectName: 'tracker',
79+
methodName: 'track',
80+
type: 'method',
81+
structName: 'Structured'
82+
},
83+
FIREBASE: {
84+
name: 'firebase',
85+
objectName: 'Analytics',
86+
methodName: 'logEvent',
87+
type: 'method'
88+
}
89+
};
90+
91+
/**
92+
* Swift type mappings to schema types
93+
* @type {Object.<string, string>}
94+
*/
95+
const SWIFT_TYPE_MAPPINGS = {
96+
'String': 'string',
97+
'Int': 'number',
98+
'Double': 'number',
99+
'Float': 'number',
100+
'Bool': 'boolean',
101+
'NSNumber': 'number',
102+
'NSString': 'string',
103+
'Array': 'array',
104+
'Dictionary': 'object',
105+
'Any': 'any'
106+
};
107+
108+
/**
109+
* Swift literal value type detection patterns
110+
* @type {Object.<string, string>}
111+
*/
112+
const LITERAL_TYPE_PATTERNS = {
113+
'string': /^".*"$/,
114+
'number': /^-?\d+(\.\d+)?$/,
115+
'boolean': /^(true|false)$/,
116+
'null': /^nil$/
117+
};
118+
119+
/**
120+
* Maximum recursion depth for Swift AST traversal
121+
* @type {number}
122+
*/
123+
const MAX_RECURSION_DEPTH = 50;
124+
125+
module.exports = {
126+
ANALYTICS_PROVIDERS,
127+
SWIFT_TYPE_MAPPINGS,
128+
LITERAL_TYPE_PATTERNS,
129+
MAX_RECURSION_DEPTH
130+
};
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/**
2+
* @fileoverview Analytics source detection for Swift tracking calls
3+
* @module analyze/swift/detectors/analytics-source
4+
*/
5+
6+
const { ANALYTICS_PROVIDERS } = require('../constants');
7+
8+
/**
9+
* Detects the analytics source from a Swift function call expression
10+
* @param {Object} callExpression - Swift AST call expression node
11+
* @param {string|null} customFunction - Optional custom function name to detect
12+
* @returns {string|null} - The detected source or null
13+
*/
14+
function detectAnalyticsSource(callExpression, customFunction = null) {
15+
if (!callExpression) return null;
16+
17+
// Handle custom function detection first
18+
if (customFunction && isCustomFunction(callExpression, customFunction)) {
19+
return 'custom';
20+
}
21+
22+
// Handle built-in analytics providers
23+
return detectBuiltinProvider(callExpression);
24+
}
25+
26+
/**
27+
* Detects if a call expression matches a custom function
28+
* @param {Object} callExpression - Swift AST call expression node
29+
* @param {string} customFunction - Custom function name or pattern
30+
* @returns {boolean}
31+
*/
32+
function isCustomFunction(callExpression, customFunction) {
33+
if (!callExpression.calledExpression) return false;
34+
35+
// Extract function name from call expression
36+
const functionName = extractFunctionName(callExpression.calledExpression);
37+
38+
if (!functionName) return false;
39+
40+
// Handle simple function names
41+
if (typeof customFunction === 'string') {
42+
return functionName === customFunction;
43+
}
44+
45+
// Handle custom function object with functionName property
46+
if (customFunction.functionName) {
47+
return functionName === customFunction.functionName;
48+
}
49+
50+
return false;
51+
}
52+
53+
/**
54+
* Detects built-in analytics providers from a call expression
55+
* @param {Object} callExpression - Swift AST call expression node
56+
* @returns {string|null}
57+
*/
58+
function detectBuiltinProvider(callExpression) {
59+
if (!callExpression.calledExpression) return null;
60+
61+
const expr = callExpression.calledExpression;
62+
63+
// Handle method calls (object.method())
64+
if (expr.kind === 'MemberAccessExpr') {
65+
return detectMethodCall(expr);
66+
}
67+
68+
// Handle direct function calls (functionName())
69+
if (expr.kind === 'UnresolvedDeclRefExpr' || expr.kind === 'DeclRefExpr') {
70+
return detectDirectFunctionCall(expr);
71+
}
72+
73+
// Handle chained calls (Object.shared().method())
74+
if (expr.kind === 'DotSyntaxCallExpr') {
75+
return detectChainedCall(expr);
76+
}
77+
78+
return null;
79+
}
80+
81+
/**
82+
* Detects analytics provider from method call expressions
83+
* @param {Object} memberExpr - Swift AST member access expression
84+
* @returns {string|null}
85+
*/
86+
function detectMethodCall(memberExpr) {
87+
if (!memberExpr.member || !memberExpr.base) return null;
88+
89+
const methodName = extractIdentifierName(memberExpr.member);
90+
const baseName = extractBaseName(memberExpr.base);
91+
92+
// Check each provider
93+
for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
94+
if (provider.methodName === methodName) {
95+
// Check if base matches the provider's object name
96+
if (baseName === provider.objectName) {
97+
return provider.name;
98+
}
99+
100+
// Check for property access (e.g., PostHogSDK.shared.capture)
101+
if (provider.property && memberExpr.base.kind === 'MemberAccessExpr') {
102+
const baseBase = extractBaseName(memberExpr.base.base);
103+
const baseProperty = extractIdentifierName(memberExpr.base.member);
104+
105+
if (baseBase === provider.objectName && baseProperty === provider.property) {
106+
return provider.name;
107+
}
108+
}
109+
}
110+
}
111+
112+
return null;
113+
}
114+
115+
/**
116+
* Detects analytics provider from direct function calls
117+
* @param {Object} declRefExpr - Swift AST declaration reference expression
118+
* @returns {string|null}
119+
*/
120+
function detectDirectFunctionCall(declRefExpr) {
121+
const functionName = extractIdentifierName(declRefExpr);
122+
123+
// Check for Firebase Analytics.logEvent pattern
124+
if (functionName === 'logEvent') {
125+
return ANALYTICS_PROVIDERS.FIREBASE.name;
126+
}
127+
128+
return null;
129+
}
130+
131+
/**
132+
* Detects analytics provider from chained method calls
133+
* @param {Object} chainExpr - Swift AST chained call expression
134+
* @returns {string|null}
135+
*/
136+
function detectChainedCall(chainExpr) {
137+
// Handle patterns like Analytics.shared().track()
138+
if (!chainExpr.fn || !chainExpr.argument) return null;
139+
140+
const methodName = extractFunctionName(chainExpr.fn);
141+
const chainBase = extractChainBase(chainExpr.argument);
142+
143+
for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
144+
if (provider.methodName === methodName && provider.chainMethods) {
145+
// Check if the chain matches the provider pattern
146+
if (chainBase === provider.objectName) {
147+
return provider.name;
148+
}
149+
}
150+
}
151+
152+
return null;
153+
}
154+
155+
/**
156+
* Extracts function name from various expression types
157+
* @param {Object} expr - Swift AST expression
158+
* @returns {string|null}
159+
*/
160+
function extractFunctionName(expr) {
161+
if (!expr) return null;
162+
163+
if (expr.kind === 'UnresolvedDeclRefExpr' || expr.kind === 'DeclRefExpr') {
164+
return extractIdentifierName(expr);
165+
}
166+
167+
if (expr.kind === 'MemberAccessExpr') {
168+
return extractIdentifierName(expr.member);
169+
}
170+
171+
return null;
172+
}
173+
174+
/**
175+
* Extracts identifier name from declaration reference
176+
* @param {Object} declRef - Swift AST declaration reference
177+
* @returns {string|null}
178+
*/
179+
function extractIdentifierName(declRef) {
180+
if (!declRef) return null;
181+
182+
// Handle different identifier structures
183+
if (declRef.identifier && declRef.identifier.name) {
184+
return declRef.identifier.name;
185+
}
186+
187+
if (declRef.name) {
188+
return declRef.name;
189+
}
190+
191+
if (typeof declRef === 'string') {
192+
return declRef;
193+
}
194+
195+
return null;
196+
}
197+
198+
/**
199+
* Extracts base name from expressions
200+
* @param {Object} base - Swift AST base expression
201+
* @returns {string|null}
202+
*/
203+
function extractBaseName(base) {
204+
if (!base) return null;
205+
206+
if (base.kind === 'UnresolvedDeclRefExpr' || base.kind === 'DeclRefExpr') {
207+
return extractIdentifierName(base);
208+
}
209+
210+
if (base.kind === 'MemberAccessExpr') {
211+
return extractBaseName(base.base);
212+
}
213+
214+
return null;
215+
}
216+
217+
/**
218+
* Extracts the base object from a chained call
219+
* @param {Object} chainArg - Swift AST chain argument
220+
* @returns {string|null}
221+
*/
222+
function extractChainBase(chainArg) {
223+
if (!chainArg) return null;
224+
225+
if (chainArg.kind === 'UnresolvedDeclRefExpr' || chainArg.kind === 'DeclRefExpr') {
226+
return extractIdentifierName(chainArg);
227+
}
228+
229+
if (chainArg.kind === 'MemberAccessExpr') {
230+
return extractBaseName(chainArg.base);
231+
}
232+
233+
return null;
234+
}
235+
236+
module.exports = {
237+
detectAnalyticsSource
238+
};

0 commit comments

Comments
 (0)