├── .changeset ├── README.md └── config.json ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── build-test.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── API.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.MD ├── package.json ├── packages ├── .gitignore ├── eslint-codemod-utils │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.MD │ ├── lib │ │ ├── __fixtures__ │ │ │ └── program.ts │ │ ├── __tests__ │ │ │ ├── index.test.ts │ │ │ ├── program.test.ts │ │ │ ├── ts-node.test.ts │ │ │ └── utils.test.ts │ │ ├── constants.ts │ │ ├── globals.d.ts │ │ ├── index.ts │ │ ├── jsx-nodes.ts │ │ ├── jsx-runtime.ts │ │ ├── nodes.ts │ │ ├── ts-nodes.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── closest-of-type.ts │ │ │ ├── get-first-comment-in-file.ts │ │ │ ├── get-identifier-in-parent-scope.ts │ │ │ ├── get-node-after-comment.ts │ │ │ ├── identity.ts │ │ │ ├── index.ts │ │ │ ├── insert-at-start-of-file.ts │ │ │ ├── insert-jsx-attribute.tsx │ │ │ ├── is-node-of-type.ts │ │ │ ├── jsx.ts │ │ │ ├── node.ts │ │ │ └── utils.ts │ ├── package.json │ ├── tsconfig.eslint.json │ ├── tsconfig.esm.json │ └── tsconfig.json └── eslint-plugin-codemod │ ├── CHANGELOG.md │ ├── README.md │ ├── examples │ ├── 01-basic │ │ ├── basic.test.ts │ │ ├── ecu.ts │ │ ├── finder.ts │ │ └── standard.ts │ ├── 02-call-expression │ │ ├── call-expression.test.ts │ │ ├── ecu.ts │ │ ├── finder.ts │ │ └── standard.ts │ └── 03-jsx.ts │ │ ├── ecu.ts │ │ ├── finder.ts │ │ ├── jsx.test.ts │ │ └── standard.ts │ ├── lib │ ├── __tests__ │ │ ├── change-composition.test.ts │ │ ├── no-codemod-comment.test.ts │ │ ├── rename-prop.test.ts │ │ └── sort-imports.test.ts │ ├── hash.ts │ ├── index.ts │ └── rules │ │ ├── change-composition.ts │ │ ├── no-codemod-comment.ts │ │ ├── rename-prop.ts │ │ └── sort-imports.ts │ ├── package.json │ ├── tsconfig.eslint.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── tsconfig.json ├── turbo.json └── vite.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.7.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | parserOptions: { 5 | ecmaVersion: 12, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | jsx: true, 9 | }, 10 | project: ['./packages/*/tsconfig.eslint.json'], 11 | }, 12 | ignorePatterns: ['dist', '*.js', 'vite*'], 13 | extends: [ 14 | 'plugin:@typescript-eslint/recommended', 15 | 'plugin:prettier/recommended', 16 | 'prettier', 17 | ], 18 | plugins: ['codemod', '@typescript-eslint', 'prettier'], 19 | rules: { 20 | 'codemod/jsx/update-prop-name': [ 21 | 'error', 22 | { 23 | source: '@atlaskit/button', 24 | specifier: 'default', 25 | oldProp: 'data-testid', 26 | newProp: 'testId', 27 | }, 28 | ], 29 | 'codemod/sort-imports': 'error', 30 | 'no-console': 'error', 31 | '@typescript-eslint/ban-ts-comment': 'off', 32 | '@typescript-eslint/no-explicit-any': 'off', 33 | '@typescript-eslint/no-unused-vars': 'error', 34 | '@typescript-eslint/no-non-null-assertion': 'error', 35 | 'prettier/prettier': 'error', 36 | }, 37 | overrides: [ 38 | { 39 | files: ['**/*.test.ts'], 40 | env: { 41 | jest: true, 42 | }, 43 | }, 44 | ], 45 | } 46 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: DarkPurple141 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: CI Build & Testing 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | concurrency: 8 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | build: 13 | name: Build & Test 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [18] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: pnpm/action-setup@v2.4.0 21 | with: 22 | version: 7 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'pnpm' 28 | - name: Install dependencies 29 | run: pnpm install 30 | - name: Build 31 | run: pnpm build 32 | - name: Run tests 33 | run: pnpm test:ci 34 | lint: 35 | runs-on: ubuntu-latest 36 | name: Lint 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | 42 | - name: Install pnpm 43 | uses: pnpm/action-setup@v2 44 | with: 45 | version: 7 46 | 47 | - name: Set node version to 18 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version: 18 51 | cache: 'pnpm' 52 | 53 | - name: Install deps 54 | run: pnpm install 55 | 56 | - name: Build 57 | run: pnpm build 58 | 59 | - name: Lint 60 | run: pnpm run lint 61 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | environment: Release 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Install pnpm 17 | uses: pnpm/action-setup@v2 18 | with: 19 | version: 7 20 | - name: Set node version to 18.x 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 18 24 | registry-url: https://registry.npmjs.org/ 25 | - name: Setup npmrc 26 | run: | 27 | cat << EOF > "$HOME/.npmrc" 28 | email=alex.hinds141@gmail.com 29 | //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }} 30 | EOF 31 | env: 32 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | - name: Install dependencies 34 | run: pnpm install 35 | - name: Publish to npm 36 | id: changesets 37 | uses: changesets/action@v1 38 | with: 39 | version: pnpm run version 40 | publish: pnpm publish -r 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | DS_Store 3 | dist 4 | .turbo 5 | coverage 6 | *debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | lib 3 | tsconfig.json -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.18.2 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | pnpm-lock.yaml 3 | *rc$ 4 | coverage -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "printWidth": 80 5 | } 6 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ### Utility Functions 2 | 3 | ````ts 4 | export declare function isNodeOfType( 5 | node: EslintCodemodUtilsBaseNode, 6 | type: T['type'] 7 | ): node is T 8 | export declare function closestOfType( 9 | node: EslintNode, 10 | type: T['type'] 11 | ): EslintNode | null 12 | export declare function hasJSXAttribute( 13 | node: JSXElement, 14 | attributeName: string 15 | ): boolean 16 | export declare function hasJSXChild( 17 | node: JSXElement, 18 | childIdentifier: string 19 | ): boolean 20 | /** 21 | * Whether a declaration does or does not include a specified source. 22 | * 23 | * @param declaration 24 | * @param source 25 | * @returns 26 | */ 27 | export declare function hasImportDeclaration( 28 | declaration: ImportDeclaration, 29 | source: string 30 | ): boolean 31 | /** 32 | * 33 | * @param declaration 34 | * @param specifierId 35 | */ 36 | export declare function hasImportSpecifier( 37 | declaration: ImportDeclaration, 38 | importName: string | 'default' 39 | ): boolean 40 | /** 41 | * Appends or adds an import specifier to an existing import declaration. 42 | * 43 | * Does not validate whether the insertion is already present. 44 | * 45 | * @param declaration 46 | * @param importName 47 | * @param specifierAlias 48 | * @returns {StringableASTNode} 49 | */ 50 | export declare function insertImportSpecifier( 51 | declaration: ImportDeclaration, 52 | importName: string | 'default', 53 | specifierAlias?: string 54 | ): StringableASTNode 55 | /** 56 | * @example 57 | * ```tsx 58 | * insertImportDeclaration('source', ['specifier', 'second']) 59 | * 60 | * // produces 61 | * import { specifier, second } from 'source' 62 | * ``` 63 | * 64 | * @example 65 | * ```tsx 66 | * * insertImportDeclaration('source', ['specifier', { imported: 'second', local: 'other' }]) 67 | * 68 | * // produces 69 | * import { specifier, second as other } from 'source' 70 | * ``` 71 | */ 72 | export declare function insertImportDeclaration( 73 | source: string, 74 | specifiers: ( 75 | | string 76 | | { 77 | local: string 78 | imported: string 79 | } 80 | )[] 81 | ): StringableASTNode 82 | /** 83 | * Removes an import specifier to an existing import declaration. 84 | * 85 | * @param declaration 86 | * @param importName 87 | * @returns {StringableASTNode} 88 | */ 89 | export declare function removeImportSpecifier( 90 | declaration: ImportDeclaration, 91 | importName: string | 'default' 92 | ): StringableASTNode 93 | ```` 94 | 95 | ### Nodes 96 | 97 | ````ts 98 | /** 99 | * __CallExpression__ 100 | * 101 | * @example 102 | * 103 | * Usage 104 | * ``` 105 | * const call = callExpression({ callee: identifier({ name: 'normalCallExpression' }) }) 106 | * ``` 107 | * 108 | * Produces 109 | * 110 | * @example 111 | * 112 | * ```js 113 | * normalCallExpression() 114 | * ``` 115 | * 116 | * @returns {CallExpression} 117 | */ 118 | export declare const callExpression: StringableASTNodeFn 119 | export declare const binaryExpression: StringableASTNodeFn 120 | /** 121 | * __ArrowFunctionExpression__ 122 | * 123 | * @example 124 | * ```js 125 | * const arrow = () => 42 126 | * ⌃⌃⌃⌃⌃⌃⌃⌃ 127 | * ``` 128 | * @returns {estree.ArrowFunctionExpression} 129 | */ 130 | export declare const arrowFunctionExpression: StringableASTNodeFn 131 | export declare const functionExpression: StringableASTNodeFn 132 | export declare const blockStatement: StringableASTNodeFn 133 | export declare const returnStatement: StringableASTNodeFn 134 | /** 135 | * __UnaryExpression__ 136 | * 137 | * @example 138 | * 139 | * ```ts 140 | * const y = typeof x 141 | * ^^^^^^ 142 | * ++x 143 | * ^^ 144 | * ``` 145 | * 146 | * @returns {estree.UnaryExpression} 147 | */ 148 | export declare const unaryExpression: StringableASTNodeFn 149 | /** 150 | * __ThisExpression__ 151 | * 152 | * @example 153 | * 154 | * ```js 155 | * // In `this.self` 'this' is a ThisExpression. 156 | * this.self 157 | * ⌃⌃⌃⌃ 158 | * ``` 159 | * 160 | * @returns {estree.ThisExpression} 161 | */ 162 | export declare const thisExpression: StringableASTNodeFn 163 | export declare const importDefaultSpecifier: StringableASTNodeFn 164 | export declare const exportNamedDeclaration: StringableASTNodeFn 165 | export declare const exportDefaultDeclaration: StringableASTNodeFn 166 | export declare const exportAllDeclaration: StringableASTNodeFn 167 | export declare const exportSpecifier: StringableASTNodeFn 168 | export declare const importSpecifier: StringableASTNodeFn 169 | export declare const yieldExpression: StringableASTNodeFn 170 | export declare const arrayExpression: StringableASTNodeFn 171 | export declare const updateExpression: StringableASTNodeFn 172 | export declare const expressionStatement: StringableASTNodeFn 173 | export declare const newExpression: StringableASTNodeFn 174 | export declare const property: StringableASTNodeFn 175 | /** 176 | * __ObjectPattern__ 177 | * 178 | * @example 179 | * ```ts 180 | * function App({ a }) {} 181 | * ^^^^^ 182 | * ``` 183 | * @returns 184 | */ 185 | export declare const objectPattern: StringableASTNodeFn 186 | /** 187 | * __SpreadElement__ 188 | * 189 | * @example 190 | * ```ts 191 | * const obj = { 192 | * ...spread 193 | * ^^^^^^^^^ 194 | * } 195 | * ``` 196 | * 197 | * @returns {estree.SpreadElement} 198 | */ 199 | export declare const spreadElement: StringableASTNodeFn 200 | export declare const objectExpression: StringableASTNodeFn 201 | export declare const emptyStatement: StringableASTNodeFn 202 | export declare const memberExpression: StringableASTNodeFn 203 | export declare const logicalExpression: StringableASTNodeFn 204 | export declare const variableDeclarator: StringableASTNodeFn 205 | export declare const variableDeclaration: StringableASTNodeFn 206 | export declare const importDeclaration: StringableASTNodeFn 207 | export declare const literal: StringableASTNodeFn 208 | export declare const identifier: StringableASTNodeFn 209 | export declare const whileStatement: StringableASTNodeFn 210 | export declare const switchCase: StringableASTNodeFn 211 | export declare const switchStatement: StringableASTNodeFn 212 | export declare const forStatement: StringableASTNodeFn 213 | export declare const continueStatement: StringableASTNodeFn 214 | export declare const debuggerStatement: StringableASTNodeFn 215 | export declare const conditionalExpression: StringableASTNodeFn 216 | export declare const awaitExpression: StringableASTNodeFn 217 | /** 218 | * __StaticBlock__ 219 | * 220 | * @example 221 | * ```ts 222 | * class A { 223 | * // only applicable inside a class 224 | * static { } 225 | * ^^^^^^^^^^ 226 | * } 227 | * ``` 228 | */ 229 | export declare const staticBlock: StringableASTNodeFn 230 | export declare const functionDeclaration: StringableASTNodeFn 231 | export declare const classDeclaration: StringableASTNodeFn 232 | export declare const classExpression: StringableASTNodeFn 233 | export declare const program: StringableASTNodeFn 234 | ```` 235 | 236 | ### JSX Nodes 237 | 238 | ````ts 239 | /** 240 | * __JSXIdentifier__ 241 | * 242 | * @param param Takes a string or the shape of a {estree.JSXIdentifier} node 243 | * @returns {estree.JSXIdentifier} node 244 | */ 245 | export declare const jsxIdentifier: ( 246 | param: WithoutType | string 247 | ) => StringableASTNode 248 | /** 249 | * __JSXOpeningFragment__ 250 | * 251 | * @example 252 | * ```ts 253 | * <>hello 254 | * ^^ 255 | * ``` 256 | */ 257 | export declare const jsxOpeningFragment: StringableASTNodeFn 258 | /** 259 | * __JSXClosingFragment__ 260 | * 261 | * @example 262 | * ```ts 263 | * <>hello 264 | * ^^ 265 | * ``` 266 | */ 267 | export declare const jsxClosingFragment: StringableASTNodeFn 268 | /** 269 | * __JSXFragment__ 270 | * 271 | * @example 272 | * ```ts 273 | * <>hello 274 | * ^^^^^^^^^^ 275 | * ``` 276 | */ 277 | export declare const jsxFragment: StringableASTNodeFn 278 | export declare const jsxSpreadChild: StringableASTNodeFn 279 | export declare const jsxMemberExpression: StringableASTNodeFn 280 | /** 281 | * __JSXElement__ 282 | * 283 | * @example 284 | * 285 | * Usage 286 | * ``` 287 | * import { jsxElement, jsxOpeningElement, jsxClosingElement, identifier } from 'eslint-codemod-utils' 288 | * 289 | * const modalName = identifier({ name: 'Modal' }) 290 | * const modal = jsxElement({ 291 | * openingElement: jsxOpeningElement({ name: modalName, selfClosing: false }), 292 | * closingElement: jsxClosingElement({ name: modalName }), 293 | * }) 294 | * ``` 295 | * 296 | * @example 297 | * 298 | * Produces 299 | * ```js 300 | * 301 | * ``` 302 | * 303 | * @returns {JSXElement} 304 | */ 305 | export declare const jsxElement: StringableASTNodeFn 306 | /** 307 | * __JSXSpreadAttribute__ 308 | * 309 | * @example Usage 310 | * 311 | * ```js 312 | * import { jsxSpreadAttribute, identifier } from 'eslint-codemod-utils' 313 | * 314 | * const spreadAttr = jsxSpreadAttribute({ 315 | * argument: identifier({ name: 'spread' }) 316 | * }) 317 | * ``` 318 | * @example 319 | * 320 | * ```js 321 | * // Produces a spread attribute 322 | *
323 | * ⌃⌃⌃⌃⌃⌃⌃⌃⌃⌃⌃ 324 | * ``` 325 | * 326 | * @returns {estree.JSXSpreadAttribute} 327 | */ 328 | export declare const jsxSpreadAttribute: StringableASTNodeFn 329 | export declare const jsxOpeningElement: StringableASTNodeFn 330 | /** 331 | * __JSXClosingElement__ 332 | * 333 | * @example 334 | * 335 | * ```js 336 | * // The below jsx div is a closing element. 337 | * // A closing element is expected to match a valid opening element of the same name 338 | *
339 | * ``` 340 | * 341 | * @returns {estree.JSXClosingElement} 342 | */ 343 | export declare const jsxClosingElement: StringableASTNodeFn 344 | /** 345 | * __JSXText__ 346 | * 347 | * @example 348 | * 349 | * ```js 350 | * // In the below jsx, the string, "hello world" is considered JSXText. 351 | * // JSXText can be a any number, boolean, or string value. 352 | *
hello world
353 | * ``` 354 | * 355 | * @returns {estree.JSXText} 356 | */ 357 | export declare const jsxText: StringableASTNodeFn 358 | /** 359 | * __JSXEmptyExpression__ 360 | * 361 | * @example 362 | * 363 | * ```tsx 364 | * 365 | * ^^ 366 | * ``` 367 | * 368 | * @returns {estree.JSXEmptyExpression} 369 | */ 370 | export declare const jsxEmptyExpression: StringableASTNodeFn 371 | /** 372 | * __JSXExpressionContainer__ 373 | * 374 | * @example 375 | * 376 | * ```tsx 377 | * 378 | * ^^^^^^^^^^^ 379 | * ``` 380 | * 381 | * @returns {estree.JSXExpressionContainer} 382 | */ 383 | export declare const jsxExpressionContainer: StringableASTNodeFn 384 | /** 385 | * __JSXAttribute__ 386 | * 387 | * @example 388 | * 389 | * ```js 390 | * // In the below jsx, `a`, `b` and `c` reflect different valid 391 | * // jsx attributes. There values can come in many forms. 392 | *
393 | * ``` 394 | * 395 | * @returns {JSXAttribute} 396 | */ 397 | export declare const jsxAttribute: StringableASTNodeFn 398 | ```` 399 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | alex.hinds141@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Alexander Hinds 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | 7 | Source: http://opensource.org/licenses/ISC 8 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # ESLint Codemod Utilities 2 | 3 | ![brach build status](https://github.com/darkpurple141/eslint-codemod-utils/actions/workflows/build-test.yml/badge.svg?branch=master) 4 | [![npm version](https://img.shields.io/npm/v/eslint-codemod-utils?style=flat-square)](https://www.npmjs.com/package/eslint-codemod-utils) 5 | 6 | The `eslint-codemod-utils` package is a library of helper functions designed to enable code evolution in a similar way to `jscodeshift` - but leaning on the live and ongoing enforcement of `eslint` in your source - rather than one off codemod scripts. It provides first class typescript support and will supercharge your custom eslint rules. 7 | 8 | ## Installation 9 | 10 | ```sh 11 | pnpm add -D eslint-codemod-utils 12 | ``` 13 | 14 | ```sh 15 | yarn add -D eslint-codemod-utils 16 | ``` 17 | 18 | ```sh 19 | npm i --save-dev eslint-codemod-utils 20 | ``` 21 | 22 | ## Getting started 23 | 24 | To create a basic JSX node, you might do something like this: 25 | 26 | ```ts 27 | import { 28 | jsxElement, 29 | jsxOpeningElement, 30 | jsxClosingElement, 31 | identifier, 32 | } from 'eslint-codemod-utils' 33 | 34 | const modalName = identifier({ name: 'Modal' }) 35 | const modal = jsxElement({ 36 | openingElement: jsxOpeningElement({ name: modalName, selfClosing: false }), 37 | closingElement: jsxClosingElement({ name: modalName }), 38 | }) 39 | ``` 40 | 41 | This would produce an `espree` compliant node type that you can **also** nicely stringify to apply your eslint 42 | fixes. For example: 43 | 44 | ```ts 45 | modal.toString() 46 | // produces: 47 | ``` 48 | 49 | The real power of this approach is when combining these utilties with `eslint` rule custom fixe. In these cases, rather than 50 | relying on string manipulation - which can be inexact, hacky or complex to reason about - you can instead focus on only the fix you actually need to affect. 51 | 52 | ### Your first `eslint` codemod 53 | 54 | Writing a codemod is generally broken down into three parts: 55 | 56 | 1. Find 57 | 2. Modify 58 | 3. Remove / Cleanup 59 | 60 | The `eslint` custom rule API allows us to find nodes fairly simply, but how might we modify them? Let's say we're trying to add a new element required to be composed by our Design System's Modal element - a `ModalBody` which is going to 61 | be wrapped by the original `Modal` container. Assuming you've found the right node a normal fix might look like this: 62 | 63 | ```ts 64 | import { Rule } from 'eslint' 65 | 66 | function fix(fixer: Rule.RuleFixer) { 67 | return fixer.replaceText(node, '') 68 | } 69 | ``` 70 | 71 | So for this input: 72 | 73 | ```ts 74 | const MyModal = () => 75 | ``` 76 | 77 | We make this change: 78 | 79 | ```diff 80 | - const MyModal = () => 81 | + const MyModal = () => 82 | ``` 83 | 84 | This kinda works, but the problem is the existing usage of Modal in our codebase is likely (guaranteed!) to be considerably more complex than 85 | this example. 86 | 87 | - If our Modal has props, we need to consider them 88 | - If our Modal has children, we need to consider them 89 | - If our Modal is aliased, we need to consider that 90 | 91 | Instead of relying on string manipulation to reconstruct the existing AST, we instead leverage the information `eslint` is already giving to us. 92 | 93 | ```ts 94 | import * as esUtils from 'eslint-codemod-utils' 95 | import { Rule } from 'eslint' 96 | 97 | // This is slightly more verbose, but it's considerably more robust - 98 | // Simply re-using and spitting out the exisitng AST as a string 99 | function fix(fixer: Rule.RuleFixer) { 100 | const jsxIdentifier = esUtils.jsxIdentifier({ name: 'ModalBody' }) 101 | const modalBodyNode = esUtils.jsxElement({ 102 | openingElement: esUtils.jsxOpeningElement({ name: jsxIdentifier }), 103 | closingElement: esUtils.jsxClosingElement({ name: jsxIdentifier }), 104 | // pass children of original element to new wrapper 105 | children: node.children, 106 | }) 107 | return fixer.replaceText( 108 | node, 109 | esUtils.jsxElement({ ...node, children: [modalBodyNode] }).toString() 110 | ) 111 | } 112 | ``` 113 | 114 | The above will work for the original example: 115 | 116 | ```diff 117 | - const MyModal = () => 118 | + const MyModal = () => 119 | ``` 120 | 121 | But it will also work for: 122 | 123 | ```diff 124 | - const MyModal = () => 125 | + const MyModal = () => 126 | ``` 127 | 128 | Or: 129 | 130 | ```diff 131 | - const MyModal = () => 132 | + const MyModal = () => 133 | ``` 134 | 135 | It's a declarative approach to solve the same problem. 136 | 137 | See the [eslint-plugin-example](packages/eslint-plugin-codemod) for examples of more real world fixes. 138 | 139 | ## How it works 140 | 141 | The library provides a 1-1 mapping of types to utility functions every `espree` node type. These are all lowercase complements to the underlying type they represent; 142 | eg. `jsxIdentifier` produces a `JSXIdentifier` node representation. These nodes all implement their own `toString`. This means any string cast will recursively produce the correct string output for any valid `espree` AST. 143 | 144 | Each helper takes in a valid `espree` node and spits out an augmented one that can be more easily stringified. See -> [API](API.md) for more. 145 | 146 | ## Motivation 147 | 148 | This idea came about after wrestling with the limitations of `eslint` rule fixes. For context, `eslint` rules rely heavily on string based utilities to apply 149 | fixes to code. For example this fix which appends a semi-colon to a `Literal` (from the `eslint` documentation website itself): 150 | 151 | ```js 152 | context.report({ 153 | node: node, 154 | message: 'Missing semicolon', 155 | fix: function (fixer) { 156 | return fixer.insertTextAfter(node, ';') 157 | }, 158 | }) 159 | ``` 160 | 161 | This works fine if your fixes are trivial, but it works less well for more complex uses cases. As soon as you need to traverse other AST nodes and combine information for a fix, combine fixes; the simplicity of the `RuleFixer` API starts to buckle. 162 | 163 | In codemod tools like [jscodeshift](https://github.com/facebook/jscodeshift), the AST is baked in to the way fixes are applied - rather than applying fixes your script needs to return a collection of AST nodes which are then parsed and integrated into the source. This is a little more heavy duty but it also is more resillient. 164 | 165 | The missing piece for `ESlint` is a matching set of utilties to allow the flexibility to dive into the AST approach where and when a developer feels it is appropriate. 166 | This library aims to bridge some of that gap and with some different thinking around just how powerful `ESLint` can be. 167 | 168 | Fixes can then theoretically deal with more complex use cases like this: 169 | 170 | ```ts 171 | /** 172 | * This is part of a fix to demonstrate changing a prop in a specific element with 173 | * a much more surgical approach to node manipulation. 174 | */ 175 | import { 176 | jsxOpeningElement, 177 | jsxAttribute, 178 | jsxIdentifier, 179 | } from 'eslint-codemod-utils' 180 | 181 | // ... further down the file 182 | context.report({ 183 | node: node, 184 | message: 'error', 185 | fix(fixer) { 186 | // The variables 'fixed' works with the espree AST to create 187 | // its own representation which can easily be stringified 188 | const fixed = jsxOpeningElement({ 189 | name: node.name, 190 | selfClosing: node.selfClosing, 191 | attributes: node.attributes.map((attr) => { 192 | if (attr.type === 'JSXAttribute' && attr.name.name === 'open') { 193 | const internal = jsxAttribute({ 194 | // espree nodes are spread into the util with no issues 195 | ...attr, 196 | // others are recreated or re-mapped 197 | name: jsxIdentifier({ 198 | ...attr.name, 199 | name: 'isOpen', 200 | }), 201 | }) 202 | return internal 203 | } 204 | 205 | return attr 206 | }), 207 | }) 208 | 209 | return fixer.replaceText(node, fixed.toString()) 210 | }, 211 | }) 212 | ``` 213 | 214 | ## Similar projects 215 | 216 | - AST Types [https://github.com/benjamn/ast-types](https://github.com/benjamn/ast-types) 217 | - Codeshift Community [https://www.codeshiftcommunity.com/](https://www.codeshiftcommunity.com/) 218 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorepo", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "turbo run build", 7 | "test": "turbo run test", 8 | "test:watch": "vitest", 9 | "lint": "eslint --ext .ts,.tsx packages/**", 10 | "check:format": "prettier packages/** --list-different", 11 | "test:ci": "vitest --run", 12 | "coverage": "vitest run --coverage", 13 | "typecheck": "tsc --noEmit", 14 | "version": "pnpm changeset version && pnpm changeset tag" 15 | }, 16 | "author": "Alex Hinds", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "@changesets/cli": "^2.25.2", 20 | "@types/eslint": "^8.4.10", 21 | "@types/estree-jsx": "^1.0.0", 22 | "@types/node": "^18.0.0", 23 | "@types/ws": "^8.5.3", 24 | "@typescript-eslint/eslint-plugin": "^5.45.0", 25 | "@typescript-eslint/parser": "^5.45.0", 26 | "c8": "^7.12.0", 27 | "eslint": "^7.32.0", 28 | "eslint-config-prettier": "^8.5.0", 29 | "eslint-plugin-codemod": "workspace:*", 30 | "eslint-plugin-prettier": "^4.2.1", 31 | "prettier": "^2.8.0", 32 | "turbo": "^1.10.0", 33 | "typescript": "^4.9.3", 34 | "vite": "4.3.9", 35 | "vitest": "latest" 36 | }, 37 | "engines": { 38 | "node": ">=16" 39 | }, 40 | "packageManager": "pnpm@7.33.1" 41 | } 42 | -------------------------------------------------------------------------------- /packages/.gitignore: -------------------------------------------------------------------------------- 1 | misc* -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # eslint-codemod-utils 2 | 3 | ## 1.9.0 4 | 5 | ### Minor Changes 6 | 7 | - 6dbd9a4: Introduces support for satisfies expression. 8 | - 8626b1d: Adds additional TS node type; `TSArrayType`. 9 | 10 | ## 1.8.7 11 | 12 | ### Patch Changes 13 | 14 | - 0839e66: Corrects the output of certain specific TS operator nodes. 15 | 16 | ## 1.8.6 17 | 18 | ### Patch Changes 19 | 20 | - 51662bd: Fix for issue with computed properties not being implemented correctly. 21 | 22 | ## 1.8.5 23 | 24 | ### Patch Changes 25 | 26 | - dcb2602: Revert TS type integration 27 | 28 | ## 1.8.4 29 | 30 | ### Patch Changes 31 | 32 | - dd2ee38: Adds `TSIntersectionType` `TSUnionType` `TSTypeAliasDeclaration` `TSTypeParameterDeclaration` `TSTypeParameter` 33 | 34 | ## 1.8.3 35 | 36 | ### Patch Changes 37 | 38 | - ad47185: Adds missing support for TS keywords `unknown`, `readonly`, `boolean`. 39 | 40 | ## 1.8.2 41 | 42 | ### Patch Changes 43 | 44 | - 3742023: Fixes missing support for TSNonNullExpression. 45 | - 3742023: Updates AST logic to account for additional valid typescript nodes. 46 | 47 | ## 1.8.1 48 | 49 | ### Patch Changes 50 | 51 | - 1c195ad: Updates AST logic to account for additional valid typescript nodes. 52 | 53 | ## 1.8.0 54 | 55 | ### Minor Changes 56 | 57 | - 07e3002: Export all types from estree-jsx 58 | 59 | ### Patch Changes 60 | 61 | - 130388a: Patch for utils. 62 | 63 | ## 1.7.0 64 | 65 | ### Minor Changes 66 | 67 | - e1edbc8: Updates to internal types. 68 | 69 | ## 1.6.3 70 | 71 | ### Patch Changes 72 | 73 | - b8bb315: Test deploy 74 | 75 | ## 1.6.2 76 | 77 | ### Patch Changes 78 | 79 | - 5b0b8d1: Amend publish script and versions. 80 | 81 | ## 1.6.1 82 | 83 | ### Patch Changes 84 | 85 | - 1d848e1: Amend build configuration - test deployment. 86 | 87 | ## 1.6.0 88 | 89 | ### Minor Changes 90 | 91 | - 24bca60: Add experimental jsx runtime to support jsx in fixers. 92 | - 1eaa1d7: Improves type behavior of AST functions to be more affording to optional properites. 93 | 94 | ## 1.5.1 95 | 96 | ### Patch Changes 97 | 98 | - 590ec6f: Fixed the resolution of the `Super` and `RestElement` nodes which were being ignored previously in certain cases. 99 | 100 | ## 1.5.0 101 | 102 | ### Minor Changes 103 | 104 | - dddc134: Updates codemod utils to support a small subset of typescript specific node types. 105 | 106 | This minor also introduces: 107 | 108 | - improved types 109 | - improved jsdocs 110 | - updates to dependencies 111 | 112 | ## 1.4.0 113 | 114 | ### Minor Changes 115 | 116 | - ca51ad9: Adds an additional common utility getIdentifierInParentScope() to find elements above the current node in scope. 117 | 118 | ## 1.3.4 119 | 120 | ### Patch Changes 121 | 122 | - f003b75: Fixes the parsing of computed properties on the `memberExpression` transformer. 123 | 124 | ## 1.3.3 125 | 126 | ### Patch Changes 127 | 128 | - ea8a017: Fixes the way unaryExpressions were stringified. 129 | 130 | ## 1.3.2 131 | 132 | ### Patch Changes 133 | 134 | - 1a18bf6: Updates to correct type coercion in `isNodeOfType` 135 | 136 | ## 1.3.1 137 | 138 | ### Patch Changes 139 | 140 | - 8cb50d3: Corrects the type inference of the `closestOfType` utility function. 141 | 142 | ## 1.3.0 143 | 144 | ### Minor Changes 145 | 146 | - 75d4cef: Adds files key in package.json. 147 | 148 | ## 1.2.1 149 | 150 | ### Patch Changes 151 | 152 | - 2e222e0: Removes src from being published in dist which was bloating bundle. 153 | 154 | ## 1.2.0 155 | 156 | ### Minor Changes 157 | 158 | - cd793bb: Correct minor as previous change didn't export new functionality. 159 | 160 | ### Patch Changes 161 | 162 | - e030ba2: Updates type to be correctly inferred in `isNodeOfType` 163 | 164 | ## 1.1.0 165 | 166 | ### Minor Changes 167 | 168 | - 8913da9: Adds additional common utilties for codemod specific transforms. 169 | 170 | ## 1.0.1 171 | 172 | ### Patch Changes 173 | 174 | - 064d923: Fixes an issue with the types not being included in the package for some functions. 175 | - 064d923: Fixes the behaviour of some of the utils when interacting with the default import of an ImportDeclaration. 176 | 177 | ## 1.0.0 178 | 179 | ### Major Changes 180 | 181 | - 0876a8d: Initial stable release. Additionally adds additional util `hasImportSpecifier`. 182 | 183 | ### Patch Changes 184 | 185 | - 41f7c0f: Fixes build target to better match desired compatibility across older node / typescript versions. 186 | 187 | ## 0.1.3 188 | 189 | ### Patch Changes 190 | 191 | - dd41354: Updates literal and identifiers to support primitive types being passed directly as arguments to AST utility functions. 192 | 193 | ## 0.1.2 194 | 195 | ### Patch Changes 196 | 197 | - d75cbdd: Removes console statement from node parsing utility. 198 | 199 | ## 0.1.1 200 | 201 | ### Patch Changes 202 | 203 | - ba82178: Adds additional test cases, further AST node types (WithStatement, IfStatement, ThrowStatement). 204 | - fbd92dd: Adds CatchClause, TryStatement, DoWhileStatement, ForInStatement, ForOfStatement, ArrayPattern support. 205 | 206 | ## 0.1.0 207 | 208 | ### Minor Changes 209 | 210 | - 5716178: Improves documentation and removes WIP status of a number of docs / types. 211 | 212 | ### Patch Changes 213 | 214 | - 3eb841a: Adds additional utility functions. 215 | 216 | ## 0.0.7 217 | 218 | ### Patch Changes 219 | 220 | - cf5df6a: Implements estree.WhileStatement & estree.BreakStatement 221 | 222 | ## 0.0.6 223 | 224 | ### Patch Changes 225 | 226 | - 8d31804: Adds MethodDefinition, ClassDefinition 227 | 228 | ## 0.0.5 229 | 230 | ### Patch Changes 231 | 232 | - b257d6d: Further API implementation to match the estree spec. Fixed a number of bugs with existing implementations. 233 | - 6e67759: Updates the documentation to match the updated extended API. 234 | 235 | ## 0.0.4 236 | 237 | ### Patch Changes 238 | 239 | - c7e2c68: Adds additional node types. 240 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, Alexander Hinds 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | 7 | Source: http://opensource.org/licenses/ISC 8 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/README.MD: -------------------------------------------------------------------------------- 1 | # ESLint Codemod Utilities 2 | 3 | ![brach build status](https://github.com/darkpurple141/eslint-codemod-utils/actions/workflows/build-test.yml/badge.svg?branch=master) 4 | [![npm version](https://img.shields.io/npm/v/eslint-codemod-utils?style=flat-square)](https://www.npmjs.com/package/eslint-codemod-utils) 5 | 6 | The `eslint-codemod-utils` package is a library of helper functions designed to enable code evolution in a similar way to `jscodeshift` - but leaning on the live and ongoing enforcement of `eslint` in your source - rather than one off codemod scripts. It provides first class typescript support and will supercharge your custom eslint rules. 7 | 8 | ## Installation 9 | 10 | ```sh 11 | pnpm add -D eslint-codemod-utils 12 | ``` 13 | 14 | ```sh 15 | yarn add -D eslint-codemod-utils 16 | ``` 17 | 18 | ```sh 19 | npm i --save-dev eslint-codemod-utils 20 | ``` 21 | 22 | ## Getting started 23 | 24 | To create a basic JSX node, you might do something like this: 25 | 26 | ```ts 27 | import { 28 | jsxElement, 29 | jsxOpeningElement, 30 | jsxClosingElement, 31 | identifier, 32 | } from 'eslint-codemod-utils' 33 | 34 | const modalName = identifier({ name: 'Modal' }) 35 | const modal = jsxElement({ 36 | openingElement: jsxOpeningElement({ name: modalName, selfClosing: false }), 37 | closingElement: jsxClosingElement({ name: modalName }), 38 | }) 39 | ``` 40 | 41 | This would produce an `espree` compliant node type that you can **also** nicely stringify to apply your eslint 42 | fixes. For example: 43 | 44 | ```ts 45 | modal.toString() 46 | // produces: 47 | ``` 48 | 49 | The real power of this approach is when combining these utilties with `eslint` rule custom fixe. In these cases, rather than 50 | relying on string manipulation - which can be inexact, hacky or complex to reason about - you can instead focus on only the fix you actually need to affect. 51 | 52 | ### Your first `eslint` codemod 53 | 54 | Writing a codemod is generally broken down into three parts: 55 | 56 | 1. Find 57 | 2. Modify 58 | 3. Remove / Cleanup 59 | 60 | The `eslint` custom rule API allows us to find nodes fairly simply, but how might we modify them? Let's say we're trying to add a new element required to be composed by our Design System's Modal element - a `ModalBody` which is going to 61 | be wrapped by the original `Modal` container. Assuming you've found the right node a normal fix might look like this: 62 | 63 | ```ts 64 | import { Rule } from 'eslint' 65 | 66 | function fix(fixer: Rule.RuleFixer) { 67 | return fixer.replaceText(node, '') 68 | } 69 | ``` 70 | 71 | So for this input: 72 | 73 | ```ts 74 | const MyModal = () => 75 | ``` 76 | 77 | We make this change: 78 | 79 | ```diff 80 | - const MyModal = () => 81 | + const MyModal = () => 82 | ``` 83 | 84 | This kinda works, but the problem is the existing usage of Modal in our codebase is likely (guaranteed!) to be considerably more complex than 85 | this example. 86 | 87 | - If our Modal has props, we need to consider them 88 | - If our Modal has children, we need to consider them 89 | - If our Modal is aliased, we need to consider that 90 | 91 | Instead of relying on string manipulation to reconstruct the existing AST, we instead leverage the information `eslint` is already giving to us. 92 | 93 | ```ts 94 | import * as esUtils from 'eslint-codemod-utils' 95 | import { Rule } from 'eslint' 96 | 97 | // This is slightly more verbose, but it's considerably more robust - 98 | // Simply re-using and spitting out the exisitng AST as a string 99 | function fix(fixer: Rule.RuleFixer) { 100 | const jsxIdentifier = esUtils.jsxIdentifier({ name: 'ModalBody' }) 101 | const modalBodyNode = esUtils.jsxElement({ 102 | openingElement: esUtils.jsxOpeningElement({ name: jsxIdentifier }), 103 | closingElement: esUtils.jsxClosingElement({ name: jsxIdentifier }), 104 | // pass children of original element to new wrapper 105 | children: node.children, 106 | }) 107 | return fixer.replaceText( 108 | node, 109 | esUtils.jsxElement({ ...node, children: [modalBodyNode] }).toString() 110 | ) 111 | } 112 | ``` 113 | 114 | The above will work for the original example: 115 | 116 | ```diff 117 | - const MyModal = () => 118 | + const MyModal = () => 119 | ``` 120 | 121 | But it will also work for: 122 | 123 | ```diff 124 | - const MyModal = () => 125 | + const MyModal = () => 126 | ``` 127 | 128 | Or: 129 | 130 | ```diff 131 | - const MyModal = () => 132 | + const MyModal = () => 133 | ``` 134 | 135 | It's a declarative approach to solve the same problem. 136 | 137 | See the [eslint-plugin-example](packages/eslint-plugin-codemod) for examples of more real world fixes. 138 | 139 | ## How it works 140 | 141 | The library provides a 1-1 mapping of types to utility functions every `espree` node type. These are all lowercase complements to the underlying type they represent; 142 | eg. `jsxIdentifier` produces a `JSXIdentifier` node representation. These nodes all implement their own `toString`. This means any string cast will recursively produce the correct string output for any valid `espree` AST. 143 | 144 | Each helper takes in a valid `espree` node and spits out an augmented one that can be more easily stringified. See -> [API](API.md) for more. 145 | 146 | ## Motivation 147 | 148 | This idea came about after wrestling with the limitations of `eslint` rule fixes. For context, `eslint` rules rely heavily on string based utilities to apply 149 | fixes to code. For example this fix which appends a semi-colon to a `Literal` (from the `eslint` documentation website itself): 150 | 151 | ```js 152 | context.report({ 153 | node: node, 154 | message: 'Missing semicolon', 155 | fix: function (fixer) { 156 | return fixer.insertTextAfter(node, ';') 157 | }, 158 | }) 159 | ``` 160 | 161 | This works fine if your fixes are trivial, but it works less well for more complex uses cases. As soon as you need to traverse other AST nodes and combine information for a fix, combine fixes; the simplicity of the `RuleFixer` API starts to buckle. 162 | 163 | In codemod tools like [jscodeshift](https://github.com/facebook/jscodeshift), the AST is baked in to the way fixes are applied - rather than applying fixes your script needs to return a collection of AST nodes which are then parsed and integrated into the source. This is a little more heavy duty but it also is more resillient. 164 | 165 | The missing piece for `ESlint` is a matching set of utilties to allow the flexibility to dive into the AST approach where and when a developer feels it is appropriate. 166 | This library aims to bridge some of that gap and with some different thinking around just how powerful `ESLint` can be. 167 | 168 | Fixes can then theoretically deal with more complex use cases like this: 169 | 170 | ```ts 171 | /** 172 | * This is part of a fix to demonstrate changing a prop in a specific element with 173 | * a much more surgical approach to node manipulation. 174 | */ 175 | import { 176 | jsxOpeningElement, 177 | jsxAttribute, 178 | jsxIdentifier, 179 | } from 'eslint-codemod-utils' 180 | 181 | // ... further down the file 182 | context.report({ 183 | node: node, 184 | message: 'error', 185 | fix(fixer) { 186 | // The variables 'fixed' works with the espree AST to create 187 | // its own representation which can easily be stringified 188 | const fixed = jsxOpeningElement({ 189 | name: node.name, 190 | selfClosing: node.selfClosing, 191 | attributes: node.attributes.map((attr) => { 192 | if (attr.type === 'JSXAttribute' && attr.name.name === 'open') { 193 | const internal = jsxAttribute({ 194 | // espree nodes are spread into the util with no issues 195 | ...attr, 196 | // others are recreated or re-mapped 197 | name: jsxIdentifier({ 198 | ...attr.name, 199 | name: 'isOpen', 200 | }), 201 | }) 202 | return internal 203 | } 204 | 205 | return attr 206 | }), 207 | }) 208 | 209 | return fixer.replaceText(node, fixed.toString()) 210 | }, 211 | }) 212 | ``` 213 | 214 | ## Similar projects 215 | 216 | - AST Types [https://github.com/benjamn/ast-types](https://github.com/benjamn/ast-types) 217 | - Codeshift Community [https://www.codeshiftcommunity.com/](https://www.codeshiftcommunity.com/) 218 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | callExpression, 3 | comment, 4 | doWhileStatement, 5 | exportAllDeclaration, 6 | identifier, 7 | ifStatement, 8 | importDeclaration, 9 | importDefaultSpecifier, 10 | importSpecifier, 11 | jsxAttribute, 12 | jsxClosingElement, 13 | jsxElement, 14 | jsxExpressionContainer, 15 | jsxIdentifier, 16 | jsxMemberExpression, 17 | jsxOpeningElement, 18 | jsxSpreadAttribute, 19 | jsxText, 20 | literal, 21 | memberExpression, 22 | node as nodeFn, 23 | objectExpression, 24 | property, 25 | staticBlock, 26 | throwStatement, 27 | unaryExpression, 28 | variableDeclaration, 29 | } from '..' 30 | 31 | import * as espree from 'espree' 32 | 33 | const ESPREE_OPTIONS = { 34 | ecmaVersion: 2015, 35 | sourceType: 'module', 36 | } 37 | 38 | describe('literal', () => { 39 | test('string', () => { 40 | expect(String(literal('hello'))).eq('hello') 41 | }) 42 | 43 | test('boolean', () => { 44 | expect(String(literal(true))).eq(`true`) 45 | }) 46 | 47 | test('null', () => { 48 | expect(String(literal(null))).eq(`null`) 49 | }) 50 | 51 | test('number', () => { 52 | expect(String(literal(9))).eq(`9`) 53 | }) 54 | }) 55 | 56 | describe('exportAllDeclaration', () => { 57 | test('basic', () => { 58 | expect( 59 | String( 60 | exportAllDeclaration({ 61 | exported: null, 62 | source: literal('@atlaskit/modal-dialog'), 63 | }) 64 | ) 65 | ).eq(`export * from '@atlaskit/modal-dialog'`) 66 | }) 67 | 68 | test('with alias', () => { 69 | expect( 70 | String( 71 | exportAllDeclaration({ 72 | exported: identifier('modal'), 73 | source: literal('@atlaskit/modal-dialog'), 74 | }) 75 | ) 76 | ).eq(`export * as modal from '@atlaskit/modal-dialog'`) 77 | }) 78 | }) 79 | 80 | describe('importDeclaration', () => { 81 | test('basic', () => { 82 | expect( 83 | String( 84 | importDeclaration({ 85 | specifiers: [], 86 | source: literal({ value: '@atlaskit/modal-dialog' }), 87 | }) 88 | ) 89 | ).eq(`import '@atlaskit/modal-dialog'`) 90 | }) 91 | 92 | test('basic - espress', () => { 93 | const { body } = espree.parse( 94 | `import '@atlaskit/modal-dialog'`, 95 | ESPREE_OPTIONS 96 | ) 97 | expect(importDeclaration(body[0]).toString()).eq( 98 | `import '@atlaskit/modal-dialog'` 99 | ) 100 | }) 101 | 102 | test('basic named import', () => { 103 | expect( 104 | String( 105 | importDeclaration({ 106 | specifiers: [ 107 | importSpecifier({ 108 | imported: identifier({ name: 'Hello' }), 109 | local: identifier({ name: 'Hello' }), 110 | }), 111 | ], 112 | source: literal({ value: '@atlaskit/modal-dialog' }), 113 | }) 114 | ) 115 | ).eq(`import { Hello } from '@atlaskit/modal-dialog'`) 116 | }) 117 | 118 | test('basic default import', () => { 119 | expect( 120 | String( 121 | importDeclaration({ 122 | specifiers: [ 123 | importDefaultSpecifier({ 124 | local: identifier({ name: 'Hello' }), 125 | }), 126 | ], 127 | source: literal({ value: '@atlaskit/modal-dialog' }), 128 | }) 129 | ) 130 | ).eq(`import Hello from '@atlaskit/modal-dialog'`) 131 | }) 132 | 133 | test('basic default + named imports', () => { 134 | expect( 135 | String( 136 | importDeclaration({ 137 | specifiers: [ 138 | importDefaultSpecifier({ 139 | local: identifier({ name: 'Hello' }), 140 | }), 141 | importSpecifier({ 142 | imported: identifier({ name: 'Bongo' }), 143 | local: identifier({ name: 'Bongo' }), 144 | }), 145 | ], 146 | source: literal({ value: '@atlaskit/modal-dialog' }), 147 | }) 148 | ) 149 | ).eq(`import Hello, { Bongo } from '@atlaskit/modal-dialog'`) 150 | }) 151 | 152 | test('multiple named imports', () => { 153 | expect( 154 | String( 155 | importDeclaration({ 156 | specifiers: [ 157 | importSpecifier({ 158 | imported: identifier({ name: 'Bongo' }), 159 | local: identifier({ name: 'Bongo' }), 160 | }), 161 | importSpecifier({ 162 | imported: identifier({ name: 'Congo' }), 163 | local: identifier({ name: 'Congo' }), 164 | }), 165 | importSpecifier({ 166 | imported: identifier({ name: 'Jongo' }), 167 | local: identifier({ name: 'Jongo' }), 168 | }), 169 | ], 170 | source: literal('@atlaskit/modal-dialog'), 171 | }) 172 | ) 173 | ).eq(`import { Bongo, Congo, Jongo } from '@atlaskit/modal-dialog'`) 174 | }) 175 | 176 | test('with aliasing', () => { 177 | expect( 178 | importDeclaration({ 179 | specifiers: [ 180 | importSpecifier({ 181 | imported: identifier({ name: 'Bongo' }), 182 | local: identifier({ name: 'BongoMan' }), 183 | }), 184 | importSpecifier({ 185 | imported: identifier({ name: 'Congo' }), 186 | local: identifier({ name: 'CongoMan' }), 187 | }), 188 | importSpecifier({ 189 | imported: identifier({ name: 'Jongo' }), 190 | local: identifier({ name: 'JongoGirl' }), 191 | }), 192 | ], 193 | source: literal({ value: '@atlaskit/modal-dialog' }), 194 | }).toString() 195 | ).eq( 196 | `import { Bongo as BongoMan, Congo as CongoMan, Jongo as JongoGirl } from '@atlaskit/modal-dialog'` 197 | ) 198 | }) 199 | }) 200 | 201 | describe('jsxClosingElement', () => { 202 | test('basic', () => { 203 | expect( 204 | jsxClosingElement({ 205 | name: jsxIdentifier({ name: 'Modal' }), 206 | }).toString() 207 | ).eq(``) 208 | }) 209 | 210 | test('with member expression', () => { 211 | expect( 212 | String( 213 | jsxClosingElement({ 214 | name: jsxMemberExpression({ 215 | object: jsxIdentifier({ name: 'AK' }), 216 | property: jsxIdentifier({ name: 'Modal' }), 217 | }), 218 | }) 219 | ) 220 | ).eq(``) 221 | }) 222 | }) 223 | 224 | describe('jsxMemberExpression', () => { 225 | test('basic', () => { 226 | expect( 227 | jsxMemberExpression({ 228 | object: jsxIdentifier({ name: 'AK' }), 229 | property: jsxIdentifier({ name: 'Modal' }), 230 | }).toString() 231 | ).eq('AK.Modal') 232 | }) 233 | }) 234 | 235 | describe('unaryExpression', () => { 236 | test('basic', () => { 237 | expect( 238 | unaryExpression({ 239 | operator: 'typeof', 240 | argument: identifier('x'), 241 | prefix: true, 242 | }).toString() 243 | ).eq('typeof x') 244 | }) 245 | }) 246 | 247 | describe('objectExpression', () => { 248 | test('basic', () => { 249 | expect( 250 | objectExpression({ 251 | properties: [ 252 | property({ 253 | key: identifier('hello'), 254 | value: identifier('world'), 255 | }), 256 | ], 257 | }).toString() 258 | ).eq('{\n hello: world\n}') 259 | }) 260 | 261 | test('via the parser', () => { 262 | expect( 263 | nodeFn( 264 | espree.parse( 265 | `const y = {\nhello: 'world',\n [thing]: 'bro',\n [thing]() {},\n [thing]: () => {},\n get x() {}\n}`, 266 | ESPREE_OPTIONS 267 | ).body[0] 268 | ).toString() 269 | ).eq( 270 | `const y = {\n hello: 'world',\n [thing]: 'bro',\n [thing]: function () {},\n [thing]: () => {},\n get x() {}\n}` 271 | ) 272 | }) 273 | }) 274 | 275 | describe('memberExpression', () => { 276 | test('basic', () => { 277 | expect( 278 | memberExpression({ 279 | object: identifier('hello'), 280 | property: identifier('x'), 281 | computed: false, 282 | optional: false, 283 | }).toString() 284 | ).eq('hello.x') 285 | }) 286 | 287 | test('computed', () => { 288 | expect( 289 | memberExpression({ 290 | object: identifier('hello'), 291 | property: identifier('x'), 292 | computed: true, 293 | }).toString() 294 | ).eq('hello[x]') 295 | }) 296 | }) 297 | 298 | describe('jsxSpeadAttribute', () => { 299 | test('basic', () => { 300 | expect( 301 | jsxSpreadAttribute({ 302 | argument: identifier({ name: 'spread' }), 303 | }).toString() 304 | ).eq('{...spread}') 305 | }) 306 | 307 | test('callExpression', () => { 308 | expect( 309 | jsxSpreadAttribute({ 310 | argument: callExpression({ 311 | callee: identifier({ name: 'spread' }), 312 | arguments: [], 313 | }), 314 | }).toString() 315 | ).eq('{...spread()}') 316 | }) 317 | 318 | test('optional callExpression', () => { 319 | expect( 320 | jsxSpreadAttribute({ 321 | argument: callExpression({ 322 | callee: identifier({ name: 'spread' }), 323 | arguments: [], 324 | optional: true, 325 | }), 326 | }).toString() 327 | ).eq('{...spread?.()}') 328 | }) 329 | }) 330 | 331 | describe('jsxElement', () => { 332 | test('basic', () => { 333 | expect( 334 | '\n' + 335 | String( 336 | jsxElement({ 337 | openingElement: jsxOpeningElement({ 338 | name: jsxIdentifier({ name: 'Modal' }), 339 | }), 340 | closingElement: jsxClosingElement({ 341 | name: jsxIdentifier({ name: 'Modal' }), 342 | }), 343 | children: [ 344 | jsxExpressionContainer({ 345 | expression: identifier({ name: 'hello' }), 346 | }), 347 | ], 348 | }) 349 | ) 350 | ).eq(` 351 | 352 | {hello} 353 | `) 354 | }) 355 | 356 | test('with attributes', () => { 357 | expect( 358 | '\n' + 359 | String( 360 | jsxElement({ 361 | openingElement: jsxOpeningElement({ 362 | selfClosing: false, 363 | attributes: [ 364 | jsxAttribute({ 365 | name: jsxIdentifier({ name: 'isOpen' }), 366 | value: jsxExpressionContainer({ 367 | expression: literal({ value: true }), 368 | }), 369 | }), 370 | jsxAttribute({ 371 | name: jsxIdentifier({ name: 'onClick' }), 372 | value: jsxExpressionContainer({ 373 | expression: identifier({ name: 'onClick' }), 374 | }), 375 | }), 376 | ], 377 | name: jsxIdentifier({ name: 'Modal' }), 378 | }), 379 | closingElement: jsxClosingElement({ 380 | name: jsxIdentifier({ name: 'Modal' }), 381 | }), 382 | children: [ 383 | jsxExpressionContainer({ 384 | expression: identifier({ name: 'hello' }), 385 | }), 386 | ], 387 | }) 388 | ) 389 | ).eq(` 390 | 391 | {hello} 392 | `) 393 | }) 394 | 395 | test('multiple children', () => { 396 | expect( 397 | '\n' + 398 | String( 399 | jsxElement({ 400 | openingElement: jsxOpeningElement({ 401 | attributes: [], 402 | name: jsxIdentifier({ name: 'Modal' }), 403 | }), 404 | closingElement: jsxClosingElement({ 405 | name: jsxIdentifier({ name: 'Modal' }), 406 | }), 407 | children: [ 408 | jsxExpressionContainer({ 409 | expression: identifier({ name: 'hello' }), 410 | }), 411 | jsxElement({ 412 | openingElement: jsxOpeningElement({ 413 | selfClosing: true, 414 | attributes: [], 415 | name: jsxIdentifier({ name: 'BadPeople' }), 416 | }), 417 | children: [], 418 | closingElement: null, 419 | }), 420 | ], 421 | }) 422 | ) 423 | ).eq(` 424 | 425 | {hello} 426 | 427 | `) 428 | }) 429 | test('nested children', () => { 430 | expect( 431 | '\n' + 432 | String( 433 | jsxElement({ 434 | openingElement: jsxOpeningElement({ 435 | attributes: [], 436 | name: jsxIdentifier({ name: 'Modal' }), 437 | }), 438 | closingElement: jsxClosingElement({ 439 | name: jsxIdentifier({ name: 'Modal' }), 440 | }), 441 | children: [ 442 | jsxExpressionContainer({ 443 | expression: identifier({ name: 'hello' }), 444 | }), 445 | jsxElement({ 446 | loc: { 447 | start: { column: 2, line: 0 }, 448 | end: { column: 10, line: 0 }, 449 | }, 450 | openingElement: jsxOpeningElement({ 451 | attributes: [], 452 | name: jsxIdentifier({ name: 'BadPeople' }), 453 | }), 454 | children: [ 455 | jsxElement({ 456 | // @ts-expect-error 457 | loc: { start: { column: 4 } }, 458 | children: [jsxText({ value: 'Hi', raw: '"Hi"' })], 459 | closingElement: jsxClosingElement({ 460 | name: jsxIdentifier({ name: 'VeryNested' }), 461 | }), 462 | openingElement: jsxOpeningElement({ 463 | attributes: [], 464 | name: jsxIdentifier({ name: 'VeryNested' }), 465 | }), 466 | }), 467 | ], 468 | closingElement: jsxClosingElement({ 469 | name: jsxIdentifier({ name: 'BadPeople' }), 470 | }), 471 | }), 472 | ], 473 | }) 474 | ) 475 | ).eq(` 476 | 477 | {hello} 478 | 479 | 480 | Hi 481 | 482 | 483 | `) 484 | }) 485 | }) 486 | 487 | describe('staticBlock', () => { 488 | test('basic', () => { 489 | expect(staticBlock({ body: [] }).toString()).eq(`static {\n\n}`) 490 | }) 491 | }) 492 | 493 | describe('doWhileStatement', () => { 494 | test('basic', () => { 495 | const testString = [ 496 | `do {`, 497 | ` console.log('work')`, 498 | `} while (1 < 3)`, 499 | ].join('\n') 500 | const { body } = espree.parse(testString) 501 | expect(doWhileStatement(body[0]).toString()).eq(testString) 502 | }) 503 | }) 504 | 505 | describe('ifStatement', () => { 506 | test('basic', () => { 507 | const testString = [ 508 | `if (1 < 3) {} else if (1 == 0) {`, 509 | ` console.log('success')`, 510 | `} else {`, 511 | ` console.log('error')`, 512 | `}`, 513 | ].join('\n') 514 | const { body } = espree.parse(testString) 515 | expect(ifStatement(body[0]).toString()).eq(testString) 516 | }) 517 | }) 518 | 519 | describe('throwStatement', () => { 520 | test('basic', () => { 521 | const testString = [`throw new Error();`].join('\n') 522 | const { body } = espree.parse(testString) 523 | expect(throwStatement(body[0]).toString()).eq(testString) 524 | }) 525 | }) 526 | 527 | describe('jsxOpeningElement', () => { 528 | test('with comment', () => { 529 | const commentValue = 'Hello this is a comment' 530 | expect( 531 | jsxOpeningElement({ 532 | leadingComments: [comment({ value: commentValue, type: 'Line' })], 533 | name: jsxIdentifier({ name: 'Modal' }), 534 | attributes: [], 535 | selfClosing: true, 536 | }).toString() 537 | ).eq(`// ${commentValue}\n`) 538 | }) 539 | test('with comments', () => { 540 | const commentValue = 'Hello this is a comment' 541 | expect( 542 | jsxOpeningElement({ 543 | leadingComments: [ 544 | comment({ value: commentValue, type: 'Line' }), 545 | comment({ value: 'Second line', type: 'Line' }), 546 | ], 547 | name: jsxIdentifier('Modal'), 548 | attributes: [], 549 | selfClosing: true, 550 | }).toString() 551 | ).eq(`// ${commentValue}\n// Second line\n`) 552 | }) 553 | 554 | test('no attributes', () => { 555 | expect( 556 | jsxOpeningElement({ 557 | name: jsxIdentifier('Modal'), 558 | attributes: [], 559 | selfClosing: true, 560 | }).toString() 561 | ).eq(``) 562 | }) 563 | 564 | test('no attributes not-self closing', () => { 565 | expect( 566 | jsxOpeningElement({ 567 | name: jsxIdentifier('Modal'), 568 | }).toString() 569 | ).eq(``) 570 | }) 571 | }) 572 | 573 | describe('TaggedTemplateExpression', () => { 574 | test('basic', () => { 575 | const testString = `const x = css\`color: red;\`` 576 | const { body } = espree.parse(testString, ESPREE_OPTIONS) 577 | expect(variableDeclaration(body[0]).toString()).eq(testString) 578 | }) 579 | }) 580 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/__tests__/program.test.ts: -------------------------------------------------------------------------------- 1 | import { program } from '..' 2 | 3 | import programFixture from '../__fixtures__/program' 4 | 5 | describe('program', () => { 6 | test('basic', () => { 7 | // @ts-expect-error This is fine it's just JSON 8 | expect(String(program(programFixture))).eq( 9 | `import A, { Welcome } from '@atlaskit/welcome' 10 | import { X } from './other' 11 | import tmm, * as x from 'thing' 12 | import 'blah' 13 | const someImport = import('hello.js') 14 | const someStatement = probably.blob 15 | function App({a: a}, {b: c}) { 16 | return ( 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | ); 31 | } 32 | const silly = (1, 3) 33 | new X(\`\${hello}: "world" \${1 == 2 ? \`\${Welcome}\` : ''}\`) 34 | X() 35 | let z,zz,zzz 36 | const p = class Z {} 37 | class D extends B { 38 | constructor () { 39 | super() 40 | this.hello='hi' 41 | } 42 | other () { 43 | return this.hello; 44 | } 45 | } 46 | switch (e) { 47 | case 'x': false; ''; 48 | default: 'zz'; 49 | } 50 | const v1 = 1 + 4 + 2 51 | { 52 | let a = 1 53 | } 54 | ; 55 | [] 56 | if (1 < 3) {} else if (1 == 0) { 57 | console.log('success') 58 | } else { 59 | console.log('error') 60 | } 61 | const yy = { 62 | a: 1, 63 | get z() {}, 64 | set j(j) {}, 65 | init: function () {}, 66 | blob: async function () {}, 67 | b: function () {}, 68 | c: function d() {}, 69 | pp: function () {}, 70 | zz: async function () {} 71 | } 72 | for (let i = 0;;i++) { 73 | continue 74 | } 75 | try { 76 | throw new Error('bad thing'); 77 | } catch {} finally { 78 | console.log('cleanup') 79 | } 80 | while (i < 0) { 81 | break 82 | } 83 | const y = 84 | hello 85 | 86 | ReactDOM.render(, document.getElementById('root')) 87 | export { b as y }from './other' 88 | export default b 89 | export const yyy = 10 90 | export * from 's'` 91 | ) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/__tests__/ts-node.test.ts: -------------------------------------------------------------------------------- 1 | import { literal, node, tsAsExpression } from '..' 2 | 3 | import * as espree from '@typescript-eslint/parser' 4 | 5 | const ESPREE_OPTIONS = { 6 | ecmaVersion: 2015, 7 | sourceType: 'module', 8 | } as const 9 | 10 | describe('tsAsExpression', () => { 11 | test('basic', () => { 12 | expect( 13 | String( 14 | tsAsExpression({ 15 | // @ts-expect-error 16 | expression: literal('hello'), 17 | // @ts-expect-error 18 | typeAnnotation: literal({ value: 'any' }), 19 | }) 20 | ) 21 | ).eq(`'hello' as any`) 22 | }) 23 | 24 | test('parsed with as string keyword', () => { 25 | const { body } = espree.parse(`const x = 'hello' as string`, ESPREE_OPTIONS) 26 | expect(node(body[0]).toString()).eq(`const x = 'hello' as string`) 27 | }) 28 | 29 | test('parsed with as type', () => { 30 | const { body } = espree.parse(`const x = 'hello' as World`, ESPREE_OPTIONS) 31 | expect(node(body[0]).toString()).eq(`const x = 'hello' as World`) 32 | }) 33 | 34 | test('parsed with as type with type parameter', () => { 35 | const { body } = espree.parse( 36 | `"2" as React.Ref`, 37 | ESPREE_OPTIONS 38 | ) 39 | expect(node(body[0]).toString()).eq(`"2" as React.Ref`) 40 | }) 41 | 42 | test('parsed with as type with type parameter', () => { 43 | const { body } = espree.parse( 44 | `"2" as React.Ref`, 45 | ESPREE_OPTIONS 46 | ) 47 | expect(node(body[0]).toString()).eq( 48 | `"2" as React.Ref` 49 | ) 50 | }) 51 | 52 | test('parsed non-null ts expression', () => { 53 | const { body } = espree.parse(`inputEl.current!.select()`, ESPREE_OPTIONS) 54 | expect(node(body[0]).toString()).eq(`inputEl.current!.select()`) 55 | }) 56 | 57 | test('parsed keyword assertions', () => { 58 | const { body } = espree.parse( 59 | `"10" as any as unknown as null as boolean`, 60 | ESPREE_OPTIONS 61 | ) 62 | expect(node(body[0]).toString()).eq( 63 | `"10" as any as unknown as null as boolean` 64 | ) 65 | }) 66 | 67 | test('parsed ts union & intersection types', () => { 68 | const { body } = espree.parse( 69 | `type X = 'hello' | 'thing' & 8`, 70 | ESPREE_OPTIONS 71 | ) 72 | expect(node(body[0]).toString()).eq(`type X = 'hello' | 'thing' & 8`) 73 | }) 74 | 75 | test('parsed ts type query', () => { 76 | const { body } = espree.parse( 77 | `type X = 'hello'\ntype Y = typeof X`, 78 | ESPREE_OPTIONS 79 | ) 80 | expect(node(body[0]).toString()).eq(`type X = 'hello'`) 81 | expect(node(body[1]).toString()).eq(`type Y = typeof X`) 82 | }) 83 | 84 | test('parsed ts type operator', () => { 85 | const { body } = espree.parse( 86 | `type X = 'hello'\ntype Y = keyof typeof X`, 87 | ESPREE_OPTIONS 88 | ) 89 | expect(node(body[0]).toString()).eq(`type X = 'hello'`) 90 | expect(node(body[1]).toString()).eq(`type Y = keyof typeof X`) 91 | }) 92 | 93 | test('type alias declaration (keyword)', () => { 94 | const { body } = espree.parse(`type X = string`, ESPREE_OPTIONS) 95 | expect(node(body[0]).toString()).eq(`type X = string`) 96 | }) 97 | 98 | test('type alias declaration (literal)', () => { 99 | const { body } = espree.parse(`type X = 'hello'`, ESPREE_OPTIONS) 100 | expect(node(body[0]).toString()).eq(`type X = 'hello'`) 101 | }) 102 | 103 | test('parsed ts union & intersection types with generic', () => { 104 | const { body } = espree.parse( 105 | `type X = 'hello' | 'thing' & 8`, 106 | ESPREE_OPTIONS 107 | ) 108 | expect(node(body[0]).toString()).eq(`type X = 'hello' | 'thing' & 8`) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import * as espree from 'espree' 2 | import { 3 | closestOfType, 4 | hasJSXAttribute, 5 | insertImportSpecifier, 6 | insertJSXAttribute, 7 | literal, 8 | removeImportSpecifier, 9 | } from '..' 10 | 11 | const ESPREE_OPTIONS = { 12 | ecmaVersion: 2015, 13 | sourceType: 'module', 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | } 18 | 19 | /** This is not a valid test in the non eslint runtime */ 20 | describe.skip('closestOfType', () => { 21 | test('basic', () => { 22 | const program = espree.parse('', ESPREE_OPTIONS) 23 | expect( 24 | closestOfType(program.body[0].expression.openingElement, 'JSXElement') 25 | ).toHaveProperty('type', 'JSXElement') 26 | }) 27 | }) 28 | 29 | describe('hasJSXAttribute', () => { 30 | test('basic', () => { 31 | const { body } = espree.parse( 32 | '', 33 | ESPREE_OPTIONS 34 | ) 35 | expect(hasJSXAttribute(body[0].expression, 'name')).to.be.true 36 | }) 37 | 38 | test('no attribute on jsx', () => { 39 | const { body } = espree.parse('', ESPREE_OPTIONS) 40 | expect(hasJSXAttribute(body[0].expression, 'name')).to.be.false 41 | }) 42 | 43 | test('is not jsx', () => { 44 | const { body } = espree.parse('1 + 1', ESPREE_OPTIONS) 45 | expect(hasJSXAttribute(body[0].expression, 'name')).to.be.false 46 | }) 47 | }) 48 | 49 | describe('insertImportSpecifier', () => { 50 | test('basic', () => { 51 | const { body } = espree.parse(`import x from 'place'`, ESPREE_OPTIONS) 52 | expect(insertImportSpecifier(body[0], 'name').toString()).eq( 53 | `import x, { name } from 'place'` 54 | ) 55 | }) 56 | 57 | test('no default', () => { 58 | const { body } = espree.parse( 59 | `import { nothing } from 'place'`, 60 | ESPREE_OPTIONS 61 | ) 62 | expect(insertImportSpecifier(body[0], 'name').toString()).eq( 63 | `import { nothing, name } from 'place'` 64 | ) 65 | }) 66 | 67 | test('with alias', () => { 68 | const { body } = espree.parse(`import x from 'place'`, ESPREE_OPTIONS) 69 | expect(insertImportSpecifier(body[0], 'name', 'alias').toString()).eq( 70 | `import x, { name as alias } from 'place'` 71 | ) 72 | }) 73 | }) 74 | 75 | describe('removeImportSpecifier', () => { 76 | test('no default', () => { 77 | const { body } = espree.parse( 78 | `import { nothing, name } from 'place'`, 79 | ESPREE_OPTIONS 80 | ) 81 | expect(removeImportSpecifier(body[0], 'name').toString()).eq( 82 | `import { nothing } from 'place'` 83 | ) 84 | }) 85 | 86 | test('with alias', () => { 87 | const { body } = espree.parse( 88 | `import x, { name as alias } from 'place'`, 89 | ESPREE_OPTIONS 90 | ) 91 | expect(removeImportSpecifier(body[0], 'name').toString()).eq( 92 | `import x from 'place'` 93 | ) 94 | }) 95 | }) 96 | 97 | describe('insertJSXAttribute', () => { 98 | test('basic', () => { 99 | const { body } = espree.parse(``, ESPREE_OPTIONS) 100 | expect( 101 | insertJSXAttribute( 102 | body[0].expression, 103 | 'hello', 104 | literal('world') 105 | ).toString() 106 | ).eq(``) 107 | }) 108 | 109 | test('with closing', () => { 110 | const { body } = espree.parse(``, ESPREE_OPTIONS) 111 | expect( 112 | insertJSXAttribute( 113 | body[0].expression, 114 | 'hello', 115 | literal('world') 116 | ).toString() 117 | ).eq(``) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { 2 | jsxAttribute, 3 | jsxClosingElement, 4 | jsxClosingFragment, 5 | jsxElement, 6 | jsxEmptyExpression, 7 | jsxExpressionContainer, 8 | jsxFragment, 9 | jsxIdentifier, 10 | jsxMemberExpression, 11 | jsxOpeningElement, 12 | jsxOpeningFragment, 13 | jsxSpreadAttribute, 14 | jsxSpreadChild, 15 | jsxText, 16 | } from './jsx-nodes' 17 | 18 | import { 19 | arrayExpression, 20 | arrayPattern, 21 | arrowFunctionExpression, 22 | assignmentExpression, 23 | awaitExpression, 24 | binaryExpression, 25 | blockStatement, 26 | breakStatement, 27 | callExpression, 28 | catchClause, 29 | chainExpression, 30 | classBody, 31 | classDeclaration, 32 | classExpression, 33 | conditionalExpression, 34 | continueStatement, 35 | debuggerStatement, 36 | doWhileStatement, 37 | emptyStatement, 38 | exportAllDeclaration, 39 | exportDefaultDeclaration, 40 | exportNamedDeclaration, 41 | exportSpecifier, 42 | expressionStatement, 43 | forInStatement, 44 | forOfStatement, 45 | forStatement, 46 | functionDeclaration, 47 | functionExpression, 48 | identifier, 49 | ifStatement, 50 | importDeclaration, 51 | importDefaultSpecifier, 52 | importExpression, 53 | importNamespaceSpecifier, 54 | importSpecifier, 55 | literal, 56 | logicalExpression, 57 | memberExpression, 58 | methodDefinition, 59 | newExpression, 60 | objectExpression, 61 | objectPattern, 62 | program, 63 | property, 64 | propertyDefinition, 65 | restElement, 66 | returnStatement, 67 | sequenceExpression, 68 | spreadElement, 69 | staticBlock, 70 | superCallExpression, 71 | switchCase, 72 | switchStatement, 73 | taggedTemplateExpression, 74 | templateElement, 75 | templateLiteral, 76 | thisExpression, 77 | throwStatement, 78 | tryStatement, 79 | unaryExpression, 80 | updateExpression, 81 | variableDeclaration, 82 | variableDeclarator, 83 | whileStatement, 84 | withStatement, 85 | yieldExpression, 86 | } from './nodes' 87 | import { 88 | tsAnyKeyword, 89 | tsArrayType, 90 | tsAsExpression, 91 | tsBooleanKeyword, 92 | tsEmptyBodyFunctionExpression, 93 | tsIntersectionType, 94 | tsLiteralType, 95 | tsNonNullExpression, 96 | tsNullKeyword, 97 | tsQualifiedName, 98 | tsReadonlyKeyword, 99 | tsStringKeyword, 100 | tsTypeAliasDeclaration, 101 | tsTypeOperator, 102 | tsTypeParameter, 103 | tsTypeParameterDeclaration, 104 | tsTypeParameterInstantiation, 105 | tsTypeQuery, 106 | tsTypeReference, 107 | tsUnionType, 108 | tsUnknownKeyword, 109 | } from './ts-nodes' 110 | import { identity } from './utils/identity' 111 | import { NodeMap } from './utils/node' 112 | 113 | export const DEFAULT_WHITESPACE = '\n ' 114 | 115 | export const typeToHelperLookup = new Proxy( 116 | // @ts-expect-error 117 | { 118 | // TODO implement 119 | AssignmentProperty: identity, 120 | // TODO implement 121 | AssignmentPattern: identity, 122 | AssignmentExpression: assignmentExpression, 123 | AwaitExpression: awaitExpression, 124 | ArrayExpression: arrayExpression, 125 | ArrayPattern: arrayPattern, 126 | BlockStatement: blockStatement, 127 | BinaryExpression: binaryExpression, 128 | ConditionalExpression: conditionalExpression, 129 | ChainExpression: chainExpression, 130 | JSXFragment: jsxFragment, 131 | JSXSpreadChild: jsxSpreadChild, 132 | JSXExpressionContainer: jsxExpressionContainer, 133 | JSXClosingElement: jsxClosingElement, 134 | JSXOpeningElement: jsxOpeningElement, 135 | JSXOpeningFragment: jsxOpeningFragment, 136 | JSXClosingFragment: jsxClosingFragment, 137 | JSXElement: jsxElement, 138 | JSXText: jsxText, 139 | JSXSpreadAttribute: jsxSpreadAttribute, 140 | JSXAttribute: jsxAttribute, 141 | JSXMemberExpression: jsxMemberExpression, 142 | JSXNamespacedName: identity, 143 | JSXIdentifier: jsxIdentifier, 144 | JSXEmptyExpression: jsxEmptyExpression, 145 | ArrowFunctionExpression: arrowFunctionExpression, 146 | FunctionExpression: functionExpression, 147 | Identifier: identifier, 148 | IfStatement: ifStatement, 149 | // TODO implement 150 | LabeledStatement: identity, 151 | Literal: literal, 152 | LogicalExpression: logicalExpression, 153 | /** this isn't a concrete node type */ 154 | Expression: identity, 155 | ForStatement: forStatement, 156 | ForInStatement: forInStatement, 157 | ForOfStatement: forOfStatement, 158 | ImportSpecifier: importSpecifier, 159 | ImportNamespaceSpecifier: importNamespaceSpecifier, 160 | ImportDefaultSpecifier: importDefaultSpecifier, 161 | ImportDeclaration: importDeclaration, 162 | ImportExpression: importExpression, 163 | ThisExpression: thisExpression, 164 | ThrowStatement: throwStatement, 165 | TemplateLiteral: templateLiteral, 166 | TemplateElement: templateElement, 167 | TaggedTemplateExpression: taggedTemplateExpression, 168 | ObjectExpression: objectExpression, 169 | ObjectPattern: objectPattern, 170 | RestElement: restElement, 171 | MemberExpression: memberExpression, 172 | // TODO: needs implementation 173 | MetaProperty: identity, 174 | MethodDefinition: methodDefinition, 175 | NewExpression: newExpression, 176 | SwitchStatement: switchStatement, 177 | EmptyStatement: emptyStatement, 178 | FunctionDeclaration: functionDeclaration, 179 | CallExpression: callExpression, 180 | SimpleCallExpression: callExpression, 181 | CatchClause: catchClause, 182 | ContinueStatement: continueStatement, 183 | ClassDeclaration: classDeclaration, 184 | ClassExpression: classExpression, 185 | ClassBody: classBody, 186 | DebuggerStatement: debuggerStatement, 187 | DoWhileStatement: doWhileStatement, 188 | ExportNamedDeclaration: exportNamedDeclaration, 189 | ExportSpecifier: exportSpecifier, 190 | ExportAllDeclaration: exportAllDeclaration, 191 | ExportDefaultDeclaration: exportDefaultDeclaration, 192 | /** this isn't a concrete node type */ 193 | Pattern: identity, 194 | /** this isn't a concrete node type */ 195 | Statement: identity, 196 | BreakStatement: breakStatement, 197 | PrivateIdentifier: identity, 198 | Property: property, 199 | Program: program, 200 | PropertyDefinition: propertyDefinition, 201 | ReturnStatement: returnStatement, 202 | Super: superCallExpression, 203 | SequenceExpression: sequenceExpression, 204 | SpreadElement: spreadElement, 205 | StaticBlock: staticBlock, 206 | SwitchCase: switchCase, 207 | TryStatement: tryStatement, 208 | WhileStatement: whileStatement, 209 | WithStatement: withStatement, 210 | ExpressionStatement: expressionStatement, 211 | UnaryExpression: unaryExpression, 212 | UpdateExpression: updateExpression, 213 | VariableDeclaration: variableDeclaration, 214 | VariableDeclarator: variableDeclarator, 215 | YieldExpression: yieldExpression, 216 | // typescript 217 | TSArrayType: tsArrayType, 218 | TSAsExpression: tsAsExpression, 219 | TSEmptyBodyFunctionExpression: tsEmptyBodyFunctionExpression, 220 | TSStringKeyword: tsStringKeyword, 221 | TSTypeReference: tsTypeReference, 222 | TSAnyKeyword: tsAnyKeyword, 223 | TSUnknownKeyword: tsUnknownKeyword, 224 | TSBooleanKeyword: tsBooleanKeyword, 225 | TSReadonlyKeyword: tsReadonlyKeyword, 226 | TSNullKeyword: tsNullKeyword, 227 | TSQualifiedName: tsQualifiedName, 228 | TSTypeParameterInstantiation: tsTypeParameterInstantiation, 229 | TSLiteralType: tsLiteralType, 230 | TSNonNullExpression: tsNonNullExpression, 231 | TSIntersectionType: tsIntersectionType, 232 | TSUnionType: tsUnionType, 233 | TSTypeQuery: tsTypeQuery, 234 | TSTypeOperator: tsTypeOperator, 235 | TSTypeAliasDeclaration: tsTypeAliasDeclaration, 236 | TSTypeParameterDeclaration: tsTypeParameterDeclaration, 237 | TSTypeParameter: tsTypeParameter, 238 | } as NodeMap, 239 | { 240 | // dynamic getter will fail and provide debug information 241 | get(target, name, receiver) { 242 | if (Reflect.has(target, name)) { 243 | return Reflect.get(target, name, receiver) 244 | } 245 | 246 | const nodeName = name.toString() 247 | const error = new Error(`\ 248 | type '${nodeName}' missing in typeMap. 249 | 250 | This is probably because the type '${nodeName}' is a Typescript or Flow specific node type. These nodes currently have only partial support. 251 | 252 | To resolve this you can: 253 | * Use a more constrained parser like esprima in your eslint config 254 | * Lodge a bug at https://github.com/DarkPurple141/eslint-codemod-utils/issues 255 | `) 256 | error.name = 'UnknownNodeError' 257 | throw error 258 | }, 259 | } 260 | ) 261 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'espree' { 2 | export function parse( 3 | code: string, 4 | options?: { ecmaVersion?: number; sourceType?: string } 5 | ): { 6 | body: any 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/index.ts: -------------------------------------------------------------------------------- 1 | /** constants has to be first so that it resolves the map */ 2 | export * from './constants' 3 | export * from './nodes' 4 | export * from './jsx-nodes' 5 | export * from './ts-nodes' 6 | export * from './utils' 7 | export * from './types' 8 | 9 | // re-export estree-jsx - unfortunately can't export * 10 | export type { 11 | ArrayExpression, 12 | ArrayPattern, 13 | ArrowFunctionExpression, 14 | AssignmentExpression, 15 | AssignmentOperator, 16 | AssignmentPattern, 17 | AssignmentProperty, 18 | AwaitExpression, 19 | BigIntLiteral, 20 | BinaryExpression, 21 | BinaryOperator, 22 | BlockStatement, 23 | BreakStatement, 24 | CallExpression, 25 | CatchClause, 26 | ChainElement, 27 | ChainExpression, 28 | Class, 29 | ClassBody, 30 | ClassDeclaration, 31 | ClassExpression, 32 | Comment, 33 | ConditionalExpression, 34 | ContinueStatement, 35 | DebuggerStatement, 36 | Declaration, 37 | Directive, 38 | DoWhileStatement, 39 | EmptyStatement, 40 | ExportAllDeclaration, 41 | ExportDefaultDeclaration, 42 | ExportNamedDeclaration, 43 | ExportSpecifier, 44 | Expression, 45 | ExpressionMap, 46 | ExpressionStatement, 47 | ForInStatement, 48 | ForOfStatement, 49 | ForStatement, 50 | Function, 51 | FunctionDeclaration, 52 | FunctionExpression, 53 | Identifier, 54 | IfStatement, 55 | ImportDeclaration, 56 | ImportDefaultSpecifier, 57 | ImportExpression, 58 | ImportNamespaceSpecifier, 59 | ImportSpecifier, 60 | JSXAttribute, 61 | JSXClosingElement, 62 | JSXClosingFragment, 63 | JSXElement, 64 | JSXEmptyExpression, 65 | JSXExpressionContainer, 66 | JSXFragment, 67 | JSXIdentifier, 68 | JSXMemberExpression, 69 | JSXNamespacedName, 70 | JSXOpeningElement, 71 | JSXOpeningFragment, 72 | JSXSpreadAttribute, 73 | JSXSpreadChild, 74 | JSXText, 75 | LabeledStatement, 76 | Literal, 77 | LogicalExpression, 78 | LogicalOperator, 79 | MemberExpression, 80 | MetaProperty, 81 | MethodDefinition, 82 | ModuleDeclaration, 83 | ModuleSpecifier, 84 | NewExpression, 85 | Node, 86 | NodeMap, 87 | ObjectExpression, 88 | ObjectPattern, 89 | Pattern, 90 | Position, 91 | PrivateIdentifier, 92 | Program, 93 | Property, 94 | PropertyDefinition, 95 | RegExpLiteral, 96 | RestElement, 97 | ReturnStatement, 98 | SequenceExpression, 99 | SimpleCallExpression, 100 | SimpleLiteral, 101 | SourceLocation, 102 | SpreadElement, 103 | Statement, 104 | StaticBlock, 105 | Super, 106 | SwitchCase, 107 | SwitchStatement, 108 | TaggedTemplateExpression, 109 | TemplateElement, 110 | TemplateLiteral, 111 | ThisExpression, 112 | ThrowStatement, 113 | TryStatement, 114 | UnaryExpression, 115 | UnaryOperator, 116 | UpdateExpression, 117 | UpdateOperator, 118 | VariableDeclaration, 119 | VariableDeclarator, 120 | WhileStatement, 121 | WithStatement, 122 | YieldExpression, 123 | } from 'estree-jsx' 124 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/jsx-nodes.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES } from '@typescript-eslint/types' 2 | import { DEFAULT_WHITESPACE } from './constants' 3 | import * as ESTree from 'estree-jsx' 4 | 5 | import type { 6 | StringableASTNode, 7 | StringableASTNodeFn, 8 | WithoutType, 9 | } from './types' 10 | import { isNodeOfType } from './utils' 11 | import { node } from './utils/node' 12 | 13 | export const whiteSpace = (loc?: ESTree.SourceLocation) => 14 | ''.padStart(loc?.start?.column || 0, ' ') 15 | 16 | export const comments = (comments: ESTree.Comment[] = []) => ({ 17 | comments, 18 | toString: () => 19 | comments.length ? `${comments.map(comment).join('\n')}\n` : '', 20 | }) 21 | 22 | export const comment = ({ value, type, loc, ...other }: ESTree.Comment) => ({ 23 | ...other, 24 | value, 25 | type, 26 | toString: () => 27 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 28 | whiteSpace(loc!) + (type === 'Line' ? `// ${value}` : `/* ${value} */`), 29 | }) 30 | 31 | /** 32 | * __JSXIdentifier__ 33 | * 34 | * @param param Takes a string or the shape of a {ESTree.JSXIdentifier} node 35 | * @returns {ESTree.JSXIdentifier} node 36 | */ 37 | export const jsxIdentifier = ( 38 | param: WithoutType | string 39 | ): StringableASTNode => { 40 | const name = typeof param === 'string' ? param : param.name 41 | const other = typeof param === 'object' ? param : {} 42 | return { 43 | ...other, 44 | name, 45 | type: AST_NODE_TYPES.JSXIdentifier, 46 | toString: () => name, 47 | } 48 | } 49 | 50 | /** 51 | * __JSXOpeningFragment__ 52 | * 53 | * @example 54 | * ```ts 55 | * <>hello 56 | * ^^ 57 | * ``` 58 | */ 59 | export const jsxOpeningFragment: StringableASTNodeFn< 60 | ESTree.JSXOpeningFragment 61 | > = ({ ...other }) => { 62 | return { 63 | ...other, 64 | type: AST_NODE_TYPES.JSXOpeningFragment, 65 | toString: () => `<>`, 66 | } 67 | } 68 | 69 | /** 70 | * __JSXClosingFragment__ 71 | * 72 | * @example 73 | * ```ts 74 | * <>hello 75 | * ^^ 76 | * ``` 77 | */ 78 | export const jsxClosingFragment: StringableASTNodeFn< 79 | ESTree.JSXClosingFragment 80 | > = ({ ...other }) => { 81 | return { 82 | ...other, 83 | type: AST_NODE_TYPES.JSXClosingFragment, 84 | toString: () => ``, 85 | } 86 | } 87 | 88 | /** 89 | * __JSXFragment__ 90 | * 91 | * @example 92 | * ```ts 93 | * <>hello 94 | * ^^^^^^^^^^ 95 | * ``` 96 | */ 97 | export const jsxFragment: StringableASTNodeFn = ({ 98 | openingFragment, 99 | closingFragment, 100 | children, 101 | ...other 102 | }) => ({ 103 | ...other, 104 | openingFragment, 105 | closingFragment, 106 | children, 107 | type: AST_NODE_TYPES.JSXFragment, 108 | toString: () => { 109 | return `${node(openingFragment)}${children 110 | .map(node) 111 | .map(String) 112 | .join('\n')}${node(closingFragment)}` 113 | }, 114 | }) 115 | 116 | /** 117 | * __JSXSpreadChild__ 118 | * 119 | * @example 120 | * ```ts 121 | * <>{...child} 122 | * ^^^^^^^^^^ 123 | * ``` 124 | */ 125 | export const jsxSpreadChild: StringableASTNodeFn = ({ 126 | expression, 127 | ...other 128 | }) => { 129 | return { 130 | ...other, 131 | expression, 132 | type: AST_NODE_TYPES.JSXSpreadChild, 133 | toString: () => `{...${node(expression)}}`, 134 | } 135 | } 136 | 137 | export const jsxMemberExpression: StringableASTNodeFn< 138 | ESTree.JSXMemberExpression 139 | > = ({ object, property, ...other }) => ({ 140 | ...other, 141 | type: AST_NODE_TYPES.JSXMemberExpression, 142 | object, 143 | property, 144 | toString: () => 145 | `${ 146 | isNodeOfType(object, 'JSXIdentifier') 147 | ? jsxIdentifier(object) 148 | : node(object) 149 | }.${jsxIdentifier(property)}`, 150 | }) 151 | 152 | const DEFAULT_LOC: ESTree.SourceLocation = { 153 | start: { 154 | column: 0, 155 | line: 0, 156 | }, 157 | end: { 158 | column: 0, 159 | line: 0, 160 | }, 161 | } 162 | 163 | /** 164 | * __JSXElement__ 165 | * 166 | * @example 167 | * 168 | * Usage 169 | * ``` 170 | * import { jsxElement, jsxOpeningElement, jsxClosingElement, identifier } from 'eslint-codemod-utils' 171 | * 172 | * const modalName = identifier({ name: 'Modal' }) 173 | * const modal = jsxElement({ 174 | * openingElement: jsxOpeningElement({ name: modalName, selfClosing: false }), 175 | * closingElement: jsxClosingElement({ name: modalName }), 176 | * }) 177 | * ``` 178 | * 179 | * @example 180 | * 181 | * Produces 182 | * ```js 183 | * 184 | * ``` 185 | * 186 | * @returns {JSXElement} 187 | */ 188 | export const jsxElement: StringableASTNodeFn< 189 | ESTree.JSXElement, 190 | 'children' | 'closingElement' 191 | > = ({ 192 | openingElement, 193 | closingElement = null, 194 | children = [], 195 | loc = DEFAULT_LOC, 196 | ...other 197 | }) => ({ 198 | ...other, 199 | openingElement, 200 | closingElement, 201 | children, 202 | loc, 203 | type: AST_NODE_TYPES.JSXElement, 204 | toString: (): string => { 205 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 206 | const indent = whiteSpace(loc!) 207 | const spacing = DEFAULT_WHITESPACE + indent 208 | return `${jsxOpeningElement(openingElement)}${ 209 | children.length 210 | ? spacing + children.map(node).map(String).join(spacing) + '\n' 211 | : '' 212 | }${closingElement ? `${indent}${jsxClosingElement(closingElement)}` : ''}` 213 | }, 214 | }) 215 | 216 | /** 217 | * __JSXSpreadAttribute__ 218 | * 219 | * @example Usage 220 | * 221 | * ```js 222 | * import { jsxSpreadAttribute, identifier } from 'eslint-codemod-utils' 223 | * 224 | * const spreadAttr = jsxSpreadAttribute({ 225 | * argument: identifier({ name: 'spread' }) 226 | * }) 227 | * ``` 228 | * @example 229 | * 230 | * ```js 231 | * // Produces a spread attribute 232 | *
233 | * ⌃⌃⌃⌃⌃⌃⌃⌃⌃⌃⌃ 234 | * ``` 235 | * 236 | * @returns {ESTree.JSXSpreadAttribute} 237 | */ 238 | export const jsxSpreadAttribute: StringableASTNodeFn< 239 | ESTree.JSXSpreadAttribute 240 | > = ({ argument, ...other }) => ({ 241 | ...other, 242 | type: AST_NODE_TYPES.JSXSpreadAttribute, 243 | argument, 244 | toString: () => `{...${node(argument)}}`, 245 | }) 246 | 247 | export const jsxOpeningElement: StringableASTNodeFn< 248 | ESTree.JSXOpeningElement, 249 | 'attributes' | 'selfClosing' 250 | > = ({ 251 | name, 252 | attributes = [], 253 | selfClosing = false, 254 | leadingComments = [], 255 | ...other 256 | }) => ({ 257 | ...other, 258 | type: AST_NODE_TYPES.JSXOpeningElement, 259 | name, 260 | attributes, 261 | selfClosing, 262 | toString: () => 263 | `${comments(leadingComments)}<${ 264 | name.type === 'JSXIdentifier' 265 | ? jsxIdentifier(name) 266 | : name.type === 'JSXMemberExpression' 267 | ? jsxMemberExpression(name) 268 | : // namespaced name not yet implemeneted 269 | name 270 | }${ 271 | attributes && attributes.length 272 | ? ' ' + attributes.map(node).map(String).join(' ') 273 | : '' 274 | }${selfClosing ? ' />' : '>'}`, 275 | }) 276 | 277 | /** 278 | * __JSXClosingElement__ 279 | * 280 | * @example 281 | * 282 | * ```js 283 | * // The below jsx div is a closing element. 284 | * // A closing element is expected to match a valid opening element of the same name 285 | *
286 | * ``` 287 | * 288 | * @returns {ESTree.JSXClosingElement} 289 | */ 290 | export const jsxClosingElement: StringableASTNodeFn< 291 | ESTree.JSXClosingElement 292 | > = ({ name, ...other }) => { 293 | return { 294 | ...other, 295 | type: AST_NODE_TYPES.JSXClosingElement, 296 | name, 297 | toString: () => ``, 298 | } 299 | } 300 | 301 | /** 302 | * __JSXText__ 303 | * 304 | * @example 305 | * 306 | * ```js 307 | * // In the below jsx, the string, "hello world" is considered JSXText. 308 | * // JSXText can be a any number, boolean, or string value. 309 | *
hello world
310 | * ``` 311 | * 312 | * @returns {ESTree.JSXText} 313 | */ 314 | export const jsxText: StringableASTNodeFn = ({ 315 | value, 316 | raw, 317 | ...other 318 | }) => ({ 319 | ...other, 320 | type: AST_NODE_TYPES.JSXText, 321 | value, 322 | raw, 323 | toString: () => value, 324 | }) 325 | 326 | /** 327 | * __JSXEmptyExpression__ 328 | * 329 | * @example 330 | * 331 | * ```tsx 332 | * 333 | * ^^ 334 | * ``` 335 | * 336 | * @returns {ESTree.JSXEmptyExpression} 337 | */ 338 | export const jsxEmptyExpression: StringableASTNodeFn< 339 | ESTree.JSXEmptyExpression 340 | > = (node) => { 341 | return { 342 | ...node, 343 | type: AST_NODE_TYPES.JSXEmptyExpression, 344 | toString: () => `{}`, 345 | } 346 | } 347 | 348 | /** 349 | * __JSXExpressionContainer__ 350 | * 351 | * @example 352 | * 353 | * ```tsx 354 | * 355 | * ^^^^^^^^^^^ 356 | * ``` 357 | * 358 | * @returns {ESTree.JSXExpressionContainer} 359 | */ 360 | export const jsxExpressionContainer: StringableASTNodeFn< 361 | ESTree.JSXExpressionContainer 362 | > = ({ expression, ...other }) => ({ 363 | ...other, 364 | expression, 365 | type: AST_NODE_TYPES.JSXExpressionContainer, 366 | toString: () => { 367 | if (isNodeOfType(expression, 'JSXEmptyExpression')) { 368 | return '{}' 369 | } 370 | 371 | // @ts-expect-error This should never happen but makes the API more accomodating 372 | if (expression.type === 'JSXExpressionContainer') { 373 | return String(jsxExpressionContainer(expression)) 374 | } 375 | 376 | return `{${node(expression)}}` 377 | }, 378 | }) 379 | 380 | /** 381 | * __JSXAttribute__ 382 | * 383 | * @example 384 | * 385 | * ```js 386 | * // In the below jsx, `a`, `b` and `c` reflect different valid 387 | * // jsx attributes. There values can come in many forms. 388 | *
389 | * ``` 390 | * 391 | * @returns {JSXAttribute} 392 | */ 393 | export const jsxAttribute: StringableASTNodeFn = ({ 394 | name, 395 | value, 396 | ...other 397 | }) => ({ 398 | ...other, 399 | type: AST_NODE_TYPES.JSXAttribute, 400 | name, 401 | value, 402 | toString: () => `${name.name}${value ? `=${node(value)}` : ''}`, 403 | }) 404 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/jsx-runtime.ts: -------------------------------------------------------------------------------- 1 | export { jsx } from './utils/jsx' 2 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/nodes.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/types' 2 | import * as ESTree from 'estree-jsx' 3 | 4 | import type { 5 | StringableASTNode, 6 | StringableASTNodeFn, 7 | WithoutType, 8 | } from './types' 9 | import { node } from './utils/node' 10 | import { DEFAULT_WHITESPACE } from './constants' 11 | import { isNodeOfType } from './utils' 12 | 13 | /** 14 | * __CallExpression__ 15 | * 16 | * @example 17 | * 18 | * Usage 19 | * ``` 20 | * const call = callExpression({ callee: identifier({ name: 'normalCallExpression' }) }) 21 | * ``` 22 | * 23 | * Produces 24 | * 25 | * @example 26 | * 27 | * ```js 28 | * normalCallExpression() 29 | * ``` 30 | * 31 | * @returns {ESTree.CallExpression} 32 | */ 33 | export const callExpression: StringableASTNodeFn< 34 | ESTree.SimpleCallExpression, 35 | 'optional' 36 | > = ({ arguments: calleeArgs, callee, optional = false, ...other }) => { 37 | return { 38 | ...other, 39 | arguments: calleeArgs, 40 | callee, 41 | optional, 42 | type: AST_NODE_TYPES.CallExpression, 43 | toString: () => 44 | `${node(callee)}${optional ? '?.' : ''}(${calleeArgs 45 | .map(node) 46 | .join(', ')})`, 47 | } 48 | } 49 | 50 | /** 51 | * __Super__ 52 | * 53 | * @example 54 | * 55 | * ``` 56 | * // note the whole expression is a `CallExpression` 57 | * // super is simply the callee / identifier 58 | * super() 59 | * ^^^^^ 60 | * ``` 61 | * 62 | * @returns {ESTree.Super} 63 | */ 64 | export const superCallExpression: StringableASTNodeFn = ({ 65 | ...other 66 | }) => { 67 | return { 68 | ...other, 69 | type: AST_NODE_TYPES.Super, 70 | toString: () => `super`, 71 | } 72 | } 73 | 74 | export const chainExpression: StringableASTNodeFn = ({ 75 | expression, 76 | ...other 77 | }) => { 78 | return { 79 | ...other, 80 | expression, 81 | type: AST_NODE_TYPES.ChainExpression, 82 | toString: () => `${node(expression)}`, 83 | } 84 | } 85 | 86 | /** 87 | * __BinaryExpression__ 88 | * 89 | * @example 90 | * ```ts 91 | * const x = 'left' + 'right' 92 | * ^^^^^^^^^^^^^^^^ 93 | * ``` 94 | */ 95 | export const binaryExpression: StringableASTNodeFn = ({ 96 | left, 97 | right, 98 | operator, 99 | ...other 100 | }) => { 101 | return { 102 | ...other, 103 | left, 104 | right, 105 | operator, 106 | type: AST_NODE_TYPES.BinaryExpression, 107 | toString: () => `${node(left)} ${operator} ${node(right)}`, 108 | } 109 | } 110 | 111 | /** 112 | * __SequenceExpression__ 113 | * 114 | * @example 115 | * ```ts 116 | * const x = (4, 8) 117 | * ^^^^^^ 118 | * ``` 119 | */ 120 | export const sequenceExpression: StringableASTNodeFn< 121 | ESTree.SequenceExpression 122 | > = ({ expressions, ...other }) => { 123 | return { 124 | ...other, 125 | expressions, 126 | type: AST_NODE_TYPES.SequenceExpression, 127 | toString: () => `(${expressions.map(node).map(String).join(', ')})`, 128 | } 129 | } 130 | 131 | /** 132 | * __ArrowFunctionExpression__ 133 | * 134 | * @example 135 | * ```js 136 | * const arrow = () => 42 137 | * ⌃⌃⌃⌃⌃⌃⌃⌃ 138 | * ``` 139 | * @returns {ESTree.ArrowFunctionExpression} 140 | */ 141 | export const arrowFunctionExpression: StringableASTNodeFn< 142 | ESTree.ArrowFunctionExpression, 143 | 'async' | 'generator' 144 | > = ({ 145 | async = false, 146 | generator = false, 147 | body, 148 | expression, 149 | params, 150 | ...other 151 | }) => { 152 | return { 153 | ...other, 154 | generator, 155 | async, 156 | expression, 157 | body, 158 | params, 159 | type: AST_NODE_TYPES.ArrowFunctionExpression, 160 | toString: () => 161 | `${async ? 'async ' : ''}(${params 162 | .map(node) 163 | .map(String) 164 | .join(', ')}) => ${node(body)}`, 165 | } 166 | } 167 | 168 | /** 169 | * __TaggedTemplateExpression__ 170 | * 171 | * @example 172 | * ```ts 173 | * const style = css`color: red;` 174 | * ^^^^^^^^^^^ 175 | * ``` 176 | */ 177 | export const taggedTemplateExpression: StringableASTNodeFn< 178 | ESTree.TaggedTemplateExpression 179 | > = ({ quasi, tag, ...other }) => { 180 | return { 181 | ...other, 182 | quasi, 183 | tag, 184 | type: AST_NODE_TYPES.TaggedTemplateExpression, 185 | toString: () => `${node(tag)}${node(quasi)}`, 186 | } 187 | } 188 | 189 | export const functionExpression: StringableASTNodeFn< 190 | ESTree.FunctionExpression, 191 | 'generator' | 'async' 192 | > = ({ async = false, generator = false, body, params, id, ...other }) => { 193 | return { 194 | ...other, 195 | id, 196 | async, 197 | generator, 198 | body, 199 | params, 200 | type: AST_NODE_TYPES.FunctionExpression, 201 | toString: () => 202 | `${async ? 'async ' : ''}function ${id ? node(id) : ''}(${params 203 | .map(node) 204 | .join(', ')}) ${node(body)}`, 205 | } 206 | } 207 | 208 | export const blockStatement: StringableASTNodeFn = ({ 209 | body, 210 | ...other 211 | }) => { 212 | return { 213 | ...other, 214 | body, 215 | type: AST_NODE_TYPES.BlockStatement, 216 | toString: () => 217 | `{${ 218 | body.length 219 | ? DEFAULT_WHITESPACE + 220 | body.map(node).map(String).join(DEFAULT_WHITESPACE) + 221 | '\n' 222 | : '' 223 | }}`, 224 | } 225 | } 226 | 227 | export const returnStatement: StringableASTNodeFn = ({ 228 | argument, 229 | ...other 230 | }) => { 231 | return { 232 | ...other, 233 | argument, 234 | type: AST_NODE_TYPES.ReturnStatement, 235 | toString: () => 236 | `return${ 237 | argument 238 | ? argument.type === 'JSXElement' 239 | ? ` (${DEFAULT_WHITESPACE}${node(argument)}${DEFAULT_WHITESPACE})` 240 | : ` ${node(argument)}` 241 | : '' 242 | };`, 243 | } 244 | } 245 | 246 | export const throwStatement: StringableASTNodeFn = ({ 247 | argument, 248 | ...other 249 | }) => { 250 | return { 251 | ...other, 252 | argument, 253 | type: AST_NODE_TYPES.ThrowStatement, 254 | toString: () => 255 | `throw${ 256 | argument 257 | ? // @ts-expect-error 258 | argument.type === 'JSXElement' || argument.type === 'JSXFragment' 259 | ? ` (${DEFAULT_WHITESPACE}${node(argument)}${DEFAULT_WHITESPACE})` 260 | : ` ${node(argument)}` 261 | : '' 262 | };`, 263 | } 264 | } 265 | 266 | /** 267 | * __UnaryExpression__ 268 | * 269 | * @example 270 | * 271 | * ```ts 272 | * const y = typeof x 273 | * ^^^^^^ 274 | * ++x 275 | * ^^ 276 | * ``` 277 | * 278 | * @returns {ESTree.UnaryExpression} 279 | */ 280 | export const unaryExpression: StringableASTNodeFn = ({ 281 | operator, 282 | argument, 283 | prefix, 284 | ...other 285 | }) => { 286 | return { 287 | ...other, 288 | operator, 289 | prefix, 290 | argument, 291 | type: AST_NODE_TYPES.UnaryExpression, 292 | toString: () => `${operator} ${node(argument)}`, 293 | } 294 | } 295 | 296 | /** 297 | * __ThisExpression__ 298 | * 299 | * @example 300 | * 301 | * ```js 302 | * // In `this.self` 'this' is a ThisExpression. 303 | * this.self 304 | * ⌃⌃⌃⌃ 305 | * ``` 306 | * 307 | * @returns {ESTree.ThisExpression} 308 | */ 309 | export const thisExpression: StringableASTNodeFn = ( 310 | node 311 | ) => ({ 312 | ...node, 313 | type: AST_NODE_TYPES.ThisExpression, 314 | toString: () => `this`, 315 | }) 316 | 317 | /** 318 | * __IfStatement__ 319 | * 320 | * @example 321 | * 322 | * ```ts 323 | * if (test) { 324 | * // consequant 325 | * } else { 326 | * // alternate 327 | * } 328 | * ⌃⌃⌃⌃^^^^^^^^ 329 | * ``` 330 | * 331 | * @returns {ESTree.IfStatement} 332 | */ 333 | export const ifStatement: StringableASTNodeFn = ({ 334 | test, 335 | alternate, 336 | consequent, 337 | ...other 338 | }) => ({ 339 | ...other, 340 | test, 341 | alternate, 342 | consequent, 343 | type: AST_NODE_TYPES.IfStatement, 344 | toString: () => 345 | `if (${node(test)}) ${node(consequent)} ${ 346 | alternate ? `else ${node(alternate)}` : '' 347 | }`, 348 | }) 349 | 350 | /** 351 | * __CatchClause__ 352 | * 353 | * @example 354 | * 355 | * ```ts 356 | * // always inside a try statement 357 | * catch (e) {} 358 | * ⌃⌃⌃⌃^^^^^^^^ 359 | * ``` 360 | * 361 | * @returns {ESTree.CatchClause} 362 | */ 363 | export const catchClause: StringableASTNodeFn = ({ 364 | body, 365 | param, 366 | ...other 367 | }) => ({ 368 | ...other, 369 | body, 370 | param, 371 | type: AST_NODE_TYPES.CatchClause, 372 | toString: () => `catch${param ? ` (${node(param)})` : ''} ${node(body)}`, 373 | }) 374 | 375 | /** 376 | * __TryStatement__ 377 | * 378 | * @example 379 | * 380 | * ```ts 381 | * try { 382 | * // block 383 | * } catch(e) { // <--- handler 384 | * 385 | * } finally {} // <--- finalizer 386 | * ⌃⌃⌃⌃^^^^^^^^ 387 | * ``` 388 | * 389 | * @returns {ESTree.TryStatement} 390 | */ 391 | export const tryStatement: StringableASTNodeFn = ({ 392 | block, 393 | finalizer, 394 | handler, 395 | ...other 396 | }) => ({ 397 | ...other, 398 | block, 399 | finalizer, 400 | handler, 401 | type: AST_NODE_TYPES.TryStatement, 402 | toString: () => 403 | `try ${node(block)} ${handler ? node(handler) : ''} ${ 404 | finalizer ? `finally ${node(finalizer)}` : '' 405 | }`, 406 | }) 407 | 408 | /** 409 | * __WithStatement__ 410 | * 411 | * @example 412 | * 413 | * ```ts 414 | * with (Math) { 415 | * a = PI * r * r; 416 | * x = r * cos(PI); 417 | * y = r * sin(PI / 2); 418 | * } 419 | * ``` 420 | * 421 | * @returns {ESTree.WithStatement} 422 | */ 423 | export const withStatement: StringableASTNodeFn = ({ 424 | object, 425 | body, 426 | ...other 427 | }) => ({ 428 | ...other, 429 | type: AST_NODE_TYPES.WithStatement, 430 | object, 431 | body, 432 | toString: () => `with (${node(object)}) ${node(body)}`, 433 | }) 434 | 435 | /** 436 | * __ImportExpression__ 437 | * 438 | * @example 439 | * 440 | * ```ts 441 | * import('some-path') 442 | * ⌃⌃⌃⌃^^^^^^^^^^^^^^^ 443 | * ``` 444 | * 445 | * @returns {ESTree.ImportExpression} 446 | */ 447 | export const importExpression: StringableASTNodeFn = ({ 448 | source, 449 | ...other 450 | }) => ({ 451 | ...other, 452 | type: AST_NODE_TYPES.ImportExpression, 453 | source, 454 | toString: () => `import(${node(source)})`, 455 | }) 456 | 457 | /** 458 | * __ImportDefaultSpecifier__ 459 | * 460 | * @example 461 | * 462 | * ```ts 463 | * import Hello from 'world' 464 | * ^^^^^ 465 | * ``` 466 | * 467 | * @returns {ESTree.ImportDefaultSpecifier} 468 | */ 469 | export const importDefaultSpecifier: StringableASTNodeFn< 470 | ESTree.ImportDefaultSpecifier 471 | > = ({ local, ...other }) => ({ 472 | ...other, 473 | local, 474 | type: AST_NODE_TYPES.ImportDefaultSpecifier, 475 | toString: () => local.name, 476 | }) 477 | 478 | /** 479 | * __ExportNamedDeclaration__ 480 | * 481 | * @example 482 | * 483 | * ```ts 484 | * export { Hello } from 'world' 485 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 486 | * ``` 487 | * 488 | * @returns {ESTree.ExportNamedDeclaration} 489 | */ 490 | export const exportNamedDeclaration: StringableASTNodeFn< 491 | ESTree.ExportNamedDeclaration 492 | > = ({ declaration, specifiers, source, ...other }) => { 493 | return { 494 | ...other, 495 | declaration, 496 | specifiers, 497 | source, 498 | type: AST_NODE_TYPES.ExportNamedDeclaration, 499 | toString: () => 500 | `export ${declaration ? node(declaration) : ''}${ 501 | specifiers.length 502 | ? `{ ${specifiers.map(node).map(String).join(', ')} }` 503 | : '' 504 | }${source ? `from ${node(source)}` : ''}`, 505 | } 506 | } 507 | 508 | /** 509 | * __ExportDefaultDeclaration__ 510 | * 511 | * @example 512 | * 513 | * ```ts 514 | * export default HelloWorld 515 | * ^^^^^^^^^^^^^^^^^^^^^^^^^ 516 | * ``` 517 | * 518 | * @returns {ESTree.ExportDefaultDeclaration} 519 | */ 520 | export const exportDefaultDeclaration: StringableASTNodeFn< 521 | ESTree.ExportDefaultDeclaration 522 | > = ({ declaration, ...other }) => { 523 | return { 524 | ...other, 525 | type: AST_NODE_TYPES.ExportDefaultDeclaration, 526 | declaration, 527 | 528 | toString: () => `export default ${node(declaration)}`, 529 | } 530 | } 531 | 532 | /** 533 | * __ExportAllDeclaration__ 534 | * 535 | * @example 536 | * 537 | * ```ts 538 | * export * from 'world' 539 | * ^^^^^^^^^^^^^^^^^^^^^^^^^ 540 | * ``` 541 | * ```ts 542 | * export * as Hello from 'world' 543 | * ^^^^^^^^^^^^^^^^^^^^^^^^^ 544 | * ``` 545 | * 546 | * @returns {ESTree.ExportAllDeclaration} 547 | */ 548 | export const exportAllDeclaration: StringableASTNodeFn< 549 | ESTree.ExportAllDeclaration, 550 | 'exported' 551 | > = ({ source, exported = null, ...other }) => { 552 | return { 553 | ...other, 554 | type: AST_NODE_TYPES.ExportAllDeclaration, 555 | source, 556 | exported, 557 | toString: () => 558 | `export * ${exported ? `as ${node(exported)} ` : ''}from ${node(source)}`, 559 | } 560 | } 561 | 562 | export const exportSpecifier: StringableASTNodeFn = ({ 563 | exported, 564 | local, 565 | ...other 566 | }) => { 567 | return { 568 | ...other, 569 | exported, 570 | local, 571 | type: AST_NODE_TYPES.ExportSpecifier, 572 | toString: () => 573 | local.name !== exported.name 574 | ? `${node(exported)} as ${node(local)}` 575 | : String(node(local)), 576 | } 577 | } 578 | 579 | export const importSpecifier: StringableASTNodeFn< 580 | ESTree.ImportSpecifier & { 581 | importKind?: TSESTree.ImportSpecifier['importKind'] 582 | } 583 | > = ({ imported, local, importKind = 'value', ...other }) => ({ 584 | ...other, 585 | type: AST_NODE_TYPES.ImportSpecifier, 586 | imported, 587 | importKind, 588 | local, 589 | toString: () => 590 | `${importKind === 'type' ? 'type ' : ''}${ 591 | local.name === imported.name 592 | ? imported.name 593 | : `${imported.name} as ${local.name}` 594 | }`, 595 | }) 596 | 597 | /** 598 | * __YieldExpression__ 599 | * 600 | * @example 601 | * 602 | * ```ts 603 | * const thing = yield someYieldExpression 604 | * ⌃⌃⌃⌃⌃⌃⌃⌃⌃⌃⌃⌃^^^^^^^^^^^^^ 605 | * ``` 606 | * 607 | * @returns {ESTree.YieldExpression} 608 | */ 609 | export const yieldExpression: StringableASTNodeFn = ({ 610 | argument, 611 | delegate, 612 | ...other 613 | }) => { 614 | return { 615 | ...other, 616 | argument, 617 | delegate, 618 | type: AST_NODE_TYPES.YieldExpression, 619 | toString: () => `yield ${argument ? node(argument) : ''}`, 620 | } 621 | } 622 | 623 | export const arrayExpression: StringableASTNodeFn = ({ 624 | elements, 625 | ...other 626 | }) => { 627 | return { 628 | ...other, 629 | type: AST_NODE_TYPES.ArrayExpression, 630 | elements, 631 | toString: () => 632 | `[${elements 633 | .filter((n): n is ESTree.SpreadElement | ESTree.Expression => 634 | Boolean(n) 635 | ) 636 | .map(node) 637 | .map(String) 638 | .join(', ')}]`, 639 | } 640 | } 641 | 642 | export const arrayPattern: StringableASTNodeFn = ({ 643 | elements, 644 | ...other 645 | }) => { 646 | return { 647 | ...other, 648 | type: AST_NODE_TYPES.ArrayPattern, 649 | elements, 650 | toString: () => 651 | // @ts-expect-error 652 | `[${elements.filter(Boolean).map(node).map(String).join(', ')}]`, 653 | } 654 | } 655 | 656 | export const updateExpression: StringableASTNodeFn = ({ 657 | argument, 658 | operator, 659 | prefix, 660 | ...other 661 | }) => { 662 | return { 663 | ...other, 664 | argument, 665 | operator, 666 | prefix, 667 | type: AST_NODE_TYPES.UpdateExpression, 668 | 669 | toString: () => 670 | `${ 671 | prefix ? `${operator}${node(argument)}` : `${node(argument)}${operator}` 672 | }`, 673 | } 674 | } 675 | 676 | export const expressionStatement: StringableASTNodeFn< 677 | ESTree.ExpressionStatement 678 | > = ({ expression, ...other }) => ({ 679 | ...other, 680 | expression, 681 | type: AST_NODE_TYPES.ExpressionStatement, 682 | toString: () => String(node(expression)), 683 | }) 684 | 685 | /** 686 | * __NewExpression__ 687 | * 688 | * @example 689 | * ```ts 690 | * new SomeThing() 691 | * ^^^^^^^^^^^^^^^ 692 | * ``` 693 | */ 694 | export const newExpression: StringableASTNodeFn = ({ 695 | callee, 696 | arguments: argumentsParam, 697 | ...other 698 | }) => ({ 699 | ...other, 700 | callee, 701 | arguments: argumentsParam, 702 | type: AST_NODE_TYPES.NewExpression, 703 | toString: () => `new ${node(callee)}(${argumentsParam.map(node).join(', ')})`, 704 | }) 705 | 706 | export const property: StringableASTNodeFn< 707 | ESTree.Property, 708 | 'kind' | 'computed' | 'shorthand' | 'method' 709 | > = ({ 710 | kind = 'init', 711 | key, 712 | value, 713 | method = false, 714 | computed = false, 715 | shorthand = false, 716 | ...other 717 | }) => { 718 | return { 719 | ...other, 720 | key, 721 | kind, 722 | value, 723 | method, 724 | shorthand, 725 | computed, 726 | type: AST_NODE_TYPES.Property, 727 | toString: () => 728 | `${kind === 'init' ? '' : kind + ' '}${computed ? '[' : ''}${node(key)}${ 729 | computed ? ']' : '' 730 | }${kind !== 'init' ? '' : ': '}${ 731 | kind !== 'init' && isNodeOfType(value, 'FunctionExpression') 732 | ? methodOrPropertyFn(value) 733 | : node(value) 734 | }`, 735 | } 736 | } 737 | 738 | /** 739 | * __ObjectPattern__ 740 | * 741 | * @example 742 | * ```ts 743 | * function App({ a }) {} 744 | * ^^^^^ 745 | * ``` 746 | * @returns 747 | */ 748 | export const objectPattern: StringableASTNodeFn = ({ 749 | properties, 750 | ...other 751 | }) => { 752 | return { 753 | ...other, 754 | properties, 755 | type: AST_NODE_TYPES.ObjectPattern, 756 | toString: () => `{${properties.map(node).map(String).join(', ')}}`, 757 | } 758 | } 759 | 760 | /** 761 | * __SpreadElement__ 762 | * 763 | * @example 764 | * ```ts 765 | * const obj = { 766 | * ...spread 767 | * ^^^^^^^^^ 768 | * } 769 | * ``` 770 | * 771 | * @returns {ESTree.SpreadElement} 772 | */ 773 | export const spreadElement: StringableASTNodeFn = ({ 774 | argument, 775 | ...other 776 | }) => { 777 | return { 778 | ...other, 779 | argument, 780 | type: AST_NODE_TYPES.SpreadElement, 781 | toString: () => `...${node(argument)}`, 782 | } 783 | } 784 | 785 | /** 786 | * __RestElement__ 787 | * 788 | * @example 789 | * ```ts 790 | * const [a, ...b] = c 791 | * ^^^^ 792 | * ``` 793 | * 794 | * * @example 795 | * ```ts 796 | * const { a, ...b } = c 797 | * ^^^^ 798 | * ``` 799 | * 800 | * @returns {ESTree.RestElement} 801 | */ 802 | export const restElement: StringableASTNodeFn = ({ 803 | argument, 804 | ...other 805 | }) => { 806 | return { 807 | ...other, 808 | argument, 809 | type: AST_NODE_TYPES.RestElement, 810 | toString: () => `...${node(argument)}`, 811 | } 812 | } 813 | 814 | /** 815 | * __ObjectExpression__ 816 | * @example 817 | * ```ts 818 | * const x = { 819 | * key: value, 820 | * get x() { return 1 }, 821 | * } 822 | * ^^^^^^^^^^^^ 823 | * ``` 824 | */ 825 | export const objectExpression: StringableASTNodeFn = ({ 826 | properties, 827 | ...other 828 | }) => { 829 | return { 830 | ...other, 831 | properties, 832 | type: AST_NODE_TYPES.ObjectExpression, 833 | toString: () => 834 | `{${DEFAULT_WHITESPACE}${properties 835 | .map(node) 836 | .map(String) 837 | .join(`,${DEFAULT_WHITESPACE}`)}\n}`, 838 | } 839 | } 840 | 841 | export const emptyStatement: StringableASTNodeFn = ({ 842 | ...other 843 | }) => ({ 844 | ...other, 845 | type: AST_NODE_TYPES.EmptyStatement, 846 | toString: () => `;`, 847 | }) 848 | 849 | export const memberExpression: StringableASTNodeFn< 850 | ESTree.MemberExpression, 851 | 'computed' | 'optional' 852 | > = ({ object, property, computed = false, optional = false, ...other }) => ({ 853 | ...other, 854 | type: AST_NODE_TYPES.MemberExpression, 855 | computed, 856 | optional, 857 | object, 858 | property, 859 | toString: () => { 860 | const translatedNode = node(property) 861 | return `${node(object)}${ 862 | computed ? `[${translatedNode}]` : `.${translatedNode}` 863 | }` 864 | }, 865 | }) 866 | 867 | export const logicalExpression: StringableASTNodeFn< 868 | ESTree.LogicalExpression 869 | > = ({ left, right, operator, ...other }) => { 870 | return { 871 | ...other, 872 | left, 873 | right, 874 | operator, 875 | type: AST_NODE_TYPES.LogicalExpression, 876 | toString: () => `${node(left)} ${operator} ${node(right)}`, 877 | } 878 | } 879 | 880 | export const variableDeclarator: StringableASTNodeFn< 881 | ESTree.VariableDeclarator 882 | > = ({ id, init, ...other }) => { 883 | return { 884 | ...other, 885 | id, 886 | init, 887 | type: AST_NODE_TYPES.VariableDeclarator, 888 | toString: () => `${node(id)}${init ? ` = ${node(init)}` : ''}`, 889 | } 890 | } 891 | 892 | export const variableDeclaration: StringableASTNodeFn< 893 | ESTree.VariableDeclaration 894 | > = ({ declarations, kind, ...other }) => { 895 | return { 896 | ...other, 897 | declarations, 898 | kind, 899 | type: AST_NODE_TYPES.VariableDeclaration, 900 | toString: () => 901 | `${kind ? `${kind} ` : ''}${declarations 902 | .map(variableDeclarator) 903 | .map(String) 904 | .join()}`, 905 | } 906 | } 907 | 908 | export const importNamespaceSpecifier: StringableASTNodeFn< 909 | ESTree.ImportNamespaceSpecifier 910 | > = ({ local, ...other }) => { 911 | return { 912 | ...other, 913 | type: AST_NODE_TYPES.ImportNamespaceSpecifier, 914 | local, 915 | toString: () => `* as ${local.name}`, 916 | } 917 | } 918 | 919 | export const templateElement: StringableASTNodeFn = ({ 920 | value, 921 | ...other 922 | }) => { 923 | return { 924 | ...other, 925 | value, 926 | type: AST_NODE_TYPES.TemplateElement, 927 | toString: () => `${value.raw}`, 928 | } 929 | } 930 | 931 | export const importDeclaration: StringableASTNodeFn< 932 | ESTree.ImportDeclaration & { 933 | importKind?: TSESTree.ImportDeclaration['importKind'] 934 | } 935 | > = ({ specifiers, source, ...other }) => ({ 936 | ...other, 937 | type: AST_NODE_TYPES.ImportDeclaration, 938 | specifiers, 939 | source, 940 | toString: () => { 941 | if (!specifiers.length) { 942 | return `import '${source.value}'` 943 | } 944 | 945 | const defaultSpecifier = specifiers.find( 946 | (spec): spec is ESTree.ImportDefaultSpecifier => 947 | isNodeOfType(spec, 'ImportDefaultSpecifier') 948 | ) 949 | const otherSpecifiers = specifiers.filter( 950 | (spec): spec is ESTree.ImportSpecifier => 951 | isNodeOfType(spec, 'ImportSpecifier') 952 | ) 953 | 954 | const nameSpaceSpecifier = specifiers.find( 955 | (node): node is ESTree.ImportNamespaceSpecifier => 956 | isNodeOfType(node, 'ImportNamespaceSpecifier') 957 | ) 958 | 959 | const seperator = 960 | otherSpecifiers.length > 4 ? `,${DEFAULT_WHITESPACE}` : ', ' 961 | const leadOrEndSpecifier = otherSpecifiers.length > 4 ? '\n' : ' ' 962 | 963 | return `import ${other['importKind'] === 'type' ? 'type ' : ''}${ 964 | defaultSpecifier ? defaultSpecifier.local.name : '' 965 | }${ 966 | otherSpecifiers.length 967 | ? defaultSpecifier 968 | ? `, {${leadOrEndSpecifier}${otherSpecifiers 969 | .map(importSpecifier) 970 | .join(seperator)}${leadOrEndSpecifier}}` 971 | : `{${leadOrEndSpecifier}${otherSpecifiers 972 | .map(importSpecifier) 973 | .join(seperator)}${leadOrEndSpecifier}}` 974 | : '' 975 | }${ 976 | (otherSpecifiers.length || defaultSpecifier) && nameSpaceSpecifier 977 | ? ', ' 978 | : '' 979 | }${ 980 | nameSpaceSpecifier ? importNamespaceSpecifier(nameSpaceSpecifier) : '' 981 | } from '${source.value}'` 982 | }, 983 | }) 984 | 985 | export const bigIntLiteral: StringableASTNodeFn = ({ 986 | value, 987 | raw, 988 | bigint, 989 | ...other 990 | }) => ({ 991 | ...other, 992 | value, 993 | raw, 994 | bigint, 995 | type: AST_NODE_TYPES.Literal, 996 | toString: () => raw || String(value), 997 | }) 998 | 999 | export const regExpLiteral: StringableASTNodeFn = ({ 1000 | value, 1001 | raw, 1002 | regex, 1003 | ...other 1004 | }) => ({ 1005 | ...other, 1006 | value, 1007 | raw, 1008 | regex, 1009 | type: AST_NODE_TYPES.Literal, 1010 | toString: () => raw || String(value), 1011 | }) 1012 | 1013 | export const literal = ( 1014 | n: WithoutType | (string | number | boolean | null) 1015 | ): StringableASTNode => { 1016 | if ( 1017 | typeof n === 'string' || 1018 | typeof n === 'boolean' || 1019 | typeof n === 'number' || 1020 | n === null 1021 | ) { 1022 | return { 1023 | raw: typeof n === 'string' ? `\'${n}\'` : String(n), 1024 | value: n, 1025 | type: AST_NODE_TYPES.Literal, 1026 | toString: () => String(n), 1027 | } 1028 | } 1029 | 1030 | if ('bigint' in n) { 1031 | return bigIntLiteral(n as ESTree.BigIntLiteral) 1032 | } else if ('regex' in n) { 1033 | return regExpLiteral(n as ESTree.RegExpLiteral) 1034 | } else { 1035 | // @ts-expect-error 1036 | return { 1037 | ...n, 1038 | type: AST_NODE_TYPES.Literal, 1039 | toString: () => n.raw || String(n.value), 1040 | } 1041 | } 1042 | } 1043 | 1044 | export const identifier = ( 1045 | param: WithoutType | string 1046 | ): StringableASTNode => { 1047 | const name = typeof param === 'string' ? param : param.name 1048 | const other = typeof param === 'object' ? param : ({} as any) 1049 | return { 1050 | ...other, 1051 | type: AST_NODE_TYPES.Identifier, 1052 | name, 1053 | toString: () => name, 1054 | } 1055 | } 1056 | 1057 | export const doWhileStatement: StringableASTNodeFn = ({ 1058 | test, 1059 | body, 1060 | ...other 1061 | }) => ({ 1062 | ...other, 1063 | test, 1064 | body, 1065 | type: AST_NODE_TYPES.DoWhileStatement, 1066 | toString() { 1067 | return `do ${node(body)} while (${node(test)})` 1068 | }, 1069 | }) 1070 | 1071 | export const whileStatement: StringableASTNodeFn = ({ 1072 | test, 1073 | body, 1074 | ...other 1075 | }) => ({ 1076 | ...other, 1077 | test, 1078 | body, 1079 | type: AST_NODE_TYPES.WhileStatement, 1080 | toString() { 1081 | return `while (${node(test)}) ${node(body)}` 1082 | }, 1083 | }) 1084 | 1085 | export const switchCase: StringableASTNodeFn = ({ 1086 | consequent, 1087 | test, 1088 | ...other 1089 | }) => { 1090 | return { 1091 | ...other, 1092 | consequent, 1093 | test, 1094 | type: AST_NODE_TYPES.SwitchCase, 1095 | toString: () => 1096 | `${!test ? 'default' : `case ${node(test)}`}: ${consequent 1097 | .map(node) 1098 | .map(String) 1099 | .join('; ')};`, 1100 | } 1101 | } 1102 | 1103 | export const switchStatement: StringableASTNodeFn = ({ 1104 | cases, 1105 | discriminant, 1106 | ...other 1107 | }) => ({ 1108 | ...other, 1109 | toString: () => `switch (${node(discriminant)}) { 1110 | ${cases.map(switchCase).join(DEFAULT_WHITESPACE)}\n}`, 1111 | cases, 1112 | discriminant, 1113 | type: AST_NODE_TYPES.SwitchStatement, 1114 | }) 1115 | 1116 | export const templateLiteral: StringableASTNodeFn = ({ 1117 | expressions, 1118 | quasis, 1119 | ...other 1120 | }) => { 1121 | if (quasis.length < expressions.length) { 1122 | throw new Error( 1123 | 'invariant: quasis should always outnumber expressions in a TemplateLiteral' 1124 | ) 1125 | } 1126 | return { 1127 | ...other, 1128 | 1129 | type: AST_NODE_TYPES.TemplateLiteral, 1130 | quasis, 1131 | expressions, 1132 | toString: () => { 1133 | const range = Array.from({ length: quasis.length + expressions.length }) 1134 | return ( 1135 | '`' + 1136 | range 1137 | .map((_, index) => { 1138 | if (index % 2 === 0) { 1139 | return node(quasis[Math.floor(index / 2)]) 1140 | } else { 1141 | return `\${${node(expressions[Math.floor(index / 2)])}}` 1142 | } 1143 | }) 1144 | .map(String) 1145 | .join('') + 1146 | '`' 1147 | ) 1148 | }, 1149 | } 1150 | } 1151 | 1152 | export const forStatement: StringableASTNodeFn = ({ 1153 | body, 1154 | init, 1155 | test, 1156 | update, 1157 | ...other 1158 | }) => ({ 1159 | ...other, 1160 | init, 1161 | body, 1162 | test, 1163 | update, 1164 | type: AST_NODE_TYPES.ForStatement, 1165 | toString: () => 1166 | `for (${init ? node(init) : ''};${test ? node(test) : ''};${ 1167 | update ? node(update) : '' 1168 | }) ${node(body)}`, 1169 | }) 1170 | 1171 | export const forInStatement: StringableASTNodeFn = ({ 1172 | body, 1173 | left, 1174 | right, 1175 | ...other 1176 | }) => ({ 1177 | ...other, 1178 | body, 1179 | left, 1180 | right, 1181 | type: AST_NODE_TYPES.ForInStatement, 1182 | toString: () => `for (${node(left)} in ${node(right)}) ${node(body)}`, 1183 | }) 1184 | 1185 | export const forOfStatement: StringableASTNodeFn = ({ 1186 | body, 1187 | left, 1188 | right, 1189 | ...other 1190 | }) => ({ 1191 | ...other, 1192 | body, 1193 | left, 1194 | right, 1195 | type: AST_NODE_TYPES.ForOfStatement, 1196 | toString: () => `for (${node(left)} of ${node(right)}) ${node(body)}`, 1197 | }) 1198 | 1199 | export const continueStatement: StringableASTNodeFn< 1200 | ESTree.ContinueStatement 1201 | > = ({ label, ...other }) => ({ 1202 | ...other, 1203 | toString: () => `continue${label ? ` ${node(label)}` : ''}`, 1204 | label, 1205 | type: AST_NODE_TYPES.ContinueStatement, 1206 | }) 1207 | 1208 | export const breakStatement: StringableASTNodeFn = ({ 1209 | label, 1210 | ...other 1211 | }) => ({ 1212 | ...other, 1213 | toString: () => `break${label ? ` ${node(label)}` : ''}`, 1214 | label, 1215 | type: AST_NODE_TYPES.BreakStatement, 1216 | }) 1217 | 1218 | export const debuggerStatement: StringableASTNodeFn< 1219 | ESTree.DebuggerStatement 1220 | > = (node) => ({ 1221 | ...node, 1222 | toString: () => `debugger`, 1223 | type: AST_NODE_TYPES.DebuggerStatement, 1224 | }) 1225 | 1226 | export const conditionalExpression: StringableASTNodeFn< 1227 | ESTree.ConditionalExpression 1228 | > = ({ consequent, alternate, test, ...other }) => ({ 1229 | ...other, 1230 | toString: () => `${node(test)} ? ${node(consequent)} : ${node(alternate)}`, 1231 | consequent, 1232 | alternate, 1233 | test, 1234 | type: AST_NODE_TYPES.ConditionalExpression, 1235 | }) 1236 | 1237 | export const assignmentExpression: StringableASTNodeFn< 1238 | ESTree.AssignmentExpression 1239 | > = ({ left, right, operator, ...other }) => { 1240 | return { 1241 | ...other, 1242 | type: AST_NODE_TYPES.AssignmentExpression, 1243 | left, 1244 | right, 1245 | operator, 1246 | toString: () => `${node(left)}${operator as string}${node(right)}`, 1247 | } 1248 | } 1249 | 1250 | export const awaitExpression: StringableASTNodeFn = ({ 1251 | argument, 1252 | ...other 1253 | }) => ({ 1254 | ...other, 1255 | toString: () => `await ${node(argument)}`, 1256 | argument, 1257 | type: AST_NODE_TYPES.AwaitExpression, 1258 | }) 1259 | 1260 | /** 1261 | * __StaticBlock__ 1262 | * 1263 | * @example 1264 | * ```ts 1265 | * class A { 1266 | * // only applicable inside a class 1267 | * static { } 1268 | * ^^^^^^^^^^ 1269 | * } 1270 | * ``` 1271 | */ 1272 | export const staticBlock: StringableASTNodeFn = ({ 1273 | body, 1274 | ...other 1275 | }) => { 1276 | return { 1277 | ...other, 1278 | body, 1279 | type: AST_NODE_TYPES.StaticBlock, 1280 | toString: () => 1281 | `static {\n${body.map(node).map(String).join(DEFAULT_WHITESPACE)}\n}`, 1282 | } 1283 | } 1284 | 1285 | export const functionDeclaration: StringableASTNodeFn< 1286 | ESTree.FunctionDeclaration, 1287 | 'generator' | 'async' 1288 | > = ({ body, async = false, id, generator = false, params, ...other }) => ({ 1289 | ...other, 1290 | type: AST_NODE_TYPES.FunctionDeclaration, 1291 | body, 1292 | async, 1293 | id, 1294 | generator, 1295 | params, 1296 | toString: () => 1297 | `${async ? 'async ' : ''}function ${id ? node(id) : ''}(${params 1298 | .map(node) 1299 | .map(String) 1300 | .join(', ')}) ${node(body)}`, 1301 | }) 1302 | 1303 | export const methodOrPropertyFn = ({ 1304 | params, 1305 | body, 1306 | }: ESTree.FunctionExpression) => { 1307 | return `(${params.map(node).join(', ')}) ${node(body)}` 1308 | } 1309 | 1310 | export const methodDefinition: StringableASTNodeFn = ({ 1311 | computed, 1312 | key, 1313 | kind, 1314 | value, 1315 | ...other 1316 | }) => { 1317 | return { 1318 | ...other, 1319 | computed, 1320 | key, 1321 | kind, 1322 | value, 1323 | type: AST_NODE_TYPES.MethodDefinition, 1324 | toString: () => 1325 | `${computed ? `[${node(key)}]` : node(key)} ${methodOrPropertyFn(value)}`, 1326 | } 1327 | } 1328 | 1329 | export const propertyDefinition: StringableASTNodeFn< 1330 | ESTree.PropertyDefinition 1331 | > = ({ computed, key, static: staticKeyWord, value, ...other }) => { 1332 | return { 1333 | ...other, 1334 | computed, 1335 | key, 1336 | static: staticKeyWord, 1337 | value, 1338 | type: AST_NODE_TYPES.PropertyDefinition, 1339 | toString: () => `UNIMPLEMENTED`, 1340 | } 1341 | } 1342 | 1343 | export const classBody: StringableASTNodeFn = ({ 1344 | body, 1345 | ...other 1346 | }) => { 1347 | return { 1348 | ...other, 1349 | type: AST_NODE_TYPES.ClassBody, 1350 | body, 1351 | toString: () => 1352 | body.length 1353 | ? `${DEFAULT_WHITESPACE}${body 1354 | .map(node) 1355 | .map(String) 1356 | .join(DEFAULT_WHITESPACE)}\n` 1357 | : '', 1358 | } 1359 | } 1360 | 1361 | export const classDeclaration: StringableASTNodeFn = ({ 1362 | superClass, 1363 | id, 1364 | body, 1365 | ...other 1366 | }) => { 1367 | return { 1368 | ...other, 1369 | type: AST_NODE_TYPES.ClassDeclaration, 1370 | superClass, 1371 | body, 1372 | id, 1373 | toString: () => 1374 | `class${id ? ` ${node(id)}` : ''}${ 1375 | superClass ? ` extends ${node(superClass)}` : '' 1376 | } {${node(body)}}`, 1377 | } 1378 | } 1379 | 1380 | export const classExpression: StringableASTNodeFn = ({ 1381 | superClass, 1382 | id, 1383 | body, 1384 | ...other 1385 | }) => { 1386 | return { 1387 | ...other, 1388 | type: AST_NODE_TYPES.ClassExpression, 1389 | superClass, 1390 | body, 1391 | id, 1392 | toString: () => 1393 | String(classDeclaration({ superClass, id: id || null, body, ...other })), 1394 | } 1395 | } 1396 | 1397 | export const program: StringableASTNodeFn = ({ 1398 | body, 1399 | ...other 1400 | }) => ({ 1401 | ...other, 1402 | type: AST_NODE_TYPES.Program, 1403 | toString: () => body.map(node).map(String).join('\n'), 1404 | body, 1405 | }) 1406 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/ts-nodes.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/types' 2 | 3 | import type { StringableASTNodeFn } from './types' 4 | import { node } from './utils/node' 5 | 6 | /** 7 | * __TSAsExpression__ 8 | * 9 | * @example 10 | * ```ts 11 | * const x = 'hello' as string 12 | * ^^^^^^^^^^^^^^^^^ 13 | * ``` 14 | * 15 | * @returns {TSESTree.TSAsExpression} 16 | */ 17 | export const tsAsExpression: StringableASTNodeFn = ({ 18 | expression, 19 | typeAnnotation, 20 | ...other 21 | }) => { 22 | return { 23 | ...other, 24 | expression, 25 | typeAnnotation, 26 | type: AST_NODE_TYPES.TSAsExpression, 27 | toString: () => `${node(expression)} as ${node(typeAnnotation)}`, 28 | } 29 | } 30 | 31 | /** 32 | * __TSStringKeyword__ 33 | * 34 | * @example 35 | * ```ts 36 | * const x = 'hello' as string 37 | * ^^^^^^ 38 | * ``` 39 | * 40 | * @returns {TSESTree.TSStringKeyword} 41 | */ 42 | export const tsStringKeyword: StringableASTNodeFn = ({ 43 | ...other 44 | }) => { 45 | return { 46 | ...other, 47 | type: AST_NODE_TYPES.TSStringKeyword, 48 | toString: () => `string`, 49 | } 50 | } 51 | 52 | /** 53 | * __TSAnyKeyword__ 54 | * 55 | * @example 56 | * ```ts 57 | * const x = 'hello' as any 58 | * ^^^ 59 | * ``` 60 | * 61 | * @returns {TSESTree.TSAnyKeyword} 62 | */ 63 | export const tsAnyKeyword: StringableASTNodeFn = ({ 64 | ...other 65 | }) => { 66 | return { 67 | ...other, 68 | type: AST_NODE_TYPES.TSAnyKeyword, 69 | toString: () => `any`, 70 | } 71 | } 72 | 73 | /** 74 | * __TSTypeReference__ 75 | * 76 | * @example 77 | * ```ts 78 | * type World = string 79 | * 80 | * const x = 'hello' as World 81 | * ^^^^^^^ 82 | * ``` 83 | * 84 | * @returns {TSESTree.TSTypeReference} 85 | */ 86 | export const tsTypeReference: StringableASTNodeFn = ({ 87 | typeName, 88 | typeParameters, 89 | ...other 90 | }) => { 91 | return { 92 | ...other, 93 | typeName, 94 | typeParameters, 95 | type: AST_NODE_TYPES.TSTypeReference, 96 | toString: () => 97 | `${node(typeName)}${typeParameters ? node(typeParameters) : ''}`, 98 | } 99 | } 100 | 101 | /** 102 | * __TSNullKeyword__ 103 | * 104 | * @example 105 | * ```ts 106 | * const x = 'hello' as null 107 | * ^^^^ 108 | * ``` 109 | * 110 | * @returns {TSESTree.TSNullKeyword} 111 | */ 112 | export const tsNullKeyword: StringableASTNodeFn = ({ 113 | ...other 114 | }) => { 115 | return { 116 | ...other, 117 | type: AST_NODE_TYPES.TSNullKeyword, 118 | toString: () => `null`, 119 | } 120 | } 121 | 122 | export const tsUnknownKeyword: StringableASTNodeFn< 123 | TSESTree.TSUnknownKeyword 124 | > = ({ ...other }) => { 125 | return { 126 | ...other, 127 | type: AST_NODE_TYPES.TSUnknownKeyword, 128 | toString: () => `unknown`, 129 | } 130 | } 131 | 132 | export const tsBooleanKeyword: StringableASTNodeFn< 133 | TSESTree.TSBooleanKeyword 134 | > = ({ ...other }) => { 135 | return { 136 | ...other, 137 | type: AST_NODE_TYPES.TSBooleanKeyword, 138 | toString: () => `boolean`, 139 | } 140 | } 141 | 142 | export const tsReadonlyKeyword: StringableASTNodeFn< 143 | TSESTree.TSReadonlyKeyword 144 | > = ({ ...other }) => { 145 | return { 146 | ...other, 147 | type: AST_NODE_TYPES.TSReadonlyKeyword, 148 | toString: () => `readonly`, 149 | } 150 | } 151 | 152 | export const tsEmptyBodyFunctionExpression: StringableASTNodeFn< 153 | TSESTree.TSEmptyBodyFunctionExpression 154 | > = ({ returnType, ...other }) => { 155 | return { 156 | returnType, 157 | ...other, 158 | type: AST_NODE_TYPES.TSEmptyBodyFunctionExpression, 159 | toString: () => `function(){}`, 160 | } 161 | } 162 | 163 | export const tsQualifiedName: StringableASTNodeFn = ({ 164 | left, 165 | right, 166 | ...other 167 | }) => { 168 | return { 169 | left, 170 | right, 171 | ...other, 172 | type: AST_NODE_TYPES.TSQualifiedName, 173 | toString: () => `${node(left)}.${node(right)}`, 174 | } 175 | } 176 | 177 | export const tsTypeParameterInstantiation: StringableASTNodeFn< 178 | TSESTree.TSTypeParameterInstantiation 179 | > = ({ params, ...other }) => { 180 | return { 181 | params, 182 | ...other, 183 | type: AST_NODE_TYPES.TSTypeParameterInstantiation, 184 | toString: () => `<${params.map(node).join(', ')}>`, 185 | } 186 | } 187 | 188 | export const tsTypeParameterDeclaration: StringableASTNodeFn< 189 | TSESTree.TSTypeParameterDeclaration 190 | > = ({ params, ...other }) => { 191 | return { 192 | params, 193 | ...other, 194 | type: AST_NODE_TYPES.TSTypeParameterDeclaration, 195 | toString: () => `<${params.map(node).join(', ')}>`, 196 | } 197 | } 198 | 199 | /** 200 | * __TSTypeOperator__ 201 | * 202 | * @example 203 | * ``` 204 | * type X = 'hello' 205 | * type Y = typeof X 206 | * ^^^^^^^^ 207 | * ``` 208 | */ 209 | export const tsTypeOperator: StringableASTNodeFn = ({ 210 | typeAnnotation, 211 | operator, 212 | ...other 213 | }) => { 214 | return { 215 | ...other, 216 | typeAnnotation, 217 | operator, 218 | type: AST_NODE_TYPES.TSTypeOperator, 219 | toString: () => 220 | `${operator}${typeAnnotation ? ` ${node(typeAnnotation)}` : ''}`, 221 | } 222 | } 223 | 224 | /** 225 | * __TSTypeQuery__ 226 | * 227 | * @example 228 | * ``` 229 | * type X = typeof 'hello' 230 | * ``` 231 | */ 232 | export const tsTypeQuery: StringableASTNodeFn = ({ 233 | exprName, 234 | typeParameters, 235 | ...other 236 | }) => { 237 | return { 238 | ...other, 239 | typeParameters, 240 | exprName, 241 | type: AST_NODE_TYPES.TSTypeQuery, 242 | toString: () => 243 | `typeof ${node(exprName)}${typeParameters ? node(typeParameters) : ''}`, 244 | } 245 | } 246 | 247 | /** 248 | * FIXME Implementation does not meet spec 249 | */ 250 | export const tsTypeParameter: StringableASTNodeFn = ({ 251 | name, 252 | ...other 253 | }) => { 254 | return { 255 | ...other, 256 | name, 257 | type: AST_NODE_TYPES.TSTypeParameter, 258 | toString: () => `${node(name)}`, 259 | } 260 | } 261 | 262 | export const tsLiteralType: StringableASTNodeFn = ({ 263 | literal, 264 | ...other 265 | }) => { 266 | return { 267 | literal, 268 | ...other, 269 | type: AST_NODE_TYPES.TSLiteralType, 270 | toString: () => `${node(literal)}`, 271 | } 272 | } 273 | 274 | /** 275 | * @example 276 | * ``` 277 | * element!.select() 278 | * ^^^^^^^^ 279 | * ``` 280 | */ 281 | export const tsNonNullExpression: StringableASTNodeFn< 282 | TSESTree.TSNonNullExpression 283 | > = ({ expression, ...other }) => { 284 | return { 285 | expression, 286 | ...other, 287 | type: AST_NODE_TYPES.TSNonNullExpression, 288 | toString: () => `${node(expression)}!`, 289 | } 290 | } 291 | 292 | /** 293 | * __TSTypeAliasDeclaration__ 294 | * @example 295 | * ``` 296 | * type Alias = number | boolean 297 | * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 298 | * ``` 299 | */ 300 | export const tsTypeAliasDeclaration: StringableASTNodeFn< 301 | TSESTree.TSTypeAliasDeclaration 302 | > = ({ id, typeAnnotation, typeParameters, declare, ...other }) => { 303 | return { 304 | id, 305 | typeAnnotation, 306 | typeParameters, 307 | declare, 308 | ...other, 309 | type: AST_NODE_TYPES.TSTypeAliasDeclaration, 310 | toString: () => 311 | `${declare ? 'declare ' : ''}type ${node(id)}${ 312 | typeParameters ? `${node(typeParameters)}` : '' 313 | } = ${node(typeAnnotation)}`, 314 | } 315 | } 316 | 317 | /** 318 | * __TSUnionType__ 319 | * @example 320 | * ``` 321 | * type Alias = number | boolean 322 | * ^^^^^^^^^^^^^^^^ 323 | * ``` 324 | */ 325 | export const tsUnionType: StringableASTNodeFn = ({ 326 | types, 327 | ...other 328 | }) => { 329 | return { 330 | types, 331 | ...other, 332 | type: AST_NODE_TYPES.TSUnionType, 333 | toString: () => `${types.map(node).join(' | ')}`, 334 | } 335 | } 336 | 337 | /** 338 | * __TSIntersectionType__ 339 | * @example 340 | * ``` 341 | * type Alias = number & boolean 342 | * ^^^^^^^^^^^^^^^^ 343 | * ``` 344 | */ 345 | export const tsIntersectionType: StringableASTNodeFn< 346 | TSESTree.TSIntersectionType 347 | > = ({ types, ...other }) => { 348 | return { 349 | types, 350 | ...other, 351 | type: AST_NODE_TYPES.TSIntersectionType, 352 | toString: () => `${types.map(node).join(' & ')}`, 353 | } 354 | } 355 | 356 | /** 357 | * __TSArrayType__ 358 | * @example 359 | * ``` 360 | * type Alias = number[] 361 | * ^^^^^^^^ 362 | * ``` 363 | */ 364 | export const tsArrayType: StringableASTNodeFn = ({ 365 | elementType, 366 | ...other 367 | }) => { 368 | return { 369 | elementType, 370 | ...other, 371 | type: AST_NODE_TYPES.TSArrayType, 372 | toString: () => `${node(elementType)}[]`, 373 | } 374 | } 375 | 376 | export const tsSatisfiesExpression: StringableASTNodeFn< 377 | TSESTree.TSSatisfiesExpression 378 | > = ({ expression, typeAnnotation, ...other }) => { 379 | return { 380 | ...other, 381 | expression, 382 | typeAnnotation, 383 | type: AST_NODE_TYPES.TSSatisfiesExpression, 384 | toString: () => `${node(expression)} satisfies ${node(typeAnnotation)}`, 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { Node as ESTreeNode, JSXSpreadChild } from 'estree-jsx' 2 | import type { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/types' 3 | import type { Rule } from 'eslint' 4 | 5 | export type EslintCodemodUtilsBaseNode = 6 | | ESTreeNode 7 | | { type: keyof typeof AST_NODE_TYPES } 8 | | JSXSpreadChild 9 | 10 | export type WithoutType = Omit 11 | 12 | export type StringableASTNode = T & { 13 | toString(): string 14 | } 15 | 16 | export type StringableASTNodeFn< 17 | EstreeNodeType extends EslintCodemodUtilsBaseNode, 18 | Key extends keyof EstreeNodeType = 'type' 19 | > = ( 20 | node: WithoutType< 21 | Key extends 'type' 22 | ? EstreeNodeType 23 | : Omit & 24 | Pick, Key> & { type: any } 25 | > 26 | ) => StringableASTNode 27 | 28 | export type EslintNode = EslintCodemodUtilsBaseNode & 29 | Partial 30 | 31 | export type TSEslintNode = EslintCodemodUtilsBaseNode & 32 | Pick 33 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/closest-of-type.ts: -------------------------------------------------------------------------------- 1 | import type { EslintNode } from '../types' 2 | import { isNodeOfType } from './is-node-of-type' 3 | 4 | /** 5 | * Traverses the node's parents until the specified `type` is found. If no `type` is found 6 | * in the traversal it will return `null`. 7 | */ 8 | export function closestOfType( 9 | node: EslintNode, 10 | type: NodeType 11 | ): Extract | null { 12 | if (isNodeOfType(node, type)) { 13 | return node 14 | } 15 | 16 | if (node.parent) { 17 | return closestOfType(node.parent, type) 18 | } 19 | 20 | return null 21 | } 22 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/get-first-comment-in-file.ts: -------------------------------------------------------------------------------- 1 | import type { SourceCode } from 'eslint' 2 | import type { Comment } from 'estree-jsx' 3 | 4 | export function getFirstCommentInFile(source: SourceCode): Comment { 5 | return source.getAllComments()[0] 6 | } 7 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/get-identifier-in-parent-scope.ts: -------------------------------------------------------------------------------- 1 | import type { Scope } from 'eslint' 2 | 3 | /** 4 | * A useful function for finding a variable / variables value. This function 5 | * traverses the scopes upwards until it arrives at the global scope. It will 6 | * return when it exhausts the scopes or finds the variable. 7 | * 8 | * @param scope The current scope the variable exists in: 9 | * @param identifierName The identifier / variable we're trying to look up 10 | * @returns 11 | */ 12 | export function getIdentifierInParentScope( 13 | scope: Scope.Scope, 14 | identifierName: string 15 | ): Scope.Variable | null { 16 | let traversingScope: Scope.Scope | null = scope 17 | 18 | while (traversingScope && traversingScope.type !== 'global') { 19 | const matchedVariable = traversingScope.variables.find( 20 | (variable) => variable.name === identifierName 21 | ) 22 | 23 | if (matchedVariable) { 24 | return matchedVariable 25 | } 26 | 27 | traversingScope = traversingScope.upper 28 | } 29 | 30 | return null 31 | } 32 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/get-node-after-comment.ts: -------------------------------------------------------------------------------- 1 | import type * as eslint from 'eslint' 2 | import type { Comment } from 'estree-jsx' 3 | 4 | /** 5 | * @example 6 | * ```tsx 7 | * // this is the search comment 8 | * const findThisNode = 10 9 | * ^^^^^^^^^^^^^^^^^^^^^^^ 10 | * ``` 11 | */ 12 | export function getNodeAfterComment( 13 | source: eslint.SourceCode, 14 | comment: Comment 15 | ): eslint.Rule.Node | null { 16 | const token = source.getTokenAfter(comment) 17 | 18 | if (token) { 19 | return source.getNodeByRangeIndex(token.range[1]) as eslint.Rule.Node 20 | } 21 | 22 | return null 23 | } 24 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/identity.ts: -------------------------------------------------------------------------------- 1 | export const identity = (node: T) => node 2 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { node } from './node' 2 | export { isNodeOfType } from './is-node-of-type' 3 | export { getIdentifierInParentScope } from './get-identifier-in-parent-scope' 4 | export { insertAtStartOfFile } from './insert-at-start-of-file' 5 | export { getFirstCommentInFile } from './get-first-comment-in-file' 6 | export { getNodeAfterComment } from './get-node-after-comment' 7 | export { closestOfType } from './closest-of-type' 8 | export { insertJSXAttribute } from './insert-jsx-attribute' 9 | export * from './utils' 10 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/insert-at-start-of-file.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from 'eslint' 2 | 3 | /** 4 | * Insert a generic string as a `Rule.Fix` at the start of a file. 5 | * 6 | * @returns {Rule.Fix} 7 | */ 8 | export function insertAtStartOfFile( 9 | fixer: Rule.RuleFixer, 10 | str: string 11 | ): Rule.Fix { 12 | return fixer.insertTextBeforeRange([0, 0], str) 13 | } 14 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/insert-jsx-attribute.tsx: -------------------------------------------------------------------------------- 1 | import type * as ESTree from 'estree-jsx' 2 | import { jsxAttribute, jsxElement, jsxIdentifier, jsxOpeningElement } from '..' 3 | 4 | /** 5 | * Adds a prop to a JSXElement. 6 | * 7 | * @author Sam Scheding 8 | * @example 9 | * ``` 10 | * const boxNode = jsxElement({ ...node }) 11 | * console.log(boxNode.toString()) // --> "" 12 | * 13 | * const boxNodeWithProp = insertJSXAttribute(node, 'display', 'block') 14 | * console.log(boxNodeWithProp.toString()) // --> "" 15 | * ``` 16 | */ 17 | export function insertJSXAttribute( 18 | node: ESTree.JSXElement, 19 | propName: string, 20 | propValue: ESTree.JSXAttribute['value'] 21 | ): ESTree.JSXElement { 22 | const { openingElement } = node 23 | const { attributes = [] } = openingElement 24 | return jsxElement({ 25 | ...node, 26 | openingElement: jsxOpeningElement({ 27 | ...openingElement, 28 | attributes: [ 29 | ...attributes, 30 | jsxAttribute({ 31 | name: jsxIdentifier(propName), 32 | value: propValue, 33 | }), 34 | ], 35 | }), 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/is-node-of-type.ts: -------------------------------------------------------------------------------- 1 | import type { EslintNode } from '../types' 2 | 3 | /** 4 | * Given a valid node return true if the node is of the specified type. 5 | * 6 | * This function uses the `is` assertion to resolve the correct TS type for the consumer. 7 | * 8 | * @return boolean 9 | */ 10 | export function isNodeOfType( 11 | node: EslintNode, 12 | type: K 13 | ): node is Extract { 14 | if (!(node && node['type'])) { 15 | return false 16 | } 17 | 18 | return node.type === type 19 | } 20 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/jsx.ts: -------------------------------------------------------------------------------- 1 | import type { JSXElement } from 'estree-jsx' 2 | import { 3 | jsxAttribute, 4 | jsxClosingElement, 5 | jsxElement, 6 | jsxIdentifier, 7 | jsxOpeningElement, 8 | literal, 9 | } from '..' 10 | import type { StringableASTNode } from '../types' 11 | 12 | export const jsx = function ( 13 | type: string | ((...args: any[]) => StringableASTNode), 14 | props: Record, 15 | ...children: any[] 16 | ): StringableASTNode | undefined { 17 | if (!type) { 18 | return 19 | } 20 | 21 | if (typeof type === 'function') { 22 | // merge props and children 23 | const componentProps = { 24 | ...props, 25 | // @ts-expect-error 26 | children: (props.children || []).concat(children), 27 | } 28 | // render the function 29 | const element = type(componentProps) 30 | return element 31 | } 32 | 33 | const filteredChildren = children.filter(Boolean) 34 | const selfClosing = !Boolean(filteredChildren.length) 35 | 36 | const name = jsxIdentifier(type) 37 | return jsxElement({ 38 | openingElement: jsxOpeningElement({ 39 | name, 40 | selfClosing, 41 | attributes: Object.keys(props).map((prop) => { 42 | return jsxAttribute({ 43 | name: jsxIdentifier(prop), 44 | value: literal('hello'), 45 | }) 46 | }), 47 | }), 48 | closingElement: selfClosing ? null : jsxClosingElement({ name }), 49 | // @ts-expect-error 50 | children: filteredChildren.map(jsx), 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/node.ts: -------------------------------------------------------------------------------- 1 | import { typeToHelperLookup } from '../constants' 2 | import type { EslintCodemodUtilsBaseNode, StringableASTNode } from '../types' 3 | 4 | export type NodeMap< 5 | T extends EslintCodemodUtilsBaseNode = EslintCodemodUtilsBaseNode 6 | > = { 7 | [E in T as E['type']]: (eventNodeListener: E) => StringableASTNode 8 | } 9 | 10 | /** 11 | * Internally focused function to help resolve / parse the AST. It hands off to the 12 | * `typeToHelperLookup` map to apply the correct transformation. 13 | * 14 | * In theory this function can be applied to any valid esprima node blindly and 15 | * it will correctly resolve to an `eslint-codemod-utils` stringable node. 16 | * 17 | * @internal 18 | */ 19 | export const node = ( 20 | estNode: EstreeNodeType 21 | ): StringableASTNode => { 22 | // @ts-expect-error 23 | return typeToHelperLookup[estNode.type](estNode) 24 | } 25 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/lib/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import * as ESTree from 'estree-jsx' 2 | import { 3 | identifier, 4 | importDeclaration, 5 | importDefaultSpecifier, 6 | importSpecifier, 7 | literal, 8 | } from '../nodes' 9 | import type { StringableASTNode } from '../types' 10 | import { isNodeOfType } from './is-node-of-type' 11 | 12 | export function hasJSXAttribute( 13 | node: ESTree.JSXElement, 14 | attributeName: string 15 | ) { 16 | if (!node.openingElement) return false 17 | 18 | if (!node.openingElement.attributes.length) return false 19 | 20 | return node.openingElement.attributes.some( 21 | (attr) => 22 | isNodeOfType(attr, 'JSXAttribute') && attr.name.name === attributeName 23 | ) 24 | } 25 | 26 | export function hasJSXChild( 27 | node: ESTree.JSXElement, 28 | childIdentifier: string 29 | ): boolean { 30 | const jsxIdentifierMatch = 31 | isNodeOfType(node.openingElement.name, 'JSXIdentifier') && 32 | node.openingElement.name.name && 33 | node.openingElement.name.name === childIdentifier 34 | 35 | return ( 36 | jsxIdentifierMatch || 37 | Boolean( 38 | node.children && 39 | node.children 40 | .filter((child): child is ESTree.JSXElement => 41 | isNodeOfType(child, 'JSXElement') 42 | ) 43 | .find((child) => hasJSXChild(child, childIdentifier)) 44 | ) 45 | ) 46 | } 47 | 48 | /** 49 | * Whether a declaration does or does not include a specified source. 50 | * 51 | * @param declaration 52 | * @param source 53 | * @returns 54 | */ 55 | export function hasImportDeclaration( 56 | declaration: ESTree.ImportDeclaration, 57 | source: string 58 | ): boolean { 59 | return declaration.source.value === source 60 | } 61 | 62 | /** 63 | * 64 | * @param declaration 65 | * @param specifierId 66 | */ 67 | export function hasImportSpecifier( 68 | declaration: ESTree.ImportDeclaration, 69 | importName: string | 'default' 70 | ) { 71 | if (importName === 'default') { 72 | return declaration.specifiers.some((spec) => 73 | isNodeOfType(spec, 'ImportDefaultSpecifier') 74 | ) 75 | } 76 | 77 | return declaration.specifiers 78 | .filter((spec): spec is ESTree.ImportSpecifier => 79 | isNodeOfType(spec, 'ImportSpecifier') 80 | ) 81 | .some((node) => node.imported.name === importName) 82 | } 83 | 84 | /** 85 | * Appends or adds an import specifier to an existing import declaration. 86 | * 87 | * Does not validate whether the insertion is already present. 88 | * 89 | * @param declaration 90 | * @param importName 91 | * @param specifierAlias 92 | * @returns {StringableASTNode} 93 | */ 94 | export function insertImportSpecifier( 95 | declaration: ESTree.ImportDeclaration, 96 | importName: string | 'default', 97 | specifierAlias?: string 98 | ): StringableASTNode { 99 | if (importName === 'default' && !specifierAlias) { 100 | throw new Error( 101 | 'A specifier name must be provided when inserting the default import.' 102 | ) 103 | } 104 | 105 | const id = identifier(importName) 106 | 107 | return importDeclaration({ 108 | ...declaration, 109 | specifiers: declaration.specifiers.concat( 110 | importName === 'default' 111 | ? importDefaultSpecifier({ 112 | // @ts-expect-error no undefined on identifier 113 | local: identifier(specifierAlias), 114 | }) 115 | : importSpecifier({ 116 | imported: identifier(importName), 117 | local: specifierAlias ? identifier(specifierAlias) : id, 118 | }) 119 | ), 120 | }) 121 | } 122 | 123 | /** 124 | * @example 125 | * ```tsx 126 | * insertImportDeclaration('source', ['specifier', 'second']) 127 | * 128 | * // produces 129 | * import { specifier, second } from 'source' 130 | * ``` 131 | * 132 | * @example 133 | * ```tsx 134 | * * insertImportDeclaration('source', ['specifier', { imported: 'second', local: 'other' }]) 135 | * 136 | * // produces 137 | * import { specifier, second as other } from 'source' 138 | * ``` 139 | */ 140 | export function insertImportDeclaration( 141 | source: string, 142 | specifiers: (string | { local: string; imported: string })[] 143 | ): StringableASTNode { 144 | return importDeclaration({ 145 | source: literal(source), 146 | specifiers: specifiers.map((spec) => { 147 | return spec === 'default' 148 | ? importDefaultSpecifier({ 149 | local: identifier('__default'), 150 | }) 151 | : importSpecifier({ 152 | imported: 153 | typeof spec === 'string' 154 | ? identifier(spec) 155 | : identifier(spec.imported), 156 | local: 157 | typeof spec === 'string' 158 | ? identifier(spec) 159 | : identifier(spec.local), 160 | }) 161 | }), 162 | }) 163 | } 164 | 165 | /** 166 | * Removes an import specifier to an existing import declaration. 167 | * 168 | * @param declaration 169 | * @param importName 170 | * @returns {StringableASTNode} 171 | */ 172 | export function removeImportSpecifier( 173 | declaration: ESTree.ImportDeclaration, 174 | importName: string | 'default' 175 | ): StringableASTNode { 176 | return importDeclaration({ 177 | ...declaration, 178 | specifiers: declaration.specifiers.filter((spec) => 179 | importName === 'default' 180 | ? spec.type !== 'ImportDefaultSpecifier' 181 | : !( 182 | isNodeOfType(spec, 'ImportSpecifier') && 183 | spec.imported.name === importName 184 | ) 185 | ), 186 | }) 187 | } 188 | -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-codemod-utils", 3 | "version": "1.9.0", 4 | "description": "A collection of AST helper functions for more complex ESLint rule fixes.", 5 | "author": "Alex Hinds", 6 | "source": "lib/index.ts", 7 | "main": "dist/cjs/index.js", 8 | "module": "dist/esm/index.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/DarkPurple141/eslint-codemod-utils.git" 12 | }, 13 | "keywords": [ 14 | "eslint", 15 | "codemods", 16 | "jscodeshift", 17 | "espree" 18 | ], 19 | "publishConfig": { 20 | "main": "dist/cjs/index.js", 21 | "module": "dist/esm/index.js", 22 | "typings": "dist/esm/index.d.ts" 23 | }, 24 | "sideEffects": false, 25 | "files": [ 26 | "dist" 27 | ], 28 | "scripts": { 29 | "build": "tsc && tsc --project tsconfig.esm.json", 30 | "test": "vitest --run", 31 | "prepublish": "pnpm build" 32 | }, 33 | "license": "ISC", 34 | "dependencies": { 35 | "@types/estree-jsx": "^1.0.0", 36 | "@typescript-eslint/types": "^5.45.0" 37 | }, 38 | "devDependencies": { 39 | "@types/eslint": "^8.4.10", 40 | "@types/node": "^18.0.0", 41 | "@typescript-eslint/typescript-estree": "^5.45.0", 42 | "espree": "^9.3.1", 43 | "typescript": "^4.9.0", 44 | "vitest": "latest" 45 | }, 46 | "exports": { 47 | ".": { 48 | "module": { 49 | "default": "./dist/esm/index.js" 50 | }, 51 | "default": "./dist/cjs/index.js" 52 | }, 53 | "./jsx-runtime": { 54 | "module": { 55 | "default": "./dist/esm/jsx-runtime.js" 56 | }, 57 | "default": "./dist/cjs/jsx-runtime.js" 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [] 4 | } -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "lib/**/*" 5 | ], 6 | "compilerOptions": { 7 | "module": "ES2020", 8 | "outDir": "dist/esm" 9 | }, 10 | "exclude": [ 11 | "**/__tests__/**" 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/eslint-codemod-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "lib/**/*" 5 | ], 6 | "compilerOptions": { 7 | "outDir": "dist/cjs" 8 | } 9 | } -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # eslint-plugin-example 2 | 3 | ## 0.0.17 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [6dbd9a4] 8 | - Updated dependencies [8626b1d] 9 | - eslint-codemod-utils@1.9.0 10 | 11 | ## 0.0.16 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [0839e66] 16 | - eslint-codemod-utils@1.8.7 17 | 18 | ## 0.0.15 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [51662bd] 23 | - eslint-codemod-utils@1.8.6 24 | 25 | ## 0.0.14 26 | 27 | ### Patch Changes 28 | 29 | - Updated dependencies [dcb2602] 30 | - eslint-codemod-utils@1.8.5 31 | 32 | ## 0.0.13 33 | 34 | ### Patch Changes 35 | 36 | - Updated dependencies [dd2ee38] 37 | - eslint-codemod-utils@1.8.4 38 | 39 | ## 0.0.12 40 | 41 | ### Patch Changes 42 | 43 | - Updated dependencies [ad47185] 44 | - eslint-codemod-utils@1.8.3 45 | 46 | ## 0.0.11 47 | 48 | ### Patch Changes 49 | 50 | - Updated dependencies [3742023] 51 | - Updated dependencies [3742023] 52 | - eslint-codemod-utils@1.8.2 53 | 54 | ## 0.0.10 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies [1c195ad] 59 | - eslint-codemod-utils@1.8.1 60 | 61 | ## 0.0.9 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies [130388a] 66 | - Updated dependencies [07e3002] 67 | - eslint-codemod-utils@1.8.0 68 | 69 | ## 0.0.8 70 | 71 | ### Patch Changes 72 | 73 | - Updated dependencies [e1edbc8] 74 | - eslint-codemod-utils@1.7.0 75 | 76 | ## 0.0.7 77 | 78 | ### Patch Changes 79 | 80 | - Updated dependencies [b8bb315] 81 | - eslint-codemod-utils@1.6.3 82 | 83 | ## 0.0.6 84 | 85 | ### Patch Changes 86 | 87 | - Updated dependencies [5b0b8d1] 88 | - eslint-codemod-utils@1.6.2 89 | 90 | ## 0.0.5 91 | 92 | ### Patch Changes 93 | 94 | - Updated dependencies [1d848e1] 95 | - eslint-codemod-utils@1.6.1 96 | 97 | ## 0.0.4 98 | 99 | ### Patch Changes 100 | 101 | - Updated dependencies [24bca60] 102 | - Updated dependencies [1eaa1d7] 103 | - eslint-codemod-utils@1.6.0 104 | 105 | ## 0.0.3 106 | 107 | ### Patch Changes 108 | 109 | - Updated dependencies [590ec6f] 110 | - eslint-codemod-utils@1.5.1 111 | 112 | ## 0.0.2 113 | 114 | ### Patch Changes 115 | 116 | - Updated dependencies [dddc134] 117 | - eslint-codemod-utils@1.5.0 118 | 119 | ## 0.0.1 120 | 121 | ### Patch Changes 122 | 123 | - Updated dependencies [ca51ad9] 124 | - eslint-codemod-utils@1.4.0 125 | 126 | ## null 127 | 128 | ### Patch Changes 129 | 130 | - Updated dependencies [f003b75] 131 | - eslint-codemod-utils@1.3.4 132 | 133 | ## null 134 | 135 | ### Patch Changes 136 | 137 | - Updated dependencies [ea8a017] 138 | - eslint-codemod-utils@1.3.3 139 | 140 | ## null 141 | 142 | ### Patch Changes 143 | 144 | - Updated dependencies [1a18bf6] 145 | - eslint-codemod-utils@1.3.2 146 | 147 | ## null 148 | 149 | ### Patch Changes 150 | 151 | - Updated dependencies [8cb50d3] 152 | - eslint-codemod-utils@1.3.1 153 | 154 | ## null 155 | 156 | ### Patch Changes 157 | 158 | - Updated dependencies [75d4cef] 159 | - eslint-codemod-utils@1.3.0 160 | 161 | ## null 162 | 163 | ### Patch Changes 164 | 165 | - Updated dependencies [2e222e0] 166 | - eslint-codemod-utils@1.2.1 167 | 168 | ## null 169 | 170 | ### Patch Changes 171 | 172 | - Updated dependencies [e030ba2] 173 | - Updated dependencies [cd793bb] 174 | - eslint-codemod-utils@1.2.0 175 | 176 | ## null 177 | 178 | ### Patch Changes 179 | 180 | - Updated dependencies [8913da9] 181 | - eslint-codemod-utils@1.1.0 182 | 183 | ## null 184 | 185 | ### Patch Changes 186 | 187 | - Updated dependencies [064d923] 188 | - Updated dependencies [064d923] 189 | - eslint-codemod-utils@1.0.1 190 | 191 | ## null 192 | 193 | ### Patch Changes 194 | 195 | - Updated dependencies [0876a8d] 196 | - Updated dependencies [41f7c0f] 197 | - eslint-codemod-utils@1.0.0 198 | 199 | ## null 200 | 201 | ### Minor Changes 202 | 203 | - c9f2a5b: Adds additional config option to example rule/ 204 | 205 | ## null 206 | 207 | ### Patch Changes 208 | 209 | - Updated dependencies [dd41354] 210 | - eslint-codemod-utils@0.1.3 211 | 212 | ## null 213 | 214 | ### Minor Changes 215 | 216 | - d75cbdd: Adds additional rule for example purposes. 217 | 218 | ### Patch Changes 219 | 220 | - Updated dependencies [d75cbdd] 221 | - eslint-codemod-utils@0.1.2 222 | 223 | ## null 224 | 225 | ### Patch Changes 226 | 227 | - Updated dependencies [ba82178] 228 | - Updated dependencies [fbd92dd] 229 | - eslint-codemod-utils@0.1.1 230 | 231 | ## null 232 | 233 | ### Patch Changes 234 | 235 | - Updated dependencies [3eb841a] 236 | - Updated dependencies [5716178] 237 | - eslint-codemod-utils@0.1.0 238 | 239 | ## null 240 | 241 | ### Patch Changes 242 | 243 | - Updated dependencies [cf5df6a] 244 | - eslint-codemod-utils@0.0.7 245 | 246 | ## null 247 | 248 | ### Patch Changes 249 | 250 | - Updated dependencies [8d31804] 251 | - eslint-codemod-utils@0.0.6 252 | 253 | ## null 254 | 255 | ### Patch Changes 256 | 257 | - Updated dependencies [b257d6d] 258 | - Updated dependencies [6e67759] 259 | - eslint-codemod-utils@0.0.5 260 | 261 | ## 1.0.1 262 | 263 | ### Patch Changes 264 | 265 | - cfe67e1: Internal changes to test changeset process 266 | 267 | ## 0.0.1 268 | 269 | ### Patch Changes 270 | 271 | - Updated dependencies [c7e2c68] 272 | - eslint-codemod-utils@0.0.4 273 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/README.md: -------------------------------------------------------------------------------- 1 | # Example Rules 2 | 3 | This package contains a collection of sample rules written with the utilties in `eslint-codemod-utils`. 4 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/01-basic/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from 'eslint' 2 | 3 | import ecuRule from './ecu' 4 | import rule from './standard' 5 | 6 | const ruleTester = new RuleTester({ 7 | parserOptions: { 8 | sourceType: 'module', 9 | ecmaVersion: 'latest', 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | }) 15 | 16 | ruleTester.run('basic/example', rule, { 17 | valid: ['import world from "hello";', 'const x = 10;'], 18 | invalid: [ 19 | { 20 | code: 'import world from "hello"', 21 | errors: ['error'], 22 | output: 'import world from "hello";', 23 | }, 24 | { 25 | code: 'const x = 10', 26 | errors: ['error'], 27 | output: 'const x = 10;', 28 | }, 29 | ], 30 | }) 31 | 32 | ruleTester.run('basic/example-ecu', ecuRule, { 33 | valid: ['import world from "hello";', 'const x = 10;'], 34 | invalid: [ 35 | { 36 | code: 'import world from "hello"', 37 | errors: ['error'], 38 | output: 'import world from "hello";', 39 | }, 40 | { 41 | code: 'const x = 10', 42 | errors: ['error'], 43 | output: 'const x = 10;', 44 | }, 45 | ], 46 | }) 47 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/01-basic/ecu.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'eslint' 2 | import { literal } from 'eslint-codemod-utils' 3 | import { findSemi } from './finder' 4 | 5 | const rule: Rule.RuleModule = { 6 | meta: { 7 | type: 'layout', 8 | docs: { 9 | description: 'Require or disallow semicolons instead of ASI', 10 | recommended: false, 11 | url: 'https://eslint.org/docs/rules/semi', 12 | }, 13 | fixable: 'code', 14 | }, 15 | create(context) { 16 | const source = context.getSourceCode() 17 | return { 18 | Literal(node) { 19 | if (!findSemi(node, source)) 20 | context.report({ 21 | node, 22 | message: 'error', 23 | fix: (fixer) => { 24 | return fixer.replaceText(node, `${literal(node)};`) 25 | }, 26 | }) 27 | }, 28 | } 29 | }, 30 | } 31 | 32 | export default rule 33 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/01-basic/finder.ts: -------------------------------------------------------------------------------- 1 | import { SourceCode } from 'eslint' 2 | import { Literal } from 'eslint-codemod-utils' 3 | 4 | /** 5 | * Finds a semicolon attached to a node literal 6 | */ 7 | export function findSemi(node: Literal, source: SourceCode) { 8 | const token = source.getLastToken(node)?.value 9 | const possibleFinalToken = source.getTokenAfter(node)?.value 10 | const tokenHasSemi = /;/.test(token || '') 11 | const punctionatorHasSemi = /;/.test(possibleFinalToken || '') 12 | 13 | return tokenHasSemi || punctionatorHasSemi 14 | } 15 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/01-basic/standard.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'eslint' 2 | import { findSemi } from './finder' 3 | 4 | const rule: Rule.RuleModule = { 5 | meta: { 6 | type: 'layout', 7 | docs: { 8 | description: 'Require or disallow semicolons instead of ASI', 9 | recommended: false, 10 | url: 'https://eslint.org/docs/rules/semi', 11 | }, 12 | fixable: 'code', 13 | }, 14 | create(context) { 15 | const source = context.getSourceCode() 16 | return { 17 | Literal(node) { 18 | if (!findSemi(node, source)) 19 | context.report({ 20 | node, 21 | message: 'error', 22 | fix: (fixer) => { 23 | return fixer.insertTextAfter(node, ';') 24 | }, 25 | }) 26 | }, 27 | } 28 | }, 29 | } 30 | 31 | export default rule 32 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/02-call-expression/call-expression.test.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from 'eslint' 2 | 3 | import ecuRule from './ecu' 4 | import rule from './standard' 5 | 6 | const ruleTester = new RuleTester({ 7 | parserOptions: { 8 | sourceType: 'module', 9 | ecmaVersion: 'latest', 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | }) 15 | 16 | ruleTester.run('call-expression/basic', rule, { 17 | valid: ['f({ first: oldArg })', 'f({ first: oldArg, second: otherarg })'], 18 | invalid: [ 19 | { 20 | code: 'f(oldArg)', 21 | errors: ['error'], 22 | output: 'f({ first: oldArg })', 23 | }, 24 | // { 25 | // code: 'f(oldArg, otherArg)', 26 | // errors: ['error'], 27 | // output: 'f({ first: oldArg, second: otherArg })', 28 | // }, 29 | // { 30 | // code: 'f(oldArg, { x: 1 })', 31 | // errors: ['error'], 32 | // output: 'f({ first: oldArg, second: { x: 1 } })', 33 | // }, 34 | ], 35 | }) 36 | 37 | ruleTester.run('call-expression/ecu', ecuRule, { 38 | valid: ['f({ first: oldArg })', 'f({ first: oldArg, second: otherarg })'], 39 | invalid: [ 40 | { 41 | code: 'f(oldArg)', 42 | errors: ['error'], 43 | output: 'f({\n first: oldArg\n})', 44 | }, 45 | { 46 | code: 'f(oldArg, otherArg)', 47 | errors: ['error'], 48 | output: 'f({\n first: oldArg,\n second: otherArg\n})', 49 | }, 50 | { 51 | code: 'f(oldArg, { x: 1 })', 52 | errors: ['error'], 53 | output: 'f({\n first: oldArg,\n second: {\n x: 1\n}\n})', 54 | }, 55 | ], 56 | }) 57 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/02-call-expression/ecu.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'eslint' 2 | import { 3 | callExpression, 4 | identifier, 5 | objectExpression, 6 | property, 7 | Property, 8 | } from 'eslint-codemod-utils' 9 | import { findF } from './finder' 10 | 11 | const rule: Rule.RuleModule = { 12 | meta: { 13 | type: 'problem', 14 | docs: { 15 | description: 'Update a fn paramater set', 16 | }, 17 | fixable: 'code', 18 | }, 19 | create(context) { 20 | return { 21 | CallExpression(node) { 22 | if (findF(node)) { 23 | context.report({ 24 | node, 25 | message: 'error', 26 | fix(fixer) { 27 | const thing = callExpression({ 28 | ...node, 29 | optional: false, 30 | arguments: [ 31 | objectExpression({ 32 | properties: ['first', 'second'] 33 | .map((id, idx) => { 34 | return node.arguments[idx] 35 | ? property({ 36 | key: identifier(id), 37 | // @ts-expect-error 38 | value: node.arguments[idx], 39 | }) 40 | : null 41 | }) 42 | .filter(Boolean) as Property[], 43 | }), 44 | ], 45 | }) 46 | return fixer.replaceText(node, `${thing}`) 47 | }, 48 | }) 49 | } 50 | }, 51 | } 52 | }, 53 | } 54 | 55 | export default rule 56 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/02-call-expression/finder.ts: -------------------------------------------------------------------------------- 1 | import { CallExpression, isNodeOfType } from 'eslint-codemod-utils' 2 | 3 | export function findF(node: CallExpression) { 4 | if (!isNodeOfType(node.callee, 'Identifier')) { 5 | return false 6 | } 7 | 8 | if (node.callee.name !== 'f') { 9 | return false 10 | } 11 | 12 | if (node.arguments.length < 1) { 13 | return false 14 | } 15 | 16 | if (isNodeOfType(node.arguments[0], 'ObjectExpression')) { 17 | return false 18 | } 19 | 20 | return true 21 | } 22 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/02-call-expression/standard.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'eslint' 2 | import { findF } from './finder' 3 | import { isNodeOfType } from 'eslint-codemod-utils' 4 | 5 | const rule: Rule.RuleModule = { 6 | meta: { 7 | type: 'problem', 8 | docs: { 9 | description: 'Update a fn paramater set', 10 | }, 11 | fixable: 'code', 12 | }, 13 | create(context) { 14 | return { 15 | CallExpression(node) { 16 | if (findF(node)) { 17 | context.report({ 18 | node, 19 | message: 'error', 20 | fix(fixer) { 21 | const fnName = 22 | isNodeOfType(node.callee, 'Identifier') && node.callee.name 23 | return fixer.replaceText( 24 | node, 25 | `${fnName}({ ${ 26 | // @ts-expect-error 27 | node.arguments[0] ? `first: ${node.arguments[0].name}` : '' 28 | }${ 29 | node.arguments[1] 30 | ? // @ts-expect-error 31 | `, second: ${node.arguments[1].name || 'unknown'}` 32 | : '' 33 | } })` 34 | ) 35 | }, 36 | }) 37 | } 38 | }, 39 | } 40 | }, 41 | } 42 | 43 | export default rule 44 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/03-jsx.ts/ecu.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'eslint' 2 | import { 3 | EslintNode, 4 | isNodeOfType, 5 | JSXAttribute, 6 | jsxClosingElement, 7 | jsxElement, 8 | jsxExpressionContainer, 9 | jsxIdentifier, 10 | jsxOpeningElement, 11 | jsxText, 12 | } from 'eslint-codemod-utils' 13 | import { findModal } from './finder' 14 | 15 | const rule: Rule.RuleModule = { 16 | meta: { 17 | type: 'problem', 18 | docs: { 19 | description: 'Update to a compositional API', 20 | }, 21 | fixable: 'code', 22 | }, 23 | create(context) { 24 | return { 25 | JSXElement(node: EslintNode) { 26 | if (!isNodeOfType(node, 'JSXElement')) { 27 | return 28 | } 29 | if (findModal(node)) { 30 | context.report({ 31 | node, 32 | message: 'error', 33 | fix(fixer) { 34 | const jsxId = jsxIdentifier('ModalTitle') 35 | const title = node.openingElement.attributes.find( 36 | (inner): inner is JSXAttribute => 37 | isNodeOfType(inner, 'JSXAttribute') && 38 | inner.name.name === 'title' 39 | ) 40 | return fixer.replaceText( 41 | node, 42 | `${jsxElement({ 43 | ...node, 44 | openingElement: jsxOpeningElement({ 45 | ...node.openingElement, 46 | attributes: node.openingElement.attributes.filter( 47 | (n) => 48 | isNodeOfType(n, 'JSXAttribute') && 49 | n.name.name !== 'title' 50 | ), 51 | selfClosing: false, 52 | }), 53 | closingElement: jsxClosingElement({ ...node.openingElement }), 54 | children: [ 55 | jsxElement({ 56 | openingElement: jsxOpeningElement({ name: jsxId }), 57 | closingElement: jsxClosingElement({ name: jsxId }), 58 | children: [ 59 | title?.value?.type === 'Literal' 60 | ? // @ts-expect-error 61 | jsxText(title.value) 62 | : jsxExpressionContainer({ 63 | // @ts-expect-error 64 | expression: title?.value, 65 | }), 66 | ], 67 | }), 68 | ...node.children, 69 | ], 70 | })}` 71 | ) 72 | }, 73 | }) 74 | } 75 | }, 76 | } 77 | }, 78 | } 79 | 80 | export default rule 81 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/03-jsx.ts/finder.ts: -------------------------------------------------------------------------------- 1 | import { isNodeOfType, JSXElement } from 'eslint-codemod-utils' 2 | 3 | export function findModal(node: JSXElement) { 4 | if (!isNodeOfType(node, 'JSXElement')) { 5 | return false 6 | } 7 | 8 | if (!isNodeOfType(node.openingElement.name, 'JSXIdentifier')) { 9 | return false 10 | } 11 | 12 | if (node.openingElement.name.name !== 'Modal') { 13 | return false 14 | } 15 | 16 | return node.openingElement.attributes.some((innerNode) => { 17 | return ( 18 | isNodeOfType(innerNode, 'JSXAttribute') && 19 | isNodeOfType(innerNode.name, 'JSXIdentifier') && 20 | innerNode.name.name === 'title' 21 | ) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/03-jsx.ts/jsx.test.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from 'eslint' 2 | 3 | import ecuRule from './ecu' 4 | // import rule from './standard' 5 | 6 | const ruleTester = new RuleTester({ 7 | parserOptions: { 8 | sourceType: 'module', 9 | ecmaVersion: 'latest', 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | }) 15 | 16 | // ruleTester.run('jsx/basic', rule, { 17 | // valid: ['', '', ''], 18 | // invalid: [ 19 | // { 20 | // code: '', 21 | // errors: ['error'], 22 | // output: '\n \n blah\n\n', 23 | // }, 24 | // { 25 | // code: '', 26 | // errors: ['error'], 27 | // output: '\n \n {blah}\n\n', 28 | // }, 29 | // { 30 | // code: '} />', 31 | // errors: ['error'], 32 | // output: '\n \n {}\n\n', 33 | // }, 34 | // ], 35 | // }) 36 | 37 | ruleTester.run('jsx/ecu', ecuRule, { 38 | valid: ['', '', ''], 39 | invalid: [ 40 | { 41 | code: '', 42 | errors: ['error'], 43 | output: '\n \n blah\n\n', 44 | }, 45 | { 46 | code: '', 47 | errors: ['error'], 48 | output: '\n \n {blah}\n\n', 49 | }, 50 | { 51 | code: '} />', 52 | errors: ['error'], 53 | output: '\n \n {}\n\n', 54 | }, 55 | ], 56 | }) 57 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/examples/03-jsx.ts/standard.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from 'eslint' 2 | import { isNodeOfType } from 'eslint-codemod-utils' 3 | import { findModal } from './finder' 4 | 5 | const rule: Rule.RuleModule = { 6 | meta: { 7 | type: 'problem', 8 | docs: { 9 | description: 'Update to a compositional API', 10 | }, 11 | fixable: 'code', 12 | }, 13 | create(context) { 14 | return { 15 | JSXElement(node: Rule.Node) { 16 | if (!node) { 17 | return 18 | } 19 | 20 | if (!isNodeOfType(node, 'JSXElement')) { 21 | return 22 | } 23 | if (findModal(node)) { 24 | context.report({ 25 | node, 26 | message: 'error', 27 | }) 28 | } 29 | }, 30 | } 31 | }, 32 | } 33 | 34 | export default rule 35 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/__tests__/change-composition.test.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from 'eslint' 2 | 3 | import rule from '../rules/change-composition' 4 | 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 'latest', 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | jsx: true, 11 | }, 12 | }, 13 | }) 14 | 15 | ruleTester.run('change-composition', rule, { 16 | valid: [ 17 | { 18 | code: '', 19 | }, 20 | ], 21 | invalid: [ 22 | { 23 | code: ` 24 | import React from 'react' 25 | import Modal, { ModalTransition } from '@atlaskit/modal-dialog' 26 | 27 | const App = () => { 28 | return 29 | } 30 | `, 31 | errors: ['error'], 32 | output: ` 33 | import React from 'react' 34 | // The import "ModalHeader" has been added by codemod 35 | import Modal, { ModalTransition, ModalHeader } from '@atlaskit/modal-dialog' 36 | 37 | const App = () => { 38 | return ( 39 | 40 | 41 | some heading 42 | 43 | 44 | ) 45 | } 46 | `, 47 | }, 48 | { 49 | code: ` 50 | import React from 'react' 51 | import Modal, { ModalTransition } from '@atlaskit/modal-dialog' 52 | 53 | const App = () => { 54 | return 55 | } 56 | `, 57 | errors: ['error'], 58 | output: ` 59 | import React from 'react' 60 | // The import "ModalHeader" has been added by codemod 61 | import Modal, { ModalTransition, ModalHeader } from '@atlaskit/modal-dialog' 62 | 63 | const App = () => { 64 | return ( 65 | 66 | 67 | 68 | some heading 69 | 70 | 71 | ) 72 | } 73 | `, 74 | }, 75 | { 76 | code: ` 77 | import React from 'react' 78 | import Modal from '@atlaskit/modal-dialog' 79 | 80 | const App = () => { 81 | return 82 | } 83 | `, 84 | errors: ['error'], 85 | output: ` 86 | import React from 'react' 87 | // The import "ModalHeader" has been added by codemod 88 | import Modal, { ModalHeader } from '@atlaskit/modal-dialog' 89 | 90 | const App = () => { 91 | return ( 92 | 93 | 94 | {"some heading"} 95 | 96 | 97 | ) 98 | } 99 | `, 100 | }, 101 | { 102 | code: ` 103 | import React from 'react' 104 | import Modal, { ModalTransition } from '@atlaskit/modal-dialog' 105 | 106 | const App = () => { 107 | return 108 | } 109 | `, 110 | errors: ['error'], 111 | output: ` 112 | import React from 'react' 113 | // The import "ModalHeader" has been added by codemod 114 | import Modal, { ModalTransition, ModalHeader } from '@atlaskit/modal-dialog' 115 | 116 | const App = () => { 117 | return ( 118 | 119 | 120 | some heading 121 | 122 | 123 | ) 124 | } 125 | `, 126 | }, 127 | ], 128 | }) 129 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/__tests__/no-codemod-comment.test.ts: -------------------------------------------------------------------------------- 1 | import { RuleTester } from 'eslint' 2 | 3 | import rule from '../rules/no-codemod-comment' 4 | 5 | const ruleTester = new RuleTester({ 6 | parserOptions: { 7 | ecmaVersion: 'latest', 8 | ecmaFeatures: { 9 | jsx: true, 10 | }, 11 | }, 12 | }) 13 | 14 | ruleTester.run('no-codemod-comment', rule, { 15 | valid: [ 16 | { 17 | code: [ 18 | // This has an invalid header so is ignored 19 | `/* integrity: codemod-hash-1399692252 */`, 20 | `// TODO codemod-generated-comment signed: codemod-hash-1399692252`, 21 | ``, 22 | ].join('\n'), 23 | }, 24 | { 25 | code: [``].join('\n'), 26 | }, 27 | ], 28 | invalid: [ 29 | { 30 | code: [ 31 | `/* AUTOGENERATED CODEMOD SIGNATURE signed: */`, 32 | `// TODO: This is a codemod generated comment.`, 33 | ``, 34 | ` `, 35 | ``, 36 | ].join('\n'), 37 | errors: [{ messageId: 'noHashMatch' }], 38 | }, 39 | { 40 | code: [ 41 | `/* AUTOGENERATED CODEMOD SIGNATURE signed: codemod-hash-1510141432 */`, 42 | `// TODO: This is a codemod generated comment.`, 43 | ``, 44 | ].join('\n'), 45 | errors: [{ messageId: 'noHashMatch' }], 46 | }, 47 | { 48 | code: [ 49 | `/* AUTOGENERATED CODEMOD SIGNATURE signed: codemod-hash-524539434,codemod-hash-2056612822 */`, 50 | `// TODO: This is a codemod generated comment.`, 51 | `const y = `, 52 | `// TODO: This is a codemod generated comment. Another comment`, 53 | `const x = `, 54 | ].join('\n'), 55 | errors: [{ messageId: 'noHashInSource' }, { messageId: 'noHashMatch' }], 56 | }, 57 | { 58 | code: [ 59 | `/* AUTOGENERATED CODEMOD SIGNATURE signed: codemod-hash-524539434,codemod-hash-2056612820 */`, 60 | `// TODO: This is a codemod generated comment.`, 61 | `const y = `, 62 | `// TODO: This is a codemod generated comment. Another comment`, 63 | `const x = `, 64 | ].join('\n'), 65 | errors: [ 66 | { messageId: 'noHashInSource' }, 67 | { messageId: 'noHashInSource' }, 68 | ], 69 | }, 70 | ], 71 | }) 72 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/__tests__/rename-prop.test.ts: -------------------------------------------------------------------------------- 1 | import { ESLintUtils } from '@typescript-eslint/utils' 2 | 3 | import rule, { UpdatePropNameOptions } from '../rules/rename-prop' 4 | 5 | const ruleTester = new ESLintUtils.RuleTester({ 6 | parser: '@typescript-eslint/parser', 7 | parserOptions: { 8 | sourceType: 'module', 9 | ecmaVersion: 'latest', 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | }) 15 | 16 | ruleTester.run('jsx/update-prop-name', rule, { 17 | valid: [ 18 | { 19 | options: [ 20 | { 21 | source: '@atlaskit/modal-dialog', 22 | specifier: 'Modal', 23 | oldProp: 'open', 24 | newProp: 'isOpen', 25 | }, 26 | ] as UpdatePropNameOptions[], 27 | code: ` 28 | import { Modal as AKModal } from '@atlaskit/modal-dialog' 29 | 30 | const App = () => ( 31 | 32 | 33 | 34 | ) 35 | `, 36 | }, 37 | { 38 | code: ` 39 | const App = () => ( 40 | 41 | 42 | 43 | ) 44 | `, 45 | }, 46 | ], 47 | invalid: [ 48 | { 49 | options: [ 50 | { 51 | source: '@atlaskit/modal-dialog', 52 | specifier: 'Modal', 53 | oldProp: 'open', 54 | newProp: 'isOpen', 55 | }, 56 | ] as UpdatePropNameOptions[], 57 | code: ` 58 | import { Modal } from '@atlaskit/modal-dialog' 59 | 60 | const App = () => ( 61 | 62 | 63 | 64 | ) 65 | `, 66 | errors: [{ messageId: 'renameProp' }], 67 | output: ` 68 | import { Modal } from '@atlaskit/modal-dialog' 69 | 70 | const App = () => ( 71 | 72 | 73 | 74 | ) 75 | `, 76 | }, 77 | { 78 | options: [ 79 | { 80 | source: '@atlaskit/modal-dialog', 81 | specifier: 'Modal', 82 | oldProp: 'open', 83 | newProp: 'isOpen', 84 | }, 85 | ] as UpdatePropNameOptions[], 86 | code: ` 87 | import { Modal as AKModal } from '@atlaskit/modal-dialog' 88 | 89 | const App = () => ( 90 | 91 | 92 | 93 | ) 94 | `, 95 | errors: [{ messageId: 'renameProp' }], 96 | output: ` 97 | import { Modal as AKModal } from '@atlaskit/modal-dialog' 98 | 99 | const App = () => ( 100 | 101 | 102 | 103 | ) 104 | `, 105 | }, 106 | { 107 | options: [ 108 | { 109 | source: '@example/thing', 110 | specifier: 'Checkbox', 111 | oldProp: 'selected', 112 | newProp: 'checked', 113 | }, 114 | ] as UpdatePropNameOptions[], 115 | code: ` 116 | import { Checkbox } from '@example/thing' 117 | 118 | const App = () => ( 119 | 120 | 121 | 122 | ) 123 | `, 124 | errors: [{ messageId: 'renameProp' }], 125 | output: ` 126 | import { Checkbox } from '@example/thing' 127 | 128 | const App = () => ( 129 | 130 | 131 | 132 | ) 133 | `, 134 | }, 135 | { 136 | options: [ 137 | { 138 | source: '@example/thing', 139 | specifier: 'default', 140 | oldProp: 'selected', 141 | newProp: 'checked', 142 | }, 143 | ] as UpdatePropNameOptions[], 144 | code: ` 145 | import Checkbox from '@example/thing' 146 | 147 | const App = () => ( 148 | 149 | 150 | 151 | ) 152 | `, 153 | errors: [{ messageId: 'renameProp' }], 154 | output: ` 155 | import Checkbox from '@example/thing' 156 | 157 | const App = () => ( 158 | 159 | 160 | 161 | ) 162 | `, 163 | }, 164 | ], 165 | }) 166 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/__tests__/sort-imports.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for sort-imports rule. 3 | * @author Christian Schuller 4 | */ 5 | 6 | 'use strict' 7 | 8 | //------------------------------------------------------------------------------ 9 | // Requirements 10 | //------------------------------------------------------------------------------ 11 | import rule from '../rules/sort-imports' 12 | import { RuleTester } from 'eslint' 13 | 14 | //------------------------------------------------------------------------------ 15 | // Tests 16 | //------------------------------------------------------------------------------ 17 | 18 | const ruleTester = new RuleTester({ 19 | parserOptions: { ecmaVersion: 6, sourceType: 'module' }, 20 | }), 21 | ignoreCaseArgs = [{ ignoreCase: true }] 22 | 23 | ruleTester.run('sort-imports', rule, { 24 | valid: [ 25 | `import a from 'foo.js'`, 26 | "import {a, b} from 'bar.js';", 27 | "import {b, c} from 'bar.js';\n" + "import A from 'foo.js';", 28 | { 29 | code: "import A from 'bar.js';\n" + "import {b, c} from 'foo.js';", 30 | }, 31 | "import {a, b} from 'bar.js';\n" + "import {c, d} from 'foo.js';", 32 | "import A from 'foo.js';\n" + "import B from 'bar.js';", 33 | "import A from 'foo.js';\n" + "import a from 'bar.js';", 34 | "import a, * as b from 'foo.js';\n" + "import c from 'bar.js';", 35 | "import 'foo.js';\n" + " import a from 'bar.js';", 36 | "import B from 'foo.js';\n" + "import a from 'bar.js';", 37 | { 38 | code: "import a from 'foo.js';\n" + "import B from 'bar.js';", 39 | options: ignoreCaseArgs, 40 | }, 41 | "import {a, b, c, d} from 'foo.js';", 42 | { 43 | code: "import a from 'foo.js';\n" + "import B from 'bar.js';", 44 | }, 45 | { 46 | code: "import {a, B, c, D} from 'foo.js';", 47 | options: ignoreCaseArgs, 48 | }, 49 | "import a, * as b from 'foo.js';", 50 | "import * as a from 'foo.js';\n" + '\n' + "import b from 'bar.js';", 51 | "import * as bar from 'bar.js';\n" + "import * as foo from 'foo.js';", 52 | 53 | // https://github.com/eslint/eslint/issues/5130 54 | { 55 | code: "import 'foo';\n" + "import bar from 'bar';", 56 | options: ignoreCaseArgs, 57 | }, 58 | 59 | // https://github.com/eslint/eslint/issues/5305 60 | "import React, {Component} from 'react';", 61 | ], 62 | invalid: [ 63 | { 64 | code: "import {b, A, C, d} from 'foo.js';", 65 | output: "import { A, b, C, d } from 'foo.js'", 66 | options: ignoreCaseArgs, 67 | errors: [ 68 | { 69 | messageId: 'sortMembersAlphabetically', 70 | type: 'ImportSpecifier', 71 | }, 72 | ], 73 | }, 74 | { 75 | code: "import a, { d, c as b } from 'foo.js';\n", 76 | output: "import a, { c as b, d } from 'foo.js'\n", 77 | errors: [ 78 | { 79 | messageId: 'sortMembersAlphabetically', 80 | type: 'ImportSpecifier', 81 | }, 82 | ], 83 | }, 84 | { 85 | code: ` 86 | import { 87 | boop, 88 | foo, 89 | zoo, 90 | baz as qux, 91 | bar, 92 | beep 93 | } from 'foo.js'; 94 | `, 95 | output: ` 96 | import { 97 | bar, 98 | beep, 99 | boop, 100 | foo, 101 | baz as qux, 102 | zoo 103 | } from 'foo.js' 104 | `, 105 | errors: [ 106 | { 107 | messageId: 'sortMembersAlphabetically', 108 | type: 'ImportSpecifier', 109 | }, 110 | ], 111 | }, 112 | ], 113 | }) 114 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/hash.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Example hash format 4 | * @example 5 | * ```ts 6 | * // TODO: This file has been altered by a codemod. integrity: codemod-hash-5131781 7 | * ``` 8 | */ 9 | export function hash(str: string) { 10 | let hashValue = 0 11 | for (let i = 0; i < str.length; i++) { 12 | const char = str.charCodeAt(i) 13 | hashValue = (hashValue << 5) - hashValue + char 14 | hashValue = hashValue & hashValue // Convert to 32bit integer 15 | } 16 | return `codemod-hash-${Math.abs(hashValue)}` 17 | } 18 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/index.ts: -------------------------------------------------------------------------------- 1 | import renameProp from './rules/rename-prop' 2 | import changeComposition from './rules/change-composition' 3 | import sortImports from './rules/sort-imports' 4 | import noCodemodComment from './rules/no-codemod-comment' 5 | 6 | export const rules = { 7 | /** 8 | * Remove or update a jsx prop 9 | */ 10 | 'jsx/update-prop-name': renameProp, 11 | /** 12 | * Remove or update import 13 | */ 14 | 'update-import': renameProp, 15 | /** 16 | * Update jsx prop value 17 | */ 18 | 'jsx/update-prop-value': renameProp, 19 | /** 20 | * Has codemod TODO 21 | */ 22 | 'no-codemod-comment': noCodemodComment, 23 | 'change-composition': changeComposition, 24 | 'sort-imports': sortImports, 25 | } 26 | 27 | export { hash } from './hash' 28 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/rules/change-composition.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from 'eslint' 2 | import { 3 | identifier, 4 | importDeclaration, 5 | ImportDeclaration, 6 | ImportDefaultSpecifier, 7 | importSpecifier, 8 | isNodeOfType, 9 | JSXAttribute, 10 | jsxClosingElement, 11 | jsxElement, 12 | jsxExpressionContainer, 13 | jsxIdentifier, 14 | jsxOpeningElement, 15 | jsxText, 16 | whiteSpace, 17 | } from 'eslint-codemod-utils' 18 | 19 | const rule: Rule.RuleModule = { 20 | meta: { 21 | type: 'suggestion', 22 | docs: { 23 | description: 24 | 'Dummy rule that changes a component prop to a composed prop in a dummy component using ast-helpers', 25 | recommended: true, 26 | }, 27 | fixable: 'code', 28 | }, 29 | create(context) { 30 | let importNode: ImportDeclaration | null = null 31 | 32 | return { 33 | ImportDeclaration(node) { 34 | if (!node) return 35 | 36 | if (node.source.value === '@atlaskit/modal-dialog') { 37 | importNode = node 38 | } 39 | }, 40 | JSXElement(node: Rule.Node) { 41 | if (!importNode) { 42 | return 43 | } 44 | 45 | if (!node) { 46 | return 47 | } 48 | 49 | if (!isNodeOfType(node, 'JSXElement')) { 50 | return 51 | } 52 | 53 | const { openingElement } = node 54 | 55 | if (!isNodeOfType(openingElement.name, 'JSXIdentifier')) { 56 | return 57 | } 58 | 59 | const localDefaultImport = importNode.specifiers.find( 60 | (spec): spec is ImportDefaultSpecifier => 61 | spec.type === 'ImportDefaultSpecifier' 62 | ) 63 | 64 | if (openingElement.name.name !== localDefaultImport?.local.name) { 65 | return 66 | } 67 | 68 | // From here we're dealing with a JSX element of the right type 69 | const headingAttribute = openingElement.attributes.find( 70 | (attr): attr is JSXAttribute => 71 | isNodeOfType(attr, 'JSXAttribute') && attr?.name?.name === 'heading' 72 | ) 73 | 74 | if (!headingAttribute) { 75 | return 76 | } 77 | 78 | context.report({ 79 | node, 80 | message: 'error', 81 | fix(fixer) { 82 | const modalHeaderIdentifer = jsxIdentifier('ModalHeader') 83 | const fixed = 84 | '(\n' + 85 | ''.padStart(node.loc?.start?.column || 0, ' ') + 86 | String( 87 | jsxElement({ 88 | range: node.range, 89 | loc: node.loc, 90 | openingElement: jsxOpeningElement({ 91 | ...node?.openingElement, 92 | selfClosing: false, 93 | attributes: node.openingElement.attributes.filter( 94 | (att) => 95 | !( 96 | att.type === 'JSXAttribute' && 97 | att.name.name === 'heading' 98 | ) 99 | ), 100 | }), 101 | closingElement: jsxClosingElement( 102 | node?.closingElement || node.openingElement 103 | ), 104 | children: (node.children || []).concat( 105 | jsxElement({ 106 | // @ts-expect-error 107 | loc: { start: { column: node.loc.start.column + 2 } }, 108 | openingElement: jsxOpeningElement({ 109 | name: modalHeaderIdentifer, 110 | selfClosing: false, 111 | attributes: [], 112 | }), 113 | closingElement: jsxClosingElement({ 114 | name: modalHeaderIdentifer, 115 | }), 116 | children: [ 117 | // JSXText case 118 | headingAttribute?.value?.type === 'Literal' && 119 | typeof headingAttribute.value.value === 'string' 120 | ? // @ts-expect-error 121 | jsxText(headingAttribute.value) 122 | : headingAttribute?.value?.type === 'JSXElement' 123 | ? jsxElement(headingAttribute.value) 124 | : jsxExpressionContainer({ 125 | // @ts-expect-error 126 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 127 | expression: headingAttribute.value!, 128 | }), 129 | ], 130 | }) 131 | ), 132 | }) 133 | ) + 134 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 135 | `\n${whiteSpace(node.loc!)})` 136 | 137 | const fixes = [fixer.replaceText(node, fixed)] 138 | 139 | // should never occurr 140 | if (!importNode) { 141 | return fixes 142 | } 143 | 144 | if ( 145 | !importNode.specifiers.some( 146 | (spec) => 147 | spec.type === 'ImportSpecifier' && 148 | spec.imported.name === 'ModalHeader' 149 | ) 150 | ) { 151 | const namedImport = importSpecifier({ 152 | local: identifier('ModalHeader'), 153 | imported: identifier('ModalHeader'), 154 | }) 155 | 156 | fixes.push( 157 | fixer.replaceText( 158 | importNode, 159 | String( 160 | importDeclaration({ 161 | ...importNode, 162 | specifiers: (importNode.specifiers || []).concat( 163 | namedImport 164 | ), 165 | }) 166 | ) 167 | ) 168 | ) 169 | fixes.push( 170 | fixer.insertTextBefore( 171 | importNode, 172 | `// The import "ModalHeader" has been added by codemod\n` 173 | ) 174 | ) 175 | } 176 | 177 | return fixes 178 | }, 179 | }) 180 | }, 181 | } 182 | }, 183 | } 184 | 185 | export default rule 186 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/rules/no-codemod-comment.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from 'eslint' 2 | import { hash } from '../hash' 3 | 4 | const SIGNATURE_HEADER = 'AUTOGENERATED CODEMOD SIGNATURE' 5 | const TODO_COMMENT = 'TODO: This is a codemod generated comment.' 6 | const HEADER_REGEX = /(codemod-hash-\d+)/g 7 | 8 | /** 9 | * If there is the presence of a header we then check all comments to verify if they have matching hashes with the header 10 | */ 11 | const rule: Rule.RuleModule = { 12 | meta: { 13 | type: 'suggestion', 14 | docs: { 15 | description: 'Errors if a block has a codemod generated comment in it', 16 | recommended: true, 17 | }, 18 | messages: { 19 | noHashInSource: 20 | 'The file {{ file }} includes a comment generated by a codemod. This comment requires further manual verification.', 21 | noHashMatch: 22 | 'The file {{ file }} includes a comment generated by a codemod but its hash <{{expectedHashValue}}> does not match the header <{{currentHashValue}}>. Please rerun the codemod, or if the codemod changes are now verified - remove the comment and header from the file.', 23 | }, 24 | }, 25 | create(context) { 26 | const filename = context.getFilename() 27 | const source = context.getSourceCode() 28 | const comments = source.getAllComments() 29 | const headerComment = comments.find((comment) => 30 | comment.value.includes(SIGNATURE_HEADER) 31 | ) 32 | 33 | return { 34 | Program() { 35 | if (!headerComment) { 36 | return 37 | } 38 | 39 | const headerSignatureMatches = Array.from( 40 | headerComment.value.matchAll(HEADER_REGEX) 41 | ) 42 | const codemodComments = comments.filter((comment) => 43 | comment.value.includes(TODO_COMMENT) 44 | ) 45 | 46 | codemodComments.forEach((com, index) => { 47 | const currentHashValue = hash(com.value) 48 | const expectedHashValue = headerSignatureMatches[index] 49 | ? headerSignatureMatches[index][0] 50 | : '' 51 | 52 | if (currentHashValue !== expectedHashValue) { 53 | context.report({ 54 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 55 | loc: com.loc!, 56 | messageId: 'noHashMatch', 57 | data: { 58 | file: filename, 59 | expectedHashValue, 60 | currentHashValue, 61 | }, 62 | }) 63 | } else { 64 | context.report({ 65 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 66 | loc: com.loc!, 67 | messageId: 'noHashInSource', 68 | data: { 69 | file: filename, 70 | }, 71 | }) 72 | } 73 | }) 74 | }, 75 | } 76 | }, 77 | } 78 | 79 | export default rule 80 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/rules/rename-prop.ts: -------------------------------------------------------------------------------- 1 | import { ESLintUtils } from '@typescript-eslint/utils' 2 | 3 | import { 4 | ImportDeclaration, 5 | isNodeOfType, 6 | jsxAttribute, 7 | JSXAttribute, 8 | jsxIdentifier, 9 | JSXOpeningElement, 10 | } from 'eslint-codemod-utils' 11 | 12 | export interface UpdatePropNameOptions { 13 | source: string 14 | specifier: string 15 | oldProp: string 16 | newProp: string 17 | } 18 | 19 | const createRule = ESLintUtils.RuleCreator( 20 | (name) => 21 | `https://github.com/DarkPurple141/eslint-codemod-utils/tree/master/packages/eslint-plugin-codemod/${name}` 22 | ) 23 | 24 | const rule = createRule({ 25 | defaultOptions: [] as UpdatePropNameOptions[], 26 | name: 'jsx/rename-prop', 27 | meta: { 28 | type: 'suggestion', 29 | docs: { 30 | description: 31 | 'Dummy rule that changes a prop name in a dummy component using ast-helpers', 32 | recommended: 'error', 33 | }, 34 | messages: { 35 | renameProp: 36 | 'The prop "{{ oldProp }}" in <{{ local }} /> has been renamed to "{{ newProp }}".', 37 | }, 38 | fixable: 'code', 39 | schema: { 40 | description: 'Change any prop to another prop using eslint', 41 | type: 'array', 42 | items: { 43 | type: 'object', 44 | required: ['source', 'specifier', 'oldProp', 'newProp'], 45 | properties: { 46 | source: { 47 | type: 'string', 48 | description: 'The source path of the JSXElement import.', 49 | }, 50 | specifier: { 51 | type: 'string', 52 | description: 53 | "The import specifier of the JSXElement being targeted - can also be simply 'default'.", 54 | }, 55 | oldProp: { 56 | type: 'string', 57 | description: 'The old name of the JSX attribute', 58 | }, 59 | newProp: { 60 | type: 'string', 61 | description: 'The new name of the JSX attribute', 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | create(context) { 68 | const config = context.options as UpdatePropNameOptions[] 69 | let importDecs: ImportDeclaration[] | null[] = config.map(() => null) 70 | 71 | function renameProp( 72 | node: JSXOpeningElement, 73 | importDec: ImportDeclaration, 74 | option: UpdatePropNameOptions 75 | ) { 76 | const specifier = importDec.specifiers.find( 77 | (spec) => 78 | (isNodeOfType(spec, 'ImportSpecifier') && 79 | spec.imported.name === option.specifier) || 80 | (isNodeOfType(spec, 'ImportDefaultSpecifier') && 81 | option.specifier === 'default') 82 | ) 83 | 84 | // The element is imported for a different reason 85 | if (!specifier) { 86 | return 87 | } 88 | 89 | if ( 90 | !( 91 | isNodeOfType(node.name, 'JSXIdentifier') && 92 | node.name.name === specifier.local.name 93 | ) 94 | ) { 95 | return 96 | } 97 | 98 | const toChangeAttr = node.attributes.find( 99 | (attr): attr is JSXAttribute => { 100 | if (isNodeOfType(attr, 'JSXAttribute')) { 101 | return attr.name.name === option.oldProp 102 | } 103 | 104 | return false 105 | } 106 | ) 107 | 108 | if (!toChangeAttr) { 109 | return 110 | } 111 | 112 | // Error cases after this point 113 | context.report({ 114 | // @ts-expect-error 115 | node: toChangeAttr, 116 | messageId: 'renameProp', 117 | data: { ...option, local: specifier.local.name }, 118 | fix(fixer) { 119 | const fixed = jsxAttribute({ 120 | ...toChangeAttr, 121 | name: jsxIdentifier(option.newProp), 122 | }) 123 | 124 | // @ts-expect-error 125 | return fixer.replaceText(toChangeAttr, `${fixed}`) 126 | }, 127 | }) 128 | } 129 | 130 | return { 131 | 'Program:exit': () => { 132 | config.forEach((_, index) => { 133 | importDecs[index] = null 134 | }) 135 | importDecs = [] 136 | }, 137 | ImportDeclaration(node) { 138 | config.forEach((c, i) => { 139 | if (c.source === node.source.value) { 140 | importDecs[i] = node 141 | } 142 | }) 143 | }, 144 | JSXOpeningElement(node) { 145 | config.forEach((c, i) => { 146 | if (importDecs[i]) { 147 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 148 | renameProp(node as JSXOpeningElement, importDecs[i]!, config[i]) 149 | } 150 | }) 151 | }, 152 | } 153 | }, 154 | }) 155 | 156 | export default rule 157 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/lib/rules/sort-imports.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from 'eslint' 2 | import { importDeclaration, isNodeOfType } from 'eslint-codemod-utils' 3 | 4 | /** 5 | * Adapted for presentational / demo purposes only 6 | * @fileoverview Rule to require sorting of import declarations 7 | * @author Christian Schuller 8 | */ 9 | 10 | //------------------------------------------------------------------------------ 11 | // Rule Definition 12 | //------------------------------------------------------------------------------ 13 | const rule: Rule.RuleModule = { 14 | meta: { 15 | type: 'suggestion', 16 | 17 | docs: { 18 | description: 'enforce sorted import declarations within modules', 19 | recommended: false, 20 | url: 'https://eslint.org/docs/rules/sort-imports', 21 | }, 22 | 23 | schema: [ 24 | { 25 | type: 'object', 26 | properties: { 27 | ignoreCase: { 28 | type: 'boolean', 29 | default: false, 30 | }, 31 | ignoreMemberSort: { 32 | type: 'boolean', 33 | default: false, 34 | }, 35 | }, 36 | additionalProperties: false, 37 | }, 38 | ], 39 | 40 | fixable: 'code', 41 | messages: { 42 | sortMembersAlphabetically: 43 | "Member '{{memberName}}' of the import declaration should be sorted alphabetically.", 44 | }, 45 | }, 46 | 47 | create(context) { 48 | const configuration = context.options[0] || {}, 49 | ignoreCase = configuration.ignoreCase || false, 50 | ignoreMemberSort = configuration.ignoreMemberSort || false 51 | 52 | return { 53 | ImportDeclaration(node) { 54 | const specifiers = node.specifiers.map((spec, index) => { 55 | return { 56 | ...spec, 57 | index, 58 | } 59 | }) 60 | 61 | const sorted = specifiers.sort((specA, specB) => { 62 | if (specA.type === 'ImportDefaultSpecifier') { 63 | return -1 64 | } 65 | 66 | if (isNodeOfType(specB, 'ImportDefaultSpecifier')) { 67 | return 1 68 | } 69 | 70 | if (ignoreCase) { 71 | return specA.local.name 72 | .toLowerCase() 73 | .localeCompare(specB.local.name.toLowerCase()) 74 | } else { 75 | return specA.local.name.localeCompare(specB.local.name) 76 | } 77 | }) 78 | const unsortedNode = sorted.find((node, index) => { 79 | return index !== node.index 80 | }) 81 | 82 | if (!ignoreMemberSort && unsortedNode) { 83 | context.report({ 84 | node: unsortedNode, 85 | messageId: 'sortMembersAlphabetically', 86 | data: { 87 | memberName: unsortedNode.local.name, 88 | }, 89 | fix(fixer) { 90 | return fixer.replaceText( 91 | node, 92 | importDeclaration({ 93 | ...node, 94 | specifiers: sorted, 95 | }).toString() 96 | ) 97 | }, 98 | }) 99 | } 100 | }, 101 | } 102 | }, 103 | } 104 | 105 | export default rule 106 | -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-codemod", 3 | "version": "0.0.17", 4 | "private": true, 5 | "dependencies": { 6 | "eslint": "^7.0.0", 7 | "eslint-codemod-utils": "workspace:*" 8 | }, 9 | "source": "src/index.ts", 10 | "main": "dist/index.js", 11 | "scripts": { 12 | "build": "tsc", 13 | "test": "vitest --run" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^4.9.0", 17 | "vitest": "latest" 18 | } 19 | } -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "lib/**/*", 5 | "examples/**/*", 6 | ], 7 | "exclude": [] 8 | } -------------------------------------------------------------------------------- /packages/eslint-plugin-codemod/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "lib/**/*" 5 | ], 6 | "exclude": [ 7 | "**/__fixtures__/**" 8 | ], 9 | "compilerOptions": { 10 | "outDir": "dist" 11 | } 12 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in subdirs of packages/ are part of monorepo 3 | - 'packages/**' 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "target": "ES2019", 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "eslint-codemod-utils", 11 | "declaration": true, 12 | "types": [ 13 | "vitest/globals" 14 | ], 15 | } 16 | } -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": [ 6 | "^build" 7 | ] 8 | }, 9 | "test": { 10 | "dependsOn": [ 11 | "build" 12 | ], 13 | "outputs": [], 14 | "inputs": [ 15 | "src/**/*.tsx", 16 | "src/**/*.ts", 17 | "test/**/*.ts", 18 | "test/**/*.tsx" 19 | ] 20 | }, 21 | "lint": { 22 | "dependsOn": [ 23 | "^build" 24 | ], 25 | "outputs": [] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // Configure Vitest (https://vitest.dev/config) 2 | import { defaultExclude, defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | exclude: [...defaultExclude, '__fixtures__'], 7 | globals: true, 8 | }, 9 | }) 10 | --------------------------------------------------------------------------------