From b6d732a661292d9dd0a349a319c30d9eeb82b741 Mon Sep 17 00:00:00 2001 From: cm-ayf Date: Sun, 28 Jan 2024 12:17:02 +0900 Subject: [PATCH] add eslint rule no-json-stringify-parse --- eslint-plugin/index.js | 2 + eslint-plugin/no-json-stringify-parse.js | 94 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 eslint-plugin/no-json-stringify-parse.js diff --git a/eslint-plugin/index.js b/eslint-plugin/index.js index ae70df66fc..0b0272d5fa 100644 --- a/eslint-plugin/index.js +++ b/eslint-plugin/index.js @@ -5,10 +5,12 @@ module.exports = { plugins: ["@voicevox"], rules: { "@voicevox/no-strict-nullable": "error", + "@voicevox/no-json-stringify-parse": "error", }, }, }, rules: { "no-strict-nullable": require("./no-strict-nullable"), + "no-json-stringify-parse": require("./no-json-stringify-parse"), }, }; diff --git a/eslint-plugin/no-json-stringify-parse.js b/eslint-plugin/no-json-stringify-parse.js new file mode 100644 index 0000000000..62be6f9967 --- /dev/null +++ b/eslint-plugin/no-json-stringify-parse.js @@ -0,0 +1,94 @@ +// @ts-check +const { AST_NODE_TYPES } = require("@typescript-eslint/types"); +const { createRule } = require("./create-rule"); + +/** + * @typedef {import("@typescript-eslint/types").TSESTree.Expression} Expression + * @typedef {import("@typescript-eslint/types").TSESTree.PrivateIdentifier} PrivateIdentifier + */ + +/** + * @param {Expression | PrivateIdentifier} node + * @param {string} name + * @returns {node is import("@typescript-eslint/types").TSESTree.Identifier} + */ +function isIdentifier(node, name) { + return node.type === "Identifier" && node.name === name; +} + +/** + * @param {Expression | PrivateIdentifier} node + * @param {string} object + * @param {string} property + * @returns {node is import("@typescript-eslint/types").TSESTree.MemberExpression} + */ +function isMemberExpression(node, object, property) { + return ( + node.type === "MemberExpression" && + isIdentifier(node.object, object) && + isIdentifier(node.property, property) + ); +} + +module.exports = createRule({ + create(context) { + return { + CallExpression(node) { + if (!isMemberExpression(node.callee, "JSON", "parse")) return; + if (node.arguments.length !== 1) return; + const arg1 = node.arguments[0]; + if ( + arg1.type !== "CallExpression" || + !isMemberExpression(arg1.callee, "JSON", "stringify") + ) + return; + if (arg1.arguments.length !== 1) return; + const arg2 = arg1.arguments[0]; + + context.report({ + node, + messageId: "report", + data: { + inner: context.getSourceCode().getText(arg2), + }, + fix(fixer) { + const fixes = [ + fixer.replaceText(node.callee, "structuredClone"), + fixer.replaceText(arg1.callee, "toRaw"), + ]; + + if (node.parent?.type === AST_NODE_TYPES.TSAsExpression) { + fixes.push( + fixer.removeRange([node.range[1], node.parent.range[1]]) + ); + } + + if (node.parent?.type === AST_NODE_TYPES.VariableDeclarator) { + const annotation = node.parent.id.typeAnnotation; + if (annotation) { + fixes.push(fixer.remove(annotation)); + } + } + + return fixes; + }, + }); + }, + }; + }, + name: "no-json-stringify-parse", + meta: { + type: "problem", + docs: { + description: "JSON.parse(JSON.stringify(hoge))を使わない", + recommended: "error", + }, + messages: { + report: + "'JSON.stringify(JSON.parse({{ inner }}))'ではなく'structuredClone(toRaw({{ inner }}))'を使用してください。", + }, + schema: [], + fixable: "code", + }, + defaultOptions: [], +});