eslint-plugin-sfmc 2.6.1 → 4.0.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 (49) hide show
  1. package/README.md +77 -65
  2. package/docs/rules/amp/no-mcn-unsupported.md +76 -0
  3. package/docs/rules/amp/no-unknown-function.md +2 -39
  4. package/docs/rules/hbs/helper-arity.md +55 -0
  5. package/docs/rules/hbs/no-mcn-unsupported.md +69 -0
  6. package/docs/rules/hbs/no-unknown-binding.md +49 -0
  7. package/docs/rules/hbs/no-unknown-helper.md +60 -0
  8. package/docs/rules/hbs/no-unsupported-construct.md +65 -0
  9. package/docs/rules/ssjs/no-mcn-unsupported.md +68 -0
  10. package/docs/rules/ssjs/no-unknown-function.md +4 -31
  11. package/package.json +14 -12
  12. package/src/ampscript-parser.js +1 -1
  13. package/src/ampscript-processor.js +22 -16
  14. package/src/handlebars-parser.js +195 -0
  15. package/src/index.js +108 -23
  16. package/src/processor.js +76 -16
  17. package/src/rules/amp/{arg-types.js → argument-types.js} +16 -12
  18. package/src/rules/amp/function-arity.js +9 -9
  19. package/src/rules/amp/no-inline-statement.js +5 -5
  20. package/src/rules/amp/no-loop-counter-assign.js +7 -7
  21. package/src/rules/amp/no-mcn-unsupported.js +102 -0
  22. package/src/rules/amp/no-nested-ampscript-delimiter.js +58 -66
  23. package/src/rules/amp/no-smart-quotes.js +41 -47
  24. package/src/rules/amp/no-unknown-function.js +2 -28
  25. package/src/rules/amp/prefer-attribute-value.js +7 -10
  26. package/src/rules/amp/require-rowcount-check.js +12 -15
  27. package/src/rules/amp/require-variable-declaration.js +3 -3
  28. package/src/rules/hbs/_shared.js +116 -0
  29. package/src/rules/hbs/helper-arity.js +97 -0
  30. package/src/rules/hbs/no-mcn-unsupported.js +164 -0
  31. package/src/rules/hbs/no-unknown-binding.js +74 -0
  32. package/src/rules/hbs/no-unknown-helper.js +79 -0
  33. package/src/rules/hbs/no-unsupported-construct.js +70 -0
  34. package/src/rules/ssjs/cache-loop-length.js +14 -17
  35. package/src/rules/ssjs/no-deprecated-function.js +8 -8
  36. package/src/rules/ssjs/no-hardcoded-credentials.js +3 -6
  37. package/src/rules/ssjs/no-mcn-unsupported.js +182 -0
  38. package/src/rules/ssjs/no-property-call.js +6 -9
  39. package/src/rules/ssjs/no-treatascontent-injection.js +6 -13
  40. package/src/rules/ssjs/no-unavailable-method.js +22 -22
  41. package/src/rules/ssjs/no-unknown-function.js +15 -59
  42. package/src/rules/ssjs/no-unsupported-syntax.js +5 -5
  43. package/src/rules/ssjs/{prefer-parsejson-safe-arg.js → prefer-parsejson-safe-argument.js} +13 -21
  44. package/src/rules/ssjs/require-hasownproperty.js +54 -42
  45. package/src/rules/ssjs/require-platform-load-order.js +1 -1
  46. package/src/rules/ssjs/require-platform-load.js +6 -6
  47. package/src/rules/ssjs/{ssjs-arg-types.js → ssjs-argument-types.js} +59 -46
  48. package/src/rules/ssjs/ssjs-core-method-arity.js +41 -15
  49. /package/src/rules/amp/{no-var-redeclaration.js → no-variable-redeclaration.js} +0 -0
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Rule: ssjs-no-mcn-unsupported
3
+ *
4
+ * Server-Side JavaScript as a whole is not available in Marketing Cloud Next.
5
+ * When targeting MCN, every SSJS API usage must be rewritten in AMPscript or
6
+ * Handlebars. This rule flags all SSJS API surface usage:
7
+ *
8
+ * - Platform.<...>() — any Platform namespace call (incl. Platform.Load)
9
+ * - HTTP.<method>() — any HTTP call
10
+ * - <coreVar>.<method>() — Core Library instance calls
11
+ * - <wsproxyVar>.<method>() — WSProxy instance calls
12
+ * - new Script.Util.WSProxy() — WSProxy construction
13
+ *
14
+ * The `apiVersion` option is accepted for parity with the AMPscript and
15
+ * Handlebars MCN rules, but currently has no effect: no MCN API version
16
+ * supports SSJS, so it always flags all SSJS usage.
17
+ */
18
+
19
+ import { coreObjectNames } from 'ssjs-data';
20
+
21
+ export default {
22
+ meta: {
23
+ type: 'problem',
24
+ docs: {
25
+ description:
26
+ 'Disallow all Server-Side JavaScript API usage, which is unavailable in Marketing Cloud Next',
27
+ },
28
+ messages: {
29
+ ssjsNotSupportedInMcn:
30
+ 'SSJS is not supported in Marketing Cloud Next. Rewrite this code in AMPscript or Handlebars.',
31
+ },
32
+ schema: [
33
+ {
34
+ type: 'object',
35
+ properties: {
36
+ apiVersion: {
37
+ type: 'number',
38
+ description:
39
+ 'Targeted Marketing Cloud Next API version. Accepted for parity with the other MCN rules; currently has no effect because no version supports SSJS.',
40
+ },
41
+ },
42
+ additionalProperties: false,
43
+ },
44
+ ],
45
+ },
46
+
47
+ create(context) {
48
+ // Track variable name → Core Library type name (assigned via TypeName.Init())
49
+ const coreVariables = new Map();
50
+ // Track variable names assigned via new Script.Util.WSProxy()
51
+ const wsproxyVariables = new Set();
52
+
53
+ return {
54
+ VariableDeclaration(node) {
55
+ for (const declaration of node.declarations) {
56
+ if (
57
+ !declaration.init ||
58
+ !declaration.id ||
59
+ declaration.id.type !== 'Identifier'
60
+ ) {
61
+ continue;
62
+ }
63
+ const coreType = getCoreInitType(declaration.init);
64
+ if (coreType) {
65
+ coreVariables.set(declaration.id.name, coreType);
66
+ }
67
+ if (isWSProxyConstructor(declaration.init)) {
68
+ wsproxyVariables.add(declaration.id.name);
69
+ }
70
+ }
71
+ },
72
+
73
+ AssignmentExpression(node) {
74
+ if (node.left.type !== 'Identifier') {
75
+ return;
76
+ }
77
+ const coreType = getCoreInitType(node.right);
78
+ if (coreType) {
79
+ coreVariables.set(node.left.name, coreType);
80
+ }
81
+ if (isWSProxyConstructor(node.right)) {
82
+ wsproxyVariables.add(node.left.name);
83
+ }
84
+ },
85
+
86
+ NewExpression(node) {
87
+ if (isWSProxyConstructor(node)) {
88
+ context.report({ node: node.callee, messageId: 'ssjsNotSupportedInMcn' });
89
+ }
90
+ },
91
+
92
+ CallExpression(node) {
93
+ const callee = node.callee;
94
+ if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier') {
95
+ return;
96
+ }
97
+
98
+ // Traverse the object chain to find the root identifier
99
+ // (handles de.Rows.Retrieve() where root is `de`,
100
+ // and Platform.Function.X() where root is `Platform`).
101
+ let rootNode = callee.object;
102
+ while (rootNode.type === 'MemberExpression') {
103
+ rootNode = rootNode.object;
104
+ }
105
+ if (rootNode.type !== 'Identifier') {
106
+ return;
107
+ }
108
+ const rootName = rootNode.name;
109
+
110
+ const isSfmcApiCall =
111
+ rootName === 'Platform' ||
112
+ rootName === 'HTTP' ||
113
+ coreVariables.has(rootName) ||
114
+ wsproxyVariables.has(rootName);
115
+
116
+ if (isSfmcApiCall) {
117
+ context.report({
118
+ node: callee.property,
119
+ messageId: 'ssjsNotSupportedInMcn',
120
+ });
121
+ }
122
+ },
123
+ };
124
+ },
125
+ };
126
+
127
+ /**
128
+ * If `node` is a Core Library Init call (e.g. `DataExtension.Init("key")`),
129
+ * return the Core Library type name; otherwise return null.
130
+ *
131
+ * @param {import('eslint').Rule.Node} node - AST node to inspect
132
+ * @returns {string | null} Core Library type name, or null when not a Core Library Init call
133
+ */
134
+ function getCoreInitType(node) {
135
+ if (!node || node.type !== 'CallExpression') {
136
+ return null;
137
+ }
138
+ const callee = node.callee;
139
+ if (callee.type !== 'MemberExpression') {
140
+ return null;
141
+ }
142
+ if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
143
+ return null;
144
+ }
145
+ if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
146
+ return callee.object.name;
147
+ }
148
+ if (
149
+ callee.object.type === 'MemberExpression' &&
150
+ callee.object.object.type === 'Identifier' &&
151
+ callee.object.property.type === 'Identifier'
152
+ ) {
153
+ const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
154
+ if (coreObjectNames.has(fullName)) {
155
+ return fullName;
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+
161
+ /**
162
+ * Returns true if `node` is `new Script.Util.WSProxy()`.
163
+ *
164
+ * @param {import('eslint').Rule.Node} node - AST node to inspect
165
+ * @returns {boolean} true when the node is a WSProxy constructor call
166
+ */
167
+ function isWSProxyConstructor(node) {
168
+ if (!node || node.type !== 'NewExpression') {
169
+ return false;
170
+ }
171
+ const c = node.callee;
172
+ return (
173
+ c.type === 'MemberExpression' &&
174
+ c.property.type === 'Identifier' &&
175
+ c.property.name === 'WSProxy' &&
176
+ c.object.type === 'MemberExpression' &&
177
+ c.object.property.type === 'Identifier' &&
178
+ c.object.property.name === 'Util' &&
179
+ c.object.object.type === 'Identifier' &&
180
+ c.object.object.name === 'Script'
181
+ );
182
+ }
@@ -38,10 +38,7 @@ function canReplaceCallWithAssignment(node) {
38
38
  if (parent.type === 'ExpressionStatement' && parent.expression === node) {
39
39
  return true;
40
40
  }
41
- if (parent.type === 'SequenceExpression' && parent.expressions.includes(node)) {
42
- return true;
43
- }
44
- return false;
41
+ return Boolean(parent.type === 'SequenceExpression' && parent.expressions.includes(node));
45
42
  }
46
43
 
47
44
  export default {
@@ -83,11 +80,11 @@ export default {
83
80
  }
84
81
 
85
82
  const ns = callee.object.property.name.toLowerCase();
86
- const propName = callee.property.name.toLowerCase();
83
+ const propertyName = callee.property.name.toLowerCase();
87
84
  const displayNs = callee.object.property.name;
88
85
  const displayName = callee.property.name;
89
86
 
90
- if (ns === 'response' && RESPONSE_PROPERTIES.has(propName)) {
87
+ if (ns === 'response' && RESPONSE_PROPERTIES.has(propertyName)) {
91
88
  if (node.arguments.length === 0) {
92
89
  context.report({
93
90
  node,
@@ -106,10 +103,10 @@ export default {
106
103
  if (!canReplaceCallWithAssignment(node)) {
107
104
  return null;
108
105
  }
109
- const argText = sourceCode.getText(node.arguments[0]);
106
+ const argumentText = sourceCode.getText(node.arguments[0]);
110
107
  return fixer.replaceText(
111
108
  node,
112
- `${sourceCode.getText(callee)} = ${argText}`,
109
+ `${sourceCode.getText(callee)} = ${argumentText}`,
113
110
  );
114
111
  },
115
112
  });
@@ -120,7 +117,7 @@ export default {
120
117
  data: { ns: displayNs, name: displayName },
121
118
  });
122
119
  }
123
- } else if (ns === 'request' && REQUEST_PROPERTIES.has(propName)) {
120
+ } else if (ns === 'request' && REQUEST_PROPERTIES.has(propertyName)) {
124
121
  if (node.arguments.length === 0) {
125
122
  context.report({
126
123
  node,
@@ -35,9 +35,9 @@ export default {
35
35
  return;
36
36
  }
37
37
 
38
- const arg = node.arguments[0];
39
- if (containsConcatenation(arg)) {
40
- context.report({ node: arg, messageId: 'injection' });
38
+ const argument = node.arguments[0];
39
+ if (containsConcatenation(argument)) {
40
+ context.report({ node: argument, messageId: 'injection' });
41
41
  }
42
42
  },
43
43
  };
@@ -53,7 +53,7 @@ function isTreatAsContentCall(node) {
53
53
  }
54
54
 
55
55
  // Platform.Function.TreatAsContent(...)
56
- if (
56
+ return (
57
57
  callee.type === 'MemberExpression' &&
58
58
  callee.property.type === 'Identifier' &&
59
59
  callee.property.name === 'TreatAsContent' &&
@@ -62,19 +62,12 @@ function isTreatAsContentCall(node) {
62
62
  callee.object.property.name === 'Function' &&
63
63
  callee.object.object.type === 'Identifier' &&
64
64
  callee.object.object.name === 'Platform'
65
- ) {
66
- return true;
67
- }
68
-
69
- return false;
65
+ );
70
66
  }
71
67
 
72
68
  function containsConcatenation(node) {
73
69
  if (node.type === 'BinaryExpression' && node.operator === '+') {
74
70
  return true;
75
71
  }
76
- if (node.type === 'TemplateLiteral' && node.expressions.length > 0) {
77
- return true;
78
- }
79
- return false;
72
+ return node.type === 'TemplateLiteral' && node.expressions.length > 0;
80
73
  }
@@ -98,8 +98,8 @@ export default {
98
98
 
99
99
  function buildSuggestFix(entry) {
100
100
  return function fix(fixer) {
101
- const src = context.sourceCode;
102
- const end = src.ast.range[1];
101
+ const source = context.sourceCode;
102
+ const end = source.ast.range[1];
103
103
  return fixer.insertTextAfterRange([end, end], '\n\n' + entry.polyfill);
104
104
  };
105
105
  }
@@ -113,27 +113,27 @@ export default {
113
113
  return;
114
114
  }
115
115
 
116
- const obj = left.object;
117
- const prop = left.property;
118
- if (prop.type !== 'Identifier') {
116
+ const object = left.object;
117
+ const property = left.property;
118
+ if (property.type !== 'Identifier') {
119
119
  return;
120
120
  }
121
121
 
122
122
  // Array.X = ...
123
- if (obj.type === 'Identifier' && obj.name === 'Array') {
124
- alreadyPolyfilled.add(prop.name);
123
+ if (object.type === 'Identifier' && object.name === 'Array') {
124
+ alreadyPolyfilled.add(property.name);
125
125
  return;
126
126
  }
127
127
 
128
128
  // Array.prototype.X = ... or String.prototype.X = ...
129
129
  if (
130
- obj.type === 'MemberExpression' &&
131
- obj.object.type === 'Identifier' &&
132
- (obj.object.name === 'Array' || obj.object.name === 'String') &&
133
- obj.property.type === 'Identifier' &&
134
- obj.property.name === 'prototype'
130
+ object.type === 'MemberExpression' &&
131
+ object.object.type === 'Identifier' &&
132
+ (object.object.name === 'Array' || object.object.name === 'String') &&
133
+ object.property.type === 'Identifier' &&
134
+ object.property.name === 'prototype'
135
135
  ) {
136
- alreadyPolyfilled.add(prop.name);
136
+ alreadyPolyfilled.add(property.name);
137
137
  }
138
138
  },
139
139
 
@@ -143,12 +143,12 @@ export default {
143
143
  return;
144
144
  }
145
145
 
146
- const prop = callee.property;
147
- if (prop.type !== 'Identifier') {
146
+ const property = callee.property;
147
+ if (property.type !== 'Identifier') {
148
148
  return;
149
149
  }
150
150
 
151
- const methodName = prop.name;
151
+ const methodName = property.name;
152
152
  const receiver = callee.object;
153
153
 
154
154
  const lowerMethod = methodName.toLowerCase();
@@ -163,7 +163,7 @@ export default {
163
163
  return;
164
164
  }
165
165
  const entry = polyfillByStaticName.get(methodName);
166
- pendingReports.push({ node: prop, entry });
166
+ pendingReports.push({ node: property, entry });
167
167
  return;
168
168
  }
169
169
 
@@ -176,7 +176,7 @@ export default {
176
176
  const entry = knownUnsupportedByStaticName.get(lowerMethod);
177
177
  const ownerBase = entry.owner.replace(/\.prototype$/, '');
178
178
  if (receiver.name === ownerBase && !ignored.has(methodName)) {
179
- pendingReports.push({ node: prop, entry, noPolyfill: true });
179
+ pendingReports.push({ node: property, entry, noPolyfill: true });
180
180
  return;
181
181
  }
182
182
  }
@@ -187,13 +187,13 @@ export default {
187
187
  return;
188
188
  }
189
189
 
190
- const entry = polyfillByPrototypeName.get(methodName);
191
-
192
190
  // Skip known SFMC top-level objects to avoid false positives.
193
191
  if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name)) {
194
192
  return;
195
193
  }
196
194
 
195
+ const entry = polyfillByPrototypeName.get(methodName);
196
+
197
197
  // For methods that also exist on String.prototype in ES3
198
198
  // (indexOf, lastIndexOf), only flag when the receiver is
199
199
  // a literal array or the call is chained from an array method
@@ -211,7 +211,7 @@ export default {
211
211
  }
212
212
  }
213
213
 
214
- pendingReports.push({ node: prop, entry });
214
+ pendingReports.push({ node: property, entry });
215
215
  return;
216
216
  }
217
217
 
@@ -225,7 +225,7 @@ export default {
225
225
  return;
226
226
  }
227
227
  const entry = knownUnsupportedByPrototypeName.get(lowerMethod);
228
- pendingReports.push({ node: prop, entry, noPolyfill: true });
228
+ pendingReports.push({ node: property, entry, noPolyfill: true });
229
229
  }
230
230
  },
231
231
 
@@ -49,29 +49,11 @@ export default {
49
49
  unknownCoreMethod:
50
50
  "'{{method}}' is not a known method of {{objectType}}. Available methods: {{available}}.",
51
51
  unknownWsproxyMethod: "'{{name}}' is not a recognized WSProxy method.",
52
- ssjsNotSupportedInMcn:
53
- 'SSJS is not supported in Marketing Cloud Next. Use AMPscript instead.',
54
52
  },
55
- schema: [
56
- {
57
- type: 'object',
58
- properties: {
59
- target: {
60
- type: 'string',
61
- enum: ['engagement', 'next'],
62
- description:
63
- "Target platform. Set to 'next' to flag all SSJS API calls as unsupported in Marketing Cloud Next.",
64
- },
65
- },
66
- additionalProperties: false,
67
- },
68
- ],
53
+ schema: [],
69
54
  },
70
55
 
71
56
  create(context) {
72
- const options = context.options[0] ?? {};
73
- const targetNext = options.target === 'next';
74
-
75
57
  // Track variable name → Core Library type name (assigned via TypeName.Init())
76
58
  const coreVariables = new Map();
77
59
  // Track variable names assigned via new Script.Util.WSProxy()
@@ -79,16 +61,20 @@ export default {
79
61
 
80
62
  return {
81
63
  VariableDeclaration(node) {
82
- for (const decl of node.declarations) {
83
- if (!decl.init || !decl.id || decl.id.type !== 'Identifier') {
64
+ for (const declaration of node.declarations) {
65
+ if (
66
+ !declaration.init ||
67
+ !declaration.id ||
68
+ declaration.id.type !== 'Identifier'
69
+ ) {
84
70
  continue;
85
71
  }
86
- const coreType = getCoreInitType(decl.init);
72
+ const coreType = getCoreInitType(declaration.init);
87
73
  if (coreType) {
88
- coreVariables.set(decl.id.name, coreType);
74
+ coreVariables.set(declaration.id.name, coreType);
89
75
  }
90
- if (isWSProxyConstructor(decl.init)) {
91
- wsproxyVariables.add(decl.id.name);
76
+ if (isWSProxyConstructor(declaration.init)) {
77
+ wsproxyVariables.add(declaration.id.name);
92
78
  }
93
79
  }
94
80
  },
@@ -118,36 +104,6 @@ export default {
118
104
 
119
105
  const methodName = property.name;
120
106
 
121
- // ── MCN target: flag all SSJS API calls as unsupported ────────
122
- if (targetNext) {
123
- // Only report on Platform.<NS>.<method>(), HTTP.<method>(),
124
- // Core Library instance calls, or WSProxy calls — not bare JS.
125
- const isPlatformCall =
126
- callee.object.type === 'MemberExpression' &&
127
- callee.object.object.type === 'Identifier' &&
128
- callee.object.object.name === 'Platform';
129
- const isHttpCall =
130
- callee.object.type === 'Identifier' && callee.object.name === 'HTTP';
131
-
132
- // Traverse the object chain to find the root identifier
133
- // (handles de.Rows.Retrieve() where root is `de`)
134
- let rootNode = callee.object;
135
- while (rootNode.type === 'MemberExpression') {
136
- rootNode = rootNode.object;
137
- }
138
- const isInstanceCall =
139
- rootNode.type === 'Identifier' &&
140
- (coreVariables.has(rootNode.name) || wsproxyVariables.has(rootNode.name));
141
-
142
- if (isPlatformCall || isHttpCall || isInstanceCall) {
143
- context.report({
144
- node: property,
145
- messageId: 'ssjsNotSupportedInMcn',
146
- });
147
- return;
148
- }
149
- }
150
-
151
107
  // ── Platform.<NS>.<method>() ──────────────────────────────────
152
108
  if (
153
109
  callee.object.type === 'MemberExpression' &&
@@ -189,10 +145,10 @@ export default {
189
145
  // Core library: var de = DataExtension.Init("key"); de.Foo();
190
146
  const coreType = coreVariables.get(objectName);
191
147
  if (coreType) {
192
- const objectDef = coreObjectLookup.get(coreType);
193
- if (objectDef) {
148
+ const objectDefinition = coreObjectLookup.get(coreType);
149
+ if (objectDefinition) {
194
150
  const knownMethods = new Set(
195
- objectDef.methods.map((m) => m.toLowerCase()),
151
+ objectDefinition.methods.map((m) => m.toLowerCase()),
196
152
  );
197
153
  if (!knownMethods.has(methodName.toLowerCase())) {
198
154
  context.report({
@@ -201,7 +157,7 @@ export default {
201
157
  data: {
202
158
  method: methodName,
203
159
  objectType: coreType,
204
- available: objectDef.methods.join(', '),
160
+ available: objectDefinition.methods.join(', '),
205
161
  },
206
162
  });
207
163
  }
@@ -21,8 +21,8 @@ const AUTO_FIX = {
21
21
 
22
22
  function nullishCoalescingFix(context, node) {
23
23
  return (fixer) => {
24
- const src = context.sourceCode.getText();
25
- const between = src.slice(node.left.range[1], node.right.range[0]);
24
+ const source = context.sourceCode.getText();
25
+ const between = source.slice(node.left.range[1], node.right.range[0]);
26
26
  const offset = between.indexOf('??');
27
27
  if (offset === -1) {
28
28
  return null;
@@ -34,9 +34,9 @@ function nullishCoalescingFix(context, node) {
34
34
 
35
35
  function directObjectReturnFix(context, node) {
36
36
  return (fixer) => {
37
- const objText = context.sourceCode.getText(node.argument);
37
+ const objectText = context.sourceCode.getText(node.argument);
38
38
  const indent = ' '.repeat(node.loc.start.column);
39
- return fixer.replaceText(node, `var _result = ${objText};\n${indent}return _result;`);
39
+ return fixer.replaceText(node, `var _result = ${objectText};\n${indent}return _result;`);
40
40
  };
41
41
  }
42
42
 
@@ -92,7 +92,7 @@ export default {
92
92
  },
93
93
  };
94
94
 
95
- if (entry.feature in AUTO_FIX) {
95
+ if (Object.hasOwn(AUTO_FIX, entry.feature)) {
96
96
  report.fix = AUTO_FIX[entry.feature](node);
97
97
  } else if (entry.feature === 'NullishCoalescing') {
98
98
  report.fix = nullishCoalescingFix(context, node);
@@ -34,19 +34,19 @@ export default {
34
34
  return;
35
35
  }
36
36
 
37
- const arg = node.arguments[0];
37
+ const argument = node.arguments[0];
38
38
 
39
- if (isAlreadySafe(arg)) {
39
+ if (isAlreadySafe(argument)) {
40
40
  return;
41
41
  }
42
42
 
43
43
  context.report({
44
- node: arg,
44
+ node: argument,
45
45
  messageId: 'unsafeArg',
46
- data: { argText: context.sourceCode.getText(arg) },
46
+ data: { argText: context.sourceCode.getText(argument) },
47
47
  fix(fixer) {
48
- const argText = context.sourceCode.getText(arg);
49
- return fixer.replaceText(arg, `${argText} + ''`);
48
+ const argumentText = context.sourceCode.getText(argument);
49
+ return fixer.replaceText(argument, `${argumentText} + ''`);
50
50
  },
51
51
  });
52
52
  },
@@ -63,7 +63,7 @@ function isParseJSONCall(node) {
63
63
  }
64
64
 
65
65
  // Platform.Function.ParseJSON(...)
66
- if (
66
+ return (
67
67
  callee.type === 'MemberExpression' &&
68
68
  callee.property.type === 'Identifier' &&
69
69
  callee.property.name === 'ParseJSON' &&
@@ -72,29 +72,21 @@ function isParseJSONCall(node) {
72
72
  callee.object.property.name === 'Function' &&
73
73
  callee.object.object.type === 'Identifier' &&
74
74
  callee.object.object.name === 'Platform'
75
- ) {
76
- return true;
77
- }
78
-
79
- return false;
75
+ );
80
76
  }
81
77
 
82
- function isAlreadySafe(arg) {
78
+ function isAlreadySafe(argument) {
83
79
  // arg + '' or '' + arg
84
80
  if (
85
- arg.type === 'BinaryExpression' &&
86
- arg.operator === '+' &&
87
- (isEmptyString(arg.left) || isEmptyString(arg.right))
81
+ argument.type === 'BinaryExpression' &&
82
+ argument.operator === '+' &&
83
+ (isEmptyString(argument.left) || isEmptyString(argument.right))
88
84
  ) {
89
85
  return true;
90
86
  }
91
87
 
92
88
  // String literal passed directly — already a string
93
- if (arg.type === 'Literal' && typeof arg.value === 'string') {
94
- return true;
95
- }
96
-
97
- return false;
89
+ return argument.type === 'Literal' && typeof argument.value === 'string';
98
90
  }
99
91
 
100
92
  function isEmptyString(node) {