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.
Files changed (55) hide show
  1. package/README.md +43 -47
  2. package/docs/rules/ssjs/arg-types.md +72 -0
  3. package/docs/rules/ssjs/core-method-arity.md +72 -0
  4. package/docs/rules/ssjs/no-deprecated-function.md +56 -0
  5. package/docs/rules/ssjs/no-property-call.md +95 -0
  6. package/docs/rules/ssjs/no-unknown-function.md +57 -0
  7. package/package.json +18 -5
  8. package/src/ampscript-parser.js +11 -4
  9. package/src/ampscript-processor.js +9 -3
  10. package/src/index.js +23 -33
  11. package/src/processor.js +12 -4
  12. package/src/rules/amp/function-arity.js +3 -1
  13. package/src/rules/amp/naming-convention.js +7 -3
  14. package/src/rules/amp/no-deprecated-function.js +6 -2
  15. package/src/rules/amp/no-email-excluded-function.js +3 -1
  16. package/src/rules/amp/no-inline-statement.js +3 -1
  17. package/src/rules/amp/no-nested-ampscript-delimiter.js +5 -5
  18. package/src/rules/amp/no-nested-script-tag.js +3 -1
  19. package/src/rules/amp/no-smart-quotes.js +1 -1
  20. package/src/rules/amp/prefer-attribute-value.js +10 -4
  21. package/src/rules/amp/require-rowcount-check.js +3 -1
  22. package/src/rules/amp/require-variable-declaration.js +7 -3
  23. package/src/rules/ssjs/cache-loop-length.js +10 -4
  24. package/src/rules/ssjs/no-deprecated-function.js +181 -0
  25. package/src/rules/ssjs/no-hardcoded-credentials.js +3 -1
  26. package/src/rules/ssjs/no-property-call.js +136 -0
  27. package/src/rules/ssjs/no-treatascontent-injection.js +8 -4
  28. package/src/rules/ssjs/no-unavailable-method.js +30 -10
  29. package/src/rules/ssjs/no-unknown-function.js +236 -0
  30. package/src/rules/ssjs/no-unsupported-syntax.js +10 -5
  31. package/src/rules/ssjs/platform-function-arity.js +6 -2
  32. package/src/rules/ssjs/prefer-parsejson-safe-arg.js +17 -9
  33. package/src/rules/ssjs/prefer-platform-load-version.js +1 -4
  34. package/src/rules/ssjs/require-hasownproperty.js +34 -27
  35. package/src/rules/ssjs/require-platform-load-order.js +6 -2
  36. package/src/rules/ssjs/require-platform-load.js +32 -12
  37. package/src/rules/ssjs/ssjs-arg-types.js +345 -0
  38. package/src/rules/ssjs/ssjs-core-method-arity.js +176 -0
  39. package/src/ssjs-processor.js +3 -1
  40. package/docs/rules/ssjs/no-unknown-core-method.md +0 -46
  41. package/docs/rules/ssjs/no-unknown-http-method.md +0 -44
  42. package/docs/rules/ssjs/no-unknown-platform-client-browser.md +0 -44
  43. package/docs/rules/ssjs/no-unknown-platform-function.md +0 -44
  44. package/docs/rules/ssjs/no-unknown-platform-request.md +0 -43
  45. package/docs/rules/ssjs/no-unknown-platform-response.md +0 -43
  46. package/docs/rules/ssjs/no-unknown-platform-variable.md +0 -43
  47. package/docs/rules/ssjs/no-unknown-wsproxy-method.md +0 -46
  48. package/src/rules/ssjs/no-unknown-core-method.js +0 -103
  49. package/src/rules/ssjs/no-unknown-http-method.js +0 -41
  50. package/src/rules/ssjs/no-unknown-platform-client-browser.js +0 -50
  51. package/src/rules/ssjs/no-unknown-platform-function.js +0 -49
  52. package/src/rules/ssjs/no-unknown-platform-request.js +0 -50
  53. package/src/rules/ssjs/no-unknown-platform-response.js +0 -50
  54. package/src/rules/ssjs/no-unknown-platform-variable.js +0 -50
  55. package/src/rules/ssjs/no-unknown-wsproxy-method.js +0 -71
