├── .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 | });
--------------------------------------------------------------------------------