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
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
|
|
42
|
-
import
|
|
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-
|
|
100
|
-
'ssjs-no-
|
|
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-
|
|
177
|
-
'sfmc/ssjs-no-
|
|
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-
|
|
201
|
-
'sfmc/ssjs-no-
|
|
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')
|
|
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)
|
|
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)
|
|
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)
|
|
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');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforces a naming convention for
|
|
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('@@'))
|
|
42
|
+
if (!name.startsWith('@') || name.startsWith('@@')) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
43
45
|
|
|
44
46
|
const key = name.toLowerCase();
|
|
45
|
-
if (reported.has(key))
|
|
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)
|
|
33
|
+
if (!functionName) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
34
36
|
|
|
35
37
|
const entry = deprecatedFunctionLookup.get(functionName.toLowerCase());
|
|
36
|
-
if (!entry)
|
|
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)
|
|
60
|
+
if (!functionName) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
61
63
|
|
|
62
64
|
if (isEmailExcluded(functionName)) {
|
|
63
65
|
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)
|
|
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))
|
|
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;
|
|
@@ -32,10 +32,16 @@ const AMPSCRIPT_KEYWORDS = new Set([
|
|
|
32
32
|
]);
|
|
33
33
|
|
|
34
34
|
function isLikelyPersonalization(node) {
|
|
35
|
-
if (node.type !== 'Identifier')
|
|
35
|
+
if (node.type !== 'Identifier') {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
36
38
|
const lower = node.value.toLowerCase();
|
|
37
|
-
if (functionNames.has(lower))
|
|
38
|
-
|
|
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:
|
|
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)
|
|
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
|
|
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)
|
|
36
|
+
if (!node.target) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
37
39
|
const name = node.target.value;
|
|
38
|
-
if (name.startsWith('@@'))
|
|
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')
|
|
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)
|
|
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)
|
|
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
|
|
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()))
|
|
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];
|