Add semicolon preference to formatter options#33402
Add semicolon preference to formatter options#33402andrewbranch merged 27 commits intomicrosoft:masterfrom
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.
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.)
amcasey
left a comment
There was a problem hiding this comment.
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.
How do you choose between for-of, for-increment, and forEach?
There was a problem hiding this comment.
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.
arg => forEach(fs, f => f(arg)) || false?
There was a problem hiding this comment.
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.
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. |
sandersn
left a comment
There was a problem hiding this comment.
One comment so far; just finished reading tests.
sandersn
left a comment
There was a problem hiding this comment.
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
alwaystonever. The formatter successfully removed all semicolons and the file was lint-free, and same for doing the reverse.