package/src/index.js CHANGED
@@ -9,7 +9,6 @@
9
9
  * - vscode-sfmc-language (IntelliSense, diagnostics)
10
10
  */
11
11
 
12
- import { SSJS_GLOBALS_MAP } from 'ssjs-data';
13
12
  import * as ampscriptParser from './ampscript-parser.js';
14
13
 
15
14
  // ── AMPscript rules ───────────────────────────────────────────────────────────
@@ -38,15 +37,10 @@ import ampNoNestedAmpscriptDelimiter from './rules/amp/no-nested-ampscript-delim
38
37
 
39
38
  import ssjsRequirePlatformLoad from './rules/ssjs/require-platform-load.js';
40
39
  import ssjsNoUnsupportedSyntax from './rules/ssjs/no-unsupported-syntax.js';
41
- import ssjsNoUnknownPlatformFunction from './rules/ssjs/no-unknown-platform-function.js';
42
- import ssjsNoUnknownCoreMethod from './rules/ssjs/no-unknown-core-method.js';
40
+ import ssjsNoUnknownFunction from './rules/ssjs/no-unknown-function.js';
41
+ import ssjsNoDeprecatedFunction from './rules/ssjs/no-deprecated-function.js';
42
+ import ssjsNoPropertyCall from './rules/ssjs/no-property-call.js';
43
43
  import ssjsPlatformFunctionArity from './rules/ssjs/platform-function-arity.js';
44
- import ssjsNoUnknownHttpMethod from './rules/ssjs/no-unknown-http-method.js';
45
- import ssjsNoUnknownWsproxyMethod from './rules/ssjs/no-unknown-wsproxy-method.js';
46
- import ssjsNoUnknownPlatformVariable from './rules/ssjs/no-unknown-platform-variable.js';
47
- import ssjsNoUnknownPlatformResponse from './rules/ssjs/no-unknown-platform-response.js';
48
- import ssjsNoUnknownPlatformRequest from './rules/ssjs/no-unknown-platform-request.js';
49
- import ssjsNoUnknownPlatformClientBrowser from './rules/ssjs/no-unknown-platform-client-browser.js';
50
44
  import ssjsCacheLoopLength from './rules/ssjs/cache-loop-length.js';
51
45
  import ssjsRequireHasownproperty from './rules/ssjs/require-hasownproperty.js';
52
46
  import ssjsRequirePlatformLoadOrder from './rules/ssjs/require-platform-load-order.js';
@@ -56,6 +50,8 @@ import ssjsNoUnavailableMethod from './rules/ssjs/no-unavailable-method.js';
56
50
  import ssjsPreferParsejsonSafeArg from './rules/ssjs/prefer-parsejson-safe-arg.js';
57
51
  import ssjsNoSwitchDefault from './rules/ssjs/no-switch-default.js';
58
52
  import ssjsNoTreatAsContentInjection from './rules/ssjs/no-treatascontent-injection.js';
53
+ import ssjsArgTypes from './rules/ssjs/ssjs-arg-types.js';
54
+ import ssjsCoreMethodArity from './rules/ssjs/ssjs-core-method-arity.js';
59
55
 
60
56
  // ── Processors ────────────────────────────────────────────────────────────────
61
57
 
@@ -63,6 +59,9 @@ import ampscriptProcessor from './ampscript-processor.js';
63
59
  import ssjsProcessor from './ssjs-processor.js';
64
60
  import combinedProcessor from './processor.js';
65
61
 
62
+ // ── Data imports ──────────────────────────────────────────────────────────────
63
+ import { SSJS_GLOBALS_MAP } from 'ssjs-data';
64
+
66
65
  // ── Plugin definition ─────────────────────────────────────────────────────────
67
66
 
