eslint-plugin-sfmc 0.1.3 → 1.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 +43 -47
- package/docs/rules/ssjs/arg-types.md +72 -0
- package/docs/rules/ssjs/core-method-arity.md +72 -0
- package/docs/rules/ssjs/no-deprecated-function.md +56 -0
- package/docs/rules/ssjs/no-property-call.md +95 -0
- package/docs/rules/ssjs/no-unknown-function.md +57 -0
- package/package.json +18 -5
- package/src/ampscript-parser.js +11 -4
- package/src/ampscript-processor.js +9 -3
- package/src/index.js +23 -33
- package/src/processor.js +12 -4
- package/src/rules/amp/function-arity.js +3 -1
- package/src/rules/amp/naming-convention.js +7 -3
- package/src/rules/amp/no-deprecated-function.js +6 -2
- package/src/rules/amp/no-email-excluded-function.js +3 -1
- package/src/rules/amp/no-inline-statement.js +3 -1
- package/src/rules/amp/no-nested-ampscript-delimiter.js +5 -5
- package/src/rules/amp/no-nested-script-tag.js +3 -1
- package/src/rules/amp/no-smart-quotes.js +1 -1
- package/src/rules/amp/prefer-attribute-value.js +10 -4
- package/src/rules/amp/require-rowcount-check.js +3 -1
- package/src/rules/amp/require-variable-declaration.js +7 -3
- package/src/rules/ssjs/cache-loop-length.js +10 -4
- package/src/rules/ssjs/no-deprecated-function.js +181 -0
- package/src/rules/ssjs/no-hardcoded-credentials.js +3 -1
- package/src/rules/ssjs/no-property-call.js +136 -0
- package/src/rules/ssjs/no-treatascontent-injection.js +8 -4
- package/src/rules/ssjs/no-unavailable-method.js +30 -10
- package/src/rules/ssjs/no-unknown-function.js +236 -0
- package/src/rules/ssjs/no-unsupported-syntax.js +10 -5
- package/src/rules/ssjs/platform-function-arity.js +6 -2
- package/src/rules/ssjs/prefer-parsejson-safe-arg.js +17 -9
- package/src/rules/ssjs/prefer-platform-load-version.js +1 -4
- package/src/rules/ssjs/require-hasownproperty.js +34 -27
- package/src/rules/ssjs/require-platform-load-order.js +6 -2
- package/src/rules/ssjs/require-platform-load.js +32 -12
- package/src/rules/ssjs/ssjs-arg-types.js +345 -0
- package/src/rules/ssjs/ssjs-core-method-arity.js +176 -0
- package/src/ssjs-processor.js +3 -1
- package/docs/rules/ssjs/no-unknown-core-method.md +0 -46
- package/docs/rules/ssjs/no-unknown-http-method.md +0 -44
- package/docs/rules/ssjs/no-unknown-platform-client-browser.md +0 -44
- package/docs/rules/ssjs/no-unknown-platform-function.md +0 -44
- package/docs/rules/ssjs/no-unknown-platform-request.md +0 -43
- package/docs/rules/ssjs/no-unknown-platform-response.md +0 -43
- package/docs/rules/ssjs/no-unknown-platform-variable.md +0 -43
- package/docs/rules/ssjs/no-unknown-wsproxy-method.md +0 -46
- package/src/rules/ssjs/no-unknown-core-method.js +0 -103
- package/src/rules/ssjs/no-unknown-http-method.js +0 -41
- package/src/rules/ssjs/no-unknown-platform-client-browser.js +0 -50
- package/src/rules/ssjs/no-unknown-platform-function.js +0 -49
- package/src/rules/ssjs/no-unknown-platform-request.js +0 -50
- package/src/rules/ssjs/no-unknown-platform-response.js +0 -50
- package/src/rules/ssjs/no-unknown-platform-variable.js +0 -50
- package/src/rules/ssjs/no-unknown-wsproxy-method.js +0 -71
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-no-property-call
|
|
3
|
+
*
|
|
4
|
+
* Flags Platform.Request and Platform.Response members that are properties,
|
|
5
|
+
* not methods. Calling them with `()` is a runtime error.
|
|
6
|
+
*
|
|
7
|
+
* Three cases are handled:
|
|
8
|
+
*
|
|
9
|
+
* 1. Read with parens (both namespaces, no args):
|
|
10
|
+
* Platform.Request.Method() → Platform.Request.Method
|
|
11
|
+
*
|
|
12
|
+
* 2. Write via function call (Platform.Response.* only, single arg):
|
|
13
|
+
* Platform.Response.ContentType("text/html")
|
|
14
|
+
* → Platform.Response.ContentType = "text/html"
|
|
15
|
+
*
|
|
16
|
+
* 3. Attempt to set a read-only property (Platform.Request.*, with args):
|
|
17
|
+
* Platform.Request.Method("POST") → no auto-fix (read-only)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { PLATFORM_RESPONSE_METHODS, PLATFORM_REQUEST_METHODS } from 'ssjs-data';
|
|
21
|
+
|
|
22
|
+
// Platform.Response properties — readable AND writable
|
|
23
|
+
const RESPONSE_PROPERTIES = new Set(
|
|
24
|
+
PLATFORM_RESPONSE_METHODS.filter((m) => m.isProperty).map((m) => m.name.toLowerCase()),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Platform.Request properties — read-only
|
|
28
|
+
const REQUEST_PROPERTIES = new Set(
|
|
29
|
+
PLATFORM_REQUEST_METHODS.filter((m) => m.isProperty).map((m) => m.name.toLowerCase()),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
meta: {
|
|
34
|
+
type: 'problem',
|
|
35
|
+
fixable: 'code',
|
|
36
|
+
docs: {
|
|
37
|
+
description: 'Disallow calling Platform.Request/Response properties as functions',
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
propertyReadWithCall:
|
|
41
|
+
"'Platform.{{ns}}.{{name}}' is a property, not a function. " +
|
|
42
|
+
"Remove the '()' to read its value.",
|
|
43
|
+
readOnlyPropertySet:
|
|
44
|
+
"'Platform.Request.{{name}}' is a read-only property. It cannot be assigned a value.",
|
|
45
|
+
writablePropertySet:
|
|
46
|
+
"'Platform.Response.{{name}}' is a property. " +
|
|
47
|
+
'Use `Platform.Response.{{name}} = value` to assign instead of calling it as a function.',
|
|
48
|
+
},
|
|
49
|
+
schema: [],
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
create(context) {
|
|
53
|
+
const sourceCode = context.sourceCode;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
CallExpression(node) {
|
|
57
|
+
const callee = node.callee;
|
|
58
|
+
|
|
59
|
+
// Must be Platform.<NS>.<property>()
|
|
60
|
+
if (
|
|
61
|
+
callee.type !== 'MemberExpression' ||
|
|
62
|
+
callee.object.type !== 'MemberExpression' ||
|
|
63
|
+
callee.object.object.type !== 'Identifier' ||
|
|
64
|
+
callee.object.object.name !== 'Platform' ||
|
|
65
|
+
callee.object.property.type !== 'Identifier' ||
|
|
66
|
+
callee.property.type !== 'Identifier'
|
|
67
|
+
) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ns = callee.object.property.name.toLowerCase();
|
|
72
|
+
const propName = callee.property.name.toLowerCase();
|
|
73
|
+
const displayNs = callee.object.property.name;
|
|
74
|
+
const displayName = callee.property.name;
|
|
75
|
+
|
|
76
|
+
if (ns === 'response' && RESPONSE_PROPERTIES.has(propName)) {
|
|
77
|
+
if (node.arguments.length === 0) {
|
|
78
|
+
// Reading with parens — remove ()
|
|
79
|
+
context.report({
|
|
80
|
+
node,
|
|
81
|
+
messageId: 'propertyReadWithCall',
|
|
82
|
+
data: { ns: displayNs, name: displayName },
|
|
83
|
+
fix(fixer) {
|
|
84
|
+
return fixer.replaceText(node, sourceCode.getText(callee));
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
} else if (node.arguments.length === 1) {
|
|
88
|
+
// Setting via function call — convert to assignment
|
|
89
|
+
context.report({
|
|
90
|
+
node,
|
|
91
|
+
messageId: 'writablePropertySet',
|
|
92
|
+
data: { ns: displayNs, name: displayName },
|
|
93
|
+
fix(fixer) {
|
|
94
|
+
// Only safe when the call is a standalone expression statement
|
|
95
|
+
if (node.parent.type !== 'ExpressionStatement') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const argText = sourceCode.getText(node.arguments[0]);
|
|
99
|
+
return fixer.replaceText(
|
|
100
|
+
node,
|
|
101
|
+
`${sourceCode.getText(callee)} = ${argText}`,
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
// >1 args — unusual, flag without a fix
|
|
107
|
+
context.report({
|
|
108
|
+
node,
|
|
109
|
+
messageId: 'writablePropertySet',
|
|
110
|
+
data: { ns: displayNs, name: displayName },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} else if (ns === 'request' && REQUEST_PROPERTIES.has(propName)) {
|
|
114
|
+
if (node.arguments.length === 0) {
|
|
115
|
+
// Reading with parens — remove ()
|
|
116
|
+
context.report({
|
|
117
|
+
node,
|
|
118
|
+
messageId: 'propertyReadWithCall',
|
|
119
|
+
data: { ns: displayNs, name: displayName },
|
|
120
|
+
fix(fixer) {
|
|
121
|
+
return fixer.replaceText(node, sourceCode.getText(callee));
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
// Attempting to set a read-only property — no fix
|
|
126
|
+
context.report({
|
|
127
|
+
node,
|
|
128
|
+
messageId: 'readOnlyPropertySet',
|
|
129
|
+
data: { name: displayName },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -19,8 +19,8 @@ export default {
|
|
|
19
19
|
messages: {
|
|
20
20
|
injection:
|
|
21
21
|
'Concatenating dynamic values into TreatAsContent() risks AMPscript injection. ' +
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
'Use Variable.SetValue() to pass values into AMPscript variables, then reference ' +
|
|
23
|
+
'them with @varName inside the TreatAsContent string.',
|
|
24
24
|
},
|
|
25
25
|
schema: [],
|
|
26
26
|
},
|
|
@@ -28,8 +28,12 @@ export default {
|
|
|
28
28
|
create(context) {
|
|
29
29
|
return {
|
|
30
30
|
CallExpression(node) {
|
|
31
|
-
if (!isTreatAsContentCall(node))
|
|
32
|
-
|
|
31
|
+
if (!isTreatAsContentCall(node)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (node.arguments.length === 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
33
37
|
|
|
34
38
|
const arg = node.arguments[0];
|
|
35
39
|
if (containsConcatenation(arg)) {
|
|
@@ -94,11 +94,15 @@ export default {
|
|
|
94
94
|
// Array.prototype.map = ... / Array.isArray = ... / String.prototype.trim = ...
|
|
95
95
|
AssignmentExpression(node) {
|
|
96
96
|
const left = node.left;
|
|
97
|
-
if (left.type !== 'MemberExpression')
|
|
97
|
+
if (left.type !== 'MemberExpression') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
98
100
|
|
|
99
101
|
const obj = left.object;
|
|
100
102
|
const prop = left.property;
|
|
101
|
-
if (prop.type !== 'Identifier')
|
|
103
|
+
if (prop.type !== 'Identifier') {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
102
106
|
|
|
103
107
|
// Array.X = ...
|
|
104
108
|
if (obj.type === 'Identifier' && obj.name === 'Array') {
|
|
@@ -120,10 +124,14 @@ export default {
|
|
|
120
124
|
|
|
121
125
|
CallExpression(node) {
|
|
122
126
|
const callee = node.callee;
|
|
123
|
-
if (callee.type !== 'MemberExpression')
|
|
127
|
+
if (callee.type !== 'MemberExpression') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
124
130
|
|
|
125
131
|
const prop = callee.property;
|
|
126
|
-
if (prop.type !== 'Identifier')
|
|
132
|
+
if (prop.type !== 'Identifier') {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
127
135
|
|
|
128
136
|
const methodName = prop.name;
|
|
129
137
|
const receiver = callee.object;
|
|
@@ -134,20 +142,28 @@ export default {
|
|
|
134
142
|
receiver.name === 'Array' &&
|
|
135
143
|
polyfillByStaticName.has(methodName)
|
|
136
144
|
) {
|
|
137
|
-
if (ignored.has(methodName))
|
|
145
|
+
if (ignored.has(methodName)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
138
148
|
const entry = polyfillByStaticName.get(methodName);
|
|
139
149
|
pendingReports.push({ node: prop, entry });
|
|
140
150
|
return;
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
// ── Prototype methods ─────────────────────────────────────────
|
|
144
|
-
if (!polyfillByPrototypeName.has(methodName))
|
|
145
|
-
|
|
154
|
+
if (!polyfillByPrototypeName.has(methodName)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (ignored.has(methodName)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
146
160
|
|
|
147
161
|
const entry = polyfillByPrototypeName.get(methodName);
|
|
148
162
|
|
|
149
163
|
// Skip known SFMC top-level objects to avoid false positives.
|
|
150
|
-
if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name))
|
|
164
|
+
if (receiver.type === 'Identifier' && SFMC_RECEIVERS.has(receiver.name)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
151
167
|
|
|
152
168
|
// For methods that also exist on String.prototype in ES3
|
|
153
169
|
// (indexOf, lastIndexOf), only flag when the receiver is
|
|
@@ -161,7 +177,9 @@ export default {
|
|
|
161
177
|
(receiver.type === 'CallExpression' &&
|
|
162
178
|
receiver.callee.type === 'MemberExpression' &&
|
|
163
179
|
polyfillByPrototypeName.has(receiver.callee.property.name));
|
|
164
|
-
if (!isDefinitelyArray)
|
|
180
|
+
if (!isDefinitelyArray) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
165
183
|
}
|
|
166
184
|
|
|
167
185
|
pendingReports.push({ node: prop, entry });
|
|
@@ -169,7 +187,9 @@ export default {
|
|
|
169
187
|
|
|
170
188
|
'Program:exit'() {
|
|
171
189
|
for (const { node, entry } of pendingReports) {
|
|
172
|
-
if (isAlreadyPolyfilled(entry.method))
|
|
190
|
+
if (isAlreadyPolyfilled(entry.method)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
173
193
|
|
|
174
194
|
context.report({
|
|
175
195
|
node,
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-no-unknown-function
|
|
3
|
+
*
|
|
4
|
+
* Unified rule that replaces the seven narrow no-unknown-* rules.
|
|
5
|
+
* Flags calls to API methods that do not exist in the SFMC catalog:
|
|
6
|
+
*
|
|
7
|
+
* - Platform.Function.<method>() — unknown Platform.Function method
|
|
8
|
+
* - Platform.Variable.<method>() — unknown Platform.Variable method
|
|
9
|
+
* - Platform.Request.<method>() — unknown Platform.Request method
|
|
10
|
+
* - Platform.Response.<method>() — unknown Platform.Response method
|
|
11
|
+
* - Platform.Recipient.<method>() — unknown Platform.Recipient method
|
|
12
|
+
* - HTTP.<method>() — unknown HTTP method
|
|
13
|
+
* - <wsproxyVar>.<method>() — unknown WSProxy method on a tracked variable
|
|
14
|
+
* - <coreVar>.<method>() — unknown method on a tracked Core Library object
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
platformFunctionNames,
|
|
19
|
+
PLATFORM_VARIABLE_METHODS,
|
|
20
|
+
PLATFORM_REQUEST_METHODS,
|
|
21
|
+
PLATFORM_RESPONSE_METHODS,
|
|
22
|
+
platformRecipientMethodNames,
|
|
23
|
+
httpMethodNames,
|
|
24
|
+
coreObjectLookup,
|
|
25
|
+
coreObjectNames,
|
|
26
|
+
wsproxyMethodNames,
|
|
27
|
+
} from 'ssjs-data';
|
|
28
|
+
|
|
29
|
+
// Map of lowercase Platform namespace → Set of known lowercase method names
|
|
30
|
+
const PLATFORM_NS = new Map([
|
|
31
|
+
['function', platformFunctionNames],
|
|
32
|
+
['variable', new Set(PLATFORM_VARIABLE_METHODS.map((m) => m.name.toLowerCase()))],
|
|
33
|
+
['request', new Set(PLATFORM_REQUEST_METHODS.map((m) => m.name.toLowerCase()))],
|
|
34
|
+
['response', new Set(PLATFORM_RESPONSE_METHODS.map((m) => m.name.toLowerCase()))],
|
|
35
|
+
['recipient', platformRecipientMethodNames],
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
meta: {
|
|
40
|
+
type: 'problem',
|
|
41
|
+
docs: {
|
|
42
|
+
description:
|
|
43
|
+
'Disallow calls to unknown SFMC API methods (Platform.*, HTTP.*, Core library, WSProxy)',
|
|
44
|
+
},
|
|
45
|
+
messages: {
|
|
46
|
+
unknownPlatformMethod:
|
|
47
|
+
"'Platform.{{ns}}.{{name}}' is not a recognized SFMC Platform.{{ns}} method.",
|
|
48
|
+
unknownHttpMethod: "'HTTP.{{name}}' is not a recognized SFMC HTTP method.",
|
|
49
|
+
unknownCoreMethod:
|
|
50
|
+
"'{{method}}' is not a known method of {{objectType}}. Available methods: {{available}}.",
|
|
51
|
+
unknownWsproxyMethod: "'{{name}}' is not a recognized WSProxy method.",
|
|
52
|
+
},
|
|
53
|
+
schema: [],
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
create(context) {
|
|
57
|
+
// Track variable name → Core Library type name (assigned via TypeName.Init())
|
|
58
|
+
const coreVariables = new Map();
|
|
59
|
+
// Track variable names assigned via new Script.Util.WSProxy()
|
|
60
|
+
const wsproxyVariables = new Set();
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
VariableDeclaration(node) {
|
|
64
|
+
for (const decl of node.declarations) {
|
|
65
|
+
if (!decl.init || !decl.id || decl.id.type !== 'Identifier') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const coreType = getCoreInitType(decl.init);
|
|
69
|
+
if (coreType) {
|
|
70
|
+
coreVariables.set(decl.id.name, coreType);
|
|
71
|
+
}
|
|
72
|
+
if (isWSProxyConstructor(decl.init)) {
|
|
73
|
+
wsproxyVariables.add(decl.id.name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
AssignmentExpression(node) {
|
|
79
|
+
if (node.left.type !== 'Identifier') {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const coreType = getCoreInitType(node.right);
|
|
83
|
+
if (coreType) {
|
|
84
|
+
coreVariables.set(node.left.name, coreType);
|
|
85
|
+
}
|
|
86
|
+
if (isWSProxyConstructor(node.right)) {
|
|
87
|
+
wsproxyVariables.add(node.left.name);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
CallExpression(node) {
|
|
92
|
+
const callee = node.callee;
|
|
93
|
+
if (callee.type !== 'MemberExpression') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const property = callee.property;
|
|
97
|
+
if (property.type !== 'Identifier') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const methodName = property.name;
|
|
102
|
+
|
|
103
|
+
// ── Platform.<NS>.<method>() ──────────────────────────────────
|
|
104
|
+
if (
|
|
105
|
+
callee.object.type === 'MemberExpression' &&
|
|
106
|
+
callee.object.object.type === 'Identifier' &&
|
|
107
|
+
callee.object.object.name === 'Platform' &&
|
|
108
|
+
callee.object.property.type === 'Identifier'
|
|
109
|
+
) {
|
|
110
|
+
const ns = callee.object.property.name.toLowerCase();
|
|
111
|
+
const knownNames = PLATFORM_NS.get(ns);
|
|
112
|
+
// Only report if we know this namespace; unknown namespaces are ignored.
|
|
113
|
+
if (knownNames && !knownNames.has(methodName.toLowerCase())) {
|
|
114
|
+
context.report({
|
|
115
|
+
node: property,
|
|
116
|
+
messageId: 'unknownPlatformMethod',
|
|
117
|
+
data: { ns: callee.object.property.name, name: methodName },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── HTTP.<method>() ───────────────────────────────────────────
|
|
124
|
+
if (
|
|
125
|
+
callee.object.type === 'Identifier' &&
|
|
126
|
+
callee.object.name === 'HTTP' &&
|
|
127
|
+
!httpMethodNames.has(methodName.toLowerCase())
|
|
128
|
+
) {
|
|
129
|
+
context.report({
|
|
130
|
+
node: property,
|
|
131
|
+
messageId: 'unknownHttpMethod',
|
|
132
|
+
data: { name: methodName },
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Instance method calls (Core Library / WSProxy) ────────────
|
|
138
|
+
if (callee.object.type === 'Identifier') {
|
|
139
|
+
const objectName = callee.object.name;
|
|
140
|
+
|
|
141
|
+
// Core library: var de = DataExtension.Init("key"); de.Foo();
|
|
142
|
+
const coreType = coreVariables.get(objectName);
|
|
143
|
+
if (coreType) {
|
|
144
|
+
const objectDef = coreObjectLookup.get(coreType);
|
|
145
|
+
if (objectDef) {
|
|
146
|
+
const knownMethods = new Set(
|
|
147
|
+
objectDef.methods.map((m) => m.toLowerCase()),
|
|
148
|
+
);
|
|
149
|
+
if (!knownMethods.has(methodName.toLowerCase())) {
|
|
150
|
+
context.report({
|
|
151
|
+
node: property,
|
|
152
|
+
messageId: 'unknownCoreMethod',
|
|
153
|
+
data: {
|
|
154
|
+
method: methodName,
|
|
155
|
+
objectType: coreType,
|
|
156
|
+
available: objectDef.methods.join(', '),
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// WSProxy: var api = new Script.Util.WSProxy(); api.Foo();
|
|
165
|
+
if (
|
|
166
|
+
wsproxyVariables.has(objectName) &&
|
|
167
|
+
!wsproxyMethodNames.has(methodName.toLowerCase())
|
|
168
|
+
) {
|
|
169
|
+
context.report({
|
|
170
|
+
node: property,
|
|
171
|
+
messageId: 'unknownWsproxyMethod',
|
|
172
|
+
data: { name: methodName },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* If `node` is a Core Library Init call (e.g. `DataExtension.Init("key")`),
|
|
183
|
+
* return the Core Library type name; otherwise return null.
|
|
184
|
+
*
|
|
185
|
+
* @param {import('eslint').Rule.Node} node
|
|
186
|
+
* @returns {string | null}
|
|
187
|
+
*/
|
|
188
|
+
function getCoreInitType(node) {
|
|
189
|
+
if (!node || node.type !== 'CallExpression') {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const callee = node.callee;
|
|
193
|
+
if (callee.type !== 'MemberExpression') {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
|
|
200
|
+
return callee.object.name;
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
callee.object.type === 'MemberExpression' &&
|
|
204
|
+
callee.object.object.type === 'Identifier' &&
|
|
205
|
+
callee.object.property.type === 'Identifier'
|
|
206
|
+
) {
|
|
207
|
+
const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
|
|
208
|
+
if (coreObjectNames.has(fullName)) {
|
|
209
|
+
return fullName;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Returns true if `node` is `new Script.Util.WSProxy()`.
|
|
217
|
+
*
|
|
218
|
+
* @param {import('eslint').Rule.Node} node
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
function isWSProxyConstructor(node) {
|
|
222
|
+
if (!node || node.type !== 'NewExpression') {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
const c = node.callee;
|
|
226
|
+
return (
|
|
227
|
+
c.type === 'MemberExpression' &&
|
|
228
|
+
c.property.type === 'Identifier' &&
|
|
229
|
+
c.property.name === 'WSProxy' &&
|
|
230
|
+
c.object.type === 'MemberExpression' &&
|
|
231
|
+
c.object.property.type === 'Identifier' &&
|
|
232
|
+
c.object.property.name === 'Util' &&
|
|
233
|
+
c.object.object.type === 'Identifier' &&
|
|
234
|
+
c.object.object.name === 'Script'
|
|
235
|
+
);
|
|
236
|
+
}
|
|
@@ -36,8 +36,7 @@ export default {
|
|
|
36
36
|
unsupported: '{{label}} are not supported in SFMC SSJS. {{suggestion}}',
|
|
37
37
|
suggestLogicalOr:
|
|
38
38
|
"Replace ?? with || (note: semantics differ for falsy values like 0, '', and false)",
|
|
39
|
-
suggestVarReturn:
|
|
40
|
-
'Assign the object to a variable, then return the variable',
|
|
39
|
+
suggestVarReturn: 'Assign the object to a variable, then return the variable',
|
|
41
40
|
},
|
|
42
41
|
schema: [
|
|
43
42
|
{
|
|
@@ -65,8 +64,12 @@ export default {
|
|
|
65
64
|
for (const [nodeType, entries] of unsupportedByNodeType) {
|
|
66
65
|
listeners[nodeType] = function (node) {
|
|
67
66
|
for (const entry of entries) {
|
|
68
|
-
if (allowed.has(entry.feature))
|
|
69
|
-
|
|
67
|
+
if (allowed.has(entry.feature)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (entry.test && !entry.test(node)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
70
73
|
|
|
71
74
|
const report = {
|
|
72
75
|
node,
|
|
@@ -90,7 +93,9 @@ export default {
|
|
|
90
93
|
node.right.range[0],
|
|
91
94
|
);
|
|
92
95
|
const offset = between.indexOf('??');
|
|
93
|
-
if (offset === -1)
|
|
96
|
+
if (offset === -1) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
94
99
|
const start = node.left.range[1] + offset;
|
|
95
100
|
return fixer.replaceTextRange([start, start + 2], '||');
|
|
96
101
|
},
|
|
@@ -26,7 +26,9 @@ export default {
|
|
|
26
26
|
return {
|
|
27
27
|
CallExpression(node) {
|
|
28
28
|
const callee = node.callee;
|
|
29
|
-
if (callee.type !== 'MemberExpression')
|
|
29
|
+
if (callee.type !== 'MemberExpression') {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
if (
|
|
32
34
|
callee.object.type === 'MemberExpression' &&
|
|
@@ -38,7 +40,9 @@ export default {
|
|
|
38
40
|
) {
|
|
39
41
|
const methodName = callee.property.name;
|
|
40
42
|
const entry = platformFunctionLookup.get(methodName.toLowerCase());
|
|
41
|
-
if (!entry)
|
|
43
|
+
if (!entry) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
42
46
|
|
|
43
47
|
const actual = node.arguments.length;
|
|
44
48
|
|
|
@@ -14,11 +14,11 @@ export default {
|
|
|
14
14
|
fixable: 'code',
|
|
15
15
|
docs: {
|
|
16
16
|
description:
|
|
17
|
-
|
|
17
|
+
'Require concatenating an empty string to Platform.Function.ParseJSON() arguments to prevent 500 errors',
|
|
18
18
|
},
|
|
19
19
|
messages: {
|
|
20
20
|
unsafeArg:
|
|
21
|
-
|
|
21
|
+
'Platform.Function.ParseJSON() may throw a 500 if the argument is undefined. ' +
|
|
22
22
|
"Concatenate an empty string to be safe: ParseJSON({{argText}} + '').",
|
|
23
23
|
},
|
|
24
24
|
schema: [],
|
|
@@ -27,12 +27,18 @@ export default {
|
|
|
27
27
|
create(context) {
|
|
28
28
|
return {
|
|
29
29
|
CallExpression(node) {
|
|
30
|
-
if (!isParseJSONCall(node))
|
|
31
|
-
|
|
30
|
+
if (!isParseJSONCall(node)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (node.arguments.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
32
36
|
|
|
33
37
|
const arg = node.arguments[0];
|
|
34
38
|
|
|
35
|
-
if (isAlreadySafe(arg))
|
|
39
|
+
if (isAlreadySafe(arg)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
36
42
|
|
|
37
43
|
context.report({
|
|
38
44
|
node: arg,
|
|
@@ -75,10 +81,12 @@ function isParseJSONCall(node) {
|
|
|
75
81
|
|
|
76
82
|
function isAlreadySafe(arg) {
|
|
77
83
|
// arg + '' or '' + arg
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
if (
|
|
85
|
+
arg.type === 'BinaryExpression' &&
|
|
86
|
+
arg.operator === '+' &&
|
|
87
|
+
(isEmptyString(arg.left) || isEmptyString(arg.right))
|
|
88
|
+
) {
|
|
89
|
+
return true;
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
// String literal passed directly — already a string
|
|
@@ -66,10 +66,7 @@ export default {
|
|
|
66
66
|
messageId: 'outdatedVersion',
|
|
67
67
|
data: { actual: '(none)', expected: expectedVersion },
|
|
68
68
|
fix(fixer) {
|
|
69
|
-
return fixer.insertTextAfter(
|
|
70
|
-
arguments_[0],
|
|
71
|
-
`, "${expectedVersion}"`,
|
|
72
|
-
);
|
|
69
|
+
return fixer.insertTextAfter(arguments_[0], `, "${expectedVersion}"`);
|
|
73
70
|
},
|
|
74
71
|
});
|
|
75
72
|
return;
|