├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── README.md ├── bin └── cli.js ├── package.json ├── transforms ├── .gitkeep └── optional-chaining │ ├── README.md │ ├── __testfixtures__ │ ├── basic.input.ts │ ├── basic.output.ts │ ├── multi-chain.input.ts │ └── multi-chain.output.ts │ ├── index.js │ └── test.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | !.* 2 | __testfixtures__ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | ecmaVersion: 2018, 4 | }, 5 | 6 | plugins: ['prettier', 'node'], 7 | extends: ['eslint:recommended', 'plugin:prettier/recommended', 'plugin:node/recommended'], 8 | env: { 9 | node: true, 10 | }, 11 | rules: {}, 12 | overrides: [ 13 | { 14 | files: ['__tests__/**/*.js'], 15 | env: { 16 | jest: true, 17 | }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 'v*' # older version branches 8 | tags: 9 | - '*' 10 | pull_request: {} 11 | schedule: 12 | - cron: '0 6 * * 0' # weekly, on sundays 13 | 14 | jobs: 15 | lint: 16 | name: Linting 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - uses: actions/setup-node@v1 22 | with: 23 | node-version: 12.x 24 | - name: install yarn 25 | run: npm install -g yarn 26 | - name: install dependencies 27 | run: yarn install 28 | - name: linting 29 | run: yarn lint 30 | 31 | test: 32 | name: Tests 33 | runs-on: ubuntu-latest 34 | 35 | strategy: 36 | matrix: 37 | node: ['^8.12.0', '10', '12'] 38 | 39 | steps: 40 | - uses: actions/checkout@v1 41 | - uses: actions/setup-node@v1 42 | with: 43 | node-version: ${{ matrix.node }} 44 | - name: install yarn 45 | run: npm install --global yarn 46 | - name: install dependencies 47 | run: yarn 48 | - name: test 49 | run: yarn test 50 | 51 | floating-test: 52 | name: Floating dependencies 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - uses: actions/checkout@v1 57 | - uses: actions/setup-node@v1 58 | with: 59 | node-version: '12.x' 60 | - name: install yarn 61 | run: npm install -g yarn 62 | - name: install dependencies 63 | run: yarn install --no-lockfile 64 | - name: test 65 | run: yarn test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.eslintcache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100 5 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "8" 5 | 6 | sudo: false 7 | dist: trusty 8 | 9 | cache: 10 | yarn: true 11 | 12 | before_install: 13 | - curl -o- -L https://yarnpkg.com/install.sh | bash 14 | - export PATH=$HOME/.yarn/bin:$PATH 15 | 16 | install: 17 | - yarn install 18 | 19 | script: 20 | - yarn lint 21 | - yarn test:coverage 22 | 23 | after_success: 24 | - yarn coveralls -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # optional-chaining-codemod 2 | 3 | Transforms: 4 | 5 | ``` 6 | foo && foo.bar; 7 | foo.bar && foo.bar.baz; 8 | 9 | (foo || {}).bar; 10 | ((foo || {}).bar || {}).baz; 11 | ((foo || {}).bar || {}).baz(); 12 | ``` 13 | 14 | to 15 | 16 | ``` 17 | foo?.bar; 18 | foo.bar?.baz; 19 | 20 | foo?.bar; 21 | foo?.bar?.baz; 22 | foo?.bar?.baz(); 23 | ``` 24 | 25 | 26 | ## Usage 27 | 28 | To run a specific codemod from this project, you would run the following: 29 | 30 | ``` 31 | npx @nullvoxpopuli/optional-chaining-codemod path/of/files/ or/some**/*glob.js 32 | 33 | # or 34 | 35 | yarn global add @nullvoxpopuli/optional-chaining-codemod 36 | optional-chaining-codemod path/of/files/ or/some**/*glob.js 37 | 38 | # or 39 | 40 | volta install @nullvoxpopuli/optional-chaining-codemod 41 | optional-chaining-codemod path/of/files/ or/some**/*glob.js 42 | ``` 43 | 44 | ## Transforms 45 | 46 | 47 | * [optional-chaining](transforms/optional-chaining/README.md) 48 | 49 | 50 | ## Contributing 51 | 52 | ### Installation 53 | 54 | * clone the repo 55 | * change into the repo directory 56 | * `yarn` 57 | 58 | ### Running tests 59 | 60 | * `yarn test` 61 | 62 | ### Update Documentation 63 | 64 | * `yarn update-docs` 65 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | require('codemod-cli').runTransform( 5 | __dirname, 6 | 'optional-chaining', 7 | process.argv.slice(2) /* paths or globs */ 8 | ); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nullvoxpopuli/optional-chaining-codemod", 3 | "version": "0.2.2", 4 | "private": false, 5 | "license": "MIT", 6 | "scripts": { 7 | "lint": "eslint --cache .", 8 | "test": "codemod-cli test", 9 | "test:coverage": "codemod-cli test --coverage", 10 | "update-docs": "codemod-cli update-docs", 11 | "coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls" 12 | }, 13 | "bin": { 14 | "optional-chaining-codemod": "./bin/cli.js" 15 | }, 16 | "keywords": [ 17 | "codemod-cli" 18 | ], 19 | "dependencies": { 20 | "codemod-cli": "^2.1.0" 21 | }, 22 | "devDependencies": { 23 | "coveralls": "^3.0.6", 24 | "eslint": "^6.5.1", 25 | "eslint-config-prettier": "^6.3.0", 26 | "eslint-plugin-node": "^10.0.0", 27 | "eslint-plugin-prettier": "^3.1.1", 28 | "jest": "^24.9.0", 29 | "prettier": "^1.18.2" 30 | }, 31 | "engines": { 32 | "node": "8.* || 10.* || >= 12" 33 | }, 34 | "jest": { 35 | "testEnvironment": "node" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /transforms/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullVoxPopuli/optional-chaining-codemod/fd7fa5cd7c40358be994c054f5cdddc1c4d0db4b/transforms/.gitkeep -------------------------------------------------------------------------------- /transforms/optional-chaining/README.md: -------------------------------------------------------------------------------- 1 | # optional-chaining 2 | 3 | 4 | ## Usage 5 | 6 | ``` 7 | npx typescript-optional-chaining-codemod optional-chaining path/of/files/ or/some**/*glob.js 8 | 9 | # or 10 | 11 | yarn global add typescript-optional-chaining-codemod 12 | typescript-optional-chaining-codemod optional-chaining path/of/files/ or/some**/*glob.js 13 | ``` 14 | 15 | ## Input / Output 16 | 17 | 18 | * [basic](#basic) 19 | 20 | 21 | 22 | --- 23 | **basic** 24 | 25 | **Input** ([basic.input.ts](transforms/optional-chaining/__testfixtures__/basic.input.ts)): 26 | ```ts 27 | foo && foo.bar; 28 | foo.bar && foo.bar.baz; 29 | foo && foo.bar && foo.bar.baz; 30 | foo && foo.bar && foo.bar.baz(); 31 | foo && foo.bar && foo.bar.baz && foo.bar.baz(); 32 | foo.bar && foo.bar.baz(); 33 | 34 | (foo || {}).bar; 35 | ((foo || {}).bar || {}).baz; 36 | ((foo || {}).bar || {}).baz(); 37 | 38 | ``` 39 | 40 | **Output** ([basic.output.ts](transforms/optional-chaining/__testfixtures__/basic.output.ts)): 41 | ```ts 42 | foo?.bar; 43 | foo.bar?.baz; 44 | foo?.bar?.baz; 45 | foo?.bar?.baz(); 46 | foo.bar?.baz(); 47 | 48 | foo?.bar; 49 | foo?.bar?.baz; 50 | foo?.bar?.baz(); 51 | 52 | ``` 53 | -------------------------------------------------------------------------------- /transforms/optional-chaining/__testfixtures__/basic.input.ts: -------------------------------------------------------------------------------- 1 | foo && foo.bar; 2 | foo.bar && foo.bar.baz; 3 | foo.bar && foo.bar.baz(); 4 | 5 | (foo || {}).bar; 6 | ((foo || {}).bar || {}).baz; 7 | ((foo || {}).bar || {}).baz(); 8 | -------------------------------------------------------------------------------- /transforms/optional-chaining/__testfixtures__/basic.output.ts: -------------------------------------------------------------------------------- 1 | foo?.bar; 2 | foo.bar?.baz; 3 | foo.bar?.baz(); 4 | 5 | foo?.bar; 6 | foo?.bar?.baz; 7 | foo?.bar?.baz(); 8 | -------------------------------------------------------------------------------- /transforms/optional-chaining/__testfixtures__/multi-chain.input.ts: -------------------------------------------------------------------------------- 1 | foo && foo.bar && foo.bar.baz; 2 | foo && foo.bar && foo.bar.baz(); 3 | foo && foo.bar && foo.bar.baz && foo.bar.baz(1, 2); 4 | -------------------------------------------------------------------------------- /transforms/optional-chaining/__testfixtures__/multi-chain.output.ts: -------------------------------------------------------------------------------- 1 | foo?.bar?.baz; 2 | foo?.bar?.baz(); 3 | foo?.bar?.baz(1, 2); 4 | 5 | -------------------------------------------------------------------------------- /transforms/optional-chaining/index.js: -------------------------------------------------------------------------------- 1 | const { getParser } = require('codemod-cli').jscodeshift; 2 | 3 | function transformer(file, api) { 4 | const j = getParser(api); 5 | // const options = getOptions(); 6 | 7 | let root = j(file.source); 8 | 9 | transformLogicalExpressions(j, root); 10 | transformMemberExpressions(j, root); 11 | 12 | return root.toSource(); 13 | } 14 | 15 | function transformMemberExpressions(j, root) { 16 | root 17 | .find(j.MemberExpression, { 18 | object: { 19 | type: 'LogicalExpression', 20 | operator: '||', 21 | left: { type: 'Identifier' }, 22 | right: { type: 'ObjectExpression' }, 23 | }, 24 | }) 25 | .forEach(path => { 26 | let { object, property } = path.node; 27 | 28 | j(path).replaceWith(j.optionalMemberExpression(object.left, property)); 29 | }); 30 | 31 | root 32 | .find(j.MemberExpression, { 33 | object: { 34 | type: 'LogicalExpression', 35 | operator: '||', 36 | right: { type: 'ObjectExpression' }, 37 | }, 38 | }) 39 | .forEach(path => { 40 | let { object, property } = path.node; 41 | 42 | j(path).replaceWith(j.optionalMemberExpression(object.left, property)); 43 | }); 44 | } 45 | 46 | function transformLogicalExpressions(j, root) { 47 | function handleMemberExpression(path, left, right) { 48 | let leftStr = memberExpressionToString(left); 49 | let rightStr = memberExpressionToString(right); 50 | 51 | if (rightStr.includes(leftStr.replace('?', ''))) { 52 | let newRight = rightStr.replace(leftStr, ''); 53 | j(path).replaceWith(j.identifier(`${leftStr}?${newRight}`)); 54 | } 55 | } 56 | 57 | function handleCallExpression(path, left, callExp) { 58 | let leftStr = memberExpressionToString(left); 59 | let rightStr = memberExpressionToString(callExp.callee); 60 | 61 | if (rightStr.includes(leftStr.replace('?', ''))) { 62 | let newRight = rightStr.replace(leftStr, ''); 63 | j(path).replaceWith( 64 | j.callExpression(j.identifier(`${leftStr}?${newRight}`), callExp.arguments) 65 | ); 66 | } 67 | } 68 | 69 | function handleLogicalExpression(path) { 70 | let node = path.node || path.value; 71 | if (!node) return; 72 | 73 | let { left, right } = node; 74 | 75 | if (left.type === 'Identifier') { 76 | let name = right.object.name; 77 | 78 | if (name !== left.name) { 79 | return; 80 | } 81 | 82 | j(path).replaceWith(j.optionalMemberExpression(j.identifier(left.name), right.property)); 83 | } else if (left.type === 'LogicalExpression') { 84 | handleLogicalExpression(j(left)); 85 | 86 | //transformLogicalExpressions(j, root); 87 | } else if (left.type === 'MemberExpression' || left.type === 'OptionalMemberExpression') { 88 | if (right.type === 'CallExpression') { 89 | handleCallExpression(path, left, right); 90 | } else { 91 | handleMemberExpression(path, left, right); 92 | } 93 | } else if (left.type === 'OptionalMemberExpression') { 94 | } else { 95 | console.log(left); 96 | } 97 | } 98 | 99 | root 100 | .find(j.LogicalExpression, { 101 | operator: '&&', 102 | //left: { type: 'Identifier' }, 103 | right: { type: 'MemberExpression' }, 104 | }) 105 | .forEach(path => { 106 | handleLogicalExpression(path); 107 | }); 108 | 109 | root 110 | .find(j.LogicalExpression, { 111 | operator: '&&', 112 | //left: { type: 'Identifier' }, 113 | right: { type: 'CallExpression' }, 114 | }) 115 | .forEach(path => { 116 | handleLogicalExpression(path); 117 | }); 118 | } 119 | 120 | function memberExpressionToString({ object, property }) { 121 | if (object.type === 'Identifier') { 122 | return `${object.name}.${property.name}`; 123 | } 124 | 125 | return `${memberExpressionToString(object)}.${property.name}`; 126 | } 127 | 128 | function toOptional(j, memberExp) { 129 | return j.optionalMemberExpression( 130 | memberExp.object.type === 'MemberExpression' 131 | ? toOptional(j, memberExp.object) 132 | : memberExp.object, 133 | memberExp.property 134 | ); 135 | } 136 | 137 | module.exports = transformer; 138 | module.exports.parser = 'ts'; 139 | -------------------------------------------------------------------------------- /transforms/optional-chaining/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { runTransformTest } = require('codemod-cli'); 4 | 5 | runTransformTest({ 6 | type: 'jscodeshift', 7 | name: 'optional-chaining', 8 | }); --------------------------------------------------------------------------------