Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix missing parens in reprinted output #1068

Merged
merged 1 commit into from
Apr 27, 2022

Conversation

conartist6
Copy link
Contributor

Fixes #1067

I am not sure the test is completely adequate, but I can verify that the bug is fixed in the minimal repro.

@conartist6
Copy link
Contributor Author

Oh hmm, nope it's not quite right yet. I found a test that breaks things.

@conartist6
Copy link
Contributor Author

I'm pretty confident that my fix is correct, but I'm seeing a problem in the typescript test.

@conartist6
Copy link
Contributor Author

Oops, didn't mean to send that yet. The problem is that printer.printGenerically(parseExpression('(function () {})')) loses its outermost parens because FPp.needsParens returns false any time a node has no parent. This short-circuits both the extra.parenthesized logic and and the firstInStatement logic, either of which could indicate that the outermost parens surrounding the FunctionExpression are necessary. Interestingly only the typescript test happens to trigger this condition, but the underlying bug is not unique to typescript.

@conartist6
Copy link
Contributor Author

It seems like I could just tweak the logic to ensure that neither of those is dependent on !node.parent. Both of those expressions already seem to make perfect sense when !node.parent.

@conartist6
Copy link
Contributor Author

By the way this is only an issue because I've removed the extra.parenthesized check here as it was causing paren doubling when printing code parsed with the babylon parser.

@conartist6
Copy link
Contributor Author

Though actually that line only handled the case when extra.parenthesized was true, which should not be a requirement for correct behavior.

@conartist6 conartist6 force-pushed the fix-babel-parens branch 4 times, most recently from 1afa93b to 2956890 Compare March 3, 2022 23:49
@conartist6
Copy link
Contributor Author

So I think I've fixed those problems.

I also renamed parens-babylon.ts to parens-extra to make it more clear what aspect of the code is under test. I've moved some assertions about that behavior from the typescript test to this one as well, and added coverage for reprinting.

@conartist6
Copy link
Contributor Author

conartist6 commented Mar 4, 2022

As it sits now this PR has one failing test that I'm not sure what to do about. The test case boils down to this:

    assert.strictEqual(
      new Printer().print(b.functionExpression(/*...*/)).code,
      "function a(b, c = 1, ...d) {}",
    );

which results in the following error:

      AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected

+ '(function a(b, c = 1, ...d) {})'
- 'function a(b, c = 1, ...d) {}'

I definitely broke this by allowing the root node of an expression AST to need parens, and I read the result as malicious compliance. For *Expression nodes which have equivalent *Statement I do not see a downside to allowing an expression at the root of a printed AST to be interpreted as a statement as a special case.

@conartist6
Copy link
Contributor Author

Ah ok, I'm putting it back exactly the way it was. All the tests pass again now. It looks a little different because it is still necessary to check extra.parenthesized even if there is no parent.

That ensures that both the formerly broken case and check("(function () {} ())"); pass.

@conartist6 conartist6 force-pushed the fix-babel-parens branch 2 times, most recently from 054daa1 to 8d327d6 Compare March 4, 2022 00:33
Copy link
Contributor Author

@conartist6 conartist6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly recommend reviewing this with the "Hide whitespace" option on as a large block was reindented.

Comment on lines -327 to -329
if (!parent) {
return false;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We delay this check so that we can reprint (function () {} ()), which otherwise will always have its outermost parens stripped since it has no parent.

Comment on lines +351 to +353
if (node.extra && node.extra.parenthesized) {
return true;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with short circuiting to false as we did previously is that a node may still really need parens (that is, non-decoratively). I opted to make this code responsible for decorative parens which as far as I can tell works fine, though it isn't apparent to me yet if there is some reason the code was written the way it was. The test suite seems to indicate that there is not, or at least is no longer.

@@ -5,6 +5,7 @@ import "./identity";
import "./jsx";
import "./lines";
import "./mapping";
import "./parens-extra";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

d'oh


expr.body.properties = [];

assert.strictEqual(printer.print(ast).code, "() => ({})");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't yet been able to verify that this test fails if the behavior regresses.


function check(expr: string) {
const ast = parse(expr);

const reprinted = printer.print(ast).code;
assert.strictEqual(reprinted, expr);
assert.strictEqual(expr, reprinted);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expected is first

lib/fast-path.ts Outdated Show resolved Hide resolved
@eventualbuddha eventualbuddha merged commit 08e43e1 into benjamn:master Apr 27, 2022
@conartist6
Copy link
Contributor Author

@coderaiser Sorry about that. The test cases for recast are really rather inadequate it seems. I'm looking into these issues.

@conartist6 conartist6 deleted the fix-babel-parens branch April 28, 2022 13:32
@conartist6
Copy link
Contributor Author

conartist6 commented Apr 28, 2022

@coderaiser I'm sorry, at the moment I'm going to need some help from you to be able to understand how to investigate what's happening in your codebase. I've figured out how to isolate a specific test and hit breakpoints, but I'm confused. If I put a breakpoint at the beginning of the recast parse function (in node_modules/@putout/recast/lib/parse.js) I hit it no problem. If I put a breakpoint in print I don't hit it at all.

@conartist6
Copy link
Contributor Author

Oh oops maybe I just focused the wrong test.

@conartist6
Copy link
Contributor Author

I'm looking at the extra parens on JSX return first. The JSX node itself is node.extra.parenthesized, and the printer always adds parens when returning a multiline JSX node as a return statement. No mystery here really.

Unfortunately it's pretty tricky to see what the correct fix may be....

@conartist6
Copy link
Contributor Author

Out of curiousity @coderaiser how do you feel about transitioning from recast to prettier if prettier were able to support transformations?

@coderaiser
Copy link
Contributor

@conartist6 thanks for so fast reply! Actually this additional parens not a very big deal since all the code transformed by 🐊Putout then processed by ESLint with no-extra-parens rule. I just want to mention this while the merge is fresh and everybody remember what is going on here :).

Out of curiousity @coderaiser how do you feel about transitioning from recast to prettier if prettier were able to support transformations?

I will definitely try it, right now @putout/engine-parser supports all actual ESTree-based JavaScript parsers, I prefer Babel for rich API, ecosystem and fast support. And Recast really does it’s best to preserve source code in state it was before any transformation. Also ESLint helps to keep code in opinionated but human readable state with help of eslint-plugin-putout, so it’s not a compromise, this is what I think the code should look like (and the way configuration abilities should look like). So if prettier will give me the same code style I have in my codebase right now of course I’ll give it a try :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ArrowFunctionExpression returning ObjectPattern prints without parens when a return type is removed
4 participants