eslint-plugin-primer-react 8.5.2 → 8.6.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.
@@ -12,7 +12,7 @@ jobs:
12
12
  PROJECT_ID: 4503
13
13
  steps:
14
14
  - id: get-primer-access-token
15
- uses: actions/create-github-app-token@v2
15
+ uses: actions/create-github-app-token@v3
16
16
  with:
17
17
  app-id: ${{ vars.PRIMER_ISSUE_TRIAGE_APP_ID }}
18
18
  private-key: ${{ secrets.PRIMER_ISSUE_TRIAGE_APP_PRIVATE_KEY }}
@@ -22,7 +22,7 @@ jobs:
22
22
  env:
23
23
  GH_TOKEN: ${{ steps.get-primer-access-token.outputs.token }}
24
24
  - id: get-github-access-token
25
- uses: actions/create-github-app-token@v2
25
+ uses: actions/create-github-app-token@v3
26
26
  with:
27
27
  app-id: ${{ vars.PRIMER_ISSUE_TRIAGE_APP_ID_FOR_GITHUB }}
28
28
  private-key: ${{ secrets.PRIMER_ISSUE_TRIAGE_APP_PRIVATE_KEY_FOR_GITHUB }}
@@ -26,7 +26,7 @@ jobs:
26
26
  fetch-depth: 0
27
27
  persist-credentials: false
28
28
 
29
- - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42
29
+ - uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
30
30
  id: app-token
31
31
  with:
32
32
  app-id: ${{ vars.PRIMER_APP_ID_SHARED }}
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # eslint-plugin-primer-react
2
2
 
3
+ ## 8.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#376](https://github.com/primer/eslint-plugin-primer-react/pull/376) [`4f0e5b5`](https://github.com/primer/eslint-plugin-primer-react/commit/4f0e5b51fef890703ed8685ec74b30aa9e7b8547) Thanks [@copilot-swe-agent](https://github.com/apps/copilot-swe-agent)! - Add ESLint rule to replace deprecated Octicon component with specific icons and remove unused imports
8
+
3
9
  ## 8.5.2
4
10
 
5
11
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-primer-react",
3
- "version": "8.5.2",
3
+ "version": "8.6.0",
4
4
  "description": "ESLint rules for Primer React",
5
5
  "main": "src/index.js",
6
6
  "engines": {
@@ -41,7 +41,7 @@
41
41
  "typescript": "^5.8.2"
42
42
  },
43
43
  "devDependencies": {
44
- "@changesets/changelog-github": "^0.5.0",
44
+ "@changesets/changelog-github": "^0.6.0",
45
45
  "@changesets/cli": "^2.27.9",
46
46
  "@github/markdownlint-github": "^0.6.3",
47
47
  "@github/prettier-config": "0.0.6",
@@ -57,8 +57,8 @@
57
57
  "eslint-plugin-prettier": "^5.5.4",
58
58
  "globals": "^17.0.0",
59
59
  "jest": "^30.0.5",
60
- "markdownlint-cli2": "^0.20.0",
61
- "markdownlint-cli2-formatter-pretty": "^0.0.9"
60
+ "markdownlint-cli2": "^0.21.0",
61
+ "markdownlint-cli2-formatter-pretty": "^0.0.10"
62
62
  },
63
63
  "prettier": "@github/prettier-config"
64
64
  }
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ module.exports = {
23
23
  'spread-props-first': require('./rules/spread-props-first'),
24
24
  'use-deprecated-from-deprecated': require('./rules/use-deprecated-from-deprecated'),
25
25
  'use-styled-react-import': require('./rules/use-styled-react-import'),
26
+ 'no-deprecated-octicon': require('./rules/no-deprecated-octicon'),
26
27
  },
