eslint-plugin-sfmc 2.6.0 → 3.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 (43) hide show
  1. package/README.md +72 -63
  2. package/docs/rules/hbs/helper-arity.md +55 -0
  3. package/docs/rules/hbs/helper-too-new-for-target.md +62 -0
  4. package/docs/rules/hbs/no-unknown-binding.md +49 -0
  5. package/docs/rules/hbs/no-unknown-helper.md +60 -0
  6. package/docs/rules/hbs/no-unsupported-construct.md +65 -0
  7. package/package.json +14 -12
  8. package/src/ampscript-parser.js +1 -1
  9. package/src/ampscript-processor.js +22 -16
  10. package/src/handlebars-parser.js +195 -0
  11. package/src/index.js +90 -11
  12. package/src/processor.js +76 -16
  13. package/src/rules/amp/{arg-types.js → argument-types.js} +16 -12
  14. package/src/rules/amp/function-arity.js +9 -9
  15. package/src/rules/amp/no-inline-statement.js +5 -5
  16. package/src/rules/amp/no-loop-counter-assign.js +7 -7
  17. package/src/rules/amp/no-nested-ampscript-delimiter.js +1 -1
  18. package/src/rules/amp/no-smart-quotes.js +18 -18
  19. package/src/rules/amp/no-unknown-function.js +16 -3
  20. package/src/rules/amp/prefer-attribute-value.js +7 -10
  21. package/src/rules/amp/require-rowcount-check.js +12 -15
  22. package/src/rules/amp/require-variable-declaration.js +3 -3
  23. package/src/rules/hbs/_shared.js +116 -0
  24. package/src/rules/hbs/helper-arity.js +97 -0
  25. package/src/rules/hbs/helper-too-new-for-target.js +94 -0
  26. package/src/rules/hbs/no-unknown-binding.js +74 -0
  27. package/src/rules/hbs/no-unknown-helper.js +79 -0
  28. package/src/rules/hbs/no-unsupported-construct.js +70 -0
  29. package/src/rules/ssjs/cache-loop-length.js +14 -17
  30. package/src/rules/ssjs/no-deprecated-function.js +8 -8
  31. package/src/rules/ssjs/no-hardcoded-credentials.js +3 -6
  32. package/src/rules/ssjs/no-property-call.js +6 -9
  33. package/src/rules/ssjs/no-treatascontent-injection.js +6 -13
  34. package/src/rules/ssjs/no-unavailable-method.js +22 -22
  35. package/src/rules/ssjs/no-unknown-function.js +16 -12
  36. package/src/rules/ssjs/no-unsupported-syntax.js +5 -5
  37. package/src/rules/ssjs/{prefer-parsejson-safe-arg.js → prefer-parsejson-safe-argument.js} +13 -21
  38. package/src/rules/ssjs/require-hasownproperty.js +54 -42
  39. package/src/rules/ssjs/require-platform-load-order.js +1 -1
  40. package/src/rules/ssjs/require-platform-load.js +6 -6
  41. package/src/rules/ssjs/{ssjs-arg-types.js → ssjs-argument-types.js} +59 -46
  42. package/src/rules/ssjs/ssjs-core-method-arity.js +19 -15
  43. /package/src/rules/amp/{no-var-redeclaration.js → no-variable-redeclaration.js} +0 -0
package/README.md CHANGED
@@ -39,18 +39,18 @@ Add the following to your `.vscode/settings.json`:
39
39
 
40
40
  ```json
41
41
  {
42
- "eslint.validate": [
43
- "javascript",
44
- "javascriptreact",
45
- "typescript",
46
- "typescriptreact",
47
- "html",
48
- "vue",
49
- "markdown",
50
- "ampscript",
51
- "ssjs",
52
- "sfmc"
53
- ]
42
+ "eslint.validate": [
43
+ "javascript",
44
+ "javascriptreact",
45
+ "typescript",
46
+ "typescriptreact",
47
+ "html",
48
+ "vue",
49
+ "markdown",
50
+ "ampscript",
51
+ "ssjs",
52
+ "sfmc"
53
+ ]
54
54
  }
55
55
  ```
56
56
 
@@ -74,13 +74,13 @@ Add the following to your `.vscode/settings.json`:
74
74
 
75
75
  Use the `-next` config variants when targeting **Marketing Cloud Next (MCN)**. MCN supports only a subset of AMPscript functions and does **not** support SSJS at all.
76
76
 
