From 4fb1e92a796f7ce4eaecc3c1f4e6386813cef743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Kud=C5=82a?= <11212541+kjugi@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:57:18 +0100 Subject: [PATCH] feat: add missing transforms #10 (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add draft for missing transforms * add some test * base idea to solve single param .send method * move out recursiveParent to utils * convert send to deprecated-signatures * fix bug in send output file * rename deprecated-signatures to v3-deprecated-signatures, cover json and jsonp cases, unify logic for 2 arguments with status and body * add valid syntax cases for send method and add test cases * split json and jsonp tests and add test cases with checking still supported syntax * add vscode config to gitignore * change deprecated-signatures prefix to v4 * update transform config for v4-deprecated-signatures --------- Co-authored-by: Filip Kudła Co-authored-by: Sebastian Beltran --- .gitignore | 5 +- config.ts | 6 +- .../__test__/v4-deprecated-signatures.spec.ts | 3 + .../v4-deprecated-signatures/json.input.ts | 57 +++++++++++ .../v4-deprecated-signatures/json.output.ts | 57 +++++++++++ .../v4-deprecated-signatures/jsonp.input.ts | 57 +++++++++++ .../v4-deprecated-signatures/jsonp.output.ts | 57 +++++++++++ .../v4-deprecated-signatures/send.input.ts | 61 ++++++++++++ .../v4-deprecated-signatures/send.output.ts | 61 ++++++++++++ transforms/magic-redirect.ts | 22 +---- transforms/param.ts | 20 ++++ transforms/v4-deprecated-signatures.ts | 97 +++++++++++++++++++ utils/recursiveParent.ts | 23 +++++ 13 files changed, 503 insertions(+), 23 deletions(-) create mode 100644 transforms/__test__/v4-deprecated-signatures.spec.ts create mode 100644 transforms/__testfixtures__/v4-deprecated-signatures/json.input.ts create mode 100644 transforms/__testfixtures__/v4-deprecated-signatures/json.output.ts create mode 100644 transforms/__testfixtures__/v4-deprecated-signatures/jsonp.input.ts create mode 100644 transforms/__testfixtures__/v4-deprecated-signatures/jsonp.output.ts create mode 100644 transforms/__testfixtures__/v4-deprecated-signatures/send.input.ts create mode 100644 transforms/__testfixtures__/v4-deprecated-signatures/send.output.ts create mode 100644 transforms/param.ts create mode 100644 transforms/v4-deprecated-signatures.ts create mode 100644 utils/recursiveParent.ts diff --git a/.gitignore b/.gitignore index ccc2e3c..755450a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ yarn-error.log* *.d.ts *.js -*.js.map \ No newline at end of file +*.js.map + +# VSCode +.vscode \ No newline at end of file diff --git a/config.ts b/config.ts index 312deed..1581753 100644 --- a/config.ts +++ b/config.ts @@ -9,5 +9,9 @@ export const TRANSFORM_OPTIONS = [ value: 'pluralized-methods', version: '5.0.0', }, - //{ description: 'Transform the deprecated signatures in Express v4', value: 'signature-deprecated', version: '5.0.0' }, + { + description: 'Transform the deprecated signatures in Express v4', + value: 'v4-deprecated-signatures', + version: '5.0.0' + }, ] diff --git a/transforms/__test__/v4-deprecated-signatures.spec.ts b/transforms/__test__/v4-deprecated-signatures.spec.ts new file mode 100644 index 0000000..4df36b4 --- /dev/null +++ b/transforms/__test__/v4-deprecated-signatures.spec.ts @@ -0,0 +1,3 @@ +import { testSpecBuilder } from './util' + +testSpecBuilder('v4-deprecated-signatures') diff --git a/transforms/__testfixtures__/v4-deprecated-signatures/json.input.ts b/transforms/__testfixtures__/v4-deprecated-signatures/json.input.ts new file mode 100644 index 0000000..56fc2b4 --- /dev/null +++ b/transforms/__testfixtures__/v4-deprecated-signatures/json.input.ts @@ -0,0 +1,57 @@ +import express from "express"; + +const app = express(); + +app.get("/json", function (req, res) { + res.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", function (req, response) { + response.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", (req, res) => { + res.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", (req, response) => { + response.json({ user: "Username", isValid: true }, 200); +}); + +app.get("/json", function (req, res) { + res.json({}, 200); +}); + +app.get("/json", function (req, response) { + response.json({}, 200); +}); + +app.get("/json", (req, res) => { + res.json({}, 200); +}); + +app.get("/json", (req, response) => { + response.json({}, 200); +}); + +// Still valid syntax -- START +app.get("/json", function (req, res) { + res.json(null) + res.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, response) { + response.json(null) + response.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, res) { + res.json(null) + res.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, response) { + response.json(null) + response.json({ user: 'tobi' }) +}) +// Still valid syntax -- END \ No newline at end of file diff --git a/transforms/__testfixtures__/v4-deprecated-signatures/json.output.ts b/transforms/__testfixtures__/v4-deprecated-signatures/json.output.ts new file mode 100644 index 0000000..92905a1 --- /dev/null +++ b/transforms/__testfixtures__/v4-deprecated-signatures/json.output.ts @@ -0,0 +1,57 @@ +import express from "express"; + +const app = express(); + +app.get("/json", function (req, res) { + res.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", function (req, response) { + response.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", (req, res) => { + res.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", (req, response) => { + response.status(200).json({ user: "Username", isValid: true }); +}); + +app.get("/json", function (req, res) { + res.status(200).json({}); +}); + +app.get("/json", function (req, response) { + response.status(200).json({}); +}); + +app.get("/json", (req, res) => { + res.status(200).json({}); +}); + +app.get("/json", (req, response) => { + response.status(200).json({}); +}); + +// Still valid syntax -- START +app.get("/json", function (req, res) { + res.json(null) + res.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, response) { + response.json(null) + response.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, res) { + res.json(null) + res.json({ user: 'tobi' }) +}) + +app.get("/json", function (req, response) { + response.json(null) + response.json({ user: 'tobi' }) +}) +// Still valid syntax -- END \ No newline at end of file diff --git a/transforms/__testfixtures__/v4-deprecated-signatures/jsonp.input.ts b/transforms/__testfixtures__/v4-deprecated-signatures/jsonp.input.ts new file mode 100644 index 0000000..99b2b2f --- /dev/null +++ b/transforms/__testfixtures__/v4-deprecated-signatures/jsonp.input.ts @@ -0,0 +1,57 @@ +import express from "express"; + +const app = express(); + +app.get("/jsonp", function (req, res) { + res.jsonp({ user: "Username", isValid: true }, 200); +}); + +app.get("/jsonp", function (req, response) { + response.jsonp({ user: "Username", isValid: true }, 200); +}); + +app.get("/jsonp", (req, res) => { + res.jsonp({ user: "Username", isValid: true }, 200); +}); + +app.get("/jsonp", (req, response) => { + response.jsonp({ user: "Username", isValid: true }, 200); +}); + +app.get("/jsonp", function (req, res) { + res.jsonp({}, 200); +}); + +app.get("/jsonp", function (req, response) { + response.jsonp({}, 200); +}); + +app.get("/jsonp", (req, res) => { + res.jsonp({}, 200) +}); + +app.get("/jsonp", (req, response) => { + response.jsonp({}, 200) +}); + +// Still valid syntax -- START +app.get("/jsonp", function (req, res) { + res.jsonp(null) + res.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, response) { + response.jsonp(null) + response.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, res) { + res.jsonp(null) + res.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, response) { + response.jsonp(null) + response.jsonp({ user: 'tobi' }) +}) +// Still valid syntax -- END \ No newline at end of file diff --git a/transforms/__testfixtures__/v4-deprecated-signatures/jsonp.output.ts b/transforms/__testfixtures__/v4-deprecated-signatures/jsonp.output.ts new file mode 100644 index 0000000..2a8e4c1 --- /dev/null +++ b/transforms/__testfixtures__/v4-deprecated-signatures/jsonp.output.ts @@ -0,0 +1,57 @@ +import express from "express"; + +const app = express(); + +app.get("/jsonp", function (req, res) { + res.status(200).jsonp({ user: "Username", isValid: true }); +}); + +app.get("/jsonp", function (req, response) { + response.status(200).jsonp({ user: "Username", isValid: true }); +}); + +app.get("/jsonp", (req, res) => { + res.status(200).jsonp({ user: "Username", isValid: true }); +}); + +app.get("/jsonp", (req, response) => { + response.status(200).jsonp({ user: "Username", isValid: true }); +}); + +app.get("/jsonp", function (req, res) { + res.status(200).jsonp({}); +}); + +app.get("/jsonp", function (req, response) { + response.status(200).jsonp({}); +}); + +app.get("/jsonp", (req, res) => { + res.status(200).jsonp({}) +}); + +app.get("/jsonp", (req, response) => { + response.status(200).jsonp({}) +}); + +// Still valid syntax -- START +app.get("/jsonp", function (req, res) { + res.jsonp(null) + res.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, response) { + response.jsonp(null) + response.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, res) { + res.jsonp(null) + res.jsonp({ user: 'tobi' }) +}) + +app.get("/jsonp", function (req, response) { + response.jsonp(null) + response.jsonp({ user: 'tobi' }) +}) +// Still valid syntax -- END \ No newline at end of file diff --git a/transforms/__testfixtures__/v4-deprecated-signatures/send.input.ts b/transforms/__testfixtures__/v4-deprecated-signatures/send.input.ts new file mode 100644 index 0000000..d2d38fa --- /dev/null +++ b/transforms/__testfixtures__/v4-deprecated-signatures/send.input.ts @@ -0,0 +1,61 @@ +import express from "express"; + +const app = express(); + +app.get("/send", function (req, res) { + res.send(200, { hello: "world" }); +}); + +app.get("/send", function (req, response) { + response.send(200, "Hello World"); +}); + +app.get("/send", function (req, res) { + res.send(200); +}); + +app.get("/send", function (req, res) { + res.send(200, true); +}); + +app.get("/send", (req, res) => { + res.send(200, { hello: "world" }); +}); + +app.get("/send", (req, res) => { + res.send(200); +}); + +app.get("/send", (req, response) => { + response.send(200); +}); + +app.get("/send", (req, response) => { + response.send(200, true); +}); + +// Still valid syntax -- START +app.get("/send", function (req, res) { + res.send(Buffer.from('whoop')); + res.send({ some: 'json' }); + res.send('

