diff --git a/.eslintrc.yml b/.eslintrc.yml index c10450533..0c335e97d 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -114,6 +114,7 @@ overrides: prettier-internal-rules/better-parent-property-check-in-needs-parens: error - files: src/**/*.js rules: + prettier-internal-rules/flat-ast-path-call: error prettier-internal-rules/no-doc-builder-concat: error prettier-internal-rules/no-empty-flat-contents-for-if-break: error prettier-internal-rules/prefer-ast-path-each: error diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js b/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js index 5a42c23e1..693cb0c37 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js @@ -49,7 +49,7 @@ module.exports = { type: "suggestion", docs: { url: - "https://github.com/prettier/prettier/blob/main/scripts/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js", + "https://github.com/prettier/prettier/blob/main/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js", }, messages: { [MESSAGE_ID_PREFER_NAME_CHECK]: diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/flat-ast-path-call.js b/scripts/tools/eslint-plugin-prettier-internal-rules/flat-ast-path-call.js new file mode 100644 index 000000000..b7a7ff986 --- /dev/null +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/flat-ast-path-call.js @@ -0,0 +1,128 @@ +"use strict"; + +// This rule only work for nested `AstPath#call()` for now + +function astPathCallSelector(path) { + const prefix = path ? `${path}.` : ""; + return [ + `[${prefix}type="CallExpression"]`, + `[${prefix}callee.type="MemberExpression"]`, + `[${prefix}callee.property.type="Identifier"]`, + `[${prefix}callee.property.name="call"]`, + `[${prefix}arguments.length>1]`, + `[${prefix}arguments.0.type!="SpreadElement"]`, + ].join(""); +} + +// Matches: +// ``` +// path.call((childPath) => childPath.call(print, "b"), "a") +// ``` +const selector = [ + astPathCallSelector(), + '[arguments.0.type="ArrowFunctionExpression"]', + "[arguments.0.params.length=1]", + '[arguments.0.params.0.type="Identifier"]', + astPathCallSelector("arguments.0.body"), + '[arguments.0.body.callee.object.type="Identifier"]', +].join(""); + +const MESSAGE_ID = "flat-ast-path-call"; + +module.exports = { + meta: { + type: "suggestion", + docs: { + url: + "https://github.com/prettier/prettier/blob/main/scripts/tools/eslint-plugin-prettier-internal-rules/flat-ast-path-call.js", + }, + messages: { + [MESSAGE_ID]: "Do not use nested `AstPath#{{method}}(…)`.", + }, + fixable: "code", + }, + create(context) { + const sourceCode = context.getSourceCode(); + + return { + [selector](outerCall) { + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + const outerCallback = outerCall.arguments[0]; + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^^^^^ + const outerCallbackParameterName = outerCallback.params[0].name; + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^^^^^^^^^^^^^^^^^^^^^^ + const innerCall = outerCallback.body; + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^^^^^ + const innerCallCalleeObjectName = innerCall.callee.object.name; + + if (outerCallbackParameterName !== innerCallCalleeObjectName) { + return; + } + + context.report({ + node: innerCall, + messageId: MESSAGE_ID, + data: { method: "call" }, + *fix(fixer) { + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^ + const innerCallback = innerCall.arguments[0]; + + yield fixer.replaceTextRange( + [ + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^ + outerCallback.range[0], + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^ + innerCallback.range[0], + ], + "" + ); + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^ + const innerNamesStart = innerCallback.range[1]; + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^ + const innerNamesEnd = innerCall.range[1] - 1; + + let innerNamesText = sourceCode.text.slice( + innerNamesStart, + innerNamesEnd + ); + + yield fixer.replaceTextRange( + [innerNamesStart, innerNamesEnd + 1], + "" + ); + + const [penultimateToken, lastToken] = sourceCode.getLastTokens( + outerCall, + 2 + ); + + // `outer` call has `trailing comma` + if ( + penultimateToken && + penultimateToken.type === "Punctuator" && + penultimateToken.value === "," + ) { + innerNamesText = innerNamesText.slice(1); + } + + yield fixer.insertTextBefore(lastToken, innerNamesText); + }, + }); + }, + }; + }, +}; diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/index.js b/scripts/tools/eslint-plugin-prettier-internal-rules/index.js index e5adaf87a..751ac8fa4 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/index.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/index.js @@ -4,6 +4,7 @@ module.exports = { rules: { "better-parent-property-check-in-needs-parens": require("./better-parent-property-check-in-needs-parens"), "directly-loc-start-end": require("./directly-loc-start-end"), + "flat-ast-path-call": require("./flat-ast-path-call"), "jsx-identifier-case": require("./jsx-identifier-case"), "no-doc-builder-concat": require("./no-doc-builder-concat"), "no-empty-flat-contents-for-if-break": require("./no-empty-flat-contents-for-if-break"), diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/test.js b/scripts/tools/eslint-plugin-prettier-internal-rules/test.js index 5b3a09b1f..ec8410669 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/test.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/test.js @@ -73,6 +73,33 @@ test("directly-loc-start-end", { ], }); +test("flat-ast-path-call", { + valid: [ + 'path.call((childPath) => childPath.notCall(print, "b"), "a")', + 'path.notCall((childPath) => childPath.call(print, "b"), "a")', + 'path.call((childPath) => childPath.call(print, "b"))', + 'path.call((childPath) => childPath.call(print), "a")', + 'path.call((childPath) => notChildPath.call(print), "a")', + 'path.call(functionReference, "a")', + // Only check `arrow function` + 'path.call((childPath) => {return childPath.call(print, "b")}, "a")', + 'path.call(function(childPath) {return childPath.call(print, "b")}, "a")', + ], + invalid: [ + { + code: 'path.call((childPath) => childPath.call(print, "b"), "a")', + output: 'path.call(print, "a", "b")', + errors: [{ message: "Do not use nested `AstPath#call(…)`." }], + }, + { + // Trailing comma + code: 'path.call((childPath) => childPath.call(print, "b"), "a",)', + output: 'path.call(print, "a", "b")', + errors: 1, + }, + ], +}); + test("jsx-identifier-case", { valid: [ { diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 1eed13085..a1ab05852 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -237,12 +237,7 @@ function printPathNoParens(path, options, print, args) { // Print @babel/parser's InterpreterDirective here so that // leading comments on the `Program` node get printed after the hashbang. if (n.program && n.program.interpreter) { - parts.push( - path.call( - (programPath) => programPath.call(print, "interpreter"), - "program" - ) - ); + parts.push(path.call(print, "program", "interpreter")); } parts.push(path.call(print, "program"));