eslint-plugin-sfmc 0.1.3 → 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.
Files changed (41) hide show
  1. package/README.md +48 -47
  2. package/docs/rules/ssjs/arg-types.md +72 -0
  3. package/docs/rules/ssjs/core-method-arity.md +72 -0
  4. package/package.json +18 -5
  5. package/src/ampscript-parser.js +11 -4
  6. package/src/ampscript-processor.js +9 -3
  7. package/src/index.js +8 -0
  8. package/src/processor.js +12 -4
  9. package/src/rules/amp/function-arity.js +3 -1
  10. package/src/rules/amp/naming-convention.js +7 -3
  11. package/src/rules/amp/no-deprecated-function.js +6 -2
  12. package/src/rules/amp/no-email-excluded-function.js +3 -1
  13. package/src/rules/amp/no-inline-statement.js +3 -1
  14. package/src/rules/amp/no-nested-ampscript-delimiter.js +5 -5
  15. package/src/rules/amp/no-nested-script-tag.js +3 -1
  16. package/src/rules/amp/no-smart-quotes.js +1 -1
  17. package/src/rules/amp/prefer-attribute-value.js +10 -4
  18. package/src/rules/amp/require-rowcount-check.js +3 -1
  19. package/src/rules/amp/require-variable-declaration.js +7 -3
  20. package/src/rules/ssjs/cache-loop-length.js +10 -4
  21. package/src/rules/ssjs/no-hardcoded-credentials.js +3 -1
  22. package/src/rules/ssjs/no-treatascontent-injection.js +8 -4
  23. package/src/rules/ssjs/no-unavailable-method.js +30 -10
  24. package/src/rules/ssjs/no-unknown-core-method.js +27 -9
  25. package/src/rules/ssjs/no-unknown-http-method.js +9 -3
  26. package/src/rules/ssjs/no-unknown-platform-client-browser.js +9 -6
  27. package/src/rules/ssjs/no-unknown-platform-function.js +3 -1
  28. package/src/rules/ssjs/no-unknown-platform-request.js +3 -1
  29. package/src/rules/ssjs/no-unknown-platform-response.js +3 -1
  30. package/src/rules/ssjs/no-unknown-platform-variable.js +3 -1
  31. package/src/rules/ssjs/no-unknown-wsproxy-method.js +30 -13
  32. package/src/rules/ssjs/no-unsupported-syntax.js +10 -5
  33. package/src/rules/ssjs/platform-function-arity.js +6 -2
  34. package/src/rules/ssjs/prefer-parsejson-safe-arg.js +17 -9
  35. package/src/rules/ssjs/prefer-platform-load-version.js +1 -4
  36. package/src/rules/ssjs/require-hasownproperty.js +34 -27
  37. package/src/rules/ssjs/require-platform-load-order.js +6 -2
  38. package/src/rules/ssjs/require-platform-load.js +4 -3
  39. package/src/rules/ssjs/ssjs-arg-types.js +345 -0
  40. package/src/rules/ssjs/ssjs-core-method-arity.js +176 -0
  41. package/src/ssjs-processor.js +3 -1
@@ -44,7 +44,9 @@ export default {
44
44
  },
45
45
 
