eslint-plugin-sfmc 2.6.1 → 4.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.
- package/README.md +77 -65
- package/docs/rules/amp/no-mcn-unsupported.md +76 -0
- package/docs/rules/amp/no-unknown-function.md +2 -39
- package/docs/rules/hbs/helper-arity.md +55 -0
- package/docs/rules/hbs/no-mcn-unsupported.md +69 -0
- package/docs/rules/hbs/no-unknown-binding.md +49 -0
- package/docs/rules/hbs/no-unknown-helper.md +60 -0
- package/docs/rules/hbs/no-unsupported-construct.md +65 -0
- package/docs/rules/ssjs/no-mcn-unsupported.md +68 -0
- package/docs/rules/ssjs/no-unknown-function.md +4 -31
- package/package.json +14 -12
- package/src/ampscript-parser.js +1 -1
- package/src/ampscript-processor.js +22 -16
- package/src/handlebars-parser.js +195 -0
- package/src/index.js +108 -23
- package/src/processor.js +76 -16
- package/src/rules/amp/{arg-types.js → argument-types.js} +16 -12
- package/src/rules/amp/function-arity.js +9 -9
- package/src/rules/amp/no-inline-statement.js +5 -5
- package/src/rules/amp/no-loop-counter-assign.js +7 -7
- package/src/rules/amp/no-mcn-unsupported.js +102 -0
- package/src/rules/amp/no-nested-ampscript-delimiter.js +58 -66
- package/src/rules/amp/no-smart-quotes.js +41 -47
- package/src/rules/amp/no-unknown-function.js +2 -28
- package/src/rules/amp/prefer-attribute-value.js +7 -10
- package/src/rules/amp/require-rowcount-check.js +12 -15
- package/src/rules/amp/require-variable-declaration.js +3 -3
- package/src/rules/hbs/_shared.js +116 -0
- package/src/rules/hbs/helper-arity.js +97 -0
- package/src/rules/hbs/no-mcn-unsupported.js +164 -0
- package/src/rules/hbs/no-unknown-binding.js +74 -0
- package/src/rules/hbs/no-unknown-helper.js +79 -0
- package/src/rules/hbs/no-unsupported-construct.js +70 -0
- package/src/rules/ssjs/cache-loop-length.js +14 -17
- package/src/rules/ssjs/no-deprecated-function.js +8 -8
- package/src/rules/ssjs/no-hardcoded-credentials.js +3 -6
- package/src/rules/ssjs/no-mcn-unsupported.js +182 -0
- package/src/rules/ssjs/no-property-call.js +6 -9
- package/src/rules/ssjs/no-treatascontent-injection.js +6 -13
- package/src/rules/ssjs/no-unavailable-method.js +22 -22
- package/src/rules/ssjs/no-unknown-function.js +15 -59
- package/src/rules/ssjs/no-unsupported-syntax.js +5 -5
- package/src/rules/ssjs/{prefer-parsejson-safe-arg.js → prefer-parsejson-safe-argument.js} +13 -21
- package/src/rules/ssjs/require-hasownproperty.js +54 -42
- package/src/rules/ssjs/require-platform-load-order.js +1 -1
- package/src/rules/ssjs/require-platform-load.js +6 -6
- package/src/rules/ssjs/{ssjs-arg-types.js → ssjs-argument-types.js} +59 -46
- package/src/rules/ssjs/ssjs-core-method-arity.js +41 -15
- /package/src/rules/amp/{no-var-redeclaration.js → no-variable-redeclaration.js} +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# `sfmc/ssjs-no-mcn-unsupported`
|
|
2
|
+
|
|
3
|
+
> Disallow all Server-Side JavaScript API usage, which is unavailable in Marketing Cloud Next.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Type** | `problem` |
|
|
8
|
+
| **Default severity** | `error` in all `-next` configs; `off` otherwise |
|
|
9
|
+
| **Fixable** | — |
|
|
10
|
+
|
|
11
|
+
## Why This Rule Exists
|
|
12
|
+
|
|
13
|
+
Server-Side JavaScript is not available in Marketing Cloud Next (MCN) at all. When targeting MCN, every SSJS API usage must be rewritten in AMPscript or Handlebars. This rule flags all SSJS API surface usage:
|
|
14
|
+
|
|
15
|
+
| Construct | Example |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Any `Platform.*` call (incl. `Platform.Load`) | `Platform.Function.LookupRows(…)` |
|
|
18
|
+
| Any `HTTP.*` call | `HTTP.Get(…)` |
|
|
19
|
+
| Core Library instance calls | `de.Rows.Retrieve()` (tracked from `DataExtension.Init(…)`) |
|
|
20
|
+
| WSProxy instance calls | `api.retrieve(…)` |
|
|
21
|
+
| WSProxy construction | `new Script.Util.WSProxy()` |
|
|
22
|
+
|
|
23
|
+
Unlike [`sfmc/ssjs-no-unknown-function`](no-unknown-function.md), which only flags method names not in the catalog, this rule flags **every** SFMC API call — the whole SSJS block must be migrated.
|
|
24
|
+
|
|
25
|
+
## Settings
|
|
26
|
+
|
|
27
|
+
| Setting | Values | Default |
|
|
28
|
+
|---------|--------|---------|
|
|
29
|
+
| severity | `"error"` \| `"warn"` \| `"off"` | `"error"` in `-next` configs |
|
|
30
|
+
|
|
31
|
+
### Options
|
|
32
|
+
|
|
33
|
+
| Option | Type | Default | Description |
|
|
34
|
+
|--------|------|---------|-------------|
|
|
35
|
+
| `apiVersion` | `number` | — | Accepted for parity with the AMPscript and Handlebars MCN rules; currently has **no effect** because no MCN API version supports SSJS. |
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// eslint.config.js
|
|
41
|
+
rules: {
|
|
42
|
+
'sfmc/ssjs-no-mcn-unsupported': 'error'
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or use the built-in `recommended-next` / `strict-next` / `embedded-next` configs, which apply this rule automatically (and disable the other SSJS quality rules, since the entire block must be migrated).
|
|
47
|
+
|
|
48
|
+
**Flagged (every API call):**
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
Platform.Load('Core', '1.1.1');
|
|
52
|
+
Platform.Function.LookupRows('MyDE', 'Status', 'Active');
|
|
53
|
+
|
|
54
|
+
var de = DataExtension.Init('CustomerData');
|
|
55
|
+
de.Rows.Retrieve();
|
|
56
|
+
|
|
57
|
+
var api = new Script.Util.WSProxy();
|
|
58
|
+
api.retrieve({ ObjectType: 'DataExtension' }, ['CustomerKey'], {});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## When to Disable
|
|
62
|
+
|
|
63
|
+
Disable this rule when you are not targeting Marketing Cloud Next (the default `engagement` configs already leave it `off`).
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
// eslint.config.js
|
|
67
|
+
rules: { 'sfmc/ssjs-no-mcn-unsupported': 'off' }
|
|
68
|
+
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
When targeting
|
|
5
|
+
This rule only reports **unknown** method names. When targeting Marketing Cloud Next, SSJS is not supported at all — every SSJS API call is flagged by the dedicated [`sfmc/ssjs-no-mcn-unsupported`](no-mcn-unsupported.md) rule instead.
|
|
6
6
|
|
|
7
7
|
## Covered namespaces
|
|
8
8
|
|
|
@@ -19,14 +19,10 @@ When targeting **Marketing Cloud Next**, SSJS is not supported at all. Set `targ
|
|
|
19
19
|
|
|
20
20
|
## Options
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|--------|------|---------|-------------|
|
|
24
|
-
| `target` | `'engagement'` \| `'next'` | — | Target platform. Set to `'next'` to flag every SSJS API call (all of `Platform.*`, `HTTP.*`, Core Library, and WSProxy) as unsupported in Marketing Cloud Next. |
|
|
22
|
+
This rule takes no options.
|
|
25
23
|
|
|
26
24
|
## Examples
|
|
27
25
|
|
|
28
|
-
### Standard usage (Marketing Cloud Engagement)
|
|
29
|
-
|
|
30
26
|
#### ❌ Incorrect
|
|
31
27
|
|
|
32
28
|
```js
|
|
@@ -54,32 +50,9 @@ var de = DataExtension.Init('CustomerData');
|
|
|
54
50
|
de.Rows.Retrieve();
|
|
55
51
|
```
|
|
56
52
|
|
|
57
|
-
### MCN target (`target: 'next'`)
|
|
58
|
-
|
|
59
|
-
SSJS is not supported in Marketing Cloud Next. Configuring `target: 'next'` flags every SFMC API call within SSJS files, indicating they must be rewritten or removed.
|
|
60
|
-
|
|
61
|
-
```js
|
|
62
|
-
// eslint.config.js
|
|
63
|
-
rules: {
|
|
64
|
-
'sfmc/ssjs-no-unknown-function': ['error', { target: 'next' }]
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
Or use the built-in `recommended-next` / `strict-next` / `embedded-next` configs which apply this automatically (and disable all other SSJS quality rules, since the entire SSJS block must be migrated).
|
|
69
|
-
|
|
70
|
-
#### ❌ Flagged (with `target: 'next'`)
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
// All SFMC API calls are flagged — SSJS is not available in MCN
|
|
74
|
-
Platform.Function.LookupRows('MyDE', 'Status', 'Active');
|
|
75
|
-
|
|
76
|
-
var de = DataExtension.Init('CustomerData');
|
|
77
|
-
de.Rows.Retrieve();
|
|
78
|
-
```
|
|
79
|
-
|
|
80
53
|
## When to use
|
|
81
54
|
|
|
82
|
-
Enable this rule to catch typos
|
|
55
|
+
Enable this rule to catch typos and outdated method names before they cause runtime failures. When migrating content to Marketing Cloud Next, use [`sfmc/ssjs-no-mcn-unsupported`](no-mcn-unsupported.md) (applied automatically by the `recommended-next` / `strict-next` / `embedded-next` configs).
|
|
83
56
|
|
|
84
57
|
## Rule details
|
|
85
58
|
|
|
@@ -87,4 +60,4 @@ Enable this rule to catch typos, outdated method names, and unsupported API call
|
|
|
87
60
|
- **Fixable:** No
|
|
88
61
|
- **Recommended:** Yes (`engagement` mode)
|
|
89
62
|
- **Strict:** Yes (`engagement` mode)
|
|
90
|
-
- **`recommended-next` / `strict-next` / `embedded-next`:**
|
|
63
|
+
- **`recommended-next` / `strict-next` / `embedded-next`:** Off (superseded by `ssjs-no-mcn-unsupported`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-sfmc",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.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
|
-
"
|
|
36
|
-
"ampscript-
|
|
37
|
-
"
|
|
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": ">=
|
|
57
|
+
"node": ">=22.0.0"
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@eslint/js": "^10.0.1",
|
|
59
|
-
"eslint": "^10.
|
|
61
|
+
"eslint": "^10.6.0",
|
|
60
62
|
"eslint-config-prettier": "^10.1.8",
|
|
61
|
-
"eslint-plugin-jsdoc": "^
|
|
62
|
-
"eslint-plugin-prettier": "^5.5.
|
|
63
|
-
"eslint-plugin-unicorn": "^
|
|
64
|
-
"globals": "^17.
|
|
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.
|
|
67
|
-
"prettier": "^3.
|
|
68
|
+
"lint-staged": "^17.0.8",
|
|
69
|
+
"prettier": "^3.9.4"
|
|
68
70
|
},
|
|
69
71
|
"lint-staged": {
|
|
70
72
|
"*.{js,mjs,cjs}": [
|
package/src/ampscript-parser.js
CHANGED
|
@@ -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
|
|
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
|
|
95
|
-
index
|
|
96
|
-
|
|
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(
|
|
119
|
+
const innerCode = text.slice(expressionStart + 3, index - 3);
|
|
114
120
|
const wrappedBlock = `%%[${innerCode}]%%`;
|
|
115
|
-
const padding = '\n'.repeat(countNewlinesBefore(text,
|
|
121
|
+
const padding = '\n'.repeat(countNewlinesBefore(text, expressionStart));
|
|
116
122
|
|
|
117
123
|
blocks.push({
|
|
118
124
|
text: padding + wrappedBlock,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint-compatible parser for Marketing Cloud Next Handlebars.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the `@handlebars/parser` package and adds the metadata ESLint needs:
|
|
5
|
+
* - `range: [start, end]` on every node (derived from the parser's
|
|
6
|
+
* 1-based-line / 0-based-column `loc` via a line-start table)
|
|
7
|
+
* - `loc: { start: {line,column}, end: {line,column} }` (kept from the parser,
|
|
8
|
+
* whose convention already matches ESLint: 1-based line, 0-based column)
|
|
9
|
+
* - Root node typed as "Program" with `body`, `tokens`, `comments`
|
|
10
|
+
* - `visitorKeys` so ESLint knows how to traverse Handlebars node types
|
|
11
|
+
*
|
|
12
|
+
* AMPscript regions inside the source must be blanked out before this parser
|
|
13
|
+
* runs (the processor does this) so Handlebars never sees `%%[ ... ]%%` syntax.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { parse as hbsParse } from '@handlebars/parser';
|
|
17
|
+
|
|
18
|
+
// ── Node renaming for ESTree collisions ───────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// `BlockStatement` is also an ESTree node type, so ESLint's built-in scope
|
|
21
|
+
// analysis (eslint-scope) tries to open a lexical scope for it and crashes on
|
|
22
|
+
// the Handlebars shape. Likewise, a Handlebars `BlockStatement` contains nested
|
|
23
|
+
// `Program` nodes (its `program`/`inverse` bodies); eslint-scope treats every
|
|
24
|
+
// `Program` as a global scope and asserts when they nest. Rename both to
|
|
25
|
+
// Hbs-prefixed types so the JS analyzer leaves them alone — only the single
|
|
26
|
+
// synthetic root keeps the real `Program` type. The hbs rules and visitorKeys
|
|
27
|
+
// reference the renamed types.
|
|
28
|
+
|
|
29
|
+
const NODE_RENAME = {
|
|
30
|
+
BlockStatement: 'HbsBlockStatement',
|
|
31
|
+
Program: 'HbsProgram',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ── Visitor keys for all Handlebars AST node types ────────────────────────
|
|
35
|
+
|
|
36
|
+
export const visitorKeys = {
|
|
37
|
+
Program: ['body'],
|
|
38
|
+
HbsProgram: ['body'],
|
|
39
|
+
MustacheStatement: ['path', 'params', 'hash'],
|
|
40
|
+
HbsBlockStatement: ['path', 'params', 'hash', 'program', 'inverse'],
|
|
41
|
+
PartialStatement: ['name', 'params', 'hash'],
|
|
42
|
+
PartialBlockStatement: ['name', 'params', 'hash', 'program'],
|
|
43
|
+
DecoratorBlock: ['path', 'params', 'hash', 'program'],
|
|
44
|
+
Decorator: ['path', 'params', 'hash'],
|
|
45
|
+
ContentStatement: [],
|
|
46
|
+
CommentStatement: [],
|
|
47
|
+
SubExpression: ['path', 'params', 'hash'],
|
|
48
|
+
PathExpression: [],
|
|
49
|
+
StringLiteral: [],
|
|
50
|
+
NumberLiteral: [],
|
|
51
|
+
BooleanLiteral: [],
|
|
52
|
+
UndefinedLiteral: [],
|
|
53
|
+
NullLiteral: [],
|
|
54
|
+
Hash: ['pairs'],
|
|
55
|
+
HashPair: ['value'],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ── line/column → offset mapping ──────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function buildLineTable(text) {
|
|
61
|
+
const starts = [0];
|
|
62
|
+
for (let index = 0; index < text.length; index++) {
|
|
63
|
+
if (text[index] === '\n') {
|
|
64
|
+
starts.push(index + 1);
|
|
65
|
+
} else if (text[index] === '\r') {
|
|
66
|
+
if (text[index + 1] === '\n') {
|
|
67
|
+
starts.push(index + 2);
|
|
68
|
+
index++;
|
|
69
|
+
} else {
|
|
70
|
+
starts.push(index + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return starts;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function locToOffset(position, lineStarts, textLength) {
|
|
78
|
+
if (!position || typeof position.line !== 'number') {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const lineIndex = Math.max(0, position.line - 1);
|
|
82
|
+
const base = lineIndex < lineStarts.length ? lineStarts[lineIndex] : textLength;
|
|
83
|
+
const offset = base + Math.max(0, position.column);
|
|
84
|
+
return Math.min(offset, textLength);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── AST annotation ────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
function annotateNode(node, lineStarts, textLength, fallback) {
|
|
90
|
+
if (!node || typeof node !== 'object') {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Rename colliding ESTree types so ESLint's JS scope analysis ignores them.
|
|
95
|
+
if (node.type && Object.hasOwn(NODE_RENAME, node.type)) {
|
|
96
|
+
node.type = NODE_RENAME[node.type];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ESLint requires every traversed node to expose a numeric `range` and a
|
|
100
|
+
// matching `loc`. `@handlebars/parser` attaches `loc` to most nodes but a
|
|
101
|
+
// few (e.g. Hash, HashPair) can omit it — inherit the parent's span so the
|
|
102
|
+
// traverser never sees an undefined range.
|
|
103
|
+
let span = fallback;
|
|
104
|
+
if (node.loc && node.loc.start && node.loc.end) {
|
|
105
|
+
const start = locToOffset(node.loc.start, lineStarts, textLength);
|
|
106
|
+
const end = locToOffset(node.loc.end, lineStarts, textLength);
|
|
107
|
+
node.range = [start, end];
|
|
108
|
+
node.start = start;
|
|
109
|
+
node.end = end;
|
|
110
|
+
node.loc = {
|
|
111
|
+
start: { line: node.loc.start.line, column: node.loc.start.column },
|
|
112
|
+
end: { line: node.loc.end.line, column: node.loc.end.column },
|
|
113
|
+
};
|
|
114
|
+
span = { range: node.range, loc: node.loc };
|
|
115
|
+
} else if (fallback) {
|
|
116
|
+
node.range = [...fallback.range];
|
|
117
|
+
node.start = fallback.range[0];
|
|
118
|
+
node.end = fallback.range[1];
|
|
119
|
+
node.loc = {
|
|
120
|
+
start: { ...fallback.loc.start },
|
|
121
|
+
end: { ...fallback.loc.end },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const keys = visitorKeys[node.type];
|
|
126
|
+
if (!keys) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const key of keys) {
|
|
131
|
+
const child = node[key];
|
|
132
|
+
if (Array.isArray(child)) {
|
|
133
|
+
for (const item of child) {
|
|
134
|
+
if (item && typeof item === 'object') {
|
|
135
|
+
annotateNode(item, lineStarts, textLength, span);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else if (child && typeof child === 'object') {
|
|
139
|
+
annotateNode(child, lineStarts, textLength, span);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Public ESLint parser interface ────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export function parseForESLint(code) {
|
|
147
|
+
const lineStarts = buildLineTable(code);
|
|
148
|
+
|
|
149
|
+
let handlebarsAst;
|
|
150
|
+
try {
|
|
151
|
+
handlebarsAst = hbsParse(code);
|
|
152
|
+
} catch (ex) {
|
|
153
|
+
const loc = ex && ex.hash && ex.hash.loc;
|
|
154
|
+
const rawMessage = (ex && ex.message) || 'Handlebars syntax error.';
|
|
155
|
+
const messageLines = rawMessage.split('\n').filter((line) => line.trim().length > 0);
|
|
156
|
+
const error = new SyntaxError(messageLines.at(-1) || rawMessage);
|
|
157
|
+
error.lineNumber = loc ? Math.max(1, loc.first_line) : 1;
|
|
158
|
+
error.column = loc ? Math.max(1, loc.first_column + 1) : 1;
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const ast = {
|
|
163
|
+
type: 'Program',
|
|
164
|
+
body: handlebarsAst.body || [],
|
|
165
|
+
start: 0,
|
|
166
|
+
end: code.length,
|
|
167
|
+
range: [0, code.length],
|
|
168
|
+
loc: {
|
|
169
|
+
start: { line: 1, column: 0 },
|
|
170
|
+
end: handlebarsAst.loc
|
|
171
|
+
? { line: handlebarsAst.loc.end.line, column: handlebarsAst.loc.end.column }
|
|
172
|
+
: {
|
|
173
|
+
line: lineStarts.length,
|
|
174
|
+
column: code.length - lineStarts.at(-1),
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
tokens: [],
|
|
178
|
+
comments: [],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (const child of ast.body) {
|
|
182
|
+
annotateNode(child, lineStarts, code.length);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
ast,
|
|
187
|
+
visitorKeys,
|
|
188
|
+
services: {},
|
|
189
|
+
scopeManager: null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function parse(code) {
|
|
194
|
+
return parseForESLint(code).ast;
|
|
195
|
+
}
|