eslint-plugin-sfmc 4.1.2 → 4.3.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 -0
- package/docs/rules/ssjs/http-property-value.md +71 -0
- package/docs/rules/ssjs/no-clr-header-access.md +91 -0
- package/docs/rules/ssjs/no-unavailable-method.md +2 -1
- package/docs/rules/ssjs/require-string-clr-content.md +69 -0
- package/package.json +2 -2
- package/src/index.js +15 -0
- package/src/rules/ssjs/http-property-value.js +232 -0
- package/src/rules/ssjs/no-clr-header-access.js +237 -0
- package/src/rules/ssjs/no-unavailable-method.js +20 -6
- package/src/rules/ssjs/require-string-clr-content.js +180 -0
package/README.md
CHANGED
|
@@ -130,6 +130,8 @@ export default [...sfmc.configs['recommended-next'], ...sfmc.configs['embedded-n
|
|
|
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
131
|
| [`sfmc/ssjs-no-deprecated-function`](docs/rules/ssjs/no-deprecated-function.md) | `error` | Flag use of deprecated SFMC SSJS APIs (e.g. ContentArea, ContentAreaObj) |
|
|
132
132
|
| [`sfmc/ssjs-no-property-call`](docs/rules/ssjs/no-property-call.md) | `error` | Disallow calling Platform.Request/Response properties as functions |
|
|
133
|
+
| [`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
|
+
| [`sfmc/ssjs-require-string-clr-content`](docs/rules/ssjs/require-string-clr-content.md) | `error` | Require wrapping `HttpResponse.content` with `String()` before use |
|
|
133
135
|
| [`sfmc/ssjs-platform-function-arity`](docs/rules/ssjs/platform-function-arity.md) | `error` | Enforce correct arity for `Platform.Function.*` |
|
|
134
136
|
| [`sfmc/ssjs-require-platform-load-order`](docs/rules/ssjs/require-platform-load-order.md) | `error` | Require `Platform.Load()` before Core usage in order |
|
|
135
137
|
| [`sfmc/ssjs-no-hardcoded-credentials`](docs/rules/ssjs/no-hardcoded-credentials.md) | `error` | Flag hardcoded keys in encryption calls |
|
|
@@ -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)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# `sfmc/ssjs-no-clr-header-access`
|
|
2
|
+
|
|
3
|
+
Flags CLR-unsafe reads of the `headers` property of an `HttpResponse` — the object
|
|
4
|
+
returned by `req.send()` on a `Script.Util.HttpRequest` or `Script.Util.HttpGet`.
|
|
5
|
+
|
|
6
|
+
`resp.headers` is a Common Language Runtime (CLR) object. Reading an individual
|
|
7
|
+
header by indexing it (`resp.headers["Content-Type"]`), or by calling `.Get()` /
|
|
8
|
+
`.Item()`, throws at runtime:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Use of Common Language Runtime (CLR) is not allowed
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The only reliable way to read header values is to enumerate `resp.headers` with a
|
|
15
|
+
`for..in` loop. Each enumeration key is shaped `"[Name, Value]"`, so the value is
|
|
16
|
+
embedded in the key string. The `getHeaderMap()` helper parses those keys into a
|
|
17
|
+
plain `{ name: value }` map (with lowercased names).
|
|
18
|
+
|
|
19
|
+
## Detection
|
|
20
|
+
|
|
21
|
+
The rule tracks data flow, so it only fires on a genuine response object:
|
|
22
|
+
|
|
23
|
+
1. A **request** variable is one assigned from `new Script.Util.HttpRequest(...)`
|
|
24
|
+
or `Script.Util.HttpGet(...)`.
|
|
25
|
+
2. A **response** variable is one assigned from `<requestVar>.send()`.
|
|
26
|
+
3. CLR-style reads of `<responseVar>.headers` are then flagged.
|
|
27
|
+
|
|
28
|
+
`.headers` access on any other object is ignored, so there are no false positives
|
|
29
|
+
on unrelated `headers` properties.
|
|
30
|
+
|
|
31
|
+
## Examples
|
|
32
|
+
|
|
33
|
+
### ❌ Incorrect — CLR-style reads throw at runtime
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
var req = new Script.Util.HttpRequest("https://api.example.com/data");
|
|
37
|
+
var resp = req.send();
|
|
38
|
+
|
|
39
|
+
var ct = resp.headers["Content-Type"]; // ← throws (CLR not allowed)
|
|
40
|
+
var loc = resp.headers.Get("Location"); // ← throws (CLR not allowed)
|
|
41
|
+
var enc = resp.headers.Item("Content-Encoding"); // ← throws (CLR not allowed)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### ✅ Correct — read via a `getHeaderMap()` helper
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
/**
|
|
48
|
+
* Build a plain { name: value } header map from an HttpResponse.
|
|
49
|
+
* Reads only the for..in enumeration keys (shaped "[Name, Value]") so it never
|
|
50
|
+
* touches a CLR value — avoiding "Use of Common Language Runtime (CLR) is not allowed".
|
|
51
|
+
* @param {object} resp - the response returned by req.send()
|
|
52
|
+
* @returns {object} map of lowercased header name => value string
|
|
53
|
+
*/
|
|
54
|
+
function getHeaderMap(resp) {
|
|
55
|
+
var map = {};
|
|
56
|
+
for (var k in resp.headers) {
|
|
57
|
+
var pair = String(k);
|
|
58
|
+
if (pair.charAt(0) === "[") { pair = pair.substring(1); }
|
|
59
|
+
if (pair.charAt(pair.length - 1) === "]") { pair = pair.substring(0, pair.length - 1); }
|
|
60
|
+
var idx = pair.indexOf(", ");
|
|
61
|
+
if (idx > -1) {
|
|
62
|
+
map[pair.substring(0, idx).toLowerCase()] = pair.substring(idx + 2);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return map;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var req = new Script.Util.HttpRequest("https://api.example.com/data");
|
|
69
|
+
var resp = req.send();
|
|
70
|
+
|
|
71
|
+
var headers = getHeaderMap(resp);
|
|
72
|
+
var ct = headers["content-type"]; // names are lowercased
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Suggestion (quick-fix)
|
|
76
|
+
|
|
77
|
+
The rule provides a suggestion that:
|
|
78
|
+
|
|
79
|
+
1. Inserts the `getHeaderMap()` helper once, at the top of the file (skipped if a
|
|
80
|
+
`getHeaderMap(` function already exists).
|
|
81
|
+
2. Rewrites the flagged access to `getHeaderMap(<resp>)[<key>]`.
|
|
82
|
+
|
|
83
|
+
The same diagnostic and quick-fix are available in the VS Code / Cursor extension
|
|
84
|
+
(`ssjs/clr-header-access`).
|
|
85
|
+
|
|
86
|
+
## Rule details
|
|
87
|
+
|
|
88
|
+
- **Type:** `problem`
|
|
89
|
+
- **Fixable:** No (offers a suggestion)
|
|
90
|
+
- **Recommended:** Yes (`error`)
|
|
91
|
+
- **Strict:** Yes (`error`)
|
|
@@ -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
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# `sfmc/ssjs-require-string-clr-content`
|
|
2
|
+
|
|
3
|
+
Requires wrapping the `content` property of an `HttpResponse` with `String()` before
|
|
4
|
+
use — the object returned by `req.send()` on a `Script.Util.HttpRequest` or
|
|
5
|
+
`Script.Util.HttpGet`.
|
|
6
|
+
|
|
7
|
+
`resp.content` is a Common Language Runtime (CLR) string, **not** a native JavaScript
|
|
8
|
+
string. Passing it directly to `Platform.Function.ParseJSON()`, calling a string
|
|
9
|
+
method on it, concatenating it, or assigning it to a variable used as a string is
|
|
10
|
+
unreliable in the SFMC SSJS (ES3/CLR) engine. The verified fix is to convert it with
|
|
11
|
+
`String(resp.content)` first:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
var responseJSON = Platform.Function.ParseJSON(String(resp.content));
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Detection
|
|
18
|
+
|
|
19
|
+
The rule tracks data flow, so it only fires on a genuine response object:
|
|
20
|
+
|
|
21
|
+
1. A **request** variable is one assigned from `new Script.Util.HttpRequest(...)`
|
|
22
|
+
or `Script.Util.HttpGet(...)`.
|
|
23
|
+
2. A **response** variable is one assigned from `<requestVar>.send()`.
|
|
24
|
+
3. Any read of `<responseVar>.content` that is **not** already the direct argument
|
|
25
|
+
of a `String(...)` call is flagged.
|
|
26
|
+
|
|
27
|
+
`.content` access on any other object is ignored, so there are no false positives on
|
|
28
|
+
unrelated `content` properties.
|
|
29
|
+
|
|
30
|
+
## Examples
|
|
31
|
+
|
|
32
|
+
### ❌ Incorrect — raw CLR content used directly
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
var req = new Script.Util.HttpRequest("https://api.example.com/data");
|
|
36
|
+
var resp = req.send();
|
|
37
|
+
|
|
38
|
+
var parsed = Platform.Function.ParseJSON(resp.content); // ← unreliable
|
|
39
|
+
var body = resp.content; // ← unreliable
|
|
40
|
+
var msg = "Body: " + resp.content; // ← unreliable
|
|
41
|
+
var head = resp.content.substring(0, 20); // ← unreliable
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### ✅ Correct — wrap with `String()` first
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
var req = new Script.Util.HttpRequest("https://api.example.com/data");
|
|
48
|
+
var resp = req.send();
|
|
49
|
+
|
|
50
|
+
var parsed = Platform.Function.ParseJSON(String(resp.content));
|
|
51
|
+
var body = String(resp.content);
|
|
52
|
+
var head = String(resp.content).substring(0, 20);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Fix (auto-fix)
|
|
56
|
+
|
|
57
|
+
The rule is auto-fixable: it wraps the flagged `<resp>.content` access in
|
|
58
|
+
`String(...)`. For example `ParseJSON(resp.content)` becomes
|
|
59
|
+
`ParseJSON(String(resp.content))`.
|
|
60
|
+
|
|
61
|
+
The same diagnostic and quick-fix are available in the VS Code / Cursor extension
|
|
62
|
+
(`ssjs/clr-content-access`).
|
|
63
|
+
|
|
64
|
+
## Rule details
|
|
65
|
+
|
|
66
|
+
- **Type:** `problem`
|
|
67
|
+
- **Fixable:** Yes (`code`)
|
|
68
|
+
- **Recommended:** Yes (`error`)
|
|
69
|
+
- **Strict:** Yes (`error`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-sfmc",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.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.18.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"eslint": ">=9.0.0"
|
package/src/index.js
CHANGED
|
@@ -56,6 +56,9 @@ import ssjsNoSwitchDefault from './rules/ssjs/no-switch-default.js';
|
|
|
56
56
|
import ssjsNoTreatAsContentInjection from './rules/ssjs/no-treatascontent-injection.js';
|
|
57
57
|
import ssjsArgumentTypes from './rules/ssjs/ssjs-argument-types.js';
|
|
58
58
|
import ssjsCoreMethodArity from './rules/ssjs/ssjs-core-method-arity.js';
|
|
59
|
+
import ssjsNoClrHeaderAccess from './rules/ssjs/no-clr-header-access.js';
|
|
60
|
+
import ssjsRequireStringClrContent from './rules/ssjs/require-string-clr-content.js';
|
|
61
|
+
import ssjsHttpPropertyValue from './rules/ssjs/http-property-value.js';
|
|
59
62
|
|
|
60
63
|
// ── Handlebars (MCN) rules ──────────────────────────────────────────────────────
|
|
61
64
|
|
|
@@ -135,6 +138,9 @@ const plugin = {
|
|
|
135
138
|
'ssjs-no-treatascontent-injection': ssjsNoTreatAsContentInjection,
|
|
136
139
|
'ssjs-arg-types': ssjsArgumentTypes,
|
|
137
140
|
'ssjs-core-method-arity': ssjsCoreMethodArity,
|
|
141
|
+
'ssjs-no-clr-header-access': ssjsNoClrHeaderAccess,
|
|
142
|
+
'ssjs-require-string-clr-content': ssjsRequireStringClrContent,
|
|
143
|
+
'ssjs-http-property-value': ssjsHttpPropertyValue,
|
|
138
144
|
|
|
139
145
|
// Handlebars (MCN) rules (hbs- prefix)
|
|
140
146
|
'hbs-no-unknown-helper': hbsNoUnknownHelper,
|
|
@@ -178,6 +184,9 @@ const ssjsMcnRules = {
|
|
|
178
184
|
'sfmc/ssjs-no-treatascontent-injection': 'off',
|
|
179
185
|
'sfmc/ssjs-arg-types': 'off',
|
|
180
186
|
'sfmc/ssjs-core-method-arity': 'off',
|
|
187
|
+
'sfmc/ssjs-no-clr-header-access': 'off',
|
|
188
|
+
'sfmc/ssjs-require-string-clr-content': 'off',
|
|
189
|
+
'sfmc/ssjs-http-property-value': 'off',
|
|
181
190
|
'no-cond-assign': 'off',
|
|
182
191
|
};
|
|
183
192
|
|
|
@@ -246,6 +255,9 @@ const ssjsRecommendedRules = {
|
|
|
246
255
|
'sfmc/ssjs-no-treatascontent-injection': 'warn',
|
|
247
256
|
'sfmc/ssjs-arg-types': 'warn',
|
|
248
257
|
'sfmc/ssjs-core-method-arity': 'warn',
|
|
258
|
+
'sfmc/ssjs-no-clr-header-access': 'error',
|
|
259
|
+
'sfmc/ssjs-require-string-clr-content': 'error',
|
|
260
|
+
'sfmc/ssjs-http-property-value': 'error',
|
|
249
261
|
'no-cond-assign': 'error',
|
|
250
262
|
};
|
|
251
263
|
|
|
@@ -267,6 +279,9 @@ const ssjsStrictRules = {
|
|
|
267
279
|
'sfmc/ssjs-no-treatascontent-injection': 'error',
|
|
268
280
|
'sfmc/ssjs-arg-types': 'warn',
|
|
269
281
|
'sfmc/ssjs-core-method-arity': 'error',
|
|
282
|
+
'sfmc/ssjs-no-clr-header-access': 'error',
|
|
283
|
+
'sfmc/ssjs-require-string-clr-content': 'error',
|
|
284
|
+
'sfmc/ssjs-http-property-value': 'error',
|
|
270
285
|
'no-cond-assign': 'error',
|
|
271
286
|
};
|
|
272
287
|
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-no-clr-header-access
|
|
3
|
+
*
|
|
4
|
+
* The `headers` property of an HttpResponse (returned by `req.send()` on a
|
|
5
|
+
* `Script.Util.HttpRequest` or `Script.Util.HttpGet`) is a CLR object. Reading
|
|
6
|
+
* an individual header via indexing (`resp.headers["Content-Type"]`), a `.Get()`
|
|
7
|
+
* / `.Item()` call, or `String(resp.headers[key])` throws at runtime:
|
|
8
|
+
*
|
|
9
|
+
* "Use of Common Language Runtime (CLR) is not allowed"
|
|
10
|
+
*
|
|
11
|
+
* The only reliable way to read header values is to enumerate `resp.headers`
|
|
12
|
+
* with a `for..in` loop — each key is shaped `"[Name, Value]"`, so the value is
|
|
13
|
+
* embedded in the key string. The `getHeaderMap()` helper parses those keys into
|
|
14
|
+
* a plain `{ name: value }` map.
|
|
15
|
+
*
|
|
16
|
+
* This rule tracks variables assigned from `req.send()` (where `req` came from a
|
|
17
|
+
* `Script.Util.HttpRequest`/`HttpGet`) and flags CLR-style reads of their
|
|
18
|
+
* `.headers`. It offers a suggestion that inserts the `getHeaderMap()` helper
|
|
19
|
+
* and rewrites the access to `getHeaderMap(resp)["name"]`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Canonical helper inserted by the quick-fix, mirrored from ssjs.guide. */
|
|
23
|
+
const HEADER_MAP_HELPER = `/**
|
|
24
|
+
* Build a plain { name: value } header map from an HttpResponse.
|
|
25
|
+
* Reads only the for..in enumeration keys (shaped "[Name, Value]") so it never
|
|
26
|
+
* touches a CLR value — avoiding "Use of Common Language Runtime (CLR) is not allowed".
|
|
27
|
+
* @param {object} resp - the response returned by req.send()
|
|
28
|
+
* @returns {object} map of lowercased header name => value string
|
|
29
|
+
*/
|
|
30
|
+
function getHeaderMap(resp) {
|
|
31
|
+
var map = {};
|
|
32
|
+
for (var k in resp.headers) {
|
|
33
|
+
var pair = String(k);
|
|
34
|
+
if (pair.charAt(0) === "[") { pair = pair.substring(1); }
|
|
35
|
+
if (pair.charAt(pair.length - 1) === "]") { pair = pair.substring(0, pair.length - 1); }
|
|
36
|
+
var idx = pair.indexOf(", ");
|
|
37
|
+
if (idx > -1) {
|
|
38
|
+
map[pair.substring(0, idx).toLowerCase()] = pair.substring(idx + 2);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return map;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns true when the node is `Script.Util.HttpRequest` or
|
|
47
|
+
* `Script.Util.HttpGet` (a MemberExpression, optionally the callee of `new`).
|
|
48
|
+
*
|
|
49
|
+
* @param {import('eslint').Rule.Node} node - MemberExpression to test
|
|
50
|
+
* @returns {boolean} Whether it references a Script.Util HTTP constructor
|
|
51
|
+
*/
|
|
52
|
+
function isHttpConstructorMember(node) {
|
|
53
|
+
return (
|
|
54
|
+
node.type === 'MemberExpression' &&
|
|
55
|
+
node.property.type === 'Identifier' &&
|
|
56
|
+
(node.property.name === 'HttpRequest' || node.property.name === 'HttpGet') &&
|
|
57
|
+
node.object.type === 'MemberExpression' &&
|
|
58
|
+
node.object.property.type === 'Identifier' &&
|
|
59
|
+
node.object.property.name === 'Util' &&
|
|
60
|
+
node.object.object.type === 'Identifier' &&
|
|
61
|
+
node.object.object.name === 'Script'
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns true when the node constructs a Script.Util HTTP request, i.e.
|
|
67
|
+
* `new Script.Util.HttpRequest(...)` or `Script.Util.HttpGet(...)` (the latter
|
|
68
|
+
* is commonly called without `new`).
|
|
69
|
+
*
|
|
70
|
+
* @param {import('eslint').Rule.Node} node - init expression of a declarator
|
|
71
|
+
* @returns {boolean} Whether the expression yields an HTTP request instance
|
|
72
|
+
*/
|
|
73
|
+
function isHttpRequestInit(node) {
|
|
74
|
+
if (!node) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (node.type === 'NewExpression') {
|
|
78
|
+
return isHttpConstructorMember(node.callee);
|
|
79
|
+
}
|
|
80
|
+
if (node.type === 'CallExpression') {
|
|
81
|
+
return isHttpConstructorMember(node.callee);
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default {
|
|
87
|
+
meta: {
|
|
88
|
+
type: 'problem',
|
|
89
|
+
hasSuggestions: true,
|
|
90
|
+
docs: {
|
|
91
|
+
description:
|
|
92
|
+
'Disallow CLR-unsafe reads of HttpResponse.headers (indexing, .Get(), .Item(), String()) — read headers via a for..in map instead',
|
|
93
|
+
},
|
|
94
|
+
messages: {
|
|
95
|
+
clrHeaderAccess:
|
|
96
|
+
'Reading `{{ text }}` throws "Use of Common Language Runtime (CLR) is not allowed" at runtime. ' +
|
|
97
|
+
'HttpResponse headers are only readable by enumerating with for..in — use a getHeaderMap() helper.',
|
|
98
|
+
insertHelperAndRewrite:
|
|
99
|
+
'Insert getHeaderMap() helper and read via getHeaderMap({{ respName }})[…]',
|
|
100
|
+
},
|
|
101
|
+
schema: [],
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
create(context) {
|
|
105
|
+
const sourceCode = context.sourceCode;
|
|
106
|
+
|
|
107
|
+
// Variable names that hold an HttpRequest/HttpGet instance.
|
|
108
|
+
const requestVariables = new Set();
|
|
109
|
+
// Variable names that hold the response of `<requestVar>.send()`.
|
|
110
|
+
const responseVariables = new Set();
|
|
111
|
+
// Deferred reports so name collection completes before we decide.
|
|
112
|
+
const pending = [];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns true when `node` is a `<requestVar>.send()` call.
|
|
116
|
+
*
|
|
117
|
+
* @param {import('eslint').Rule.Node} node - init expression
|
|
118
|
+
* @returns {boolean} Whether it is a tracked request's send() call
|
|
119
|
+
*/
|
|
120
|
+
function isTrackedSendCall(node) {
|
|
121
|
+
return Boolean(
|
|
122
|
+
node &&
|
|
123
|
+
node.type === 'CallExpression' &&
|
|
124
|
+
node.callee.type === 'MemberExpression' &&
|
|
125
|
+
node.callee.property.type === 'Identifier' &&
|
|
126
|
+
node.callee.property.name === 'send' &&
|
|
127
|
+
node.callee.object.type === 'Identifier' &&
|
|
128
|
+
requestVariables.has(node.callee.object.name),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns the response variable name for a `<obj>.headers` member
|
|
134
|
+
* expression when `<obj>` is a tracked response variable, else null.
|
|
135
|
+
*
|
|
136
|
+
* @param {import('eslint').Rule.Node} headersMember - the `.headers` MemberExpression
|
|
137
|
+
* @returns {string|null} Response variable name or null
|
|
138
|
+
*/
|
|
139
|
+
function trackedResponseName(headersMember) {
|
|
140
|
+
if (
|
|
141
|
+
headersMember.type === 'MemberExpression' &&
|
|
142
|
+
headersMember.property.type === 'Identifier' &&
|
|
143
|
+
headersMember.property.name === 'headers' &&
|
|
144
|
+
headersMember.object.type === 'Identifier' &&
|
|
145
|
+
responseVariables.has(headersMember.object.name)
|
|
146
|
+
) {
|
|
147
|
+
return headersMember.object.name;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Builds the suggestion descriptor: insert the helper at the top of the
|
|
154
|
+
* program (once) and rewrite the flagged access to use getHeaderMap().
|
|
155
|
+
*
|
|
156
|
+
* @param {import('eslint').Rule.Node} reportNode - node covered by the diagnostic
|
|
157
|
+
* @param {import('eslint').Rule.Node} keyNode - the header-key expression
|
|
158
|
+
* @param {string} respName - tracked response variable name
|
|
159
|
+
* @returns {object} ESLint suggestion object
|
|
160
|
+
*/
|
|
161
|
+
function buildSuggestion(reportNode, keyNode, respName) {
|
|
162
|
+
const keyText = keyNode ? sourceCode.getText(keyNode) : '';
|
|
163
|
+
return {
|
|
164
|
+
messageId: 'insertHelperAndRewrite',
|
|
165
|
+
data: { respName },
|
|
166
|
+
fix(fixer) {
|
|
167
|
+
const fixes = [
|
|
168
|
+
fixer.replaceText(reportNode, `getHeaderMap(${respName})[${keyText}]`),
|
|
169
|
+
];
|
|
170
|
+
// Insert the helper once, at the very top of the program.
|
|
171
|
+
if (!sourceCode.getText().includes('function getHeaderMap(')) {
|
|
172
|
+
const program = sourceCode.ast;
|
|
173
|
+
fixes.push(fixer.insertTextBefore(program, `${HEADER_MAP_HELPER}\n`));
|
|
174
|
+
}
|
|
175
|
+
return fixes;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
VariableDeclarator(node) {
|
|
182
|
+
if (node.id.type !== 'Identifier') {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (isHttpRequestInit(node.init)) {
|
|
186
|
+
requestVariables.add(node.id.name);
|
|
187
|
+
} else if (isTrackedSendCall(node.init)) {
|
|
188
|
+
responseVariables.add(node.id.name);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// resp.headers["Content-Type"] — computed index read.
|
|
193
|
+
'MemberExpression[computed=true]'(node) {
|
|
194
|
+
const respName = trackedResponseName(node.object);
|
|
195
|
+
if (!respName) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
pending.push({
|
|
199
|
+
reportNode: node,
|
|
200
|
+
keyNode: node.property,
|
|
201
|
+
respName,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
// resp.headers.Get(...) / resp.headers.Item(...) — CLR method call.
|
|
206
|
+
'CallExpression > MemberExpression.callee'(node) {
|
|
207
|
+
if (
|
|
208
|
+
node.property.type !== 'Identifier' ||
|
|
209
|
+
(node.property.name !== 'Get' && node.property.name !== 'Item')
|
|
210
|
+
) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const respName = trackedResponseName(node.object);
|
|
214
|
+
if (!respName) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const call = node.parent;
|
|
218
|
+
pending.push({
|
|
219
|
+
reportNode: call,
|
|
220
|
+
keyNode: call.arguments.length > 0 ? call.arguments[0] : null,
|
|
221
|
+
respName,
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
'Program:exit'() {
|
|
226
|
+
for (const { reportNode, keyNode, respName } of pending) {
|
|
227
|
+
context.report({
|
|
228
|
+
node: reportNode,
|
|
229
|
+
messageId: 'clrHeaderAccess',
|
|
230
|
+
data: { text: sourceCode.getText(reportNode) },
|
|
231
|
+
suggest: [buildSuggestion(reportNode, keyNode, respName)],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
};
|
|
@@ -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
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-require-string-clr-content
|
|
3
|
+
*
|
|
4
|
+
* The `content` property of an HttpResponse (returned by `req.send()` on a
|
|
5
|
+
* `Script.Util.HttpRequest` or `Script.Util.HttpGet`) is a CLR string object,
|
|
6
|
+
* not a native JavaScript string. Using it directly — passing it to
|
|
7
|
+
* `Platform.Function.ParseJSON()`, calling a string method on it, concatenating
|
|
8
|
+
* it, or assigning it to a variable used as a string — is unreliable in the
|
|
9
|
+
* SFMC SSJS (ES3/CLR) engine.
|
|
10
|
+
*
|
|
11
|
+
* The verified fix is to wrap the value with `String(resp.content)` before any
|
|
12
|
+
* further use, e.g. `Platform.Function.ParseJSON(String(resp.content))`.
|
|
13
|
+
*
|
|
14
|
+
* This rule tracks variables assigned from `req.send()` (where `req` came from a
|
|
15
|
+
* `Script.Util.HttpRequest`/`HttpGet`) and flags any read of their `.content`
|
|
16
|
+
* that is not already the direct argument of a `String(...)` call. The quick-fix
|
|
17
|
+
* wraps the access in `String(...)`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns true when the node is `Script.Util.HttpRequest` or
|
|
22
|
+
* `Script.Util.HttpGet` (a MemberExpression, optionally the callee of `new`).
|
|
23
|
+
*
|
|
24
|
+
* @param {import('eslint').Rule.Node} node - MemberExpression to test
|
|
25
|
+
* @returns {boolean} Whether it references a Script.Util HTTP constructor
|
|
26
|
+
*/
|
|
27
|
+
function isHttpConstructorMember(node) {
|
|
28
|
+
return (
|
|
29
|
+
node.type === 'MemberExpression' &&
|
|
30
|
+
node.property.type === 'Identifier' &&
|
|
31
|
+
(node.property.name === 'HttpRequest' || node.property.name === 'HttpGet') &&
|
|
32
|
+
node.object.type === 'MemberExpression' &&
|
|
33
|
+
node.object.property.type === 'Identifier' &&
|
|
34
|
+
node.object.property.name === 'Util' &&
|
|
35
|
+
node.object.object.type === 'Identifier' &&
|
|
36
|
+
node.object.object.name === 'Script'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true when the node constructs a Script.Util HTTP request, i.e.
|
|
42
|
+
* `new Script.Util.HttpRequest(...)` or `Script.Util.HttpGet(...)` (the latter
|
|
43
|
+
* is commonly called without `new`).
|
|
44
|
+
*
|
|
45
|
+
* @param {import('eslint').Rule.Node} node - init expression of a declarator
|
|
46
|
+
* @returns {boolean} Whether the expression yields an HTTP request instance
|
|
47
|
+
*/
|
|
48
|
+
function isHttpRequestInit(node) {
|
|
49
|
+
if (!node) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (node.type === 'NewExpression') {
|
|
53
|
+
return isHttpConstructorMember(node.callee);
|
|
54
|
+
}
|
|
55
|
+
if (node.type === 'CallExpression') {
|
|
56
|
+
return isHttpConstructorMember(node.callee);
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns true when the `.content` member is already the direct single argument
|
|
63
|
+
* of a `String(...)` call — the verified-safe pattern.
|
|
64
|
+
*
|
|
65
|
+
* @param {import('eslint').Rule.Node} node - the `.content` MemberExpression
|
|
66
|
+
* @returns {boolean} Whether it is wrapped by String(...)
|
|
67
|
+
*/
|
|
68
|
+
function isWrappedInString(node) {
|
|
69
|
+
const parent = node.parent;
|
|
70
|
+
return Boolean(
|
|
71
|
+
parent &&
|
|
72
|
+
parent.type === 'CallExpression' &&
|
|
73
|
+
parent.callee.type === 'Identifier' &&
|
|
74
|
+
parent.callee.name === 'String' &&
|
|
75
|
+
parent.arguments.length === 1 &&
|
|
76
|
+
parent.arguments[0] === node,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default {
|
|
81
|
+
meta: {
|
|
82
|
+
type: 'problem',
|
|
83
|
+
fixable: 'code',
|
|
84
|
+
docs: {
|
|
85
|
+
description:
|
|
86
|
+
'Require wrapping HttpResponse.content with String() before use — the raw CLR string is unreliable in the SSJS engine',
|
|
87
|
+
},
|
|
88
|
+
messages: {
|
|
89
|
+
clrContentAccess:
|
|
90
|
+
'Reading `{{ text }}` directly is unreliable — `content` is a CLR string, not a JavaScript string. ' +
|
|
91
|
+
'Wrap it with `String({{ text }})` before passing it to ParseJSON() or any string operation.',
|
|
92
|
+
},
|
|
93
|
+
schema: [],
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
create(context) {
|
|
97
|
+
const sourceCode = context.sourceCode;
|
|
98
|
+
|
|
99
|
+
// Variable names that hold an HttpRequest/HttpGet instance.
|
|
100
|
+
const requestVariables = new Set();
|
|
101
|
+
// Variable names that hold the response of `<requestVar>.send()`.
|
|
102
|
+
const responseVariables = new Set();
|
|
103
|
+
// Deferred reports so name collection completes before we decide.
|
|
104
|
+
const pending = [];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns true when `node` is a `<requestVar>.send()` call.
|
|
108
|
+
*
|
|
109
|
+
* @param {import('eslint').Rule.Node} node - init expression
|
|
110
|
+
* @returns {boolean} Whether it is a tracked request's send() call
|
|
111
|
+
*/
|
|
112
|
+
function isTrackedSendCall(node) {
|
|
113
|
+
return Boolean(
|
|
114
|
+
node &&
|
|
115
|
+
node.type === 'CallExpression' &&
|
|
116
|
+
node.callee.type === 'MemberExpression' &&
|
|
117
|
+
node.callee.property.type === 'Identifier' &&
|
|
118
|
+
node.callee.property.name === 'send' &&
|
|
119
|
+
node.callee.object.type === 'Identifier' &&
|
|
120
|
+
requestVariables.has(node.callee.object.name),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns true when `node` is a `<responseVar>.content` member read on a
|
|
126
|
+
* tracked response variable.
|
|
127
|
+
*
|
|
128
|
+
* @param {import('eslint').Rule.Node} node - MemberExpression to test
|
|
129
|
+
* @returns {boolean} Whether it reads .content on a tracked response var
|
|
130
|
+
*/
|
|
131
|
+
function isTrackedContentMember(node) {
|
|
132
|
+
return (
|
|
133
|
+
node.type === 'MemberExpression' &&
|
|
134
|
+
!node.computed &&
|
|
135
|
+
node.property.type === 'Identifier' &&
|
|
136
|
+
node.property.name === 'content' &&
|
|
137
|
+
node.object.type === 'Identifier' &&
|
|
138
|
+
responseVariables.has(node.object.name)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
VariableDeclarator(node) {
|
|
144
|
+
if (node.id.type !== 'Identifier') {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (isHttpRequestInit(node.init)) {
|
|
148
|
+
requestVariables.add(node.id.name);
|
|
149
|
+
} else if (isTrackedSendCall(node.init)) {
|
|
150
|
+
responseVariables.add(node.id.name);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// resp.content — non-computed member read on a tracked response var.
|
|
155
|
+
'MemberExpression[computed=false]'(node) {
|
|
156
|
+
if (!isTrackedContentMember(node)) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (isWrappedInString(node)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
pending.push(node);
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
'Program:exit'() {
|
|
166
|
+
for (const node of pending) {
|
|
167
|
+
const text = sourceCode.getText(node);
|
|
168
|
+
context.report({
|
|
169
|
+
node,
|
|
170
|
+
messageId: 'clrContentAccess',
|
|
171
|
+
data: { text },
|
|
172
|
+
fix(fixer) {
|
|
173
|
+
return fixer.replaceText(node, `String(${text})`);
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
};
|