77
- | Config | Files | What it does |
78
- | -------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------- |
79
- | `sfmc.configs['ampscript-next']` | `**/*.ampscript`, `**/*.amp` | AMPscript rules + flags functions unsupported in MCN (single config object) |
80
- | `sfmc.configs['ssjs-next']` | `**/*.ssjs` | Flags all SSJS API calls as MCN-unsupported; all other SSJS quality rules disabled |
81
- | `sfmc.configs['recommended-next']` | Both of the above | AMPscript MCN-aware + SSJS flagged for standalone files |
82
- | `sfmc.configs['embedded-next']` | `**/*.html` | AMPscript MCN-aware + SSJS flagged for HTML-embedded code |
83
- | `sfmc.configs['strict-next']` | All of the above + HTML | All AMPscript rules at `error` severity + MCN flag; SSJS fully flagged |
77
+ | Config | Files | What it does |
78
+ | ---------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------- |
79
+ | `sfmc.configs['ampscript-next']` | `**/*.ampscript`, `**/*.amp` | AMPscript rules + flags functions unsupported in MCN (single config object) |
80
+ | `sfmc.configs['ssjs-next']` | `**/*.ssjs` | Flags all SSJS API calls as MCN-unsupported; all other SSJS quality rules disabled |
81
+ | `sfmc.configs['recommended-next']` | Both of the above | AMPscript MCN-aware + SSJS flagged for standalone files |
82
+ | `sfmc.configs['embedded-next']` | `**/*.html` | AMPscript MCN-aware + SSJS flagged for HTML-embedded code |
83
+ | `sfmc.configs['strict-next']` | All of the above + HTML | All AMPscript rules at `error` severity + MCN flag; SSJS fully flagged |
84
84
 
85
85
  `recommended-next`, `embedded-next`, and `strict-next` are arrays — spread them with `...`.
86
86
 
@@ -88,58 +88,67 @@ Use the `-next` config variants when targeting **Marketing Cloud Next (MCN)**. M
88
88
  // eslint.config.js — targeting Marketing Cloud Next
89
89
  import sfmc from 'eslint-plugin-sfmc';
90
90
 
