eslint-plugin-sfmc 2.6.1 → 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.
- package/README.md +72 -63
- package/docs/rules/hbs/helper-arity.md +55 -0
- package/docs/rules/hbs/helper-too-new-for-target.md +62 -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/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 +90 -11
- 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-nested-ampscript-delimiter.js +1 -1
- package/src/rules/amp/no-smart-quotes.js +18 -18
- package/src/rules/amp/no-unknown-function.js +16 -3
- 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/helper-too-new-for-target.js +94 -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-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 +16 -12
- 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 +19 -15
- /package/src/rules/amp/{no-var-redeclaration.js → no-variable-redeclaration.js} +0 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isHelper, helperNames } from 'handlebars-data';
|
|
2
|
+
|
|
3
|
+
import { simpleHelperName, isInvocation, closestMatch } from './_shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flags Handlebars helper invocations whose name is not part of the Marketing
|
|
7
|
+
* Cloud Next catalog. The MCN engine is locked down and cannot register custom
|
|
8
|
+
* helpers, so any unknown helper is reported.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the `handlebars/unknown-helper` diagnostic emitted by
|
|
11
|
+
* sfmc-language-lsp. A bare `{{foo}}` mustache with no arguments is treated as a
|
|
12
|
+
* data binding (not a helper) and is not flagged here.
|
|
13
|
+
*/
|
|
14
|
+
export default {
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'problem',
|
|
17
|
+
docs: {
|
|
18
|
+
description:
|
|
19
|
+
'Disallow Handlebars helper invocations that are not part of the Marketing Cloud Next catalog',
|
|
20
|
+
recommended: true,
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
unknownHelper:
|
|
24
|
+
"Unknown Handlebars {{kind}} '{{name}}'. It is not part of the Marketing Cloud Next catalog, and the MCN engine cannot register custom helpers.",
|
|
25
|
+
unknownHelperSuggest:
|
|
26
|
+
"Unknown Handlebars {{kind}} '{{name}}'. It is not part of the Marketing Cloud Next catalog, and the MCN engine cannot register custom helpers. Did you mean '{{suggestion}}'?",
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
create(context) {
|
|
32
|
+
/**
|
|
33
|
+
* Reports an unknown helper, attaching a "did you mean" suggestion when
|
|
34
|
+
* a close catalog match exists.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} node - The offending AST node.
|
|
37
|
+
* @param {string} name - The unknown helper name.
|
|
38
|
+
* @param {boolean} isBlock - True when the node is a block helper.
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
function reportUnknown(node, name, isBlock) {
|
|
42
|
+
const kind = isBlock ? 'block helper' : 'helper';
|
|
43
|
+
const suggestion = closestMatch(name, helperNames);
|
|
44
|
+
if (suggestion) {
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
messageId: 'unknownHelperSuggest',
|
|
48
|
+
data: { kind, name, suggestion },
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
context.report({ node, messageId: 'unknownHelper', data: { kind, name } });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Checks an inline mustache or subexpression invocation.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} node - The AST node.
|
|
59
|
+
* @returns {void}
|
|
60
|
+
*/
|
|
61
|
+
function checkInline(node) {
|
|
62
|
+
const helperName = simpleHelperName(node.path);
|
|
63
|
+
if (helperName && isInvocation(node) && !isHelper(helperName)) {
|
|
64
|
+
reportUnknown(node, helperName, false);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
MustacheStatement: checkInline,
|
|
70
|
+
SubExpression: checkInline,
|
|
71
|
+
HbsBlockStatement(node) {
|
|
72
|
+
const helperName = simpleHelperName(node.path);
|
|
73
|
+
if (helperName && !isHelper(helperName)) {
|
|
74
|
+
reportUnknown(node, helperName, true);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { unsupportedByNodeType } from 'handlebars-data';
|
|
2
|
+
|
|
3
|
+
import { simpleHelperName } from './_shared.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flags Handlebars constructs that the locked-down Marketing Cloud Next engine
|
|
7
|
+
* cannot support: partials, partial blocks, decorators, inline partials, and the
|
|
8
|
+
* handlebars.js-only `{{log}}` debugging helper.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the `handlebars/unsupported-construct` diagnostic emitted by
|
|
11
|
+
* sfmc-language-lsp.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** AST node types that may carry an unsupported construct. */
|
|
15
|
+
const NODE_TYPES = unsupportedByNodeType.keys().toArray();
|
|
16
|
+
|
|
17
|
+
// The handlebars-data messages contain literal `{{> ...}}`, `{{log}}`, etc. as
|
|
18
|
+
// examples. ESLint treats `{{...}}` in a message template as an interpolation
|
|
19
|
+
// placeholder, so the literal text is passed through a single `{{text}}`
|
|
20
|
+
// placeholder instead of being baked into the template.
|
|
21
|
+
/** messageId per construct id → passthrough template, declared statically. */
|
|
22
|
+
const MESSAGES = Object.fromEntries(
|
|
23
|
+
unsupportedByNodeType
|
|
24
|
+
.values()
|
|
25
|
+
.toArray()
|
|
26
|
+
.flat()
|
|
27
|
+
.map((entry) => [entry.id, '{{text}}']),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
meta: {
|
|
32
|
+
type: 'problem',
|
|
33
|
+
docs: {
|
|
34
|
+
description:
|
|
35
|
+
'Disallow Handlebars constructs unsupported by the Marketing Cloud Next engine (partials, decorators, log)',
|
|
36
|
+
recommended: true,
|
|
37
|
+
},
|
|
38
|
+
messages: MESSAGES,
|
|
39
|
+
schema: [],
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
create(context) {
|
|
43
|
+
/**
|
|
44
|
+
* Reports the first unsupported-construct entry matching a node.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} node - The Handlebars AST node.
|
|
47
|
+
* @returns {void}
|
|
48
|
+
*/
|
|
49
|
+
function check(node) {
|
|
50
|
+
const candidates = unsupportedByNodeType.get(node.type);
|
|
51
|
+
if (!candidates) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const helperName = simpleHelperName(node.path);
|
|
55
|
+
for (const entry of candidates) {
|
|
56
|
+
if (entry.helperName !== null && entry.helperName !== helperName) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
context.report({ node, messageId: entry.id, data: { text: entry.message } });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const visitors = {};
|
|
65
|
+
for (const nodeType of NODE_TYPES) {
|
|
66
|
+
visitors[nodeType] = check;
|
|
67
|
+
}
|
|
68
|
+
return visitors;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -34,23 +34,23 @@ export default {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
let
|
|
37
|
+
let lengthExpression = null;
|
|
38
38
|
if (containsMemberLength(test.right)) {
|
|
39
|
-
|
|
39
|
+
lengthExpression = test.right;
|
|
40
40
|
} else if (containsMemberLength(test.left)) {
|
|
41
|
-
|
|
41
|
+
lengthExpression = test.left;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
if (!
|
|
44
|
+
if (!lengthExpression) {
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const fix = buildCacheFix(node,
|
|
48
|
+
const fix = buildCacheFix(node, lengthExpression, context);
|
|
49
49
|
|
|
50
50
|
context.report({
|
|
51
51
|
node: test,
|
|
52
52
|
messageId: 'cacheLength',
|
|
53
|
-
...(fix
|
|
53
|
+
...(fix && { fix }),
|
|
54
54
|
});
|
|
55
55
|
},
|
|
56
56
|
};
|
|
@@ -61,17 +61,14 @@ function containsMemberLength(node) {
|
|
|
61
61
|
if (!node) {
|
|
62
62
|
return false;
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
return (
|
|
65
65
|
node.type === 'MemberExpression' &&
|
|
66
66
|
node.property.type === 'Identifier' &&
|
|
67
67
|
node.property.name === 'length'
|
|
68
|
-
)
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
return false;
|
|
68
|
+
);
|
|
72
69
|
}
|
|
73
70
|
|
|
74
|
-
function buildCacheFix(forNode,
|
|
71
|
+
function buildCacheFix(forNode, lengthExpression, context) {
|
|
75
72
|
const init = forNode.init;
|
|
76
73
|
// Only fix when init is a VariableDeclaration so we can safely
|
|
77
74
|
// append a new declarator without restructuring the loop header.
|
|
@@ -79,14 +76,14 @@ function buildCacheFix(forNode, lengthExpr, context) {
|
|
|
79
76
|
return null;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
const
|
|
83
|
-
const
|
|
79
|
+
const objectText = context.sourceCode.getText(lengthExpression.object);
|
|
80
|
+
const lengthVariable = '_len';
|
|
84
81
|
|
|
85
82
|
return function fix(fixer) {
|
|
86
|
-
const
|
|
83
|
+
const lastDeclaration = init.declarations.at(-1);
|
|
87
84
|
return [
|
|
88
|
-
fixer.insertTextAfter(
|
|
89
|
-
fixer.replaceText(
|
|
85
|
+
fixer.insertTextAfter(lastDeclaration, `, ${lengthVariable} = ${objectText}.length`),
|
|
86
|
+
fixer.replaceText(lengthExpression, lengthVariable),
|
|
90
87
|
];
|
|
91
88
|
};
|
|
92
89
|
}
|
|
@@ -62,20 +62,20 @@ export default {
|
|
|
62
62
|
|
|
63
63
|
return {
|
|
64
64
|
VariableDeclaration(node) {
|
|
65
|
-
for (const
|
|
65
|
+
for (const declaration of node.declarations) {
|
|
66
66
|
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
declaration.init &&
|
|
68
|
+
declaration.id &&
|
|
69
|
+
declaration.id.type === 'Identifier' &&
|
|
70
|
+
isContentAreaObjectInit(declaration.init)
|
|
71
71
|
) {
|
|
72
|
-
contentAreaVariables.add(
|
|
72
|
+
contentAreaVariables.add(declaration.id.name);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
|
|
77
77
|
AssignmentExpression(node) {
|
|
78
|
-
if (node.left.type === 'Identifier' &&
|
|
78
|
+
if (node.left.type === 'Identifier' && isContentAreaObjectInit(node.right)) {
|
|
79
79
|
contentAreaVariables.add(node.left.name);
|
|
80
80
|
}
|
|
81
81
|
},
|
|
@@ -166,7 +166,7 @@ export default {
|
|
|
166
166
|
* @param {import('eslint').Rule.Node} node - AST node to inspect
|
|
167
167
|
* @returns {boolean} true when the node is a ContentAreaObj.Init call
|
|
168
168
|
*/
|
|
169
|
-
function
|
|
169
|
+
function isContentAreaObjectInit(node) {
|
|
170
170
|
if (!node || node.type !== 'CallExpression') {
|
|
171
171
|
return false;
|
|
172
172
|
}
|
|
@@ -29,9 +29,7 @@ export default {
|
|
|
29
29
|
CallExpression(node) {
|
|
30
30
|
const callee = node.callee;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (
|
|
32
|
+
const functionName =
|
|
35
33
|
callee.type === 'MemberExpression' &&
|
|
36
34
|
callee.object.type === 'MemberExpression' &&
|
|
37
35
|
callee.object.object.type === 'Identifier' &&
|
|
@@ -39,9 +37,8 @@ export default {
|
|
|
39
37
|
callee.object.property.type === 'Identifier' &&
|
|
40
38
|
callee.object.property.name === 'Function' &&
|
|
41
39
|
callee.property.type === 'Identifier'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
40
|
+
? callee.property.name
|
|
41
|
+
: null;
|
|
45
42
|
|
|
46
43
|
if (!functionName || !ENCRYPT_FUNCTIONS.has(functionName.toLowerCase())) {
|
|
47
44
|
return;
|
|
@@ -38,10 +38,7 @@ function canReplaceCallWithAssignment(node) {
|
|
|
38
38
|
if (parent.type === 'ExpressionStatement' && parent.expression === node) {
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
41
|
+
return Boolean(parent.type === 'SequenceExpression' && parent.expressions.includes(node));
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
export default {
|
|
@@ -83,11 +80,11 @@ export default {
|
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
const ns = callee.object.property.name.toLowerCase();
|
|
86
|
-
const
|
|
83
|
+
const propertyName = callee.property.name.toLowerCase();
|
|
87
84
|
const displayNs = callee.object.property.name;
|
|
88
85
|
const displayName = callee.property.name;
|
|
89
86
|
|
|
90
|
-
if (ns === 'response' && RESPONSE_PROPERTIES.has(
|
|
87
|
+
if (ns === 'response' && RESPONSE_PROPERTIES.has(propertyName)) {
|
|
91
88
|
if (node.arguments.length === 0) {
|
|
92
89
|
context.report({
|
|
93
90
|
node,
|
|
@@ -106,10 +103,10 @@ export default {
|
|
|
106
103
|
if (!canReplaceCallWithAssignment(node)) {
|
|
107
104
|
return null;
|
|
108
105
|
}
|
|
109
|
-
const
|
|
106
|
+
const argumentText = sourceCode.getText(node.arguments[0]);
|
|
110
107
|
return fixer.replaceText(
|
|
111
108
|
node,
|
|
112
|
-
`${sourceCode.getText(callee)} = ${
|
|
109
|
+
`${sourceCode.getText(callee)} = ${argumentText}`,
|
|
113
110
|
);
|
|
114
111
|
},
|
|
115
112
|
});
|
|
@@ -120,7 +117,7 @@ export default {
|
|
|
120
117
|
data: { ns: displayNs, name: displayName },
|
|
121
118
|
});
|
|
122
119
|
}
|
|
123
|
-
} else if (ns === 'request' && REQUEST_PROPERTIES.has(
|
|
120
|
+
} else if (ns === 'request' && REQUEST_PROPERTIES.has(propertyName)) {
|
|
124
121
|
if (node.arguments.length === 0) {
|
|
125
122
|
context.report({
|
|
126
123
|
node,
|
|
@@ -35,9 +35,9 @@ export default {
|
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const
|
|
39
|
-
if (containsConcatenation(
|
|
40
|
-
context.report({ node:
|
|
38
|
+
const argument = node.arguments[0];
|
|
39
|
+
if (containsConcatenation(argument)) {
|
|
40
|
+
context.report({ node: argument, messageId: 'injection' });
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
};
|
|
@@ -53,7 +53,7 @@ function isTreatAsContentCall(node) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// Platform.Function.TreatAsContent(...)
|
|
56
|
-
|
|
56
|
+
return (
|
|
57
57
|
callee.type === 'MemberExpression' &&
|
|
58
58
|
callee.property.type === 'Identifier' &&
|
|
59
59
|
callee.property.name === 'TreatAsContent' &&
|
|
@@ -62,19 +62,12 @@ function isTreatAsContentCall(node) {
|
|
|
62
62
|
callee.object.property.name === 'Function' &&
|
|
63
63
|
callee.object.object.type === 'Identifier' &&
|
|
64
64
|
callee.object.object.name === 'Platform'
|
|
65
|
-
)
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return false;
|
|
65
|
+
);
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
function containsConcatenation(node) {
|
|
73
69
|
if (node.type === 'BinaryExpression' && node.operator === '+') {
|
|
74
70
|
return true;
|
|
75
71
|
}
|
|
76
|
-
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
72
|
+
return node.type === 'TemplateLiteral' && node.expressions.length > 0;
|
|
80
73
|
}
|
|
@@ -98,8 +98,8 @@ export default {
|
|
|
98
98
|
|
|
99
99
|
function buildSuggestFix(entry) {
|
|
100
100
|
return function fix(fixer) {
|
|
101
|
-
const
|
|
102
|
-
const end =
|
|
101
|
+
const source = context.sourceCode;
|
|
102
|
+
const end = source.ast.range[1];
|
|
103
103
|
return fixer.insertTextAfterRange([end, end], '\n\n' + entry.polyfill);
|
|
104
104
|
};
|
|
105
105
|
}
|
|
@@ -113,27 +113,27 @@ export default {
|
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
116
|
+
const object = left.object;
|
|
117
|
+
const property = left.property;
|
|
118
|
+
if (property.type !== 'Identifier') {
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// Array.X = ...
|
|
123
|
-
if (
|
|
124
|
-
alreadyPolyfilled.add(
|
|
123
|
+
if (object.type === 'Identifier' && object.name === 'Array') {
|
|
124
|
+
alreadyPolyfilled.add(property.name);
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// Array.prototype.X = ... or String.prototype.X = ...
|
|
129
129
|
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
(
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
object.type === 'MemberExpression' &&
|
|
131
|
+
object.object.type === 'Identifier' &&
|
|
132
|
+
(object.object.name === 'Array' || object.object.name === 'String') &&
|
|
133
|
+
object.property.type === 'Identifier' &&
|
|
134
|
+
object.property.name === 'prototype'
|
|
135
135
|
) {
|
|
136
|
-
alreadyPolyfilled.add(
|
|
136
|
+
alreadyPolyfilled.add(property.name);
|
|
137
137
|
}
|
|
138
138
|
},
|
|
139
139
|
|
|
@@ -143,12 +143,12 @@ export default {
|
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
146
|
+
const property = callee.property;
|
|
147
|
+
if (property.type !== 'Identifier') {
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
const methodName =
|
|
151
|
+
const methodName = property.name;
|
|
152
152
|
const receiver = callee.object;
|
|
153
153
|
|
|
154
154
|
const lowerMethod = methodName.toLowerCase();
|
|
@@ -163,7 +163,7 @@ export default {
|
|
|
163
163
|
return;
|
|
164
164
|
}
|
|
165
165
|
const entry = polyfillByStaticName.get(methodName);
|
|
166
|
-
pendingReports.push({ node:
|
|
166
|
+
pendingReports.push({ node: property, entry });
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -176,7 +176,7 @@ export default {
|
|
|
176
176
|
const entry = knownUnsupportedByStaticName.get(lowerMethod);
|
|
177
177
|
const ownerBase = entry.owner.replace(/\.prototype$/, '');
|
|
178
178
|
if (receiver.name === ownerBase && !ignored.has(methodName)) {
|
|
179
|
-
pendingReports.push({ node:
|
|
179
|
+
pendingReports.push({ node: property, entry, noPolyfill: true });
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -187,13 +187,13 @@ export default {
|
|
|
187
187
|
return;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
const entry = polyfillByPrototypeName.get(methodName);
|
|
191
|
-
|
|
192
190
|
// Skip known SFMC top-level objects to avoid false positives.
|
|
193
191
|
if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name)) {
|
|
194
192
|
return;
|
|
195
193
|
}
|
|
196
194
|
|
|
195
|
+
const entry = polyfillByPrototypeName.get(methodName);
|
|
196
|
+
|
|
197
197
|
// For methods that also exist on String.prototype in ES3
|
|
198
198
|
// (indexOf, lastIndexOf), only flag when the receiver is
|
|
199
199
|
// a literal array or the call is chained from an array method
|
|
@@ -211,7 +211,7 @@ export default {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
pendingReports.push({ node:
|
|
214
|
+
pendingReports.push({ node: property, entry });
|
|
215
215
|
return;
|
|
216
216
|
}
|
|
217
217
|
|
|
@@ -225,7 +225,7 @@ export default {
|
|
|
225
225
|
return;
|
|
226
226
|
}
|
|
227
227
|
const entry = knownUnsupportedByPrototypeName.get(lowerMethod);
|
|
228
|
-
pendingReports.push({ node:
|
|
228
|
+
pendingReports.push({ node: property, entry, noPolyfill: true });
|
|
229
229
|
}
|
|
230
230
|
},
|
|
231
231
|
|
|
@@ -70,7 +70,7 @@ export default {
|
|
|
70
70
|
|
|
71
71
|
create(context) {
|
|
72
72
|
const options = context.options[0] ?? {};
|
|
73
|
-
const
|
|
73
|
+
const isTargetNext = options.target === 'next';
|
|
74
74
|
|
|
75
75
|
// Track variable name → Core Library type name (assigned via TypeName.Init())
|
|
76
76
|
const coreVariables = new Map();
|
|
@@ -79,16 +79,20 @@ export default {
|
|
|
79
79
|
|
|
80
80
|
return {
|
|
81
81
|
VariableDeclaration(node) {
|
|
82
|
-
for (const
|
|
83
|
-
if (
|
|
82
|
+
for (const declaration of node.declarations) {
|
|
83
|
+
if (
|
|
84
|
+
!declaration.init ||
|
|
85
|
+
!declaration.id ||
|
|
86
|
+
declaration.id.type !== 'Identifier'
|
|
87
|
+
) {
|
|
84
88
|
continue;
|
|
85
89
|
}
|
|
86
|
-
const coreType = getCoreInitType(
|
|
90
|
+
const coreType = getCoreInitType(declaration.init);
|
|
87
91
|
if (coreType) {
|
|
88
|
-
coreVariables.set(
|
|
92
|
+
coreVariables.set(declaration.id.name, coreType);
|
|
89
93
|
}
|
|
90
|
-
if (isWSProxyConstructor(
|
|
91
|
-
wsproxyVariables.add(
|
|
94
|
+
if (isWSProxyConstructor(declaration.init)) {
|
|
95
|
+
wsproxyVariables.add(declaration.id.name);
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
},
|
|
@@ -119,7 +123,7 @@ export default {
|
|
|
119
123
|
const methodName = property.name;
|
|
120
124
|
|
|
121
125
|
// ── MCN target: flag all SSJS API calls as unsupported ────────
|
|
122
|
-
if (
|
|
126
|
+
if (isTargetNext) {
|
|
123
127
|
// Only report on Platform.<NS>.<method>(), HTTP.<method>(),
|
|
124
128
|
// Core Library instance calls, or WSProxy calls — not bare JS.
|
|
125
129
|
const isPlatformCall =
|
|
@@ -189,10 +193,10 @@ export default {
|
|
|
189
193
|
// Core library: var de = DataExtension.Init("key"); de.Foo();
|
|
190
194
|
const coreType = coreVariables.get(objectName);
|
|
191
195
|
if (coreType) {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
196
|
+
const objectDefinition = coreObjectLookup.get(coreType);
|
|
197
|
+
if (objectDefinition) {
|
|
194
198
|
const knownMethods = new Set(
|
|
195
|
-
|
|
199
|
+
objectDefinition.methods.map((m) => m.toLowerCase()),
|
|
196
200
|
);
|
|
197
201
|
if (!knownMethods.has(methodName.toLowerCase())) {
|
|
198
202
|
context.report({
|
|
@@ -201,7 +205,7 @@ export default {
|
|
|
201
205
|
data: {
|
|
202
206
|
method: methodName,
|
|
203
207
|
objectType: coreType,
|
|
204
|
-
available:
|
|
208
|
+
available: objectDefinition.methods.join(', '),
|
|
205
209
|
},
|
|
206
210
|
});
|
|
207
211
|
}
|
|
@@ -21,8 +21,8 @@ const AUTO_FIX = {
|
|
|
21
21
|
|
|
22
22
|
function nullishCoalescingFix(context, node) {
|
|
23
23
|
return (fixer) => {
|
|
24
|
-
const
|
|
25
|
-
const between =
|
|
24
|
+
const source = context.sourceCode.getText();
|
|
25
|
+
const between = source.slice(node.left.range[1], node.right.range[0]);
|
|
26
26
|
const offset = between.indexOf('??');
|
|
27
27
|
if (offset === -1) {
|
|
28
28
|
return null;
|
|
@@ -34,9 +34,9 @@ function nullishCoalescingFix(context, node) {
|
|
|
34
34
|
|
|
35
35
|
function directObjectReturnFix(context, node) {
|
|
36
36
|
return (fixer) => {
|
|
37
|
-
const
|
|
37
|
+
const objectText = context.sourceCode.getText(node.argument);
|
|
38
38
|
const indent = ' '.repeat(node.loc.start.column);
|
|
39
|
-
return fixer.replaceText(node, `var _result = ${
|
|
39
|
+
return fixer.replaceText(node, `var _result = ${objectText};\n${indent}return _result;`);
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -92,7 +92,7 @@ export default {
|
|
|
92
92
|
},
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
-
if (entry.feature
|
|
95
|
+
if (Object.hasOwn(AUTO_FIX, entry.feature)) {
|
|
96
96
|
report.fix = AUTO_FIX[entry.feature](node);
|
|
97
97
|
} else if (entry.feature === 'NullishCoalescing') {
|
|
98
98
|
report.fix = nullishCoalescingFix(context, node);
|
|
@@ -34,19 +34,19 @@ export default {
|
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const argument = node.arguments[0];
|
|
38
38
|
|
|
39
|
-
if (isAlreadySafe(
|
|
39
|
+
if (isAlreadySafe(argument)) {
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
context.report({
|
|
44
|
-
node:
|
|
44
|
+
node: argument,
|
|
45
45
|
messageId: 'unsafeArg',
|
|
46
|
-
data: { argText: context.sourceCode.getText(
|
|
46
|
+
data: { argText: context.sourceCode.getText(argument) },
|
|
47
47
|
fix(fixer) {
|
|
48
|
-
const
|
|
49
|
-
return fixer.replaceText(
|
|
48
|
+
const argumentText = context.sourceCode.getText(argument);
|
|
49
|
+
return fixer.replaceText(argument, `${argumentText} + ''`);
|
|
50
50
|
},
|
|
51
51
|
});
|
|
52
52
|
},
|
|
@@ -63,7 +63,7 @@ function isParseJSONCall(node) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// Platform.Function.ParseJSON(...)
|
|
66
|
-
|
|
66
|
+
return (
|
|
67
67
|
callee.type === 'MemberExpression' &&
|
|
68
68
|
callee.property.type === 'Identifier' &&
|
|
69
69
|
callee.property.name === 'ParseJSON' &&
|
|
@@ -72,29 +72,21 @@ function isParseJSONCall(node) {
|
|
|
72
72
|
callee.object.property.name === 'Function' &&
|
|
73
73
|
callee.object.object.type === 'Identifier' &&
|
|
74
74
|
callee.object.object.name === 'Platform'
|
|
75
|
-
)
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return false;
|
|
75
|
+
);
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
function isAlreadySafe(
|
|
78
|
+
function isAlreadySafe(argument) {
|
|
83
79
|
// arg + '' or '' + arg
|
|
84
80
|
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
(isEmptyString(
|
|
81
|
+
argument.type === 'BinaryExpression' &&
|
|
82
|
+
argument.operator === '+' &&
|
|
83
|
+
(isEmptyString(argument.left) || isEmptyString(argument.right))
|
|
88
84
|
) {
|
|
89
85
|
return true;
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
// String literal passed directly — already a string
|
|
93
|
-
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return false;
|
|
89
|
+
return argument.type === 'Literal' && typeof argument.value === 'string';
|
|
98
90
|
}
|
|
99
91
|
|
|
100
92
|
function isEmptyString(node) {
|