@hero-design/eslint-plugin 9.2.4-test-auto-workflow.2 → 9.2.4
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/CHANGELOG.md
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
# @hero-design/eslint-plugin
|
|
2
2
|
|
|
3
|
-
## 9.2.4
|
|
3
|
+
## 9.2.4
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- [#5230](https://github.com/Thinkei/hero-design/pull/5230) [`866862f`](https://github.com/Thinkei/hero-design/commit/866862fa2b9f814ee8abd4ca9ee6f75c06587716) Thanks [@tqdungit](https://github.com/tqdungit)! - [ANG-5494] Add allowClassNames option to banning-snowflake-approve-comment rule
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
### Patch Changes
|
|
12
|
-
|
|
13
|
-
- test
|
|
14
|
-
|
|
15
|
-
## 9.2.4-test-auto-workflow.0
|
|
16
|
-
|
|
17
|
-
### Patch Changes
|
|
18
|
-
|
|
19
|
-
- bump version
|
|
9
|
+
- [#5233](https://github.com/Thinkei/hero-design/pull/5233) [`95838f9`](https://github.com/Thinkei/hero-design/commit/95838f9bd37ba5ec1c64e379a0cb87531e3fd102) Thanks [@phthhieu](https://github.com/phthhieu)! - Resolve Dependabot vulnerabilities
|
|
20
10
|
|
|
21
11
|
## 9.2.3
|
|
22
12
|
|
|
@@ -36,7 +36,69 @@ function fetchUserData() {
|
|
|
36
36
|
const config = { apiKey: process.env.API_KEY };
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
The rule accepts an optional configuration object:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"rules": {
|
|
46
|
+
"@hero-design/banning-snowflake-approve-comment": [
|
|
47
|
+
"error",
|
|
48
|
+
{ "allowClassNames": ["rr-mark", "abc"] }
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `allowClassNames` (default: `[]`)
|
|
55
|
+
|
|
56
|
+
An array of classnames pre-approved for non-CSS usage. When `allowClassNames` is configured, the rule reads the `className` value from the adjacent JSX element and checks it against the list. If all classnames pass, the `@snowflake-guard/approved-classname` comment is allowed to be committed.
|
|
57
|
+
|
|
58
|
+
This is useful for classnames that are known to be non-CSS (e.g. analytics markers, third-party library identifiers) and are permanently allowed across your project.
|
|
59
|
+
|
|
60
|
+
**In ESLint config:**
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"rules": {
|
|
65
|
+
"@hero-design/banning-snowflake-approve-comment": [
|
|
66
|
+
"error",
|
|
67
|
+
{ "allowClassNames": ["rr-mark", "abc"] }
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**In code** — place `@snowflake-guard/approved-classname` above or inside the element. The classname does **not** need to be written in the comment — the rule reads it from the `className` prop directly:
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
// ✅ allowed — rr-mark is in allowClassNames, comment above element
|
|
77
|
+
// @snowflake-guard/approved-classname
|
|
78
|
+
<Button className="rr-mark" />
|
|
79
|
+
|
|
80
|
+
// ✅ allowed — comment inside element (between attributes)
|
|
81
|
+
<Button
|
|
82
|
+
// @snowflake-guard/approved-classname
|
|
83
|
+
className="rr-mark"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
// ✅ allowed — all classnames are in allowClassNames
|
|
87
|
+
// @snowflake-guard/approved-classname
|
|
88
|
+
<Button className="rr-mark abc" />
|
|
89
|
+
|
|
90
|
+
// ❌ error — custom-style is not in allowClassNames
|
|
91
|
+
// @snowflake-guard/approved-classname
|
|
92
|
+
<Button className="custom-style" />
|
|
93
|
+
|
|
94
|
+
// ❌ error — one classname is not in allowClassNames
|
|
95
|
+
// @snowflake-guard/approved-classname
|
|
96
|
+
<Button className="rr-mark custom-style" />
|
|
97
|
+
|
|
98
|
+
// ❌ error — allowClassNames only affects approved-classname, not other patterns
|
|
99
|
+
// @snowflake-guard/approved-inline-style
|
|
100
|
+
```
|
|
101
|
+
|
|
39
102
|
## When To Use It
|
|
40
103
|
|
|
41
104
|
Use this rule to prevent Snowflake Guard approval comments from being committed to the repository. If you need Snowflake Guard approval, please contact the Andromeda team directly.
|
|
42
|
-
|
|
@@ -8,23 +8,113 @@ module.exports = {
|
|
|
8
8
|
messages: {
|
|
9
9
|
noSnowflakeGuard:
|
|
10
10
|
'Comments including @snowflake-guard/ are not allowed. Please contact Andromeda team for the approval.',
|
|
11
|
+
classNameNotAllowed:
|
|
12
|
+
'"{{ classNames }}" is not in allowClassNames. Add it to allowClassNames in your ESLint config or contact Andromeda team for approval.',
|
|
11
13
|
},
|
|
12
|
-
schema: [
|
|
14
|
+
schema: [
|
|
15
|
+
{
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
allowClassNames: {
|
|
19
|
+
type: 'array',
|
|
20
|
+
items: { type: 'string' },
|
|
21
|
+
uniqueItems: true,
|
|
22
|
+
default: [],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
13
28
|
},
|
|
14
29
|
create(context) {
|
|
30
|
+
const allowClassNames = (context.options[0] || {}).allowClassNames || [];
|
|
31
|
+
const sourceCode = context.getSourceCode();
|
|
32
|
+
|
|
33
|
+
// Comments approved via allowClassNames — collected in JSXOpeningElement
|
|
34
|
+
const approvedComments = new Set();
|
|
35
|
+
// Comments with classnames not in allowClassNames — maps comment → rejected classnames
|
|
36
|
+
const rejectedComments = new Map();
|
|
37
|
+
|
|
38
|
+
const getClassNameValue = (attr) => {
|
|
39
|
+
if (attr.value.type === 'Literal') return String(attr.value.value);
|
|
40
|
+
if (
|
|
41
|
+
attr.value.type === 'JSXExpressionContainer' &&
|
|
42
|
+
attr.value.expression.type === 'Literal'
|
|
43
|
+
)
|
|
44
|
+
return String(attr.value.expression.value);
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
15
48
|
return {
|
|
16
|
-
|
|
17
|
-
const
|
|
49
|
+
JSXOpeningElement(node) {
|
|
50
|
+
const classNameAttr = node.attributes.find(
|
|
51
|
+
(attr) =>
|
|
52
|
+
attr.type === 'JSXAttribute' && attr.name.name === 'className'
|
|
53
|
+
);
|
|
54
|
+
if (!classNameAttr || !classNameAttr.value) return;
|
|
55
|
+
|
|
56
|
+
const value = getClassNameValue(classNameAttr);
|
|
57
|
+
if (value === null) return;
|
|
58
|
+
|
|
59
|
+
// Also check {/* @snowflake-guard/approved-classname */} JSX siblings
|
|
60
|
+
const jsxSiblingComments = [];
|
|
61
|
+
const parentElement = node.parent.parent;
|
|
62
|
+
if (parentElement && parentElement.type === 'JSXElement') {
|
|
63
|
+
const siblings = parentElement.children;
|
|
64
|
+
const myIndex = siblings.indexOf(node.parent);
|
|
65
|
+
for (let i = myIndex - 1; i >= 0; i--) {
|
|
66
|
+
const sib = siblings[i];
|
|
67
|
+
if (sib.type === 'JSXExpressionContainer') {
|
|
68
|
+
jsxSiblingComments.push(...sourceCode.getCommentsInside(sib));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (sib.type !== 'JSXText') break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const approvalComment = [
|
|
76
|
+
...sourceCode.getCommentsBefore(node),
|
|
77
|
+
...sourceCode.getCommentsBefore(classNameAttr),
|
|
78
|
+
...jsxSiblingComments,
|
|
79
|
+
].find((c) =>
|
|
80
|
+
c.value.trim().startsWith('@snowflake-guard/approved-classname')
|
|
81
|
+
);
|
|
82
|
+
if (!approvalComment) return;
|
|
83
|
+
|
|
84
|
+
const classNames = value.trim().split(/\s+/);
|
|
85
|
+
const rejectedClassNames = classNames.filter(
|
|
86
|
+
(cn) => !allowClassNames.includes(cn)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (rejectedClassNames.length === 0) {
|
|
90
|
+
approvedComments.add(approvalComment);
|
|
91
|
+
} else {
|
|
92
|
+
rejectedComments.set(approvalComment, rejectedClassNames);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
'Program:exit'() {
|
|
18
97
|
const comments = sourceCode.getAllComments();
|
|
19
98
|
|
|
20
99
|
comments.forEach((comment) => {
|
|
21
100
|
// Check if the comment includes @snowflake-guard/
|
|
22
|
-
if (comment.value.includes('@snowflake-guard/'))
|
|
101
|
+
if (!comment.value.includes('@snowflake-guard/')) return;
|
|
102
|
+
if (approvedComments.has(comment)) return;
|
|
103
|
+
|
|
104
|
+
const rejectedClassNames = rejectedComments.get(comment);
|
|
105
|
+
if (rejectedClassNames) {
|
|
23
106
|
context.report({
|
|
24
107
|
node: comment,
|
|
25
|
-
messageId: '
|
|
108
|
+
messageId: 'classNameNotAllowed',
|
|
109
|
+
data: { classNames: rejectedClassNames.join(', ') },
|
|
26
110
|
});
|
|
111
|
+
return;
|
|
27
112
|
}
|
|
113
|
+
|
|
114
|
+
context.report({
|
|
115
|
+
node: comment,
|
|
116
|
+
messageId: 'noSnowflakeGuard',
|
|
117
|
+
});
|
|
28
118
|
});
|
|
29
119
|
},
|
|
30
120
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hero-design/eslint-plugin",
|
|
3
|
-
"version": "9.2.4
|
|
3
|
+
"version": "9.2.4",
|
|
4
4
|
"description": "Hero Design's eslint plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@eslint/eslintrc": "^3.1.0",
|
|
26
26
|
"@eslint/js": "^9.8.0",
|
|
27
|
-
"eslint": "^8.
|
|
27
|
+
"eslint": "^8.57.0",
|
|
28
28
|
"eslint-plugin-eslint-plugin": "^5.0.0",
|
|
29
29
|
"eslint-plugin-node": "^11.1.0",
|
|
30
|
-
"jest": "^29.
|
|
31
|
-
"prettier-config-hd": "8.42.
|
|
30
|
+
"jest": "^29.7.0",
|
|
31
|
+
"prettier-config-hd": "8.42.4"
|
|
32
32
|
},
|
|
33
33
|
"engines": {
|
|
34
34
|
"node": "^20.0.0 || ^22.0.0"
|
|
@@ -1,46 +1,209 @@
|
|
|
1
1
|
const { RuleTester } = require('eslint');
|
|
2
2
|
const rule = require('../../../lib/rules/banning-snowflake-approve-comment');
|
|
3
3
|
|
|
4
|
+
const parserOptions = {
|
|
5
|
+
ecmaVersion: 6,
|
|
6
|
+
sourceType: 'module',
|
|
7
|
+
ecmaFeatures: { jsx: true },
|
|
8
|
+
};
|
|
9
|
+
|
|
4
10
|
const ruleTester = new RuleTester();
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
const ERROR_MESSAGE =
|
|
13
|
+
'Comments including @snowflake-guard/ are not allowed. Please contact Andromeda team for the approval.';
|
|
14
|
+
|
|
15
|
+
const classNameErrorMessage = (classNames) =>
|
|
16
|
+
`"${classNames}" is not in allowClassNames. Add it to allowClassNames in your ESLint config or contact Andromeda team for approval.`;
|
|
17
|
+
|
|
7
18
|
ruleTester.run('no-snowflake-guard', rule, {
|
|
8
|
-
// Valid cases (should not trigger the rule)
|
|
9
19
|
valid: [
|
|
10
|
-
|
|
11
|
-
'//
|
|
12
|
-
'
|
|
13
|
-
'
|
|
20
|
+
// Non-snowflake comments — always allowed
|
|
21
|
+
{ code: '// A normal comment without snowflake-guard', parserOptions },
|
|
22
|
+
{ code: '// Another valid comment', parserOptions },
|
|
23
|
+
{ code: '/* @no-snowflake-guard */', parserOptions },
|
|
24
|
+
{ code: 'console.log("no issues here");', parserOptions },
|
|
25
|
+
|
|
26
|
+
// Scenario 1: 1 HD component, 1 class (valid)
|
|
27
|
+
{
|
|
28
|
+
code: [
|
|
29
|
+
'// @snowflake-guard/approved-classname',
|
|
30
|
+
'<Button className="rr-mark" />',
|
|
31
|
+
].join('\n'),
|
|
32
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
33
|
+
parserOptions,
|
|
34
|
+
},
|
|
35
|
+
// Scenario 1: comment inside the element (between attributes)
|
|
36
|
+
{
|
|
37
|
+
code: [
|
|
38
|
+
'<Button',
|
|
39
|
+
' // @snowflake-guard/approved-classname',
|
|
40
|
+
' className="rr-mark"',
|
|
41
|
+
'/>',
|
|
42
|
+
].join('\n'),
|
|
43
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
44
|
+
parserOptions,
|
|
45
|
+
},
|
|
46
|
+
// Scenario 1: className expression container form
|
|
47
|
+
{
|
|
48
|
+
code: [
|
|
49
|
+
'// @snowflake-guard/approved-classname',
|
|
50
|
+
"<Button className={'rr-mark'} />",
|
|
51
|
+
].join('\n'),
|
|
52
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
53
|
+
parserOptions,
|
|
54
|
+
},
|
|
55
|
+
// Scenario 1: multi-line JSX with className not on first line
|
|
56
|
+
{
|
|
57
|
+
code: [
|
|
58
|
+
'// @snowflake-guard/approved-classname',
|
|
59
|
+
'<Typography.Text',
|
|
60
|
+
' intent="body"',
|
|
61
|
+
' className="rr-mark"',
|
|
62
|
+
'>text</Typography.Text>',
|
|
63
|
+
].join('\n'),
|
|
64
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
65
|
+
parserOptions,
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Scenario 2: 1 HD component, many classes (all valid)
|
|
69
|
+
{
|
|
70
|
+
code: [
|
|
71
|
+
'// @snowflake-guard/approved-classname',
|
|
72
|
+
'<Button className="rr-mark abc" />',
|
|
73
|
+
].join('\n'),
|
|
74
|
+
options: [{ allowClassNames: ['rr-mark', 'abc'] }],
|
|
75
|
+
parserOptions,
|
|
76
|
+
},
|
|
77
|
+
// Scenario 2: all valid within a larger allowClassNames list
|
|
78
|
+
{
|
|
79
|
+
code: [
|
|
80
|
+
'// @snowflake-guard/approved-classname',
|
|
81
|
+
'<Button className="rr-mark abc" />',
|
|
82
|
+
].join('\n'),
|
|
83
|
+
options: [{ allowClassNames: ['rr-mark', 'abc', 'other'] }],
|
|
84
|
+
parserOptions,
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Scenario 1: {/* */} JSX expression comment as preceding sibling
|
|
88
|
+
{
|
|
89
|
+
code: [
|
|
90
|
+
'<Card>',
|
|
91
|
+
' {/* @snowflake-guard/approved-classname */}',
|
|
92
|
+
' <Card.Content className="rr-mark" />',
|
|
93
|
+
'</Card>',
|
|
94
|
+
].join('\n'),
|
|
95
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
96
|
+
parserOptions,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Scenario 6: multiple HD components, many classes (all valid)
|
|
100
|
+
{
|
|
101
|
+
code: [
|
|
102
|
+
'<>',
|
|
103
|
+
' <Button',
|
|
104
|
+
' // @snowflake-guard/approved-classname',
|
|
105
|
+
' className="rr-mark"',
|
|
106
|
+
' />',
|
|
107
|
+
' <Typography.Text',
|
|
108
|
+
' // @snowflake-guard/approved-classname',
|
|
109
|
+
' className="rr-mark abc"',
|
|
110
|
+
' />',
|
|
111
|
+
'</>',
|
|
112
|
+
].join('\n'),
|
|
113
|
+
options: [{ allowClassNames: ['rr-mark', 'abc'] }],
|
|
114
|
+
parserOptions,
|
|
115
|
+
},
|
|
14
116
|
],
|
|
15
117
|
|
|
16
|
-
// Invalid cases (should trigger the rule)
|
|
17
118
|
invalid: [
|
|
119
|
+
// Scenario 3: 1 HD component, 1 class (invalid)
|
|
18
120
|
{
|
|
19
|
-
code:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
],
|
|
121
|
+
code: [
|
|
122
|
+
'// @snowflake-guard/approved-classname',
|
|
123
|
+
'<Button className="custom-style" />',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
126
|
+
parserOptions,
|
|
127
|
+
errors: [{ message: classNameErrorMessage('custom-style') }],
|
|
26
128
|
},
|
|
129
|
+
|
|
130
|
+
// Scenario 4: 1 HD component, many classes (all invalid)
|
|
27
131
|
{
|
|
28
|
-
code:
|
|
132
|
+
code: [
|
|
133
|
+
'// @snowflake-guard/approved-classname',
|
|
134
|
+
'<Button className="custom-style another-class" />',
|
|
135
|
+
].join('\n'),
|
|
136
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
137
|
+
parserOptions,
|
|
29
138
|
errors: [
|
|
30
|
-
{
|
|
31
|
-
message:
|
|
32
|
-
'Comments including @snowflake-guard/ are not allowed. Please contact Andromeda team for the approval.',
|
|
33
|
-
},
|
|
139
|
+
{ message: classNameErrorMessage('custom-style, another-class') },
|
|
34
140
|
],
|
|
35
141
|
},
|
|
142
|
+
|
|
143
|
+
// Scenario 5: 1 HD component, many classes (valid + invalid)
|
|
36
144
|
{
|
|
37
|
-
code:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
],
|
|
145
|
+
code: [
|
|
146
|
+
'// @snowflake-guard/approved-classname',
|
|
147
|
+
'<Button className="rr-mark custom-style" />',
|
|
148
|
+
].join('\n'),
|
|
149
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
150
|
+
parserOptions,
|
|
151
|
+
errors: [{ message: classNameErrorMessage('custom-style') }],
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Scenario 7: multiple HD components, many classes (valid + invalid)
|
|
155
|
+
// only the invalid component's comment is reported
|
|
156
|
+
{
|
|
157
|
+
code: [
|
|
158
|
+
'<>',
|
|
159
|
+
' <Button',
|
|
160
|
+
' // @snowflake-guard/approved-classname',
|
|
161
|
+
' className="rr-mark"',
|
|
162
|
+
' />',
|
|
163
|
+
' <Typography.Text',
|
|
164
|
+
' // @snowflake-guard/approved-classname',
|
|
165
|
+
' className="custom-style"',
|
|
166
|
+
' />',
|
|
167
|
+
'</>',
|
|
168
|
+
].join('\n'),
|
|
169
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
170
|
+
parserOptions,
|
|
171
|
+
errors: [{ message: classNameErrorMessage('custom-style') }],
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Other @snowflake-guard/ patterns — always banned regardless of allowClassNames
|
|
175
|
+
{
|
|
176
|
+
code: '// @snowflake-guard/none-css-classname',
|
|
177
|
+
parserOptions,
|
|
178
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
code: '/* @snowflake-guard/custom-pattern */',
|
|
182
|
+
parserOptions,
|
|
183
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
code: '// @snowflake-guard/approved-inline-style',
|
|
187
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
188
|
+
parserOptions,
|
|
189
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
// approved-classname with no allowClassNames configured
|
|
193
|
+
{
|
|
194
|
+
code: [
|
|
195
|
+
'// @snowflake-guard/approved-classname',
|
|
196
|
+
'<Button className="rr-mark" />',
|
|
197
|
+
].join('\n'),
|
|
198
|
+
parserOptions,
|
|
199
|
+
errors: [{ message: classNameErrorMessage('rr-mark') }],
|
|
200
|
+
},
|
|
201
|
+
// approved-classname with no adjacent JSX element
|
|
202
|
+
{
|
|
203
|
+
code: '// @snowflake-guard/approved-classname',
|
|
204
|
+
options: [{ allowClassNames: ['rr-mark'] }],
|
|
205
|
+
parserOptions,
|
|
206
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
44
207
|
},
|
|
45
208
|
],
|
|
46
209
|
});
|