eslint-plugin-sfmc 2.4.1 → 2.6.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-sfmc",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "ESLint plugin for Salesforce Marketing Cloud — AMPscript and Server-Side JavaScript (SSJS)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "ampscript-data": "^2.0.3",
36
36
  "ampscript-parser": "^0.1.3",
37
- "ssjs-data": "^0.9.0"
37
+ "ssjs-data": "^0.10.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "eslint": ">=9.0.0"
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * Rule: no-unavailable-method
3
3
  *
4
- * Flags calls to Array methods that are either missing or broken in SFMC's
5
- * legacy ECMAScript engine (Jint/ECMAScript 3) and suggests inserting a
6
- * polyfill at the end of the file.
4
+ * Flags calls to ECMAScript built-in methods that are either missing or broken
5
+ * in SFMC's legacy engine (Jint/ECMAScript 3).
6
+ *
7
+ * Two catalogs from ssjs-data feed this rule:
8
+ *
9
+ * 1. POLYFILLABLE_METHODS — a shipped ES3-safe polyfill exists. The report
10
+ * carries a suggestion that inserts the polyfill at the end of the file.
11
+ * 2. KNOWN_UNSUPPORTED — no polyfill is feasible. The report has no fix; the
12
+ * message carries the ssjs-data `suggestion` (e.g. use Platform.Function.X).
7
13
  *
8
14
  * - category 'unavailable': method does not exist; calling it throws a runtime error.
9
15
  * - category 'broken': method exists natively but returns incorrect results.
@@ -13,7 +19,12 @@
13
19
  * should verify placement before the first call (or load via Content Block).
14
20
  */
15
21
 
16
- import { polyfillByPrototypeName, polyfillByStaticName } from 'ssjs-data';
22
+ import {
23
+ polyfillByPrototypeName,
24
+ polyfillByStaticName,
25
+ knownUnsupportedByPrototypeName,
26
+ knownUnsupportedByStaticName,
27
+ } from 'ssjs-data';
17
28
 
18
29
  // SFMC-specific top-level objects whose methods must never be flagged even
19
30
  // when their method names collide with Array.prototype (e.g. Platform.find).
