Skip to content

Commit e78db3d

Browse files
committed
detect class method arrow function names
1 parent 1d71540 commit e78db3d

File tree

5 files changed

+460
-139
lines changed

5 files changed

+460
-139
lines changed

src/analyze/typescript/utils/function-finder.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ function findParentFunctionName(node) {
110110
}
111111
}
112112

113+
// Property declaration in class: myFunc = () => {}
114+
if (ts.isPropertyDeclaration(parent) && parent.name) {
115+
if (ts.isIdentifier(parent.name)) {
116+
return parent.name.escapedText;
117+
}
118+
if (ts.isStringLiteral(parent.name)) {
119+
return parent.name.text;
120+
}
121+
}
122+
113123
// Method property in object literal: { myFunc() {} }
114124
if (ts.isMethodDeclaration(parent) && parent.name) {
115125
return parent.name.escapedText;

tests/analyzeTypeScript.test.js

Lines changed: 222 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -518,25 +518,37 @@ test.describe('analyzeTsFile', () => {
518518
// Sort events by line number for consistent ordering
519519
events.sort((a, b) => a.line - b.line);
520520

521-
assert.strictEqual(events.length, 7);
521+
// Updated count - 8 events for regular analysis (complex_operation is only detected with custom function)
522+
assert.strictEqual(events.length, 8);
523+
524+
// Test new Segment event from ComplexUploadComponent (regression test pattern)
525+
const complexSegmentEvent = events.find(e => e.source === 'segment' && e.eventName === 'document_upload_clicked');
526+
assert.ok(complexSegmentEvent);
527+
assert.strictEqual(complexSegmentEvent.eventName, 'document_upload_clicked');
528+
assert.strictEqual(complexSegmentEvent.functionName, 'onFileUploadClick'); // Arrow function methods now show proper names
529+
assert.strictEqual(complexSegmentEvent.line, 53);
530+
assert.deepStrictEqual(complexSegmentEvent.properties, {
531+
documentId: { type: 'any' },
532+
documentType: { type: 'string' }
533+
});
522534

523535
// Test PostHog event
524536
const posthogEvent = events.find(e => e.source === 'posthog');
525537
assert.ok(posthogEvent);
526538
assert.strictEqual(posthogEvent.eventName, 'cart_viewed');
527539
assert.strictEqual(posthogEvent.functionName, 'useEffect()');
528-
assert.strictEqual(posthogEvent.line, 15);
540+
assert.strictEqual(posthogEvent.line, 89);
529541
assert.deepStrictEqual(posthogEvent.properties, {
530542
item_count: { type: 'number' },
531543
total_value: { type: 'number' }
532544
});
533545

534-
// Test Segment event
535-
const segmentEvent = events.find(e => e.source === 'segment');
546+
// Test Segment event from useCallback
547+
const segmentEvent = events.find(e => e.source === 'segment' && e.eventName === 'add_to_cart');
536548
assert.ok(segmentEvent);
537549
assert.strictEqual(segmentEvent.eventName, 'add_to_cart');
538550
assert.strictEqual(segmentEvent.functionName, 'useCallback(handleAddToCart)');
539-
assert.strictEqual(segmentEvent.line, 27);
551+
assert.strictEqual(segmentEvent.line, 101);
540552
assert.deepStrictEqual(segmentEvent.properties, {
541553
product_id: { type: 'string' },
542554
product_name: { type: 'string' },
@@ -548,7 +560,7 @@ test.describe('analyzeTsFile', () => {
548560
assert.ok(amplitudeEvent);
549561
assert.strictEqual(amplitudeEvent.eventName, 'item_added');
550562
assert.strictEqual(amplitudeEvent.functionName, 'useCallback(handleAddToCart)');
551-
assert.strictEqual(amplitudeEvent.line, 34);
563+
assert.strictEqual(amplitudeEvent.line, 108);
552564
assert.deepStrictEqual(amplitudeEvent.properties, {
553565
item_details: {
554566
type: 'object',
@@ -567,7 +579,7 @@ test.describe('analyzeTsFile', () => {
567579
assert.ok(mixpanelEvent);
568580
assert.strictEqual(mixpanelEvent.eventName, 'remove_from_cart');
569581
assert.strictEqual(mixpanelEvent.functionName, 'removeFromCart');
570-
assert.strictEqual(mixpanelEvent.line, 45);
582+
assert.strictEqual(mixpanelEvent.line, 119);
571583
assert.deepStrictEqual(mixpanelEvent.properties, {
572584
product_id: { type: 'string' },
573585
timestamp: { type: 'string' }
@@ -578,7 +590,7 @@ test.describe('analyzeTsFile', () => {
578590
assert.ok(gaEvent);
579591
assert.strictEqual(gaEvent.eventName, 'begin_checkout');
580592
assert.strictEqual(gaEvent.functionName, 'handleCheckout');
581-
assert.strictEqual(gaEvent.line, 56);
593+
assert.strictEqual(gaEvent.line, 130);
582594
assert.deepStrictEqual(gaEvent.properties, {
583595
items: {
584596
type: 'array',
@@ -596,7 +608,7 @@ test.describe('analyzeTsFile', () => {
596608
assert.ok(rudderstackEvent);
597609
assert.strictEqual(rudderstackEvent.eventName, 'checkout_started');
598610
assert.strictEqual(rudderstackEvent.functionName, 'handleCheckout');
599-
assert.strictEqual(rudderstackEvent.line, 63);
611+
assert.strictEqual(rudderstackEvent.line, 137);
600612
assert.deepStrictEqual(rudderstackEvent.properties, {
601613
products: {
602614
type: 'array',
@@ -613,7 +625,7 @@ test.describe('analyzeTsFile', () => {
613625
assert.ok(mparticleEvent);
614626
assert.strictEqual(mparticleEvent.eventName, 'InitiateCheckout');
615627
assert.strictEqual(mparticleEvent.functionName, 'handleCheckout');
616-
assert.strictEqual(mparticleEvent.line, 69);
628+
assert.strictEqual(mparticleEvent.line, 143);
617629
assert.deepStrictEqual(mparticleEvent.properties, {
618630
cart_items: {
619631
type: 'array',
@@ -624,21 +636,215 @@ test.describe('analyzeTsFile', () => {
624636
},
625637
checkout_step: { type: 'number' }
626638
});
639+
640+
// Note: cart_update event is only detected with custom function detection
641+
// Note: complex_operation event is only detected with custom function detection
627642
});
628643

629644
test('should correctly analyze React TypeScript file with custom function', () => {
630645
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
631646
const program = createProgram(reactFilePath);
632647
const events = analyzeTsFile(reactFilePath, program, 'tracker.track');
633648

634-
console.log({ events });
635-
const trackEvent = events.find(e => e.source === 'custom');
649+
// Should find both tracker.track events (cart_update and complex_operation)
650+
const trackEvents = events.filter(e => e.source === 'custom');
651+
assert.strictEqual(trackEvents.length, 2);
636652

637-
assert.strictEqual(trackEvent.eventName, 'cart_update');
638-
assert.strictEqual(trackEvent.functionName, 'trackCartUpdate');
639-
assert.strictEqual(trackEvent.line, 81);
640-
assert.deepStrictEqual(trackEvent.properties, {
653+
const cartUpdateEvent = trackEvents.find(e => e.eventName === 'cart_update');
654+
assert.ok(cartUpdateEvent);
655+
assert.strictEqual(cartUpdateEvent.eventName, 'cart_update');
656+
assert.strictEqual(cartUpdateEvent.functionName, 'trackCartUpdate');
657+
assert.strictEqual(cartUpdateEvent.line, 155);
658+
assert.deepStrictEqual(cartUpdateEvent.properties, {
641659
cart_size: { type: 'number' }
642660
});
661+
662+
const complexOpEvent = trackEvents.find(e => e.eventName === 'complex_operation');
663+
assert.ok(complexOpEvent);
664+
assert.strictEqual(complexOpEvent.eventName, 'complex_operation');
665+
assert.strictEqual(complexOpEvent.functionName, 'handleComplexOperation');
666+
assert.strictEqual(complexOpEvent.line, 62);
667+
assert.deepStrictEqual(complexOpEvent.properties, {
668+
hasRef: { type: 'boolean' },
669+
timestamp: { type: 'number' }
670+
});
671+
});
672+
673+
// Regression tests for "Cannot read properties of undefined (reading 'kind')" fix
674+
test('should handle complex React class component patterns without crashing (regression test)', () => {
675+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
676+
const program = createProgram(reactFilePath);
677+
678+
// This should not throw any errors - the main test is that it doesn't crash
679+
assert.doesNotThrow(() => {
680+
const events = analyzeTsFile(reactFilePath, program);
681+
// Should complete analysis without throwing undefined .kind errors
682+
assert.ok(Array.isArray(events));
683+
assert.ok(events.length > 0);
684+
});
685+
});
686+
687+
test('should handle complex class component with custom function detection without crashing', () => {
688+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
689+
const program = createProgram(reactFilePath);
690+
691+
// This was the specific case that was causing "Cannot read properties of undefined (reading 'kind')"
692+
assert.doesNotThrow(() => {
693+
const events = analyzeTsFile(reactFilePath, program, 'track');
694+
assert.ok(Array.isArray(events));
695+
696+
// Should find the analytics.track call when looking for 'track' custom function
697+
const analyticsEvent = events.find(e => e.eventName === 'document_upload_clicked');
698+
assert.ok(analyticsEvent);
699+
assert.strictEqual(analyticsEvent.source, 'segment');
700+
assert.strictEqual(analyticsEvent.line, 53);
701+
assert.deepStrictEqual(analyticsEvent.properties, {
702+
documentId: { type: 'any' },
703+
documentType: { type: 'string' }
704+
});
705+
});
706+
});
707+
708+
test('should handle various custom function detection patterns without undefined errors', () => {
709+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
710+
const program = createProgram(reactFilePath);
711+
712+
// Test various custom function patterns that could trigger the bug
713+
const customFunctionTests = [
714+
'track',
715+
'analytics.track',
716+
'tracker.track',
717+
'this.track',
718+
'mixpanel.track',
719+
'nonexistent.function'
720+
];
721+
722+
customFunctionTests.forEach(customFunction => {
723+
assert.doesNotThrow(() => {
724+
const events = analyzeTsFile(reactFilePath, program, customFunction);
725+
assert.ok(Array.isArray(events));
726+
}, `Should not throw error with custom function: ${customFunction}`);
727+
});
728+
});
729+
730+
test('should handle nested property access expressions in custom function detection', () => {
731+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
732+
const program = createProgram(reactFilePath);
733+
734+
// Test deeply nested property access that could cause undefined node traversal
735+
const complexCustomFunctions = [
736+
'this.props.analytics.track',
737+
'window.analytics.track',
738+
'deep.nested.property.track',
739+
'undefined.property.access'
740+
];
741+
742+
complexCustomFunctions.forEach(customFunction => {
743+
assert.doesNotThrow(() => {
744+
const events = analyzeTsFile(reactFilePath, program, customFunction);
745+
assert.ok(Array.isArray(events));
746+
}, `Should not crash with complex custom function: ${customFunction}`);
747+
});
748+
});
749+
750+
test('should correctly identify React class method contexts without undefined errors', () => {
751+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
752+
const program = createProgram(reactFilePath);
753+
754+
const events = analyzeTsFile(reactFilePath, program);
755+
756+
// Should find the analytics.track call in the arrow function method
757+
const analyticsEvent = events.find(e => e.eventName === 'document_upload_clicked');
758+
assert.ok(analyticsEvent);
759+
assert.strictEqual(analyticsEvent.functionName, 'onFileUploadClick'); // Arrow function methods now show proper names
760+
assert.strictEqual(analyticsEvent.source, 'segment');
761+
});
762+
763+
test('should handle TypeScript React component with complex type intersections', () => {
764+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
765+
const program = createProgram(reactFilePath);
766+
767+
// The file has complex type intersections: MappedProps & ExplicitProps & ActionProps
768+
// This should not cause AST traversal issues
769+
assert.doesNotThrow(() => {
770+
const events = analyzeTsFile(reactFilePath, program, 'uploadError');
771+
assert.ok(Array.isArray(events));
772+
});
773+
});
774+
775+
test('should handle React refs and generic type parameters without errors', () => {
776+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
777+
const program = createProgram(reactFilePath);
778+
779+
// The file uses React.createRef<any>() which creates complex AST nodes
780+
assert.doesNotThrow(() => {
781+
const events = analyzeTsFile(reactFilePath, program, 'open');
782+
assert.ok(Array.isArray(events));
783+
});
784+
});
785+
786+
test('should handle both React functional and class components correctly', () => {
787+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
788+
const program = createProgram(reactFilePath);
789+
790+
// Should work without errors for file containing both patterns
791+
assert.doesNotThrow(() => {
792+
const events = analyzeTsFile(reactFilePath, program, 'track');
793+
794+
assert.ok(Array.isArray(events));
795+
796+
// Should have events from both functional and class components
797+
assert.ok(events.length > 0);
798+
799+
// Should have functional component events (from hooks)
800+
const functionalEvents = events.filter(e => e.functionName.includes('useCallback') || e.functionName.includes('useEffect'));
801+
assert.ok(functionalEvents.length > 0);
802+
803+
// Should have class component events (they now show proper method names)
804+
const classEvents = events.filter(e => e.functionName === 'onFileUploadClick' || e.functionName === 'handleComplexOperation');
805+
assert.ok(classEvents.length > 0);
806+
});
807+
});
808+
809+
test('should handle edge cases in isCustomFunction without undefined property access', () => {
810+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
811+
const program = createProgram(reactFilePath);
812+
813+
// These edge cases were specifically causing the "reading 'kind'" error
814+
const edgeCaseCustomFunctions = [
815+
'track', // matches .track in analytics.track
816+
'current', // matches .current in dropzoneRef.current
817+
'props', // matches this.props
818+
'state' // common React property
819+
];
820+
821+
edgeCaseCustomFunctions.forEach(customFunction => {
822+
assert.doesNotThrow(() => {
823+
const events = analyzeTsFile(reactFilePath, program, customFunction);
824+
assert.ok(Array.isArray(events));
825+
}, `Should handle edge case custom function: ${customFunction}`);
826+
});
827+
});
828+
829+
test('should preserve correct event extraction while fixing undefined errors', () => {
830+
const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx');
831+
const program = createProgram(reactFilePath);
832+
833+
// Verify that our fix doesn't break the actual tracking detection
834+
const events = analyzeTsFile(reactFilePath, program);
835+
836+
// Should correctly identify multiple tracking events including the complex class component
837+
assert.ok(events.length >= 8);
838+
839+
// Should still correctly identify the analytics.track call from complex component
840+
const complexEvent = events.find(e => e.eventName === 'document_upload_clicked');
841+
assert.ok(complexEvent);
842+
assert.strictEqual(complexEvent.source, 'segment');
843+
assert.strictEqual(complexEvent.functionName, 'onFileUploadClick');
844+
assert.strictEqual(complexEvent.line, 53);
845+
assert.deepStrictEqual(complexEvent.properties, {
846+
documentId: { type: 'any' },
847+
documentType: { type: 'string' }
848+
});
643849
});
644850
});

0 commit comments

Comments
 (0)