Skip to content

Add semicolon preference to formatter options #33402

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

Merged

Conversation

andrewbranch
Copy link
Member

@andrewbranch andrewbranch commented Sep 12, 2019

Fixes #32831
Related: #18780

Corresponding VS Code PR: microsoft/vscode#80828

Adds a first-class formatter option for optional semicolons, with three options:

  1. Ignore (default): Don’t touch semicolons or lackthereof.
  2. Insert: Add a real semicolon wherever ASI would.
  3. Remove: Remove semicolons that are optional due to ASI.

Because new and reprinted nodes must come out of the writer with semicolons by default, a setting of “Ignore” will try to pick whether to keep or remove semicolons based on the current file content. (This is the same behavior as currently exists via #31801/#32903, but uses the formatter instead of a wrapper around EmitTextWriter. The change here is that the “smart” behavior can be disabled by picking a preference in the formatter.)

I tried this out on various files in the TypeScript codebase including checker.ts, validating by swapping our ESLint semicolon rule from always to never. The formatter successfully removed all semicolons and the file was lint-free, and same for doing the reverse.

Screen capture of running semicolon removal then insertion on a large source file

@typescript-bot
Copy link
Collaborator

Thanks for the PR! It looks like you've changed the TSServer protocol in some way. Please ensure that any changes here don't break consumers of the current TSServer API. For some extra review, we'll ping @sheetalkamat, @amcasey, @mjbvz, @minestarks for you. Feel free to loop in other consumers/maintainers if necessary

@amcasey
Copy link
Member

amcasey commented Sep 12, 2019

FYI @jessetrinity, who's thinking about how to consume this from VS

rule("NoSpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionDisabledOrUndefined("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.Delete),
rule("NoSpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionDisabledOrUndefined("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.DeleteTrivia),
rule("NoOptionalSemicolon", SyntaxKind.SemicolonToken, anyTokenIncludingEOF, [optionEquals("semicolons", SemicolonPreference.Remove), isSemicolonDeletionContext], RuleAction.DeleteToken),
rule("OptionalSemicolon", anyToken, anyTokenIncludingEOF, [optionEquals("semicolons", SemicolonPreference.Insert), isSemicolonInsertionContext], RuleAction.TrailingSemicolon),
Copy link
Member Author

@andrewbranch andrewbranch Sep 12, 2019

Choose a reason for hiding this comment

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

Perf note: surprisingly, setting anyToken for both tokens doesn’t make too bad of an impact. I had thought about coming back to this and doing some optimization, but it doesn’t seem like it would be a good return on investment. Running “Format Document” on checker.ts with this rule enabled is under two seconds, which is just barely noticeably longer than it took in 3.6. (We don’t log timings on formatting, so my numbers here are unscientific. I would have added timings if the experience felt bad. Update: I’ve been told we have them somewhere. They didn’t show up in TSServer logs with normal verbosity.)

@orta orta added the Update Docs on Next Release Indicates that this PR affects docs label Sep 13, 2019
Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

Mostly questions for my own edification.
Edit: I'll bet you're wondering what my questions are. I'll let you know as soon as I figure out how to get Code to publish them.

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

I probably had more questions, but I'm only willing to retype them so many times...

return arg => f(arg) || g(arg);
export function or<T extends unknown>(...fs: ((arg: T) => boolean)[]): (arg: T) => boolean {
return arg => {
for (const f of fs) {
Copy link
Member

Choose a reason for hiding this comment

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

How do you choose between for-of, for-increment, and forEach?

Copy link
Member Author

Choose a reason for hiding this comment

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

My personal tendencies:

  • for-of: lets you early return enclosing function, doesn’t give you element index
  • for-increment: generally only use when element index is important, or if I need to start somewhere other than the first element
  • forEach: gives you early termination of iteration but not return of the enclosing function, result transformation, and element index. It’s generally replaceable by for-of but is more elegant if you return something, like
const firstMatchingSignature = forEach(types, type => {
  return firstDefined(type.getCallSignatures(), somePredicateThing);
});
  • Array.prototype.forEach: no early termination, no return value 👎

Copy link
Member

Choose a reason for hiding this comment

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

arg => forEach(fs, f => f(arg)) || false?

Copy link
Member Author

@andrewbranch andrewbranch Sep 23, 2019

Choose a reason for hiding this comment

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

Yeah, that would work. I think I also have a tendency to implement abstractions with building blocks that are less abstract than the function I’m implementing, if that makes sense. or and forEach are both fairly low-level atomic abstractions, so it makes sense to me that neither would depend on the other, but would rather be built with primitive loops. (I’ve never been consciously aware of this thought process before now; it’s typically just driven by a vague intuition.)

Copy link
Member

Choose a reason for hiding this comment

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

I guess, in my mind, if or can straightforwardly be reduced to forEach it is higher-level. forEach also seems like the sort of thing that might eventually be performance-tuned and it would be nice to pick up the benefits automatically. I don't feel strongly about it though.

@andrewbranch
Copy link
Member Author

I’m starting to feel like doing this through the existing formatter/rule infrastructure was an awkwardly roundabout approach, and it would be cleaner to add some kind of “formatter preprocessors” concept where a preprocessor operates similarly to a codefix; it gets some context (selection/range/file, formatter options) and returns some TextChanges that get applied before the main formatter (which could continue to operate exclusively on whitespace) runs. (Although separate preprocessors could potentially step on each others’ toes, which could get messy. One advantage of the approach I’ve taken here is that conflicting rules trying to take effect at the same position are properly excluded.)

@andrewbranch
Copy link
Member Author

Update: this now removes the last semicolon from single-line object-like types (that are run through the formatter—doesn’t effect compiler emit):

- type X = { w: string; x: { y: { z: string; }; }; };
+ type X = { w: string; x: { y: { z: string } } };

(To be more specific, when the semicolon preference is “I generally like semicolons,” it removes them from synthesized nodes that get inserted and it doesn’t try to insert them into pre-existing user code on “format document.”)

@andrewbranch
Copy link
Member Author

Update: this now removes the last semicolon from single-line object-like types (that are run through the formatter—doesn’t effect compiler emit):

Update: Reverted this because I think it makes more sense to do this in the emitter. Will probably look at that in a separate PR later.

@sandersn sandersn self-assigned this Sep 25, 2019
Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

One comment so far; just finished reading tests.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

I don't know this area of the code too well so I have mostly questions and low-level suggestions. One possible bug.

@andrewbranch andrewbranch merged commit 7d9b22e into microsoft:master Sep 26, 2019
@andrewbranch andrewbranch deleted the feature/semicolon-preference branch September 26, 2019 20:23
@thermarthae
Copy link

Update: this now removes the last semicolon from single-line object-like types (that are run through the formatter—doesn’t effect compiler emit):

Update: Reverted this because I think it makes more sense to do this in the emitter. Will probably look at that in a separate PR later.

@andrewbranch Are there any plans to make it work in the near future?

@andrewbranch
Copy link
Member Author

@thermarthae thanks, opened #35042 to track 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Update Docs on Next Release Indicates that this PR affects docs
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve semicolon detection experience for new files
8 participants