@@ -38,7 +49,7 @@ export default {
38
49
  hasSuggestions: true,
39
50
  docs: {
40
51
  description:
41
- 'Flag Array methods unavailable or broken in SFMC SSJS and suggest polyfills',
52
+ 'Flag ECMAScript built-in methods unavailable or broken in SFMC SSJS and suggest polyfills',
42
53
  },
43
54
  messages: {
44
55
  unavailable:
@@ -48,6 +59,10 @@ export default {
48
59
  "'{{owner}}.{{method}}' exists in SFMC SSJS but produces incorrect results. " +
49
60
  'Add a polyfill to get the correct behavior.',
50
61
  addPolyfill: "Insert '{{owner}}.{{method}}' polyfill at end of file",
62
+ unavailableNoPolyfill:
63
+ "'{{owner}}.{{method}}' is not available in SFMC SSJS (ECMAScript 3). {{suggestion}}",
64
+ brokenNoPolyfill:
65
+ "'{{owner}}.{{method}}' exists in SFMC SSJS but produces incorrect results. {{suggestion}}",
51
66
  },
52
67
  schema: [
53
68
  {
@@ -136,7 +151,9 @@ export default {
136
151
  const methodName = prop.name;
137
152
  const receiver = callee.object;
138
153
 
139
- // ── Static Array methods: Array.isArray(), Array.of() ──────────
154
+ const lowerMethod = methodName.toLowerCase();
155
+
156
+ // ── Static methods with a polyfill: Array.isArray(), Array.of() ─
140
157
  if (
141
158
  receiver.type === 'Identifier' &&
142
159
  receiver.name === 'Array' &&
@@ -150,55 +167,104 @@ export default {
150
167
  return;
151
168
  }
152
169
 
153
- // ── Prototype methods ─────────────────────────────────────────
154
- if (!polyfillByPrototypeName.has(methodName)) {
155
- return;
156
- }
157
- if (ignored.has(methodName)) {
158
- return;
170
+ // ── Static methods with NO polyfill: JSON.parse, Object.keys, … ─
171
+ // The owner prefix is explicit, so match owner.member precisely.
172
+ if (
173
+ receiver.type === 'Identifier' &&
174
+ knownUnsupportedByStaticName.has(lowerMethod)
175
+ ) {
176
+ const entry = knownUnsupportedByStaticName.get(lowerMethod);
177
+ const ownerBase = entry.owner.replace(/\.prototype$/, '');
178
+ if (receiver.name === ownerBase && !ignored.has(methodName)) {
179
+ pendingReports.push({ node: prop, entry, noPolyfill: true });
180
+ return;
181
+ }
159
182
  }
160
183
 
161
- const entry = polyfillByPrototypeName.get(methodName);
184
+ // ── Prototype methods with a polyfill ──────────────────────────
185
+ if (polyfillByPrototypeName.has(methodName)) {
186
+ if (ignored.has(methodName)) {
187
+ return;
188
+ }
189
+
190
+ const entry = polyfillByPrototypeName.get(methodName);
162
191
 
163
- // Skip known SFMC top-level objects to avoid false positives.
164
- if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name)) {
192
+ // Skip known SFMC top-level objects to avoid false positives.
193
+ if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name)) {
194
+ return;
195
+ }
196
+
197
+ // For methods that also exist on String.prototype in ES3
198
+ // (indexOf, lastIndexOf), only flag when the receiver is
199
+ // a literal array or the call is chained from an array method
200
+ // we already know about — otherwise we'd falsely flag string usage.
201
+ if (entry.ambiguousWithString) {
202
+ const isDefinitelyArray =
203
+ receiver.type === 'ArrayExpression' ||
204
+ // arr.filter(...).indexOf(...) — left side is a CallExpression
205
+ // whose callee refers to another Array method
206
+ (receiver.type === 'CallExpression' &&
207
+ receiver.callee.type === 'MemberExpression' &&
208
+ polyfillByPrototypeName.has(receiver.callee.property.name));
209
+ if (!isDefinitelyArray) {
210
+ return;
211
+ }
212
+ }
213
+
214
+ pendingReports.push({ node: prop, entry });
165
215
  return;
166
216
  }
167
217
 
168
- // For methods that also exist on String.prototype in ES3
169
- // (indexOf, lastIndexOf), only flag when the receiver is
170
- // a literal array or the call is chained from an array method
171
- // we already know about — otherwise we'd falsely flag string usage.
172
- if (entry.ambiguousWithString) {
173
- const isDefinitelyArray =
174
- receiver.type === 'ArrayExpression' ||
175
- // arr.filter(...).indexOf(...) — left side is a CallExpression
176
- // whose callee refers to another Array method
177
- (receiver.type === 'CallExpression' &&
178
- receiver.callee.type === 'MemberExpression' &&
179
- polyfillByPrototypeName.has(receiver.callee.property.name));
180
- if (!isDefinitelyArray) {
218
+ // ── Prototype methods with NO polyfill: .trimStart(), .flat(),
219
+ if (knownUnsupportedByPrototypeName.has(lowerMethod)) {
220
+ if (ignored.has(methodName)) {
181
221
  return;
182
222
  }
223
+ // Skip known SFMC top-level objects to avoid false positives.
224
+ if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name)) {
225
+ return;
226
+ }
227
+ const entry = knownUnsupportedByPrototypeName.get(lowerMethod);
228
+ pendingReports.push({ node: prop, entry, noPolyfill: true });
183
229
  }
184
-
185
- pendingReports.push({ node: prop, entry });
186
230
  },
187
231
 
188
232
  'Program:exit'() {
189
- for (const { node, entry } of pendingReports) {
190
- if (isAlreadyPolyfilled(entry.method)) {
233
+ for (const { node, entry, noPolyfill } of pendingReports) {
234
+ // KNOWN_UNSUPPORTED entries use `member`; polyfillable use `method`.
235
+ const methodName = entry.method ?? entry.member;
236
+ const ownerDisplay = entry.owner.replace(/\.prototype$/, '');
237
+
238
+ if (noPolyfill) {
239
+ // No polyfill is feasible — report with the ssjs-data suggestion
240
+ // and no auto-insert fix.
241
+ context.report({
242
+ node,
243
+ messageId:
244
+ entry.category === 'broken'
245
+ ? 'brokenNoPolyfill'
246
+ : 'unavailableNoPolyfill',
247
+ data: {
248
+ owner: ownerDisplay,
249
+ method: methodName,
250
+ suggestion: entry.suggestion,
251
+ },
252
+ });
253
+ continue;
254
+ }
255
+
256
+ if (isAlreadyPolyfilled(methodName)) {
191
257
  continue;
192
258
  }
193
259
 
194
260
  context.report({
195
261
  node,
196
262
  messageId: entry.category,
197
- data: { owner: entry.owner, method: entry.method },
263
+ data: { owner: entry.owner, method: methodName },
198
264
  suggest: [
199
265
  {
200
266
  messageId: 'addPolyfill',
201
- data: { owner: entry.owner, method: entry.method },
267
+ data: { owner: entry.owner, method: methodName },
202
268
  fix: buildSuggestFix(entry),
203
269
  },
204
270
  ],