eslint-plugin-sfmc 0.1.1 → 0.2.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.
Files changed (41) hide show
  1. package/README.md +48 -47
  2. package/docs/rules/ssjs/arg-types.md +72 -0
  3. package/docs/rules/ssjs/core-method-arity.md +72 -0
  4. package/package.json +20 -7
  5. package/src/ampscript-parser.js +11 -4
  6. package/src/ampscript-processor.js +9 -3
  7. package/src/index.js +8 -0
  8. package/src/processor.js +12 -4
  9. package/src/rules/amp/function-arity.js +3 -1
  10. package/src/rules/amp/naming-convention.js +7 -3
  11. package/src/rules/amp/no-deprecated-function.js +6 -2
  12. package/src/rules/amp/no-email-excluded-function.js +3 -1
  13. package/src/rules/amp/no-inline-statement.js +3 -1
  14. package/src/rules/amp/no-nested-ampscript-delimiter.js +5 -5
  15. package/src/rules/amp/no-nested-script-tag.js +3 -1
  16. package/src/rules/amp/no-smart-quotes.js +1 -1
  17. package/src/rules/amp/prefer-attribute-value.js +10 -4
  18. package/src/rules/amp/require-rowcount-check.js +3 -1
  19. package/src/rules/amp/require-variable-declaration.js +7 -3
  20. package/src/rules/ssjs/cache-loop-length.js +10 -4
  21. package/src/rules/ssjs/no-hardcoded-credentials.js +3 -1
  22. package/src/rules/ssjs/no-treatascontent-injection.js +8 -4
  23. package/src/rules/ssjs/no-unavailable-method.js +30 -10
  24. package/src/rules/ssjs/no-unknown-core-method.js +27 -9
  25. package/src/rules/ssjs/no-unknown-http-method.js +9 -3
  26. package/src/rules/ssjs/no-unknown-platform-client-browser.js +9 -6
  27. package/src/rules/ssjs/no-unknown-platform-function.js +3 -1
  28. package/src/rules/ssjs/no-unknown-platform-request.js +3 -1
  29. package/src/rules/ssjs/no-unknown-platform-response.js +3 -1
  30. package/src/rules/ssjs/no-unknown-platform-variable.js +3 -1
  31. package/src/rules/ssjs/no-unknown-wsproxy-method.js +30 -13
  32. package/src/rules/ssjs/no-unsupported-syntax.js +10 -5
  33. package/src/rules/ssjs/platform-function-arity.js +6 -2
  34. package/src/rules/ssjs/prefer-parsejson-safe-arg.js +17 -9
  35. package/src/rules/ssjs/prefer-platform-load-version.js +1 -4
  36. package/src/rules/ssjs/require-hasownproperty.js +34 -27
  37. package/src/rules/ssjs/require-platform-load-order.js +6 -2
  38. package/src/rules/ssjs/require-platform-load.js +4 -3
  39. package/src/rules/ssjs/ssjs-arg-types.js +345 -0
  40. package/src/rules/ssjs/ssjs-core-method-arity.js +176 -0
  41. package/src/ssjs-processor.js +3 -1