some html

'); +}); + +app.get("/send", function (req, response) { + response.send(Buffer.from('whoop')); + response.send({ some: 'json' }); + response.send('

some html

'); +}); + +app.get("/send", (req, response) => { + response.send(Buffer.from('whoop')); + response.send({ some: 'json' }); + response.send('

some html

'); +}); + +app.get("/send", (req, res) => { + res.send(Buffer.from('whoop')); + res.send({ some: 'json' }); + res.send('

some html

'); +}); +// Still valid syntax -- END \ No newline at end of file diff --git a/transforms/__testfixtures__/v4-deprecated-signatures/send.output.ts b/transforms/__testfixtures__/v4-deprecated-signatures/send.output.ts new file mode 100644 index 0000000..f317983 --- /dev/null +++ b/transforms/__testfixtures__/v4-deprecated-signatures/send.output.ts @@ -0,0 +1,61 @@ +import express from "express"; + +const app = express(); + +app.get("/send", function (req, res) { + res.status(200).send({ hello: "world" }); +}); + +app.get("/send", function (req, response) { + response.status(200).send("Hello World"); +}); + +app.get("/send", function (req, res) { + res.sendStatus(200); +}); + +app.get("/send", function (req, res) { + res.status(200).send(true); +}); + +app.get("/send", (req, res) => { + res.status(200).send({ hello: "world" }); +}); + +app.get("/send", (req, res) => { + res.sendStatus(200); +}); + +app.get("/send", (req, response) => { + response.sendStatus(200); +}); + +app.get("/send", (req, response) => { + response.status(200).send(true); +}); + +// Still valid syntax -- START +app.get("/send", function (req, res) { + res.send(Buffer.from('whoop')); + res.send({ some: 'json' }); + res.send('

