@jtl-software/eslint-plugin-posthog 0.1.0 → 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.
@@ -0,0 +1,19 @@
1
+ version: 2
2
+ updates:
3
+ # Enable version updates for npm dependencies
4
+ - package-ecosystem: "npm"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ open-pull-requests-limit: 5
9
+ groups:
10
+ dependencies:
11
+ patterns:
12
+ - "*"
13
+
14
+ # Enable version updates for GitHub Actions
15
+ - package-ecosystem: "github-actions"
16
+ directory: "/"
17
+ schedule:
18
+ interval: "weekly"
19
+ open-pull-requests-limit: 5
package/README.md CHANGED
@@ -115,20 +115,6 @@ To test the plugin in your project before publishing:
115
115
 
116
116
  3. Add the plugin to your ESLint config as shown above
117
117
 
118
- ### CI/CD
119
-
120
- This project uses GitHub Actions for continuous integration and deployment:
121
-
122
- - **Tests** run automatically on every push and pull request
123
- - **Publishing to npm** happens automatically when pushing to the `main` branch
124
-
125
- To set up automatic publishing, add an `NPM_TOKEN` secret to your GitHub repository:
126
-
127
- 1. Generate an npm access token at https://www.npmjs.com/settings/tokens
128
- 2. Go to your repository settings on GitHub
129
- 3. Navigate to Settings → Secrets and variables → Actions
130
- 4. Add a new repository secret named `NPM_TOKEN` with your npm token
131
-
132
118
  ## License
133
119
 
134
120
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtl-software/eslint-plugin-posthog",
3
- "version": "0.1.0",
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",
@@ -13,6 +13,10 @@
13
13
  ],
14
14
  "author": "JTL Platform Team",
15
15
  "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/jtl-software/eslint-plugin-posthog.git"
19
+ },
16
20
  "peerDependencies": {
17
21
  "eslint": "^9.0.0"
18
22
  },
@@ -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
  });