From 3e7d16203d8c0df36ee4e5bebca93c457832568f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 Aug 2025 20:21:11 +0000
Subject: [PATCH 1/5] Initial plan
From d35c34c7ad843fc6eb5b5b24c9ee7c63922a243c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 Aug 2025 20:37:14 +0000
Subject: [PATCH 2/5] Add no-deprecated-octicon ESLint rule to replace Octicon
with specific icons
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
src/index.js | 1 +
.../__tests__/no-deprecated-octicon.test.js | 188 ++++++++++++++++++
src/rules/no-deprecated-octicon.js | 124 ++++++++++++
3 files changed, 313 insertions(+)
create mode 100644 src/rules/__tests__/no-deprecated-octicon.test.js
create mode 100644 src/rules/no-deprecated-octicon.js
diff --git a/src/index.js b/src/index.js
index 68de6f2..a8c51a4 100644
--- a/src/index.js
+++ b/src/index.js
@@ -19,6 +19,7 @@ module.exports = {
'prefer-action-list-item-onselect': require('./rules/prefer-action-list-item-onselect'),
'enforce-css-module-identifier-casing': require('./rules/enforce-css-module-identifier-casing'),
'enforce-css-module-default-import': require('./rules/enforce-css-module-default-import'),
+ 'no-deprecated-octicon': require('./rules/no-deprecated-octicon'),
},
configs: {
recommended: require('./configs/recommended'),
diff --git a/src/rules/__tests__/no-deprecated-octicon.test.js b/src/rules/__tests__/no-deprecated-octicon.test.js
new file mode 100644
index 0000000..af5418c
--- /dev/null
+++ b/src/rules/__tests__/no-deprecated-octicon.test.js
@@ -0,0 +1,188 @@
+'use strict'
+
+const {RuleTester} = require('eslint')
+const rule = require('../no-deprecated-octicon')
+
+const ruleTester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+})
+
+ruleTester.run('no-deprecated-octicon', rule, {
+ valid: [
+ // Not an Octicon component
+ {
+ code: `import {Button} from '@primer/react'
+export default function App() {
+ return
+}`,
+ },
+
+ // Already using direct icon import
+ {
+ code: `import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ },
+
+ // Octicon without icon prop (edge case - can't transform)
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+export default function App() {
+ return
+}`,
+ },
+ ],
+
+ invalid: [
+ // Basic case: simple Octicon with icon prop
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ output: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Octicon with additional props
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ output: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Octicon with spread props
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ const props = { size: 16 }
+ return
+}`,
+ output: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ const props = { size: 16 }
+ return
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Octicon with closing tag
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+ Content
+
+}`,
+ output: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+ Content
+
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Multiple Octicons
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon, CheckIcon} from '@primer/octicons-react'
+export default function App() {
+ return (
+
+
+
+
+ )
+}`,
+ output: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon, CheckIcon} from '@primer/octicons-react'
+export default function App() {
+ return (
+
+
+
+
+ )
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Complex conditional case - should report but not autofix
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon, CheckIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ output: null,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Dynamic icon access - should report but not autofix
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+export default function App() {
+ const icons = { x: XIcon }
+ return
+}`,
+ output: null,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+ ],
+})
diff --git a/src/rules/no-deprecated-octicon.js b/src/rules/no-deprecated-octicon.js
new file mode 100644
index 0000000..887d773
--- /dev/null
+++ b/src/rules/no-deprecated-octicon.js
@@ -0,0 +1,124 @@
+'use strict'
+
+const {getJSXOpeningElementAttribute} = require('../utils/get-jsx-opening-element-attribute')
+const {getJSXOpeningElementName} = require('../utils/get-jsx-opening-element-name')
+const url = require('../url')
+
+/**
+ * @type {import('eslint').Rule.RuleModule}
+ */
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'Replace deprecated `Octicon` component with specific icon imports from `@primer/octicons-react`',
+ recommended: true,
+ url: url(module),
+ },
+ fixable: 'code',
+ schema: [],
+ messages: {
+ replaceDeprecatedOcticon:
+ 'Replace deprecated `Octicon` component with the specific icon from `@primer/octicons-react`',
+ },
+ },
+ create(context) {
+ const sourceCode = context.getSourceCode()
+
+ return {
+ JSXElement(node) {
+ const {openingElement, closingElement} = node
+ const elementName = getJSXOpeningElementName(openingElement)
+
+ if (elementName !== 'Octicon') {
+ return
+ }
+
+ // Get the icon prop
+ const iconProp = getJSXOpeningElementAttribute(openingElement, 'icon')
+ if (!iconProp) {
+ // No icon prop - can't determine what to replace with
+ return
+ }
+
+ let iconName = null
+ let isDynamic = false
+
+ // Analyze the icon prop to determine the icon name
+ if (iconProp.value?.type === 'JSXExpressionContainer') {
+ const expression = iconProp.value.expression
+
+ if (expression.type === 'Identifier') {
+ // Simple case: icon={XIcon}
+ iconName = expression.name
+ } else if (expression.type === 'ConditionalExpression') {
+ // Conditional case: icon={condition ? XIcon : YIcon}
+ // For now, we'll skip auto-fixing complex conditionals
+ isDynamic = true
+ } else if (expression.type === 'MemberExpression') {
+ // Dynamic lookup: icon={icons.x}
+ isDynamic = true
+ }
+ }
+
+ if (!iconName && !isDynamic) {
+ return
+ }
+
+ // For simple cases, we can provide an autofix
+ if (iconName && !isDynamic) {
+ context.report({
+ node: openingElement,
+ messageId: 'replaceDeprecatedOcticon',
+ *fix(fixer) {
+ // Replace opening element name
+ yield fixer.replaceText(openingElement.name, iconName)
+
+ // Replace closing element name if it exists
+ if (closingElement) {
+ yield fixer.replaceText(closingElement.name, iconName)
+ }
+
+ // Remove the icon prop with proper whitespace handling
+ // Use the JSXAttribute node's properties to determine proper removal boundaries
+ const attributes = openingElement.attributes
+ const iconIndex = attributes.indexOf(iconProp)
+
+ if (iconIndex === 0 && attributes.length === 1) {
+ // Only attribute: remove with leading space
+ const beforeIcon = sourceCode.getTokenBefore(iconProp)
+ const startPos =
+ beforeIcon && /\s/.test(sourceCode.getText().substring(beforeIcon.range[1], iconProp.range[0]))
+ ? beforeIcon.range[1]
+ : iconProp.range[0]
+ yield fixer.removeRange([startPos, iconProp.range[1]])
+ } else if (iconIndex === 0) {
+ // First attribute: remove including trailing whitespace/comma
+ const afterIcon = attributes[1]
+ const afterPos = sourceCode.getText().substring(iconProp.range[1], afterIcon.range[0])
+ const whitespaceMatch = /^\s*/.exec(afterPos)
+ const endPos = whitespaceMatch ? iconProp.range[1] + whitespaceMatch[0].length : iconProp.range[1]
+ yield fixer.removeRange([iconProp.range[0], endPos])
+ } else {
+ // Not first attribute: remove including leading whitespace/comma
+ const beforeIcon = attributes[iconIndex - 1]
+ const beforePos = sourceCode.getText().substring(beforeIcon.range[1], iconProp.range[0])
+ const whitespaceMatch = /\s*$/.exec(beforePos)
+ const startPos = whitespaceMatch
+ ? beforeIcon.range[1] + beforePos.length - whitespaceMatch[0].length
+ : iconProp.range[0]
+ yield fixer.removeRange([startPos, iconProp.range[1]])
+ }
+ },
+ })
+ } else {
+ // For complex cases, just report without autofix
+ context.report({
+ node: openingElement,
+ messageId: 'replaceDeprecatedOcticon',
+ })
+ }
+ },
+ }
+ },
+}
From 5f920de26cf2cb03fca0034e460612fb3a83185b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 6 Aug 2025 21:17:14 +0000
Subject: [PATCH 3/5] Add autofix support for conditional and dynamic icon
usage
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
.../__tests__/no-deprecated-octicon.test.js | 54 +++++++++-
src/rules/no-deprecated-octicon.js | 102 ++++++++++++++++--
2 files changed, 144 insertions(+), 12 deletions(-)
diff --git a/src/rules/__tests__/no-deprecated-octicon.test.js b/src/rules/__tests__/no-deprecated-octicon.test.js
index af5418c..ceca22b 100644
--- a/src/rules/__tests__/no-deprecated-octicon.test.js
+++ b/src/rules/__tests__/no-deprecated-octicon.test.js
@@ -155,14 +155,37 @@ export default function App() {
],
},
- // Complex conditional case - should report but not autofix
+ // Complex conditional case - now provides autofix
{
code: `import {Octicon} from '@primer/react/deprecated'
import {XIcon, CheckIcon} from '@primer/octicons-react'
export default function App() {
return
}`,
- output: null,
+ output: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon, CheckIcon} from '@primer/octicons-react'
+export default function App() {
+ return condition ? :
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Complex conditional case with props - applies props to both components
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon, CheckIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ output: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon, CheckIcon} from '@primer/octicons-react'
+export default function App() {
+ return condition ? :
+}`,
errors: [
{
messageId: 'replaceDeprecatedOcticon',
@@ -170,14 +193,37 @@ export default function App() {
],
},
- // Dynamic icon access - should report but not autofix
+ // Dynamic icon access - now provides autofix
{
code: `import {Octicon} from '@primer/react/deprecated'
export default function App() {
const icons = { x: XIcon }
return
}`,
- output: null,
+ output: `import {Octicon} from '@primer/react/deprecated'
+export default function App() {
+ const icons = { x: XIcon }
+ return React.createElement(icons.x, {})
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Dynamic icon access with props
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+export default function App() {
+ const icons = { x: XIcon }
+ return
+}`,
+ output: `import {Octicon} from '@primer/react/deprecated'
+export default function App() {
+ const icons = { x: XIcon }
+ return React.createElement(icons.x, {size: 16, className: "btn-icon"})
+}`,
errors: [
{
messageId: 'replaceDeprecatedOcticon',
diff --git a/src/rules/no-deprecated-octicon.js b/src/rules/no-deprecated-octicon.js
index 887d773..8f8ffbc 100644
--- a/src/rules/no-deprecated-octicon.js
+++ b/src/rules/no-deprecated-octicon.js
@@ -42,9 +42,12 @@ module.exports = {
}
let iconName = null
- let isDynamic = false
+ let isConditional = false
+ let isMemberExpression = false
+ let conditionalExpression = null
+ let memberExpression = null
- // Analyze the icon prop to determine the icon name
+ // Analyze the icon prop to determine the icon name and type
if (iconProp.value?.type === 'JSXExpressionContainer') {
const expression = iconProp.value.expression
@@ -53,20 +56,25 @@ module.exports = {
iconName = expression.name
} else if (expression.type === 'ConditionalExpression') {
// Conditional case: icon={condition ? XIcon : YIcon}
- // For now, we'll skip auto-fixing complex conditionals
- isDynamic = true
+ isConditional = true
+ conditionalExpression = expression
} else if (expression.type === 'MemberExpression') {
// Dynamic lookup: icon={icons.x}
- isDynamic = true
+ isMemberExpression = true
+ memberExpression = expression
}
}
- if (!iconName && !isDynamic) {
+ if (!iconName && !isConditional && !isMemberExpression) {
return
}
+ // Get all props except the icon prop to preserve them
+ const otherProps = openingElement.attributes.filter(attr => attr !== iconProp)
+ const propsText = otherProps.map(attr => sourceCode.getText(attr)).join(' ')
+
// For simple cases, we can provide an autofix
- if (iconName && !isDynamic) {
+ if (iconName) {
context.report({
node: openingElement,
messageId: 'replaceDeprecatedOcticon',
@@ -111,8 +119,86 @@ module.exports = {
}
},
})
+ } else if (isConditional) {
+ // Handle conditional expressions: icon={condition ? XIcon : YIcon}
+ // Transform to: condition ? :
+ context.report({
+ node: openingElement,
+ messageId: 'replaceDeprecatedOcticon',
+ *fix(fixer) {
+ const test = sourceCode.getText(conditionalExpression.test)
+ const consequentName = conditionalExpression.consequent.type === 'Identifier'
+ ? conditionalExpression.consequent.name
+ : sourceCode.getText(conditionalExpression.consequent)
+ const alternateName = conditionalExpression.alternate.type === 'Identifier'
+ ? conditionalExpression.alternate.name
+ : sourceCode.getText(conditionalExpression.alternate)
+
+ const propsString = propsText ? ` ${propsText}` : ''
+ let replacement = `${test} ? <${consequentName}${propsString} /> : <${alternateName}${propsString} />`
+
+ // If it has children, we need to include them in both branches
+ if (node.children && node.children.length > 0) {
+ const childrenText = node.children.map(child => sourceCode.getText(child)).join('')
+ replacement = `${test} ? <${consequentName}${propsString}>${childrenText}${consequentName}> : <${alternateName}${propsString}>${childrenText}${alternateName}>`
+ }
+
+ yield fixer.replaceText(node, replacement)
+ },
+ })
+ } else if (isMemberExpression) {
+ // Handle member expressions: icon={icons.x}
+ // Transform to: React.createElement(icons.x, otherProps)
+ context.report({
+ node: openingElement,
+ messageId: 'replaceDeprecatedOcticon',
+ *fix(fixer) {
+ const memberText = sourceCode.getText(memberExpression)
+
+ // Build props object
+ let propsObject = '{}'
+ if (otherProps.length > 0) {
+ const propStrings = otherProps.map(attr => {
+ if (attr.type === 'JSXSpreadAttribute') {
+ return `...${sourceCode.getText(attr.argument)}`
+ } else {
+ const name = attr.name.name
+ const value = attr.value
+ if (!value) {
+ return `${name}: true`
+ } else if (value.type === 'Literal') {
+ return `${name}: ${JSON.stringify(value.value)}`
+ } else if (value.type === 'JSXExpressionContainer') {
+ return `${name}: ${sourceCode.getText(value.expression)}`
+ }
+ return `${name}: ${sourceCode.getText(value)}`
+ }
+ })
+ propsObject = `{${propStrings.join(', ')}}`
+ }
+
+ let replacement = `React.createElement(${memberText}, ${propsObject})`
+
+ // If it has children, include them as additional arguments
+ if (node.children && node.children.length > 0) {
+ const childrenArgs = node.children.map(child => {
+ if (child.type === 'JSXText') {
+ return JSON.stringify(child.value.trim()).replace(/\n\s*/g, ' ')
+ } else {
+ return sourceCode.getText(child)
+ }
+ }).filter(child => child !== '""') // Filter out empty text nodes
+
+ if (childrenArgs.length > 0) {
+ replacement = `React.createElement(${memberText}, ${propsObject}, ${childrenArgs.join(', ')})`
+ }
+ }
+
+ yield fixer.replaceText(node, replacement)
+ },
+ })
} else {
- // For complex cases, just report without autofix
+ // For other complex cases, just report without autofix
context.report({
node: openingElement,
messageId: 'replaceDeprecatedOcticon',
From ba61171cb43a4c3ef6202476da1970cf6ed0e532 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 Aug 2025 21:42:56 +0000
Subject: [PATCH 4/5] Add import removal functionality to no-deprecated-octicon
rule
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
.../__tests__/no-deprecated-octicon.test.js | 83 +++++++++++++----
src/rules/no-deprecated-octicon.js | 88 +++++++++++++++++++
2 files changed, 153 insertions(+), 18 deletions(-)
diff --git a/src/rules/__tests__/no-deprecated-octicon.test.js b/src/rules/__tests__/no-deprecated-octicon.test.js
index ceca22b..341be4e 100644
--- a/src/rules/__tests__/no-deprecated-octicon.test.js
+++ b/src/rules/__tests__/no-deprecated-octicon.test.js
@@ -48,8 +48,7 @@ import {XIcon} from '@primer/octicons-react'
export default function App() {
return
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-import {XIcon} from '@primer/octicons-react'
+ output: `import {XIcon} from '@primer/octicons-react'
export default function App() {
return
}`,
@@ -67,8 +66,7 @@ import {XIcon} from '@primer/octicons-react'
export default function App() {
return
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-import {XIcon} from '@primer/octicons-react'
+ output: `import {XIcon} from '@primer/octicons-react'
export default function App() {
return
}`,
@@ -87,8 +85,7 @@ export default function App() {
const props = { size: 16 }
return
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-import {XIcon} from '@primer/octicons-react'
+ output: `import {XIcon} from '@primer/octicons-react'
export default function App() {
const props = { size: 16 }
return
@@ -109,8 +106,7 @@ export default function App() {
Content
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-import {XIcon} from '@primer/octicons-react'
+ output: `import {XIcon} from '@primer/octicons-react'
export default function App() {
return
Content
@@ -135,8 +131,7 @@ export default function App() {
)
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-import {XIcon, CheckIcon} from '@primer/octicons-react'
+ output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
export default function App() {
return (
@@ -162,8 +157,7 @@ import {XIcon, CheckIcon} from '@primer/octicons-react'
export default function App() {
return
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-import {XIcon, CheckIcon} from '@primer/octicons-react'
+ output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
export default function App() {
return condition ? :
}`,
@@ -181,8 +175,7 @@ import {XIcon, CheckIcon} from '@primer/octicons-react'
export default function App() {
return
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-import {XIcon, CheckIcon} from '@primer/octicons-react'
+ output: `import {XIcon, CheckIcon} from '@primer/octicons-react'
export default function App() {
return condition ? :
}`,
@@ -200,8 +193,7 @@ export default function App() {
const icons = { x: XIcon }
return
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-export default function App() {
+ output: `export default function App() {
const icons = { x: XIcon }
return React.createElement(icons.x, {})
}`,
@@ -219,10 +211,65 @@ export default function App() {
const icons = { x: XIcon }
return
}`,
- output: `import {Octicon} from '@primer/react/deprecated'
-export default function App() {
+ output: `export default function App() {
const icons = { x: XIcon }
return React.createElement(icons.x, {size: 16, className: "btn-icon"})
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Test import removal - single Octicon import gets removed
+ {
+ code: `import {Octicon} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ output: `import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Test partial import removal - Octicon removed but other imports remain
+ {
+ code: `import {Octicon, Button} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ output: `import {Button} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ errors: [
+ {
+ messageId: 'replaceDeprecatedOcticon',
+ },
+ ],
+ },
+
+ // Test partial import removal - Octicon in middle of import list
+ {
+ code: `import {Button, Octicon, TextField} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
+}`,
+ output: `import {Button, TextField} from '@primer/react/deprecated'
+import {XIcon} from '@primer/octicons-react'
+export default function App() {
+ return
}`,
errors: [
{
diff --git a/src/rules/no-deprecated-octicon.js b/src/rules/no-deprecated-octicon.js
index 8f8ffbc..2f32633 100644
--- a/src/rules/no-deprecated-octicon.js
+++ b/src/rules/no-deprecated-octicon.js
@@ -24,8 +24,25 @@ module.exports = {
},
create(context) {
const sourceCode = context.getSourceCode()
+
+ // Track Octicon imports
+ const octiconImports = []
return {
+ ImportDeclaration(node) {
+ if (node.source.value !== '@primer/react/deprecated') {
+ return
+ }
+
+ const hasOcticon = node.specifiers.some(
+ specifier => specifier.imported && specifier.imported.name === 'Octicon',
+ )
+
+ if (hasOcticon) {
+ octiconImports.push(node)
+ }
+ },
+
JSXElement(node) {
const {openingElement, closingElement} = node
const elementName = getJSXOpeningElementName(openingElement)
@@ -73,6 +90,68 @@ module.exports = {
const otherProps = openingElement.attributes.filter(attr => attr !== iconProp)
const propsText = otherProps.map(attr => sourceCode.getText(attr)).join(' ')
+ // Helper function to determine if this is the last Octicon in the file
+ function isLastOcticon() {
+ // Get all Octicon elements using the source code
+ const sourceText = sourceCode.getText()
+ const octiconMatches = [...sourceText.matchAll(/ match.index > currentNodeStart)
+ return laterOcticons.length === 0
+ }
+
+ // Helper function to generate import fixes if this is the last Octicon usage
+ function* generateImportFixes(fixer) {
+ if (isLastOcticon() && octiconImports.length > 0) {
+ const importNode = octiconImports[0]
+ const octiconSpecifier = importNode.specifiers.find(
+ specifier => specifier.imported && specifier.imported.name === 'Octicon',
+ )
+
+ if (importNode.specifiers.length === 1) {
+ // Octicon is the only import, remove the entire import statement
+ // Also remove trailing newline if present
+ const nextToken = sourceCode.getTokenAfter(importNode)
+ const importEnd = importNode.range[1]
+ const nextStart = nextToken ? nextToken.range[0] : sourceCode.getText().length
+ const textBetween = sourceCode.getText().substring(importEnd, nextStart)
+ const hasTrailingNewline = /^\s*\n/.test(textBetween)
+
+ if (hasTrailingNewline) {
+ const newlineMatch = textBetween.match(/^\s*\n/)
+ const endRange = importEnd + newlineMatch[0].length
+ yield fixer.removeRange([importNode.range[0], endRange])
+ } else {
+ yield fixer.remove(importNode)
+ }
+ } else {
+ // Remove just the Octicon specifier from the import
+ const previousToken = sourceCode.getTokenBefore(octiconSpecifier)
+ const nextToken = sourceCode.getTokenAfter(octiconSpecifier)
+ const hasTrailingComma = nextToken && nextToken.value === ','
+ const hasLeadingComma = previousToken && previousToken.value === ','
+
+ let rangeToRemove
+ if (hasTrailingComma) {
+ rangeToRemove = [octiconSpecifier.range[0], nextToken.range[1] + 1]
+ } else if (hasLeadingComma) {
+ rangeToRemove = [previousToken.range[0], octiconSpecifier.range[1]]
+ } else {
+ rangeToRemove = [octiconSpecifier.range[0], octiconSpecifier.range[1]]
+ }
+ yield fixer.removeRange(rangeToRemove)
+ }
+ }
+ }
+
// For simple cases, we can provide an autofix
if (iconName) {
context.report({
@@ -117,6 +196,9 @@ module.exports = {
: iconProp.range[0]
yield fixer.removeRange([startPos, iconProp.range[1]])
}
+
+ // Handle import removal if this is the last Octicon usage
+ yield* generateImportFixes(fixer)
},
})
} else if (isConditional) {
@@ -144,6 +226,9 @@ module.exports = {
}
yield fixer.replaceText(node, replacement)
+
+ // Handle import removal if this is the last Octicon usage
+ yield* generateImportFixes(fixer)
},
})
} else if (isMemberExpression) {
@@ -195,6 +280,9 @@ module.exports = {
}
yield fixer.replaceText(node, replacement)
+
+ // Handle import removal if this is the last Octicon usage
+ yield* generateImportFixes(fixer)
},
})
} else {
From ab189c58f3da65ea3e0c3d52aa9a4fcaeac81ad7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 7 Aug 2025 21:51:23 +0000
Subject: [PATCH 5/5] Implement import removal with edge case documented
Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com>
---
src/rules/no-deprecated-octicon.js | 30 +++++++++++++++++-------------
1 file changed, 17 insertions(+), 13 deletions(-)
diff --git a/src/rules/no-deprecated-octicon.js b/src/rules/no-deprecated-octicon.js
index 2f32633..9314e7d 100644
--- a/src/rules/no-deprecated-octicon.js
+++ b/src/rules/no-deprecated-octicon.js
@@ -90,27 +90,31 @@ module.exports = {
const otherProps = openingElement.attributes.filter(attr => attr !== iconProp)
const propsText = otherProps.map(attr => sourceCode.getText(attr)).join(' ')
- // Helper function to determine if this is the last Octicon in the file
- function isLastOcticon() {
- // Get all Octicon elements using the source code
+ // Helper function to determine if this is the last Octicon in the file that needs fixing
+ function isLastOcticonToFix() {
+ // Get all JSX elements in the source code that are Octicons with icon props
const sourceText = sourceCode.getText()
- const octiconMatches = [...sourceText.matchAll(/ {
+ if (line.includes(' match.index > currentNodeStart)
- return laterOcticons.length === 0
+ // Check if this is the last one
+ const currentIndex = octiconLines.indexOf(currentLine)
+ return currentIndex === octiconLines.length - 1
}
// Helper function to generate import fixes if this is the last Octicon usage
function* generateImportFixes(fixer) {
- if (isLastOcticon() && octiconImports.length > 0) {
+ if (isLastOcticonToFix() && octiconImports.length > 0) {
const importNode = octiconImports[0]
const octiconSpecifier = importNode.specifiers.find(
specifier => specifier.imported && specifier.imported.name === 'Octicon',