eslint-plugin-sfmc 0.1.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/LICENSE +21 -0
- package/README.md +100 -0
- package/docs/rules/amp/function-arity.md +53 -0
- package/docs/rules/amp/naming-convention.md +76 -0
- package/docs/rules/amp/no-deprecated-function.md +78 -0
- package/docs/rules/amp/no-email-excluded-function.md +69 -0
- package/docs/rules/amp/no-empty-block.md +59 -0
- package/docs/rules/amp/no-empty-then.md +62 -0
- package/docs/rules/amp/no-html-comment.md +67 -0
- package/docs/rules/amp/no-inline-statement.md +51 -0
- package/docs/rules/amp/no-js-line-comment.md +54 -0
- package/docs/rules/amp/no-loop-counter-assign.md +52 -0
- package/docs/rules/amp/no-nested-ampscript-delimiter.md +69 -0
- package/docs/rules/amp/no-nested-script-tag.md +58 -0
- package/docs/rules/amp/no-smart-quotes.md +60 -0
- package/docs/rules/amp/no-unknown-function.md +50 -0
- package/docs/rules/amp/no-var-redeclaration.md +52 -0
- package/docs/rules/amp/prefer-attribute-value.md +64 -0
- package/docs/rules/amp/require-rowcount-check.md +62 -0
- package/docs/rules/amp/require-variable-declaration.md +51 -0
- package/docs/rules/amp/set-requires-target.md +47 -0
- package/docs/rules/ssjs/cache-loop-length.md +59 -0
- package/docs/rules/ssjs/no-hardcoded-credentials.md +52 -0
- package/docs/rules/ssjs/no-switch-default.md +37 -0
- package/docs/rules/ssjs/no-treatascontent-injection.md +38 -0
- package/docs/rules/ssjs/no-unavailable-method.md +134 -0
- package/docs/rules/ssjs/no-unknown-core-method.md +46 -0
- package/docs/rules/ssjs/no-unknown-http-method.md +44 -0
- package/docs/rules/ssjs/no-unknown-platform-client-browser.md +44 -0
- package/docs/rules/ssjs/no-unknown-platform-function.md +44 -0
- package/docs/rules/ssjs/no-unknown-platform-request.md +43 -0
- package/docs/rules/ssjs/no-unknown-platform-response.md +43 -0
- package/docs/rules/ssjs/no-unknown-platform-variable.md +43 -0
- package/docs/rules/ssjs/no-unknown-wsproxy-method.md +46 -0
- package/docs/rules/ssjs/no-unsupported-syntax.md +83 -0
- package/docs/rules/ssjs/platform-function-arity.md +45 -0
- package/docs/rules/ssjs/prefer-parsejson-safe-arg.md +42 -0
- package/docs/rules/ssjs/prefer-platform-load-version.md +68 -0
- package/docs/rules/ssjs/require-hasownproperty.md +57 -0
- package/docs/rules/ssjs/require-platform-load-order.md +46 -0
- package/docs/rules/ssjs/require-platform-load.md +57 -0
- package/package.json +52 -0
- package/src/ampscript-parser.js +159 -0
- package/src/ampscript-processor.js +132 -0
- package/src/index.js +355 -0
- package/src/processor.js +147 -0
- package/src/rules/amp/function-arity.js +51 -0
- package/src/rules/amp/naming-convention.js +58 -0
- package/src/rules/amp/no-deprecated-function.js +75 -0
- package/src/rules/amp/no-email-excluded-function.js +72 -0
- package/src/rules/amp/no-empty-block.js +37 -0
- package/src/rules/amp/no-empty-then.js +34 -0
- package/src/rules/amp/no-html-comment.js +57 -0
- package/src/rules/amp/no-inline-statement.js +45 -0
- package/src/rules/amp/no-js-line-comment.js +54 -0
- package/src/rules/amp/no-loop-counter-assign.js +71 -0
- package/src/rules/amp/no-nested-ampscript-delimiter.js +102 -0
- package/src/rules/amp/no-nested-script-tag.js +65 -0
- package/src/rules/amp/no-smart-quotes.js +78 -0
- package/src/rules/amp/no-unknown-function.js +31 -0
- package/src/rules/amp/no-var-redeclaration.js +36 -0
- package/src/rules/amp/prefer-attribute-value.js +94 -0
- package/src/rules/amp/require-rowcount-check.js +116 -0
- package/src/rules/amp/require-variable-declaration.js +49 -0
- package/src/rules/amp/set-requires-target.js +27 -0
- package/src/rules/ssjs/cache-loop-length.js +92 -0
- package/src/rules/ssjs/no-hardcoded-credentials.js +66 -0
- package/src/rules/ssjs/no-switch-default.js +32 -0
- package/src/rules/ssjs/no-treatascontent-injection.js +76 -0
- package/src/rules/ssjs/no-unavailable-method.js +190 -0
- package/src/rules/ssjs/no-unknown-core-method.js +103 -0
- package/src/rules/ssjs/no-unknown-http-method.js +41 -0
- package/src/rules/ssjs/no-unknown-platform-client-browser.js +50 -0
- package/src/rules/ssjs/no-unknown-platform-function.js +49 -0
- package/src/rules/ssjs/no-unknown-platform-request.js +50 -0
- package/src/rules/ssjs/no-unknown-platform-response.js +50 -0
- package/src/rules/ssjs/no-unknown-platform-variable.js +50 -0
- package/src/rules/ssjs/no-unknown-wsproxy-method.js +71 -0
- package/src/rules/ssjs/no-unsupported-syntax.js +122 -0
- package/src/rules/ssjs/platform-function-arity.js +70 -0
- package/src/rules/ssjs/prefer-parsejson-safe-arg.js +94 -0
- package/src/rules/ssjs/prefer-platform-load-version.js +96 -0
- package/src/rules/ssjs/require-hasownproperty.js +142 -0
- package/src/rules/ssjs/require-platform-load-order.js +112 -0
- package/src/rules/ssjs/require-platform-load.js +124 -0
- package/src/ssjs-processor.js +48 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: no-unknown-wsproxy-method
|
|
3
|
+
*
|
|
4
|
+
* Flags calls to WSProxy instances where the method name does not
|
|
5
|
+
* exist in the known SFMC WSProxy method catalog.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { wsproxyMethodNames } from 'ssjs-data';
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Disallow calls to unknown WSProxy methods',
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
unknownMethod: "'{{name}}' is not a recognized WSProxy method.",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
create(context) {
|
|
23
|
+
const wsproxyVariables = new Set();
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
VariableDeclaration(node) {
|
|
27
|
+
for (const decl of node.declarations) {
|
|
28
|
+
if (
|
|
29
|
+
decl.id &&
|
|
30
|
+
decl.id.type === 'Identifier' &&
|
|
31
|
+
decl.init &&
|
|
32
|
+
decl.init.type === 'NewExpression' &&
|
|
33
|
+
decl.init.callee.type === 'Identifier' &&
|
|
34
|
+
decl.init.callee.name === 'WSProxy'
|
|
35
|
+
) {
|
|
36
|
+
wsproxyVariables.add(decl.id.name);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
AssignmentExpression(node) {
|
|
42
|
+
if (
|
|
43
|
+
node.left.type === 'Identifier' &&
|
|
44
|
+
node.right.type === 'NewExpression' &&
|
|
45
|
+
node.right.callee.type === 'Identifier' &&
|
|
46
|
+
node.right.callee.name === 'WSProxy'
|
|
47
|
+
) {
|
|
48
|
+
wsproxyVariables.add(node.left.name);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
CallExpression(node) {
|
|
53
|
+
const callee = node.callee;
|
|
54
|
+
if (callee.type !== 'MemberExpression') return;
|
|
55
|
+
if (callee.object.type !== 'Identifier') return;
|
|
56
|
+
if (callee.property.type !== 'Identifier') return;
|
|
57
|
+
|
|
58
|
+
if (!wsproxyVariables.has(callee.object.name)) return;
|
|
59
|
+
|
|
60
|
+
const methodName = callee.property.name;
|
|
61
|
+
if (!wsproxyMethodNames.has(methodName.toLowerCase())) {
|
|
62
|
+
context.report({
|
|
63
|
+
node: callee.property,
|
|
64
|
+
messageId: 'unknownMethod',
|
|
65
|
+
data: { name: methodName },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: no-unsupported-syntax
|
|
3
|
+
*
|
|
4
|
+
* Flags ES6+ syntax features that are not supported by SFMC's legacy
|
|
5
|
+
* ECMAScript engine. These features cause runtime errors in SFMC.
|
|
6
|
+
*
|
|
7
|
+
* Auto-fix: let/const declarations are rewritten to var (safe, 1:1 swap per
|
|
8
|
+
* Mateusz Dąbrowski's SSJS style guide).
|
|
9
|
+
*
|
|
10
|
+
* Suggestions:
|
|
11
|
+
* - nullish coalescing (??) → replace with ||
|
|
12
|
+
* - direct object literal return → extract to variable
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { unsupportedByNodeType } from 'ssjs-data';
|
|
16
|
+
|
|
17
|
+
// Features that can be safely auto-fixed without changing semantics.
|
|
18
|
+
const AUTO_FIX = {
|
|
19
|
+
LetDeclaration: (node) => (fixer) =>
|
|
20
|
+
// "let" is 3 chars; the keyword starts at node.range[0].
|
|
21
|
+
fixer.replaceTextRange([node.range[0], node.range[0] + 3], 'var'),
|
|
22
|
+
ConstDeclaration: (node) => (fixer) =>
|
|
23
|
+
// "const" is 5 chars.
|
|
24
|
+
fixer.replaceTextRange([node.range[0], node.range[0] + 5], 'var'),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
meta: {
|
|
29
|
+
type: 'problem',
|
|
30
|
+
fixable: 'code',
|
|
31
|
+
hasSuggestions: true,
|
|
32
|
+
docs: {
|
|
33
|
+
description: 'Disallow ES6+ syntax features not supported by the SFMC SSJS engine',
|
|
34
|
+
},
|
|
35
|
+
messages: {
|
|
36
|
+
unsupported: '{{label}} are not supported in SFMC SSJS. {{suggestion}}',
|
|
37
|
+
suggestLogicalOr:
|
|
38
|
+
"Replace ?? with || (note: semantics differ for falsy values like 0, '', and false)",
|
|
39
|
+
suggestVarReturn:
|
|
40
|
+
'Assign the object to a variable, then return the variable',
|
|
41
|
+
},
|
|
42
|
+
schema: [
|
|
43
|
+
{
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
allow: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
items: { type: 'string' },
|
|
49
|
+
uniqueItems: true,
|
|
50
|
+
description:
|
|
51
|
+
"Feature names to allow (e.g. ['LetDeclaration', 'ConstDeclaration']).",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
create(context) {
|
|
60
|
+
const options = context.options[0] || {};
|
|
61
|
+
const allowed = new Set(options.allow || []);
|
|
62
|
+
|
|
63
|
+
const listeners = {};
|
|
64
|
+
|
|
65
|
+
for (const [nodeType, entries] of unsupportedByNodeType) {
|
|
66
|
+
listeners[nodeType] = function (node) {
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (allowed.has(entry.feature)) continue;
|
|
69
|
+
if (entry.test && !entry.test(node)) continue;
|
|
70
|
+
|
|
71
|
+
const report = {
|
|
72
|
+
node,
|
|
73
|
+
messageId: 'unsupported',
|
|
74
|
+
data: {
|
|
75
|
+
label: entry.label,
|
|
76
|
+
suggestion: entry.suggestion,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (entry.feature in AUTO_FIX) {
|
|
81
|
+
report.fix = AUTO_FIX[entry.feature](node);
|
|
82
|
+
} else if (entry.feature === 'NullishCoalescing') {
|
|
83
|
+
report.suggest = [
|
|
84
|
+
{
|
|
85
|
+
messageId: 'suggestLogicalOr',
|
|
86
|
+
fix(fixer) {
|
|
87
|
+
const src = context.sourceCode.getText();
|
|
88
|
+
const between = src.slice(
|
|
89
|
+
node.left.range[1],
|
|
90
|
+
node.right.range[0],
|
|
91
|
+
);
|
|
92
|
+
const offset = between.indexOf('??');
|
|
93
|
+
if (offset === -1) return null;
|
|
94
|
+
const start = node.left.range[1] + offset;
|
|
95
|
+
return fixer.replaceTextRange([start, start + 2], '||');
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
} else if (entry.feature === 'DirectObjectReturn') {
|
|
100
|
+
report.suggest = [
|
|
101
|
+
{
|
|
102
|
+
messageId: 'suggestVarReturn',
|
|
103
|
+
fix(fixer) {
|
|
104
|
+
const objText = context.sourceCode.getText(node.argument);
|
|
105
|
+
const indent = ' '.repeat(node.loc.start.column);
|
|
106
|
+
return fixer.replaceText(
|
|
107
|
+
node,
|
|
108
|
+
`var _result = ${objText};\n${indent}return _result;`,
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
context.report(report);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return listeners;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: platform-function-arity
|
|
3
|
+
*
|
|
4
|
+
* Enforces correct argument counts for Platform.Function.* calls
|
|
5
|
+
* using arity metadata from ssjs-data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { platformFunctionLookup } from 'ssjs-data';
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Enforce correct argument counts for Platform.Function methods',
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
tooFewArgs:
|
|
18
|
+
"'Platform.Function.{{name}}' requires at least {{min}} argument(s) but was called with {{actual}}.",
|
|
19
|
+
tooManyArgs:
|
|
20
|
+
"'Platform.Function.{{name}}' accepts at most {{max}} argument(s) but was called with {{actual}}.",
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
CallExpression(node) {
|
|
28
|
+
const callee = node.callee;
|
|
29
|
+
if (callee.type !== 'MemberExpression') return;
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
callee.object.type === 'MemberExpression' &&
|
|
33
|
+
callee.object.object.type === 'Identifier' &&
|
|
34
|
+
callee.object.object.name === 'Platform' &&
|
|
35
|
+
callee.object.property.type === 'Identifier' &&
|
|
36
|
+
callee.object.property.name === 'Function' &&
|
|
37
|
+
callee.property.type === 'Identifier'
|
|
38
|
+
) {
|
|
39
|
+
const methodName = callee.property.name;
|
|
40
|
+
const entry = platformFunctionLookup.get(methodName.toLowerCase());
|
|
41
|
+
if (!entry) return;
|
|
42
|
+
|
|
43
|
+
const actual = node.arguments.length;
|
|
44
|
+
|
|
45
|
+
if (actual < entry.minArgs) {
|
|
46
|
+
context.report({
|
|
47
|
+
node: callee.property,
|
|
48
|
+
messageId: 'tooFewArgs',
|
|
49
|
+
data: {
|
|
50
|
+
name: entry.name,
|
|
51
|
+
min: String(entry.minArgs),
|
|
52
|
+
actual: String(actual),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
} else if (actual > entry.maxArgs) {
|
|
56
|
+
context.report({
|
|
57
|
+
node: callee.property,
|
|
58
|
+
messageId: 'tooManyArgs',
|
|
59
|
+
data: {
|
|
60
|
+
name: entry.name,
|
|
61
|
+
max: String(entry.maxArgs),
|
|
62
|
+
actual: String(actual),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: prefer-parsejson-safe-arg
|
|
3
|
+
*
|
|
4
|
+
* Platform.Function.ParseJSON() throws a 500 if the argument is undefined or
|
|
5
|
+
* not a string. Concatenating an empty string (someVar + '') is a common SFMC
|
|
6
|
+
* pattern to guard against this.
|
|
7
|
+
*
|
|
8
|
+
* Auto-fix: wraps the argument with `(arg + '')`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
fixable: 'code',
|
|
15
|
+
docs: {
|
|
16
|
+
description:
|
|
17
|
+
"Require concatenating an empty string to Platform.Function.ParseJSON() arguments to prevent 500 errors",
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
unsafeArg:
|
|
21
|
+
"Platform.Function.ParseJSON() may throw a 500 if the argument is undefined. " +
|
|
22
|
+
"Concatenate an empty string to be safe: ParseJSON({{argText}} + '').",
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
CallExpression(node) {
|
|
30
|
+
if (!isParseJSONCall(node)) return;
|
|
31
|
+
if (node.arguments.length === 0) return;
|
|
32
|
+
|
|
33
|
+
const arg = node.arguments[0];
|
|
34
|
+
|
|
35
|
+
if (isAlreadySafe(arg)) return;
|
|
36
|
+
|
|
37
|
+
context.report({
|
|
38
|
+
node: arg,
|
|
39
|
+
messageId: 'unsafeArg',
|
|
40
|
+
data: { argText: context.sourceCode.getText(arg) },
|
|
41
|
+
fix(fixer) {
|
|
42
|
+
const argText = context.sourceCode.getText(arg);
|
|
43
|
+
return fixer.replaceText(arg, `${argText} + ''`);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function isParseJSONCall(node) {
|
|
52
|
+
const callee = node.callee;
|
|
53
|
+
|
|
54
|
+
// ParseJSON(...)
|
|
55
|
+
if (callee.type === 'Identifier' && callee.name === 'ParseJSON') {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Platform.Function.ParseJSON(...)
|
|
60
|
+
if (
|
|
61
|
+
callee.type === 'MemberExpression' &&
|
|
62
|
+
callee.property.type === 'Identifier' &&
|
|
63
|
+
callee.property.name === 'ParseJSON' &&
|
|
64
|
+
callee.object.type === 'MemberExpression' &&
|
|
65
|
+
callee.object.property.type === 'Identifier' &&
|
|
66
|
+
callee.object.property.name === 'Function' &&
|
|
67
|
+
callee.object.object.type === 'Identifier' &&
|
|
68
|
+
callee.object.object.name === 'Platform'
|
|
69
|
+
) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isAlreadySafe(arg) {
|
|
77
|
+
// arg + '' or '' + arg
|
|
78
|
+
if (arg.type === 'BinaryExpression' && arg.operator === '+') {
|
|
79
|
+
if (isEmptyString(arg.left) || isEmptyString(arg.right)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// String literal passed directly — already a string
|
|
85
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isEmptyString(node) {
|
|
93
|
+
return node.type === 'Literal' && node.value === '';
|
|
94
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: prefer-platform-load-version
|
|
3
|
+
*
|
|
4
|
+
* Warns when Platform.Load("Core", version) uses a version string other
|
|
5
|
+
* than the recommended "1.1.5". Older versions like "1" or "1.1.1" miss
|
|
6
|
+
* bug-fixes and features available in the latest Core library release.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DEFAULT_VERSION = '1.1.5';
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Enforce a minimum version string in Platform.Load() calls',
|
|
16
|
+
},
|
|
17
|
+
fixable: 'code',
|
|
18
|
+
messages: {
|
|
19
|
+
outdatedVersion:
|
|
20
|
+
'Platform.Load("Core", "{{actual}}") should use version "{{expected}}" to get the latest bug-fixes. Update the second argument.',
|
|
21
|
+
},
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
version: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'The recommended Core library version string.',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
create(context) {
|
|
37
|
+
const options = context.options[0] || {};
|
|
38
|
+
const expectedVersion = options.version || DEFAULT_VERSION;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
CallExpression(node) {
|
|
42
|
+
const callee = node.callee;
|
|
43
|
+
if (
|
|
44
|
+
callee.type !== 'MemberExpression' ||
|
|
45
|
+
callee.object.type !== 'Identifier' ||
|
|
46
|
+
callee.object.name !== 'Platform' ||
|
|
47
|
+
callee.property.type !== 'Identifier' ||
|
|
48
|
+
callee.property.name !== 'Load'
|
|
49
|
+
) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const arguments_ = node.arguments;
|
|
54
|
+
if (
|
|
55
|
+
arguments_.length === 0 ||
|
|
56
|
+
arguments_[0].type !== 'Literal' ||
|
|
57
|
+
typeof arguments_[0].value !== 'string' ||
|
|
58
|
+
arguments_[0].value.toLowerCase() !== 'core'
|
|
59
|
+
) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (arguments_.length < 2) {
|
|
64
|
+
context.report({
|
|
65
|
+
node,
|
|
66
|
+
messageId: 'outdatedVersion',
|
|
67
|
+
data: { actual: '(none)', expected: expectedVersion },
|
|
68
|
+
fix(fixer) {
|
|
69
|
+
return fixer.insertTextAfter(
|
|
70
|
+
arguments_[0],
|
|
71
|
+
`, "${expectedVersion}"`,
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const versionArgument = arguments_[1];
|
|
79
|
+
if (
|
|
80
|
+
versionArgument.type === 'Literal' &&
|
|
81
|
+
typeof versionArgument.value === 'string' &&
|
|
82
|
+
versionArgument.value !== expectedVersion
|
|
83
|
+
) {
|
|
84
|
+
context.report({
|
|
85
|
+
node: versionArgument,
|
|
86
|
+
messageId: 'outdatedVersion',
|
|
87
|
+
data: { actual: versionArgument.value, expected: expectedVersion },
|
|
88
|
+
fix(fixer) {
|
|
89
|
+
return fixer.replaceText(versionArgument, `"${expectedVersion}"`);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: require-hasownproperty
|
|
3
|
+
*
|
|
4
|
+
* In for-in loops, require a hasOwnProperty guard to avoid iterating
|
|
5
|
+
* over inherited properties (like _type) that SSJS objects may have.
|
|
6
|
+
*
|
|
7
|
+
* Suggestion: wraps the existing loop body in an
|
|
8
|
+
* `if (obj.hasOwnProperty(key)) { ... }` guard.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
hasSuggestions: true,
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Require hasOwnProperty guard in for-in loops',
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
missingGuard:
|
|
20
|
+
'Add a hasOwnProperty check inside for-in loops to avoid iterating over inherited properties.',
|
|
21
|
+
suggestAddGuard:
|
|
22
|
+
'Wrap the loop body in an `if ({{obj}}.hasOwnProperty({{key}})) { ... }` guard',
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
ForInStatement(node) {
|
|
30
|
+
const body = node.body;
|
|
31
|
+
|
|
32
|
+
const stmts = body.type === 'BlockStatement' ? body.body : [body];
|
|
33
|
+
|
|
34
|
+
if (stmts.length === 0) return;
|
|
35
|
+
|
|
36
|
+
const hasGuard = stmts.some((stmt) => containsHasOwnProperty(stmt));
|
|
37
|
+
|
|
38
|
+
if (!hasGuard) {
|
|
39
|
+
const keyName = getKeyName(node.left);
|
|
40
|
+
const objText = context.sourceCode.getText(node.right);
|
|
41
|
+
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
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);
|
|
65
|
+
return fixer.replaceText(
|
|
66
|
+
body,
|
|
67
|
+
`{ if (${objText}.hasOwnProperty(${keyName})) { ${stmtText} } }`,
|
|
68
|
+
);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
: [],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function containsHasOwnProperty(node) {
|
|
81
|
+
if (!node) return false;
|
|
82
|
+
|
|
83
|
+
if (node.type === 'IfStatement' && node.test && hasOwnPropertyTest(node.test)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (node.type === 'IfStatement') {
|
|
88
|
+
return (
|
|
89
|
+
hasOwnPropertyTest(node.test) ||
|
|
90
|
+
containsHasOwnProperty(node.consequent) ||
|
|
91
|
+
containsHasOwnProperty(node.alternate)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (node.type === 'BlockStatement') {
|
|
96
|
+
return node.body.some((child) => containsHasOwnProperty(child));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (node.type === 'ExpressionStatement') {
|
|
100
|
+
return containsHasOwnProperty(node.expression);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasOwnPropertyTest(node) {
|
|
107
|
+
if (!node) return false;
|
|
108
|
+
|
|
109
|
+
if (
|
|
110
|
+
node.type === 'CallExpression' &&
|
|
111
|
+
node.callee.type === 'MemberExpression' &&
|
|
112
|
+
node.callee.property.type === 'Identifier' &&
|
|
113
|
+
node.callee.property.name === 'hasOwnProperty'
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (node.type === 'LogicalExpression') {
|
|
119
|
+
return hasOwnPropertyTest(node.left) || hasOwnPropertyTest(node.right);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (node.type === 'UnaryExpression') {
|
|
123
|
+
return hasOwnPropertyTest(node.argument);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Extracts the loop key variable name from a for-in left-hand side.
|
|
131
|
+
* Handles both `for (var k in obj)` and `for (k in obj)`.
|
|
132
|
+
*/
|
|
133
|
+
function getKeyName(left) {
|
|
134
|
+
if (left.type === 'VariableDeclaration' && left.declarations.length > 0) {
|
|
135
|
+
const id = left.declarations[0].id;
|
|
136
|
+
return id.type === 'Identifier' ? id.name : null;
|
|
137
|
+
}
|
|
138
|
+
if (left.type === 'Identifier') {
|
|
139
|
+
return left.name;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|