46
46
  'Program:exit'() {
47
- if (platformLoadLine === null) return;
47
+ if (platformLoadLine === null) {
48
+ return;
49
+ }
48
50
 
49
51
  for (const usage of coreUsages) {
50
52
  if (usage.line < platformLoadLine) {
@@ -87,7 +89,9 @@ function isPlatformLoadCall(node) {
87
89
 
88
90
  function getCoreObjectUsage(node) {
89
91
  const callee = node.callee;
90
- if (callee.type !== 'MemberExpression') return null;
92
+ if (callee.type !== 'MemberExpression') {
93
+ return null;
94
+ }
91
95
 
92
96
  if (
93
97
  callee.object.type === 'MemberExpression' &&
@@ -49,8 +49,7 @@ export default {
49
49
 
50
50
  'Program:exit'() {
51
51
  if (!hasPlatformLoad) {
52
- for (let i = 0; i < pendingReports.length; i++) {
53
- const { node, name } = pendingReports[i];
52
+ for (const [i, { node, name }] of pendingReports.entries()) {
54
53
  context.report({
55
54
  node,
56
55
  messageId: 'missingLoad',
@@ -97,7 +96,9 @@ function isPlatformLoadCall(node) {
97
96
 
98
97
  function getCoreObjectUsage(node) {
99
98
  const callee = node.callee;
100
- if (callee.type !== 'MemberExpression') return null;
99
+ if (callee.type !== 'MemberExpression') {
100
+ return null;
101
+ }
101
102
 
102
103
  if (
103
104
  callee.object.type === 'MemberExpression' &&
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Rule: ssjs-arg-types
3
+ *
4
+ * Checks that literal arguments match the expected parameter types for known
5
+ * SSJS functions and methods. Only literal values are checked — variable
6
+ * arguments are skipped because their type cannot be determined statically.
7
+ *
8
+ * Covers:
9
+ * - Platform.Function.*, Platform.Variable.*, Platform.Response.*,
10
+ * Platform.Request.*, Platform.Recipient.*
11
+ * - HTTP.*, HTTPHeader.*, Attribute.*
12
+ * - WSProxy instance methods
13
+ * - Core Library static calls (DataExtension.Init, BounceEvent.Retrieve, …)
14
+ * - Core Library instance methods (var de = DataExtension.Init(…); de.Add(…))
15
+ * - Global functions (Format, String, Error)
16
+ */
17
+
18
+ import {
19
+ platformFunctionLookup,
20
+ platformResponseLookup,
21
+ platformVariableLookup,
22
+ platformRequestLookup,
23
+ platformRecipientLookup,
24
+ httpMethodLookup,
25
+ httpHeaderMethodLookup,
26
+ wsproxyMethodLookup,
27
+ attributeMethodLookup,
28
+ ssjsGlobalsLookup,
29
+ coreMethodArityLookup,
30
+ coreObjectNames,
31
+ } from 'ssjs-data';
32
+
33
+ // Platform.SubNS → lookup Map
34
+ const PLATFORM_SUBNS_LOOKUPS = new Map([
35
+ ['function', platformFunctionLookup],
36
+ ['variable', platformVariableLookup],
37
+ ['response', platformResponseLookup],
38
+ ['request', platformRequestLookup],
39
+ ['recipient', platformRecipientLookup],
40
+ ]);
41
+
42
+ // Top-level non-Platform objects with static method lookups
43
+ const TOPLEVEL_LOOKUPS = new Map([
44
+ ['http', httpMethodLookup],
45
+ ['httpheader', httpHeaderMethodLookup],
46
+ ['attribute', attributeMethodLookup],
47
+ ]);
48
+
49
+ export default {
50
+ meta: {
51
+ type: 'suggestion',
52
+ docs: {
53
+ description:
54
+ 'Check that literal arguments match expected parameter types for SSJS functions',
55
+ },
56
+ messages: {
57
+ typeMismatch:
58
+ "Argument {{pos}} ('{{param}}') of '{{call}}' expects type '{{expected}}' but received '{{actual}}'.",
59
+ },
60
+ schema: [],
61
+ },
62
+
63
+ create(context) {
64
+ const coreVars = new Map(); // varName → className (Core Library instances)
65
+ const wsproxyVars = new Set(); // varNames assigned new WSProxy()
66
+
67
+ function checkArgs(entry, args, callName) {
68
+ if (!entry || !entry.params || entry.params.length === 0) {
69
+ return;
70
+ }
71
+ for (const [i, arg] of args.entries()) {
72
+ const param = entry.params[i];
73
+ if (!param) {
74
+ break;
75
+ } // beyond declared params — arity rule handles this
76
+ if (!param.type || param.type === 'any') {
77
+ continue;
78
+ }
79
+ const actual = inferArgType(arg);
80
+ if (actual === null) {
81
+ continue;
82
+ } // unknown type (variable/call) — skip
83
+ if (!isTypeCompatible(actual, param.type)) {
84
+ context.report({
85
+ node: arg,
86
+ messageId: 'typeMismatch',
87
+ data: {
88
+ pos: String(i + 1),
89
+ param: param.name,
90
+ call: callName,
91
+ expected: param.type,
92
+ actual,
93
+ },
94
+ });
95
+ }
96
+ }
97
+ }
98
+
99
+ return {
100
+ VariableDeclaration(node) {
101
+ for (const decl of node.declarations) {
102
+ if (!decl.init || !decl.id || decl.id.type !== 'Identifier') {
103
+ continue;
104
+ }
105
+ if (isWSProxyConstructor(decl.init)) {
106
+ wsproxyVars.add(decl.id.name);
107
+ continue;
108
+ }
109
+ const coreType = getCoreInitType(decl.init);
110
+ if (coreType) {
111
+ coreVars.set(decl.id.name, coreType);
112
+ }
113
+ }
114
+ },
115
+
116
+ AssignmentExpression(node) {
117
+ if (node.left.type !== 'Identifier') {
118
+ return;
119
+ }
120
+ if (isWSProxyConstructor(node.right)) {
121
+ wsproxyVars.add(node.left.name);
122
+ return;
123
+ }
124
+ const coreType = getCoreInitType(node.right);
125
+ if (coreType) {
126
+ coreVars.set(node.left.name, coreType);
127
+ }
128
+ },
129
+
130
+ CallExpression(node) {
131
+ const callee = node.callee;
132
+
133
+ // Global function: Format(...), String(...), Error(...)
134
+ if (callee.type === 'Identifier') {
135
+ const entry = ssjsGlobalsLookup.get(callee.name.toLowerCase());
136
+ if (entry) {
137
+ checkArgs(entry, node.arguments, callee.name);
138
+ }
139
+ return;
140
+ }
141
+
142
+ if (callee.type !== 'MemberExpression') {
143
+ return;
144
+ }
145
+ if (callee.property.type !== 'Identifier') {
146
+ return;
147
+ }
148
+ const methodName = callee.property.name;
149
+
150
+ // Platform.SubNS.Method(...): Platform.Function.X, Platform.Response.X, …
151
+ if (
152
+ callee.object.type === 'MemberExpression' &&
153
+ callee.object.object.type === 'Identifier' &&
154
+ callee.object.object.name === 'Platform' &&
155
+ callee.object.property.type === 'Identifier'
156
+ ) {
157
+ const subNs = callee.object.property.name.toLowerCase();
158
+ const lookup = PLATFORM_SUBNS_LOOKUPS.get(subNs);
159
+ if (lookup) {
160
+ const entry = lookup.get(methodName.toLowerCase());
161
+ if (entry) {
162
+ checkArgs(
163
+ entry,
164
+ node.arguments,
165
+ `Platform.${callee.object.property.name}.${methodName}`,
166
+ );
167
+ }
168
+ }
169
+ return;
170
+ }
171
+
172
+ if (callee.object.type === 'Identifier') {
173
+ const objName = callee.object.name;
174
+ const objLower = objName.toLowerCase();
175
+
176
+ // Static top-level: HTTP.Post(...), HTTPHeader.SetValue(...), Attribute.GetValue(...)
177
+ const topLookup = TOPLEVEL_LOOKUPS.get(objLower);
178
+ if (topLookup) {
179
+ const entry = topLookup.get(methodName.toLowerCase());
180
+ if (entry) {
181
+ checkArgs(entry, node.arguments, `${objName}.${methodName}`);
182
+ }
183
+ return;
184
+ }
185
+
186
+ // WSProxy instance method: proxy.Retrieve(...)
187
+ if (wsproxyVars.has(objName)) {
188
+ const entry = wsproxyMethodLookup.get(methodName.toLowerCase());
189
+ if (entry) {
190
+ checkArgs(entry, node.arguments, `WSProxy.${methodName}`);
191
+ }
192
+ return;
193
+ }
194
+
195
+ // Core Library instance method: de.Add(...)
196
+ const coreType = coreVars.get(objName);
197
+ if (coreType) {
198
+ const classLookup = coreMethodArityLookup.get(coreType.toLowerCase());
199
+ if (classLookup) {
200
+ const entry = classLookup.get(methodName.toLowerCase());
201
+ if (entry) {
202
+ checkArgs(entry, node.arguments, `${coreType}.${methodName}`);
203
+ }
204
+ }
205
+ return;
206
+ }
207
+
208
+ // Static Core Library single-name: DataExtension.Init(...), BounceEvent.Retrieve(...)
209
+ if (coreObjectNames.has(objName)) {
210
+ const classLookup = coreMethodArityLookup.get(objLower);
211
+ if (classLookup) {
212
+ const entry = classLookup.get(methodName.toLowerCase());
213
+ if (entry) {
214
+ checkArgs(entry, node.arguments, `${objName}.${methodName}`);
215
+ }
216
+ }
217
+ }
218
+ return;
219
+ }
220
+
221
+ // Multi-part static: DataExtension.Rows.Add(...), TriggeredSend.Tracking.Clicks.Retrieve(...)
222
+ const objectPath = getMemberPath(callee.object);
223
+ if (objectPath && coreObjectNames.has(objectPath)) {
224
+ const classLookup = coreMethodArityLookup.get(objectPath.toLowerCase());
225
+ if (classLookup) {
226
+ const entry = classLookup.get(methodName.toLowerCase());
227
+ if (entry) {
228
+ checkArgs(entry, node.arguments, `${objectPath}.${methodName}`);
229
+ }
230
+ }
231
+ }
232
+ },
233
+ };
234
+ },
235
+ };
236
+
237
+ function getCoreInitType(node) {
238
+ if (!node || node.type !== 'CallExpression') {
239
+ return null;
240
+ }
241
+ const callee = node.callee;
242
+ if (callee.type !== 'MemberExpression') {
243
+ return null;
244
+ }
245
+ if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
246
+ return null;
247
+ }
248
+ if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
249
+ return callee.object.name;
250
+ }
251
+ if (
252
+ callee.object.type === 'MemberExpression' &&
253
+ callee.object.object.type === 'Identifier' &&
254
+ callee.object.property.type === 'Identifier'
255
+ ) {
256
+ const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
257
+ if (coreObjectNames.has(fullName)) {
258
+ return fullName;
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+
264
+ function getMemberPath(node) {
265
+ if (node.type === 'Identifier') {
266
+ return node.name;
267
+ }
268
+ if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
269
+ const obj = getMemberPath(node.object);
270
+ return obj ? `${obj}.${node.property.name}` : null;
271
+ }
272
+ return null;
273
+ }
274
+
275
+ function inferArgType(node) {
276
+ if (!node || node.type === 'SpreadElement') {
277
+ return null;
278
+ }
279
+ if (node.type === 'Literal') {
280
+ if (typeof node.value === 'string') {
281
+ return 'string';
282
+ }
283
+ if (typeof node.value === 'number') {
284
+ return 'number';
285
+ }
286
+ if (typeof node.value === 'boolean') {
287
+ return 'boolean';
288
+ }
289
+ if (node.value === null) {
290
+ return 'null';
291
+ }
292
+ }
293
+ if (node.type === 'TemplateLiteral' && !node.tag) {
294
+ return 'string';
295
+ }
296
+ if (node.type === 'ArrayExpression') {
297
+ const elems = node.elements.filter(Boolean);
298
+ if (elems.length === 0) {
299
+ return 'array';
300
+ }
301
+ if (elems.every((e) => e.type === 'Literal' && typeof e.value === 'string')) {
302
+ return 'string[]';
303
+ }
304
+ if (elems.every((e) => e.type === 'Literal' && typeof e.value === 'number')) {
305
+ return 'number[]';
306
+ }
307
+ return 'array';
308
+ }
309
+ if (node.type === 'ObjectExpression') {
310
+ return 'object';
311
+ }
312
+ return null;
313
+ }
314
+
315
+ function isTypeCompatible(actual, expected) {
316
+ if (!expected || expected === 'any') {
317
+ return true;
318
+ }
319
+ const allowed = expected.split('|').map((t) => t.trim());
320
+ if (allowed.includes(actual)) {
321
+ return true;
322
+ }
323
+ // 'array' is compatible with typed array variants
324
+ if (allowed.includes('array') && (actual === 'string[]' || actual === 'number[]')) {
325
+ return true;
326
+ }
327
+ return false;
328
+ }
329
+
330
+ function isWSProxyConstructor(node) {
331
+ if (!node || node.type !== 'NewExpression') {
332
+ return false;
333
+ }
334
+ const c = node.callee;
335
+ return (
336
+ c.type === 'MemberExpression' &&
337
+ c.property.type === 'Identifier' &&
338
+ c.property.name === 'WSProxy' &&
339
+ c.object.type === 'MemberExpression' &&
340
+ c.object.property.type === 'Identifier' &&
341
+ c.object.property.name === 'Util' &&
342
+ c.object.object.type === 'Identifier' &&
343
+ c.object.object.name === 'Script'
344
+ );
345
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Rule: ssjs-core-method-arity
3
+ *
4
+ * Enforces correct argument counts for Core Library object method calls.
5
+ * Covers both instance methods and static calls:
6
+ *
7
+ * var de = DataExtension.Init("MyDE");
8
+ * de.Add(rowObj); // instance
9
+ * BounceEvent.Retrieve(filter); // static single-name
10
+ * DataExtension.Rows.Add(rowObj); // static multi-part
11
+ */
12
+
13
+ import { coreObjectNames, coreMethodArityLookup } from 'ssjs-data';
14
+
15
+ export default {
16
+ meta: {
17
+ type: 'problem',
18
+ docs: {
19
+ description: 'Enforce correct argument counts for Core Library object methods',
20
+ },
21
+ messages: {
22
+ tooFewArgs:
23
+ "'{{call}}' requires at least {{min}} argument(s) but was called with {{actual}}.",
24
+ tooManyArgs:
25
+ "'{{call}}' accepts at most {{max}} argument(s) but was called with {{actual}}.",
26
+ },
27
+ schema: [],
28
+ },
29
+
30
+ create(context) {
31
+ const coreVars = new Map(); // varName → className
32
+
33
+ function checkArity(entry, args, callName, reportNode) {
34
+ if (!entry) {
35
+ return;
36
+ }
37
+ const actual = args.length;
38
+ if (actual < entry.minArgs) {
39
+ context.report({
40
+ node: reportNode,
41
+ messageId: 'tooFewArgs',
42
+ data: { call: callName, min: String(entry.minArgs), actual: String(actual) },
43
+ });
44
+ } else if (actual > entry.maxArgs) {
45
+ context.report({
46
+ node: reportNode,
47
+ messageId: 'tooManyArgs',
48
+ data: { call: callName, max: String(entry.maxArgs), actual: String(actual) },
49
+ });
50
+ }
51
+ }
52
+
53
+ return {
54
+ VariableDeclaration(node) {
55
+ for (const decl of node.declarations) {
56
+ if (!decl.init || !decl.id || decl.id.type !== 'Identifier') {
57
+ continue;
58
+ }
59
+ const coreType = getCoreInitType(decl.init);
60
+ if (coreType) {
61
+ coreVars.set(decl.id.name, coreType);
62
+ }
63
+ }
64
+ },
65
+
66
+ AssignmentExpression(node) {
67
+ if (node.left.type !== 'Identifier') {
68
+ return;
69
+ }
70
+ const coreType = getCoreInitType(node.right);
71
+ if (coreType) {
72
+ coreVars.set(node.left.name, coreType);
73
+ }
74
+ },
75
+
76
+ CallExpression(node) {
77
+ const callee = node.callee;
78
+ if (callee.type !== 'MemberExpression') {
79
+ return;
80
+ }
81
+ if (callee.property.type !== 'Identifier') {
82
+ return;
83
+ }
84
+ const methodName = callee.property.name;
85
+
86
+ if (callee.object.type === 'Identifier') {
87
+ const objName = callee.object.name;
88
+
89
+ // Core Library instance method: de.Add(...)
90
+ const coreType = coreVars.get(objName);
91
+ if (coreType) {
92
+ const classLookup = coreMethodArityLookup.get(coreType.toLowerCase());
93
+ if (classLookup) {
94
+ const entry = classLookup.get(methodName.toLowerCase());
95
+ checkArity(
96
+ entry,
97
+ node.arguments,
98
+ `${coreType}.${methodName}`,
99
+ callee.property,
100
+ );
101
+ }
102
+ return;
103
+ }
104
+
105
+ // Static single-name: DataExtension.Init(...), BounceEvent.Retrieve(...)
106
+ if (coreObjectNames.has(objName)) {
107
+ const classLookup = coreMethodArityLookup.get(objName.toLowerCase());
108
+ if (classLookup) {
109
+ const entry = classLookup.get(methodName.toLowerCase());
110
+ checkArity(
111
+ entry,
112
+ node.arguments,
113
+ `${objName}.${methodName}`,
114
+ callee.property,
115
+ );
116
+ }
117
+ return;
118
+ }
119
+ }
120
+
121
+ // Static multi-part: DataExtension.Rows.Add(...), TriggeredSend.Tracking.Clicks.Retrieve(...)
122
+ const objectPath = getMemberPath(callee.object);
123
+ if (objectPath && coreObjectNames.has(objectPath)) {
124
+ const classLookup = coreMethodArityLookup.get(objectPath.toLowerCase());
125
+ if (classLookup) {
126
+ const entry = classLookup.get(methodName.toLowerCase());
127
+ checkArity(
128
+ entry,
129
+ node.arguments,
130
+ `${objectPath}.${methodName}`,
131
+ callee.property,
132
+ );
133
+ }
134
+ }
135
+ },
136
+ };
137
+ },
138
+ };
139
+
140
+ function getCoreInitType(node) {
141
+ if (!node || node.type !== 'CallExpression') {
142
+ return null;
143
+ }
144
+ const callee = node.callee;
145
+ if (callee.type !== 'MemberExpression') {
146
+ return null;
147
+ }
148
+ if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
149
+ return null;
150
+ }
151
+ if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
152
+ return callee.object.name;
153
+ }
154
+ if (
155
+ callee.object.type === 'MemberExpression' &&
156
+ callee.object.object.type === 'Identifier' &&
157
+ callee.object.property.type === 'Identifier'
158
+ ) {
159
+ const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
160
+ if (coreObjectNames.has(fullName)) {
161
+ return fullName;
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+
167
+ function getMemberPath(node) {
168
+ if (node.type === 'Identifier') {
169
+ return node.name;
170
+ }
171
+ if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
172
+ const obj = getMemberPath(node.object);
173
+ return obj ? `${obj}.${node.property.name}` : null;
174
+ }
175
+ return null;
176
+ }
@@ -20,7 +20,9 @@ export function preprocess(text, filename) {
20
20
 
21
21
  SCRIPT_CLOSE_RE.lastIndex = openEnd;
22
22
  const closeMatch = SCRIPT_CLOSE_RE.exec(text);
23
- if (!closeMatch) break;
23
+ if (!closeMatch) {
24
+ break;
25
+ }
24
26
 
25
27
  const jsCode = text.slice(openEnd, closeMatch.index);
26
28