91
- export default [
92
- ...sfmc.configs['recommended-next'],
93
- ...sfmc.configs['embedded-next'],
94
- ];
91
+ export default [...sfmc.configs['recommended-next'], ...sfmc.configs['embedded-next']];
95
92
  ```
96
93
 
97
94
  ## AMPscript Rules (`amp-*`)
98
95
 
99
- | Rule | Default | Description |
100
- | ------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------ |
101
- | [`sfmc/amp-no-unknown-function`](docs/rules/amp/no-unknown-function.md) | `error` | Disallow calls to unknown AMPscript functions |
102
- | [`sfmc/amp-function-arity`](docs/rules/amp/function-arity.md) | `error` | Enforce correct argument counts |
96
+ | Rule | Default | Description |
97
+ | ------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------ |
98
+ | [`sfmc/amp-no-unknown-function`](docs/rules/amp/no-unknown-function.md) | `error` | Disallow calls to unknown AMPscript functions |
99
+ | [`sfmc/amp-function-arity`](docs/rules/amp/function-arity.md) | `error` | Enforce correct argument counts |
103
100
  | [`sfmc/amp-arg-types`](docs/rules/amp/arg-types.md) | `error` | Check that literal arguments match expected parameter types and allowed values |
104
- | [`sfmc/amp-set-requires-target`](docs/rules/amp/set-requires-target.md) | `error` | Require `set` to have a target variable |
105
- | [`sfmc/amp-no-smart-quotes`](docs/rules/amp/no-smart-quotes.md) | `error` | Disallow smart/curly quotes in strings |
106
- | [`sfmc/amp-no-var-redeclaration`](docs/rules/amp/no-var-redeclaration.md) | `warn` | Disallow re-declaring a variable with `var` |
107
- | [`sfmc/amp-no-empty-block`](docs/rules/amp/no-empty-block.md) | `warn` | Disallow empty `%%[ ]%%` blocks |
108
- | [`sfmc/amp-no-loop-counter-assign`](docs/rules/amp/no-loop-counter-assign.md) | `warn` | Disallow assigning to the `for` loop counter |
109
- | [`sfmc/amp-no-inline-statement`](docs/rules/amp/no-inline-statement.md) | `warn` | Disallow statements inside inline expressions |
110
- | [`sfmc/amp-no-deprecated-function`](docs/rules/amp/no-deprecated-function.md) | `warn` | Flag deprecated functions and suggest replacements |
111
- | [`sfmc/amp-naming-convention`](docs/rules/amp/naming-convention.md) | `warn` | Enforce variable naming convention |
112
- | [`sfmc/amp-no-empty-then`](docs/rules/amp/no-empty-then.md) | `warn` | Disallow IF with empty THEN branch |
113
- | [`sfmc/amp-require-rowcount-check`](docs/rules/amp/require-rowcount-check.md) | `warn` | Require RowCount check before FOR on LookupRows |
114
- | [`sfmc/amp-no-html-comment`](docs/rules/amp/no-html-comment.md) | `warn` | Disallow HTML comments inside AMPscript blocks |
115
- | [`sfmc/amp-no-js-line-comment`](docs/rules/amp/no-js-line-comment.md) | `warn` | Disallow JS-style `//` line comments in AMPscript |
116
- | [`sfmc/amp-no-nested-script-tag`](docs/rules/amp/no-nested-script-tag.md) | `error` | Disallow `<script>` tags nested inside AMPscript script tags |
117
- | [`sfmc/amp-no-nested-ampscript-delimiter`](docs/rules/amp/no-nested-ampscript-delimiter.md) | `error` | Disallow AMPscript delimiters nested inside AMPscript blocks |
118
- | [`sfmc/amp-prefer-attribute-value`](docs/rules/amp/prefer-attribute-value.md) | off | Prefer `AttributeValue()` over bare personalization |
119
- | [`sfmc/amp-require-variable-declaration`](docs/rules/amp/require-variable-declaration.md) | off | Require `var` before `set` |
120
- | [`sfmc/amp-no-email-excluded-function`](docs/rules/amp/no-email-excluded-function.md) | off | Flag functions unavailable in email context |
101
+ | [`sfmc/amp-set-requires-target`](docs/rules/amp/set-requires-target.md) | `error` | Require `set` to have a target variable |
102
+ | [`sfmc/amp-no-smart-quotes`](docs/rules/amp/no-smart-quotes.md) | `error` | Disallow smart/curly quotes in strings |
103
+ | [`sfmc/amp-no-var-redeclaration`](docs/rules/amp/no-var-redeclaration.md) | `warn` | Disallow re-declaring a variable with `var` |
104
+ | [`sfmc/amp-no-empty-block`](docs/rules/amp/no-empty-block.md) | `warn` | Disallow empty `%%[ ]%%` blocks |
105
+ | [`sfmc/amp-no-loop-counter-assign`](docs/rules/amp/no-loop-counter-assign.md) | `warn` | Disallow assigning to the `for` loop counter |
106
+ | [`sfmc/amp-no-inline-statement`](docs/rules/amp/no-inline-statement.md) | `warn` | Disallow statements inside inline expressions |
107
+ | [`sfmc/amp-no-deprecated-function`](docs/rules/amp/no-deprecated-function.md) | `warn` | Flag deprecated functions and suggest replacements |
108
+ | [`sfmc/amp-naming-convention`](docs/rules/amp/naming-convention.md) | `warn` | Enforce variable naming convention |
109
+ | [`sfmc/amp-no-empty-then`](docs/rules/amp/no-empty-then.md) | `warn` | Disallow IF with empty THEN branch |
110
+ | [`sfmc/amp-require-rowcount-check`](docs/rules/amp/require-rowcount-check.md) | `warn` | Require RowCount check before FOR on LookupRows |
111
+ | [`sfmc/amp-no-html-comment`](docs/rules/amp/no-html-comment.md) | `warn` | Disallow HTML comments inside AMPscript blocks |
112
+ | [`sfmc/amp-no-js-line-comment`](docs/rules/amp/no-js-line-comment.md) | `warn` | Disallow JS-style `//` line comments in AMPscript |
113
+ | [`sfmc/amp-no-nested-script-tag`](docs/rules/amp/no-nested-script-tag.md) | `error` | Disallow `<script>` tags nested inside AMPscript script tags |
114
+ | [`sfmc/amp-no-nested-ampscript-delimiter`](docs/rules/amp/no-nested-ampscript-delimiter.md) | `error` | Disallow AMPscript delimiters nested inside AMPscript blocks |
115
+ | [`sfmc/amp-prefer-attribute-value`](docs/rules/amp/prefer-attribute-value.md) | off | Prefer `AttributeValue()` over bare personalization |
116
+ | [`sfmc/amp-require-variable-declaration`](docs/rules/amp/require-variable-declaration.md) | off | Require `var` before `set` |
117
+ | [`sfmc/amp-no-email-excluded-function`](docs/rules/amp/no-email-excluded-function.md) | off | Flag functions unavailable in email context |
121
118
 
122
119
  ## SSJS Rules (`ssjs-*`)
123
120
 
