eslint-plugin-sfmc 0.1.3 → 0.2.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 +48 -47
- package/docs/rules/ssjs/arg-types.md +72 -0
- package/docs/rules/ssjs/core-method-arity.md +72 -0
- package/package.json +18 -5
- package/src/ampscript-parser.js +11 -4
- package/src/ampscript-processor.js +9 -3
- package/src/index.js +8 -0
- package/src/processor.js +12 -4
- package/src/rules/amp/function-arity.js +3 -1
- package/src/rules/amp/naming-convention.js +7 -3
- package/src/rules/amp/no-deprecated-function.js +6 -2
- package/src/rules/amp/no-email-excluded-function.js +3 -1
- package/src/rules/amp/no-inline-statement.js +3 -1
- package/src/rules/amp/no-nested-ampscript-delimiter.js +5 -5
- package/src/rules/amp/no-nested-script-tag.js +3 -1
- package/src/rules/amp/no-smart-quotes.js +1 -1
- package/src/rules/amp/prefer-attribute-value.js +10 -4
- package/src/rules/amp/require-rowcount-check.js +3 -1
- package/src/rules/amp/require-variable-declaration.js +7 -3
- package/src/rules/ssjs/cache-loop-length.js +10 -4
- package/src/rules/ssjs/no-hardcoded-credentials.js +3 -1
- package/src/rules/ssjs/no-treatascontent-injection.js +8 -4
- package/src/rules/ssjs/no-unavailable-method.js +30 -10
- package/src/rules/ssjs/no-unknown-core-method.js +27 -9
- package/src/rules/ssjs/no-unknown-http-method.js +9 -3
- package/src/rules/ssjs/no-unknown-platform-client-browser.js +9 -6
- package/src/rules/ssjs/no-unknown-platform-function.js +3 -1
- package/src/rules/ssjs/no-unknown-platform-request.js +3 -1
- package/src/rules/ssjs/no-unknown-platform-response.js +3 -1
- package/src/rules/ssjs/no-unknown-platform-variable.js +3 -1
- package/src/rules/ssjs/no-unknown-wsproxy-method.js +30 -13
- package/src/rules/ssjs/no-unsupported-syntax.js +10 -5
- package/src/rules/ssjs/platform-function-arity.js +6 -2
- package/src/rules/ssjs/prefer-parsejson-safe-arg.js +17 -9
- package/src/rules/ssjs/prefer-platform-load-version.js +1 -4
- package/src/rules/ssjs/require-hasownproperty.js +34 -27
- package/src/rules/ssjs/require-platform-load-order.js +6 -2
- package/src/rules/ssjs/require-platform-load.js +4 -3
- package/src/rules/ssjs/ssjs-arg-types.js +345 -0
- package/src/rules/ssjs/ssjs-core-method-arity.js +176 -0
- package/src/ssjs-processor.js +3 -1
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ Requires ESLint 9+ (flat config).
|
|
|
17
17
|
import sfmc from 'eslint-plugin-sfmc';
|
|
18
18
|
|
|
19
19
|
export default [
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Lint standalone .ampscript/.amp and .ssjs files
|
|
21
|
+
...sfmc.configs.recommended,
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// Lint AMPscript + SSJS embedded in .html files
|
|
24
|
+
...sfmc.configs.embedded,
|
|
25
25
|
];
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -39,52 +39,54 @@ export default [
|
|
|
39
39
|
|
|
40
40
|
## AMPscript Rules (`amp-*`)
|
|
41
41
|
|
|
42
|
-
| Rule
|
|
43
|
-
|
|
|
44
|
-
| [`sfmc/amp-no-unknown-function`](docs/rules/amp/no-unknown-function.md)
|
|
45
|
-
| [`sfmc/amp-function-arity`](docs/rules/amp/function-arity.md)
|
|
46
|
-
| [`sfmc/amp-set-requires-target`](docs/rules/amp/set-requires-target.md)
|
|
47
|
-
| [`sfmc/amp-no-smart-quotes`](docs/rules/amp/no-smart-quotes.md)
|
|
48
|
-
| [`sfmc/amp-no-var-redeclaration`](docs/rules/amp/no-var-redeclaration.md)
|
|
49
|
-
| [`sfmc/amp-no-empty-block`](docs/rules/amp/no-empty-block.md)
|
|
50
|
-
| [`sfmc/amp-no-loop-counter-assign`](docs/rules/amp/no-loop-counter-assign.md)
|
|
51
|
-
| [`sfmc/amp-no-inline-statement`](docs/rules/amp/no-inline-statement.md)
|
|
52
|
-
| [`sfmc/amp-no-deprecated-function`](docs/rules/amp/no-deprecated-function.md)
|
|
53
|
-
| [`sfmc/amp-naming-convention`](docs/rules/amp/naming-convention.md)
|
|
54
|
-
| [`sfmc/amp-no-empty-then`](docs/rules/amp/no-empty-then.md)
|
|
55
|
-
| [`sfmc/amp-require-rowcount-check`](docs/rules/amp/require-rowcount-check.md)
|
|
56
|
-
| [`sfmc/amp-no-html-comment`](docs/rules/amp/no-html-comment.md)
|
|
57
|
-
| [`sfmc/amp-no-js-line-comment`](docs/rules/amp/no-js-line-comment.md)
|
|
58
|
-
| [`sfmc/amp-no-nested-script-tag`](docs/rules/amp/no-nested-script-tag.md)
|
|
42
|
+
| Rule | Default | Description |
|
|
43
|
+
| ------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------ |
|
|
44
|
+
| [`sfmc/amp-no-unknown-function`](docs/rules/amp/no-unknown-function.md) | `error` | Disallow calls to unknown AMPscript functions |
|
|
45
|
+
| [`sfmc/amp-function-arity`](docs/rules/amp/function-arity.md) | `error` | Enforce correct argument counts |
|
|
46
|
+
| [`sfmc/amp-set-requires-target`](docs/rules/amp/set-requires-target.md) | `error` | Require `set` to have a target variable |
|
|
47
|
+
| [`sfmc/amp-no-smart-quotes`](docs/rules/amp/no-smart-quotes.md) | `error` | Disallow smart/curly quotes in strings |
|
|
48
|
+
| [`sfmc/amp-no-var-redeclaration`](docs/rules/amp/no-var-redeclaration.md) | `warn` | Disallow re-declaring a variable with `var` |
|
|
49
|
+
| [`sfmc/amp-no-empty-block`](docs/rules/amp/no-empty-block.md) | `warn` | Disallow empty `%%[ ]%%` blocks |
|
|
50
|
+
| [`sfmc/amp-no-loop-counter-assign`](docs/rules/amp/no-loop-counter-assign.md) | `warn` | Disallow assigning to the `for` loop counter |
|
|
51
|
+
| [`sfmc/amp-no-inline-statement`](docs/rules/amp/no-inline-statement.md) | `warn` | Disallow statements inside inline expressions |
|
|
52
|
+
| [`sfmc/amp-no-deprecated-function`](docs/rules/amp/no-deprecated-function.md) | `warn` | Flag deprecated functions and suggest replacements |
|
|
53
|
+
| [`sfmc/amp-naming-convention`](docs/rules/amp/naming-convention.md) | `warn` | Enforce variable naming convention |
|
|
54
|
+
| [`sfmc/amp-no-empty-then`](docs/rules/amp/no-empty-then.md) | `warn` | Disallow IF with empty THEN branch |
|
|
55
|
+
| [`sfmc/amp-require-rowcount-check`](docs/rules/amp/require-rowcount-check.md) | `warn` | Require RowCount check before FOR on LookupRows |
|
|
56
|
+
| [`sfmc/amp-no-html-comment`](docs/rules/amp/no-html-comment.md) | `warn` | Disallow HTML comments inside AMPscript blocks |
|
|
57
|
+
| [`sfmc/amp-no-js-line-comment`](docs/rules/amp/no-js-line-comment.md) | `warn` | Disallow JS-style `//` line comments in AMPscript |
|
|
58
|
+
| [`sfmc/amp-no-nested-script-tag`](docs/rules/amp/no-nested-script-tag.md) | `error` | Disallow `<script>` tags nested inside AMPscript script tags |
|
|
59
59
|
| [`sfmc/amp-no-nested-ampscript-delimiter`](docs/rules/amp/no-nested-ampscript-delimiter.md) | `error` | Disallow AMPscript delimiters nested inside AMPscript blocks |
|
|
60
|
-
| [`sfmc/amp-prefer-attribute-value`](docs/rules/amp/prefer-attribute-value.md)
|
|
61
|
-
| [`sfmc/amp-require-variable-declaration`](docs/rules/amp/require-variable-declaration.md)
|
|
62
|
-
| [`sfmc/amp-no-email-excluded-function`](docs/rules/amp/no-email-excluded-function.md)
|
|
60
|
+
| [`sfmc/amp-prefer-attribute-value`](docs/rules/amp/prefer-attribute-value.md) | off | Prefer `AttributeValue()` over bare personalization |
|
|
61
|
+
| [`sfmc/amp-require-variable-declaration`](docs/rules/amp/require-variable-declaration.md) | off | Require `var` before `set` |
|
|
62
|
+
| [`sfmc/amp-no-email-excluded-function`](docs/rules/amp/no-email-excluded-function.md) | off | Flag functions unavailable in email context |
|
|
63
63
|
|
|
64
64
|
## SSJS Rules (`ssjs-*`)
|
|
65
65
|
|
|
66
|
-
| Rule
|
|
67
|
-
|
|
|
68
|
-
| [`sfmc/ssjs-require-platform-load`](docs/rules/ssjs/require-platform-load.md)
|
|
69
|
-
| [`sfmc/ssjs-no-unsupported-syntax`](docs/rules/ssjs/no-unsupported-syntax.md)
|
|
70
|
-
| [`sfmc/ssjs-no-unknown-platform-function`](docs/rules/ssjs/no-unknown-platform-function.md)
|
|
71
|
-
| [`sfmc/ssjs-platform-function-arity`](docs/rules/ssjs/platform-function-arity.md)
|
|
72
|
-
| [`sfmc/ssjs-no-unknown-http-method`](docs/rules/ssjs/no-unknown-http-method.md)
|
|
73
|
-
| [`sfmc/ssjs-no-unknown-platform-variable`](docs/rules/ssjs/no-unknown-platform-variable.md)
|
|
74
|
-
| [`sfmc/ssjs-no-unknown-platform-response`](docs/rules/ssjs/no-unknown-platform-response.md)
|
|
75
|
-
| [`sfmc/ssjs-no-unknown-platform-request`](docs/rules/ssjs/no-unknown-platform-request.md)
|
|
76
|
-
| [`sfmc/ssjs-require-platform-load-order`](docs/rules/ssjs/require-platform-load-order.md)
|
|
77
|
-
| [`sfmc/ssjs-no-hardcoded-credentials`](docs/rules/ssjs/no-hardcoded-credentials.md)
|
|
78
|
-
| [`sfmc/ssjs-no-unknown-platform-client-browser`](docs/rules/ssjs/no-unknown-platform-client-browser.md) | `error` | Disallow unknown `Platform.ClientBrowser.*` methods
|
|
79
|
-
| [`sfmc/ssjs-no-unknown-core-method`](docs/rules/ssjs/no-unknown-core-method.md)
|
|
80
|
-
| [`sfmc/ssjs-no-unknown-wsproxy-method`](docs/rules/ssjs/no-unknown-wsproxy-method.md)
|
|
81
|
-
| [`sfmc/ssjs-cache-loop-length`](docs/rules/ssjs/cache-loop-length.md)
|
|
82
|
-
| [`sfmc/ssjs-require-hasownproperty`](docs/rules/ssjs/require-hasownproperty.md)
|
|
83
|
-
| [`sfmc/ssjs-prefer-platform-load-version`](docs/rules/ssjs/prefer-platform-load-version.md)
|
|
84
|
-
| [`sfmc/ssjs-no-unavailable-method`](docs/rules/ssjs/no-unavailable-method.md)
|
|
85
|
-
| [`sfmc/ssjs-prefer-parsejson-safe-arg`](docs/rules/ssjs/prefer-parsejson-safe-arg.md)
|
|
86
|
-
| [`sfmc/ssjs-no-switch-default`](docs/rules/ssjs/no-switch-default.md)
|
|
87
|
-
| [`sfmc/ssjs-no-treatascontent-injection`](docs/rules/ssjs/no-treatascontent-injection.md)
|
|
66
|
+
| Rule | Default | Description |
|
|
67
|
+
| ------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------- |
|
|
68
|
+
| [`sfmc/ssjs-require-platform-load`](docs/rules/ssjs/require-platform-load.md) | `error` | Require `Platform.Load("core")` before Core usage |
|
|
69
|
+
| [`sfmc/ssjs-no-unsupported-syntax`](docs/rules/ssjs/no-unsupported-syntax.md) | `error` | Flag ES6+ syntax not supported by SFMC |
|
|
70
|
+
| [`sfmc/ssjs-no-unknown-platform-function`](docs/rules/ssjs/no-unknown-platform-function.md) | `error` | Disallow unknown `Platform.Function.*` methods |
|
|
71
|
+
| [`sfmc/ssjs-platform-function-arity`](docs/rules/ssjs/platform-function-arity.md) | `error` | Enforce correct arity for `Platform.Function.*` |
|
|
72
|
+
| [`sfmc/ssjs-no-unknown-http-method`](docs/rules/ssjs/no-unknown-http-method.md) | `error` | Disallow unknown `HTTP.*` methods |
|
|
73
|
+
| [`sfmc/ssjs-no-unknown-platform-variable`](docs/rules/ssjs/no-unknown-platform-variable.md) | `error` | Disallow unknown `Platform.Variable.*` methods |
|
|
74
|
+
| [`sfmc/ssjs-no-unknown-platform-response`](docs/rules/ssjs/no-unknown-platform-response.md) | `error` | Disallow unknown `Platform.Response.*` methods |
|
|
75
|
+
| [`sfmc/ssjs-no-unknown-platform-request`](docs/rules/ssjs/no-unknown-platform-request.md) | `error` | Disallow unknown `Platform.Request.*` methods |
|
|
76
|
+
| [`sfmc/ssjs-require-platform-load-order`](docs/rules/ssjs/require-platform-load-order.md) | `error` | Require `Platform.Load()` before Core usage in order |
|
|
77
|
+
| [`sfmc/ssjs-no-hardcoded-credentials`](docs/rules/ssjs/no-hardcoded-credentials.md) | `error` | Flag hardcoded keys in encryption calls |
|
|
78
|
+
| [`sfmc/ssjs-no-unknown-platform-client-browser`](docs/rules/ssjs/no-unknown-platform-client-browser.md) | `error` | Disallow unknown `Platform.ClientBrowser.*` methods |
|
|
79
|
+
| [`sfmc/ssjs-no-unknown-core-method`](docs/rules/ssjs/no-unknown-core-method.md) | `warn` | Disallow unknown methods on Core library objects |
|
|
80
|
+
| [`sfmc/ssjs-no-unknown-wsproxy-method`](docs/rules/ssjs/no-unknown-wsproxy-method.md) | `warn` | Disallow unknown WSProxy methods |
|
|
81
|
+
| [`sfmc/ssjs-cache-loop-length`](docs/rules/ssjs/cache-loop-length.md) | `warn` | Require caching `.length` in for-loops |
|
|
82
|
+
| [`sfmc/ssjs-require-hasownproperty`](docs/rules/ssjs/require-hasownproperty.md) | `warn` | Require `hasOwnProperty` guard in for-in loops |
|
|
83
|
+
| [`sfmc/ssjs-prefer-platform-load-version`](docs/rules/ssjs/prefer-platform-load-version.md) | `warn` | Enforce a minimum `Platform.Load` version string |
|
|
84
|
+
| [`sfmc/ssjs-no-unavailable-method`](docs/rules/ssjs/no-unavailable-method.md) | `warn` | Flag Array/String methods unavailable or broken in SFMC's ES3 engine |
|
|
85
|
+
| [`sfmc/ssjs-prefer-parsejson-safe-arg`](docs/rules/ssjs/prefer-parsejson-safe-arg.md) | `warn` | Require string coercion on `ParseJSON` argument |
|
|
86
|
+
| [`sfmc/ssjs-no-switch-default`](docs/rules/ssjs/no-switch-default.md) | `warn` | Disallow `default` clause in `switch` statements |
|
|
87
|
+
| [`sfmc/ssjs-no-treatascontent-injection`](docs/rules/ssjs/no-treatascontent-injection.md) | `warn` | Flag dynamic string concatenation in `TreatAsContent` calls |
|
|
88
|
+
| [`sfmc/ssjs-core-method-arity`](docs/rules/ssjs/core-method-arity.md) | `warn` | Enforce correct argument counts for Core Library object methods |
|
|
89
|
+
| [`sfmc/ssjs-arg-types`](docs/rules/ssjs/arg-types.md) | `warn` | Check that literal arguments match expected parameter types |
|
|
88
90
|
|
|
89
91
|
## Processors
|
|
90
92
|
|
|
@@ -94,7 +96,6 @@ export default [
|
|
|
94
96
|
| `sfmc/ssjs` | Extract `<script runat="server">` (non-ampscript) |
|
|
95
97
|
| `sfmc/sfmc` | Combined: extracts both AMPscript and SSJS from HTML |
|
|
96
98
|
|
|
97
|
-
|
|
98
99
|
## License
|
|
99
100
|
|
|
100
101
|
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# `sfmc/ssjs-arg-types`
|
|
2
|
+
|
|
3
|
+
> Check that literal arguments match the expected parameter types for SSJS functions and methods.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Type** | `suggestion` |
|
|
8
|
+
| **Default severity** | `warn` in `recommended` and `strict` |
|
|
9
|
+
| **Fixable** | — |
|
|
10
|
+
|
|
11
|
+
## Why This Rule Exists
|
|
12
|
+
|
|
13
|
+
Many SSJS functions and methods have documented parameter types (`string`, `number`, `boolean`, `object`, `string[]`, etc.). Passing a literal value of the wrong type (e.g. a number where a string is required) is a common source of runtime errors. This rule validates every literal argument against the expected type declared in the ssjs-data catalog.
|
|
14
|
+
|
|
15
|
+
Only literal values are checked — variables, function call results, and other non-literal expressions are always treated as valid because their types cannot be determined statically.
|
|
16
|
+
|
|
17
|
+
## Coverage
|
|
18
|
+
|
|
19
|
+
| Namespace | Example |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `Platform.Function.*` | `Platform.Function.Lookup("MyDE", "Value", "Key", 42)` |
|
|
22
|
+
| `Platform.Variable.*` | `Platform.Variable.SetValue("varName", value)` |
|
|
23
|
+
| `Platform.Response.*` | `Platform.Response.SetResponseHeader("X-Header", 123)` |
|
|
24
|
+
| `Platform.Request.*` | `Platform.Request.GetQueryStringParameter(0)` |
|
|
25
|
+
| `Platform.Recipient.*` | `Platform.Recipient.GetAttributeValue(true)` |
|
|
26
|
+
| `HTTP.*` | `HTTP.Get(42)` |
|
|
27
|
+
| `HTTPHeader.*` | `HTTPHeader.SetValue("Content-Type", value)` |
|
|
28
|
+
| `Attribute.*` | `Attribute.GetValue(false)` |
|
|
29
|
+
| `WSProxy` instance methods | `proxy.retrieve(42, fieldArray, filter)` |
|
|
30
|
+
| Core Library static | `DataExtension.Init(123)` |
|
|
31
|
+
| Core Library instance | `de.Add(true)` |
|
|
32
|
+
| Global (`Format`, `String`, `Error`) | `Format(42, true)` |
|
|
33
|
+
|
|
34
|
+
## Settings
|
|
35
|
+
|
|
36
|
+
| Setting | Values | Default |
|
|
37
|
+
|---------|--------|---------|
|
|
38
|
+
| severity | `"error"` \| `"warn"` \| `"off"` | `"warn"` |
|
|
39
|
+
|
|
40
|
+
This rule has no configuration options.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
**Not allowed:**
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
/* Platform.Function.Lookup expects string for first arg */
|
|
48
|
+
var val = Platform.Function.Lookup(42, "Value", "Key", keyValue);
|
|
49
|
+
|
|
50
|
+
/* DataExtension.Init expects a string name, not a number */
|
|
51
|
+
var de = DataExtension.Init(123);
|
|
52
|
+
|
|
53
|
+
/* BounceEvent.Retrieve expects an object filter, not a string */
|
|
54
|
+
var rows = BounceEvent.Retrieve("all");
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Allowed:**
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
var val = Platform.Function.Lookup("MyDE", "Value", "Key", keyValue);
|
|
61
|
+
|
|
62
|
+
var de = DataExtension.Init("MyDE");
|
|
63
|
+
|
|
64
|
+
var rows = BounceEvent.Retrieve({ Property: "ID", SimpleOperator: "isNotNull", Value: "" });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## When to Disable
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// eslint.config.js
|
|
71
|
+
rules: { 'sfmc/ssjs-arg-types': 'off' }
|
|
72
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# `sfmc/ssjs-core-method-arity`
|
|
2
|
+
|
|
3
|
+
> Enforce correct argument counts for Core Library object method calls.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Type** | `problem` |
|
|
8
|
+
| **Default severity** | `warn` in `recommended`; `error` in `strict` |
|
|
9
|
+
| **Fixable** | — |
|
|
10
|
+
|
|
11
|
+
## Why This Rule Exists
|
|
12
|
+
|
|
13
|
+
Every method on a Core Library object has a documented minimum and maximum argument count. Passing too few arguments causes a runtime error; passing too many may fail silently or behave unexpectedly. This rule validates every Core Library method call against the known min/max argument counts from the ssjs-data catalog.
|
|
14
|
+
|
|
15
|
+
## Coverage
|
|
16
|
+
|
|
17
|
+
Both **static** and **instance** call styles are covered:
|
|
18
|
+
|
|
19
|
+
| Style | Example |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Instance | `var de = DataExtension.Init("MyDE"); de.Add(row);` |
|
|
22
|
+
| Static single-name | `BounceEvent.Retrieve(filter);` |
|
|
23
|
+
| Static multi-part | `DataExtension.Rows.Add(rowObj);` |
|
|
24
|
+
| Deep chain | `TriggeredSend.Tracking.Clicks.Retrieve(filter);` |
|
|
25
|
+
|
|
26
|
+
Covered Core Library classes: `Account`, `Account.Tracking`, `AccountUser`, `Portfolio`, `ContentAreaObj`, `Folder`, `Template`, `DeliveryProfile`, `SenderProfile`, `SendClassification`, `FilterDefinition`, `QueryDefinition`, `List`, `List.Subscribers`, `List.Subscribers.Tracking`, `Subscriber`, `Subscriber.Attributes`, `Subscriber.Lists`, `Email`, `Send`, `Send.Tracking`, `Send.Definition`, `TriggeredSend`, `TriggeredSend.Tracking`, `TriggeredSend.Tracking.Clicks`, `TriggeredSend.Tracking.TotalByInterval`, `DataExtension`, `DataExtension.Fields`, `DataExtension.Rows`, `DateTime.TimeZone` — and event classes: `BounceEvent`, `ClickEvent`, `OpenEvent`, `SentEvent`, `UnsubEvent`, `ForwardedEmailEvent`, `ForwardedEmailOptInEvent`, `SurveyEvent`.
|
|
27
|
+
|
|
28
|
+
## Settings
|
|
29
|
+
|
|
30
|
+
| Setting | Values | Default |
|
|
31
|
+
|---------|--------|---------|
|
|
32
|
+
| severity | `"error"` \| `"warn"` \| `"off"` | `"warn"` (`"error"` in `strict`) |
|
|
33
|
+
|
|
34
|
+
This rule has no configuration options.
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
**Not allowed:**
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
Platform.Load("core", "1.1.5");
|
|
42
|
+
|
|
43
|
+
/* DataExtension.Rows.Add requires exactly 1 argument */
|
|
44
|
+
DataExtension.Rows.Add("MyDE", extraArg);
|
|
45
|
+
|
|
46
|
+
/* BounceEvent.Retrieve requires exactly 1 argument */
|
|
47
|
+
BounceEvent.Retrieve();
|
|
48
|
+
|
|
49
|
+
/* de.Add requires exactly 1 argument */
|
|
50
|
+
var de = DataExtension.Init("MyDE");
|
|
51
|
+
de.Add(rowObj, extraArg);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Allowed:**
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
Platform.Load("core", "1.1.5");
|
|
58
|
+
|
|
59
|
+
DataExtension.Rows.Add(rowObj);
|
|
60
|
+
|
|
61
|
+
BounceEvent.Retrieve({ Property: "ID", SimpleOperator: "isNotNull", Value: "" });
|
|
62
|
+
|
|
63
|
+
var de = DataExtension.Init("MyDE");
|
|
64
|
+
de.Add(rowObj);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## When to Disable
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// eslint.config.js
|
|
71
|
+
rules: { 'sfmc/ssjs-core-method-arity': 'off' }
|
|
72
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-sfmc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ESLint plugin for Salesforce Marketing Cloud
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "ESLint plugin for Salesforce Marketing Cloud — AMPscript and Server-Side JavaScript (SSJS)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -34,19 +34,32 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"ampscript-data": "^0.1.3",
|
|
36
36
|
"ampscript-parser": "^0.1.1",
|
|
37
|
-
"ssjs-data": "^0.
|
|
37
|
+
"ssjs-data": "^0.3.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"eslint": ">=9.0.0"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
-
"test": "node --test tests/rules.test.js"
|
|
43
|
+
"test": "node --test tests/rules.test.js",
|
|
44
|
+
"lint": "eslint .",
|
|
45
|
+
"lint:fix": "eslint --fix .",
|
|
46
|
+
"prepare": "husky || true",
|
|
47
|
+
"format": "prettier --write .",
|
|
48
|
+
"format:check": "prettier --check ."
|
|
44
49
|
},
|
|
45
50
|
"license": "MIT",
|
|
46
51
|
"engines": {
|
|
47
52
|
"node": ">=18.0.0"
|
|
48
53
|
},
|
|
49
54
|
"devDependencies": {
|
|
50
|
-
"eslint": "^10.1
|
|
55
|
+
"@eslint/js": "^10.0.1",
|
|
56
|
+
"eslint": "^10.1.0",
|
|
57
|
+
"eslint-config-prettier": "^10.1.8",
|
|
58
|
+
"eslint-plugin-jsdoc": "^62.0.0",
|
|
59
|
+
"eslint-plugin-prettier": "^5.5.0",
|
|
60
|
+
"eslint-plugin-unicorn": "^64.0.0",
|
|
61
|
+
"globals": "^17.4.0",
|
|
62
|
+
"husky": "^9.1.7",
|
|
63
|
+
"prettier": "^3.8.2"
|
|
51
64
|
}
|
|
52
65
|
}
|
package/src/ampscript-parser.js
CHANGED
|
@@ -74,8 +74,11 @@ function offsetToLoc(offset, lineStarts) {
|
|
|
74
74
|
let hi = lineStarts.length - 1;
|
|
75
75
|
while (lo < hi) {
|
|
76
76
|
const mid = (lo + hi + 1) >> 1;
|
|
77
|
-
if (lineStarts[mid] <= offset)
|
|
78
|
-
|
|
77
|
+
if (lineStarts[mid] <= offset) {
|
|
78
|
+
lo = mid;
|
|
79
|
+
} else {
|
|
80
|
+
hi = mid - 1;
|
|
81
|
+
}
|
|
79
82
|
}
|
|
80
83
|
return { line: lo + 1, column: offset - lineStarts[lo] };
|
|
81
84
|
}
|
|
@@ -83,7 +86,9 @@ function offsetToLoc(offset, lineStarts) {
|
|
|
83
86
|
// ── AST annotation ────────────────────────────────────────────────────────
|
|
84
87
|
|
|
85
88
|
function annotateNode(node, lineStarts, comments) {
|
|
86
|
-
if (!node || typeof node !== 'object')
|
|
89
|
+
if (!node || typeof node !== 'object') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
87
92
|
|
|
88
93
|
// Rename colliding ESTree types to Amp-prefixed equivalents
|
|
89
94
|
if (node.type && NODE_RENAME[node.type]) {
|
|
@@ -103,7 +108,9 @@ function annotateNode(node, lineStarts, comments) {
|
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
const keys = visitorKeys[node.type];
|
|
106
|
-
if (!keys)
|
|
111
|
+
if (!keys) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
107
114
|
|
|
108
115
|
for (const key of keys) {
|
|
109
116
|
const child = node[key];
|
|
@@ -17,7 +17,9 @@ const SCRIPT_CLOSE_G = /<\/script\s*>/gi;
|
|
|
17
17
|
function countNewlinesBefore(text, pos) {
|
|
18
18
|
let count = 0;
|
|
19
19
|
for (let index = 0; index < pos; index++) {
|
|
20
|
-
if (text[index] === '\n')
|
|
20
|
+
if (text[index] === '\n') {
|
|
21
|
+
count++;
|
|
22
|
+
}
|
|
21
23
|
}
|
|
22
24
|
return count;
|
|
23
25
|
}
|
|
@@ -73,7 +75,9 @@ export function preprocess(text, filename) {
|
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
if (depth !== 0)
|
|
78
|
+
if (depth !== 0) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
77
81
|
|
|
78
82
|
const fullBlock = text.slice(blockStart, index);
|
|
79
83
|
const padding = '\n'.repeat(countNewlinesBefore(text, blockStart));
|
|
@@ -100,7 +104,9 @@ export function preprocess(text, filename) {
|
|
|
100
104
|
index++;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
if (!found)
|
|
107
|
+
if (!found) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
104
110
|
|
|
105
111
|
// Wrap in %%[ ]%% so the AMPscript parser receives valid block syntax.
|
|
106
112
|
// %%[ is 3 chars, same as %%= — column offsets for inner code are preserved.
|
package/src/index.js
CHANGED
|
@@ -56,6 +56,8 @@ import ssjsNoUnavailableMethod from './rules/ssjs/no-unavailable-method.js';
|
|
|
56
56
|
import ssjsPreferParsejsonSafeArg from './rules/ssjs/prefer-parsejson-safe-arg.js';
|
|
57
57
|
import ssjsNoSwitchDefault from './rules/ssjs/no-switch-default.js';
|
|
58
58
|
import ssjsNoTreatAsContentInjection from './rules/ssjs/no-treatascontent-injection.js';
|
|
59
|
+
import ssjsArgTypes from './rules/ssjs/ssjs-arg-types.js';
|
|
60
|
+
import ssjsCoreMethodArity from './rules/ssjs/ssjs-core-method-arity.js';
|
|
59
61
|
|
|
60
62
|
// ── Processors ────────────────────────────────────────────────────────────────
|
|
61
63
|
|
|
@@ -114,6 +116,8 @@ const plugin = {
|
|
|
114
116
|
'ssjs-prefer-parsejson-safe-arg': ssjsPreferParsejsonSafeArg,
|
|
115
117
|
'ssjs-no-switch-default': ssjsNoSwitchDefault,
|
|
116
118
|
'ssjs-no-treatascontent-injection': ssjsNoTreatAsContentInjection,
|
|
119
|
+
'ssjs-arg-types': ssjsArgTypes,
|
|
120
|
+
'ssjs-core-method-arity': ssjsCoreMethodArity,
|
|
117
121
|
},
|
|
118
122
|
|
|
119
123
|
processors: {
|
|
@@ -191,6 +195,8 @@ const ssjsRecommendedRules = {
|
|
|
191
195
|
'sfmc/ssjs-prefer-parsejson-safe-arg': 'warn',
|
|
192
196
|
'sfmc/ssjs-no-switch-default': 'warn',
|
|
193
197
|
'sfmc/ssjs-no-treatascontent-injection': 'warn',
|
|
198
|
+
'sfmc/ssjs-arg-types': 'warn',
|
|
199
|
+
'sfmc/ssjs-core-method-arity': 'warn',
|
|
194
200
|
'no-cond-assign': 'error',
|
|
195
201
|
};
|
|
196
202
|
|
|
@@ -215,6 +221,8 @@ const ssjsStrictRules = {
|
|
|
215
221
|
'sfmc/ssjs-prefer-parsejson-safe-arg': 'error',
|
|
216
222
|
'sfmc/ssjs-no-switch-default': 'error',
|
|
217
223
|
'sfmc/ssjs-no-treatascontent-injection': 'error',
|
|
224
|
+
'sfmc/ssjs-arg-types': 'warn',
|
|
225
|
+
'sfmc/ssjs-core-method-arity': 'error',
|
|
218
226
|
'no-cond-assign': 'error',
|
|
219
227
|
};
|
|
220
228
|
|
package/src/processor.js
CHANGED
|
@@ -22,7 +22,9 @@ const SCRIPT_CLOSE_G = /<\/script\s*>/gi;
|
|
|
22
22
|
function countNewlinesBefore(text, pos) {
|
|
23
23
|
let count = 0;
|
|
24
24
|
for (let index = 0; index < pos; index++) {
|
|
25
|
-
if (text[index] === '\n')
|
|
25
|
+
if (text[index] === '\n') {
|
|
26
|
+
count++;
|
|
27
|
+
}
|
|
26
28
|
}
|
|
27
29
|
return count;
|
|
28
30
|
}
|
|
@@ -76,7 +78,9 @@ export function preprocess(text, filename) {
|
|
|
76
78
|
index++;
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
if (depth !== 0)
|
|
81
|
+
if (depth !== 0) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
80
84
|
const fullBlock = text.slice(blockStart, index);
|
|
81
85
|
const padding = '\n'.repeat(countNewlinesBefore(text, blockStart));
|
|
82
86
|
blocks.push({
|
|
@@ -99,7 +103,9 @@ export function preprocess(text, filename) {
|
|
|
99
103
|
}
|
|
100
104
|
index++;
|
|
101
105
|
}
|
|
102
|
-
if (!found)
|
|
106
|
+
if (!found) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
103
109
|
const innerCode = text.slice(exprStart + 3, index - 3);
|
|
104
110
|
const wrappedBlock = `%%[${innerCode}]%%`;
|
|
105
111
|
const padding = '\n'.repeat(countNewlinesBefore(text, exprStart));
|
|
@@ -120,7 +126,9 @@ export function preprocess(text, filename) {
|
|
|
120
126
|
const openEnd = match.index + match[0].length;
|
|
121
127
|
SCRIPT_CLOSE_G.lastIndex = openEnd;
|
|
122
128
|
const closeMatch = SCRIPT_CLOSE_G.exec(text);
|
|
123
|
-
if (!closeMatch)
|
|
129
|
+
if (!closeMatch) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
124
132
|
|
|
125
133
|
const jsCode = text.slice(openEnd, closeMatch.index);
|
|
126
134
|
const linesBefore = text.slice(0, openEnd).split('\n');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforces a naming convention for
|
|
2
|
+
* Enforces a naming convention for `@variable` names in AMPscript.
|
|
3
3
|
*
|
|
4
4
|
* AMPscript variables are case-insensitive, but consistent casing improves
|
|
5
5
|
* readability and reduces confusion during code reviews.
|
|
@@ -39,10 +39,14 @@ export default {
|
|
|
39
39
|
return {
|
|
40
40
|
Variable(node) {
|
|
41
41
|
const name = node.value;
|
|
42
|
-
if (!name.startsWith('@') || name.startsWith('@@'))
|
|
42
|
+
if (!name.startsWith('@') || name.startsWith('@@')) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
43
45
|
|
|
44
46
|
const key = name.toLowerCase();
|
|
45
|
-
if (reported.has(key))
|
|
47
|
+
if (reported.has(key)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
46
50
|
|
|
47
51
|
if (!pattern.test(name)) {
|
|
48
52
|
reported.add(key);
|
|
@@ -30,10 +30,14 @@ export default {
|
|
|
30
30
|
return {
|
|
31
31
|
FunctionCall(node) {
|
|
32
32
|
const functionName = node.name || (node.callee && node.callee.name) || '';
|
|
33
|
-
if (!functionName)
|
|
33
|
+
if (!functionName) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
34
36
|
|
|
35
37
|
const entry = deprecatedFunctionLookup.get(functionName.toLowerCase());
|
|
36
|
-
if (!entry)
|
|
38
|
+
if (!entry) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
37
41
|
|
|
38
42
|
const report = {
|
|
39
43
|
node,
|
|
@@ -57,7 +57,9 @@ export default {
|
|
|
57
57
|
return {
|
|
58
58
|
FunctionCall(node) {
|
|
59
59
|
const functionName = node.name || (node.callee && node.callee.name) || '';
|
|
60
|
-
if (!functionName)
|
|
60
|
+
if (!functionName) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
61
63
|
|
|
62
64
|
if (isEmailExcluded(functionName)) {
|
|
63
65
|
context.report({
|
|
@@ -13,15 +13,13 @@ export default {
|
|
|
13
13
|
type: 'problem',
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
docs: {
|
|
16
|
-
description:
|
|
17
|
-
'Disallow %%[ or %%= delimiters inside an already-open AMPscript region.',
|
|
16
|
+
description: 'Disallow %%[ or %%= delimiters inside an already-open AMPscript region.',
|
|
18
17
|
recommended: true,
|
|
19
18
|
},
|
|
20
19
|
messages: {
|
|
21
20
|
nestedDelimiterInScript:
|
|
22
21
|
'AMPscript delimiter {{delimiter}} is not needed inside a <script language="ampscript"> block.',
|
|
23
|
-
nestedDelimiter:
|
|
24
|
-
'Nested {{delimiter}} inside an already-open AMPscript block.',
|
|
22
|
+
nestedDelimiter: 'Nested {{delimiter}} inside an already-open AMPscript block.',
|
|
25
23
|
},
|
|
26
24
|
schema: [],
|
|
27
25
|
},
|
|
@@ -66,7 +64,9 @@ export default {
|
|
|
66
64
|
const end = start + delimiter.length;
|
|
67
65
|
|
|
68
66
|
// Skip the outermost %%[ opener (it is the block wrapper, not nested)
|
|
69
|
-
if (!isScriptBlock && start === outerOpenStart)
|
|
67
|
+
if (!isScriptBlock && start === outerOpenStart) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
70
|
|
|
71
71
|
const closeToken = delimiter === '%%[' ? ']%%' : '=%%';
|
|
72
72
|
|
|
@@ -31,7 +31,9 @@ export default {
|
|
|
31
31
|
// %%[...]%% blocks cannot contain a nested <script> opening tag in
|
|
32
32
|
// a way the processor would expose, so skip non-script-tag blocks.
|
|
33
33
|
SCRIPT_OPEN_RE.lastIndex = 0;
|
|
34
|
-
if (!SCRIPT_OPEN_RE.test(text))
|
|
34
|
+
if (!SCRIPT_OPEN_RE.test(text)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
35
37
|
|
|
36
38
|
// Reset and find all script open tags in this virtual block.
|
|
37
39
|
SCRIPT_OPEN_RE.lastIndex = 0;
|
|
@@ -32,10 +32,16 @@ const AMPSCRIPT_KEYWORDS = new Set([
|
|
|
32
32
|
]);
|
|
33
33
|
|
|
34
34
|
function isLikelyPersonalization(node) {
|
|
35
|
-
if (node.type !== 'Identifier')
|
|
35
|
+
if (node.type !== 'Identifier') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
36
38
|
const lower = node.value.toLowerCase();
|
|
37
|
-
if (functionNames.has(lower))
|
|
38
|
-
|
|
39
|
+
if (functionNames.has(lower)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (AMPSCRIPT_KEYWORDS.has(lower)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
39
45
|
return true;
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -51,7 +57,7 @@ export default {
|
|
|
51
57
|
messages: {
|
|
52
58
|
preferAttributeValue:
|
|
53
59
|
'Use `AttributeValue("{{name}}")` instead of bare `{{name}}` to safely handle missing attributes.',
|
|
54
|
-
wrapWithAttributeValue:
|
|
60
|
+
wrapWithAttributeValue: "Wrap '{{name}}' in AttributeValue() for safe attribute access",
|
|
55
61
|
},
|
|
56
62
|
schema: [],
|
|
57
63
|
},
|
|
@@ -24,7 +24,9 @@ function isRowCountCall(node) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function extractRowCountVariables(node, into) {
|
|
27
|
-
if (!node)
|
|
27
|
+
if (!node) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
28
30
|
|
|
29
31
|
if (isRowCountCall(node) && node.arguments && node.arguments.length > 0) {
|
|
30
32
|
const argument = node.arguments[0];
|