diff --git a/bulk-decaffeinate.config.js b/bulk-decaffeinate.config.js index 7f81e1d7f506..e7af5f254a2d 100644 --- a/bulk-decaffeinate.config.js +++ b/bulk-decaffeinate.config.js @@ -13,5 +13,10 @@ module.exports = { path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-multi-assign-class-export.js'), path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-implicit-return-assignment.js'), path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-existential-conditional-assignment.js'), + './scripts/decaff/remove-comment-sharp.js', + './scripts/decaff/switch-false.js', + './scripts/decaff/empty-catch.js', + './scripts/decaff/no-cond-assign.js', + './scripts/decaff/arrow-comment.js', ], } diff --git a/circle.yml b/circle.yml index 951cfab046bd..de86112a72f3 100644 --- a/circle.yml +++ b/circle.yml @@ -290,6 +290,8 @@ jobs: - run: npm run test-mocha-snapshot # make sure packages with TypeScript can be transpiled to JS - run: npm run all build-js + # test codemods + - run: npm run test-jscodeshift # run unit tests from individual packages - run: npm run all test -- --package cli - run: npm run all test -- --package electron diff --git a/package.json b/package.json index 38a527786125..0797b06f1109 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "stop-only-all": "npm run stop-only -- --folder packages", "test": "echo '⚠️ This root monorepo is only for local development and new contributions. There are no tests.'", "test-debug-package": "node ./scripts/test-debug-package.js", + "test-jscodeshift": "jest ./scripts/decaff", "test-mocha": "mocha --reporter spec scripts/spec.js", "test-mocha-snapshot": "mocha scripts/mocha-snapshot-spec.js", "test-s3-api": "node -r ./packages/coffee/register -r ./packages/ts/register scripts/binary/s3-api-demo.ts", @@ -133,6 +134,7 @@ "husky": "2.4.1", "inquirer": "3.3.0", "inquirer-confirm": "2.0.3", + "jest": "24.9.0", "js-codemod": "cpojer/js-codemod#29dafed", "jscodemods": "cypress-io/jscodemods#01b546e", "jscodeshift": "0.6.3", diff --git a/scripts/decaff/.eslintrc.json b/scripts/decaff/.eslintrc.json new file mode 100644 index 000000000000..db6ab5794049 --- /dev/null +++ b/scripts/decaff/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "parser": "espree" +} diff --git a/scripts/decaff/__testfixtures__/.eslintrc.json b/scripts/decaff/__testfixtures__/.eslintrc.json new file mode 100644 index 000000000000..21e7f159b82c --- /dev/null +++ b/scripts/decaff/__testfixtures__/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "parser": "espree", + "rules": { + "no-empty": "off", + "no-undef": "off", + "no-cond-assign": "off", + "semi": "off", + "brace-style": "off", + "no-case-declarations": "off" + } +} diff --git a/scripts/decaff/__testfixtures__/arrow-comment.input.js b/scripts/decaff/__testfixtures__/arrow-comment.input.js new file mode 100644 index 000000000000..357c42d5825e --- /dev/null +++ b/scripts/decaff/__testfixtures__/arrow-comment.input.js @@ -0,0 +1,10 @@ +// Original comment +const func = (a, b) => +// Multiline comment +// is here +{ + a.works() + b.check.thisOut() +} + +func() diff --git a/scripts/decaff/__testfixtures__/arrow-comment.output.js b/scripts/decaff/__testfixtures__/arrow-comment.output.js new file mode 100644 index 000000000000..1593e89d37d5 --- /dev/null +++ b/scripts/decaff/__testfixtures__/arrow-comment.output.js @@ -0,0 +1,10 @@ +// Original comment +// Multiline comment +// is here +const func = (a, b) => +{ + a.works() + b.check.thisOut() +}; + +func() diff --git a/scripts/decaff/__testfixtures__/empty-catch.input.js b/scripts/decaff/__testfixtures__/empty-catch.input.js new file mode 100644 index 000000000000..ea887d4b034c --- /dev/null +++ b/scripts/decaff/__testfixtures__/empty-catch.input.js @@ -0,0 +1,7 @@ +try { + // do something +} catch (e) {} + +try { + // do something +} finally {} diff --git a/scripts/decaff/__testfixtures__/empty-catch.output.js b/scripts/decaff/__testfixtures__/empty-catch.output.js new file mode 100644 index 000000000000..e7ef9ff68c38 --- /dev/null +++ b/scripts/decaff/__testfixtures__/empty-catch.output.js @@ -0,0 +1,7 @@ +try { + // do something +} catch (e) {} // eslint-disable-line no-empty + +try { + // do something +} finally {} diff --git a/scripts/decaff/__testfixtures__/no-cond-assign.input.js b/scripts/decaff/__testfixtures__/no-cond-assign.input.js new file mode 100644 index 000000000000..896e62036218 --- /dev/null +++ b/scripts/decaff/__testfixtures__/no-cond-assign.input.js @@ -0,0 +1,6 @@ +let a + +// Comment +if (a = c()) { + a.fix() +} diff --git a/scripts/decaff/__testfixtures__/no-cond-assign.output.js b/scripts/decaff/__testfixtures__/no-cond-assign.output.js new file mode 100644 index 000000000000..3541fbd343a2 --- /dev/null +++ b/scripts/decaff/__testfixtures__/no-cond-assign.output.js @@ -0,0 +1,8 @@ +let a + +// Comment +a = c(); + +if (a) { + a.fix() +} diff --git a/scripts/decaff/__testfixtures__/remove-comment-sharp.input.js b/scripts/decaff/__testfixtures__/remove-comment-sharp.input.js new file mode 100644 index 000000000000..6a2f2a008f44 --- /dev/null +++ b/scripts/decaff/__testfixtures__/remove-comment-sharp.input.js @@ -0,0 +1,6 @@ +//# Testing this +// eslint-disable-next-line +console.log('it works') //# Test + +// This # should not be removed. +/*# it's not wrong */ diff --git a/scripts/decaff/__testfixtures__/remove-comment-sharp.output.js b/scripts/decaff/__testfixtures__/remove-comment-sharp.output.js new file mode 100644 index 000000000000..834260f7bfd0 --- /dev/null +++ b/scripts/decaff/__testfixtures__/remove-comment-sharp.output.js @@ -0,0 +1,6 @@ +// Testing this +// eslint-disable-next-line +console.log('it works') // Test + +// This # should not be removed. +/*# it's not wrong */ diff --git a/scripts/decaff/__testfixtures__/switch-false.input.js b/scripts/decaff/__testfixtures__/switch-false.input.js new file mode 100644 index 000000000000..78af2d53df24 --- /dev/null +++ b/scripts/decaff/__testfixtures__/switch-false.input.js @@ -0,0 +1,33 @@ +function f () { + switch (false) { + // Comment should be reserved + case !a.subject(a): + let x = 30 + + b.doSomething(x) + break + // Multi line comment + // should be reserved + case c.isGood: + c.checkThisOut() + findThings() + break + case isBad: + c.neverCheck() + break + case !isAwesome: + a.subject(a) + break + case hi: + // This should be reserved, too + return 3 + case you: + // This comment is preserved + break + default: + b.goToNext() + break + } +} + +f() diff --git a/scripts/decaff/__testfixtures__/switch-false.output.js b/scripts/decaff/__testfixtures__/switch-false.output.js new file mode 100644 index 000000000000..cb82f14828a0 --- /dev/null +++ b/scripts/decaff/__testfixtures__/switch-false.output.js @@ -0,0 +1,25 @@ +function f () { + if (a.subject(a)) { + // Comment should be reserved + let x = 30; + + b.doSomething(x) + } else if (!c.isGood) { + // Multi line comment + // should be reserved + c.checkThisOut(); + findThings() + } else if (!isBad) { + c.neverCheck(); + } else if (isAwesome) { + a.subject(a); + } else if (!hi) { + // This should be reserved, too + return 3 + } else if (!you) // This comment is preserved + {} else { + b.goToNext(); + } +} + +f() diff --git a/scripts/decaff/__tests__/decaff.test.js b/scripts/decaff/__tests__/decaff.test.js new file mode 100644 index 000000000000..4921d9b52b37 --- /dev/null +++ b/scripts/decaff/__tests__/decaff.test.js @@ -0,0 +1,10 @@ +/* global jest */ + +jest.autoMockOff() +const defineTest = require('jscodeshift/dist/testUtils').defineTest + +defineTest(__dirname, 'switch-false') +defineTest(__dirname, 'empty-catch') +defineTest(__dirname, 'remove-comment-sharp') +defineTest(__dirname, 'arrow-comment') +defineTest(__dirname, 'no-cond-assign') diff --git a/scripts/decaff/arrow-comment.js b/scripts/decaff/arrow-comment.js new file mode 100644 index 000000000000..8607e1047cd7 --- /dev/null +++ b/scripts/decaff/arrow-comment.js @@ -0,0 +1,28 @@ +module.exports = (fileInfo, api) => { + const j = api.jscodeshift + + return j(fileInfo.source) + .find(j.VariableDeclaration, { + declarations: [{ + type: 'VariableDeclarator', + init: { + type: 'ArrowFunctionExpression', + body: { + type: 'BlockStatement', + }, + }, + }], + }) + .replaceWith((nodePath) => { + const { node } = nodePath + const comments = node.declarations[0].init.body.comments + + if (comments && comments.length > 0) { + node.comments = [...node.comments, ...comments] + node.declarations[0].init.body.comments = null + } + + return node + }) + .toSource() +} diff --git a/scripts/decaff/empty-catch.js b/scripts/decaff/empty-catch.js new file mode 100644 index 000000000000..815b8395859b --- /dev/null +++ b/scripts/decaff/empty-catch.js @@ -0,0 +1,28 @@ +module.exports = (fileInfo, api) => { + const j = api.jscodeshift + + const source = j(fileInfo.source) + .find(j.TryStatement) + .replaceWith((nodePath) => { + const { node } = nodePath + + // Add trailing eslint-disable-line for empty catch block + if (node.handler && node.handler.body.body.length === 0) { + node.handler.body.comments = [ + { + type: 'Line', + value: ' eslint-disable-line no-empty', + leading: false, + trailing: true, + }, + ] + } + + return node + }) + .toSource() + + // Generated source above creates {}// eslint-disable-line block. + // So, add a space with replace + return source.replace(/\{\}\/\/ eslint-disable-line/g, '{} // eslint-disable-line') +} diff --git a/scripts/decaff/no-cond-assign.js b/scripts/decaff/no-cond-assign.js new file mode 100644 index 000000000000..3c74a3b1f89d --- /dev/null +++ b/scripts/decaff/no-cond-assign.js @@ -0,0 +1,29 @@ +module.exports = (fileInfo, api) => { + const j = api.jscodeshift + + return j(fileInfo.source) + .find(j.IfStatement, { + test: { + type: 'AssignmentExpression', + }, + }) + .replaceWith((nodePath) => { + const { node } = nodePath + + const assign = j.expressionStatement(node.test) + + assign.comments = node.comments + + const ifStatement = j.ifStatement( + node.test.left, + node.consequent, + node.alternate + ) + + return [ + assign, + ifStatement, + ] + }) + .toSource() +} diff --git a/scripts/decaff/remove-comment-sharp.js b/scripts/decaff/remove-comment-sharp.js new file mode 100644 index 000000000000..08273d5abd0f --- /dev/null +++ b/scripts/decaff/remove-comment-sharp.js @@ -0,0 +1,4 @@ +// This file doesn't use AST because it makes things too complicated. +module.exports = (fileInfo) => { + return fileInfo.source.replace(/\/\/#/g, '//') +} diff --git a/scripts/decaff/switch-false.js b/scripts/decaff/switch-false.js new file mode 100644 index 000000000000..850a8fe90495 --- /dev/null +++ b/scripts/decaff/switch-false.js @@ -0,0 +1,98 @@ +module.exports = (fileInfo, api) => { + const j = api.jscodeshift + + return j(fileInfo.source) + .find(j.SwitchStatement, { + discriminant: { + type: 'Literal', + value: false, + }, + }) + .replaceWith((nodePath) => { + const { node } = nodePath + + const cases = node.cases.map((c) => { + const { test, consequent, comments } = c + + return { + test: generateTest(j, test), + content: generateContent(j, consequent), + comments, + } + }) + + const ifStatement = generateIfStatement(j, cases) + + ifStatement.comments = node.comments + + return ifStatement + }) + .toSource() +} + +function generateTest (j, test) { + if (test) { + if (test.type === 'UnaryExpression') { + return test.argument + } + + return j.unaryExpression('!', test) + } + + return null +} + +function generateContent (j, consequent) { + if (consequent.length === 1 && consequent[0].type === 'BreakStatement') { + const block = j.blockStatement([]) + + block.comments = consequent[0].comments + + return block + } + + return j.blockStatement(consequent.filter((c) => c.type !== 'BreakStatement')) +} + +function generateIfStatement (j, cases) { + const nonDefaultCases = cases.filter((c) => c.test !== null) + const defaultCase = cases.filter((c) => c.test === null)[0] + + let ifStatement = null + + if (defaultCase) { + const content = addComment(defaultCase.content, defaultCase.comments) + + ifStatement = content + } + + nonDefaultCases.reverse().forEach((c) => { + const content = addComment(c.content, c.comments) + + ifStatement = j.ifStatement( + c.test, + content, + ifStatement + ) + }) + + return ifStatement +} + +function addComment (content, comments) { + if (content.body.length > 0) { + content.body[0].comments = [...(comments || []), ...(content.body[0].comments || [])] + } else { + const newComments = (comments || []).map((co) => { + return { + ...co, + leading: false, + trailing: false, + } + }) + + content.comments = [...newComments, ...(content.comments || [])] + } + + return content +}