eslint-plugin-sfmc 2.5.0 → 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.
|
|
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.10.
|
|
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
|
|
5
|
-
* legacy
|
|
6
|
-
*
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
// ──
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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:
|
|
263
|
+
data: { owner: entry.owner, method: methodName },
|
|
198
264
|
suggest: [
|
|
199
265
|
{
|
|
200
266
|
messageId: 'addPolyfill',
|
|
201
|
-
data: { owner: entry.owner, method:
|
|
267
|
+
data: { owner: entry.owner, method: methodName },
|
|
202
268
|
fix: buildSuggestFix(entry),
|
|
203
269
|
},
|
|
204
270
|
],
|