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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-no-mcn-unsupported
|
|
3
|
+
*
|
|
4
|
+
* Server-Side JavaScript as a whole is not available in Marketing Cloud Next.
|
|
5
|
+
* When targeting MCN, every SSJS API usage must be rewritten in AMPscript or
|
|
6
|
+
* Handlebars. This rule flags all SSJS API surface usage:
|
|
7
|
+
*
|
|
8
|
+
* - Platform.<...>() — any Platform namespace call (incl. Platform.Load)
|
|
9
|
+
* - HTTP.<method>() — any HTTP call
|
|
10
|
+
* - <coreVar>.<method>() — Core Library instance calls
|
|
11
|
+
* - <wsproxyVar>.<method>() — WSProxy instance calls
|
|
12
|
+
* - new Script.Util.WSProxy() — WSProxy construction
|
|
13
|
+
*
|
|
14
|
+
* The `apiVersion` option is accepted for parity with the AMPscript and
|
|
15
|
+
* Handlebars MCN rules, but currently has no effect: no MCN API version
|
|
16
|
+
* supports SSJS, so it always flags all SSJS usage.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { coreObjectNames } from 'ssjs-data';
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
meta: {
|
|
23
|
+
type: 'problem',
|
|
24
|
+
docs: {
|
|
25
|
+
description:
|
|
26
|
+
'Disallow all Server-Side JavaScript API usage, which is unavailable in Marketing Cloud Next',
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
ssjsNotSupportedInMcn:
|
|
30
|
+
'SSJS is not supported in Marketing Cloud Next. Rewrite this code in AMPscript or Handlebars.',
|
|
31
|
+
},
|
|
32
|
+
schema: [
|
|
33
|
+
{
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
apiVersion: {
|
|
37
|
+
type: 'number',
|
|
38
|
+
description:
|
|
39
|
+
'Targeted Marketing Cloud Next API version. Accepted for parity with the other MCN rules; currently has no effect because no version supports SSJS.',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
create(context) {
|
|
48
|
+
// Track variable name → Core Library type name (assigned via TypeName.Init())
|
|
49
|
+
const coreVariables = new Map();
|
|
50
|
+
// Track variable names assigned via new Script.Util.WSProxy()
|
|
51
|
+
const wsproxyVariables = new Set();
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
VariableDeclaration(node) {
|
|
55
|
+
for (const declaration of node.declarations) {
|
|
56
|
+
if (
|
|
57
|
+
!declaration.init ||
|
|
58
|
+
!declaration.id ||
|
|
59
|
+
declaration.id.type !== 'Identifier'
|
|
60
|
+
) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const coreType = getCoreInitType(declaration.init);
|
|
64
|
+
if (coreType) {
|
|
65
|
+
coreVariables.set(declaration.id.name, coreType);
|
|
66
|
+
}
|
|
67
|
+
if (isWSProxyConstructor(declaration.init)) {
|
|
68
|
+
wsproxyVariables.add(declaration.id.name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
AssignmentExpression(node) {
|
|
74
|
+
if (node.left.type !== 'Identifier') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const coreType = getCoreInitType(node.right);
|
|
78
|
+
if (coreType) {
|
|
79
|
+
coreVariables.set(node.left.name, coreType);
|
|
80
|
+
}
|
|
81
|
+
if (isWSProxyConstructor(node.right)) {
|
|
82
|
+
wsproxyVariables.add(node.left.name);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
NewExpression(node) {
|
|
87
|
+
if (isWSProxyConstructor(node)) {
|
|
88
|
+
context.report({ node: node.callee, messageId: 'ssjsNotSupportedInMcn' });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
CallExpression(node) {
|
|
93
|
+
const callee = node.callee;
|
|
94
|
+
if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier') {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Traverse the object chain to find the root identifier
|
|
99
|
+
// (handles de.Rows.Retrieve() where root is `de`,
|
|
100
|
+
// and Platform.Function.X() where root is `Platform`).
|
|
101
|
+
let rootNode = callee.object;
|
|
102
|
+
while (rootNode.type === 'MemberExpression') {
|
|
103
|
+
rootNode = rootNode.object;
|
|
104
|
+
}
|
|
105
|
+
if (rootNode.type !== 'Identifier') {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const rootName = rootNode.name;
|
|
109
|
+
|
|
110
|
+
const isSfmcApiCall =
|
|
111
|
+
rootName === 'Platform' ||
|
|
112
|
+
rootName === 'HTTP' ||
|
|
113
|
+
coreVariables.has(rootName) ||
|
|
114
|
+
wsproxyVariables.has(rootName);
|
|
115
|
+
|
|
116
|
+
if (isSfmcApiCall) {
|
|
117
|
+
context.report({
|
|
118
|
+
node: callee.property,
|
|
119
|
+
messageId: 'ssjsNotSupportedInMcn',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* If `node` is a Core Library Init call (e.g. `DataExtension.Init("key")`),
|
|
129
|
+
* return the Core Library type name; otherwise return null.
|
|
130
|
+
*
|
|
131
|
+
* @param {import('eslint').Rule.Node} node - AST node to inspect
|
|
132
|
+
* @returns {string | null} Core Library type name, or null when not a Core Library Init call
|
|
133
|
+
*/
|
|
134
|
+
function getCoreInitType(node) {
|
|
135
|
+
if (!node || node.type !== 'CallExpression') {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const callee = node.callee;
|
|
139
|
+
if (callee.type !== 'MemberExpression') {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
|
|
146
|
+
return callee.object.name;
|
|
147
|
+
}
|
|
148
|
+
if (
|
|
149
|
+
callee.object.type === 'MemberExpression' &&
|
|
150
|
+
callee.object.object.type === 'Identifier' &&
|
|
151
|
+
callee.object.property.type === 'Identifier'
|
|
152
|
+
) {
|
|
153
|
+
const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
|
|
154
|
+
if (coreObjectNames.has(fullName)) {
|
|
155
|
+
return fullName;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns true if `node` is `new Script.Util.WSProxy()`.
|
|
163
|
+
*
|
|
164
|
+
* @param {import('eslint').Rule.Node} node - AST node to inspect
|
|
165
|
+
* @returns {boolean} true when the node is a WSProxy constructor call
|
|
166
|
+
*/
|
|
167
|
+
function isWSProxyConstructor(node) {
|
|
168
|
+
if (!node || node.type !== 'NewExpression') {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const c = node.callee;
|
|
172
|
+
return (
|
|
173
|
+
c.type === 'MemberExpression' &&
|
|
174
|
+
c.property.type === 'Identifier' &&
|
|
175
|
+
c.property.name === 'WSProxy' &&
|
|
176
|
+
c.object.type === 'MemberExpression' &&
|
|
177
|
+
c.object.property.type === 'Identifier' &&
|
|
178
|
+
c.object.property.name === 'Util' &&
|
|
179
|
+
c.object.object.type === 'Identifier' &&
|
|
180
|
+
c.object.object.name === 'Script'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -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
|
|
|
@@ -49,29 +49,11 @@ export default {
|
|
|
49
49
|
unknownCoreMethod:
|
|
50
50
|
"'{{method}}' is not a known method of {{objectType}}. Available methods: {{available}}.",
|
|
51
51
|
unknownWsproxyMethod: "'{{name}}' is not a recognized WSProxy method.",
|
|
52
|
-
ssjsNotSupportedInMcn:
|
|
53
|
-
'SSJS is not supported in Marketing Cloud Next. Use AMPscript instead.',
|
|
54
52
|
},
|
|
55
|
-
schema: [
|
|
56
|
-
{
|
|
57
|
-
type: 'object',
|
|
58
|
-
properties: {
|
|
59
|
-
target: {
|
|
60
|
-
type: 'string',
|
|
61
|
-
enum: ['engagement', 'next'],
|
|
62
|
-
description:
|
|
63
|
-
"Target platform. Set to 'next' to flag all SSJS API calls as unsupported in Marketing Cloud Next.",
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
additionalProperties: false,
|
|
67
|
-
},
|
|
68
|
-
],
|
|
53
|
+
schema: [],
|
|
69
54
|
},
|
|
70
55
|
|
|
71
56
|
create(context) {
|
|
72
|
-
const options = context.options[0] ?? {};
|
|
73
|
-
const targetNext = options.target === 'next';
|
|
74
|
-
|
|
75
57
|
// Track variable name → Core Library type name (assigned via TypeName.Init())
|
|
76
58
|
const coreVariables = new Map();
|
|
77
59
|
// Track variable names assigned via new Script.Util.WSProxy()
|
|
@@ -79,16 +61,20 @@ export default {
|
|
|
79
61
|
|
|
80
62
|
return {
|
|
81
63
|
VariableDeclaration(node) {
|
|
82
|
-
for (const
|
|
83
|
-
if (
|
|
64
|
+
for (const declaration of node.declarations) {
|
|
65
|
+
if (
|
|
66
|
+
!declaration.init ||
|
|
67
|
+
!declaration.id ||
|
|
68
|
+
declaration.id.type !== 'Identifier'
|
|
69
|
+
) {
|
|
84
70
|
continue;
|
|
85
71
|
}
|
|
86
|
-
const coreType = getCoreInitType(
|
|
72
|
+
const coreType = getCoreInitType(declaration.init);
|
|
87
73
|
if (coreType) {
|
|
88
|
-
coreVariables.set(
|
|
74
|
+
coreVariables.set(declaration.id.name, coreType);
|
|
89
75
|
}
|
|
90
|
-
if (isWSProxyConstructor(
|
|
91
|
-
wsproxyVariables.add(
|
|
76
|
+
if (isWSProxyConstructor(declaration.init)) {
|
|
77
|
+
wsproxyVariables.add(declaration.id.name);
|
|
92
78
|
}
|
|
93
79
|
}
|
|
94
80
|
},
|
|
@@ -118,36 +104,6 @@ export default {
|
|
|
118
104
|
|
|
119
105
|
const methodName = property.name;
|
|
120
106
|
|
|
121
|
-
// ── MCN target: flag all SSJS API calls as unsupported ────────
|
|
122
|
-
if (targetNext) {
|
|
123
|
-
// Only report on Platform.<NS>.<method>(), HTTP.<method>(),
|
|
124
|
-
// Core Library instance calls, or WSProxy calls — not bare JS.
|
|
125
|
-
const isPlatformCall =
|
|
126
|
-
callee.object.type === 'MemberExpression' &&
|
|
127
|
-
callee.object.object.type === 'Identifier' &&
|
|
128
|
-
callee.object.object.name === 'Platform';
|
|
129
|
-
const isHttpCall =
|
|
130
|
-
callee.object.type === 'Identifier' && callee.object.name === 'HTTP';
|
|
131
|
-
|
|
132
|
-
// Traverse the object chain to find the root identifier
|
|
133
|
-
// (handles de.Rows.Retrieve() where root is `de`)
|
|
134
|
-
let rootNode = callee.object;
|
|
135
|
-
while (rootNode.type === 'MemberExpression') {
|
|
136
|
-
rootNode = rootNode.object;
|
|
137
|
-
}
|
|
138
|
-
const isInstanceCall =
|
|
139
|
-
rootNode.type === 'Identifier' &&
|
|
140
|
-
(coreVariables.has(rootNode.name) || wsproxyVariables.has(rootNode.name));
|
|
141
|
-
|
|
142
|
-
if (isPlatformCall || isHttpCall || isInstanceCall) {
|
|
143
|
-
context.report({
|
|
144
|
-
node: property,
|
|
145
|
-
messageId: 'ssjsNotSupportedInMcn',
|
|
146
|
-
});
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
107
|
// ── Platform.<NS>.<method>() ──────────────────────────────────
|
|
152
108
|
if (
|
|
153
109
|
callee.object.type === 'MemberExpression' &&
|
|
@@ -189,10 +145,10 @@ export default {
|
|
|
189
145
|
// Core library: var de = DataExtension.Init("key"); de.Foo();
|
|
190
146
|
const coreType = coreVariables.get(objectName);
|
|
191
147
|
if (coreType) {
|
|
192
|
-
const
|
|
193
|
-
if (
|
|
148
|
+
const objectDefinition = coreObjectLookup.get(coreType);
|
|
149
|
+
if (objectDefinition) {
|
|
194
150
|
const knownMethods = new Set(
|
|
195
|
-
|
|
151
|
+
objectDefinition.methods.map((m) => m.toLowerCase()),
|
|
196
152
|
);
|
|
197
153
|
if (!knownMethods.has(methodName.toLowerCase())) {
|
|
198
154
|
context.report({
|
|
@@ -201,7 +157,7 @@ export default {
|
|
|
201
157
|
data: {
|
|
202
158
|
method: methodName,
|
|
203
159
|
objectType: coreType,
|
|
204
|
-
available:
|
|
160
|
+
available: objectDefinition.methods.join(', '),
|
|
205
161
|
},
|
|
206
162
|
});
|
|
207
163
|
}
|
|
@@ -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) {
|