124
- | Rule | Default | Description |
125
- | ------------------------------------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------- |
126
- | [`sfmc/ssjs-require-platform-load`](docs/rules/ssjs/require-platform-load.md) | `error` | Require `Platform.Load("core")` before Core or requiresCoreLoad globals |
127
- | [`sfmc/ssjs-no-unsupported-syntax`](docs/rules/ssjs/no-unsupported-syntax.md) | `error` | Flag ES6+ syntax not supported by SFMC |
128
- | [`sfmc/ssjs-no-unknown-function`](docs/rules/ssjs/no-unknown-function.md) | `error` | Disallow unknown methods on Platform.*, HTTP, Core Library, and WSProxy |
129
- | [`sfmc/ssjs-no-deprecated-function`](docs/rules/ssjs/no-deprecated-function.md) | `error` | Flag use of deprecated SFMC SSJS APIs (e.g. ContentArea, ContentAreaObj) |
130
- | [`sfmc/ssjs-no-property-call`](docs/rules/ssjs/no-property-call.md) | `error` | Disallow calling Platform.Request/Response properties as functions |
131
- | [`sfmc/ssjs-platform-function-arity`](docs/rules/ssjs/platform-function-arity.md) | `error` | Enforce correct arity for `Platform.Function.*` |
132
- | [`sfmc/ssjs-require-platform-load-order`](docs/rules/ssjs/require-platform-load-order.md) | `error` | Require `Platform.Load()` before Core usage in order |
133
- | [`sfmc/ssjs-no-hardcoded-credentials`](docs/rules/ssjs/no-hardcoded-credentials.md) | `error` | Flag hardcoded keys in encryption calls |
134
- | [`sfmc/ssjs-cache-loop-length`](docs/rules/ssjs/cache-loop-length.md) | `warn` | Require caching `.length` in for-loops |
135
- | [`sfmc/ssjs-require-hasownproperty`](docs/rules/ssjs/require-hasownproperty.md) | `warn` | Require `hasOwnProperty` guard in for-in loops |
136
- | [`sfmc/ssjs-prefer-platform-load-version`](docs/rules/ssjs/prefer-platform-load-version.md) | `warn` | Enforce a minimum `Platform.Load` version string |
137
- | [`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 |
138
- | [`sfmc/ssjs-prefer-parsejson-safe-arg`](docs/rules/ssjs/prefer-parsejson-safe-arg.md) | `warn` | Require string coercion on `ParseJSON` argument |
139
- | [`sfmc/ssjs-no-switch-default`](docs/rules/ssjs/no-switch-default.md) | `warn` | Disallow `default` clause in `switch` statements |
140
- | [`sfmc/ssjs-no-treatascontent-injection`](docs/rules/ssjs/no-treatascontent-injection.md) | `warn` | Flag dynamic string concatenation in `TreatAsContent` calls |
141
- | [`sfmc/ssjs-core-method-arity`](docs/rules/ssjs/core-method-arity.md) | `warn` | Enforce correct argument counts for Core Library object methods |
142
- | [`sfmc/ssjs-arg-types`](docs/rules/ssjs/arg-types.md) | `warn` | Check that literal arguments match expected parameter types |
121
+ | Rule | Default | Description |
122
+ | ------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------ |
123
+ | [`sfmc/ssjs-require-platform-load`](docs/rules/ssjs/require-platform-load.md) | `error` | Require `Platform.Load("core")` before Core or requiresCoreLoad globals |
124
+ | [`sfmc/ssjs-no-unsupported-syntax`](docs/rules/ssjs/no-unsupported-syntax.md) | `error` | Flag ES6+ syntax not supported by SFMC |
125
+ | [`sfmc/ssjs-no-unknown-function`](docs/rules/ssjs/no-unknown-function.md) | `error` | Disallow unknown methods on Platform.\*, HTTP, Core Library, and WSProxy |
126
+ | [`sfmc/ssjs-no-deprecated-function`](docs/rules/ssjs/no-deprecated-function.md) | `error` | Flag use of deprecated SFMC SSJS APIs (e.g. ContentArea, ContentAreaObj) |
127
+ | [`sfmc/ssjs-no-property-call`](docs/rules/ssjs/no-property-call.md) | `error` | Disallow calling Platform.Request/Response properties as functions |
128
+ | [`sfmc/ssjs-platform-function-arity`](docs/rules/ssjs/platform-function-arity.md) | `error` | Enforce correct arity for `Platform.Function.*` |
129
+ | [`sfmc/ssjs-require-platform-load-order`](docs/rules/ssjs/require-platform-load-order.md) | `error` | Require `Platform.Load()` before Core usage in order |
130
+ | [`sfmc/ssjs-no-hardcoded-credentials`](docs/rules/ssjs/no-hardcoded-credentials.md) | `error` | Flag hardcoded keys in encryption calls |
131
+ | [`sfmc/ssjs-cache-loop-length`](docs/rules/ssjs/cache-loop-length.md) | `warn` | Require caching `.length` in for-loops |
132
+ | [`sfmc/ssjs-require-hasownproperty`](docs/rules/ssjs/require-hasownproperty.md) | `warn` | Require `hasOwnProperty` guard in for-in loops |
133
+ | [`sfmc/ssjs-prefer-platform-load-version`](docs/rules/ssjs/prefer-platform-load-version.md) | `warn` | Enforce a minimum `Platform.Load` version string |
134
+ | [`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 |
135
+ | [`sfmc/ssjs-prefer-parsejson-safe-arg`](docs/rules/ssjs/prefer-parsejson-safe-arg.md) | `warn` | Require string coercion on `ParseJSON` argument |
136
+ | [`sfmc/ssjs-no-switch-default`](docs/rules/ssjs/no-switch-default.md) | `warn` | Disallow `default` clause in `switch` statements |
137
+ | [`sfmc/ssjs-no-treatascontent-injection`](docs/rules/ssjs/no-treatascontent-injection.md) | `warn` | Flag dynamic string concatenation in `TreatAsContent` calls |
138
+ | [`sfmc/ssjs-core-method-arity`](docs/rules/ssjs/core-method-arity.md) | `warn` | Enforce correct argument counts for Core Library object methods |
139
+ | [`sfmc/ssjs-arg-types`](docs/rules/ssjs/arg-types.md) | `warn` | Check that literal arguments match expected parameter types |
140
+
141
+ ## Handlebars Rules (`hbs-*`)
142
+
143
+ Handlebars is the templating language for **Marketing Cloud Next (MCN)** only. These rules are enabled at `error` severity in the `-next` configs and are `off` in the classic (Engagement) configs — in classic SFMC, `{{...}}` is plain content and must not be flagged.
144
+
145
+ | Rule | Default (`-next`) | Description |
146
+ | ----------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------- |
147
+ | [`sfmc/hbs-no-unknown-helper`](docs/rules/hbs/no-unknown-helper.md) | `error` | Disallow helper invocations that are not part of the MCN catalog |
148
+ | [`sfmc/hbs-no-unknown-binding`](docs/rules/hbs/no-unknown-binding.md) | `error` | Disallow unknown `{!$...}` built-in data bindings |
149
+ | [`sfmc/hbs-helper-arity`](docs/rules/hbs/helper-arity.md) | `error` | Enforce correct positional-argument counts for known helpers |
150
+ | [`sfmc/hbs-no-unsupported-construct`](docs/rules/hbs/no-unsupported-construct.md) | `error` | Disallow constructs unsupported by the MCN engine (partials, decorators, `log`) |
151
+ | [`sfmc/hbs-helper-too-new-for-target`](docs/rules/hbs/helper-too-new-for-target.md) | off | Flag helpers newer than the targeted MCN API version (opt-in via `apiVersion`) |
143
152
 
144
153
  ## Processors
145
154
 
@@ -0,0 +1,55 @@
1
+ # `sfmc/hbs-helper-arity`
2
+
3
+ > Enforce correct positional-argument counts for known Marketing Cloud Next Handlebars helpers.
4
+
5
+ | | |
6
+ |---|---|
7
+ | **Type** | `problem` |
8
+ | **Default severity** | `error` in the `-next` configs; `off` in classic configs |
9
+ | **Fixable** | — |
10
+
11
+ ## Why This Rule Exists
12
+
13
+ Each Marketing Cloud Next helper has a fixed set of positional parameters defined in the catalog. Calling a helper with too few required arguments, or with more arguments than it accepts, fails at render time. This rule validates every helper invocation against the helper's `params` definition.
14
+
15
+ Only **positional** arguments are checked. Helpers with a variadic trailing parameter (for example `concat`, `and`, `set`) have no upper bound. Hash (named) arguments are not counted as positional arguments. A bare `{{foo}}` mustache with no arguments is a data binding, not an invocation, and is skipped.
16
+
17
+ Handlebars only exists when targeting MCN, so this rule is enabled only in the `-next` configs.
18
+
19
+ ## Settings
20
+
21
+ | Setting | Values | Default |
22
+ |---------|--------|---------|
23
+ | severity | `"error"` \| `"warn"` \| `"off"` | `"error"` (in `-next` configs) |
24
+
25
+ This rule has no configuration options.
26
+
27
+ ## Examples
28
+
29
+ **Not allowed:**
30
+
31
+ ```handlebars
32
+ {{! formatDate requires a value and a format string }}
33
+ {{formatDate order.createdAt}}
34
+
35
+ {{! uppercase takes exactly one argument }}
36
+ {{uppercase firstName lastName}}
37
+ ```
38
+
39
+ **Allowed:**
40
+
41
+ ```handlebars
42
+ {{formatDate order.createdAt "yyyy-MM-dd"}}
43
+
44
+ {{uppercase firstName}}
45
+
46
+ {{! concat is variadic — any number of arguments is fine }}
47
+ {{concat firstName " " lastName}}
48
+ ```
49
+
50
+ ## When to Disable
51
+
52
+ ```js
53
+ // eslint.config.js
54
+ rules: { 'sfmc/hbs-helper-arity': 'off' }
55
+ ```
@@ -0,0 +1,62 @@
1
+ # `sfmc/hbs-helper-too-new-for-target`
2
+
3
+ > Disallow Handlebars helpers newer than the targeted Marketing Cloud Next API version.
4
+
5
+ | | |
6
+ |---|---|
7
+ | **Type** | `problem` |
8
+ | **Default severity** | `off` (opt-in — requires the `apiVersion` option) |
9
+ | **Fixable** | — |
10
+
11
+ ## Why This Rule Exists
12
+
13
+ Marketing Cloud Next ships new Handlebars helpers over time. Each helper in the catalog records the API version that introduced it (`mcnSince`). If your account targets an older API version, a newer helper is not yet available and fails at render time.
14
+
15
+ This rule flags any known helper whose introducing version (`mcnSince`) is greater than the API version you target. For example, `dateAdd` first shipped in MCN API version 67 (Summer '26); using it while targeting API version 65 (Winter '26) is reported.
16
+
17
+ The rule does nothing until you tell it which API version you target via the `apiVersion` option, which is why it defaults to `off` even in the `-next` configs. Set the option to opt in.
18
+
19
+ ## Settings
20
+
21
+ | Setting | Values | Default |
22
+ |---------|--------|---------|
23
+ | severity | `"error"` \| `"warn"` \| `"off"` | `"off"` |
24
+
25
+ ### Options
26
+
27
+ | Option | Type | Default | Description |
28
+ |--------|------|---------|-------------|
29
+ | `apiVersion` | `number` | — | The targeted Marketing Cloud Next API version (e.g. `65` = Winter '26, `67` = Summer '26). Helpers introduced after this version are flagged. When omitted, no helper is flagged. |
30
+
31
+ ## Examples
32
+
33
+ Configure the rule with the API version you target:
34
+
35
+ ```js
36
+ // eslint.config.js
37
+ rules: {
38
+ 'sfmc/hbs-helper-too-new-for-target': ['error', { apiVersion: 65 }]
39
+ }
40
+ ```
41
+
42
+ **Not allowed (with `apiVersion: 65`):**
43
+
44
+ ```handlebars
45
+ {{! dateAdd was introduced in API version 67 }}
46
+ {{dateAdd order.createdAt 7 "days"}}
47
+ ```
48
+
49
+ **Allowed (with `apiVersion: 65`):**
50
+
51
+ ```handlebars
52
+ {{formatDate order.createdAt "yyyy-MM-dd"}}
53
+ ```
54
+
55
+ ## When to Disable
56
+
57
+ Leave this rule `off` (its default) if you always target the latest MCN API version, or if you do not need to guard against version drift.
58
+
59
+ ```js
60
+ // eslint.config.js
61
+ rules: { 'sfmc/hbs-helper-too-new-for-target': 'off' }
62
+ ```
@@ -0,0 +1,49 @@
1
+ # `sfmc/hbs-no-unknown-binding`
2
+
3
+ > Disallow unknown `{!$...}` built-in data bindings in Marketing Cloud Next Handlebars.
4
+
5
+ | | |
6
+ |---|---|
7
+ | **Type** | `problem` |
8
+ | **Default severity** | `error` in the `-next` configs; `off` in classic configs |
9
+ | **Fixable** | — |
10
+
11
+ ## Why This Rule Exists
12
+
13
+ Marketing Cloud Next exposes a fixed set of built-in data bindings written as `{!$namespace.Field}` tokens (for example `{!$Context.MessageContext}`). These `{!$...}` tokens are **not** Handlebars helper syntax — the Handlebars parser treats them as literal content — so this rule scans the raw source text with a regex pass rather than walking the AST, mirroring the `handlebars/unknown-binding` diagnostic emitted by the language server.
14
+
15
+ An unrecognized binding name is a typo or a reference to a namespace/field that MCN does not provide, and resolves to empty output at render time. The rule offers a "did you mean" suggestion when a close match exists.
16
+
17
+ Handlebars only exists when targeting MCN, so this rule is enabled only in the `-next` configs.
18
+
19
+ ## Settings
20
+
21
+ | Setting | Values | Default |
22
+ |---------|--------|---------|
23
+ | severity | `"error"` \| `"warn"` \| `"off"` | `"error"` (in `-next` configs) |
24
+
25
+ This rule has no configuration options.
26
+
27
+ ## Examples
28
+
29
+ **Not allowed:**
30
+
31
+ ```handlebars
32
+ {{! 'Contect' is not a known binding namespace }}
33
+ {!$Contect.MessageContext}
34
+ ```
35
+
36
+ **Allowed:**
37
+
38
+ ```handlebars
39
+ {!$Context.MessageContext}
40
+ ```
41
+
42
+ ## When to Disable
43
+
44
+ Disable only if you use a binding that is available in your MCN account but missing from the bundled catalog (please report it so the catalog can be updated).
45
+
46
+ ```js
47
+ // eslint.config.js
48
+ rules: { 'sfmc/hbs-no-unknown-binding': 'off' }
49
+ ```
@@ -0,0 +1,60 @@
1
+ # `sfmc/hbs-no-unknown-helper`
2
+
3
+ > Disallow Handlebars helper invocations that are not part of the Marketing Cloud Next catalog.
4
+
5
+ | | |
6
+ |---|---|
7
+ | **Type** | `problem` |
8
+ | **Default severity** | `error` in the `-next` configs; `off` in classic configs |
9
+ | **Fixable** | — |
10
+
11
+ ## Why This Rule Exists
12
+
13
+ Marketing Cloud Next (MCN) runs a locked-down Handlebars engine that cannot register custom helpers. Every helper name must exist in the published MCN catalog. An unknown helper name is a typo or an unsupported construct that fails at render time. This rule flags any helper invocation whose name is not in the catalog and offers a "did you mean" suggestion when a close match exists.
14
+
15
+ A bare `{{foo}}` mustache with no arguments is treated as a **data binding**, not a helper invocation, and is not flagged by this rule. Helper detection applies to inline mustaches with arguments, subexpressions, and block helpers.
16
+
17
+ Handlebars only exists when targeting MCN, so this rule is enabled only in the `-next` configs. In classic (Marketing Cloud Engagement) configs it is disabled because `{{...}}` is plain content there.
18
+
19
+ ## Settings
20
+
21
+ | Setting | Values | Default |
22
+ |---------|--------|---------|
23
+ | severity | `"error"` \| `"warn"` \| `"off"` | `"error"` (in `-next` configs) |
24
+
25
+ This rule has no configuration options.
26
+
27
+ ## Examples
28
+
29
+ **Not allowed:**
30
+
31
+ ```handlebars
32
+ {{! 'formatDates' is not a catalog helper — did you mean 'formatDate'? }}
33
+ {{formatDates order.createdAt "yyyy-MM-dd"}}
34
+
35
+ {{#eachItem items}}
36
+ {{this.name}}
37
+ {{/eachItem}}
38
+ ```
39
+
40
+ **Allowed:**
41
+
42
+ ```handlebars
43
+ {{formatDate order.createdAt "yyyy-MM-dd"}}
44
+
45
+ {{#each items}}
46
+ {{this.name}}
47
+ {{/each}}
48
+
49
+ {{! bare binding, not a helper — never flagged }}
50
+ {{firstName}}
51
+ ```
52
+
53
+ ## When to Disable
54
+
55
+ Disable only if you rely on a helper that is genuinely available in your MCN account but missing from the bundled catalog (please report it so the catalog can be updated).
56
+
57
+ ```js
58
+ // eslint.config.js
59
+ rules: { 'sfmc/hbs-no-unknown-helper': 'off' }
60
+ ```
@@ -0,0 +1,65 @@
1
+ # `sfmc/hbs-no-unsupported-construct`
2
+
3
+ > Disallow Handlebars constructs unsupported by the Marketing Cloud Next engine (partials, decorators, log).
4
+
5
+ | | |
6
+ |---|---|
7
+ | **Type** | `problem` |
8
+ | **Default severity** | `error` in the `-next` configs; `off` in classic configs |
9
+ | **Fixable** | — |
10
+
11
+ ## Why This Rule Exists
12
+
13
+ The Marketing Cloud Next Handlebars engine is a locked-down subset of handlebars.js. Several standard handlebars.js constructs are not available and fail at render time:
14
+
15
+ - Partials — `{{> partialName}}`
16
+ - Partial blocks — `{{#> partialName}}...{{/partialName}}`
17
+ - Inline partials — `{{#*inline "name"}}...{{/inline}}`
18
+ - Decorators — `{{* decorator}}` and `{{#* decorator}}...{{/*decorator}}`
19
+ - The debugging helper `{{log}}`
20
+
21
+ This rule flags each of these, mirroring the `handlebars/unsupported-construct` diagnostic emitted by the language server. The full explanation for the specific construct is included in the reported message.
22
+
23
+ Handlebars only exists when targeting MCN, so this rule is enabled only in the `-next` configs.
24
+
25
+ ## Settings
26
+
27
+ | Setting | Values | Default |
28
+ |---------|--------|---------|
29
+ | severity | `"error"` \| `"warn"` \| `"off"` | `"error"` (in `-next` configs) |
30
+
31
+ This rule has no configuration options.
32
+
33
+ ## Examples
34
+
35
+ **Not allowed:**
36
+
37
+ ```handlebars
38
+ {{> header}}
39
+
40
+ {{#> layout}}
41
+ body
42
+ {{/layout}}
43
+
44
+ {{log "debugging"}}
45
+ ```
46
+
47
+ **Allowed:**
48
+
49
+ ```handlebars
50
+ {{! inline the shared markup instead of using a partial }}
51
+ <h1>{{title}}</h1>
52
+
53
+ {{#each items}}
54
+ {{this.name}}
55
+ {{/each}}
56
+ ```
57
+
58
+ ## When to Disable
59
+
60
+ These constructs do not run in MCN, so disabling the rule only hides a guaranteed render-time failure. Leave it enabled.
61
+
62
+ ```js
63
+ // eslint.config.js
64
+ rules: { 'sfmc/hbs-no-unsupported-construct': 'off' }
65
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-sfmc",
3
- "version": "2.6.0",
3
+ "version": "3.0.0",
4
4
  "description": "ESLint plugin for Salesforce Marketing Cloud — AMPscript and Server-Side JavaScript (SSJS)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -32,9 +32,11 @@
32
32
  "sfmc"
33
33
  ],
34
34
  "dependencies": {
35
- "ampscript-data": "^2.0.3",
36
- "ampscript-parser": "^0.1.3",
37
- "ssjs-data": "^0.10.1"
35
+ "@handlebars/parser": "^2.2.2",
36
+ "ampscript-data": "^3.0.0",
37
+ "ampscript-parser": "^0.2.0",
38
+ "handlebars-data": "^0.3.0",
39
+ "ssjs-data": "^0.14.0"
38
40
  },
39
41
  "peerDependencies": {
40
42
  "eslint": ">=9.0.0"
@@ -52,19 +54,19 @@
52
54
  },
53
55
  "license": "MIT",
54
56
  "engines": {
55
- "node": ">=18.0.0"
57
+ "node": ">=22.0.0"
56
58
  },
57
59
  "devDependencies": {
58
60
  "@eslint/js": "^10.0.1",
59
- "eslint": "^10.1.0",
61
+ "eslint": "^10.6.0",
60
62
  "eslint-config-prettier": "^10.1.8",
61
- "eslint-plugin-jsdoc": "^62.0.0",
62
- "eslint-plugin-prettier": "^5.5.0",
63
- "eslint-plugin-unicorn": "^64.0.0",
64
- "globals": "^17.4.0",
63
+ "eslint-plugin-jsdoc": "^63.0.10",
64
+ "eslint-plugin-prettier": "^5.5.6",
65
+ "eslint-plugin-unicorn": "^70.0.0",
66
+ "globals": "^17.7.0",
65
67
  "husky": "^9.1.7",
66
- "lint-staged": "^17.0.5",
67
- "prettier": "^3.8.2"
68
+ "lint-staged": "^17.0.8",
69
+ "prettier": "^3.9.4"
68
70
  },
69
71
  "lint-staged": {
70
72
  "*.{js,mjs,cjs}": [
@@ -91,7 +91,7 @@ function annotateNode(node, lineStarts, comments) {
91
91
  }
92
92
 
93
93
  // Rename colliding ESTree types to Amp-prefixed equivalents
94
- if (node.type && NODE_RENAME[node.type]) {
94
+ if (node.type && Object.hasOwn(NODE_RENAME, node.type)) {
95
95
  node.type = NODE_RENAME[node.type];
96
96
  }
97
97
 
@@ -24,6 +24,22 @@ function countNewlinesBefore(text, pos) {
24
24
  return count;
25
25
  }
26
26
 
27
+ /**
28
+ * Scans forward for the closing `=%%` of an inline AMPscript expression.
29
+ *
30
+ * @param {string} text - Full document text.
31
+ * @param {number} from - Index to start scanning from (just past the opening `%%=`).
32
+ * @returns {number} Index just past the closing `=%%`, or -1 when not found.
33
+ */
34
+ function findInlineExpressionEnd(text, from) {
35
+ for (let index = from; index < text.length; index++) {
36
+ if (text[index] === '=' && text[index + 1] === '%' && text[index + 2] === '%') {
37
+ return index + 3;
38
+ }
39
+ }
40
+ return -1;
41
+ }
42
+
27
43
  export function preprocess(text, filename) {
28
44
  const blocks = [];
29
45
  let index = 0;
@@ -91,28 +107,18 @@ export function preprocess(text, filename) {
91
107
 
92
108
  // %%= ... =%%
93
109
  if (text[index] === '%' && text[index + 1] === '%' && text[index + 2] === '=') {
94
- const exprStart = index;
95
- index += 3;
96
- let found = false;
97
-
98
- while (index < text.length) {
99
- if (text[index] === '=' && text[index + 1] === '%' && text[index + 2] === '%') {
100
- index += 3;
101
- found = true;
102
- break;
103
- }
104
- index++;
105
- }
106
-
107
- if (!found) {
110
+ const expressionStart = index;
111
+ const expressionEnd = findInlineExpressionEnd(text, index + 3);
112
+ if (expressionEnd === -1) {
108
113
  continue;
109
114
  }
115
+ index = expressionEnd;
110
116
 
111
117
  // Wrap in %%[ ]%% so the AMPscript parser receives valid block syntax.
112
118
  // %%[ is 3 chars, same as %%= — column offsets for inner code are preserved.
113
- const innerCode = text.slice(exprStart + 3, index - 3);
119
+ const innerCode = text.slice(expressionStart + 3, index - 3);
114
120
  const wrappedBlock = `%%[${innerCode}]%%`;
115
- const padding = '\n'.repeat(countNewlinesBefore(text, exprStart));
121
+ const padding = '\n'.repeat(countNewlinesBefore(text, expressionStart));
116
122
 
117
123
  blocks.push({
118
124
  text: padding + wrappedBlock,