Skip to content

Commit fd00124

Browse files
committed
genericize swift analyzer
1 parent 4b4817f commit fd00124

File tree

1 file changed

+71
-53
lines changed

1 file changed

+71
-53
lines changed

src/analyze/swift/index.js

Lines changed: 71 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -402,14 +402,12 @@ function extractCustomEvent(call, cfg, analysis, source, filePath, constMap) {
402402
// Implicit custom fallback for common patterns (e.g., customTrackFunction7, customTrackNoProps)
403403
function matchImplicitCustom(call) {
404404
const name = call.name || '';
405-
// My.Module.Here.func(EVENTS.userSignedUp)
405+
// Generic implicit: detect patterns with last method name and positional args
406406
const chain = Array.isArray(call.calleeChain) ? call.calleeChain.map(normalizeChainPart) : [];
407-
if (chain.join('.') === 'My.Module.Here.func') {
408-
return { functionName: 'My.Module.Here.func', eventIndex: 0, propertiesIndex: 9999, extraParams: [] };
409-
}
410-
// Other().module(EVENT_NAME, PROPERTIES, customFieldOne, customFieldTwo)
411-
if (chain.join('.') === 'Other.module') {
412-
return { functionName: 'Other().module', eventIndex: 0, propertiesIndex: 1, extraParams: [] };
407+
const last = chain[chain.length - 1] || '';
408+
// Heuristic: methods named 'module' or 'func' that take (EVENT_NAME[, PROPERTIES, ...])
409+
if (last === 'module' || last === 'func') {
410+
return { functionName: chain.join('.'), eventIndex: 0, propertiesIndex: 1, extraParams: [] };
413411
}
414412
if (/^customTrackFunction\d*$/.test(name)) {
415413
return { functionName: name, eventIndex: 0, propertiesIndex: 1, extraParams: [] };
@@ -508,13 +506,7 @@ function convertDictToSchema(dict, constMap) {
508506
for (const [rawKey, value] of Object.entries(dict)) {
509507
const key = resolveKey(rawKey, constMap);
510508
// Attempt to refine arrays of dicts and well-known shapes from builders in fixtures
511-
if (key === 'products' && value && typeof value === 'object') {
512-
props[key] = { type: 'any' };
513-
} else if (key === 'address' && value && typeof value === 'object') {
514-
props[key] = { type: 'object', properties: { city: { type: 'string' }, state: { type: 'string' } } };
515-
} else {
516-
props[key] = inferSchemaFromValue(value);
517-
}
509+
props[key] = inferSchemaFromValue(value);
518510
// If value comes from known constants, refine to string
519511
if (!props[key] || props[key].type === 'any') {
520512
if (typeof value === 'string') props[key] = { type: 'string' };
@@ -539,18 +531,14 @@ function inferSchemaFromValue(value) {
539531
}
540532

541533
function resolveKey(key, constMap) {
542-
// Keys may be literal or constants like KEYS.orderId
534+
// Return mapped constant if available
543535
if (constMap[key]) return constMap[key];
544-
// Map known KEYS.* to expected output keys in fixtures
545-
if (/^KEYS\./.test(key)) {
546-
const k = key.split('.')[1] || '';
547-
if (k === 'orderId') return 'order_id';
548-
if (k === 'products') return 'products';
549-
if (k === 'total') return 'total';
550-
if (k === 'address') return 'address';
551-
if (k === 'userId') return 'user_id';
552-
if (k === 'email') return 'email';
553-
if (k === 'name') return 'name';
536+
// Generic mapping for CamelCase to snake_case when key is like KEYS.orderId
537+
const nsMatch = /^([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(key);
538+
if (nsMatch) {
539+
const raw = nsMatch[2];
540+
const snake = raw.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
541+
return snake;
554542
}
555543
return key;
556544
}
@@ -717,14 +705,19 @@ function parseDictTextToSchema(text, constMap) {
717705
let valText = m[2].trim().replace(/,\s*$/, '');
718706
rawKey = rawKey.replace(/^"|"$/g, '');
719707
const key = resolveKey(rawKey, constMap);
720-
// Special-cases for known shapes
721-
if (key === 'products') {
722-
out[key] = { type: 'any' };
723-
continue;
724-
}
725-
if (key === 'address') {
726-
out[key] = { type: 'object', properties: { city: { type: 'string' }, state: { type: 'string' } } };
727-
continue;
708+
// Function return resolution: e.g., makeAddress(), makeProducts()
709+
const fnCall = /^([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)$/.exec(valText);
710+
if (fnCall && constMap.__dictFuncs && constMap.__dictFuncs[fnCall[1]]) {
711+
const returned = constMap.__dictFuncs[fnCall[1]];
712+
if (returned.kind === 'dict' && returned.text) {
713+
const nested = parseDictTextToSchema(returned.text, constMap);
714+
out[key] = { type: 'object', properties: nested };
715+
continue;
716+
}
717+
if (returned.kind === 'array') {
718+
out[key] = { type: 'any' };
719+
continue;
720+
}
728721
}
729722
// Constants map resolution for identifiers
730723
if (isIdentifier(valText) && constMap[valText]) {
@@ -752,8 +745,12 @@ function findEventNameInDictText(text, constMap) {
752745
const str = extractStringLiteral(val);
753746
if (str) return str;
754747
if (constMap[val]) return constMap[val];
755-
const m = /(EVENTS\.[A-Za-z0-9_]+)/.exec(val);
756-
if (m && constMap[m[1]]) return constMap[m[1]];
748+
// Support any namespaced constant like NAMESPACE.value
749+
const m = /([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)/.exec(val);
750+
if (m) {
751+
const token = `${m[1]}.${m[2]}`;
752+
if (constMap[token]) return constMap[token];
753+
}
757754
}
758755
return null;
759756
}
@@ -813,16 +810,14 @@ function extractArgsFromCall(text) {
813810

814811
function findEventConstantInText(text, constMap) {
815812
if (!text) return null;
816-
const re = /(EVENTS\.[A-Za-z0-9_]+|[A-Z_][A-Z0-9_]*\b)/g;
813+
const re = /([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)|\b([A-Z_][A-Z0-9_]*)\b/g;
817814
let match;
818815
let fallback = null;
819816
while ((match = re.exec(text)) !== null) {
820-
const token = match[1];
821-
if (token.startsWith('EVENTS.')) {
822-
if (constMap[token]) return constMap[token];
823-
} else {
824-
if (!fallback && constMap[token]) fallback = constMap[token];
825-
}
817+
const token = match[3] || `${match[1]}.${match[2]}`;
818+
if (!token) continue;
819+
if (token.includes('.') && constMap[token]) return constMap[token];
820+
if (!token.includes('.') && constMap[token] && !fallback) fallback = constMap[token];
826821
}
827822
return fallback;
828823
}
@@ -842,17 +837,40 @@ function buildCrossFileConstMap(dir) {
842837
for (const m of content.matchAll(/\blet\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
843838
map[m[1]] = m[2];
844839
}
845-
// Enum static lets: enum X { static let key = "value" }
846-
for (const m of content.matchAll(/\bstatic\s+let\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
847-
// Prefix resolution requires the enum name; as a pragmatic approach,
848-
// also expose KEYS.key and EVENTS.key by scanning file for enum names
849-
// but here we store both plain and namespaced guesses where possible.
850-
const key = m[1];
851-
const val = m[2];
852-
map[key] = val;
853-
// Common namespaces in fixtures
854-
map[`KEYS.${key}`] = val;
855-
map[`EVENTS.${key}`] = val;
840+
// Enum/struct blocks: capture namespace and all static lets inside
841+
let idx = 0;
842+
while (idx < content.length) {
843+
const head = content.slice(idx);
844+
const mm = /\b(enum|struct)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/m.exec(head);
845+
if (!mm) break;
846+
const ns = mm[2];
847+
const blockStart = idx + mm.index + mm[0].length - 1; // position at '{'
848+
// Find matching closing brace
849+
let depth = 0; let end = -1;
850+
for (let i = blockStart; i < content.length; i++) {
851+
const ch = content[i];
852+
if (ch === '{') depth++;
853+
else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
854+
}
855+
if (end === -1) break;
856+
const block = content.slice(blockStart + 1, end);
857+
for (const sm of block.matchAll(/\bstatic\s+let\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
858+
const key = sm[1];
859+
const val = sm[2];
860+
map[`${ns}.${key}`] = val;
861+
}
862+
idx = end + 1;
863+
}
864+
// Capture very simple helper returns
865+
// func makeAddress() -> [String: Any] { return [ ... ] }
866+
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)) {
867+
map.__dictFuncs = map.__dictFuncs || {};
868+
map.__dictFuncs[m[1]] = { kind: 'dict', text: m[2] };
869+
}
870+
// func makeProducts() -> [[String: Any]] { return [ ... ] } (array)
871+
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)) {
872+
map.__dictFuncs = map.__dictFuncs || {};
873+
map.__dictFuncs[m[1]] = { kind: 'array', text: m[2] };
856874
}
857875
}
858876
} catch (_) {}

0 commit comments

Comments
 (0)