-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Add semicolon preference to formatter options #33402
Conversation
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 |
FYI @jessetrinity, who's thinking about how to consume this from VS |
src/services/formatting/rules.ts
Outdated
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), |
There was a problem hiding this comment.
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.)
There was a problem hiding this 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.
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 👎
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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.
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.) |
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.”) |
This reverts commit 5625cb0.
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. |
There was a problem hiding this 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.
There was a problem hiding this 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 Are there any plans to make it work in the near future? |
@thermarthae thanks, opened #35042 to track 👍 |
Fixes #32831
Related: #18780
Corresponding VS Code PR: microsoft/vscode#80828
Adds a first-class formatter option for optional semicolons, with three options:
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
tonever
. The formatter successfully removed all semicolons and the file was lint-free, and same for doing the reverse.