some html

'); +}); + +app.get("/send", function (req, response) { + response.send(Buffer.from('whoop')); + response.send({ some: 'json' }); + response.send('

some html

'); +}); + +app.get("/send", (req, response) => { + response.send(Buffer.from('whoop')); + response.send({ some: 'json' }); + response.send('

some html

'); +}); + +app.get("/send", (req, res) => { + res.send(Buffer.from('whoop')); + res.send({ some: 'json' }); + res.send('

some html

'); +}); +// Still valid syntax -- END \ No newline at end of file diff --git a/transforms/magic-redirect.ts b/transforms/magic-redirect.ts index 9eefd66..eb988c3 100644 --- a/transforms/magic-redirect.ts +++ b/transforms/magic-redirect.ts @@ -1,11 +1,8 @@ import { type API, - type ASTNode, type ASTPath, - type ArrowFunctionExpression, CallExpression, type FileInfo, - type FunctionExpression, callExpression, identifier, literal, @@ -13,8 +10,7 @@ import { memberExpression, } from 'jscodeshift' import { getParsedFile } from '../utils/parse' - -const parentExpressionType = ['ArrowFunctionExpression', 'FunctionExpression'] as const +import { recursiveParent } from '../utils/recursiveParent' const unifiedMagicString = (path: ASTPath, projectRequestName: string) => { const pathArguments = path.value.arguments @@ -31,22 +27,6 @@ const unifiedMagicString = (path: ASTPath, projectRequestName: s } } -const recursiveParent = (parent: ASTPath): string | null => { - if (parentExpressionType.some((type) => parent.value.type === type)) { - const foundNode = parent.value as unknown as ArrowFunctionExpression | FunctionExpression - if (foundNode.params[0].type === 'Identifier') { - return foundNode.params[0].name - } - return null - } - - if (parent?.parentPath) { - return recursiveParent(parent.parentPath) - } - - return null -} - export default function transformer(file: FileInfo, _api: API) { const parsedFile = getParsedFile(file) diff --git a/transforms/param.ts b/transforms/param.ts new file mode 100644 index 0000000..e5b9b1c --- /dev/null +++ b/transforms/param.ts @@ -0,0 +1,20 @@ +import type { API, FileInfo } from 'jscodeshift' +import { CallExpression, withParser } from 'jscodeshift' + +export default function transformer(file: FileInfo, _api: API): string { + const parser = withParser('ts') + + return ( + parser(file.source) + .find(CallExpression, { + callee: { + property: { + name: 'json', + }, + }, + }) + // TODO: app.param(fn): This method has been deprecated. Instead, access parameters directly via req.params, or use req.body or req.query as needed. + // Add comment line before with this information + .toSource() + ) +} diff --git a/transforms/v4-deprecated-signatures.ts b/transforms/v4-deprecated-signatures.ts new file mode 100644 index 0000000..56d96ed --- /dev/null +++ b/transforms/v4-deprecated-signatures.ts @@ -0,0 +1,97 @@ +import type { API, ASTPath, FileInfo } from 'jscodeshift' +import { CallExpression, callExpression, identifier, memberExpression, withParser } from 'jscodeshift' +import { recursiveParent } from '../utils/recursiveParent' + +const separateStatusAndBody = (path: ASTPath, calleePropertyName: string) => { + const pathArguments = path.node.arguments + const statusParamIndex = pathArguments.findIndex((arg) => arg.type === 'NumericLiteral') + const bodyParamIndex = pathArguments.findIndex((arg) => arg.type !== 'NumericLiteral') + + path.replace( + callExpression( + memberExpression( + callExpression( + memberExpression(identifier(recursiveParent(path.parentPath, 1) || 'res'), identifier('status')), + [pathArguments[statusParamIndex]], + ), + identifier(calleePropertyName), + ), + [pathArguments[bodyParamIndex]], + ), + ) +} + +export default function transformer(file: FileInfo, _api: API): string { + const parser = withParser('ts') + const parsedFile = parser(file.source) + + parsedFile + .find(CallExpression, { + callee: { + property: { + name: 'send', + }, + }, + }) + .map((path) => { + // from v3: https://expressjs.com/en/3x/api.html#res.send + const pathArguments = path.node.arguments + + if (pathArguments.length === 2) { + separateStatusAndBody(path, 'send') + } else if (pathArguments.length === 1) { + const statusValue = pathArguments[0].type === 'NumericLiteral' ? pathArguments[0].value : false + + if (statusValue) { + path.replace( + callExpression( + memberExpression(identifier(recursiveParent(path.parentPath, 1) || 'res'), identifier('sendStatus')), + [pathArguments[0]], + ), + ) + } + } + + return path + }) + + parsedFile + .find(CallExpression, { + callee: { + property: { + name: 'json', + }, + }, + }) + .map((path) => { + // from v3: https://expressjs.com/en/3x/api.html#res.json + const pathArguments = path.node.arguments + + if (pathArguments.length === 2) { + separateStatusAndBody(path, 'json') + } + + return path + }) + + parsedFile + .find(CallExpression, { + callee: { + property: { + name: 'jsonp', + }, + }, + }) + .map((path) => { + // from v3: https://expressjs.com/en/3x/api.html#res.jsonp + const pathArguments = path.node.arguments + + if (pathArguments.length === 2) { + separateStatusAndBody(path, 'jsonp') + } + + return path + }) + + return parsedFile.toSource() +} diff --git a/utils/recursiveParent.ts b/utils/recursiveParent.ts new file mode 100644 index 0000000..ba904eb --- /dev/null +++ b/utils/recursiveParent.ts @@ -0,0 +1,23 @@ +import type { ASTNode, ASTPath, ArrowFunctionExpression, FunctionExpression } from 'jscodeshift' + +const defaultParentExpressionType = ['ArrowFunctionExpression', 'FunctionExpression'] as const + +export const recursiveParent = ( + parent: ASTPath, + paramIndex = 0, + parentExpressionType = defaultParentExpressionType, +): string | null => { + if (parentExpressionType.some((type) => parent.value.type === type)) { + const foundNode = parent.value as unknown as ArrowFunctionExpression | FunctionExpression + if (foundNode.params[paramIndex].type === 'Identifier') { + return foundNode.params[paramIndex].name + } + return null + } + + if (parent?.parentPath) { + return recursiveParent(parent.parentPath, paramIndex, parentExpressionType) + } + + return null +}