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,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: require-platform-load-order
|
|
3
|
+
*
|
|
4
|
+
* Warns when Core library .Init() calls appear *before*
|
|
5
|
+
* Platform.Load("core", ...) in source order. Unlike require-platform-load
|
|
6
|
+
* which only checks existence, this rule checks ordering.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { coreObjectNames } from 'ssjs-data';
|
|
10
|
+
|
|
11
|
+
const TOP_LEVEL_CORE_NAMES = new Set([...coreObjectNames].map((n) => n.split('.')[0]));
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description:
|
|
18
|
+
'Require Platform.Load() to appear before Core library usage in source order',
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
loadAfterUse:
|
|
22
|
+
'Platform.Load("core", ...) must appear before "{{name}}" usage at line {{useLine}}. Move it earlier in the file.',
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
create(context) {
|
|
28
|
+
let platformLoadLine = null;
|
|
29
|
+
const coreUsages = [];
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
CallExpression(node) {
|
|
33
|
+
if (isPlatformLoadCall(node)) {
|
|
34
|
+
if (platformLoadLine === null) {
|
|
35
|
+
platformLoadLine = node.loc.start.line;
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const coreName = getCoreObjectUsage(node);
|
|
41
|
+
if (coreName) {
|
|
42
|
+
coreUsages.push({ node, name: coreName, line: node.loc.start.line });
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
'Program:exit'() {
|
|
47
|
+
if (platformLoadLine === null) return;
|
|
48
|
+
|
|
49
|
+
for (const usage of coreUsages) {
|
|
50
|
+
if (usage.line < platformLoadLine) {
|
|
51
|
+
context.report({
|
|
52
|
+
node: usage.node,
|
|
53
|
+
messageId: 'loadAfterUse',
|
|
54
|
+
data: {
|
|
55
|
+
name: usage.name,
|
|
56
|
+
useLine: String(usage.line),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function isPlatformLoadCall(node) {
|
|
67
|
+
const callee = node.callee;
|
|
68
|
+
if (
|
|
69
|
+
callee.type === 'MemberExpression' &&
|
|
70
|
+
callee.object.type === 'Identifier' &&
|
|
71
|
+
callee.object.name === 'Platform' &&
|
|
72
|
+
callee.property.type === 'Identifier' &&
|
|
73
|
+
callee.property.name === 'Load'
|
|
74
|
+
) {
|
|
75
|
+
const arguments_ = node.arguments;
|
|
76
|
+
if (
|
|
77
|
+
arguments_.length > 0 &&
|
|
78
|
+
arguments_[0].type === 'Literal' &&
|
|
79
|
+
typeof arguments_[0].value === 'string' &&
|
|
80
|
+
arguments_[0].value.toLowerCase() === 'core'
|
|
81
|
+
) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getCoreObjectUsage(node) {
|
|
89
|
+
const callee = node.callee;
|
|
90
|
+
if (callee.type !== 'MemberExpression') return null;
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
callee.object.type === 'MemberExpression' &&
|
|
94
|
+
callee.object.object.type === 'Identifier' &&
|
|
95
|
+
TOP_LEVEL_CORE_NAMES.has(callee.object.object.name) &&
|
|
96
|
+
callee.object.property.type === 'Identifier' &&
|
|
97
|
+
callee.property.type === 'Identifier'
|
|
98
|
+
) {
|
|
99
|
+
return `${callee.object.object.name}.${callee.object.property.name}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
callee.object.type === 'Identifier' &&
|
|
104
|
+
TOP_LEVEL_CORE_NAMES.has(callee.object.name) &&
|
|
105
|
+
callee.property.type === 'Identifier' &&
|
|
106
|
+
callee.property.name === 'Init'
|
|
107
|
+
) {
|
|
108
|
+
return callee.object.name;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: require-platform-load
|
|
3
|
+
*
|
|
4
|
+
* Ensures Platform.Load("core", "1") is called before any Core library
|
|
5
|
+
* object constructors are used. Without this call, Core library objects
|
|
6
|
+
* like DataExtension.Init() crash at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Auto-fix: inserts Platform.Load("core", "1.1.5") at the very top of the
|
|
9
|
+
* file when it is entirely absent. The fix is only attached to the first
|
|
10
|
+
* reported violation so ESLint does not duplicate the insertion.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { coreObjectNames } from 'ssjs-data';
|
|
14
|
+
|
|
15
|
+
const TOP_LEVEL_CORE_NAMES = new Set([...coreObjectNames].map((n) => n.split('.')[0]));
|
|
16
|
+
|
|
17
|
+
const PLATFORM_LOAD_STATEMENT = 'Platform.Load("core", "1.1.5");\n';
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'problem',
|
|
22
|
+
fixable: 'code',
|
|
23
|
+
docs: {
|
|
24
|
+
description: 'Require Platform.Load() before using Core library objects',
|
|
25
|
+
},
|
|
26
|
+
messages: {
|
|
27
|
+
missingLoad:
|
|
28
|
+
'Platform.Load("core", "1") must be called before using Core library object "{{name}}". Without it, the call will fail at runtime.',
|
|
29
|
+
},
|
|
30
|
+
schema: [],
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
create(context) {
|
|
34
|
+
let hasPlatformLoad = false;
|
|
35
|
+
const pendingReports = [];
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
CallExpression(node) {
|
|
39
|
+
if (isPlatformLoadCall(node)) {
|
|
40
|
+
hasPlatformLoad = true;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const coreName = getCoreObjectUsage(node);
|
|
45
|
+
if (coreName && !hasPlatformLoad) {
|
|
46
|
+
pendingReports.push({ node, name: coreName });
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
'Program:exit'() {
|
|
51
|
+
if (!hasPlatformLoad) {
|
|
52
|
+
for (let i = 0; i < pendingReports.length; i++) {
|
|
53
|
+
const { node, name } = pendingReports[i];
|
|
54
|
+
context.report({
|
|
55
|
+
node,
|
|
56
|
+
messageId: 'missingLoad',
|
|
57
|
+
data: { name },
|
|
58
|
+
// Only attach the fix to the first violation to prevent
|
|
59
|
+
// ESLint from inserting the statement multiple times.
|
|
60
|
+
fix:
|
|
61
|
+
i === 0
|
|
62
|
+
? (fixer) =>
|
|
63
|
+
fixer.insertTextBeforeRange(
|
|
64
|
+
[0, 0],
|
|
65
|
+
PLATFORM_LOAD_STATEMENT,
|
|
66
|
+
)
|
|
67
|
+
: undefined,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function isPlatformLoadCall(node) {
|
|
77
|
+
const callee = node.callee;
|
|
78
|
+
if (
|
|
79
|
+
callee.type === 'MemberExpression' &&
|
|
80
|
+
callee.object.type === 'Identifier' &&
|
|
81
|
+
callee.object.name === 'Platform' &&
|
|
82
|
+
callee.property.type === 'Identifier' &&
|
|
83
|
+
callee.property.name === 'Load'
|
|
84
|
+
) {
|
|
85
|
+
const arguments_ = node.arguments;
|
|
86
|
+
if (
|
|
87
|
+
arguments_.length > 0 &&
|
|
88
|
+
arguments_[0].type === 'Literal' &&
|
|
89
|
+
typeof arguments_[0].value === 'string' &&
|
|
90
|
+
arguments_[0].value.toLowerCase() === 'core'
|
|
91
|
+
) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getCoreObjectUsage(node) {
|
|
99
|
+
const callee = node.callee;
|
|
100
|
+
if (callee.type !== 'MemberExpression') return null;
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
callee.object.type === 'MemberExpression' &&
|
|
104
|
+
callee.object.object.type === 'Identifier' &&
|
|
105
|
+
TOP_LEVEL_CORE_NAMES.has(callee.object.object.name) &&
|
|
106
|
+
callee.object.property.type === 'Identifier' &&
|
|
107
|
+
callee.property.type === 'Identifier'
|
|
108
|
+
) {
|
|
109
|
+
return `${callee.object.object.name}.${callee.object.property.name}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
callee.object.type === 'Identifier' &&
|
|
114
|
+
TOP_LEVEL_CORE_NAMES.has(callee.object.name) &&
|
|
115
|
+
callee.property.type === 'Identifier'
|
|
116
|
+
) {
|
|
117
|
+
const method = callee.property.name;
|
|
118
|
+
if (method === 'Init') {
|
|
119
|
+
return callee.object.name;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint processor that extracts SSJS blocks from HTML/AMPscript files.
|
|
3
|
+
*
|
|
4
|
+
* Detects <script runat="server"> blocks (without language="ampscript")
|
|
5
|
+
* and returns them as JavaScript code blocks for ESLint to lint.
|
|
6
|
+
* Line offsets are preserved so ESLint reports errors at correct locations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const SCRIPT_OPEN_RE =
|
|
10
|
+
/<script\b(?=[^>]*\brunat\s*=\s*['"]server['"])(?![^>]*\blanguage\s*=\s*['"]ampscript['"])[^>]*>/gi;
|
|
11
|
+
const SCRIPT_CLOSE_RE = /<\/script\s*>/gi;
|
|
12
|
+
|
|
13
|
+
export function preprocess(text, filename) {
|
|
14
|
+
const blocks = [];
|
|
15
|
+
let match;
|
|
16
|
+
|
|
17
|
+
SCRIPT_OPEN_RE.lastIndex = 0;
|
|
18
|
+
while ((match = SCRIPT_OPEN_RE.exec(text)) !== null) {
|
|
19
|
+
const openEnd = match.index + match[0].length;
|
|
20
|
+
|
|
21
|
+
SCRIPT_CLOSE_RE.lastIndex = openEnd;
|
|
22
|
+
const closeMatch = SCRIPT_CLOSE_RE.exec(text);
|
|
23
|
+
if (!closeMatch) break;
|
|
24
|
+
|
|
25
|
+
const jsCode = text.slice(openEnd, closeMatch.index);
|
|
26
|
+
|
|
27
|
+
const linesBefore = text.slice(0, openEnd).split('\n');
|
|
28
|
+
const startLine = linesBefore.length - 1;
|
|
29
|
+
const padding = '\n'.repeat(startLine);
|
|
30
|
+
|
|
31
|
+
blocks.push({
|
|
32
|
+
text: padding + jsCode,
|
|
33
|
+
filename: `${filename}/ssjs-block-${blocks.length}.js`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (blocks.length === 0) {
|
|
38
|
+
return [text];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return blocks;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function postprocess(messages, _filename) {
|
|
45
|
+
return messages.flat();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default { preprocess, postprocess, supportsAutofix: false };
|