Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement codemod transform which replaces enum usage with string literal #1639

Merged
merged 13 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/unlucky-crabs-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@channel.io/bezier-codemod": minor
---

Implement codemod transform which replaces enum usage with string literal
36 changes: 36 additions & 0 deletions packages/bezier-codemod/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,39 @@ import { Button, Icon, IconSize } from '@channel.io/bezier-react'

import Foo from './foo'
```

### Enum Member to String Literal

**`enum-member-to-string-literal`**

Replace deprecated enum usage to string literal.

For example:

```tsx
import { ProgressBar, ProgressBarSize, ProgressBarVariant } from '@channel.io/bezier-react'

export default () => (
<ProgressBar
width='100%'
size={ProgressBarSize.M}
variant={ProgressBarVariant.GreenAlt}
value={uploadProgressPercentage / 100}
/>
)
```

Transforms into:

```tsx
import { ProgressBar } from '@channel.io/bezier-react'

export default () => (
<ProgressBar
width='100%'
size='m'
variant='green-alt'
value={uploadProgressPercentage / 100}
/>
)
```
3 changes: 3 additions & 0 deletions packages/bezier-codemod/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from 'ink'

import project from './project.js'
import enumMemberToStringLiteral from './transforms/enum-member-to-string-literal.js'
import iconNameToBezierIcon from './transforms/icon-name-to-bezier-icon.js'
import iconsToBezierIcons from './transforms/icons-to-bezier-icons.js'

Expand All @@ -31,6 +32,7 @@ enum Step {
enum Option {
IconsToBezierIcons = 'icons-to-bezier-icons',
IconNameToBezierIcon = 'icon-name-to-bezier-icon',
EnumMemberToStringLiteral = 'enum-member-to-string-literal',
Exit = 'Exit',
}

Expand All @@ -39,6 +41,7 @@ type TransformName = Exclude<Option, Option.Exit>
const transformMap = {
[Option.IconsToBezierIcons]: iconsToBezierIcons,
[Option.IconNameToBezierIcon]: iconNameToBezierIcon,
[Option.EnumMemberToStringLiteral]: enumMemberToStringLiteral,
}

const options = (Object.keys(transformMap) as Option[]).map((transformName) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
Node,
type SourceFile,
SyntaxKind,
} from 'ts-morph'

type EnumTransforms = Record<string, Record<string, string>>

function transformEnumMemberToStringLiteral(sourceFile: SourceFile, enumTransforms: EnumTransforms) {
const transformedEnumNames: string[] = []

sourceFile.forEachDescendant((node) => {
if (node.isKind(SyntaxKind.PropertyAccessExpression)) {
const firstIdentifier = node.getFirstChildByKind(SyntaxKind.Identifier)
const lastIdentifier = node.getLastChildByKind(SyntaxKind.Identifier)

if (firstIdentifier && lastIdentifier) {
const declarationSymbol = firstIdentifier.getSymbol()
const memberSymbol = lastIdentifier.getSymbol()
const memberValueDeclaration = memberSymbol?.getValueDeclaration()

if (Node.isEnumMember(memberValueDeclaration)) {
const enumName = declarationSymbol?.getName()
const enumMember = memberSymbol?.getName()

if (enumName && enumMember) {
const newEnumMemberValue = enumTransforms[enumName][enumMember]
const ancestor = node.getFirstAncestor()
if (ancestor?.isKind(SyntaxKind.JsxExpression)) {
ancestor.replaceWithText(`'${newEnumMemberValue}'`)
Dogdriip marked this conversation as resolved.
Show resolved Hide resolved
} else {
node.replaceWithText(`'${newEnumMemberValue}'`)
Dogdriip marked this conversation as resolved.
Show resolved Hide resolved
}

transformedEnumNames.push(enumName)
}
}
}
}
})

if (transformedEnumNames.length > 0) {
sourceFile.fixUnusedIdentifiers()
return true
}

return undefined
}

function enumMemberToStringLiteral(sourceFile: SourceFile): true | void {
const enumTransforms: EnumTransforms = {
ProgressBarSize: {
M: 'm',
S: 's',
},
ProgressBarVariant: {
Green: 'green',
GreenAlt: 'green-alt',
Monochrome: 'monochrome',
},
}
return transformEnumMemberToStringLiteral(sourceFile, enumTransforms)
}

export default enumMemberToStringLiteral
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from 'fs'
import path from 'path'

import project from '../../../project.js'
import enumMemberToStringLiteral from '../../enum-member-to-string-literal.js'

describe('enumMemberToStringLiteral', () => {
it('should transform enum members to string literals correctly', () => {
const inputPath = path.join(__dirname, 'fixtures', 'input1.tsx')
const outputPath = path.join(__dirname, 'fixtures', 'output1.tsx')

const inputCode = fs.readFileSync(inputPath, 'utf-8')
const outputCode = fs.readFileSync(outputPath, 'utf-8')
const sourceFile = project.createSourceFile('test.tsx', inputCode, { overwrite: true })

const isMigrated = enumMemberToStringLiteral(sourceFile)

expect(sourceFile.getFullText()).toBe(outputCode)
expect(isMigrated).toBe(true)
})

it('should transform enum members to string literals correctly with type imports', () => {
const inputPath = path.join(__dirname, 'fixtures', 'input2.tsx')
const outputPath = path.join(__dirname, 'fixtures', 'output2.tsx')

const inputCode = fs.readFileSync(inputPath, 'utf-8')
const outputCode = fs.readFileSync(outputPath, 'utf-8')
const sourceFile = project.createSourceFile('test.tsx', inputCode, { overwrite: true })

const isMigrated = enumMemberToStringLiteral(sourceFile)

expect(sourceFile.getFullText()).toBe(outputCode)
expect(isMigrated).toBe(true)
})

it('should transform enum members to string literals correctly with mixed imports', () => {
const inputPath = path.join(__dirname, 'fixtures', 'input3.tsx')
const outputPath = path.join(__dirname, 'fixtures', 'output3.tsx')

const inputCode = fs.readFileSync(inputPath, 'utf-8')
const outputCode = fs.readFileSync(outputPath, 'utf-8')
const sourceFile = project.createSourceFile('test.tsx', inputCode, { overwrite: true })

const isMigrated = enumMemberToStringLiteral(sourceFile)

expect(sourceFile.getFullText()).toBe(outputCode)
expect(isMigrated).toBe(true)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'
import { ProgressBar, ProgressBarSize, ProgressBarVariant } from '@channel.io/bezier-react'

export default function UploadProgress({
uploadProgressPercentage,
}: {
uploadProgressPercentage: number
}) {
return (
<ProgressBar
width='100%'
size={ProgressBarSize.M}
variant={ProgressBarVariant.GreenAlt}
value={uploadProgressPercentage / 100}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import { ProgressBar, type ProgressBarSize, ProgressBarVariant } from '@channel.io/bezier-react'

export default function UploadProgress({
uploadProgressPercentage,
size,
}: {
uploadProgressPercentage: number
size: ProgressBarSize
}) {
return (
<ProgressBar
width='100%'
size={size}
variant={ProgressBarVariant.GreenAlt}
value={uploadProgressPercentage / 100}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react'
import { ProgressBar, ProgressBarSize, ProgressBarVariant } from '@channel.io/bezier-react'

export default function UploadProgress({
uploadProgressPercentage,
variant,
}: {
uploadProgressPercentage: number
variant: ProgressBarVariant
}) {
if (variant === ProgressBarVariant.Monochrome) {
return (
<>
<p>Monochrome ProgressBar</p>
<ProgressBar
width='100%'
size={ProgressBarSize.M}
variant={ProgressBarVariant.Monochrome}
value={uploadProgressPercentage / 100}
/>
</>
)
}

return (
<>
<p>Colored ProgressBar</p>
<ProgressBar
width='100%'
size={ProgressBarSize.M}
variant={variant}
value={uploadProgressPercentage / 100}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'
import { ProgressBar } from '@channel.io/bezier-react'

export default function UploadProgress({
uploadProgressPercentage,
}: {
uploadProgressPercentage: number
}) {
return (
<ProgressBar
width='100%'
size='m'
variant='green-alt'
value={uploadProgressPercentage / 100}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react'
import { ProgressBar, type ProgressBarSize } from '@channel.io/bezier-react'

export default function UploadProgress({
uploadProgressPercentage,
size,
}: {
uploadProgressPercentage: number
size: ProgressBarSize
}) {
return (
<ProgressBar
width='100%'
size={size}
variant='green-alt'
value={uploadProgressPercentage / 100}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react'
import { ProgressBar, ProgressBarVariant } from '@channel.io/bezier-react'

export default function UploadProgress({
uploadProgressPercentage,
variant,
}: {
uploadProgressPercentage: number
variant: ProgressBarVariant
}) {
if (variant === 'monochrome') {
return (
<>
<p>Monochrome ProgressBar</p>
<ProgressBar
width='100%'
size='m'
variant='monochrome'
value={uploadProgressPercentage / 100}
/>
</>
)
}

return (
<>
<p>Colored ProgressBar</p>
<ProgressBar
width='100%'
size='m'
variant={variant}
value={uploadProgressPercentage / 100}
/>
</>
)
}