eslint-plugin-sfmc 3.0.0 → 4.0.1
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 +60 -57
- package/docs/rules/amp/no-mcn-unsupported.md +76 -0
- package/docs/rules/amp/no-unknown-function.md +2 -39
- package/docs/rules/hbs/no-mcn-unsupported.md +69 -0
- package/docs/rules/ssjs/no-mcn-unsupported.md +68 -0
- package/docs/rules/ssjs/no-unknown-function.md +4 -31
- package/package.json +4 -4
- package/src/index.js +22 -16
- 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 +25 -31
- package/src/rules/amp/no-unknown-function.js +2 -41
- package/src/rules/hbs/no-mcn-unsupported.js +164 -0
- package/src/rules/ssjs/no-mcn-unsupported.js +182 -0
- package/src/rules/ssjs/no-unknown-function.js +1 -49
- package/src/rules/ssjs/ssjs-core-method-arity.js +22 -0
- package/docs/rules/hbs/helper-too-new-for-target.md +0 -62
- package/src/rules/hbs/helper-too-new-for-target.js +0 -94
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as handlebarsParser from './handlebars-parser.js';
|
|
|
15
15
|
// ── AMPscript rules ───────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
17
|
import ampNoUnknownFunction from './rules/amp/no-unknown-function.js';
|
|
18
|
+
import ampNoMcnUnsupported from './rules/amp/no-mcn-unsupported.js';
|
|
18
19
|
import ampNoVariableRedeclaration from './rules/amp/no-variable-redeclaration.js';
|
|
19
20
|
import ampSetRequiresTarget from './rules/amp/set-requires-target.js';
|
|
20
21
|
import ampNoEmptyBlock from './rules/amp/no-empty-block.js';
|
|
@@ -40,6 +41,7 @@ import ampNoNestedAmpscriptDelimiter from './rules/amp/no-nested-ampscript-delim
|
|
|
40
41
|
import ssjsRequirePlatformLoad from './rules/ssjs/require-platform-load.js';
|
|
41
42
|
import ssjsNoUnsupportedSyntax from './rules/ssjs/no-unsupported-syntax.js';
|
|
42
43
|
import ssjsNoUnknownFunction from './rules/ssjs/no-unknown-function.js';
|
|
44
|
+
import ssjsNoMcnUnsupported from './rules/ssjs/no-mcn-unsupported.js';
|
|
43
45
|
import ssjsNoDeprecatedFunction from './rules/ssjs/no-deprecated-function.js';
|
|
44
46
|
import ssjsNoPropertyCall from './rules/ssjs/no-property-call.js';
|
|
45
47
|
import ssjsPlatformFunctionArity from './rules/ssjs/platform-function-arity.js';
|
|
@@ -58,7 +60,7 @@ import ssjsCoreMethodArity from './rules/ssjs/ssjs-core-method-arity.js';
|
|
|
58
60
|
// ── Handlebars (MCN) rules ──────────────────────────────────────────────────────
|
|
59
61
|
|
|
60
62
|
import hbsNoUnknownHelper from './rules/hbs/no-unknown-helper.js';
|
|
61
|
-
import
|
|
63
|
+
import hbsNoMcnUnsupported from './rules/hbs/no-mcn-unsupported.js';
|
|
62
64
|
import hbsNoUnknownBinding from './rules/hbs/no-unknown-binding.js';
|
|
63
65
|
import hbsHelperArity from './rules/hbs/helper-arity.js';
|
|
64
66
|
import hbsNoUnsupportedConstruct from './rules/hbs/no-unsupported-construct.js';
|
|
@@ -83,6 +85,7 @@ const plugin = {
|
|
|
83
85
|
rules: {
|
|
84
86
|
// AMPscript rules (amp- prefix)
|
|
85
87
|
'amp-no-unknown-function': ampNoUnknownFunction,
|
|
88
|
+
'amp-no-mcn-unsupported': ampNoMcnUnsupported,
|
|
86
89
|
'amp-no-var-redeclaration': ampNoVariableRedeclaration,
|
|
87
90
|
'amp-set-requires-target': ampSetRequiresTarget,
|
|
88
91
|
'amp-no-empty-block': ampNoEmptyBlock,
|
|
@@ -107,6 +110,7 @@ const plugin = {
|
|
|
107
110
|
'ssjs-require-platform-load': ssjsRequirePlatformLoad,
|
|
108
111
|
'ssjs-no-unsupported-syntax': ssjsNoUnsupportedSyntax,
|
|
109
112
|
'ssjs-no-unknown-function': ssjsNoUnknownFunction,
|
|
113
|
+
'ssjs-no-mcn-unsupported': ssjsNoMcnUnsupported,
|
|
110
114
|
'ssjs-no-deprecated-function': ssjsNoDeprecatedFunction,
|
|
111
115
|
'ssjs-no-property-call': ssjsNoPropertyCall,
|
|
112
116
|
'ssjs-platform-function-arity': ssjsPlatformFunctionArity,
|
|
@@ -124,7 +128,7 @@ const plugin = {
|
|
|
124
128
|
|
|
125
129
|
// Handlebars (MCN) rules (hbs- prefix)
|
|
126
130
|
'hbs-no-unknown-helper': hbsNoUnknownHelper,
|
|
127
|
-
'hbs-
|
|
131
|
+
'hbs-no-mcn-unsupported': hbsNoMcnUnsupported,
|
|
128
132
|
'hbs-no-unknown-binding': hbsNoUnknownBinding,
|
|
129
133
|
'hbs-helper-arity': hbsHelperArity,
|
|
130
134
|
'hbs-no-unsupported-construct': hbsNoUnsupportedConstruct,
|
|
@@ -141,11 +145,13 @@ const plugin = {
|
|
|
141
145
|
|
|
142
146
|
/**
|
|
143
147
|
* SSJS rules for MCN targets.
|
|
144
|
-
* All quality rules are disabled — only the presence of SSJS is flagged,
|
|
145
|
-
* because SSJS as a whole must be
|
|
148
|
+
* All quality rules are disabled — only the presence of SSJS is flagged, via the
|
|
149
|
+
* dedicated `ssjs-no-mcn-unsupported` rule, because SSJS as a whole must be
|
|
150
|
+
* deleted when targeting Marketing Cloud Next.
|
|
146
151
|
*/
|
|
147
152
|
const ssjsMcnRules = {
|
|
148
|
-
'sfmc/ssjs-no-
|
|
153
|
+
'sfmc/ssjs-no-mcn-unsupported': 'error',
|
|
154
|
+
'sfmc/ssjs-no-unknown-function': 'off',
|
|
149
155
|
'sfmc/ssjs-require-platform-load': 'off',
|
|
150
156
|
'sfmc/ssjs-no-unsupported-syntax': 'off',
|
|
151
157
|
'sfmc/ssjs-no-deprecated-function': 'off',
|
|
@@ -191,7 +197,7 @@ const ampStrictRules = {
|
|
|
191
197
|
'sfmc/amp-no-unknown-function': 'error',
|
|
192
198
|
'sfmc/amp-no-var-redeclaration': 'error',
|
|
193
199
|
'sfmc/amp-set-requires-target': 'error',
|
|
194
|
-
'sfmc/amp-no-empty-block': '
|
|
200
|
+
'sfmc/amp-no-empty-block': 'warn',
|
|
195
201
|
'sfmc/amp-no-smart-quotes': 'error',
|
|
196
202
|
'sfmc/amp-prefer-attribute-value': 'warn',
|
|
197
203
|
'sfmc/amp-no-loop-counter-assign': 'error',
|
|
@@ -199,8 +205,8 @@ const ampStrictRules = {
|
|
|
199
205
|
'sfmc/amp-require-variable-declaration': 'warn',
|
|
200
206
|
'sfmc/amp-function-arity': 'error',
|
|
201
207
|
'sfmc/amp-arg-types': 'error',
|
|
202
|
-
'sfmc/amp-no-email-excluded-function':
|
|
203
|
-
'sfmc/amp-no-deprecated-function': '
|
|
208
|
+
'sfmc/amp-no-email-excluded-function': 'off',
|
|
209
|
+
'sfmc/amp-no-deprecated-function': 'warn',
|
|
204
210
|
'sfmc/amp-naming-convention': ['error', { format: 'camelCase' }],
|
|
205
211
|
'sfmc/amp-no-empty-then': 'error',
|
|
206
212
|
'sfmc/amp-require-rowcount-check': 'error',
|
|
@@ -244,7 +250,7 @@ const ssjsStrictRules = {
|
|
|
244
250
|
'sfmc/ssjs-require-hasownproperty': 'error',
|
|
245
251
|
'sfmc/ssjs-require-platform-load-order': 'error',
|
|
246
252
|
'sfmc/ssjs-no-hardcoded-credentials': 'error',
|
|
247
|
-
'sfmc/ssjs-prefer-platform-load-version': '
|
|
253
|
+
'sfmc/ssjs-prefer-platform-load-version': 'warn',
|
|
248
254
|
'sfmc/ssjs-no-unavailable-method': 'warn',
|
|
249
255
|
'sfmc/ssjs-prefer-parsejson-safe-arg': 'error',
|
|
250
256
|
'sfmc/ssjs-no-switch-default': 'error',
|
|
@@ -266,7 +272,7 @@ const hbsNextRules = {
|
|
|
266
272
|
'sfmc/hbs-no-unknown-binding': 'error',
|
|
267
273
|
'sfmc/hbs-helper-arity': 'error',
|
|
268
274
|
'sfmc/hbs-no-unsupported-construct': 'error',
|
|
269
|
-
'sfmc/hbs-
|
|
275
|
+
'sfmc/hbs-no-mcn-unsupported': 'error',
|
|
270
276
|
};
|
|
271
277
|
|
|
272
278
|
/**
|
|
@@ -279,7 +285,7 @@ const hbsOffRules = {
|
|
|
279
285
|
'sfmc/hbs-no-unknown-binding': 'off',
|
|
280
286
|
'sfmc/hbs-helper-arity': 'off',
|
|
281
287
|
'sfmc/hbs-no-unsupported-construct': 'off',
|
|
282
|
-
'sfmc/hbs-
|
|
288
|
+
'sfmc/hbs-no-mcn-unsupported': 'off',
|
|
283
289
|
};
|
|
284
290
|
|
|
285
291
|
/** Shared languageOptions for linting virtual `.hbs` files. */
|
|
@@ -449,7 +455,7 @@ plugin.configs = {
|
|
|
449
455
|
files: ['**/*.ampscript', '**/*.amp'],
|
|
450
456
|
rules: {
|
|
451
457
|
...ampRecommendedRules,
|
|
452
|
-
'sfmc/amp-no-
|
|
458
|
+
'sfmc/amp-no-mcn-unsupported': 'error',
|
|
453
459
|
},
|
|
454
460
|
},
|
|
455
461
|
|
|
@@ -480,7 +486,7 @@ plugin.configs = {
|
|
|
480
486
|
files: ['**/*.ampscript', '**/*.amp'],
|
|
481
487
|
rules: {
|
|
482
488
|
...ampRecommendedRules,
|
|
483
|
-
'sfmc/amp-no-
|
|
489
|
+
'sfmc/amp-no-mcn-unsupported': 'error',
|
|
484
490
|
},
|
|
485
491
|
},
|
|
486
492
|
{
|
|
@@ -513,7 +519,7 @@ plugin.configs = {
|
|
|
513
519
|
files: ['**/*.html/*.amp'],
|
|
514
520
|
rules: {
|
|
515
521
|
...ampRecommendedRules,
|
|
516
|
-
'sfmc/amp-no-
|
|
522
|
+
'sfmc/amp-no-mcn-unsupported': 'error',
|
|
517
523
|
},
|
|
518
524
|
},
|
|
519
525
|
{
|
|
@@ -548,7 +554,7 @@ plugin.configs = {
|
|
|
548
554
|
files: ['**/*.ampscript', '**/*.amp'],
|
|
549
555
|
rules: {
|
|
550
556
|
...ampStrictRules,
|
|
551
|
-
'sfmc/amp-no-
|
|
557
|
+
'sfmc/amp-no-mcn-unsupported': 'error',
|
|
552
558
|
},
|
|
553
559
|
},
|
|
554
560
|
{
|
|
@@ -575,7 +581,7 @@ plugin.configs = {
|
|
|
575
581
|
files: ['**/*.html/*.amp'],
|
|
576
582
|
rules: {
|
|
577
583
|
...ampStrictRules,
|
|
578
|
-
'sfmc/amp-no-
|
|
584
|
+
'sfmc/amp-no-mcn-unsupported': 'error',
|
|
579
585
|
},
|
|
580
586
|
},
|
|
581
587
|
{
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { functionNames, functionLookup, getMcnApiVersion } from 'ampscript-data';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flags AMPscript functions that cannot be used when targeting Marketing Cloud
|
|
5
|
+
* Next, and functions that work in MCN AMPscript but have no Handlebars
|
|
6
|
+
* equivalent (so they block a Handlebars migration).
|
|
7
|
+
*
|
|
8
|
+
* Each function carries an `mcnSince` value = the MCN API version it first
|
|
9
|
+
* became available in, or `null`/unset when it was never supported in MCN.
|
|
10
|
+
*
|
|
11
|
+
* `apiVersion` semantics (shared with the SSJS and Handlebars MCN rules):
|
|
12
|
+
* - Not set / null: flag every function whose `mcnSince` is null/unset. Any
|
|
13
|
+
* function supported at some point in MCN (numeric `mcnSince`) passes.
|
|
14
|
+
* - Set to N: flag everything the null case flags, PLUS everything whose
|
|
15
|
+
* `mcnSince` is greater than N (too new for the target). Pass iff
|
|
16
|
+
* `mcnSince != null && mcnSince <= N`.
|
|
17
|
+
*
|
|
18
|
+
* Unknown-function reporting stays in `amp-no-unknown-function`; this rule only
|
|
19
|
+
* evaluates functions that exist in the AMPscript catalog.
|
|
20
|
+
*/
|
|
21
|
+
export default {
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'problem',
|
|
24
|
+
docs: {
|
|
25
|
+
description:
|
|
26
|
+
'Disallow AMPscript functions that are not available in the targeted Marketing Cloud Next API version',
|
|
27
|
+
recommended: true,
|
|
28
|
+
},
|
|
29
|
+
messages: {
|
|
30
|
+
notSupportedInMcn: "'{{name}}' is not supported in Marketing Cloud Next.",
|
|
31
|
+
tooNewForTarget:
|
|
32
|
+
"'{{name}}' was introduced in Marketing Cloud Next API version {{since}}, which is newer than the targeted version {{target}}.",
|
|
33
|
+
noHandlebarsEquivalent:
|
|
34
|
+
"'{{name}}' is supported by Marketing Cloud Next AMPscript but has no Handlebars for Marketing Cloud Next equivalent. It cannot be migrated to a Handlebars helper.",
|
|
35
|
+
},
|
|
36
|
+
schema: [
|
|
37
|
+
{
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
apiVersion: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
description:
|
|
43
|
+
'The targeted Marketing Cloud Next API version (e.g. 65 = Winter \u{2019}26, 67 = Summer \u{2019}26). Functions newer than this are flagged.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
create(context) {
|
|
52
|
+
const options = context.options[0] ?? {};
|
|
53
|
+
const apiVersion = typeof options.apiVersion === 'number' ? options.apiVersion : null;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
FunctionCall(node) {
|
|
57
|
+
const lower = node.name.toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Unknown functions are handled by amp-no-unknown-function.
|
|
60
|
+
if (!functionNames.has(lower)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const since = getMcnApiVersion(lower);
|
|
65
|
+
|
|
66
|
+
// Never supported in MCN → always flag.
|
|
67
|
+
if (since === null) {
|
|
68
|
+
context.report({
|
|
69
|
+
node,
|
|
70
|
+
messageId: 'notSupportedInMcn',
|
|
71
|
+
data: { name: node.name },
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Supported in MCN but newer than the targeted API version.
|
|
77
|
+
if (apiVersion !== null && since > apiVersion) {
|
|
78
|
+
context.report({
|
|
79
|
+
node,
|
|
80
|
+
messageId: 'tooNewForTarget',
|
|
81
|
+
data: {
|
|
82
|
+
name: node.name,
|
|
83
|
+
since: String(since),
|
|
84
|
+
target: String(apiVersion),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Works in MCN AMPscript but has no Handlebars counterpart, so it
|
|
91
|
+
// cannot be migrated to a Handlebars helper.
|
|
92
|
+
if (functionLookup.get(lower)?.mcnHandlebarsGap === true) {
|
|
93
|
+
context.report({
|
|
94
|
+
node,
|
|
95
|
+
messageId: 'noHandlebarsEquivalent',
|
|
96
|
+
data: { name: node.name },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// The processor extracts each AMPscript region into its own virtual .amp file,
|
|
2
2
|
// including the outer delimiters (%%[...]/%%] or <script>...</script>).
|
|
3
|
-
// Any %%[ or %%= found INSIDE
|
|
4
|
-
|
|
3
|
+
// Any %%[ or %%= found INSIDE an already-open block is nested and invalid.
|
|
4
|
+
//
|
|
5
|
+
// A raw .amp file may contain several *sibling* blocks (e.g. one %%[...]%% block
|
|
6
|
+
// followed by another). Those are not nested. To distinguish nesting from
|
|
7
|
+
// siblings we scan every open/close token in document order and track depth:
|
|
8
|
+
// an opener is only nested when depth > 0.
|
|
9
|
+
const TOKEN_RE = /%%\[|%%=|\]%%|=%%/g;
|
|
5
10
|
const SCRIPT_TAG_RE = /^[\n]*<script\b/i;
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const INLINE_CLOSE_SUFFIX = '=%%';
|
|
11
|
+
|
|
12
|
+
const OPEN_TOKENS = new Set(['%%[', '%%=']);
|
|
13
|
+
const CLOSE_FOR_OPEN = { '%%[': ']%%', '%%=': '=%%' };
|
|
10
14
|
|
|
11
15
|
export default {
|
|
12
16
|
meta: {
|
|
@@ -25,76 +29,64 @@ export default {
|
|
|
25
29
|
},
|
|
26
30
|
|
|
27
31
|
create(context) {
|
|
32
|
+
const sourceCode = context.sourceCode;
|
|
33
|
+
|
|
34
|
+
const reportNested = (text, match, isScriptBlock) => {
|
|
35
|
+
const delimiter = match[0];
|
|
36
|
+
const start = match.index;
|
|
37
|
+
const end = start + delimiter.length;
|
|
38
|
+
const closeToken = CLOSE_FOR_OPEN[delimiter];
|
|
39
|
+
const closeIndex = text.indexOf(closeToken, end);
|
|
40
|
+
|
|
41
|
+
context.report({
|
|
42
|
+
loc: {
|
|
43
|
+
start: sourceCode.getLocFromIndex(start),
|
|
44
|
+
end: sourceCode.getLocFromIndex(end),
|
|
45
|
+
},
|
|
46
|
+
messageId: isScriptBlock ? 'nestedDelimiterInScript' : 'nestedDelimiter',
|
|
47
|
+
data: { delimiter },
|
|
48
|
+
fix:
|
|
49
|
+
closeIndex === -1
|
|
50
|
+
? (fixer) => fixer.removeRange([start, end])
|
|
51
|
+
: (fixer) => [
|
|
52
|
+
fixer.removeRange([closeIndex, closeIndex + closeToken.length]),
|
|
53
|
+
fixer.removeRange([start, end]),
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
28
58
|
return {
|
|
29
59
|
Program() {
|
|
30
|
-
const sourceCode = context.sourceCode;
|
|
31
60
|
const text = sourceCode.getText();
|
|
32
61
|
const isScriptBlock = SCRIPT_TAG_RE.test(text);
|
|
33
62
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (blockOpenMatch) {
|
|
45
|
-
outerOpenStart = blockOpenMatch.index + blockOpenMatch[0].length - 3;
|
|
46
|
-
outerCloseSuffix = BLOCK_CLOSE_SUFFIX;
|
|
47
|
-
} else if (inlineOpenMatch) {
|
|
48
|
-
outerOpenStart = inlineOpenMatch.index + inlineOpenMatch[0].length - 3;
|
|
49
|
-
outerCloseSuffix = INLINE_CLOSE_SUFFIX;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// The outer close is the last occurrence of the matching close token
|
|
53
|
-
const lastClose = text.lastIndexOf(outerCloseSuffix);
|
|
54
|
-
if (lastClose !== -1) {
|
|
55
|
-
outerCloseStart = lastClose;
|
|
63
|
+
if (isScriptBlock) {
|
|
64
|
+
// Inside a <script language="ampscript"> block the whole body is
|
|
65
|
+
// already AMPscript; every %%[ / %%= delimiter is redundant.
|
|
66
|
+
TOKEN_RE.lastIndex = 0;
|
|
67
|
+
let match;
|
|
68
|
+
while ((match = TOKEN_RE.exec(text)) !== null) {
|
|
69
|
+
if (OPEN_TOKENS.has(match[0])) {
|
|
70
|
+
reportNested(text, match, true);
|
|
71
|
+
}
|
|
56
72
|
}
|
|
73
|
+
return;
|
|
57
74
|
}
|
|
58
75
|
|
|
59
|
-
|
|
76
|
+
// Depth-based scan for raw %%[...]%% / %%=...=%% content. Sibling blocks
|
|
77
|
+
// return depth to 0 between them, so their openers are not flagged.
|
|
78
|
+
let depth = 0;
|
|
79
|
+
TOKEN_RE.lastIndex = 0;
|
|
60
80
|
let match;
|
|
61
|
-
while ((match =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
while ((match = TOKEN_RE.exec(text)) !== null) {
|
|
82
|
+
if (OPEN_TOKENS.has(match[0])) {
|
|
83
|
+
if (depth > 0) {
|
|
84
|
+
reportNested(text, match, false);
|
|
85
|
+
}
|
|
86
|
+
depth++;
|
|
87
|
+
} else {
|
|
88
|
+
depth = Math.max(0, depth - 1);
|
|
68
89
|
}
|
|
69
|
-
|
|
70
|
-
const end = start + delimiter.length;
|
|
71
|
-
const closeToken = delimiter === '%%[' ? ']%%' : '=%%';
|
|
72
|
-
|
|
73
|
-
// Find the matching close token for the fix (first occurrence after this open)
|
|
74
|
-
let closeIndex = text.indexOf(closeToken, end);
|
|
75
|
-
// Don't use the outer ]%% as the fix target for an inner %%[
|
|
76
|
-
if (!isScriptBlock && closeIndex === outerCloseStart) {
|
|
77
|
-
closeIndex = -1;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
context.report({
|
|
81
|
-
loc: {
|
|
82
|
-
start: sourceCode.getLocFromIndex(start),
|
|
83
|
-
end: sourceCode.getLocFromIndex(end),
|
|
84
|
-
},
|
|
85
|
-
messageId: isScriptBlock ? 'nestedDelimiterInScript' : 'nestedDelimiter',
|
|
86
|
-
data: { delimiter },
|
|
87
|
-
fix:
|
|
88
|
-
closeIndex === -1
|
|
89
|
-
? (fixer) => fixer.removeRange([start, end])
|
|
90
|
-
: (fixer) => [
|
|
91
|
-
fixer.removeRange([
|
|
92
|
-
closeIndex,
|
|
93
|
-
closeIndex + closeToken.length,
|
|
94
|
-
]),
|
|
95
|
-
fixer.removeRange([start, end]),
|
|
96
|
-
],
|
|
97
|
-
});
|
|
98
90
|
}
|
|
99
91
|
},
|
|
100
92
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const SMART_QUOTE_RE = /[\u{2018}\u{2019}\u{201C}\u{201D}\u{201A}\u{201E}\u{2039}\u{203A}]/
|
|
1
|
+
const SMART_QUOTE_RE = /[\u{2018}\u{2019}\u{201C}\u{201D}\u{201A}\u{201E}\u{2039}\u{203A}]/gu;
|
|
2
2
|
|
|
3
3
|
const SMART_QUOTES = {
|
|
4
4
|
'\u{2018}': 'left single curly quote \u{2018}',
|
|
@@ -30,7 +30,7 @@ export default {
|
|
|
30
30
|
fixable: 'code',
|
|
31
31
|
docs: {
|
|
32
32
|
description:
|
|
33
|
-
'Disallow smart/curly quotes in
|
|
33
|
+
'Disallow smart/curly quotes in AMPscript (AMPscript requires ASCII quotes)',
|
|
34
34
|
recommended: true,
|
|
35
35
|
},
|
|
36
36
|
messages: {
|
|
@@ -41,36 +41,30 @@ export default {
|
|
|
41
41
|
},
|
|
42
42
|
|
|
43
43
|
create(context) {
|
|
44
|
+
const sourceCode = context.sourceCode;
|
|
45
|
+
|
|
44
46
|
return {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
return fixer.replaceText(node, q + fixed + q);
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
47
|
+
// The AMPscript parser tokenizes string literals as RAW + IDENTIFIER
|
|
48
|
+
// tokens rather than a StringLiteral node, so a node listener never
|
|
49
|
+
// matches. Scan the virtual .amp region's raw text instead — the whole
|
|
50
|
+
// extracted file is AMPscript, mirroring the LSP smart-quote check.
|
|
51
|
+
Program() {
|
|
52
|
+
const text = sourceCode.getText();
|
|
53
|
+
SMART_QUOTE_RE.lastIndex = 0;
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = SMART_QUOTE_RE.exec(text)) !== null) {
|
|
56
|
+
const char = match[0];
|
|
57
|
+
const start = match.index;
|
|
58
|
+
const end = start + char.length;
|
|
59
|
+
context.report({
|
|
60
|
+
loc: {
|
|
61
|
+
start: sourceCode.getLocFromIndex(start),
|
|
62
|
+
end: sourceCode.getLocFromIndex(end),
|
|
63
|
+
},
|
|
64
|
+
messageId: 'smartQuote',
|
|
65
|
+
data: { kind: SMART_QUOTES[char] },
|
|
66
|
+
fix: (fixer) => fixer.replaceTextRange([start, end], SMART_TO_ASCII[char]),
|
|
67
|
+
});
|
|
74
68
|
}
|
|
75
69
|
},
|
|
76
70
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { functionNames
|
|
1
|
+
import { functionNames } from 'ampscript-data';
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
meta: {
|
|
@@ -11,30 +11,11 @@ export default {
|
|
|
11
11
|
messages: {
|
|
12
12
|
unknownFunction:
|
|
13
13
|
"'{{name}}' is not a recognized AMPscript function. AMPscript does not support custom functions.",
|
|
14
|
-
notSupportedInMcn: "'{{name}}' is not supported in Marketing Cloud Next.",
|
|
15
|
-
noHandlebarsEquivalent:
|
|
16
|
-
"'{{name}}' is supported by Marketing Cloud Next AMPscript but has no Handlebars for Marketing Cloud Next equivalent. It cannot be migrated to a Handlebars helper.",
|
|
17
14
|
},
|
|
18
|
-
schema: [
|
|
19
|
-
{
|
|
20
|
-
type: 'object',
|
|
21
|
-
properties: {
|
|
22
|
-
target: {
|
|
23
|
-
type: 'string',
|
|
24
|
-
enum: ['engagement', 'next'],
|
|
25
|
-
description:
|
|
26
|
-
"Target platform. Set to 'next' to additionally flag functions not available in Marketing Cloud Next.",
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
additionalProperties: false,
|
|
30
|
-
},
|
|
31
|
-
],
|
|
15
|
+
schema: [],
|
|
32
16
|
},
|
|
33
17
|
|
|
34
18
|
create(context) {
|
|
35
|
-
const options = context.options[0] ?? {};
|
|
36
|
-
const isTargetNext = options.target === 'next';
|
|
37
|
-
|
|
38
19
|
return {
|
|
39
20
|
FunctionCall(node) {
|
|
40
21
|
const lower = node.name.toLowerCase();
|
|
@@ -45,26 +26,6 @@ export default {
|
|
|
45
26
|
messageId: 'unknownFunction',
|
|
46
27
|
data: { name: node.name },
|
|
47
28
|
});
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (isTargetNext && !isMcnSupported(lower)) {
|
|
52
|
-
context.report({
|
|
53
|
-
node,
|
|
54
|
-
messageId: 'notSupportedInMcn',
|
|
55
|
-
data: { name: node.name },
|
|
56
|
-
});
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Category C: the function works in MCN AMPscript but has no
|
|
61
|
-
// Handlebars counterpart, so it cannot be migrated to Handlebars.
|
|
62
|
-
if (isTargetNext && functionLookup.get(lower)?.mcnHandlebarsGap === true) {
|
|
63
|
-
context.report({
|
|
64
|
-
node,
|
|
65
|
-
messageId: 'noHandlebarsEquivalent',
|
|
66
|
-
data: { name: node.name },
|
|
67
|
-
});
|
|
68
29
|
}
|
|
69
30
|
},
|
|
70
31
|
};
|