68
67
  const plugin = {
@@ -96,15 +95,10 @@ const plugin = {
96
95
  // SSJS rules (ssjs- prefix)
97
96
  'ssjs-require-platform-load': ssjsRequirePlatformLoad,
98
97
  'ssjs-no-unsupported-syntax': ssjsNoUnsupportedSyntax,
99
- 'ssjs-no-unknown-platform-function': ssjsNoUnknownPlatformFunction,
100
- 'ssjs-no-unknown-core-method': ssjsNoUnknownCoreMethod,
98
+ 'ssjs-no-unknown-function': ssjsNoUnknownFunction,
99
+ 'ssjs-no-deprecated-function': ssjsNoDeprecatedFunction,
100
+ 'ssjs-no-property-call': ssjsNoPropertyCall,
101
101
  'ssjs-platform-function-arity': ssjsPlatformFunctionArity,
102
- 'ssjs-no-unknown-http-method': ssjsNoUnknownHttpMethod,
103
- 'ssjs-no-unknown-wsproxy-method': ssjsNoUnknownWsproxyMethod,
104
- 'ssjs-no-unknown-platform-variable': ssjsNoUnknownPlatformVariable,
105
- 'ssjs-no-unknown-platform-response': ssjsNoUnknownPlatformResponse,
106
- 'ssjs-no-unknown-platform-request': ssjsNoUnknownPlatformRequest,
107
- 'ssjs-no-unknown-platform-client-browser': ssjsNoUnknownPlatformClientBrowser,
108
102
  'ssjs-cache-loop-length': ssjsCacheLoopLength,
109
103
  'ssjs-require-hasownproperty': ssjsRequireHasownproperty,
110
104
  'ssjs-require-platform-load-order': ssjsRequirePlatformLoadOrder,
@@ -114,6 +108,8 @@ const plugin = {
114
108
  'ssjs-prefer-parsejson-safe-arg': ssjsPreferParsejsonSafeArg,
115
109
  'ssjs-no-switch-default': ssjsNoSwitchDefault,
116
110
  'ssjs-no-treatascontent-injection': ssjsNoTreatAsContentInjection,
111
+ 'ssjs-arg-types': ssjsArgTypes,
112
+ 'ssjs-core-method-arity': ssjsCoreMethodArity,
117
113
  },
118
114
 
119
115
  processors: {
@@ -173,15 +169,10 @@ const ampStrictRules = {
173
169
  const ssjsRecommendedRules = {
174
170
  'sfmc/ssjs-require-platform-load': 'error',
175
171
  'sfmc/ssjs-no-unsupported-syntax': 'error',
176
- 'sfmc/ssjs-no-unknown-platform-function': 'error',
177
- 'sfmc/ssjs-no-unknown-core-method': 'warn',
172
+ 'sfmc/ssjs-no-unknown-function': 'error',
173
+ 'sfmc/ssjs-no-deprecated-function': 'error',
174
+ 'sfmc/ssjs-no-property-call': 'error',
178
175
  'sfmc/ssjs-platform-function-arity': 'error',
179
- 'sfmc/ssjs-no-unknown-http-method': 'error',
180
- 'sfmc/ssjs-no-unknown-wsproxy-method': 'warn',
181
- 'sfmc/ssjs-no-unknown-platform-variable': 'error',
182
- 'sfmc/ssjs-no-unknown-platform-response': 'error',
183
- 'sfmc/ssjs-no-unknown-platform-request': 'error',
184
- 'sfmc/ssjs-no-unknown-platform-client-browser': 'error',
185
176
  'sfmc/ssjs-cache-loop-length': 'warn',
186
177
  'sfmc/ssjs-require-hasownproperty': 'warn',
187
178
  'sfmc/ssjs-require-platform-load-order': 'error',
@@ -191,21 +182,18 @@ const ssjsRecommendedRules = {
191
182
  'sfmc/ssjs-prefer-parsejson-safe-arg': 'warn',
192
183
  'sfmc/ssjs-no-switch-default': 'warn',
193
184
  'sfmc/ssjs-no-treatascontent-injection': 'warn',
185
+ 'sfmc/ssjs-arg-types': 'warn',
186
+ 'sfmc/ssjs-core-method-arity': 'warn',
194
187
  'no-cond-assign': 'error',
195
188
  };
196
189
 
197
190
  const ssjsStrictRules = {
198
191
  'sfmc/ssjs-require-platform-load': 'error',
199
192
  'sfmc/ssjs-no-unsupported-syntax': 'error',
200
- 'sfmc/ssjs-no-unknown-platform-function': 'error',
201
- 'sfmc/ssjs-no-unknown-core-method': 'error',
193
+ 'sfmc/ssjs-no-unknown-function': 'error',
194
+ 'sfmc/ssjs-no-deprecated-function': 'error',
195
+ 'sfmc/ssjs-no-property-call': 'error',
202
196
  'sfmc/ssjs-platform-function-arity': 'error',
203
- 'sfmc/ssjs-no-unknown-http-method': 'error',
204
- 'sfmc/ssjs-no-unknown-wsproxy-method': 'error',
205
- 'sfmc/ssjs-no-unknown-platform-variable': 'error',
206
- 'sfmc/ssjs-no-unknown-platform-response': 'error',
207
- 'sfmc/ssjs-no-unknown-platform-request': 'error',
208
- 'sfmc/ssjs-no-unknown-platform-client-browser': 'error',
209
197
  'sfmc/ssjs-cache-loop-length': 'error',
210
198
  'sfmc/ssjs-require-hasownproperty': 'error',
211
199
  'sfmc/ssjs-require-platform-load-order': 'error',
@@ -215,6 +203,8 @@ const ssjsStrictRules = {
215
203
  'sfmc/ssjs-prefer-parsejson-safe-arg': 'error',
216
204
  'sfmc/ssjs-no-switch-default': 'error',
217
205
  'sfmc/ssjs-no-treatascontent-injection': 'error',
206
+ 'sfmc/ssjs-arg-types': 'warn',
207
+ 'sfmc/ssjs-core-method-arity': 'error',
218
208
  'no-cond-assign': 'error',
219
209
  };
220
210
 
package/src/processor.js CHANGED
@@ -22,7 +22,9 @@ const SCRIPT_CLOSE_G = /<\/script\s*>/gi;
22
22
  function countNewlinesBefore(text, pos) {
23
23
  let count = 0;
24
24
  for (let index = 0; index < pos; index++) {
25
- if (text[index] === '\n') count++;
25
+ if (text[index] === '\n') {
26
+ count++;
27
+ }
26
28
  }
27
29
  return count;
28
30
  }
@@ -76,7 +78,9 @@ export function preprocess(text, filename) {
76
78
  index++;
77
79
  }
78
80
  }
79
- if (depth !== 0) continue;
81
+ if (depth !== 0) {
82
+ continue;
83
+ }
80
84
  const fullBlock = text.slice(blockStart, index);
81
85
  const padding = '\n'.repeat(countNewlinesBefore(text, blockStart));
82
86
  blocks.push({
@@ -99,7 +103,9 @@ export function preprocess(text, filename) {
99
103
  }
100
104
  index++;
101
105
  }
102
- if (!found) continue;
106
+ if (!found) {
107
+ continue;
108
+ }
103
109
  const innerCode = text.slice(exprStart + 3, index - 3);
104
110
  const wrappedBlock = `%%[${innerCode}]%%`;
105
111
  const padding = '\n'.repeat(countNewlinesBefore(text, exprStart));
@@ -120,7 +126,9 @@ export function preprocess(text, filename) {
120
126
  const openEnd = match.index + match[0].length;
121
127
  SCRIPT_CLOSE_G.lastIndex = openEnd;
122
128
  const closeMatch = SCRIPT_CLOSE_G.exec(text);
123
- if (!closeMatch) break;
129
+ if (!closeMatch) {
130
+ break;
131
+ }
124
132
 
125
133
  const jsCode = text.slice(openEnd, closeMatch.index);
126
134
  const linesBefore = text.slice(0, openEnd).split('\n');
@@ -20,7 +20,9 @@ export default {
20
20
  return {
21
21
  FunctionCall(node) {
22
22
  const entry = functionLookup.get(node.name.toLowerCase());
23
- if (!entry) return;
23
+ if (!entry) {
24
+ return;
25
+ }
24
26
 
25
27
  const actual = node.arguments.length;
26
28
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Enforces a naming convention for @variable names in AMPscript.
2
+ * Enforces a naming convention for `@variable` names in AMPscript.
3
3
  *
4
4
  * AMPscript variables are case-insensitive, but consistent casing improves
5
5
  * readability and reduces confusion during code reviews.
@@ -39,10 +39,14 @@ export default {
39
39
  return {
40
40
  Variable(node) {
41
41
  const name = node.value;
42
- if (!name.startsWith('@') || name.startsWith('@@')) return;
42
+ if (!name.startsWith('@') || name.startsWith('@@')) {
43
+ return;
44
+ }
43
45
 
44
46
  const key = name.toLowerCase();
45
- if (reported.has(key)) return;
47
+ if (reported.has(key)) {
48
+ return;
49
+ }
46
50
 
47
51
  if (!pattern.test(name)) {
48
52
  reported.add(key);
@@ -30,10 +30,14 @@ export default {
30
30
  return {
31
31
  FunctionCall(node) {
32
32
  const functionName = node.name || (node.callee && node.callee.name) || '';
33
- if (!functionName) return;
33
+ if (!functionName) {
34
+ return;
35
+ }
34
36
 
35
37
  const entry = deprecatedFunctionLookup.get(functionName.toLowerCase());
36
- if (!entry) return;
38
+ if (!entry) {
39
+ return;
40
+ }
37
41
 
38
42
  const report = {
39
43
  node,
@@ -57,7 +57,9 @@ export default {
57
57
  return {
58
58
  FunctionCall(node) {
59
59
  const functionName = node.name || (node.callee && node.callee.name) || '';
60
- if (!functionName) return;
60
+ if (!functionName) {
61
+ return;
62
+ }
61
63
 
62
64
  if (isEmailExcluded(functionName)) {
63
65
  context.report({
@@ -30,7 +30,9 @@ export default {
30
30
  return {
31
31
  InlineExpression(node) {
32
32
  const expr = node.expression;
33
- if (!expr) return;
33
+ if (!expr) {
34
+ return;
35
+ }
34
36
 
35
37
  if (STATEMENT_TYPES.has(expr.type)) {
36
38
  context.report({
@@ -13,15 +13,13 @@ export default {
13
13
  type: 'problem',
14
14
  fixable: 'code',
15
15
  docs: {
16
- description:
17
- 'Disallow %%[ or %%= delimiters inside an already-open AMPscript region.',
16
+ description: 'Disallow %%[ or %%= delimiters inside an already-open AMPscript region.',
18
17
  recommended: true,
19
18
  },
20
19
  messages: {
21
20
  nestedDelimiterInScript:
22
21
  'AMPscript delimiter {{delimiter}} is not needed inside a <script language="ampscript"> block.',
23
- nestedDelimiter:
24
- 'Nested {{delimiter}} inside an already-open AMPscript block.',
22
+ nestedDelimiter: 'Nested {{delimiter}} inside an already-open AMPscript block.',
25
23
  },
26
24
  schema: [],
27
25
  },
@@ -66,7 +64,9 @@ export default {
66
64
  const end = start + delimiter.length;
67
65
 
68
66
  // Skip the outermost %%[ opener (it is the block wrapper, not nested)
69
- if (!isScriptBlock && start === outerOpenStart) continue;
67
+ if (!isScriptBlock && start === outerOpenStart) {
68
+ continue;
69
+ }
70
70
 
71
71
  const closeToken = delimiter === '%%[' ? ']%%' : '=%%';
72
72
 
@@ -31,7 +31,9 @@ export default {
31
31
  // %%[...]%% blocks cannot contain a nested <script> opening tag in
32
32
  // a way the processor would expose, so skip non-script-tag blocks.
33
33
  SCRIPT_OPEN_RE.lastIndex = 0;
34
- if (!SCRIPT_OPEN_RE.test(text)) return;
34
+ if (!SCRIPT_OPEN_RE.test(text)) {
35
+ return;
36
+ }
35
37
 
36
38
  // Reset and find all script open tags in this virtual block.
37
39
  SCRIPT_OPEN_RE.lastIndex = 0;
@@ -51,7 +51,7 @@ export default {
51
51
  messageId: 'smartQuote',
52
52
  data: { kind },
53
53
  fix(fixer) {
54
- const fixed = node.value.replace(
54
+ const fixed = node.value.replaceAll(
55
55
  /[\u2018\u2019\u201C\u201D\u201A\u201E\u2039\u203A]/g,
56
56
  (c) => SMART_TO_ASCII[c],
57
57
  );
@@ -32,10 +32,16 @@ const AMPSCRIPT_KEYWORDS = new Set([
32
32
  ]);
33
33
 
34
34
  function isLikelyPersonalization(node) {
35
- if (node.type !== 'Identifier') return false;
35
+ if (node.type !== 'Identifier') {
36
+ return false;
37
+ }
36
38
  const lower = node.value.toLowerCase();
37
- if (functionNames.has(lower)) return false;
38
- if (AMPSCRIPT_KEYWORDS.has(lower)) return false;
39
+ if (functionNames.has(lower)) {
40
+ return false;
41
+ }
42
+ if (AMPSCRIPT_KEYWORDS.has(lower)) {
43
+ return false;
44
+ }
39
45
  return true;
40
46
  }
41
47
 
@@ -51,7 +57,7 @@ export default {
51
57
  messages: {
52
58
  preferAttributeValue:
53
59
  'Use `AttributeValue("{{name}}")` instead of bare `{{name}}` to safely handle missing attributes.',
54
- wrapWithAttributeValue: 'Wrap \'{{name}}\' in AttributeValue() for safe attribute access',
60
+ wrapWithAttributeValue: "Wrap '{{name}}' in AttributeValue() for safe attribute access",
55
61
  },
56
62
  schema: [],
57
63
  },
@@ -24,7 +24,9 @@ function isRowCountCall(node) {
24
24
  }
25
25
 
26
26
  function extractRowCountVariables(node, into) {
27
- if (!node) return;
27
+ if (!node) {
28
+ return;
29
+ }
28
30
 
29
31
  if (isRowCountCall(node) && node.arguments && node.arguments.length > 0) {
30
32
  const argument = node.arguments[0];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Requires that @variables are declared with `var` before being used in
2
+ * Requires that `@variables` are declared with `var` before being used in
3
3
  * `set` statements. System variables (@@) and personalization strings
4
4
  * (no @ prefix) are not affected.
5
5
  *
@@ -33,9 +33,13 @@ export default {
33
33
  },
34
34
 
35
35
  SetStatement(node) {
36
- if (!node.target) return;
36
+ if (!node.target) {
37
+ return;
38
+ }
37
39
  const name = node.target.value;
38
- if (name.startsWith('@@')) return;
40
+ if (name.startsWith('@@')) {
41
+ return;
42
+ }
39
43
  if (!declared.has(name.toLowerCase())) {
40
44
  context.report({
41
45
  node: node.target,
@@ -32,7 +32,9 @@ export default {
32
32
  return {
33
33
  ForStatement(node) {
34
34
  const test = node.test;
35
- if (!test || test.type !== 'BinaryExpression') return;
35
+ if (!test || test.type !== 'BinaryExpression') {
36
+ return;
37
+ }
36
38
 
37
39
  let lengthExpr = null;
38
40
  if (containsMemberLength(test.right)) {
@@ -41,7 +43,9 @@ export default {
41
43
  lengthExpr = test.left;
42
44
  }
43
45
 
44
- if (!lengthExpr) return;
46
+ if (!lengthExpr) {
47
+ return;
48
+ }
45
49
 
46
50
  context.report({
47
51
  node: test,
@@ -54,7 +58,9 @@ export default {
54
58
  };
55
59
 
56
60
  function containsMemberLength(node) {
57
- if (!node) return false;
61
+ if (!node) {
62
+ return false;
63
+ }
58
64
  if (
59
65
  node.type === 'MemberExpression' &&
60
66
  node.property.type === 'Identifier' &&
@@ -81,7 +87,7 @@ function buildCacheSuggestion(forNode, lengthExpr, context) {
81
87
  messageId: 'suggestCacheLength',
82
88
  data: { obj: objText },
83
89
  fix(fixer) {
84
- const lastDecl = init.declarations[init.declarations.length - 1];
90
+ const lastDecl = init.declarations.at(-1);
85
91
  return [
86
92
  fixer.insertTextAfter(lastDecl, `, ${lenVar} = ${objText}.length`),
87
93
  fixer.replaceText(lengthExpr, lenVar),
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Rule: ssjs-no-deprecated-function
3
+ *
4
+ * Flags use of deprecated SFMC SSJS APIs. Currently covers:
5
+ *
6
+ * - ContentArea(...) — bare alias (use Platform.Function.ContentAreaByName instead)
7
+ * - ContentAreaByName(...) — bare alias (use Platform.Function.ContentAreaByName instead)
8
+ * - Platform.Function.ContentArea(...)
9
+ * - Platform.Function.ContentAreaByName(...)
10
+ * - ContentAreaObj.Init(...) — static method (class is deprecated)
11
+ * - ContentAreaObj.Add(...) — static method
12
+ * - ContentAreaObj.Retrieve(...) — static method
13
+ * - <contentAreaObjVar>.Update(...) — instance method on a tracked ContentAreaObj variable
14
+ * - <contentAreaObjVar>.Remove() — instance method on a tracked ContentAreaObj variable
15
+ */
16
+
17
+ import { platformFunctionLookup, SSJS_GLOBALS, CONTENT_AREA_OBJ_METHODS } from 'ssjs-data';
18
+
19
+ // Lookup Map: lowercase name → entry, for SSJS_GLOBALS entries that are deprecated.
20
+ // Used to flag bare calls like ContentArea(...) and ContentAreaByName(...).
21
+ const DEPRECATED_GLOBALS = new Map(
22
+ SSJS_GLOBALS.filter((g) => g.deprecated).map((g) => [g.name.toLowerCase(), g]),
23
+ );
24
+
25
+ // Build sets of deprecated static and instance methods from ssjs-data.
26
+ // Exclude 'Init' — that call is already implicitly covered when we track the
27
+ // returned instance and flag its instance methods. Reporting it here as well
28
+ // would produce a duplicate error on the same statement.
29
+ const CONTENT_AREA_STATIC_DEPRECATED = new Set(
30
+ CONTENT_AREA_OBJ_METHODS.filter(
31
+ (m) => m.deprecated && m.isStatic && m.name.toLowerCase() !== 'init',
32
+ ).map((m) => m.name.toLowerCase()),
33
+ );
34
+
35
+ const CONTENT_AREA_INSTANCE_DEPRECATED = new Set(
36
+ CONTENT_AREA_OBJ_METHODS.filter((m) => m.deprecated && !m.isStatic).map((m) =>
37
+ m.name.toLowerCase(),
38
+ ),
39
+ );
40
+
41
+ export default {
42
+ meta: {
43
+ type: 'suggestion',
44
+ docs: {
45
+ description: 'Disallow use of deprecated SFMC SSJS APIs',
46
+ },
47
+ messages: {
48
+ deprecatedGlobal: "'{{name}}' is deprecated. {{replacement}}",
49
+ deprecatedPlatformFunction:
50
+ "'Platform.Function.{{name}}' is deprecated. Use a supported alternative.",
51
+ deprecatedCoreStatic:
52
+ "'ContentAreaObj.{{name}}' is deprecated. Content Areas are no longer supported.",
53
+ deprecatedCoreInstance:
54
+ "'{{method}}' called on a ContentAreaObj variable is deprecated. Content Areas are no longer supported.",
55
+ },
56
+ schema: [],
57
+ },
58
+
59
+ create(context) {
60
+ // Track variable names assigned via ContentAreaObj.Init()
61
+ const contentAreaVariables = new Set();
62
+
63
+ return {
64
+ VariableDeclaration(node) {
65
+ for (const decl of node.declarations) {
66
+ if (
67
+ decl.init &&
68
+ decl.id &&
69
+ decl.id.type === 'Identifier' &&
70
+ isContentAreaObjInit(decl.init)
71
+ ) {
72
+ contentAreaVariables.add(decl.id.name);
73
+ }
74
+ }
75
+ },
76
+
77
+ AssignmentExpression(node) {
78
+ if (node.left.type === 'Identifier' && isContentAreaObjInit(node.right)) {
79
+ contentAreaVariables.add(node.left.name);
80
+ }
81
+ },
82
+
83
+ CallExpression(node) {
84
+ const callee = node.callee;
85
+
86
+ // ── Bare globals: ContentArea(…) and ContentAreaByName(…) ──────
87
+ if (callee.type === 'Identifier') {
88
+ const entry = DEPRECATED_GLOBALS.get(callee.name.toLowerCase());
89
+ if (entry && entry.deprecated) {
90
+ const replacement = entry.aliasOf
91
+ ? `Use '${entry.aliasOf}' instead.`
92
+ : 'Use a supported alternative.';
93
+ context.report({
94
+ node: callee,
95
+ messageId: 'deprecatedGlobal',
96
+ data: { name: callee.name, replacement },
97
+ });
98
+ }
99
+ return;
100
+ }
101
+
102
+ if (callee.type !== 'MemberExpression') {
103
+ return;
104
+ }
105
+
106
+ const property = callee.property;
107
+ if (property.type !== 'Identifier') {
108
+ return;
109
+ }
110
+
111
+ const methodName = property.name;
112
+
113
+ // ── Platform.Function.ContentArea(…) / ContentAreaByName(…) ───
114
+ if (
115
+ callee.object.type === 'MemberExpression' &&
116
+ callee.object.object.type === 'Identifier' &&
117
+ callee.object.object.name === 'Platform' &&
118
+ callee.object.property.type === 'Identifier' &&
119
+ callee.object.property.name === 'Function'
120
+ ) {
121
+ const entry = platformFunctionLookup.get(methodName.toLowerCase());
122
+ if (entry && entry.deprecated) {
123
+ context.report({
124
+ node: property,
125
+ messageId: 'deprecatedPlatformFunction',
126
+ data: { name: methodName },
127
+ });
128
+ }
129
+ return;
130
+ }
131
+
132
+ // ── ContentAreaObj.Init/Add/Retrieve(…) — static deprecated ───
133
+ if (
134
+ callee.object.type === 'Identifier' &&
135
+ callee.object.name === 'ContentAreaObj' &&
136
+ CONTENT_AREA_STATIC_DEPRECATED.has(methodName.toLowerCase())
137
+ ) {
138
+ context.report({
139
+ node: property,
140
+ messageId: 'deprecatedCoreStatic',
141
+ data: { name: methodName },
142
+ });
143
+ return;
144
+ }
145
+
146
+ // ── <contentAreaVar>.Update/Remove() — instance deprecated ─────
147
+ if (
148
+ callee.object.type === 'Identifier' &&
149
+ contentAreaVariables.has(callee.object.name) &&
150
+ CONTENT_AREA_INSTANCE_DEPRECATED.has(methodName.toLowerCase())
151
+ ) {
152
+ context.report({
153
+ node: property,
154
+ messageId: 'deprecatedCoreInstance',
155
+ data: { method: methodName },
156
+ });
157
+ }
158
+ },
159
+ };
160
+ },
161
+ };
162
+
163
+ /**
164
+ * Returns true if `node` is a `ContentAreaObj.Init(…)` call.
165
+ *
166
+ * @param {import('eslint').Rule.Node} node
167
+ * @returns {boolean}
168
+ */
169
+ function isContentAreaObjInit(node) {
170
+ if (!node || node.type !== 'CallExpression') {
171
+ return false;
172
+ }
173
+ const callee = node.callee;
174
+ return (
175
+ callee.type === 'MemberExpression' &&
176
+ callee.object.type === 'Identifier' &&
177
+ callee.object.name === 'ContentAreaObj' &&
178
+ callee.property.type === 'Identifier' &&
179
+ callee.property.name === 'Init'
180
+ );
181
+ }
@@ -43,7 +43,9 @@ export default {
43
43
  functionName = callee.property.name;
44
44
  }
45
45
 
46
- if (!functionName || !ENCRYPT_FUNCTIONS.has(functionName.toLowerCase())) return;
46
+ if (!functionName || !ENCRYPT_FUNCTIONS.has(functionName.toLowerCase())) {
47
+ return;
48
+ }
47
49
 
48
50
  for (const index of KEY_ARG_INDICES) {
49
51
  const argument = node.arguments[index];