@jtl-software/eslint-plugin-posthog 0.1.1 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtl-software/eslint-plugin-posthog",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "ESLint rules for PostHog event tracking best practices",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -18,6 +18,9 @@ export default {
18
18
  },
19
19
 
20
20
  create(context) {
21
+ const wrapperFunctions = new Map();
22
+ const captureCalls = [];
23
+
21
24
  /**
22
25
  * Check if a string is in camelCase
23
26
  * @param {string} str - The string to check
@@ -43,30 +46,137 @@ export default {
43
46
  );
44
47
  }
45
48
 
49
+ function validateObjectProperties(objectNode) {
50
+ if (!objectNode || objectNode.type !== 'ObjectExpression') {
51
+ return;
52
+ }
53
+
54
+ objectNode.properties.forEach((prop) => {
55
+ if (prop.type === 'Property' && prop.key.type === 'Identifier') {
56
+ const propertyName = prop.key.name;
57
+ if (!isCamelCase(propertyName)) {
58
+ context.report({
59
+ node: prop.key,
60
+ messageId: 'notCamelCase',
61
+ data: {
62
+ property: propertyName,
63
+ },
64
+ });
65
+ }
66
+ }
67
+ });
68
+ }
69
+
70
+ function resolvePropertiesArgument(propertiesArg) {
71
+ if (!propertiesArg) {
72
+ return null;
73
+ }
74
+
75
+ // Direct object literal
76
+ if (propertiesArg.type === 'ObjectExpression') {
77
+ return propertiesArg;
78
+ }
79
+
80
+ // Variable reference - trace back to definition
81
+ if (propertiesArg.type === 'Identifier') {
82
+ const scope = context.sourceCode.getScope(propertiesArg);
83
+ const variable = scope.variables.find((v) => v.name === propertiesArg.name);
84
+
85
+ if (variable && variable.defs.length > 0) {
86
+ const def = variable.defs[0];
87
+ if (def.node.init && def.node.init.type === 'ObjectExpression') {
88
+ return def.node.init;
89
+ }
90
+ }
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ function getFunctionName(node) {
97
+ // Regular function declaration: function foo() {}
98
+ if (node.type === 'FunctionDeclaration' && node.id) {
99
+ return node.id.name;
100
+ }
101
+
102
+ // Variable declaration with arrow function: const foo = () => {}
103
+ // Variable declaration with function expression: const foo = function() {}
104
+ if (node.parent && node.parent.type === 'VariableDeclarator' && node.parent.id) {
105
+ return node.parent.id.name;
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ function getParentFunction(node) {
112
+ let current = node.parent;
113
+ while (current) {
114
+ if (
115
+ current.type === 'FunctionDeclaration' ||
116
+ current.type === 'FunctionExpression' ||
117
+ current.type === 'ArrowFunctionExpression'
118
+ ) {
119
+ return current;
120
+ }
121
+ current = current.parent;
122
+ }
123
+ return null;
124
+ }
125
+
126
+ const allCallExpressions = [];
127
+
46
128
  return {
47
129
  CallExpression(node) {
48
- if (!isPostHogCapture(node)) {
49
- return;
50
- }
130
+ allCallExpressions.push(node);
51
131
 
52
- // Second argument should be the properties object
53
- const propertiesArg = node.arguments[1];
54
- if (!propertiesArg || propertiesArg.type !== 'ObjectExpression') {
55
- return;
132
+ if (isPostHogCapture(node)) {
133
+ captureCalls.push(node);
56
134
  }
135
+ },
136
+
137
+ 'Program:exit'() {
138
+ captureCalls.forEach((captureCall) => {
139
+ const propertiesArg = captureCall.arguments[1];
140
+
141
+ if (propertiesArg && propertiesArg.type === 'Identifier') {
142
+ const parentFunc = getParentFunction(captureCall);
143
+ if (parentFunc) {
144
+ const paramName = propertiesArg.name;
145
+ const paramIndex = parentFunc.params.findIndex(
146
+ (p) => p.type === 'Identifier' && p.name === paramName,
147
+ );
148
+
149
+ if (paramIndex !== -1) {
150
+ const functionName = getFunctionName(parentFunc);
151
+ if (functionName) {
152
+ wrapperFunctions.set(functionName, paramIndex);
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ // validate direct calls
159
+ const objectNode = resolvePropertiesArgument(propertiesArg);
160
+ if (objectNode) {
161
+ validateObjectProperties(objectNode);
162
+ }
163
+ });
164
+
165
+ // validate calls to wrapper functions
166
+ allCallExpressions.forEach((callNode) => {
167
+ if (isPostHogCapture(callNode)) {
168
+ return;
169
+ }
57
170
 
58
- // Check each property in the object
59
- propertiesArg.properties.forEach((prop) => {
60
- if (prop.type === 'Property' && prop.key.type === 'Identifier') {
61
- const propertyName = prop.key.name;
62
- if (!isCamelCase(propertyName)) {
63
- context.report({
64
- node: prop.key,
65
- messageId: 'notCamelCase',
66
- data: {
67
- property: propertyName,
68
- },
69
- });
171
+ if (callNode.callee.type === 'Identifier') {
172
+ const functionName = callNode.callee.name;
173
+ if (wrapperFunctions.has(functionName)) {
174
+ const paramIndex = wrapperFunctions.get(functionName);
175
+ const propertiesArg = callNode.arguments[paramIndex];
176
+ const objectNode = resolvePropertiesArgument(propertiesArg);
177
+ if (objectNode) {
178
+ validateObjectProperties(objectNode);
179
+ }
70
180
  }
71
181
  }
72
182
  });
@@ -23,6 +23,39 @@ ruleTester.run('consistent-property-naming', rule, {
23
23
  code: `// Not a PostHog call
24
24
  someOtherFunction({ user_id: '123' })`,
25
25
  },
26
+ // Variable tracking - valid camelCase
27
+ {
28
+ code: `
29
+ const properties = { userId: '123', productName: 'Test' };
30
+ postHog.capture('event_name', properties);
31
+ `,
32
+ },
33
+ // Wrapper function - valid camelCase
34
+ {
35
+ code: `
36
+ function trackEvent(name, props) {
37
+ postHog.capture(name, props);
38
+ }
39
+ trackEvent('event_name', { userId: '123', productName: 'Test' });
40
+ `,
41
+ },
42
+ // Nested function with wrapper - valid camelCase
43
+ {
44
+ code: `
45
+ function Component() {
46
+ const handleCapture = (eventName, properties) => {
47
+ postHog.capture(eventName, properties);
48
+ };
49
+
50
+ function handleClick() {
51
+ handleCapture('button_clicked', {
52
+ buttonId: '123',
53
+ buttonName: 'Submit'
54
+ });
55
+ }
56
+ }
57
+ `,
58
+ },
26
59
  ],
27
60
 
28
61
  invalid: [
@@ -70,5 +103,117 @@ ruleTester.run('consistent-property-naming', rule, {
70
103
  },
71
104
  ],
72
105
  },
106
+ // Variable tracking scenarios
107
+ {
108
+ code: `
109
+ const properties = { user_id: '123' };
110
+ postHog.capture('event_name', properties);
111
+ `,
112
+ errors: [
113
+ {
114
+ messageId: 'notCamelCase',
115
+ data: { property: 'user_id' },
116
+ },
117
+ ],
118
+ },
119
+ {
120
+ code: `
121
+ const props = { product_id: '123', product_name: 'Test' };
122
+ postHog.capture('event_name', props);
123
+ `,
124
+ errors: [
125
+ {
126
+ messageId: 'notCamelCase',
127
+ data: { property: 'product_id' },
128
+ },
129
+ {
130
+ messageId: 'notCamelCase',
131
+ data: { property: 'product_name' },
132
+ },
133
+ ],
134
+ },
135
+ {
136
+ code: `
137
+ const eventProps = {
138
+ userId: '123',
139
+ product_name: 'Test',
140
+ is_active: true
141
+ };
142
+ postHog.capture('event_name', eventProps);
143
+ `,
144
+ errors: [
145
+ {
146
+ messageId: 'notCamelCase',
147
+ data: { property: 'product_name' },
148
+ },
149
+ {
150
+ messageId: 'notCamelCase',
151
+ data: { property: 'is_active' },
152
+ },
153
+ ],
154
+ },
155
+ // Wrapper function scenarios
156
+ {
157
+ code: `
158
+ function trackEvent(name, props) {
159
+ postHog.capture(name, props);
160
+ }
161
+ trackEvent('event_name', { user_id: '123' });
162
+ `,
163
+ errors: [
164
+ {
165
+ messageId: 'notCamelCase',
166
+ data: { property: 'user_id' },
167
+ },
168
+ ],
169
+ },
170
+ {
171
+ code: `
172
+ const handleCapture = (eventName, properties) => {
173
+ postHog.capture(eventName, properties);
174
+ };
175
+ handleCapture('product_added', {
176
+ product_id: '123',
177
+ product_name: 'Test'
178
+ });
179
+ `,
180
+ errors: [
181
+ {
182
+ messageId: 'notCamelCase',
183
+ data: { property: 'product_id' },
184
+ },
185
+ {
186
+ messageId: 'notCamelCase',
187
+ data: { property: 'product_name' },
188
+ },
189
+ ],
190
+ },
191
+ // Nested function scenario (like the original bug report)
192
+ {
193
+ code: `
194
+ function Component() {
195
+ const handleCapture = (eventName, properties) => {
196
+ postHog.capture(eventName, properties);
197
+ };
198
+
199
+ function handleClick() {
200
+ handleCapture('button_clicked', {
201
+ button_id: '123',
202
+ button_name: 'Submit'
203
+ });
204
+ }
205
+ }
206
+ `,
207
+ errors: [
208
+ {
209
+ messageId: 'notCamelCase',
210
+ data: { property: 'button_id' },
211
+ },
212
+ {
213
+ messageId: 'notCamelCase',
214
+ data: { property: 'button_name' },
215
+ },
216
+ ],
217
+ },
73
218
  ],
74
219
  });