eslint-plugin-sfmc 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.
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 +20 -7
  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
@@ -24,7 +24,9 @@ function isRowCountCall(node) {
24
24
  }
25
25
 
26
26
  function extractRowCountVariables(node, into) {
27
- if (!node) return;
27
+ if (!node) {
28
+ return;
29
+ }
28
30
 
29
31
  if (isRowCountCall(node) && node.arguments && node.arguments.length > 0) {
30
32
  const argument = node.arguments[0];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Requires that @variables are declared with `var` before being used in
2
+ * Requires that `@variables` are declared with `var` before being used in
3
3
  * `set` statements. System variables (@@) and personalization strings
4
4
  * (no @ prefix) are not affected.
5
5
  *
@@ -33,9 +33,13 @@ export default {
33
33
  },
34
34
 
35
35
  SetStatement(node) {
36
- if (!node.target) return;
36
+ if (!node.target) {
37
+ return;
38
+ }
37
39
  const name = node.target.value;
38
- if (name.startsWith('@@')) return;
40
+ if (name.startsWith('@@')) {
41
+ return;
42
+ }
39
43
  if (!declared.has(name.toLowerCase())) {
40
44
  context.report({
41
45
  node: node.target,
@@ -32,7 +32,9 @@ export default {
32
32
  return {
33
33
  ForStatement(node) {
34
34
  const test = node.test;
35
- if (!test || test.type !== 'BinaryExpression') return;
35
+ if (!test || test.type !== 'BinaryExpression') {
36
+ return;
37
+ }
36
38
 
37
39
  let lengthExpr = null;
38
40
  if (containsMemberLength(test.right)) {
@@ -41,7 +43,9 @@ export default {
41
43
  lengthExpr = test.left;
42
44
  }
43
45
 
44
- if (!lengthExpr) return;
46
+ if (!lengthExpr) {
47
+ return;
48
+ }
45
49
 
46
50
  context.report({
47
51
  node: test,
@@ -54,7 +58,9 @@ export default {
54
58
  };
55
59
 
56
60
  function containsMemberLength(node) {
57
- if (!node) return false;
61
+ if (!node) {
62
+ return false;
63
+ }
58
64
  if (
59
65
  node.type === 'MemberExpression' &&
60
66
  node.property.type === 'Identifier' &&
@@ -81,7 +87,7 @@ function buildCacheSuggestion(forNode, lengthExpr, context) {
81
87
  messageId: 'suggestCacheLength',
82
88
  data: { obj: objText },
83
89
  fix(fixer) {
84
- const lastDecl = init.declarations[init.declarations.length - 1];
90
+ const lastDecl = init.declarations.at(-1);
85
91
  return [
86
92
  fixer.insertTextAfter(lastDecl, `, ${lenVar} = ${objText}.length`),
87
93
  fixer.replaceText(lengthExpr, lenVar),
@@ -43,7 +43,9 @@ export default {
43
43
  functionName = callee.property.name;
44
44
  }
45
45
 
46
- if (!functionName || !ENCRYPT_FUNCTIONS.has(functionName.toLowerCase())) return;
46
+ if (!functionName || !ENCRYPT_FUNCTIONS.has(functionName.toLowerCase())) {
47
+ return;
48
+ }
47
49
 
48
50
  for (const index of KEY_ARG_INDICES) {
49
51
  const argument = node.arguments[index];
@@ -19,8 +19,8 @@ export default {
19
19
  messages: {
20
20
  injection:
21
21
  'Concatenating dynamic values into TreatAsContent() risks AMPscript injection. ' +
22
- "Use Variable.SetValue() to pass values into AMPscript variables, then reference " +
23
- "them with @varName inside the TreatAsContent string.",
22
+ 'Use Variable.SetValue() to pass values into AMPscript variables, then reference ' +
23
+ 'them with @varName inside the TreatAsContent string.',
24
24
  },
25
25
  schema: [],
26
26
  },
@@ -28,8 +28,12 @@ export default {
28
28
  create(context) {
29
29
  return {
30
30
  CallExpression(node) {
31
- if (!isTreatAsContentCall(node)) return;
32
- if (node.arguments.length === 0) return;
31
+ if (!isTreatAsContentCall(node)) {
32
+ return;
33
+ }
34
+ if (node.arguments.length === 0) {
35
+ return;
36
+ }
33
37
 
34
38
  const arg = node.arguments[0];
35
39
  if (containsConcatenation(arg)) {
@@ -94,11 +94,15 @@ export default {
94
94
  // Array.prototype.map = ... / Array.isArray = ... / String.prototype.trim = ...
95
95
  AssignmentExpression(node) {
96
96
  const left = node.left;
97
- if (left.type !== 'MemberExpression') return;
97
+ if (left.type !== 'MemberExpression') {
98
+ return;
99
+ }
98
100
 
99
101
  const obj = left.object;
100
102
  const prop = left.property;
101
- if (prop.type !== 'Identifier') return;
103
+ if (prop.type !== 'Identifier') {
104
+ return;
105
+ }
102
106
 
103
107
  // Array.X = ...
104
108
  if (obj.type === 'Identifier' && obj.name === 'Array') {
@@ -120,10 +124,14 @@ export default {
120
124
 
121
125
  CallExpression(node) {
122
126
  const callee = node.callee;
123
- if (callee.type !== 'MemberExpression') return;
127
+ if (callee.type !== 'MemberExpression') {
128
+ return;
129
+ }
124
130
 
125
131
  const prop = callee.property;
126
- if (prop.type !== 'Identifier') return;
132
+ if (prop.type !== 'Identifier') {
133
+ return;
134
+ }
127
135
 
128
136
  const methodName = prop.name;
129
137
  const receiver = callee.object;
@@ -134,20 +142,28 @@ export default {
134
142
  receiver.name === 'Array' &&
135
143
  polyfillByStaticName.has(methodName)
136
144
  ) {
137
- if (ignored.has(methodName)) return;
145
+ if (ignored.has(methodName)) {
146
+ return;
147
+ }
138
148
  const entry = polyfillByStaticName.get(methodName);
139
149
  pendingReports.push({ node: prop, entry });
140
150
  return;
141
151
  }
142
152
 
143
153
  // ── Prototype methods ─────────────────────────────────────────
144
- if (!polyfillByPrototypeName.has(methodName)) return;
145
- if (ignored.has(methodName)) return;
154
+ if (!polyfillByPrototypeName.has(methodName)) {
155
+ return;
156
+ }
157
+ if (ignored.has(methodName)) {
158
+ return;
159
+ }
146
160
 
147
161
  const entry = polyfillByPrototypeName.get(methodName);
148
162
 
149
163
  // Skip known SFMC top-level objects to avoid false positives.
150
- if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name)) return;
164
+ if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name)) {
165
+ return;
166
+ }
151
167
 
152
168
  // For methods that also exist on String.prototype in ES3
153
169
  // (indexOf, lastIndexOf), only flag when the receiver is
@@ -161,7 +177,9 @@ export default {
161
177
  (receiver.type === 'CallExpression' &&
162
178
  receiver.callee.type === 'MemberExpression' &&
163
179
  polyfillByPrototypeName.has(receiver.callee.property.name));
164
- if (!isDefinitelyArray) return;
180
+ if (!isDefinitelyArray) {
181
+ return;
182
+ }
165
183
  }
166
184
 
167
185
  pendingReports.push({ node: prop, entry });
@@ -169,7 +187,9 @@ export default {
169
187
 
170
188
  'Program:exit'() {
171
189
  for (const { node, entry } of pendingReports) {
172
- if (isAlreadyPolyfilled(entry.method)) continue;
190
+ if (isAlreadyPolyfilled(entry.method)) {
191
+ continue;
192
+ }
173
193
 
174
194
  context.report({
175
195
  node,
@@ -30,7 +30,9 @@ export default {
30
30
  return {
31
31
  VariableDeclaration(node) {
32
32
  for (const decl of node.declarations) {
33
- if (!decl.init || !decl.id || decl.id.type !== 'Identifier') continue;
33
+ if (!decl.init || !decl.id || decl.id.type !== 'Identifier') {
34
+ continue;
35
+ }
34
36
  const coreType = getCoreInitType(decl.init);
35
37
  if (coreType) {
36
38
  variableTypes.set(decl.id.name, coreType);
@@ -39,7 +41,9 @@ export default {
39
41
  },
40
42
 
41
43
  AssignmentExpression(node) {
42
- if (node.left.type !== 'Identifier') return;
44
+ if (node.left.type !== 'Identifier') {
45
+ return;
46
+ }
43
47
  const coreType = getCoreInitType(node.right);
44
48
  if (coreType) {
45
49
  variableTypes.set(node.left.name, coreType);
@@ -48,18 +52,28 @@ export default {
48
52
 
49
53
  CallExpression(node) {
50
54
  const callee = node.callee;
51
- if (callee.type !== 'MemberExpression') return;
52
- if (callee.object.type !== 'Identifier') return;
53
- if (callee.property.type !== 'Identifier') return;
55
+ if (callee.type !== 'MemberExpression') {
56
+ return;
57
+ }
58
+ if (callee.object.type !== 'Identifier') {
59
+ return;
60
+ }
61
+ if (callee.property.type !== 'Identifier') {
62
+ return;
63
+ }
54
64
 
55
65
  const objectName = callee.object.name;
56
66
  const method = callee.property.name;
57
67
 
58
68
  const coreType = variableTypes.get(objectName);
59
- if (!coreType) return;
69
+ if (!coreType) {
70
+ return;
71
+ }
60
72
 
61
73
  const objectDef = coreObjectLookup.get(coreType);
62
- if (!objectDef) return;
74
+ if (!objectDef) {
75
+ return;
76
+ }
63
77
 
64
78
  const knownMethods = new Set(objectDef.methods.map((m) => m.toLowerCase()));
65
79
  if (!knownMethods.has(method.toLowerCase())) {
@@ -79,9 +93,13 @@ export default {
79
93
  };
80
94
 
81
95
  function getCoreInitType(node) {
82
- if (!node || node.type !== 'CallExpression') return null;
96
+ if (!node || node.type !== 'CallExpression') {
97
+ return null;
98
+ }
83
99
  const callee = node.callee;
84
- if (callee.type !== 'MemberExpression') return null;
100
+ if (callee.type !== 'MemberExpression') {
101
+ return null;
102
+ }
85
103
 
86
104
  if (callee.property.type === 'Identifier' && callee.property.name === 'Init') {
87
105
  if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
@@ -23,9 +23,15 @@ export default {
23
23
  return {
24
24
  CallExpression(node) {
25
25
  const callee = node.callee;
26
- if (callee.type !== 'MemberExpression') return;
27
- if (callee.object.type !== 'Identifier' || callee.object.name !== 'HTTP') return;
28
- if (callee.property.type !== 'Identifier') return;
26
+ if (callee.type !== 'MemberExpression') {
27
+ return;
28
+ }
29
+ if (callee.object.type !== 'Identifier' || callee.object.name !== 'HTTP') {
30
+ return;
31
+ }
32
+ if (callee.property.type !== 'Identifier') {
33
+ return;
34
+ }
29
35
 
30
36
  const methodName = callee.property.name;
31
37
  if (!httpMethodNames.has(methodName.toLowerCase())) {
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Rule: no-unknown-platform-client-browser
3
3
  *
4
- * Flags calls to Platform.ClientBrowser.* where the method name does not
5
- * exist in the known SFMC Platform.ClientBrowser catalog.
4
+ * Flags calls to Platform.ClientBrowser.* this namespace is deprecated.
5
+ * The methods previously under Platform.ClientBrowser.* are now available
6
+ * under Platform.Response.* (e.g. Platform.Response.Redirect,
7
+ * Platform.Response.SetCookie, Platform.Response.RemoveCookie).
8
+ * All Platform.ClientBrowser.* calls are flagged as unknown.
6
9
  */
7
10
 
8
- import { PLATFORM_CLIENT_BROWSER_METHODS } from 'ssjs-data';
9
-
10
- const knownMethods = new Set(PLATFORM_CLIENT_BROWSER_METHODS.map((m) => m.name.toLowerCase()));
11
+ const knownMethods = new Set();
11
12
 
12
13
  export default {
13
14
  meta: {
@@ -25,7 +26,9 @@ export default {
25
26
  return {
26
27
  CallExpression(node) {
27
28
  const callee = node.callee;
28
- if (callee.type !== 'MemberExpression') return;
29
+ if (callee.type !== 'MemberExpression') {
30
+ return;
31
+ }
29
32
 
30
33
  if (
31
34
  callee.object.type === 'MemberExpression' &&
@@ -24,7 +24,9 @@ export default {
24
24
  return {
25
25
  CallExpression(node) {
26
26
  const callee = node.callee;
27
- if (callee.type !== 'MemberExpression') return;
27
+ if (callee.type !== 'MemberExpression') {
28
+ return;
29
+ }
28
30
 
29
31
  if (
30
32
  callee.object.type === 'MemberExpression' &&
@@ -25,7 +25,9 @@ export default {
25
25
  return {
26
26
  CallExpression(node) {
27
27
  const callee = node.callee;
28
- if (callee.type !== 'MemberExpression') return;
28
+ if (callee.type !== 'MemberExpression') {
29
+ return;
30
+ }
29
31
 
30
32
  if (
31
33
  callee.object.type === 'MemberExpression' &&
@@ -25,7 +25,9 @@ export default {
25
25
  return {
26
26
  CallExpression(node) {
27
27
  const callee = node.callee;
28
- if (callee.type !== 'MemberExpression') return;
28
+ if (callee.type !== 'MemberExpression') {
29
+ return;
30
+ }
29
31
 
30
32
  if (
31
33
  callee.object.type === 'MemberExpression' &&
@@ -25,7 +25,9 @@ export default {
25
25
  return {
26
26
  CallExpression(node) {
27
27
  const callee = node.callee;
28
- if (callee.type !== 'MemberExpression') return;
28
+ if (callee.type !== 'MemberExpression') {
29
+ return;
30
+ }
29
31
 
30
32
  if (
31
33
  callee.object.type === 'MemberExpression' &&
@@ -29,9 +29,7 @@ export default {
29
29
  decl.id &&
30
30
  decl.id.type === 'Identifier' &&
31
31
  decl.init &&
32
- decl.init.type === 'NewExpression' &&
33
- decl.init.callee.type === 'Identifier' &&
34
- decl.init.callee.name === 'WSProxy'
32
+ isWSProxyConstructor(decl.init)
35
33
  ) {
36
34
  wsproxyVariables.add(decl.id.name);
37
35
  }
@@ -39,23 +37,26 @@ export default {
39
37
  },
40
38
 
41
39
  AssignmentExpression(node) {
42
- if (
43
- node.left.type === 'Identifier' &&
44
- node.right.type === 'NewExpression' &&
45
- node.right.callee.type === 'Identifier' &&
46
- node.right.callee.name === 'WSProxy'
47
- ) {
40
+ if (node.left.type === 'Identifier' && isWSProxyConstructor(node.right)) {
48
41
  wsproxyVariables.add(node.left.name);
49
42
  }
50
43
  },
51
44
 
52
45
  CallExpression(node) {
53
46
  const callee = node.callee;
54
- if (callee.type !== 'MemberExpression') return;
55
- if (callee.object.type !== 'Identifier') return;
56
- if (callee.property.type !== 'Identifier') return;
47
+ if (callee.type !== 'MemberExpression') {
48
+ return;
49
+ }
50
+ if (callee.object.type !== 'Identifier') {
51
+ return;
52
+ }
53
+ if (callee.property.type !== 'Identifier') {
54
+ return;
55
+ }
57
56
 
58
- if (!wsproxyVariables.has(callee.object.name)) return;
57
+ if (!wsproxyVariables.has(callee.object.name)) {
58
+ return;
59
+ }
59
60
 
60
61
  const methodName = callee.property.name;
61
62
  if (!wsproxyMethodNames.has(methodName.toLowerCase())) {
@@ -69,3 +70,19 @@ export default {
69
70
  };
70
71
  },
71
72
  };
73
+ function isWSProxyConstructor(node) {
74
+ if (!node || node.type !== 'NewExpression') {
75
+ return false;
76
+ }
77
+ const c = node.callee;
78
+ return (
79
+ c.type === 'MemberExpression' &&
80
+ c.property.type === 'Identifier' &&
81
+ c.property.name === 'WSProxy' &&
82
+ c.object.type === 'MemberExpression' &&
83
+ c.object.property.type === 'Identifier' &&
84
+ c.object.property.name === 'Util' &&
85
+ c.object.object.type === 'Identifier' &&
86
+ c.object.object.name === 'Script'
87
+ );
88
+ }
@@ -36,8 +36,7 @@ export default {
36
36
  unsupported: '{{label}} are not supported in SFMC SSJS. {{suggestion}}',
37
37
  suggestLogicalOr:
38
38
  "Replace ?? with || (note: semantics differ for falsy values like 0, '', and false)",
39
- suggestVarReturn:
40
- 'Assign the object to a variable, then return the variable',
39
+ suggestVarReturn: 'Assign the object to a variable, then return the variable',
41
40
  },
42
41
  schema: [
43
42
  {
@@ -65,8 +64,12 @@ export default {
65
64
  for (const [nodeType, entries] of unsupportedByNodeType) {
66
65
  listeners[nodeType] = function (node) {
67
66
  for (const entry of entries) {
68
- if (allowed.has(entry.feature)) continue;
69
- if (entry.test && !entry.test(node)) continue;
67
+ if (allowed.has(entry.feature)) {
68
+ continue;
69
+ }
70
+ if (entry.test && !entry.test(node)) {
71
+ continue;
72
+ }
70
73
 
71
74
  const report = {
72
75
  node,
@@ -90,7 +93,9 @@ export default {
90
93
  node.right.range[0],
91
94
  );
92
95
  const offset = between.indexOf('??');
93
- if (offset === -1) return null;
96
+ if (offset === -1) {
97
+ return null;
98
+ }
94
99
  const start = node.left.range[1] + offset;
95
100
  return fixer.replaceTextRange([start, start + 2], '||');
96
101
  },
@@ -26,7 +26,9 @@ export default {
26
26
  return {
27
27
  CallExpression(node) {
28
28
  const callee = node.callee;
29
- if (callee.type !== 'MemberExpression') return;
29
+ if (callee.type !== 'MemberExpression') {
30
+ return;
31
+ }
30
32
 
31
33
  if (
32
34
  callee.object.type === 'MemberExpression' &&
@@ -38,7 +40,9 @@ export default {
38
40
  ) {
39
41
  const methodName = callee.property.name;
40
42
  const entry = platformFunctionLookup.get(methodName.toLowerCase());
41
- if (!entry) return;
43
+ if (!entry) {
44
+ return;
45
+ }
42
46
 
43
47
  const actual = node.arguments.length;
44
48
 
@@ -14,11 +14,11 @@ export default {
14
14
  fixable: 'code',
15
15
  docs: {
16
16
  description:
17
- "Require concatenating an empty string to Platform.Function.ParseJSON() arguments to prevent 500 errors",
17
+ 'Require concatenating an empty string to Platform.Function.ParseJSON() arguments to prevent 500 errors',
18
18
  },
19
19
  messages: {
20
20
  unsafeArg:
21
- "Platform.Function.ParseJSON() may throw a 500 if the argument is undefined. " +
21
+ 'Platform.Function.ParseJSON() may throw a 500 if the argument is undefined. ' +
22
22
  "Concatenate an empty string to be safe: ParseJSON({{argText}} + '').",
23
23
  },
24
24
  schema: [],
@@ -27,12 +27,18 @@ export default {
27
27
  create(context) {
28
28
  return {
29
29
  CallExpression(node) {
30
- if (!isParseJSONCall(node)) return;
31
- if (node.arguments.length === 0) return;
30
+ if (!isParseJSONCall(node)) {
31
+ return;
32
+ }
33
+ if (node.arguments.length === 0) {
34
+ return;
35
+ }
32
36
 
33
37
  const arg = node.arguments[0];
34
38
 
35
- if (isAlreadySafe(arg)) return;
39
+ if (isAlreadySafe(arg)) {
40
+ return;
41
+ }
36
42
 
37
43
  context.report({
38
44
  node: arg,
@@ -75,10 +81,12 @@ function isParseJSONCall(node) {
75
81
 
76
82
  function isAlreadySafe(arg) {
77
83
  // arg + '' or '' + arg
78
- if (arg.type === 'BinaryExpression' && arg.operator === '+') {
79
- if (isEmptyString(arg.left) || isEmptyString(arg.right)) {
80
- return true;
81
- }
84
+ if (
85
+ arg.type === 'BinaryExpression' &&
86
+ arg.operator === '+' &&
87
+ (isEmptyString(arg.left) || isEmptyString(arg.right))
88
+ ) {
89
+ return true;
82
90
  }
83
91
 
84
92
  // String literal passed directly — already a string
@@ -66,10 +66,7 @@ export default {
66
66
  messageId: 'outdatedVersion',
67
67
  data: { actual: '(none)', expected: expectedVersion },
68
68
  fix(fixer) {
69
- return fixer.insertTextAfter(
70
- arguments_[0],
71
- `, "${expectedVersion}"`,
72
- );
69
+ return fixer.insertTextAfter(arguments_[0], `, "${expectedVersion}"`);
73
70
  },
74
71
  });
75
72
  return;