@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-test-auto-workflow.2
3
+ ## 9.2.4
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - test
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
- ## 9.2.4-test-auto-workflow.1
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
- Program() {
17
- const sourceCode = context.getSourceCode();
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: 'noSnowflakeGuard',
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-test-auto-workflow.2",
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.56.0",
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.2.1",
31
- "prettier-config-hd": "8.42.5-test-auto-workflow.2"
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
- // Define valid and invalid test cases
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
- '// A normal comment without snowflake-guard',
11
- '// Another valid comment',
12
- '/* @no-snowflake-guard */',
13
- 'console.log("no issues here");',
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: '// @snowflake-guard/none-css-classname', // Triggers the rule
20
- errors: [
21
- {
22
- message:
23
- 'Comments including @snowflake-guard/ are not allowed. Please contact Andromeda team for the approval.',
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: '// @snowflake-guard/another-pattern', // Triggers the rule
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: '/* @snowflake-guard/custom-pattern */', // Triggers the rule for block comments
38
- errors: [
39
- {
40
- message:
41
- 'Comments including @snowflake-guard/ are not allowed. Please contact Andromeda team for the approval.',
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
  });