eslint-plugin-sfmc 0.1.3 → 1.0.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.
Files changed (55) hide show
  1. package/README.md +43 -47
  2. package/docs/rules/ssjs/arg-types.md +72 -0
  3. package/docs/rules/ssjs/core-method-arity.md +72 -0
  4. package/docs/rules/ssjs/no-deprecated-function.md +56 -0
  5. package/docs/rules/ssjs/no-property-call.md +95 -0
  6. package/docs/rules/ssjs/no-unknown-function.md +57 -0
  7. package/package.json +18 -5
  8. package/src/ampscript-parser.js +11 -4
  9. package/src/ampscript-processor.js +9 -3
  10. package/src/index.js +23 -33
  11. package/src/processor.js +12 -4
  12. package/src/rules/amp/function-arity.js +3 -1
  13. package/src/rules/amp/naming-convention.js +7 -3
  14. package/src/rules/amp/no-deprecated-function.js +6 -2
  15. package/src/rules/amp/no-email-excluded-function.js +3 -1
  16. package/src/rules/amp/no-inline-statement.js +3 -1
  17. package/src/rules/amp/no-nested-ampscript-delimiter.js +5 -5
  18. package/src/rules/amp/no-nested-script-tag.js +3 -1
  19. package/src/rules/amp/no-smart-quotes.js +1 -1
  20. package/src/rules/amp/prefer-attribute-value.js +10 -4
  21. package/src/rules/amp/require-rowcount-check.js +3 -1
  22. package/src/rules/amp/require-variable-declaration.js +7 -3
  23. package/src/rules/ssjs/cache-loop-length.js +10 -4
  24. package/src/rules/ssjs/no-deprecated-function.js +181 -0
  25. package/src/rules/ssjs/no-hardcoded-credentials.js +3 -1
  26. package/src/rules/ssjs/no-property-call.js +136 -0
  27. package/src/rules/ssjs/no-treatascontent-injection.js +8 -4
  28. package/src/rules/ssjs/no-unavailable-method.js +30 -10
  29. package/src/rules/ssjs/no-unknown-function.js +236 -0
  30. package/src/rules/ssjs/no-unsupported-syntax.js +10 -5
  31. package/src/rules/ssjs/platform-function-arity.js +6 -2
  32. package/src/rules/ssjs/prefer-parsejson-safe-arg.js +17 -9
  33. package/src/rules/ssjs/prefer-platform-load-version.js +1 -4
  34. package/src/rules/ssjs/require-hasownproperty.js +34 -27
  35. package/src/rules/ssjs/require-platform-load-order.js +6 -2
  36. package/src/rules/ssjs/require-platform-load.js +32 -12
  37. package/src/rules/ssjs/ssjs-arg-types.js +345 -0
  38. package/src/rules/ssjs/ssjs-core-method-arity.js +176 -0
  39. package/src/ssjs-processor.js +3 -1
  40. package/docs/rules/ssjs/no-unknown-core-method.md +0 -46
  41. package/docs/rules/ssjs/no-unknown-http-method.md +0 -44
  42. package/docs/rules/ssjs/no-unknown-platform-client-browser.md +0 -44
  43. package/docs/rules/ssjs/no-unknown-platform-function.md +0 -44
  44. package/docs/rules/ssjs/no-unknown-platform-request.md +0 -43
  45. package/docs/rules/ssjs/no-unknown-platform-response.md +0 -43
  46. package/docs/rules/ssjs/no-unknown-platform-variable.md +0 -43
  47. package/docs/rules/ssjs/no-unknown-wsproxy-method.md +0 -46
  48. package/src/rules/ssjs/no-unknown-core-method.js +0 -103
  49. package/src/rules/ssjs/no-unknown-http-method.js +0 -41
  50. package/src/rules/ssjs/no-unknown-platform-client-browser.js +0 -50
  51. package/src/rules/ssjs/no-unknown-platform-function.js +0 -49
  52. package/src/rules/ssjs/no-unknown-platform-request.js +0 -50
  53. package/src/rules/ssjs/no-unknown-platform-response.js +0 -50
  54. package/src/rules/ssjs/no-unknown-platform-variable.js +0 -50
  55. package/src/rules/ssjs/no-unknown-wsproxy-method.js +0 -71
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
- // Lint standalone .ampscript/.amp and .ssjs files
21
- ...sfmc.configs.recommended,
20
+ // Lint standalone .ampscript/.amp and .ssjs files
21
+ ...sfmc.configs.recommended,
22
22
 
