eslint-plugin-sfmc 4.5.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.5.0",
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.21.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
+ }