27
28
  configs: {
28
29
  recommended: require('./configs/recommended'),
@@ -0,0 +1,357 @@
1
+ 'use strict'
2
+
3
+ const {RuleTester} = require('eslint')
4
+ const rule = require('../no-deprecated-octicon')
5
+
6
+ const ruleTester = new RuleTester({
7
+ languageOptions: {
8
+ ecmaVersion: 'latest',
9
+ sourceType: 'module',
10
+ parserOptions: {
11
+ ecmaFeatures: {
12
+ jsx: true,
13
+ },
14
+ },
15
+ },
16
+ })
17
+
18
+ ruleTester.run('no-deprecated-octicon', rule, {
19
+ valid: [
20
+ // Not an Octicon component
21
+ {
22
+ code: `import {Button} from '@primer/react'
23
+ export default function App() {
24
+ return <Button>Click me</Button>
25
+ }`,
26
+ },
27
+
28
+ // Already using direct icon import
29
+ {
30
+ code: `import {XIcon} from '@primer/octicons-react'
31
+ export default function App() {
32
+ return <XIcon />
33
+ }`,
34
+ },
35
+
36
+ // Octicon without icon prop (edge case - can't transform)
37
+ {
38
+ code: `import {Octicon} from '@primer/react/deprecated'
39
+ export default function App() {
40
+ return <Octicon />
41
+ }`,
42
+ },
43
+ ],
44
+
45
+ invalid: [
46
+ // Basic case: simple Octicon with icon prop
47
+ {
48
+ code: `import {Octicon} from '@primer/react/deprecated'
49
+ import {XIcon} from '@primer/octicons-react'
50
+ export default function App() {
51
+ return <Octicon icon={XIcon} />
52
+ }`,
53
+ output: `import {XIcon} from '@primer/octicons-react'
54
+ export default function App() {
55
+ return <XIcon />
56
+ }`,
57
+ errors: [
58
+ {
59
+ messageId: 'replaceDeprecatedOcticon',
60
+ },
61
+ ],
62
+ },
63
+
64
+ // Octicon with additional props
65
+ {
66
+ code: `import {Octicon} from '@primer/react/deprecated'
67
+ import {XIcon} from '@primer/octicons-react'
68
+ export default function App() {
69
+ return <Octicon icon={XIcon} size={16} className="test" />
70
+ }`,
71
+ output: `import {XIcon} from '@primer/octicons-react'
72
+ export default function App() {
73
+ return <XIcon size={16} className="test" />
74
+ }`,
75
+ errors: [
76
+ {
77
+ messageId: 'replaceDeprecatedOcticon',
78
+ },
79
+ ],
80
+ },
81
+
82
+ // Octicon with spread props
83
+ {
84
+ code: `import {Octicon} from '@primer/react/deprecated'
85
+ import {XIcon} from '@primer/octicons-react'
86
+ export default function App() {
87
+ const props = { size: 16 }
88
+ return <Octicon {...props} icon={XIcon} className="test" />
89
+ }`,
90
+ output: `import {XIcon} from '@primer/octicons-react'
91
+ export default function App() {
92
+ const props = { size: 16 }
93
+ return <XIcon {...props} className="test" />
94
+ }`,
95
+ errors: [
96
+ {
97
+ messageId: 'replaceDeprecatedOcticon',
98
+ },
99
+ ],
100
+ },
101
+
102
+ // Octicon with closing tag
103
+ {
104
+ code: `import {Octicon} from '@primer/react/deprecated'
105
+ import {XIcon} from '@primer/octicons-react'
106
+ export default function App() {
107
+ return <Octicon icon={XIcon}>
108
+ <span>Content</span>
109
+ </Octicon>
110
+ }`,
111
+ output: `import {XIcon} from '@primer/octicons-react'
112
+ export default function App() {
113
+ return <XIcon>
114
+ <span>Content</span>
115
+ </XIcon>
116
+ }`,
117
+ errors: [
118
+ {
119
+ messageId: 'replaceDeprecatedOcticon',
120
+ },
121
+ ],
122
+ },
123
+
124
+ // Multiple Octicons
125
+ {
126
+ code: `import {Octicon} from '@primer/react/deprecated'
127
+ import {XIcon, CheckIcon} from '@primer/octicons-react'
128
+ export default function App() {
129
+ return (
130
+ <div>
131
+ <Octicon icon={XIcon} />
132
+ <Octicon icon={CheckIcon} size={24} />
133
+ </div>
134
+ )
135
+ }`,
136
+ output: `import {Octicon} from '@primer/react/deprecated'
137
+ import {XIcon, CheckIcon} from '@primer/octicons-react'
138
+ export default function App() {
139
+ return (
140
+ <div>
141
+ <XIcon />
142
+ <CheckIcon size={24} />
143
+ </div>
144
+ )
145
+ }`,
146
+ errors: [
147
+ {
148
+ messageId: 'replaceDeprecatedOcticon',
149
+ },
150
+ {
151
+ messageId: 'replaceDeprecatedOcticon',
152
+ },
153
+ ],
154
+ },
155
+
156
+ // Complex conditional case - now provides autofix
157
+ {
158
+ code: `import {Octicon} from '@primer/react/deprecated'
159
+ import {XIcon, CheckIcon} from '@primer/octicons-react'
160
+ export default function App() {
161
+ return <Octicon icon={condition ? XIcon : CheckIcon} />
162
+ }`,
163
+ output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
164
+ export default function App() {
165
+ return condition ? <XIcon /> : <CheckIcon />
166
+ }`,
167
+ errors: [
168
+ {
169
+ messageId: 'replaceDeprecatedOcticon',
170
+ },
171
+ ],
172
+ },
173
+
174
+ // Complex conditional case with props - applies props to both components
175
+ {
176
+ code: `import {Octicon} from '@primer/react/deprecated'
177
+ import {XIcon, CheckIcon} from '@primer/octicons-react'
178
+ export default function App() {
179
+ return <Octicon icon={condition ? XIcon : CheckIcon} size={16} className="test" />
180
+ }`,
181
+ output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
182
+ export default function App() {
183
+ return condition ? <XIcon size={16} className="test" /> : <CheckIcon size={16} className="test" />
184
+ }`,
185
+ errors: [
186
+ {
187
+ messageId: 'replaceDeprecatedOcticon',
188
+ },
189
+ ],
190
+ },
191
+
192
+ // Dynamic icon access - now provides autofix
193
+ {
194
+ code: `import {Octicon} from '@primer/react/deprecated'
195
+ export default function App() {
196
+ const icons = { x: XIcon }
197
+ return <Octicon icon={icons.x} />
198
+ }`,
199
+ output: `export default function App() {
200
+ const icons = { x: XIcon }
201
+ return React.createElement(icons.x, {})
202
+ }`,
203
+ errors: [
204
+ {
205
+ messageId: 'replaceDeprecatedOcticon',
206
+ },
207
+ ],
208
+ },
209
+
210
+ // Dynamic icon access with props
211
+ {
212
+ code: `import {Octicon} from '@primer/react/deprecated'
213
+ export default function App() {
214
+ const icons = { x: XIcon }
215
+ return <Octicon icon={icons.x} size={16} className="btn-icon" />
216
+ }`,
217
+ output: `export default function App() {
218
+ const icons = { x: XIcon }
219
+ return React.createElement(icons.x, {size: 16, className: "btn-icon"})
220
+ }`,
221
+ errors: [
222
+ {
223
+ messageId: 'replaceDeprecatedOcticon',
224
+ },
225
+ ],
226
+ },
227
+
228
+ // Test partial import removal - Octicon removed but other imports remain
229
+ {
230
+ code: `import {Octicon, Button} from '@primer/react/deprecated'
231
+ import {XIcon} from '@primer/octicons-react'
232
+ export default function App() {
233
+ return <Octicon icon={XIcon} />
234
+ }`,
235
+ output: `import {Button} from '@primer/react/deprecated'
236
+ import {XIcon} from '@primer/octicons-react'
237
+ export default function App() {
238
+ return <XIcon />
239
+ }`,
240
+ errors: [
241
+ {
242
+ messageId: 'replaceDeprecatedOcticon',
243
+ },
244
+ ],
245
+ },
246
+
247
+ // Test partial import removal - Octicon in middle of import list
248
+ {
249
+ code: `import {Button, Octicon, TextField} from '@primer/react/deprecated'
250
+ import {XIcon} from '@primer/octicons-react'
251
+ export default function App() {
252
+ return <Octicon icon={XIcon} />
253
+ }`,
254
+ output: `import {Button, TextField} from '@primer/react/deprecated'
255
+ import {XIcon} from '@primer/octicons-react'
256
+ export default function App() {
257
+ return <XIcon />
258
+ }`,
259
+ errors: [
260
+ {
261
+ messageId: 'replaceDeprecatedOcticon',
262
+ },
263
+ ],
264
+ },
265
+
266
+ // @primer/styled-react: basic case
267
+ {
268
+ code: `import {Octicon} from '@primer/styled-react'
269
+ import {XIcon} from '@primer/octicons-react'
270
+ export default function App() {
271
+ return <Octicon icon={XIcon} />
272
+ }`,
273
+ output: `import {XIcon} from '@primer/octicons-react'
274
+ export default function App() {
275
+ return <XIcon />
276
+ }`,
277
+ errors: [
278
+ {
279
+ messageId: 'replaceDeprecatedOcticon',
280
+ },
281
+ ],
282
+ },
283
+
284
+ // @primer/styled-react: Octicon with additional props
285
+ {
286
+ code: `import {Octicon} from '@primer/styled-react'
287
+ import {XIcon} from '@primer/octicons-react'
288
+ export default function App() {
289
+ return <Octicon icon={XIcon} size={16} className="test" />
290
+ }`,
291
+ output: `import {XIcon} from '@primer/octicons-react'
292
+ export default function App() {
293
+ return <XIcon size={16} className="test" />
294
+ }`,
295
+ errors: [
296
+ {
297
+ messageId: 'replaceDeprecatedOcticon',
298
+ },
299
+ ],
300
+ },
301
+
302
+ // @primer/styled-react: partial import removal
303
+ {
304
+ code: `import {Octicon, Button} from '@primer/styled-react'
305
+ import {XIcon} from '@primer/octicons-react'
306
+ export default function App() {
307
+ return <Octicon icon={XIcon} />
308
+ }`,
309
+ output: `import {Button} from '@primer/styled-react'
310
+ import {XIcon} from '@primer/octicons-react'
311
+ export default function App() {
312
+ return <XIcon />
313
+ }`,
314
+ errors: [
315
+ {
316
+ messageId: 'replaceDeprecatedOcticon',
317
+ },
318
+ ],
319
+ },
320
+
321
+ // @primer/styled-react: conditional case
322
+ {
323
+ code: `import {Octicon} from '@primer/styled-react'
324
+ import {XIcon, CheckIcon} from '@primer/octicons-react'
325
+ export default function App() {
326
+ return <Octicon icon={condition ? XIcon : CheckIcon} size={16} />
327
+ }`,
328
+ output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
329
+ export default function App() {
330
+ return condition ? <XIcon size={16} /> : <CheckIcon size={16} />
331
+ }`,
332
+ errors: [
333
+ {
334
+ messageId: 'replaceDeprecatedOcticon',
335
+ },
336
+ ],
337
+ },
338
+
339
+ // @primer/styled-react: dynamic icon access
340
+ {
341
+ code: `import {Octicon} from '@primer/styled-react'
342
+ export default function App() {
343
+ const icons = { x: XIcon }
344
+ return <Octicon icon={icons.x} size={24} />
345
+ }`,
346
+ output: `export default function App() {
347
+ const icons = { x: XIcon }
348
+ return React.createElement(icons.x, {size: 24})
349
+ }`,
350
+ errors: [
351
+ {
352
+ messageId: 'replaceDeprecatedOcticon',
353
+ },
354
+ ],
355
+ },
356
+ ],
357
+ })
@@ -0,0 +1,382 @@
1
+ 'use strict'
2
+
3
+ const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
4
+ const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
5
+ const url = require('../url')
6
+
7
+ /**
8
+ * @type {import('eslint').Rule.RuleModule}
9
+ */
10
+ module.exports = {
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Replace deprecated `Octicon` component with specific icon imports from `@primer/octicons-react`',
15
+ recommended: true,
16
+ url: url(module),
17
+ },
18
+ fixable: 'code',
19
+ schema: [],
20
+ messages: {
21
+ replaceDeprecatedOcticon:
22
+ 'Replace deprecated `Octicon` component with the specific icon from `@primer/octicons-react`',
23
+ },
24
+ },
25
+ create(context) {
26
+ const sourceCode = context.getSourceCode()
27
+
28
+ // Track Octicon imports and usages
29
+ const octiconImports = []
30
+ let totalOcticonUsages = 0
31
+ let processedOcticonUsages = 0
32
+
33
+ // Count total Octicon usages with icon props at the start
34
+ const sourceText = sourceCode.getText()
35
+ const octiconMatches = sourceText.match(/<Octicon[^>]*icon=/g)
36
+ totalOcticonUsages = octiconMatches ? octiconMatches.length : 0
37
+
38
+ return {
39
+ ImportDeclaration(node) {
40
+ if (node.source.value !== '@primer/react/deprecated' && node.source.value !== '@primer/styled-react') {
41
+ return
42
+ }
43
+
44
+ const hasOcticon = node.specifiers.some(
45
+ specifier => specifier.imported && specifier.imported.name === 'Octicon',
46
+ )
47
+
48
+ if (hasOcticon) {
49
+ octiconImports.push(node)
50
+ }
51
+ },
52
+
53
+ JSXElement(node) {
54
+ const {openingElement, closingElement} = node
55
+ const elementName = getJSXOpeningElementName(openingElement)
56
+
57
+ if (elementName !== 'Octicon') {
58
+ return
59
+ }
60
+
61
+ // Get the icon prop
62
+ const iconProp = getJSXOpeningElementAttribute(openingElement, 'icon')
63
+ if (!iconProp) {
64
+ // No icon prop - can't determine what to replace with
65
+ return
66
+ }
67
+
68
+ let iconName = null
69
+ let isConditional = false
70
+ let isMemberExpression = false
71
+ let conditionalExpression = null
72
+ let memberExpression = null
73
+
74
+ // Analyze the icon prop to determine the icon name and type
75
+ if (iconProp.value?.type === 'JSXExpressionContainer') {
76
+ const expression = iconProp.value.expression
77
+
78
+ if (expression.type === 'Identifier') {
79
+ // Simple case: icon={XIcon}
80
+ iconName = expression.name
81
+ } else if (expression.type === 'ConditionalExpression') {
82
+ // Conditional case: icon={condition ? XIcon : YIcon}
83
+ isConditional = true
84
+ conditionalExpression = expression
85
+ } else if (expression.type === 'MemberExpression') {
86
+ // Dynamic lookup: icon={icons.x}
87
+ isMemberExpression = true
88
+ memberExpression = expression
89
+ }
90
+ }
91
+
92
+ if (!iconName && !isConditional && !isMemberExpression) {
93
+ return
94
+ }
95
+
96
+ // Get all props except the icon prop to preserve them
97
+ const otherProps = openingElement.attributes.filter(attr => attr !== iconProp)
98
+ const propsText = otherProps.map(attr => sourceCode.getText(attr)).join(' ')
99
+
100
+ // For simple cases, we can provide an autofix
101
+ if (iconName) {
102
+ context.report({
103
+ node: openingElement,
104
+ messageId: 'replaceDeprecatedOcticon',
105
+ *fix(fixer) {
106
+ // Replace opening element name
107
+ yield fixer.replaceText(openingElement.name, iconName)
108
+
109
+ // Replace closing element name if it exists
110
+ if (closingElement) {
111
+ yield fixer.replaceText(closingElement.name, iconName)
112
+ }
113
+
114
+ // Remove the icon prop with proper whitespace handling
115
+ // Use the JSXAttribute node's properties to determine proper removal boundaries
116
+ const attributes = openingElement.attributes
117
+ const iconIndex = attributes.indexOf(iconProp)
118
+
119
+ if (iconIndex === 0 && attributes.length === 1) {
120
+ // Only attribute: remove with leading space
121
+ const beforeIcon = sourceCode.getTokenBefore(iconProp)
122
+ const startPos =
123
+ beforeIcon && /\s/.test(sourceCode.getText().substring(beforeIcon.range[1], iconProp.range[0]))
124
+ ? beforeIcon.range[1]
125
+ : iconProp.range[0]
126
+ yield fixer.removeRange([startPos, iconProp.range[1]])
127
+ } else if (iconIndex === 0) {
128
+ // First attribute: remove including trailing whitespace/comma
129
+ const afterIcon = attributes[1]
130
+ const afterPos = sourceCode.getText().substring(iconProp.range[1], afterIcon.range[0])
131
+ const whitespaceMatch = /^\s*/.exec(afterPos)
132
+ const endPos = whitespaceMatch ? iconProp.range[1] + whitespaceMatch[0].length : iconProp.range[1]
133
+ yield fixer.removeRange([iconProp.range[0], endPos])
134
+ } else {
135
+ // Not first attribute: remove including leading whitespace/comma
136
+ const beforeIcon = attributes[iconIndex - 1]
137
+ const beforePos = sourceCode.getText().substring(beforeIcon.range[1], iconProp.range[0])
138
+ const whitespaceMatch = /\s*$/.exec(beforePos)
139
+ const startPos = whitespaceMatch
140
+ ? beforeIcon.range[1] + beforePos.length - whitespaceMatch[0].length
141
+ : iconProp.range[0]
142
+ yield fixer.removeRange([startPos, iconProp.range[1]])
143
+ }
144
+
145
+ // Import removal: only enabled for single Octicon cases to prevent ESLint fix conflicts
146
+ // For multiple Octicons, JSX transformation works but import remains (can be cleaned up manually)
147
+ processedOcticonUsages++
148
+ if (
149
+ processedOcticonUsages === totalOcticonUsages &&
150
+ totalOcticonUsages === 1 &&
151
+ octiconImports.length > 0
152
+ ) {
153
+ const importNode = octiconImports[0]
154
+ const octiconSpecifier = importNode.specifiers.find(
155
+ specifier => specifier.imported && specifier.imported.name === 'Octicon',
156
+ )
157
+
158
+ if (importNode.specifiers.length === 1) {
159
+ // Octicon is the only import, remove the entire import statement
160
+ // Also remove trailing newline if present
161
+ const nextToken = sourceCode.getTokenAfter(importNode)
162
+ const importEnd = importNode.range[1]
163
+ const nextStart = nextToken ? nextToken.range[0] : sourceCode.getText().length
164
+ const textBetween = sourceCode.getText().substring(importEnd, nextStart)
165
+ const hasTrailingNewline = /^\s*\n/.test(textBetween)
166
+
167
+ if (hasTrailingNewline) {
168
+ const newlineMatch = textBetween.match(/^\s*\n/)
169
+ const endRange = importEnd + newlineMatch[0].length
170
+ yield fixer.removeRange([importNode.range[0], endRange])
171
+ } else {
172
+ yield fixer.remove(importNode)
173
+ }
174
+ } else {
175
+ // Remove just the Octicon specifier from the import
176
+ const previousToken = sourceCode.getTokenBefore(octiconSpecifier)
177
+ const nextToken = sourceCode.getTokenAfter(octiconSpecifier)
178
+ const hasTrailingComma = nextToken && nextToken.value === ','
179
+ const hasLeadingComma = previousToken && previousToken.value === ','
180
+
181
+ let rangeToRemove
182
+ if (hasTrailingComma) {
183
+ rangeToRemove = [octiconSpecifier.range[0], nextToken.range[1] + 1]
184
+ } else if (hasLeadingComma) {
185
+ rangeToRemove = [previousToken.range[0], octiconSpecifier.range[1]]
186
+ } else {
187
+ rangeToRemove = [octiconSpecifier.range[0], octiconSpecifier.range[1]]
188
+ }
189
+ yield fixer.removeRange(rangeToRemove)
190
+ }
191
+ }
192
+ },
193
+ })
194
+ } else if (isConditional) {
195
+ // Handle conditional expressions: icon={condition ? XIcon : YIcon}
196
+ // Transform to: condition ? <XIcon otherProps /> : <YIcon otherProps />
197
+ context.report({
198
+ node: openingElement,
199
+ messageId: 'replaceDeprecatedOcticon',
200
+ *fix(fixer) {
201
+ const test = sourceCode.getText(conditionalExpression.test)
202
+ const consequentName =
203
+ conditionalExpression.consequent.type === 'Identifier'
204
+ ? conditionalExpression.consequent.name
205
+ : sourceCode.getText(conditionalExpression.consequent)
206
+ const alternateName =
207
+ conditionalExpression.alternate.type === 'Identifier'
208
+ ? conditionalExpression.alternate.name
209
+ : sourceCode.getText(conditionalExpression.alternate)
210
+
211
+ const propsString = propsText ? ` ${propsText}` : ''
212
+ let replacement = `${test} ? <${consequentName}${propsString} /> : <${alternateName}${propsString} />`
213
+
214
+ // If it has children, we need to include them in both branches
215
+ if (node.children && node.children.length > 0) {
216
+ const childrenText = node.children.map(child => sourceCode.getText(child)).join('')
217
+ replacement = `${test} ? <${consequentName}${propsString}>${childrenText}</${consequentName}> : <${alternateName}${propsString}>${childrenText}</${alternateName}>`
218
+ }
219
+
220
+ yield fixer.replaceText(node, replacement)
221
+
222
+ // Import removal: only enabled for single Octicon cases to prevent ESLint fix conflicts
223
+ // For multiple Octicons, JSX transformation works but import remains (can be cleaned up manually)
224
+ processedOcticonUsages++
225
+ if (
226
+ processedOcticonUsages === totalOcticonUsages &&
227
+ totalOcticonUsages === 1 &&
228
+ octiconImports.length > 0
229
+ ) {
230
+ const importNode = octiconImports[0]
231
+ const octiconSpecifier = importNode.specifiers.find(
232
+ specifier => specifier.imported && specifier.imported.name === 'Octicon',
233
+ )
234
+
235
+ if (importNode.specifiers.length === 1) {
236
+ // Octicon is the only import, remove the entire import statement
237
+ // Also remove trailing newline if present
238
+ const nextToken = sourceCode.getTokenAfter(importNode)
239
+ const importEnd = importNode.range[1]
240
+ const nextStart = nextToken ? nextToken.range[0] : sourceCode.getText().length
241
+ const textBetween = sourceCode.getText().substring(importEnd, nextStart)
242
+ const hasTrailingNewline = /^\s*\n/.test(textBetween)
243
+
244
+ if (hasTrailingNewline) {
245
+ const newlineMatch = textBetween.match(/^\s*\n/)
246
+ const endRange = importEnd + newlineMatch[0].length
247
+ yield fixer.removeRange([importNode.range[0], endRange])
248
+ } else {
249
+ yield fixer.remove(importNode)
250
+ }
251
+ } else {
252
+ // Remove just the Octicon specifier from the import
253
+ const previousToken = sourceCode.getTokenBefore(octiconSpecifier)
254
+ const nextToken = sourceCode.getTokenAfter(octiconSpecifier)
255
+ const hasTrailingComma = nextToken && nextToken.value === ','
256
+ const hasLeadingComma = previousToken && previousToken.value === ','
257
+
258
+ let rangeToRemove
259
+ if (hasTrailingComma) {
260
+ rangeToRemove = [octiconSpecifier.range[0], nextToken.range[1] + 1]
261
+ } else if (hasLeadingComma) {
262
+ rangeToRemove = [previousToken.range[0], octiconSpecifier.range[1]]
263
+ } else {
264
+ rangeToRemove = [octiconSpecifier.range[0], octiconSpecifier.range[1]]
265
+ }
266
+ yield fixer.removeRange(rangeToRemove)
267
+ }
268
+ }
269
+ },
270
+ })
271
+ } else if (isMemberExpression) {
272
+ // Handle member expressions: icon={icons.x}
273
+ // Transform to: React.createElement(icons.x, otherProps)
274
+ context.report({
275
+ node: openingElement,
276
+ messageId: 'replaceDeprecatedOcticon',
277
+ *fix(fixer) {
278
+ const memberText = sourceCode.getText(memberExpression)
279
+
280
+ // Build props object
281
+ let propsObject = '{}'
282
+ if (otherProps.length > 0) {
283
+ const propStrings = otherProps.map(attr => {
284
+ if (attr.type === 'JSXSpreadAttribute') {
285
+ return `...${sourceCode.getText(attr.argument)}`
286
+ } else {
287
+ const name = attr.name.name
288
+ const value = attr.value
289
+ if (!value) {
290
+ return `${name}: true`
291
+ } else if (value.type === 'Literal') {
292
+ return `${name}: ${JSON.stringify(value.value)}`
293
+ } else if (value.type === 'JSXExpressionContainer') {
294
+ return `${name}: ${sourceCode.getText(value.expression)}`
295
+ }
296
+ return `${name}: ${sourceCode.getText(value)}`
297
+ }
298
+ })
299
+ propsObject = `{${propStrings.join(', ')}}`
300
+ }
301
+
302
+ let replacement = `React.createElement(${memberText}, ${propsObject})`
303
+
304
+ // If it has children, include them as additional arguments
305
+ if (node.children && node.children.length > 0) {
306
+ const childrenArgs = node.children
307
+ .map(child => {
308
+ if (child.type === 'JSXText') {
309
+ return JSON.stringify(child.value.trim()).replace(/\n\s*/g, ' ')
310
+ } else {
311
+ return sourceCode.getText(child)
312
+ }
313
+ })
314
+ .filter(child => child !== '""') // Filter out empty text nodes
315
+
316
+ if (childrenArgs.length > 0) {
317
+ replacement = `React.createElement(${memberText}, ${propsObject}, ${childrenArgs.join(', ')})`
318
+ }
319
+ }
320
+
321
+ yield fixer.replaceText(node, replacement)
322
+
323
+ // Import removal: only enabled for single Octicon cases to prevent ESLint fix conflicts
324
+ // For multiple Octicons, JSX transformation works but import remains (can be cleaned up manually)
325
+ processedOcticonUsages++
326
+ if (
327
+ processedOcticonUsages === totalOcticonUsages &&
328
+ totalOcticonUsages === 1 &&
329
+ octiconImports.length > 0
330
+ ) {
331
+ const importNode = octiconImports[0]
332
+ const octiconSpecifier = importNode.specifiers.find(
333
+ specifier => specifier.imported && specifier.imported.name === 'Octicon',
334
+ )
335
+
336
+ if (importNode.specifiers.length === 1) {
337
+ // Octicon is the only import, remove the entire import statement
338
+ // Also remove trailing newline if present
339
+ const nextToken = sourceCode.getTokenAfter(importNode)
340
+ const importEnd = importNode.range[1]
341
+ const nextStart = nextToken ? nextToken.range[0] : sourceCode.getText().length
342
+ const textBetween = sourceCode.getText().substring(importEnd, nextStart)
343
+ const hasTrailingNewline = /^\s*\n/.test(textBetween)
344
+
345
+ if (hasTrailingNewline) {
346
+ const newlineMatch = textBetween.match(/^\s*\n/)
347
+ const endRange = importEnd + newlineMatch[0].length
348
+ yield fixer.removeRange([importNode.range[0], endRange])
349
+ } else {
350
+ yield fixer.remove(importNode)
351
+ }
352
+ } else {
353
+ // Remove just the Octicon specifier from the import
354
+ const previousToken = sourceCode.getTokenBefore(octiconSpecifier)
355
+ const nextToken = sourceCode.getTokenAfter(octiconSpecifier)
356
+ const hasTrailingComma = nextToken && nextToken.value === ','
357
+ const hasLeadingComma = previousToken && previousToken.value === ','
358
+
359
+ let rangeToRemove
360
+ if (hasTrailingComma) {
361
+ rangeToRemove = [octiconSpecifier.range[0], nextToken.range[1] + 1]
362
+ } else if (hasLeadingComma) {
363
+ rangeToRemove = [previousToken.range[0], octiconSpecifier.range[1]]
364
+ } else {
365
+ rangeToRemove = [octiconSpecifier.range[0], octiconSpecifier.range[1]]
366
+ }
367
+ yield fixer.removeRange(rangeToRemove)
368
+ }
369
+ }
370
+ },
371
+ })
372
+ } else {
373
+ // For other complex cases, just report without autofix
374
+ context.report({
375
+ node: openingElement,
376
+ messageId: 'replaceDeprecatedOcticon',
377
+ })
378
+ }
379
+ },
380
+ }
381
+ },
382
+ }