23
- // Lint AMPscript + SSJS embedded in .html files
24
- ...sfmc.configs.embedded,
23
+ // Lint AMPscript + SSJS embedded in .html files
24
+ ...sfmc.configs.embedded,
25
25
  ];
26
26
  ```
27
27
 
@@ -39,52 +39,49 @@ export default [
39
39
 
40
40
  ## AMPscript Rules (`amp-*`)
41
41
 
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 |
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) | 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 |
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 | 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 |
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 or requiresCoreLoad globals |
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-function`](docs/rules/ssjs/no-unknown-function.md) | `error` | Disallow unknown methods on Platform.*, HTTP, Core Library, and WSProxy |
71
+ | [`sfmc/ssjs-no-deprecated-function`](docs/rules/ssjs/no-deprecated-function.md) | `error` | Flag use of deprecated SFMC SSJS APIs (e.g. ContentArea, ContentAreaObj) |
72
+ | [`sfmc/ssjs-no-property-call`](docs/rules/ssjs/no-property-call.md) | `error` | Disallow calling Platform.Request/Response properties as functions |
73
+ | [`sfmc/ssjs-platform-function-arity`](docs/rules/ssjs/platform-function-arity.md) | `error` | Enforce correct arity for `Platform.Function.*` |
74
+ | [`sfmc/ssjs-require-platform-load-order`](docs/rules/ssjs/require-platform-load-order.md) | `error` | Require `Platform.Load()` before Core usage in order |
75
+ | [`sfmc/ssjs-no-hardcoded-credentials`](docs/rules/ssjs/no-hardcoded-credentials.md) | `error` | Flag hardcoded keys in encryption calls |
76
+ | [`sfmc/ssjs-cache-loop-length`](docs/rules/ssjs/cache-loop-length.md) | `warn` | Require caching `.length` in for-loops |
77
+ | [`sfmc/ssjs-require-hasownproperty`](docs/rules/ssjs/require-hasownproperty.md) | `warn` | Require `hasOwnProperty` guard in for-in loops |
78
+ | [`sfmc/ssjs-prefer-platform-load-version`](docs/rules/ssjs/prefer-platform-load-version.md) | `warn` | Enforce a minimum `Platform.Load` version string |
79
+ | [`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 |
80
+ | [`sfmc/ssjs-prefer-parsejson-safe-arg`](docs/rules/ssjs/prefer-parsejson-safe-arg.md) | `warn` | Require string coercion on `ParseJSON` argument |
81
+ | [`sfmc/ssjs-no-switch-default`](docs/rules/ssjs/no-switch-default.md) | `warn` | Disallow `default` clause in `switch` statements |
82
+ | [`sfmc/ssjs-no-treatascontent-injection`](docs/rules/ssjs/no-treatascontent-injection.md) | `warn` | Flag dynamic string concatenation in `TreatAsContent` calls |
83
+ | [`sfmc/ssjs-core-method-arity`](docs/rules/ssjs/core-method-arity.md) | `warn` | Enforce correct argument counts for Core Library object methods |
84
+ | [`sfmc/ssjs-arg-types`](docs/rules/ssjs/arg-types.md) | `warn` | Check that literal arguments match expected parameter types |
88
85
 
89
86
  ## Processors
90
87
 
@@ -94,7 +91,6 @@ export default [
94
91
  | `sfmc/ssjs` | Extract `<script runat="server">` (non-ampscript) |
95
92
  | `sfmc/sfmc` | Combined: extracts both AMPscript and SSJS from HTML |
96
93
 
97
-
98
94
  ## License
99
95
 
100
96
  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
+ ```
@@ -0,0 +1,56 @@
1
+ # ssjs-no-deprecated-function
2
+
3
+ Flags calls to deprecated SFMC SSJS APIs. Currently focuses on the **Content Areas** feature,
4
+ which has been retired and no longer allows creating or updating content.
5
+
6
+ ## What is flagged
7
+
8
+ | API | Reason |
9
+ |---|---|
10
+ | `ContentArea(…)` | Global alias; Content Areas are deprecated |
11
+ | `ContentAreaByName(…)` | Global alias; Content Areas are deprecated |
12
+ | `Platform.Function.ContentArea(…)` | Content Areas are deprecated |
13
+ | `Platform.Function.ContentAreaByName(…)` | Content Areas are deprecated |
14
+ | `ContentAreaObj.Init(…)` | ContentAreaObj class is deprecated |
15
+ | `ContentAreaObj.Add(…)` | ContentAreaObj class is deprecated |
16
+ | `ContentAreaObj.Retrieve(…)` | ContentAreaObj class is deprecated |
17
+ | `<contentAreaVar>.Update(…)` | Instance method on a deprecated ContentAreaObj variable |
18
+ | `<contentAreaVar>.Remove()` | Instance method on a deprecated ContentAreaObj variable |
19
+
20
+ ## Examples
21
+
22
+ ### ❌ Incorrect
23
+
24
+ ```js
25
+ // Global alias — deprecated
26
+ var html = ContentAreaByName('Public Content/MyBlock');
27
+
28
+ // Platform.Function — deprecated
29
+ var content = Platform.Function.ContentArea(12345);
30
+
31
+ // ContentAreaObj static method — deprecated
32
+ var results = ContentAreaObj.Retrieve({
33
+ Property: 'CustomerKey',
34
+ SimpleOperator: 'equals',
35
+ Value: 'myCA',
36
+ });
37
+
38
+ // ContentAreaObj instance — deprecated
39
+ var area = ContentAreaObj.Init('myCA');
40
+ var status = area.Update({ Name: 'Updated Name' });
41
+ ```
42
+
43
+ ### ✅ Correct
44
+
45
+ ```js
46
+ // Use Content Builder blocks via Platform.Function.ContentBlockByKey / ContentBlockById
47
+ var html = Platform.Function.ContentBlockByKey('Public Content/MyBlock');
48
+ var content = Platform.Function.ContentBlockById(12345);
49
+ ```
50
+
51
+ ## Rule details
52
+
53
+ - **Type:** `suggestion`
54
+ - **Fixable:** No
55
+ - **Recommended:** Yes
56
+ - **Strict:** Yes
@@ -0,0 +1,95 @@
1
+ # ssjs-no-property-call
2
+
3
+ Flags `Platform.Request` and `Platform.Response` members that are **properties**, not functions.
4
+ Calling them with parentheses `()` is a runtime error — they must be accessed without parentheses.
5
+
6
+ The rule is fixable:
7
+
8
+ - **No-arg calls** (`Property()`) → removes the `()` on both `Request` and `Response` properties.
9
+ - **Single-arg calls on `Platform.Response.*`** (`ContentType("text/html")`) → rewrites to an assignment (`= value`), because those two properties are read/write.
10
+ - **Any-arg calls on `Platform.Request.*`** → reported as an error with no auto-fix, because `Request` properties are read-only.
11
+
12
+ ## Properties affected
13
+
14
+ ### `Platform.Request` — read-only
15
+
16
+ | Property |
17
+ |---|
18
+ | `Browser` |
19
+ | `ClientIP` |
20
+ | `HasSSL` |
21
+ | `IsSSL` |
22
+ | `Method` |
23
+ | `QueryString` |
24
+ | `ReferrerURL` |
25
+ | `RequestURL` |
26
+ | `UserAgent` |
27
+
28
+ ### `Platform.Response` — read/write
29
+
30
+ | Property |
31
+ |---|
32
+ | `ContentType` |
33
+ | `CharacterSet` |
34
+
35
+ ## Examples
36
+
37
+ ### ❌ Incorrect — reading a property with `()`
38
+
39
+ ```js
40
+ var method = Platform.Request.Method(); // ← runtime error
41
+ var ip = Platform.Request.ClientIP(); // ← runtime error
42
+ var ct = Platform.Response.ContentType(); // ← runtime error
43
+ ```
44
+
45
+ Auto-fix removes the parentheses:
46
+
47
+ ```js
48
+ var method = Platform.Request.Method;
49
+ var ip = Platform.Request.ClientIP;
50
+ var ct = Platform.Response.ContentType;
51
+ ```
52
+
53
+ ### ❌ Incorrect — setting a `Platform.Response` property via function call
54
+
55
+ ```js
56
+ Platform.Response.ContentType("application/json"); // ← should be an assignment
57
+ Platform.Response.CharacterSet("UTF-8"); // ← should be an assignment
58
+ ```
59
+
60
+ Auto-fix converts to an assignment statement:
61
+
62
+ ```js
63
+ Platform.Response.ContentType = "application/json";
64
+ Platform.Response.CharacterSet = "UTF-8";
65
+ ```
66
+
67
+ ### ❌ Incorrect — attempting to set a read-only `Platform.Request` property (no fix)
68
+
69
+ ```js
70
+ Platform.Request.Method("POST"); // ← read-only, cannot be set
71
+ ```
72
+
73
+ ### ✅ Correct
74
+
75
+ ```js
76
+ // Reading properties — no parentheses
77
+ var method = Platform.Request.Method;
78
+ var ip = Platform.Request.ClientIP;
79
+ var ct = Platform.Response.ContentType;
80
+
81
+ // Setting writable Response properties — use assignment
82
+ Platform.Response.ContentType = "application/json";
83
+ Platform.Response.CharacterSet = "UTF-8";
84
+
85
+ // Calling real methods — parentheses are correct
86
+ Platform.Request.GetQueryStringParameter("id");
87
+ Platform.Response.Write("<p>Hello</p>");
88
+ ```
89
+
90
+ ## Rule details
91
+
92
+ - **Type:** `problem`
93
+ - **Fixable:** Yes (`code`)
94
+ - **Recommended:** Yes
95
+ - **Strict:** Yes
@@ -0,0 +1,57 @@
1
+ # ssjs-no-unknown-function
2
+
3
+ Flags calls to SFMC API methods that do not exist in the catalog. This single rule replaces the seven narrow `no-unknown-*` rules from earlier versions of the plugin.
4
+
5
+ ## Covered namespaces
6
+
7
+ | Namespace | Example |
8
+ |---|---|
9
+ | `Platform.Function` | `Platform.Function.LookupRows(…)` |
10
+ | `Platform.Variable` | `Platform.Variable.GetValue(…)` |
11
+ | `Platform.Request` | `Platform.Request.GetQueryStringParameter(…)` |
12
+ | `Platform.Response` | `Platform.Response.Write(…)` |
13
+ | `Platform.Recipient` | `Platform.Recipient.GetAttributeValue(…)` |
14
+ | `HTTP` | `HTTP.Get(…)` |
15
+ | Core Library instance methods | `de.Rows.Retrieve()` (tracked from `DataExtension.Init(…)`) |
16
+ | WSProxy instance methods | `api.retrieve(…)` (tracked from `new Script.Util.WSProxy()`) |
17
+
18
+ ## Examples
19
+
20
+ ### ❌ Incorrect
21
+
22
+ ```js
23
+ // Typo in Platform.Function method name
24
+ Platform.Function.FetchRows('MyDE', 'Status', 'Active');
25
+
26
+ // Non-existent WSProxy method
27
+ var api = new Script.Util.WSProxy();
28
+ api.getAllResults({ ObjectType: 'DataExtension' });
29
+
30
+ // Unknown Core Library method on a DataExtension instance
31
+ var de = DataExtension.Init('CustomerData');
32
+ de.ExportRows();
33
+ ```
34
+
35
+ ### ✅ Correct
36
+
37
+ ```js
38
+ Platform.Function.LookupRows('MyDE', 'Status', 'Active');
39
+
40
+ var api = new Script.Util.WSProxy();
41
+ api.retrieve({ ObjectType: 'DataExtension' }, ['CustomerKey', 'Name'], {});
42
+
43
+ var de = DataExtension.Init('CustomerData');
44
+ de.Rows.Retrieve();
45
+ ```
46
+
47
+ ## When to use
48
+
49
+ Enable this rule to catch typos, outdated method names, and unsupported API calls before
50
+ they cause runtime failures.
51
+
52
+ ## Rule details
53
+
54
+ - **Type:** `problem`
55
+ - **Fixable:** No
56
+ - **Recommended:** Yes
57
+ - **Strict:** Yes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-sfmc",
3
- "version": "0.1.3",
4
- "description": "ESLint plugin for Salesforce Marketing Cloud AMPscript and Server-Side JavaScript (SSJS)",
3
+ "version": "1.0.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.2.2"
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.0"
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
  }
@@ -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) lo = mid;
78
- else hi = mid - 1;
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') return;
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) return;
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') count++;
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) continue;
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) continue;
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.