Skip to content

Commit ba61171

Browse files
Copilotjoshblack
andcommitted
Add import removal functionality to no-deprecated-octicon rule
Co-authored-by: joshblack <[email protected]>
1 parent 5f920de commit ba61171

File tree

2 files changed

+153
-18
lines changed

2 files changed

+153
-18
lines changed

src/rules/__tests__/no-deprecated-octicon.test.js

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ import {XIcon} from '@primer/octicons-react'
4848
export default function App() {
4949
return <Octicon icon={XIcon} />
5050
}`,
51-
output: `import {Octicon} from '@primer/react/deprecated'
52-
import {XIcon} from '@primer/octicons-react'
51+
output: `import {XIcon} from '@primer/octicons-react'
5352
export default function App() {
5453
return <XIcon />
5554
}`,
@@ -67,8 +66,7 @@ import {XIcon} from '@primer/octicons-react'
6766
export default function App() {
6867
return <Octicon icon={XIcon} size={16} className="test" />
6968
}`,
70-
output: `import {Octicon} from '@primer/react/deprecated'
71-
import {XIcon} from '@primer/octicons-react'
69+
output: `import {XIcon} from '@primer/octicons-react'
7270
export default function App() {
7371
return <XIcon size={16} className="test" />
7472
}`,
@@ -87,8 +85,7 @@ export default function App() {
8785
const props = { size: 16 }
8886
return <Octicon {...props} icon={XIcon} className="test" />
8987
}`,
90-
output: `import {Octicon} from '@primer/react/deprecated'
91-
import {XIcon} from '@primer/octicons-react'
88+
output: `import {XIcon} from '@primer/octicons-react'
9289
export default function App() {
9390
const props = { size: 16 }
9491
return <XIcon {...props} className="test" />
@@ -109,8 +106,7 @@ export default function App() {
109106
<span>Content</span>
110107
</Octicon>
111108
}`,
112-
output: `import {Octicon} from '@primer/react/deprecated'
113-
import {XIcon} from '@primer/octicons-react'
109+
output: `import {XIcon} from '@primer/octicons-react'
114110
export default function App() {
115111
return <XIcon>
116112
<span>Content</span>
@@ -135,8 +131,7 @@ export default function App() {
135131
</div>
136132
)
137133
}`,
138-
output: `import {Octicon} from '@primer/react/deprecated'
139-
import {XIcon, CheckIcon} from '@primer/octicons-react'
134+
output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
140135
export default function App() {
141136
return (
142137
<div>
@@ -162,8 +157,7 @@ import {XIcon, CheckIcon} from '@primer/octicons-react'
162157
export default function App() {
163158
return <Octicon icon={condition ? XIcon : CheckIcon} />
164159
}`,
165-
output: `import {Octicon} from '@primer/react/deprecated'
166-
import {XIcon, CheckIcon} from '@primer/octicons-react'
160+
output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
167161
export default function App() {
168162
return condition ? <XIcon /> : <CheckIcon />
169163
}`,
@@ -181,8 +175,7 @@ import {XIcon, CheckIcon} from '@primer/octicons-react'
181175
export default function App() {
182176
return <Octicon icon={condition ? XIcon : CheckIcon} size={16} className="test" />
183177
}`,
184-
output: `import {Octicon} from '@primer/react/deprecated'
185-
import {XIcon, CheckIcon} from '@primer/octicons-react'
178+
output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
186179
export default function App() {
187180
return condition ? <XIcon size={16} className="test" /> : <CheckIcon size={16} className="test" />
188181
}`,
@@ -200,8 +193,7 @@ export default function App() {
200193
const icons = { x: XIcon }
201194
return <Octicon icon={icons.x} />
202195
}`,
203-
output: `import {Octicon} from '@primer/react/deprecated'
204-
export default function App() {
196+
output: `export default function App() {
205197
const icons = { x: XIcon }
206198
return React.createElement(icons.x, {})
207199
}`,
@@ -219,10 +211,65 @@ export default function App() {
219211
const icons = { x: XIcon }
220212
return <Octicon icon={icons.x} size={16} className="btn-icon" />
221213
}`,
222-
output: `import {Octicon} from '@primer/react/deprecated'
223-
export default function App() {
214+
output: `export default function App() {
224215
const icons = { x: XIcon }
225216
return React.createElement(icons.x, {size: 16, className: "btn-icon"})
217+
}`,
218+
errors: [
219+
{
220+
messageId: 'replaceDeprecatedOcticon',
221+
},
222+
],
223+
},
224+
225+
// Test import removal - single Octicon import gets removed
226+
{
227+
code: `import {Octicon} from '@primer/react/deprecated'
228+
import {XIcon} from '@primer/octicons-react'
229+
export default function App() {
230+
return <Octicon icon={XIcon} />
231+
}`,
232+
output: `import {XIcon} from '@primer/octicons-react'
233+
export default function App() {
234+
return <XIcon />
235+
}`,
236+
errors: [
237+
{
238+
messageId: 'replaceDeprecatedOcticon',
239+
},
240+
],
241+
},
242+
243+
// Test partial import removal - Octicon removed but other imports remain
244+
{
245+
code: `import {Octicon, Button} from '@primer/react/deprecated'
246+
import {XIcon} from '@primer/octicons-react'
247+
export default function App() {
248+
return <Octicon icon={XIcon} />
249+
}`,
250+
output: `import {Button} from '@primer/react/deprecated'
251+
import {XIcon} from '@primer/octicons-react'
252+
export default function App() {
253+
return <XIcon />
254+
}`,
255+
errors: [
256+
{
257+
messageId: 'replaceDeprecatedOcticon',
258+
},
259+
],
260+
},
261+
262+
// Test partial import removal - Octicon in middle of import list
263+
{
264+
code: `import {Button, Octicon, TextField} from '@primer/react/deprecated'
265+
import {XIcon} from '@primer/octicons-react'
266+
export default function App() {
267+
return <Octicon icon={XIcon} />
268+
}`,
269+
output: `import {Button, TextField} from '@primer/react/deprecated'
270+
import {XIcon} from '@primer/octicons-react'
271+
export default function App() {
272+
return <XIcon />
226273
}`,
227274
errors: [
228275
{

src/rules/no-deprecated-octicon.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,25 @@ module.exports = {
2424
},
2525
create(context) {
2626
const sourceCode = context.getSourceCode()
27+
28+
// Track Octicon imports
29+
const octiconImports = []
2730

2831
return {
32+
ImportDeclaration(node) {
33+
if (node.source.value !== '@primer/react/deprecated') {
34+
return
35+
}
36+
37+
const hasOcticon = node.specifiers.some(
38+
specifier => specifier.imported && specifier.imported.name === 'Octicon',
39+
)
40+
41+
if (hasOcticon) {
42+
octiconImports.push(node)
43+
}
44+
},
45+
2946
JSXElement(node) {
3047
const {openingElement, closingElement} = node
3148
const elementName = getJSXOpeningElementName(openingElement)
@@ -73,6 +90,68 @@ module.exports = {
7390
const otherProps = openingElement.attributes.filter(attr => attr !== iconProp)
7491
const propsText = otherProps.map(attr => sourceCode.getText(attr)).join(' ')
7592

93+
// Helper function to determine if this is the last Octicon in the file
94+
function isLastOcticon() {
95+
// Get all Octicon elements using the source code
96+
const sourceText = sourceCode.getText()
97+
const octiconMatches = [...sourceText.matchAll(/<Octicon\s/g)]
98+
99+
if (octiconMatches.length <= 1) {
100+
return true
101+
}
102+
103+
// Find the position of the current node in the source
104+
const currentNodeStart = node.range[0]
105+
106+
// Check if there are any more Octicon elements after this one
107+
const laterOcticons = octiconMatches.filter(match => match.index > currentNodeStart)
108+
return laterOcticons.length === 0
109+
}
110+
111+
// Helper function to generate import fixes if this is the last Octicon usage
112+
function* generateImportFixes(fixer) {
113+
if (isLastOcticon() && octiconImports.length > 0) {
114+
const importNode = octiconImports[0]
115+
const octiconSpecifier = importNode.specifiers.find(
116+
specifier => specifier.imported && specifier.imported.name === 'Octicon',
117+
)
118+
119+
if (importNode.specifiers.length === 1) {
120+
// Octicon is the only import, remove the entire import statement
121+
// Also remove trailing newline if present
122+
const nextToken = sourceCode.getTokenAfter(importNode)
123+
const importEnd = importNode.range[1]
124+
const nextStart = nextToken ? nextToken.range[0] : sourceCode.getText().length
125+
const textBetween = sourceCode.getText().substring(importEnd, nextStart)
126+
const hasTrailingNewline = /^\s*\n/.test(textBetween)
127+
128+
if (hasTrailingNewline) {
129+
const newlineMatch = textBetween.match(/^\s*\n/)
130+
const endRange = importEnd + newlineMatch[0].length
131+
yield fixer.removeRange([importNode.range[0], endRange])
132+
} else {
133+
yield fixer.remove(importNode)
134+
}
135+
} else {
136+
// Remove just the Octicon specifier from the import
137+
const previousToken = sourceCode.getTokenBefore(octiconSpecifier)
138+
const nextToken = sourceCode.getTokenAfter(octiconSpecifier)
139+
const hasTrailingComma = nextToken && nextToken.value === ','
140+
const hasLeadingComma = previousToken && previousToken.value === ','
141+
142+
let rangeToRemove
143+
if (hasTrailingComma) {
144+
rangeToRemove = [octiconSpecifier.range[0], nextToken.range[1] + 1]
145+
} else if (hasLeadingComma) {
146+
rangeToRemove = [previousToken.range[0], octiconSpecifier.range[1]]
147+
} else {
148+
rangeToRemove = [octiconSpecifier.range[0], octiconSpecifier.range[1]]
149+
}
150+
yield fixer.removeRange(rangeToRemove)
151+
}
152+
}
153+
}
154+
76155
// For simple cases, we can provide an autofix
77156
if (iconName) {
78157
context.report({
@@ -117,6 +196,9 @@ module.exports = {
117196
: iconProp.range[0]
118197
yield fixer.removeRange([startPos, iconProp.range[1]])
119198
}
199+
200+
// Handle import removal if this is the last Octicon usage
201+
yield* generateImportFixes(fixer)
120202
},
121203
})
122204
} else if (isConditional) {
@@ -144,6 +226,9 @@ module.exports = {
144226
}
145227

146228
yield fixer.replaceText(node, replacement)
229+
230+
// Handle import removal if this is the last Octicon usage
231+
yield* generateImportFixes(fixer)
147232
},
148233
})
149234
} else if (isMemberExpression) {
@@ -195,6 +280,9 @@ module.exports = {
195280
}
196281

197282
yield fixer.replaceText(node, replacement)
283+
284+
// Handle import removal if this is the last Octicon usage
285+
yield* generateImportFixes(fixer)
198286
},
199287
})
200288
} else {

0 commit comments

Comments
 (0)