eslint-plugin-sfmc 4.2.0 → 4.4.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/README.md +2 -1
- package/docs/rules/ssjs/http-property-value.md +71 -0
- package/docs/rules/ssjs/no-deprecated-function.md +4 -2
- package/docs/rules/ssjs/no-nonexistent-global.md +37 -0
- package/docs/rules/ssjs/no-unavailable-method.md +2 -1
- package/package.json +2 -2
- package/src/index.js +10 -0
- package/src/rules/ssjs/http-property-value.js +232 -0
- package/src/rules/ssjs/no-deprecated-function.js +29 -1
- package/src/rules/ssjs/no-nonexistent-global.js +63 -0
- package/src/rules/ssjs/no-unavailable-method.js +20 -6
package/README.md
CHANGED
|
@@ -128,7 +128,8 @@ export default [...sfmc.configs['recommended-next'], ...sfmc.configs['embedded-n
|
|
|
128
128
|
| [`sfmc/ssjs-no-unsupported-syntax`](docs/rules/ssjs/no-unsupported-syntax.md) | `error` | Flag ES6+ syntax not supported by SFMC |
|
|
129
129
|
| [`sfmc/ssjs-no-unknown-function`](docs/rules/ssjs/no-unknown-function.md) | `error` | Disallow unknown methods on Platform.\*, HTTP, Core Library, and WSProxy |
|
|
130
130
|
| [`sfmc/ssjs-no-mcn-unsupported`](docs/rules/ssjs/no-mcn-unsupported.md) | off (`error` in `-next`) | Flag all SSJS API usage as unsupported in Marketing Cloud Next |
|
|
131
|
-
| [`sfmc/ssjs-no-deprecated-function`](docs/rules/ssjs/no-deprecated-function.md) | `error` | Flag use of deprecated SFMC SSJS APIs (e.g. ContentArea,
|
|
131
|
+
| [`sfmc/ssjs-no-deprecated-function`](docs/rules/ssjs/no-deprecated-function.md) | `error` | Flag use of deprecated SFMC SSJS APIs (e.g. ContentArea, ErrorUtil) |
|
|
132
|
+
| [`sfmc/ssjs-no-nonexistent-global`](docs/rules/ssjs/no-nonexistent-global.md) | `error` | Flag documented SSJS globals that throw ReferenceError at runtime |
|
|
132
133
|
| [`sfmc/ssjs-no-property-call`](docs/rules/ssjs/no-property-call.md) | `error` | Disallow calling Platform.Request/Response properties as functions |
|
|
133
134
|
| [`sfmc/ssjs-no-clr-header-access`](docs/rules/ssjs/no-clr-header-access.md) | `error` | Disallow CLR-unsafe reads of `HttpResponse.headers`; read via `for..in` |
|
|
134
135
|
| [`sfmc/ssjs-require-string-clr-content`](docs/rules/ssjs/require-string-clr-content.md) | `error` | Require wrapping `HttpResponse.content` with `String()` before use |
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# `sfmc/ssjs-http-property-value`
|
|
2
|
+
|
|
3
|
+
Flags literal assignments to writable `Script.Util.HttpRequest` / `Script.Util.HttpGet`
|
|
4
|
+
instance properties whose value violates the property's documented, runtime-confirmed
|
|
5
|
+
constraint (allowed enum, integer-ness, or minimum).
|
|
6
|
+
|
|
7
|
+
Several request properties only accept a fixed set of values. Assigning something outside
|
|
8
|
+
that set throws or silently misbehaves in the SFMC SSJS engine:
|
|
9
|
+
|
|
10
|
+
| Property | Allowed values |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `method` | `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
|
|
13
|
+
| `emptyContentHandling` | `0`, `1`, `2` |
|
|
14
|
+
| `retries` | non-negative integer |
|
|
15
|
+
| `timeout` | non-negative integer |
|
|
16
|
+
|
|
17
|
+
The allowed values live in [`ssjs-data`](../../../..) (`SCRIPT_UTIL_REQUEST_PROPERTIES` /
|
|
18
|
+
`SCRIPT_UTIL_HTTPGET_PROPERTIES` → `valueConstraint`), so this rule and the VS Code /
|
|
19
|
+
Cursor diagnostic (`ssjs/invalid-http-property-value`) share one source of truth.
|
|
20
|
+
|
|
21
|
+
## Detection
|
|
22
|
+
|
|
23
|
+
The rule tracks data flow, so it only fires on a genuine request object:
|
|
24
|
+
|
|
25
|
+
1. A **request** variable is one assigned from `new Script.Util.HttpRequest(...)` or
|
|
26
|
+
`Script.Util.HttpGet(...)`.
|
|
27
|
+
2. An assignment `<requestVar>.<prop> = <literal>` where `<prop>` has a `valueConstraint`
|
|
28
|
+
and the right-hand side is a **literal** (string, number, or a negative numeric like
|
|
29
|
+
`-2.45`) is validated against the constraint.
|
|
30
|
+
|
|
31
|
+
Only literal values are checked. Variable / expression assignments cannot be verified
|
|
32
|
+
statically and are left alone to avoid false positives.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
### ❌ Incorrect
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
var req = new Script.Util.HttpRequest("https://api.example.com/data");
|
|
40
|
+
|
|
41
|
+
req.emptyContentHandling = 5; // ← allowed: 0 | 1 | 2
|
|
42
|
+
req.retries = -2.45; // ← must be a non-negative integer
|
|
43
|
+
req.method = 'POT'; // ← allowed: GET | POST | PUT | PATCH | DELETE
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### ✅ Correct
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
var req = new Script.Util.HttpRequest("https://api.example.com/data");
|
|
50
|
+
|
|
51
|
+
req.emptyContentHandling = 1;
|
|
52
|
+
req.retries = 3;
|
|
53
|
+
req.method = 'POST';
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Fix (suggestions)
|
|
57
|
+
|
|
58
|
+
For enum violations the rule offers a suggestion per allowed value (e.g. replace `'POT'`
|
|
59
|
+
with `'GET'`, `'POST'`, …). Numeric violations are reported without an automatic
|
|
60
|
+
replacement, since there is no single unambiguous valid value.
|
|
61
|
+
|
|
62
|
+
The same diagnostic and quick-fix are available in the VS Code / Cursor extension
|
|
63
|
+
(`ssjs/invalid-http-property-value`).
|
|
64
|
+
|
|
65
|
+
## Rule details
|
|
66
|
+
|
|
67
|
+
- **Type:** `problem`
|
|
68
|
+
- **Fixable:** No (offers suggestions)
|
|
69
|
+
- **Recommended:** Yes (`error`)
|
|
70
|
+
- **Strict:** Yes (`error`)
|
|
71
|
+
- **MCN:** Off (SSJS is not supported on Marketing Cloud Next)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# `sfmc/ssjs-no-deprecated-function`
|
|
2
2
|
|
|
3
|
-
Flags calls to deprecated SFMC SSJS APIs
|
|
4
|
-
which has been retired and no longer allows creating or updating content
|
|
3
|
+
Flags calls to deprecated SFMC SSJS APIs, chiefly the **Content Areas** feature,
|
|
4
|
+
which has been retired and no longer allows creating or updating content, plus the
|
|
5
|
+
legacy `ErrorUtil` helper that only exists under the oldest Core version.
|
|
5
6
|
|
|
6
7
|
## What is flagged
|
|
7
8
|
|
|
@@ -16,6 +17,7 @@ which has been retired and no longer allows creating or updating content.
|
|
|
16
17
|
| `ContentAreaObj.Retrieve(…)` | ContentAreaObj class is deprecated |
|
|
17
18
|
| `<contentAreaVar>.Update(…)` | Instance method on a deprecated ContentAreaObj variable |
|
|
18
19
|
| `<contentAreaVar>.Remove()` | Instance method on a deprecated ContentAreaObj variable |
|
|
20
|
+
| `ErrorUtil.ThrowWSProxyError(…)` | Only exists under `Platform.Load("Core", "1")`; undefined in newer Core versions — check `result.Status` and `throw new Error(…)` instead |
|
|
19
21
|
|
|
20
22
|
## Examples
|
|
21
23
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# `sfmc/ssjs-no-nonexistent-global`
|
|
2
|
+
|
|
3
|
+
Flags bare-name SSJS globals that are officially documented but proven **not to exist
|
|
4
|
+
at runtime** — calling them throws a `ReferenceError`. Unlike deprecated APIs (which
|
|
5
|
+
still run), these never work.
|
|
6
|
+
|
|
7
|
+
## What is flagged
|
|
8
|
+
|
|
9
|
+
| API | Reason |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `Redirect(url, movedPermanently)` | Not injected by `Platform.Load` under any Core version; calling it throws a `ReferenceError`. Use `Platform.Response.Redirect(url, movedPermanently)` instead. |
|
|
12
|
+
|
|
13
|
+
The list is driven by `ssjs-data`'s `notDefinedAtRuntime` flag, so new phantom globals
|
|
14
|
+
are picked up automatically without editing this rule.
|
|
15
|
+
|
|
16
|
+
## Examples
|
|
17
|
+
|
|
18
|
+
### ❌ Incorrect
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// Bare-name Redirect does not exist at runtime — throws ReferenceError
|
|
22
|
+
Redirect('https://example.com', false);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### ✅ Correct
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
// Platform.Response.Redirect is always available in CloudPages
|
|
29
|
+
Platform.Response.Redirect('https://example.com', false);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Rule details
|
|
33
|
+
|
|
34
|
+
- **Type:** `problem`
|
|
35
|
+
- **Fixable:** No
|
|
36
|
+
- **Recommended:** Yes
|
|
37
|
+
- **Strict:** Yes
|
|
@@ -21,7 +21,8 @@ return wrong results:
|
|
|
21
21
|
- `lastIndexOf` always returns `-1`.
|
|
22
22
|
|
|
23
23
|
This rule detects both categories and offers a lightbulb suggestion to insert a polyfill
|
|
24
|
-
at the
|
|
24
|
+
at the top of the file (after a leading `/* global … */` directive when present), matching
|
|
25
|
+
the language-server quick-fix so both tools place polyfills consistently.
|
|
25
26
|
|
|
26
27
|
## Covered methods
|
|
27
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-sfmc",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "ESLint plugin for Salesforce Marketing Cloud Engagement+Next - AMPscript, Server-Side JavaScript (SSJS) and Handlebars",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"eslint-plugin-mso-email": "^2.0.0",
|
|
43
43
|
"handlebars-data": "^0.3.0",
|
|
44
44
|
"mso-conditional-parser": "^2.0.1",
|
|
45
|
-
"ssjs-data": "^0.
|
|
45
|
+
"ssjs-data": "^0.19.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"eslint": ">=9.0.0"
|
package/src/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import ssjsNoUnsupportedSyntax from './rules/ssjs/no-unsupported-syntax.js';
|
|
|
43
43
|
import ssjsNoUnknownFunction from './rules/ssjs/no-unknown-function.js';
|
|
44
44
|
import ssjsNoMcnUnsupported from './rules/ssjs/no-mcn-unsupported.js';
|
|
45
45
|
import ssjsNoDeprecatedFunction from './rules/ssjs/no-deprecated-function.js';
|
|
46
|
+
import ssjsNoNonexistentGlobal from './rules/ssjs/no-nonexistent-global.js';
|
|
46
47
|
import ssjsNoPropertyCall from './rules/ssjs/no-property-call.js';
|
|
47
48
|
import ssjsPlatformFunctionArity from './rules/ssjs/platform-function-arity.js';
|
|
48
49
|
import ssjsCacheLoopLength from './rules/ssjs/cache-loop-length.js';
|
|
@@ -58,6 +59,7 @@ import ssjsArgumentTypes from './rules/ssjs/ssjs-argument-types.js';
|
|
|
58
59
|
import ssjsCoreMethodArity from './rules/ssjs/ssjs-core-method-arity.js';
|
|
59
60
|
import ssjsNoClrHeaderAccess from './rules/ssjs/no-clr-header-access.js';
|
|
60
61
|
import ssjsRequireStringClrContent from './rules/ssjs/require-string-clr-content.js';
|
|
62
|
+
import ssjsHttpPropertyValue from './rules/ssjs/http-property-value.js';
|
|
61
63
|
|
|
62
64
|
// ── Handlebars (MCN) rules ──────────────────────────────────────────────────────
|
|
63
65
|
|
|
@@ -124,6 +126,7 @@ const plugin = {
|
|
|
124
126
|
'ssjs-no-unknown-function': ssjsNoUnknownFunction,
|
|
125
127
|
'ssjs-no-mcn-unsupported': ssjsNoMcnUnsupported,
|
|
126
128
|
'ssjs-no-deprecated-function': ssjsNoDeprecatedFunction,
|
|
129
|
+
'ssjs-no-nonexistent-global': ssjsNoNonexistentGlobal,
|
|
127
130
|
'ssjs-no-property-call': ssjsNoPropertyCall,
|
|
128
131
|
'ssjs-platform-function-arity': ssjsPlatformFunctionArity,
|
|
129
132
|
'ssjs-cache-loop-length': ssjsCacheLoopLength,
|
|
@@ -139,6 +142,7 @@ const plugin = {
|
|
|
139
142
|
'ssjs-core-method-arity': ssjsCoreMethodArity,
|
|
140
143
|
'ssjs-no-clr-header-access': ssjsNoClrHeaderAccess,
|
|
141
144
|
'ssjs-require-string-clr-content': ssjsRequireStringClrContent,
|
|
145
|
+
'ssjs-http-property-value': ssjsHttpPropertyValue,
|
|
142
146
|
|
|
143
147
|
// Handlebars (MCN) rules (hbs- prefix)
|
|
144
148
|
'hbs-no-unknown-helper': hbsNoUnknownHelper,
|
|
@@ -169,6 +173,7 @@ const ssjsMcnRules = {
|
|
|
169
173
|
'sfmc/ssjs-require-platform-load': 'off',
|
|
170
174
|
'sfmc/ssjs-no-unsupported-syntax': 'off',
|
|
171
175
|
'sfmc/ssjs-no-deprecated-function': 'off',
|
|
176
|
+
'sfmc/ssjs-no-nonexistent-global': 'off',
|
|
172
177
|
'sfmc/ssjs-no-property-call': 'off',
|
|
173
178
|
'sfmc/ssjs-platform-function-arity': 'off',
|
|
174
179
|
'sfmc/ssjs-cache-loop-length': 'off',
|
|
@@ -184,6 +189,7 @@ const ssjsMcnRules = {
|
|
|
184
189
|
'sfmc/ssjs-core-method-arity': 'off',
|
|
185
190
|
'sfmc/ssjs-no-clr-header-access': 'off',
|
|
186
191
|
'sfmc/ssjs-require-string-clr-content': 'off',
|
|
192
|
+
'sfmc/ssjs-http-property-value': 'off',
|
|
187
193
|
'no-cond-assign': 'off',
|
|
188
194
|
};
|
|
189
195
|
|
|
@@ -239,6 +245,7 @@ const ssjsRecommendedRules = {
|
|
|
239
245
|
'sfmc/ssjs-no-unsupported-syntax': 'error',
|
|
240
246
|
'sfmc/ssjs-no-unknown-function': 'error',
|
|
241
247
|
'sfmc/ssjs-no-deprecated-function': 'error',
|
|
248
|
+
'sfmc/ssjs-no-nonexistent-global': 'error',
|
|
242
249
|
'sfmc/ssjs-no-property-call': 'error',
|
|
243
250
|
'sfmc/ssjs-platform-function-arity': 'error',
|
|
244
251
|
'sfmc/ssjs-cache-loop-length': 'warn',
|
|
@@ -254,6 +261,7 @@ const ssjsRecommendedRules = {
|
|
|
254
261
|
'sfmc/ssjs-core-method-arity': 'warn',
|
|
255
262
|
'sfmc/ssjs-no-clr-header-access': 'error',
|
|
256
263
|
'sfmc/ssjs-require-string-clr-content': 'error',
|
|
264
|
+
'sfmc/ssjs-http-property-value': 'error',
|
|
257
265
|
'no-cond-assign': 'error',
|
|
258
266
|
};
|
|
259
267
|
|
|
@@ -262,6 +270,7 @@ const ssjsStrictRules = {
|
|
|
262
270
|
'sfmc/ssjs-no-unsupported-syntax': 'error',
|
|
263
271
|
'sfmc/ssjs-no-unknown-function': 'error',
|
|
264
272
|
'sfmc/ssjs-no-deprecated-function': 'error',
|
|
273
|
+
'sfmc/ssjs-no-nonexistent-global': 'error',
|
|
265
274
|
'sfmc/ssjs-no-property-call': 'error',
|
|
266
275
|
'sfmc/ssjs-platform-function-arity': 'error',
|
|
267
276
|
'sfmc/ssjs-cache-loop-length': 'error',
|
|
@@ -277,6 +286,7 @@ const ssjsStrictRules = {
|
|
|
277
286
|
'sfmc/ssjs-core-method-arity': 'error',
|
|
278
287
|
'sfmc/ssjs-no-clr-header-access': 'error',
|
|
279
288
|
'sfmc/ssjs-require-string-clr-content': 'error',
|
|
289
|
+
'sfmc/ssjs-http-property-value': 'error',
|
|
280
290
|
'no-cond-assign': 'error',
|
|
281
291
|
};
|
|
282
292
|
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-http-property-value
|
|
3
|
+
*
|
|
4
|
+
* Flags literal assignments to writable `Script.Util.HttpRequest` /
|
|
5
|
+
* `Script.Util.HttpGet` instance properties whose value violates the property's
|
|
6
|
+
* documented, runtime-confirmed constraint (allowed enum, integer-ness, or
|
|
7
|
+
* minimum). Examples that throw or misbehave at runtime:
|
|
8
|
+
*
|
|
9
|
+
* req.emptyContentHandling = 5; // allowed: 0 | 1 | 2
|
|
10
|
+
* req.retries = -2.45; // must be a non-negative integer
|
|
11
|
+
* req.method = 'POT'; // allowed: GET | POST | PUT | PATCH | DELETE
|
|
12
|
+
*
|
|
13
|
+
* Constraints live in ssjs-data (`SCRIPT_UTIL_REQUEST_PROPERTIES` /
|
|
14
|
+
* `SCRIPT_UTIL_HTTPGET_PROPERTIES` -> `valueConstraint`) so the LSP diagnostic
|
|
15
|
+
* (`ssjs/invalid-http-property-value`) and this rule share one source of truth.
|
|
16
|
+
* Only **literal** right-hand sides are checked; variables / expressions cannot
|
|
17
|
+
* be statically verified and are left alone to avoid false positives.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { SCRIPT_UTIL_REQUEST_PROPERTIES, SCRIPT_UTIL_HTTPGET_PROPERTIES } from 'ssjs-data';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Map of property name => valueConstraint, built once from ssjs-data. When both
|
|
24
|
+
* request and get define the same property, the constraints are identical.
|
|
25
|
+
*
|
|
26
|
+
* @returns {Map<string, object>} property name -> valueConstraint
|
|
27
|
+
*/
|
|
28
|
+
function buildConstraintLookup() {
|
|
29
|
+
const lookup = new Map();
|
|
30
|
+
for (const property of [...SCRIPT_UTIL_REQUEST_PROPERTIES, ...SCRIPT_UTIL_HTTPGET_PROPERTIES]) {
|
|
31
|
+
if (property && property.valueConstraint && !lookup.has(property.name)) {
|
|
32
|
+
lookup.set(property.name, property.valueConstraint);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return lookup;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const constraintLookup = buildConstraintLookup();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true when the node is `Script.Util.HttpRequest` or
|
|
42
|
+
* `Script.Util.HttpGet` (a MemberExpression, optionally the callee of `new`).
|
|
43
|
+
*
|
|
44
|
+
* @param {import('eslint').Rule.Node} node - MemberExpression to test
|
|
45
|
+
* @returns {boolean} Whether it references a Script.Util HTTP constructor
|
|
46
|
+
*/
|
|
47
|
+
function isHttpConstructorMember(node) {
|
|
48
|
+
return (
|
|
49
|
+
node.type === 'MemberExpression' &&
|
|
50
|
+
node.property.type === 'Identifier' &&
|
|
51
|
+
(node.property.name === 'HttpRequest' || node.property.name === 'HttpGet') &&
|
|
52
|
+
node.object.type === 'MemberExpression' &&
|
|
53
|
+
node.object.property.type === 'Identifier' &&
|
|
54
|
+
node.object.property.name === 'Util' &&
|
|
55
|
+
node.object.object.type === 'Identifier' &&
|
|
56
|
+
node.object.object.name === 'Script'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns true when the node constructs a Script.Util HTTP request, i.e.
|
|
62
|
+
* `new Script.Util.HttpRequest(...)` or `Script.Util.HttpGet(...)`.
|
|
63
|
+
*
|
|
64
|
+
* @param {import('eslint').Rule.Node} node - init expression of a declarator
|
|
65
|
+
* @returns {boolean} Whether the expression yields an HTTP request instance
|
|
66
|
+
*/
|
|
67
|
+
function isHttpRequestInit(node) {
|
|
68
|
+
if (!node) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (node.type === 'NewExpression' || node.type === 'CallExpression') {
|
|
72
|
+
return isHttpConstructorMember(node.callee);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extracts the literal value from a RHS node, or null when it is not a plain
|
|
79
|
+
* literal (string / number / boolean, incl. a negative numeric like `-2.45`).
|
|
80
|
+
*
|
|
81
|
+
* @param {import('eslint').Rule.Node} node - the assignment right-hand side
|
|
82
|
+
* @returns {{value: string|number|boolean}|null} parsed value wrapper or null
|
|
83
|
+
*/
|
|
84
|
+
function parseLiteral(node) {
|
|
85
|
+
if (node.type === 'Literal' && typeof node.value !== 'object' && node.value !== null) {
|
|
86
|
+
return { value: node.value };
|
|
87
|
+
}
|
|
88
|
+
// Unary minus / plus on a numeric literal, e.g. -2.45.
|
|
89
|
+
if (
|
|
90
|
+
node.type === 'UnaryExpression' &&
|
|
91
|
+
(node.operator === '-' || node.operator === '+') &&
|
|
92
|
+
node.argument.type === 'Literal' &&
|
|
93
|
+
typeof node.argument.value === 'number'
|
|
94
|
+
) {
|
|
95
|
+
const n = node.operator === '-' ? -node.argument.value : node.argument.value;
|
|
96
|
+
return { value: n };
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validates a value against a constraint. Returns a human-readable violation
|
|
103
|
+
* fragment (e.g. `must be one of "GET", "POST"`) or null when the value is ok.
|
|
104
|
+
*
|
|
105
|
+
* @param {string|number|boolean} value - the parsed literal value
|
|
106
|
+
* @param {object} constraint - the valueConstraint from ssjs-data
|
|
107
|
+
* @returns {string|null} violation description or null when valid
|
|
108
|
+
*/
|
|
109
|
+
function checkConstraint(value, constraint) {
|
|
110
|
+
if (Array.isArray(constraint.enum)) {
|
|
111
|
+
if (!constraint.enum.includes(value)) {
|
|
112
|
+
const allowed = constraint.enum
|
|
113
|
+
.map((v) => (typeof v === 'string' ? `"${v}"` : String(v)))
|
|
114
|
+
.join(', ');
|
|
115
|
+
return `must be one of ${allowed}`;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (constraint.numeric) {
|
|
120
|
+
if (typeof value !== 'number') {
|
|
121
|
+
return 'must be a number';
|
|
122
|
+
}
|
|
123
|
+
if (constraint.numeric === 'integer' && !Number.isSafeInteger(value)) {
|
|
124
|
+
return 'must be an integer';
|
|
125
|
+
}
|
|
126
|
+
if (typeof constraint.min === 'number' && value < constraint.min) {
|
|
127
|
+
return `must be >= ${constraint.min}`;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Builds source-ready replacement suggestions for a violation. Enum -> each
|
|
136
|
+
* allowed value (quoted for strings) with its optional `enumLabels` meaning.
|
|
137
|
+
* Numeric -> nothing (no single fix).
|
|
138
|
+
*
|
|
139
|
+
* @param {object} constraint - the valueConstraint from ssjs-data
|
|
140
|
+
* @returns {{code: string, label?: string}[]} replacement snippets to offer as suggestions
|
|
141
|
+
*/
|
|
142
|
+
function buildSuggestions(constraint) {
|
|
143
|
+
if (Array.isArray(constraint.enum)) {
|
|
144
|
+
const labels = constraint.enumLabels;
|
|
145
|
+
return constraint.enum.map((v) => ({
|
|
146
|
+
code: typeof v === 'string' ? `'${v}'` : String(v),
|
|
147
|
+
label: labels ? labels[String(v)] : undefined,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default {
|
|
154
|
+
meta: {
|
|
155
|
+
type: 'problem',
|
|
156
|
+
hasSuggestions: true,
|
|
157
|
+
docs: {
|
|
158
|
+
description:
|
|
159
|
+
'Disallow invalid literal values assigned to Script.Util.HttpRequest/HttpGet properties (e.g. method, emptyContentHandling, retries) based on their documented allowed values',
|
|
160
|
+
},
|
|
161
|
+
messages: {
|
|
162
|
+
invalidValue: 'Invalid value for {{ prop }}: it {{ violation }}.',
|
|
163
|
+
replaceWith: 'Replace with {{ value }}',
|
|
164
|
+
replaceWithLabel: 'Replace with {{ value }} ({{ label }})',
|
|
165
|
+
},
|
|
166
|
+
schema: [],
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
create(context) {
|
|
170
|
+
const requestVariables = new Set();
|
|
171
|
+
const pending = [];
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
VariableDeclarator(node) {
|
|
175
|
+
if (node.id.type === 'Identifier' && isHttpRequestInit(node.init)) {
|
|
176
|
+
requestVariables.add(node.id.name);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
AssignmentExpression(node) {
|
|
181
|
+
if (
|
|
182
|
+
node.operator !== '=' ||
|
|
183
|
+
node.left.type !== 'MemberExpression' ||
|
|
184
|
+
node.left.computed ||
|
|
185
|
+
node.left.object.type !== 'Identifier' ||
|
|
186
|
+
!requestVariables.has(node.left.object.name) ||
|
|
187
|
+
node.left.property.type !== 'Identifier'
|
|
188
|
+
) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const propertyName = node.left.property.name;
|
|
192
|
+
const constraint = constraintLookup.get(propertyName);
|
|
193
|
+
if (!constraint) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const parsed = parseLiteral(node.right);
|
|
197
|
+
if (!parsed) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const violation = checkConstraint(parsed.value, constraint);
|
|
201
|
+
if (!violation) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
pending.push({ rhs: node.right, propName: propertyName, violation, constraint });
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
'Program:exit'() {
|
|
208
|
+
for (const { rhs, propName, violation, constraint } of pending) {
|
|
209
|
+
const suggest = buildSuggestions(constraint).map(({ code, label }) =>
|
|
210
|
+
label
|
|
211
|
+
? {
|
|
212
|
+
messageId: 'replaceWithLabel',
|
|
213
|
+
data: { value: code, label },
|
|
214
|
+
fix: (fixer) => fixer.replaceText(rhs, code),
|
|
215
|
+
}
|
|
216
|
+
: {
|
|
217
|
+
messageId: 'replaceWith',
|
|
218
|
+
data: { value: code },
|
|
219
|
+
fix: (fixer) => fixer.replaceText(rhs, code),
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
context.report({
|
|
223
|
+
node: rhs,
|
|
224
|
+
messageId: 'invalidValue',
|
|
225
|
+
data: { prop: propName, violation },
|
|
226
|
+
suggest,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
};
|
|
@@ -12,9 +12,15 @@
|
|
|
12
12
|
* - ContentAreaObj.Retrieve(...) — static method
|
|
13
13
|
* - <contentAreaObjVar>.Update(...) — instance method on a tracked ContentAreaObj variable
|
|
14
14
|
* - <contentAreaObjVar>.Remove() — instance method on a tracked ContentAreaObj variable
|
|
15
|
+
* - ErrorUtil.ThrowWSProxyError(...) — deprecated; only exists under Platform.Load("Core", "1")
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
platformFunctionLookup,
|
|
20
|
+
SSJS_GLOBALS,
|
|
21
|
+
CONTENT_AREA_OBJ_METHODS,
|
|
22
|
+
ERROR_UTIL_METHODS,
|
|
23
|
+
} from 'ssjs-data';
|
|
18
24
|
|
|
19
25
|
// Lookup Map: lowercase name → entry, for SSJS_GLOBALS entries that are deprecated.
|
|
20
26
|
// Used to flag bare calls like ContentArea(...) and ContentAreaByName(...).
|
|
@@ -38,6 +44,12 @@ const CONTENT_AREA_INSTANCE_DEPRECATED = new Set(
|
|
|
38
44
|
),
|
|
39
45
|
);
|
|
40
46
|
|
|
47
|
+
// Deprecated ErrorUtil methods (e.g. ThrowWSProxyError). Used to flag calls like
|
|
48
|
+
// ErrorUtil.ThrowWSProxyError(result), which only exists under Platform.Load("Core", "1").
|
|
49
|
+
const ERRORUTIL_DEPRECATED = new Set(
|
|
50
|
+
ERROR_UTIL_METHODS.filter((m) => m.deprecated).map((m) => m.name.toLowerCase()),
|
|
51
|
+
);
|
|
52
|
+
|
|
41
53
|
export default {
|
|
42
54
|
meta: {
|
|
43
55
|
type: 'suggestion',
|
|
@@ -52,6 +64,8 @@ export default {
|
|
|
52
64
|
"'ContentAreaObj.{{name}}' is deprecated. Content Areas are no longer supported.",
|
|
53
65
|
deprecatedCoreInstance:
|
|
54
66
|
"'{{method}}' called on a ContentAreaObj variable is deprecated. Content Areas are no longer supported.",
|
|
67
|
+
deprecatedErrorUtil:
|
|
68
|
+
"'ErrorUtil.{{name}}' is deprecated — it only exists under Platform.Load(\"Core\", \"1\") and is undefined in newer Core versions. Check 'result.Status' and 'throw new Error(...)' instead.",
|
|
55
69
|
},
|
|
56
70
|
schema: [],
|
|
57
71
|
},
|
|
@@ -143,6 +157,20 @@ export default {
|
|
|
143
157
|
return;
|
|
144
158
|
}
|
|
145
159
|
|
|
160
|
+
// ── ErrorUtil.ThrowWSProxyError(…) — deprecated ───────────────
|
|
161
|
+
if (
|
|
162
|
+
callee.object.type === 'Identifier' &&
|
|
163
|
+
callee.object.name === 'ErrorUtil' &&
|
|
164
|
+
ERRORUTIL_DEPRECATED.has(methodName.toLowerCase())
|
|
165
|
+
) {
|
|
166
|
+
context.report({
|
|
167
|
+
node: property,
|
|
168
|
+
messageId: 'deprecatedErrorUtil',
|
|
169
|
+
data: { name: methodName },
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
146
174
|
// ── <contentAreaVar>.Update/Remove() — instance deprecated ─────
|
|
147
175
|
if (
|
|
148
176
|
callee.object.type === 'Identifier' &&
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-no-nonexistent-global
|
|
3
|
+
*
|
|
4
|
+
* Flags bare-name SSJS globals that are officially documented but proven NOT to
|
|
5
|
+
* exist at runtime — calling them throws a ReferenceError. Currently covers:
|
|
6
|
+
*
|
|
7
|
+
* - Redirect(url, movedPermanently) — undefined under every Core version;
|
|
8
|
+
* use Platform.Response.Redirect(url, movedPermanently) instead.
|
|
9
|
+
*
|
|
10
|
+
* The offending names come from ssjs-data's `notDefinedAtRuntime` flag, so new
|
|
11
|
+
* phantom globals are picked up automatically without editing this rule.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { notDefinedAtRuntimeGlobalLookup } from 'ssjs-data';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract a runtime-safe replacement suggestion for a phantom global from its
|
|
18
|
+
* ssjs-data entry. Prefers the `Platform.*` call named in the officialDocsNote,
|
|
19
|
+
* falling back to a generic hint.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} entry - The ssjs-data global entry.
|
|
22
|
+
* @returns {string} A replacement suggestion (e.g. `Platform.Response.Redirect(...)`).
|
|
23
|
+
*/
|
|
24
|
+
function replacementFor(entry) {
|
|
25
|
+
const source = `${entry.officialDocsNote ?? ''} ${entry.description ?? ''}`;
|
|
26
|
+
const match = source.match(/Platform\.[A-Za-z.]+\([^)]*\)/);
|
|
27
|
+
return match ? match[0] : 'a supported alternative';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
meta: {
|
|
32
|
+
type: 'problem',
|
|
33
|
+
docs: {
|
|
34
|
+
description:
|
|
35
|
+
'Disallow SSJS globals that are documented but do not exist at runtime (throw ReferenceError)',
|
|
36
|
+
},
|
|
37
|
+
messages: {
|
|
38
|
+
nonexistentGlobal:
|
|
39
|
+
"'{{name}}' does not exist at runtime (calling it throws a ReferenceError). Use {{replacement}} instead.",
|
|
40
|
+
},
|
|
41
|
+
schema: [],
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
create(context) {
|
|
45
|
+
return {
|
|
46
|
+
CallExpression(node) {
|
|
47
|
+
const callee = node.callee;
|
|
48
|
+
if (callee.type !== 'Identifier') {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const entry = notDefinedAtRuntimeGlobalLookup.get(callee.name.toLowerCase());
|
|
52
|
+
if (!entry) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
context.report({
|
|
56
|
+
node: callee,
|
|
57
|
+
messageId: 'nonexistentGlobal',
|
|
58
|
+
data: { name: callee.name, replacement: replacementFor(entry) },
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Two catalogs from ssjs-data feed this rule:
|
|
8
8
|
*
|
|
9
9
|
* 1. POLYFILLABLE_METHODS — a shipped ES3-safe polyfill exists. The report
|
|
10
|
-
* carries a suggestion that inserts the polyfill at the
|
|
10
|
+
* carries a suggestion that inserts the polyfill at the top of the file.
|
|
11
11
|
* 2. KNOWN_UNSUPPORTED — no polyfill is feasible. The report has no fix; the
|
|
12
12
|
* message carries the ssjs-data `suggestion` (e.g. use Platform.Function.X).
|
|
13
13
|
*
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* - category 'broken': method exists natively but returns incorrect results.
|
|
16
16
|
*
|
|
17
17
|
* No auto-fix is applied because prototype assignments are not hoisted —
|
|
18
|
-
* the suggestion inserts the polyfill at the
|
|
19
|
-
*
|
|
18
|
+
* the suggestion inserts the polyfill at the top of the file (after a leading
|
|
19
|
+
* `/* global *\/` directive when present), and users should verify placement
|
|
20
|
+
* before the first call (or load via Content Block).
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
import {
|
|
@@ -58,7 +59,7 @@ export default {
|
|
|
58
59
|
broken:
|
|
59
60
|
"'{{owner}}.{{method}}' exists in SFMC SSJS but produces incorrect results. " +
|
|
60
61
|
'Add a polyfill to get the correct behavior.',
|
|
61
|
-
addPolyfill: "Insert '{{owner}}.{{method}}' polyfill at
|
|
62
|
+
addPolyfill: "Insert '{{owner}}.{{method}}' polyfill at top of file",
|
|
62
63
|
unavailableNoPolyfill:
|
|
63
64
|
"'{{owner}}.{{method}}' is not available in SFMC SSJS (ECMAScript 3). {{suggestion}}",
|
|
64
65
|
brokenNoPolyfill:
|
|
@@ -99,8 +100,21 @@ export default {
|
|
|
99
100
|
function buildSuggestFix(entry) {
|
|
100
101
|
return function fix(fixer) {
|
|
101
102
|
const source = context.sourceCode;
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
// Insert the polyfill at the TOP of the file so it is defined
|
|
104
|
+
// before any call site (SSJS does not hoist prototype
|
|
105
|
+
// assignments). This mirrors the LSP quick-fix. When a leading
|
|
106
|
+
// `/* global ... */` ESLint directive is the first comment,
|
|
107
|
+
// insert right after it so the directive stays at the very top.
|
|
108
|
+
const leadingGlobal = source
|
|
109
|
+
.getAllComments()
|
|
110
|
+
.find(
|
|
111
|
+
(c) =>
|
|
112
|
+
c.type === 'Block' && c.range[0] === 0 && /^\s*global\b/.test(c.value),
|
|
113
|
+
);
|
|
114
|
+
if (leadingGlobal) {
|
|
115
|
+
return fixer.insertTextAfterRange(leadingGlobal.range, '\n\n' + entry.polyfill);
|
|
116
|
+
}
|
|
117
|
+
return fixer.insertTextBeforeRange([0, 0], entry.polyfill + '\n\n');
|
|
104
118
|
};
|
|
105
119
|
}
|
|
106
120
|
|