eslint-plugin-sfmc 4.6.0 → 4.7.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
CHANGED
|
@@ -140,6 +140,7 @@ export default [...sfmc.configs['recommended-next'], ...sfmc.configs['embedded-n
|
|
|
140
140
|
| [`sfmc/ssjs-require-hasownproperty`](docs/rules/ssjs/require-hasownproperty.md) | `warn` | Require `hasOwnProperty` guard in for-in loops |
|
|
141
141
|
| [`sfmc/ssjs-prefer-platform-load-version`](docs/rules/ssjs/prefer-platform-load-version.md) | `warn` | Enforce a minimum `Platform.Load` version string |
|
|
142
142
|
| [`sfmc/ssjs-no-unavailable-method`](docs/rules/ssjs/no-unavailable-method.md) | `warn` | Flag Array/String methods unavailable or broken in SFMC's ES3 engine |
|
|
143
|
+
| [`sfmc/ssjs-no-nonfunctional-method`](docs/rules/ssjs/no-nonfunctional-method.md) | `warn` | Flag Core Library methods that never take effect at runtime |
|
|
143
144
|
| [`sfmc/ssjs-prefer-parsejson-safe-arg`](docs/rules/ssjs/prefer-parsejson-safe-arg.md) | `warn` | Require string coercion on `ParseJSON` argument |
|
|
144
145
|
| [`sfmc/ssjs-no-switch-default`](docs/rules/ssjs/no-switch-default.md) | `warn` | Disallow `default` clause in `switch` statements |
|
|
145
146
|
| [`sfmc/ssjs-no-treatascontent-injection`](docs/rules/ssjs/no-treatascontent-injection.md) | `warn` | Flag dynamic string concatenation in `TreatAsContent` calls |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# `sfmc/ssjs-no-nonfunctional-method`
|
|
2
|
+
|
|
3
|
+
> Warn when calling a Core Library method that resolves at runtime but has no known working invocation.
|
|
4
|
+
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **Type** | `problem` |
|
|
8
|
+
| **Default severity** | `warn` in `recommended` and `strict` |
|
|
9
|
+
| **Fixable** | — |
|
|
10
|
+
|
|
11
|
+
## Why This Rule Exists
|
|
12
|
+
|
|
13
|
+
A few Core Library methods **exist** and **resolve** at runtime — the namespace or instance exposes them as callables — but exhaustive live testing has found **no working invocation**: every attempted call fails (returns the string `"Error"` or throws), and the documented success path could not be reproduced.
|
|
14
|
+
|
|
15
|
+
Because these methods DO exist, they remain in completions and in the generated `.d.ts` (unlike nonexistent members). This rule warns at the **call site** so you know the call will not work at runtime, using the `nonFunctionalAtRuntime` flag from the ssjs-data catalog.
|
|
16
|
+
|
|
17
|
+
## Coverage
|
|
18
|
+
|
|
19
|
+
Both **static** and **instance** call styles are covered, mirroring `sfmc/ssjs-core-method-arity`:
|
|
20
|
+
|
|
21
|
+
| Style | Example |
|
|
22
|
+
|---|---|
|
|
23
|
+
| Instance | `var fd = FilterDefinition.Init("x"); fd.Update({});` |
|
|
24
|
+
| Static single-name | `FilterDefinition.Update({});` |
|
|
25
|
+
| Static multi-part | `DataExtension.Rows.Add(rowObj);` |
|
|
26
|
+
|
|
27
|
+
Flagged methods today: `FilterDefinition.Update`, `FilterDefinition.Remove`.
|
|
28
|
+
|
|
29
|
+
## Settings
|
|
30
|
+
|
|
31
|
+
| Setting | Values | Default |
|
|
32
|
+
|---------|--------|---------|
|
|
33
|
+
| severity | `"error"` \| `"warn"` \| `"off"` | `"warn"` |
|
|
34
|
+
|
|
35
|
+
This rule has no configuration options.
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
**Not allowed:**
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
Platform.Load("core", "1.1.5");
|
|
43
|
+
|
|
44
|
+
var fd = FilterDefinition.Init("my-filter");
|
|
45
|
+
|
|
46
|
+
/* FilterDefinition.Update has no known working invocation at runtime */
|
|
47
|
+
fd.Update({ Name: "renamed" });
|
|
48
|
+
|
|
49
|
+
/* FilterDefinition.Remove has no known working invocation at runtime */
|
|
50
|
+
fd.Remove();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Allowed:**
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
Platform.Load("core", "1.1.5");
|
|
57
|
+
|
|
58
|
+
var fd = FilterDefinition.Init("my-filter");
|
|
59
|
+
|
|
60
|
+
/* Add, Init, Retrieve are working invocations */
|
|
61
|
+
fd.Retrieve();
|
|
62
|
+
fd.Add(filterDefinitionObj);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## When to Disable
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
// eslint.config.js
|
|
69
|
+
rules: { 'sfmc/ssjs-no-nonfunctional-method': 'off' }
|
|
70
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-sfmc",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "ESLint plugin for Salesforce Marketing Cloud Engagement+Next - AMPscript, Server-Side JavaScript (SSJS) and Handlebars",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"eslint-plugin-mso-email": "^2.0.0",
|
|
43
43
|
"handlebars-data": "^0.3.0",
|
|
44
44
|
"mso-conditional-parser": "^2.0.1",
|
|
45
|
-
"ssjs-data": "^0.
|
|
45
|
+
"ssjs-data": "^1.0.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"eslint": ">=9.0.0"
|
package/src/index.js
CHANGED
|
@@ -57,6 +57,7 @@ import ssjsNoSwitchDefault from './rules/ssjs/no-switch-default.js';
|
|
|
57
57
|
import ssjsNoTreatAsContentInjection from './rules/ssjs/no-treatascontent-injection.js';
|
|
58
58
|
import ssjsArgumentTypes from './rules/ssjs/ssjs-argument-types.js';
|
|
59
59
|
import ssjsCoreMethodArity from './rules/ssjs/ssjs-core-method-arity.js';
|
|
60
|
+
import ssjsNoNonfunctionalMethod from './rules/ssjs/ssjs-no-nonfunctional-method.js';
|
|
60
61
|
import ssjsNoClrHeaderAccess from './rules/ssjs/no-clr-header-access.js';
|
|
61
62
|
import ssjsRequireStringClrContent from './rules/ssjs/require-string-clr-content.js';
|
|
62
63
|
import ssjsHttpPropertyValue from './rules/ssjs/http-property-value.js';
|
|
@@ -140,6 +141,7 @@ const plugin = {
|
|
|
140
141
|
'ssjs-no-treatascontent-injection': ssjsNoTreatAsContentInjection,
|
|
141
142
|
'ssjs-arg-types': ssjsArgumentTypes,
|
|
142
143
|
'ssjs-core-method-arity': ssjsCoreMethodArity,
|
|
144
|
+
'ssjs-no-nonfunctional-method': ssjsNoNonfunctionalMethod,
|
|
143
145
|
'ssjs-no-clr-header-access': ssjsNoClrHeaderAccess,
|
|
144
146
|
'ssjs-require-string-clr-content': ssjsRequireStringClrContent,
|
|
145
147
|
'ssjs-http-property-value': ssjsHttpPropertyValue,
|
|
@@ -187,6 +189,7 @@ const ssjsMcnRules = {
|
|
|
187
189
|
'sfmc/ssjs-no-treatascontent-injection': 'off',
|
|
188
190
|
'sfmc/ssjs-arg-types': 'off',
|
|
189
191
|
'sfmc/ssjs-core-method-arity': 'off',
|
|
192
|
+
'sfmc/ssjs-no-nonfunctional-method': 'off',
|
|
190
193
|
'sfmc/ssjs-no-clr-header-access': 'off',
|
|
191
194
|
'sfmc/ssjs-require-string-clr-content': 'off',
|
|
192
195
|
'sfmc/ssjs-http-property-value': 'off',
|
|
@@ -259,6 +262,7 @@ const ssjsRecommendedRules = {
|
|
|
259
262
|
'sfmc/ssjs-no-treatascontent-injection': 'warn',
|
|
260
263
|
'sfmc/ssjs-arg-types': 'warn',
|
|
261
264
|
'sfmc/ssjs-core-method-arity': 'warn',
|
|
265
|
+
'sfmc/ssjs-no-nonfunctional-method': 'warn',
|
|
262
266
|
'sfmc/ssjs-no-clr-header-access': 'error',
|
|
263
267
|
'sfmc/ssjs-require-string-clr-content': 'error',
|
|
264
268
|
'sfmc/ssjs-http-property-value': 'error',
|
|
@@ -284,6 +288,7 @@ const ssjsStrictRules = {
|
|
|
284
288
|
'sfmc/ssjs-no-treatascontent-injection': 'error',
|
|
285
289
|
'sfmc/ssjs-arg-types': 'warn',
|
|
286
290
|
'sfmc/ssjs-core-method-arity': 'error',
|
|
291
|
+
'sfmc/ssjs-no-nonfunctional-method': 'warn',
|
|
287
292
|
'sfmc/ssjs-no-clr-header-access': 'error',
|
|
288
293
|
'sfmc/ssjs-require-string-clr-content': 'error',
|
|
289
294
|
'sfmc/ssjs-http-property-value': 'error',
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: ssjs-no-nonfunctional-method
|
|
3
|
+
*
|
|
4
|
+
* Warns when a Core Library method that RESOLVES at runtime but has NO known
|
|
5
|
+
* working invocation (ssjs-data `nonFunctionalAtRuntime`) is called. Unlike a
|
|
6
|
+
* nonexistent member, these methods DO exist (the namespace/instance exposes them
|
|
7
|
+
* as callables), so they remain in completions and the generated `.d.ts` — but
|
|
8
|
+
* every tested call fails, so the call site is flagged.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the receiver-resolution structure of ssjs-core-method-arity:
|
|
11
|
+
*
|
|
12
|
+
* var fd = FilterDefinition.Init("x");
|
|
13
|
+
* fd.Update({...}); // instance → warn
|
|
14
|
+
* DataExtension.Rows.Add(rowObj); // static multi-part
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { coreObjectNames, coreNonFunctionalMethodLookup } from 'ssjs-data';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract a short factual pointer from an entry's officialDocsNote (first
|
|
21
|
+
* sentence) for the warning message. Returns an empty string when absent.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} entry - ssjs-data method entry
|
|
24
|
+
* @returns {string} A short note, or empty string
|
|
25
|
+
*/
|
|
26
|
+
function shortNote(entry) {
|
|
27
|
+
if (!entry || typeof entry.officialDocsNote !== 'string') {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
const trimmed = entry.officialDocsNote.trim();
|
|
31
|
+
if (trimmed === '') {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
const sentenceEnd = trimmed.indexOf('. ');
|
|
35
|
+
return sentenceEnd === -1 ? trimmed : trimmed.slice(0, sentenceEnd + 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
meta: {
|
|
40
|
+
type: 'problem',
|
|
41
|
+
docs: {
|
|
42
|
+
description:
|
|
43
|
+
'Warn when calling a Core Library method that resolves at runtime but has no known working invocation',
|
|
44
|
+
},
|
|
45
|
+
hasSuggestions: false,
|
|
46
|
+
messages: {
|
|
47
|
+
nonFunctional:
|
|
48
|
+
"'{{call}}' exists in SFMC SSJS but has no known working invocation at runtime (every tested call fails). {{note}}",
|
|
49
|
+
},
|
|
50
|
+
schema: [],
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
create(context) {
|
|
54
|
+
const coreVariables = new Map(); // varName → className
|
|
55
|
+
|
|
56
|
+
function checkNonFunctional(entry, callName, reportNode) {
|
|
57
|
+
if (!entry) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
context.report({
|
|
61
|
+
node: reportNode,
|
|
62
|
+
messageId: 'nonFunctional',
|
|
63
|
+
data: { call: callName, note: shortNote(entry) },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
VariableDeclaration(node) {
|
|
69
|
+
for (const declaration of node.declarations) {
|
|
70
|
+
if (
|
|
71
|
+
!declaration.init ||
|
|
72
|
+
!declaration.id ||
|
|
73
|
+
declaration.id.type !== 'Identifier'
|
|
74
|
+
) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const coreType = getCoreInitType(declaration.init);
|
|
78
|
+
if (coreType) {
|
|
79
|
+
coreVariables.set(declaration.id.name, coreType);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
AssignmentExpression(node) {
|
|
85
|
+
if (node.left.type !== 'Identifier') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const coreType = getCoreInitType(node.right);
|
|
89
|
+
if (coreType) {
|
|
90
|
+
coreVariables.set(node.left.name, coreType);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
CallExpression(node) {
|
|
95
|
+
const callee = node.callee;
|
|
96
|
+
if (callee.type !== 'MemberExpression') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (callee.property.type !== 'Identifier') {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const methodName = callee.property.name;
|
|
103
|
+
|
|
104
|
+
if (callee.object.type === 'Identifier') {
|
|
105
|
+
const objectName = callee.object.name;
|
|
106
|
+
|
|
107
|
+
// Core Library instance method: fd.Update(...)
|
|
108
|
+
const coreType = coreVariables.get(objectName);
|
|
109
|
+
if (coreType) {
|
|
110
|
+
const classLookup = coreNonFunctionalMethodLookup.get(
|
|
111
|
+
coreType.toLowerCase(),
|
|
112
|
+
);
|
|
113
|
+
if (classLookup) {
|
|
114
|
+
const entry = classLookup.get(methodName.toLowerCase());
|
|
115
|
+
checkNonFunctional(entry, `${coreType}.${methodName}`, callee.property);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Static single-name: FilterDefinition.Update(...) directly
|
|
121
|
+
if (coreObjectNames.has(objectName)) {
|
|
122
|
+
const classLookup = coreNonFunctionalMethodLookup.get(
|
|
123
|
+
objectName.toLowerCase(),
|
|
124
|
+
);
|
|
125
|
+
if (classLookup) {
|
|
126
|
+
const entry = classLookup.get(methodName.toLowerCase());
|
|
127
|
+
checkNonFunctional(
|
|
128
|
+
entry,
|
|
129
|
+
`${objectName}.${methodName}`,
|
|
130
|
+
callee.property,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Static multi-part: DataExtension.Rows.Add(...)
|
|
138
|
+
const objectPath = getMemberPath(callee.object);
|
|
139
|
+
if (objectPath && coreObjectNames.has(objectPath)) {
|
|
140
|
+
const classLookup = coreNonFunctionalMethodLookup.get(objectPath.toLowerCase());
|
|
141
|
+
if (classLookup) {
|
|
142
|
+
const entry = classLookup.get(methodName.toLowerCase());
|
|
143
|
+
checkNonFunctional(entry, `${objectPath}.${methodName}`, callee.property);
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Instance sub-path: de.Rows.Add(...) where `de` is a tracked
|
|
149
|
+
// Init(...) instance. Substitute the instance's core type for the
|
|
150
|
+
// leftmost identifier and resolve the class key.
|
|
151
|
+
if (objectPath) {
|
|
152
|
+
const segments = objectPath.split('.');
|
|
153
|
+
const rootCoreType = coreVariables.get(segments[0]);
|
|
154
|
+
if (rootCoreType) {
|
|
155
|
+
const resolvedPath = [rootCoreType, ...segments.slice(1)].join('.');
|
|
156
|
+
const classLookup = coreNonFunctionalMethodLookup.get(
|
|
157
|
+
resolvedPath.toLowerCase(),
|
|
158
|
+
);
|
|
159
|
+
if (classLookup) {
|
|
160
|
+
const entry = classLookup.get(methodName.toLowerCase());
|
|
161
|
+
checkNonFunctional(
|
|
162
|
+
entry,
|
|
163
|
+
`${resolvedPath}.${methodName}`,
|
|
164
|
+
callee.property,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Resolve the Core class name for a `Class.Init(...)` / `A.B.Init(...)` call.
|
|
176
|
+
*
|
|
177
|
+
* @param {object} node - the init expression node
|
|
178
|
+
* @returns {string|null} The resolved core class name, or null
|
|
179
|
+
*/
|
|
180
|
+
function getCoreInitType(node) {
|
|
181
|
+
if (!node || node.type !== 'CallExpression') {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const callee = node.callee;
|
|
185
|
+
if (callee.type !== 'MemberExpression') {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
|
|
192
|
+
return callee.object.name;
|
|
193
|
+
}
|
|
194
|
+
if (
|
|
195
|
+
callee.object.type === 'MemberExpression' &&
|
|
196
|
+
callee.object.object.type === 'Identifier' &&
|
|
197
|
+
callee.object.property.type === 'Identifier'
|
|
198
|
+
) {
|
|
199
|
+
const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
|
|
200
|
+
if (coreObjectNames.has(fullName)) {
|
|
201
|
+
return fullName;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build a dotted member path string from a MemberExpression / Identifier.
|
|
209
|
+
*
|
|
210
|
+
* @param {object} node - the object node
|
|
211
|
+
* @returns {string|null} The dotted path, or null
|
|
212
|
+
*/
|
|
213
|
+
function getMemberPath(node) {
|
|
214
|
+
if (node.type === 'Identifier') {
|
|
215
|
+
return node.name;
|
|
216
|
+
}
|
|
217
|
+
if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
|
|
218
|
+
const object = getMemberPath(node.object);
|
|
219
|
+
return object ? `${object}.${node.property.name}` : null;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|