@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
|
@@ -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
|
-
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
130
|
+
allCallExpressions.push(node);
|
|
51
131
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
});
|