Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/analyze/typescript/detectors/analytics-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ function isCustomFunction(node, customFunction) {
ts.isPropertyAccessExpression(node.expression) ||
ts.isCallExpression(node.expression) || // For chained calls like getTracker().track()
ts.isElementAccessExpression(node.expression) || // For array/object access like trackers['analytics'].track()
(ts.isPropertyAccessExpression(node.expression?.expression) && ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()
(node.expression?.expression &&
ts.isPropertyAccessExpression(node.expression.expression) &&
node.expression.expression.expression &&
ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()

return canBeCustomFunction && node.expression.getText() === customFunction;
}
Expand Down
117 changes: 111 additions & 6 deletions src/analyze/typescript/extractors/event-extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function extractGoogleAnalyticsEvent(node, checker, sourceFile) {
}

// gtag('event', 'event_name', { properties })
const eventName = getStringValue(node.arguments[1]);
const eventName = getStringValue(node.arguments[1], checker, sourceFile);
const propertiesNode = node.arguments[2];

return { eventName, propertiesNode };
Expand Down Expand Up @@ -79,7 +79,7 @@ function extractSnowplowEvent(node, checker, sourceFile) {
const structEventArg = firstArg.arguments[0];
if (ts.isObjectLiteralExpression(structEventArg)) {
const actionProperty = findPropertyByKey(structEventArg, 'action');
const eventName = actionProperty ? getStringValue(actionProperty.initializer) : null;
const eventName = actionProperty ? getStringValue(actionProperty.initializer, checker, sourceFile) : null;
return { eventName, propertiesNode: structEventArg };
}
}
Expand All @@ -93,7 +93,7 @@ function extractSnowplowEvent(node, checker, sourceFile) {
const structEventArg = resolvedNode.arguments[0];
if (ts.isObjectLiteralExpression(structEventArg)) {
const actionProperty = findPropertyByKey(structEventArg, 'action');
const eventName = actionProperty ? getStringValue(actionProperty.initializer) : null;
const eventName = actionProperty ? getStringValue(actionProperty.initializer, checker, sourceFile) : null;
return { eventName, propertiesNode: structEventArg };
}
}
Expand All @@ -115,7 +115,7 @@ function extractMparticleEvent(node, checker, sourceFile) {
}

// mParticle.logEvent('event_name', mParticle.EventType.Navigation, { properties })
const eventName = getStringValue(node.arguments[0]);
const eventName = getStringValue(node.arguments[0], checker, sourceFile);
const propertiesNode = node.arguments[2];

return { eventName, propertiesNode };
Expand All @@ -134,7 +134,7 @@ function extractDefaultEvent(node, checker, sourceFile) {
}

// provider.track('event_name', { properties })
const eventName = getStringValue(node.arguments[0]);
const eventName = getStringValue(node.arguments[0], checker, sourceFile);
const propertiesNode = node.arguments[1];

return { eventName, propertiesNode };
Expand Down Expand Up @@ -197,16 +197,121 @@ function processEventData(eventData, source, filePath, line, functionName, check
/**
* Gets string value from a TypeScript AST node
* @param {Object} node - TypeScript AST node
* @param {Object} checker - TypeScript type checker
* @param {Object} sourceFile - TypeScript source file
* @returns {string|null} String value or null
*/
function getStringValue(node) {
function getStringValue(node, checker, sourceFile) {
if (!node) return null;

// Handle string literals (existing behavior)
if (ts.isStringLiteral(node)) {
return node.text;
}

// Handle property access expressions like TRACKING_EVENTS.ECOMMERCE_PURCHASE
if (ts.isPropertyAccessExpression(node)) {
return resolvePropertyAccessToString(node, checker, sourceFile);
}

// Handle identifiers that might reference constants
if (ts.isIdentifier(node)) {
return resolveIdentifierToString(node, checker, sourceFile);
}

return null;
}

/**
* Resolves a property access expression to its string value
* @param {Object} node - PropertyAccessExpression node
* @param {Object} checker - TypeScript type checker
* @param {Object} sourceFile - TypeScript source file
* @returns {string|null} String value or null
*/
function resolvePropertyAccessToString(node, checker, sourceFile) {
try {
// Get the symbol for the property access
const symbol = checker.getSymbolAtLocation(node);
if (!symbol || !symbol.valueDeclaration) {
return null;
}

// Check if it's a property assignment with a string initializer
if (ts.isPropertyAssignment(symbol.valueDeclaration) &&
symbol.valueDeclaration.initializer &&
ts.isStringLiteral(symbol.valueDeclaration.initializer)) {
return symbol.valueDeclaration.initializer.text;
}

// Check if it's a variable declaration property
if (ts.isPropertySignature(symbol.valueDeclaration) ||
ts.isMethodSignature(symbol.valueDeclaration)) {
// Try to get the type and see if it's a string literal type
const type = checker.getTypeAtLocation(node);
if (type.isStringLiteral && type.isStringLiteral()) {
return type.value;
}
}

return null;
} catch (error) {
return null;
}
}

/**
* Resolves an identifier to its string value
* @param {Object} node - Identifier node
* @param {Object} checker - TypeScript type checker
* @param {Object} sourceFile - TypeScript source file
* @returns {string|null} String value or null
*/
function resolveIdentifierToString(node, checker, sourceFile) {
try {
const symbol = checker.getSymbolAtLocation(node);
if (!symbol) {
return null;
}

// First try to resolve through value declaration
if (symbol.valueDeclaration) {
const declaration = symbol.valueDeclaration;

// Handle variable declarations with string literal initializers
if (ts.isVariableDeclaration(declaration) &&
declaration.initializer &&
ts.isStringLiteral(declaration.initializer)) {
return declaration.initializer.text;
}

// Handle const declarations with object literals containing string properties
if (ts.isVariableDeclaration(declaration) &&
declaration.initializer &&
ts.isObjectLiteralExpression(declaration.initializer)) {
// This case is handled by property access resolution
return null;
}
}

// If value declaration doesn't exist or doesn't help, try type resolution
// This handles imported constants that are resolved through TypeScript's type system
const type = checker.getTypeOfSymbolAtLocation(symbol, node);
if (type && type.isStringLiteral && typeof type.isStringLiteral === 'function' && type.isStringLiteral()) {
return type.value;
}

// Alternative approach for string literal types (different TypeScript versions)
if (type && type.flags && (type.flags & ts.TypeFlags.StringLiteral)) {
return type.value;
}

return null;
} catch (error) {
return null;
}
}

/**
* Finds a property by key in an ObjectLiteralExpression
* @param {Object} objectNode - ObjectLiteralExpression node
Expand Down
54 changes: 53 additions & 1 deletion src/analyze/typescript/extractors/property-extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ function extractProperties(checker, node) {
const properties = {};

for (const prop of node.properties) {
// Handle spread assignments like {...object}
if (ts.isSpreadAssignment(prop)) {
const spreadProperties = extractSpreadProperties(checker, prop);
Object.assign(properties, spreadProperties);
continue;
}

const key = getPropertyKey(prop);
if (!key) continue;

Expand Down Expand Up @@ -350,7 +357,7 @@ function resolveTypeSchema(checker, typeString) {
* @returns {string|null} Literal type or null
*/
function getLiteralType(node) {
if (!node) return null;
if (!node || typeof node.kind === 'undefined') return null;
if (ts.isStringLiteral(node)) return 'string';
if (ts.isNumericLiteral(node)) return 'number';
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return 'boolean';
Expand All @@ -371,6 +378,51 @@ function isArrayType(typeString) {
typeString.startsWith('readonly ');
}

/**
* Extracts properties from a spread assignment
* @param {Object} checker - TypeScript type checker
* @param {Object} spreadNode - SpreadAssignment node
* @returns {Object.<string, PropertySchema>}
*/
function extractSpreadProperties(checker, spreadNode) {
if (!spreadNode.expression) {
return {};
}

// If the spread is an identifier, resolve it to its declaration
if (ts.isIdentifier(spreadNode.expression)) {
const symbol = checker.getSymbolAtLocation(spreadNode.expression);
if (symbol && symbol.declarations && symbol.declarations.length > 0) {
const declaration = symbol.declarations[0];

// If it's a variable declaration with an object literal initializer
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
if (ts.isObjectLiteralExpression(declaration.initializer)) {
// Extract properties directly from the object literal
return extractProperties(checker, declaration.initializer);
}
}
}

// Fallback to the original identifier schema extraction
const identifierSchema = extractIdentifierSchema(checker, spreadNode.expression);
return identifierSchema.properties || {};
}

// If the spread is an object literal, extract its properties
if (ts.isObjectLiteralExpression(spreadNode.expression)) {
return extractProperties(checker, spreadNode.expression);
}

// For other expressions, try to get the type and extract properties from it
try {
const spreadType = checker.getTypeAtLocation(spreadNode.expression);
return extractInterfaceProperties(checker, spreadType);
} catch (error) {
return {};
}
}

/**
* Extracts properties from a TypeScript interface or type
* @param {Object} checker - TypeScript type checker
Expand Down
11 changes: 11 additions & 0 deletions src/analyze/typescript/utils/function-finder.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,24 @@ function findParentFunctionName(node) {
}
}

// Property declaration in class: myFunc = () => {}
if (ts.isPropertyDeclaration(parent) && parent.name) {
if (ts.isIdentifier(parent.name)) {
return parent.name.escapedText;
}
if (ts.isStringLiteral(parent.name)) {
return parent.name.text;
}
}

// Method property in object literal: { myFunc() {} }
if (ts.isMethodDeclaration(parent) && parent.name) {
return parent.name.escapedText;
}

// Binary expression assignment: obj.myFunc = () => {}
if (ts.isBinaryExpression(parent) &&
parent.operatorToken &&
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
if (ts.isPropertyAccessExpression(parent.left)) {
return parent.left.name.escapedText;
Expand Down
2 changes: 1 addition & 1 deletion src/analyze/typescript/utils/type-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ function isCustomType(typeString) {
* @returns {string} Basic type string
*/
function getBasicTypeOfArrayElement(checker, element) {
if (!element) return 'any';
if (!element || typeof element.kind === 'undefined') return 'any';

// Check for literal values first
if (ts.isStringLiteral(element)) {
Expand Down
Loading