@@ -31,7 +31,9 @@ export default {
31
31
 
32
32
  const stmts = body.type === 'BlockStatement' ? body.body : [body];
33
33
 
34
- if (stmts.length === 0) return;
34
+ if (stmts.length === 0) {
35
+ return;
36
+ }
35
37
 
36
38
  const hasGuard = stmts.some((stmt) => containsHasOwnProperty(stmt));
37
39
 
@@ -42,34 +44,32 @@ export default {
42
44
  context.report({
43
45
  node,
44
46
  messageId: 'missingGuard',
45
- suggest:
46
- keyName
47
- ? [
48
- {
49
- messageId: 'suggestAddGuard',
50
- data: { obj: objText, key: keyName },
51
- fix(fixer) {
52
- if (body.type === 'BlockStatement') {
53
- // Wrap the inner content of the existing block.
54
- const inner = context.sourceCode
55
- .getText(body)
56
- .slice(1, -1);
57
- return fixer.replaceText(
58
- body,
59
- `{ if (${objText}.hasOwnProperty(${keyName})) {${inner}} }`,
60
- );
61
- }
62
- // Single-statement body — create a new block with guard.
63
- const stmtText =
64
- context.sourceCode.getText(body);
47
+ suggest: keyName
48
+ ? [
49
+ {
50
+ messageId: 'suggestAddGuard',
51
+ data: { obj: objText, key: keyName },
52
+ fix(fixer) {
53
+ if (body.type === 'BlockStatement') {
54
+ // Wrap the inner content of the existing block.
55
+ const inner = context.sourceCode
56
+ .getText(body)
57
+ .slice(1, -1);
65
58
  return fixer.replaceText(
66
59
  body,
67
- `{ if (${objText}.hasOwnProperty(${keyName})) { ${stmtText} } }`,
60
+ `{ if (${objText}.hasOwnProperty(${keyName})) {${inner}} }`,
68
61
  );
69
- },
62
+ }
63
+ // Single-statement body — create a new block with guard.
64
+ const stmtText = context.sourceCode.getText(body);
65
+ return fixer.replaceText(
66
+ body,
67
+ `{ if (${objText}.hasOwnProperty(${keyName})) { ${stmtText} } }`,
68
+ );
70
69
  },
71
- ]
72
- : [],
70
+ },
71
+ ]
72
+ : [],
73
73
  });
74
74
  }
75
75
  },
@@ -78,7 +78,9 @@ export default {
78
78
  };
79
79
 
80
80
  function containsHasOwnProperty(node) {
81
- if (!node) return false;
81
+ if (!node) {
82
+ return false;
83
+ }
82
84
 
83
85
  if (node.type === 'IfStatement' && node.test && hasOwnPropertyTest(node.test)) {
84
86
  return true;
@@ -104,7 +106,9 @@ function containsHasOwnProperty(node) {
104
106
  }
105
107
 
106
108
  function hasOwnPropertyTest(node) {
107
- if (!node) return false;
109
+ if (!node) {
110
+ return false;
111
+ }
108
112
 
109
113
  if (
110
114
  node.type === 'CallExpression' &&
@@ -129,6 +133,9 @@ function hasOwnPropertyTest(node) {
129
133
  /**
130
134
  * Extracts the loop key variable name from a for-in left-hand side.
131
135
  * Handles both `for (var k in obj)` and `for (k in obj)`.
136
+ *
137
+ * @param left - The left-hand side node of a ForInStatement.
138
+ * @returns The key variable name, or null if it cannot be determined.
132
139
  */
133
140
  function getKeyName(left) {
134
141
  if (left.type === 'VariableDeclaration' && left.declarations.length > 0) {
@@ -44,7 +44,9 @@ export default {
44
44
  },
45
45
 
46
46
  'Program:exit'() {
47
- if (platformLoadLine === null) return;
47
+ if (platformLoadLine === null) {
48
+ return;
49
+ }
48
50
 
49
51
  for (const usage of coreUsages) {
50
52
  if (usage.line < platformLoadLine) {
@@ -87,7 +89,9 @@ function isPlatformLoadCall(node) {
87
89
 
88
90
  function getCoreObjectUsage(node) {
89
91
  const callee = node.callee;
90
- if (callee.type !== 'MemberExpression') return null;
92
+ if (callee.type !== 'MemberExpression') {
93
+ return null;
94
+ }
91
95
 
92
96
  if (
93
97
  callee.object.type === 'MemberExpression' &&
@@ -49,8 +49,7 @@ export default {
49
49
 
50
50
  'Program:exit'() {
51
51
  if (!hasPlatformLoad) {
52
- for (let i = 0; i < pendingReports.length; i++) {
53
- const { node, name } = pendingReports[i];
52
+ for (const [i, { node, name }] of pendingReports.entries()) {
54
53
  context.report({
55
54
  node,
56
55
  messageId: 'missingLoad',
@@ -97,7 +96,9 @@ function isPlatformLoadCall(node) {
97
96
 
98
97
  function getCoreObjectUsage(node) {
99
98
  const callee = node.callee;
100
- if (callee.type !== 'MemberExpression') return null;
99
+ if (callee.type !== 'MemberExpression') {
100
+ return null;
101
+ }
101
102
 
102
103
  if (
103
104
  callee.object.type === 'MemberExpression' &&
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Rule: ssjs-arg-types
3
+ *
4
+ * Checks that literal arguments match the expected parameter types for known
5
+ * SSJS functions and methods. Only literal values are checked — variable
6
+ * arguments are skipped because their type cannot be determined statically.
7
+ *
8
+ * Covers:
9
+ * - Platform.Function.*, Platform.Variable.*, Platform.Response.*,
10
+ * Platform.Request.*, Platform.Recipient.*
11
+ * - HTTP.*, HTTPHeader.*, Attribute.*
12
+ * - WSProxy instance methods
13
+ * - Core Library static calls (DataExtension.Init, BounceEvent.Retrieve, …)
14
+ * - Core Library instance methods (var de = DataExtension.Init(…); de.Add(…))
15
+ * - Global functions (Format, String, Error)
16
+ */
17
+
18
+ import {
19
+ platformFunctionLookup,
20
+ platformResponseLookup,
21
+ platformVariableLookup,
22
+ platformRequestLookup,
23
+ platformRecipientLookup,
24
+ httpMethodLookup,
25
+ httpHeaderMethodLookup,
26
+ wsproxyMethodLookup,
27
+ attributeMethodLookup,
28
+ ssjsGlobalsLookup,
29
+ coreMethodArityLookup,
30
+ coreObjectNames,
31
+ } from 'ssjs-data';
32
+
33
+ // Platform.SubNS → lookup Map
34
+ const PLATFORM_SUBNS_LOOKUPS = new Map([
35
+ ['function', platformFunctionLookup],
36
+ ['variable', platformVariableLookup],
37
+ ['response', platformResponseLookup],
38
+ ['request', platformRequestLookup],
39
+ ['recipient', platformRecipientLookup],
40
+ ]);
41
+
42
+ // Top-level non-Platform objects with static method lookups
43
+ const TOPLEVEL_LOOKUPS = new Map([
44
+ ['http', httpMethodLookup],
45
+ ['httpheader', httpHeaderMethodLookup],
46
+ ['attribute', attributeMethodLookup],
47
+ ]);
48
+
49
+ export default {
50
+ meta: {
51
+ type: 'suggestion',
52
+ docs: {
53
+ description:
54
+ 'Check that literal arguments match expected parameter types for SSJS functions',
55
+ },
56
+ messages: {
57
+ typeMismatch:
58
+ "Argument {{pos}} ('{{param}}') of '{{call}}' expects type '{{expected}}' but received '{{actual}}'.",
59
+ },
60
+ schema: [],
61
+ },
62
+
63
+ create(context) {
64
+ const coreVars = new Map(); // varName → className (Core Library instances)
65
+ const wsproxyVars = new Set(); // varNames assigned new WSProxy()
66
+
67
+ function checkArgs(entry, args, callName) {
68
+ if (!entry || !entry.params || entry.params.length === 0) {
69
+ return;
70
+ }
71
+ for (const [i, arg] of args.entries()) {
72
+ const param = entry.params[i];
73
+ if (!param) {
74
+ break;
75
+ } // beyond declared params — arity rule handles this
76
+ if (!param.type || param.type === 'any') {
77
+ continue;
78
+ }
79
+ const actual = inferArgType(arg);
80
+ if (actual === null) {
81
+ continue;
82
+ } // unknown type (variable/call) — skip
83
+ if (!isTypeCompatible(actual, param.type)) {
84
+ context.report({
85
+ node: arg,
86
+ messageId: 'typeMismatch',
87
+ data: {
88
+ pos: String(i + 1),
89
+ param: param.name,
90
+ call: callName,
91
+ expected: param.type,
92
+ actual,
93
+ },
94
+ });
95
+ }
96
+ }
97
+ }
98
+
99
+ return {
100
+ VariableDeclaration(node) {
101
+ for (const decl of node.declarations) {
102
+ if (!decl.init || !decl.id || decl.id.type !== 'Identifier') {
103
+ continue;
104
+ }
105
+ if (isWSProxyConstructor(decl.init)) {
106
+ wsproxyVars.add(decl.id.name);
107
+ continue;
108
+ }
109
+ const coreType = getCoreInitType(decl.init);
110
+ if (coreType) {
111
+ coreVars.set(decl.id.name, coreType);
112
+ }
113
+ }
114
+ },
115
+
116
+ AssignmentExpression(node) {
117
+ if (node.left.type !== 'Identifier') {
118
+ return;
119
+ }
120
+ if (isWSProxyConstructor(node.right)) {
121
+ wsproxyVars.add(node.left.name);
122
+ return;
123
+ }
124
+ const coreType = getCoreInitType(node.right);
125
+ if (coreType) {
126
+ coreVars.set(node.left.name, coreType);
127
+ }
128
+ },
129
+
130
+ CallExpression(node) {
131
+ const callee = node.callee;
132
+
133
+ // Global function: Format(...), String(...), Error(...)
134
+ if (callee.type === 'Identifier') {
135
+ const entry = ssjsGlobalsLookup.get(callee.name.toLowerCase());
136
+ if (entry) {
137
+ checkArgs(entry, node.arguments, callee.name);
138
+ }
139
+ return;
140
+ }
141
+
142
+ if (callee.type !== 'MemberExpression') {
143
+ return;
144
+ }
145
+ if (callee.property.type !== 'Identifier') {
146
+ return;
147
+ }
148
+ const methodName = callee.property.name;
149
+
150
+ // Platform.SubNS.Method(...): Platform.Function.X, Platform.Response.X, …
151
+ if (
152
+ callee.object.type === 'MemberExpression' &&
153
+ callee.object.object.type === 'Identifier' &&
154
+ callee.object.object.name === 'Platform' &&
155
+ callee.object.property.type === 'Identifier'
156
+ ) {
157
+ const subNs = callee.object.property.name.toLowerCase();
158
+ const lookup = PLATFORM_SUBNS_LOOKUPS.get(subNs);
159
+ if (lookup) {
160
+ const entry = lookup.get(methodName.toLowerCase());
161
+ if (entry) {
162
+ checkArgs(
163
+ entry,
164
+ node.arguments,
165
+ `Platform.${callee.object.property.name}.${methodName}`,
166
+ );
167
+ }
168
+ }
169
+ return;
170
+ }
171
+
172
+ if (callee.object.type === 'Identifier') {
173
+ const objName = callee.object.name;
174
+ const objLower = objName.toLowerCase();
175
+
176
+ // Static top-level: HTTP.Post(...), HTTPHeader.SetValue(...), Attribute.GetValue(...)
177
+ const topLookup = TOPLEVEL_LOOKUPS.get(objLower);
178
+ if (topLookup) {
179
+ const entry = topLookup.get(methodName.toLowerCase());
180
+ if (entry) {
181
+ checkArgs(entry, node.arguments, `${objName}.${methodName}`);
182
+ }
183
+ return;
184
+ }
185
+
186
+ // WSProxy instance method: proxy.Retrieve(...)
187
+ if (wsproxyVars.has(objName)) {
188
+ const entry = wsproxyMethodLookup.get(methodName.toLowerCase());
189
+ if (entry) {
190
+ checkArgs(entry, node.arguments, `WSProxy.${methodName}`);
191
+ }
192
+ return;
193
+ }
194
+
195
+ // Core Library instance method: de.Add(...)
196
+ const coreType = coreVars.get(objName);
197
+ if (coreType) {
198
+ const classLookup = coreMethodArityLookup.get(coreType.toLowerCase());
199
+ if (classLookup) {
200
+ const entry = classLookup.get(methodName.toLowerCase());
201
+ if (entry) {
202
+ checkArgs(entry, node.arguments, `${coreType}.${methodName}`);
203
+ }
204
+ }
205
+ return;
206
+ }
207
+
208
+ // Static Core Library single-name: DataExtension.Init(...), BounceEvent.Retrieve(...)
209
+ if (coreObjectNames.has(objName)) {
210
+ const classLookup = coreMethodArityLookup.get(objLower);
211
+ if (classLookup) {
212
+ const entry = classLookup.get(methodName.toLowerCase());
213
+ if (entry) {
214
+ checkArgs(entry, node.arguments, `${objName}.${methodName}`);
215
+ }
216
+ }
217
+ }
218
+ return;
219
+ }
220
+
221
+ // Multi-part static: DataExtension.Rows.Add(...), TriggeredSend.Tracking.Clicks.Retrieve(...)
222
+ const objectPath = getMemberPath(callee.object);
223
+ if (objectPath && coreObjectNames.has(objectPath)) {
224
+ const classLookup = coreMethodArityLookup.get(objectPath.toLowerCase());
225
+ if (classLookup) {
226
+ const entry = classLookup.get(methodName.toLowerCase());
227
+ if (entry) {
228
+ checkArgs(entry, node.arguments, `${objectPath}.${methodName}`);
229
+ }
230
+ }
231
+ }
232
+ },
233
+ };
234
+ },
235
+ };
236
+
237
+ function getCoreInitType(node) {
238
+ if (!node || node.type !== 'CallExpression') {
239
+ return null;
240
+ }
241
+ const callee = node.callee;
242
+ if (callee.type !== 'MemberExpression') {
243
+ return null;
244
+ }
245
+ if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
246
+ return null;
247
+ }
248
+ if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
249
+ return callee.object.name;
250
+ }
251
+ if (
252
+ callee.object.type === 'MemberExpression' &&
253
+ callee.object.object.type === 'Identifier' &&
254
+ callee.object.property.type === 'Identifier'
255
+ ) {
256
+ const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
257
+ if (coreObjectNames.has(fullName)) {
258
+ return fullName;
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+
264
+ function getMemberPath(node) {
265
+ if (node.type === 'Identifier') {
266
+ return node.name;
267
+ }
268
+ if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
269
+ const obj = getMemberPath(node.object);
270
+ return obj ? `${obj}.${node.property.name}` : null;
271
+ }
272
+ return null;
273
+ }
274
+
275
+ function inferArgType(node) {
276
+ if (!node || node.type === 'SpreadElement') {
277
+ return null;
278
+ }
279
+ if (node.type === 'Literal') {
280
+ if (typeof node.value === 'string') {
281
+ return 'string';
282
+ }
283
+ if (typeof node.value === 'number') {
284
+ return 'number';
285
+ }
286
+ if (typeof node.value === 'boolean') {
287
+ return 'boolean';
288
+ }
289
+ if (node.value === null) {
290
+ return 'null';
291
+ }
292
+ }
293
+ if (node.type === 'TemplateLiteral' && !node.tag) {
294
+ return 'string';
295
+ }
296
+ if (node.type === 'ArrayExpression') {
297
+ const elems = node.elements.filter(Boolean);
298
+ if (elems.length === 0) {
299
+ return 'array';
300
+ }
301
+ if (elems.every((e) => e.type === 'Literal' && typeof e.value === 'string')) {
302
+ return 'string[]';
303
+ }
304
+ if (elems.every((e) => e.type === 'Literal' && typeof e.value === 'number')) {
305
+ return 'number[]';
306
+ }
307
+ return 'array';
308
+ }
309
+ if (node.type === 'ObjectExpression') {
310
+ return 'object';
311
+ }
312
+ return null;
313
+ }
314
+
315
+ function isTypeCompatible(actual, expected) {
316
+ if (!expected || expected === 'any') {
317
+ return true;
318
+ }
319
+ const allowed = expected.split('|').map((t) => t.trim());
320
+ if (allowed.includes(actual)) {
321
+ return true;
322
+ }
323
+ // 'array' is compatible with typed array variants
324
+ if (allowed.includes('array') && (actual === 'string[]' || actual === 'number[]')) {
325
+ return true;
326
+ }
327
+ return false;
328
+ }
329
+
330
+ function isWSProxyConstructor(node) {
331
+ if (!node || node.type !== 'NewExpression') {
332
+ return false;
333
+ }
334
+ const c = node.callee;
335
+ return (
336
+ c.type === 'MemberExpression' &&
337
+ c.property.type === 'Identifier' &&
338
+ c.property.name === 'WSProxy' &&
339
+ c.object.type === 'MemberExpression' &&
340
+ c.object.property.type === 'Identifier' &&
341
+ c.object.property.name === 'Util' &&
342
+ c.object.object.type === 'Identifier' &&
343
+ c.object.object.name === 'Script'
344
+ );
345
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Rule: ssjs-core-method-arity
3
+ *
4
+ * Enforces correct argument counts for Core Library object method calls.
5
+ * Covers both instance methods and static calls:
6
+ *
7
+ * var de = DataExtension.Init("MyDE");
8
+ * de.Add(rowObj); // instance
9
+ * BounceEvent.Retrieve(filter); // static single-name
10
+ * DataExtension.Rows.Add(rowObj); // static multi-part
11
+ */
12
+
13
+ import { coreObjectNames, coreMethodArityLookup } from 'ssjs-data';
14
+
15
+ export default {
16
+ meta: {
17
+ type: 'problem',
18
+ docs: {
19
+ description: 'Enforce correct argument counts for Core Library object methods',
20
+ },
21
+ messages: {
22
+ tooFewArgs:
23
+ "'{{call}}' requires at least {{min}} argument(s) but was called with {{actual}}.",
24
+ tooManyArgs:
25
+ "'{{call}}' accepts at most {{max}} argument(s) but was called with {{actual}}.",
26
+ },
27
+ schema: [],
28
+ },
29
+
30
+ create(context) {
31
+ const coreVars = new Map(); // varName → className
32
+
33
+ function checkArity(entry, args, callName, reportNode) {
34
+ if (!entry) {
35
+ return;
36
+ }
37
+ const actual = args.length;
38
+ if (actual < entry.minArgs) {
39
+ context.report({
40
+ node: reportNode,
41
+ messageId: 'tooFewArgs',
42
+ data: { call: callName, min: String(entry.minArgs), actual: String(actual) },
43
+ });
44
+ } else if (actual > entry.maxArgs) {
45
+ context.report({
46
+ node: reportNode,
47
+ messageId: 'tooManyArgs',
48
+ data: { call: callName, max: String(entry.maxArgs), actual: String(actual) },
49
+ });
50
+ }
51
+ }
52
+
53
+ return {
54
+ VariableDeclaration(node) {
55
+ for (const decl of node.declarations) {
56
+ if (!decl.init || !decl.id || decl.id.type !== 'Identifier') {
57
+ continue;
58
+ }
59
+ const coreType = getCoreInitType(decl.init);
60
+ if (coreType) {
61
+ coreVars.set(decl.id.name, coreType);
62
+ }
63
+ }
64
+ },
65
+
66
+ AssignmentExpression(node) {
67
+ if (node.left.type !== 'Identifier') {
68
+ return;
69
+ }
70
+ const coreType = getCoreInitType(node.right);
71
+ if (coreType) {
72
+ coreVars.set(node.left.name, coreType);
73
+ }
74
+ },
75
+
76
+ CallExpression(node) {
77
+ const callee = node.callee;
78
+ if (callee.type !== 'MemberExpression') {
79
+ return;
80
+ }
81
+ if (callee.property.type !== 'Identifier') {
82
+ return;
83
+ }
84
+ const methodName = callee.property.name;
85
+
86
+ if (callee.object.type === 'Identifier') {
87
+ const objName = callee.object.name;
88
+
89
+ // Core Library instance method: de.Add(...)
90
+ const coreType = coreVars.get(objName);
91
+ if (coreType) {
92
+ const classLookup = coreMethodArityLookup.get(coreType.toLowerCase());
93
+ if (classLookup) {
94
+ const entry = classLookup.get(methodName.toLowerCase());
95
+ checkArity(
96
+ entry,
97
+ node.arguments,
98
+ `${coreType}.${methodName}`,
99
+ callee.property,
100
+ );
101
+ }
102
+ return;
103
+ }
104
+
105
+ // Static single-name: DataExtension.Init(...), BounceEvent.Retrieve(...)
106
+ if (coreObjectNames.has(objName)) {
107
+ const classLookup = coreMethodArityLookup.get(objName.toLowerCase());
108
+ if (classLookup) {
109
+ const entry = classLookup.get(methodName.toLowerCase());
110
+ checkArity(
111
+ entry,
112
+ node.arguments,
113
+ `${objName}.${methodName}`,
114
+ callee.property,
115
+ );
116
+ }
117
+ return;
118
+ }
119
+ }
120
+
121
+ // Static multi-part: DataExtension.Rows.Add(...), TriggeredSend.Tracking.Clicks.Retrieve(...)
122
+ const objectPath = getMemberPath(callee.object);
123
+ if (objectPath && coreObjectNames.has(objectPath)) {
124
+ const classLookup = coreMethodArityLookup.get(objectPath.toLowerCase());
125
+ if (classLookup) {
126
+ const entry = classLookup.get(methodName.toLowerCase());
127
+ checkArity(
128
+ entry,
129
+ node.arguments,
130
+ `${objectPath}.${methodName}`,
131
+ callee.property,
132
+ );
133
+ }
134
+ }
135
+ },
136
+ };
137
+ },
138
+ };
139
+
140
+ function getCoreInitType(node) {
141
+ if (!node || node.type !== 'CallExpression') {
142
+ return null;
143
+ }
144
+ const callee = node.callee;
145
+ if (callee.type !== 'MemberExpression') {
146
+ return null;
147
+ }
148
+ if (callee.property.type !== 'Identifier' || callee.property.name !== 'Init') {
149
+ return null;
150
+ }
151
+ if (callee.object.type === 'Identifier' && coreObjectNames.has(callee.object.name)) {
152
+ return callee.object.name;
153
+ }
154
+ if (
155
+ callee.object.type === 'MemberExpression' &&
156
+ callee.object.object.type === 'Identifier' &&
157
+ callee.object.property.type === 'Identifier'
158
+ ) {
159
+ const fullName = `${callee.object.object.name}.${callee.object.property.name}`;
160
+ if (coreObjectNames.has(fullName)) {
161
+ return fullName;
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+
167
+ function getMemberPath(node) {
168
+ if (node.type === 'Identifier') {
169
+ return node.name;
170
+ }
171
+ if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
172
+ const obj = getMemberPath(node.object);
173
+ return obj ? `${obj}.${node.property.name}` : null;
174
+ }
175
+ return null;
176
+ }
@@ -20,7 +20,9 @@ export function preprocess(text, filename) {
20
20
 
21
21
  SCRIPT_CLOSE_RE.lastIndex = openEnd;
22
22
  const closeMatch = SCRIPT_CLOSE_RE.exec(text);
23
- if (!closeMatch) break;
23
+ if (!closeMatch) {
24
+ break;
25
+ }
24
26
 
25
27
  const jsCode = text.slice(openEnd, closeMatch.index);
26
28