-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Added typeToTypeNode with truncation #59332
Added typeToTypeNode with truncation #59332
Conversation
src/services/codefixes/helpers.ts
Outdated
return parameters.map((parameter, i) => | ||
parameter.type | ||
&& (!signature.thisParameter && i === 0) | ||
&& typeToTypeNode(checker, checker.getParameterType(signature, i - (signature.thisParameter ? 1 : 0)), enclosingDeclaration, flags, context).isTruncated |
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 think I understand why we need to round-trip from a TypeNode (parameter.type
) to a Type back to a TypeNode 🤔
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.
The only method I have found to make the checker re-serialize is by using checker.typeToTypeNode
. Since parameter.type
is TypeNode
, I first get the type, then the TypeNode after identifying truncation.
Hope this makes sense.
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.
But why do we need to reserialize?
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 think the problem is that we're calling checker.signatureToSignatureDeclaration
on this line: https://github.com/microsoft/TypeScript/pull/59332/files#diff-880eafae481c815c3af893123092cc0d93305284d8e6d2200638e7d546123b6cR414.
That means that the types get converted to type nodes as part of that call to signatureToSignatureDeclaration
, and if we don't have direct control here of where the type nodes are being created, we can't replace the truncated ones.
I think a better approach to avoid serializing parameter types twice (and any other types that show up in a signature) would be to directly change the truncation behavior of the checker functions that convert things to nodes, e.g. typeToTypeNode
, etc. So instead of inserting ...
and similar, it could insert any
, for instance.
I think we could have a new context flag to control this, and set this new flag everywhere the relevant checker functions are being called in this file and others modified by this PR.
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.
You're correct @gabritto. The first time serializing is during checker.signatureToSignatureDeclaration
. Replacing truncation there is going to be complicated becuase reporting truncation happens on high level.
I do agree that is better to replace the NoTruncation
behavior with using an any
node. The challenge I see there is that by the time we have identified truncation, we have already serialized a bunch of nodes. We must then replace all of the with a single any
node. Otherwise, we'll end up with a huge union with with an any
in the second to last node.
I have not analyzed if that's feasible, but I can certainly look into it if it's promising.
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.
As per @gabritto comment, I've looked into adding a new flag that replaces the truncation behavior for replacing it with an any
keyword. So far, the change looks good and very promising. The only problem is that we no longer have space for the NodeBuilderFlag to add a flag; I'm starting to look into that.
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'm slightly concerned around any
showing up in some of these places in case a user ever asks "where did this any
come from?"
One alternative might be to create a node like {/*...*/}
or [/*...*/]
which retains that comment, or even [...elided]
.
Another approach might be to introduce a special ...
type node that is recognized by the parser. It could still be an error, but gracefully parsed and interpreted as an errorType
in the checker.
Maybe the concern with the any
isn't as big a deal as I'm making it out to be, but I'm wondering what others think.
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 think we should insert a syntax error. I think adding an inline comment like /* elided */
or /* error */
to an any
is fine, maybe an improvement over a silently introduced any
. I think a new special error type node is an interesting idea and I’m not against it; however, remember that this bug comes from fuzzing, not user reports or even telemetry, so I’m not sure how much effort should go into polishing what seems to be a rare edge case.
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 kind of like the idea of prepending a comment like /* elided */
or something similar just to indicate to the user that this node got truncated and has been transformed to any
. Arguably this might be confusing to the user, but at least something to search.
I have not search in telemetry to figure out the recurrence of this bug in release and I don't think we will get many user reports either because this causes completions to not be displayed. Maybe users didn't notice or care to report. Nonetheless, the error-deltas recurrently finds these errors.
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 have updated the PR to instead elide in case of truncation. This doesn't add a new flag as we discussed, because I think it would be unnecessary. Plus. we already handle it like that when creating declarations, so this is consistent with that behavior.
Ping @weswigham as there's some discussion about it privately.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #59332 +/- ##
=======================================
Coverage ? 94.07%
=======================================
Files ? 587
Lines ? 300872
Branches ? 5080
=======================================
Hits ? 283044
Misses ? 12748
Partials ? 5080 ☔ View full report in Codecov by Sentry. |
c05195a
to
4d598b0
Compare
tests/baselines/reference/emitClassExpressionInDeclarationFile.js
Outdated
Show resolved
Hide resolved
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 think createTypeNodesFromResolvedType
and the later half of mapToTypeNodes
have references to checkTruncationLength
that don't have a style switch like this (since they don't go through createElidedInformationPlaceholder
), still. They need to be a bit more creative than an any
to be grammatical, though, since they're not type node positions.
typeToString
also has an emergency hard cutoff that slices the output and adds a trailing ...
in the event the smart truncation fails - not sure if you want to adjust that, or if you even can without introducing a hang (you definitely have to have a cutoff for typeToString
at some point to avoid a hang from sending 5MB of type text across the protocol). I don't think typeToString
's output ever gets formatted, so it having non-grammatical output for that function specifically is probably fine.
Otherwise, this seems OK (sans duplicated comment in that one test - but that should be avoidable) - dunno if an /*elided*/
comment is sufficient feedback for users, but if it works 🤷♀️
@weswigham I don't understand how to fix the i.e the code from test hugeDeclarationOutputGetsTruncatedWithError.ts shows the error // @declaration: true
type props = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z";
type manyprops = `${props}${props}`;
export const c = [null as any as {[K in manyprops]: {[K2 in manyprops]: `${K}.${K2}`}}][0]; because >c : { aa: { aa: "aa.aa"; ... 527 more ...; zz: { ...; }; } we can instead resolve |
Truncations already report a truncation error (via |
Do you find feasible to do |
To your comment about |
Yeah, I generally do, too, that's why usually it all goes through the >c : { aa: { aa: "aa.aa"; ... 527 more ...; zz: { ...; }; } when >c : { aa: { aa: "aa.aa"; /*... 527 more ...*/ zz: { /*elided*/ }; } attaching the elision text as a (trailing) comment to the last successful node, rather than inserting the syntactically invalid node or a bogus property/signature. |
Changed as requested. I still have to figure out how to handle the |
8dc147e
to
52c2ff1
Compare
@@ -7278,6 +7291,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
for (const type of types) { | |||
i++; | |||
if (checkTruncationLength(context) && (i + 2 < types.length - 1)) { | |||
if (context.flags & NodeBuilderFlags.NoTruncation) { |
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.
Am I confused or is this truncating things more when the flag is enabled?
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.
It is not more per se, but it is replacing the previous values with an any
We kinda have 3 states: when NoTruncation
is disabled, we truncate up to defaultMaximumTruncationLength
, if NoTruncation
is enabled it truncates up to noTruncationMaximumTruncationLength
, anything after that is an any
.
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.
A ... ${types.length - i} more elided ...
comment would be more in-line with the non-grammatical node this replaces when NoTruncation
is set.
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.
If we decide to remove all of the types and just have an any
it will not make sense to have this message as the code will look like:
foo(bar: /*... 516 more elided ...*/ any): /*elided*/ any {
throw new Error("Method not implemented.");
}
get bar(): /*... 516 more elided ...*/ any {
throw new Error("Method not implemented.");
}
set bar(value: /*... 516 more elided ...*/ any) {
throw new Error("Method not implemented.");
}
baz<V extends /*... 516 more elided ...*/ any>(value: V): V {
throw new Error("Method not implemented.");
}
We can instead count all of the types that were elided and put that like the example below buy I honestly prefer just having /*elided*/
for something short and descriptive enough IMO.
foo(bar: /*... 800 elided ...*/ any): /*... 800 elided ...*/ any {
throw new Error("Method not implemented.");
}
Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page. Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up. |
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.
It'd also be nice to add a test case that actually hits the createNotEmittedTypeElement
call, just so we know it looks like what we expect. That should just be a matter of a quickfix test like the existing one where the truncation limit gets hit within an object literal type, afaik.
@@ -7278,6 +7291,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
for (const type of types) { | |||
i++; | |||
if (checkTruncationLength(context) && (i + 2 < types.length - 1)) { | |||
if (context.flags & NodeBuilderFlags.NoTruncation) { |
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.
A ... ${types.length - i} more elided ...
comment would be more in-line with the non-grammatical node this replaces when NoTruncation
is set.
@@ -7278,6 +7291,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
for (const type of types) { | |||
i++; | |||
if (checkTruncationLength(context) && (i + 2 < types.length - 1)) { | |||
if (context.flags & NodeBuilderFlags.NoTruncation) { | |||
return [addSyntheticLeadingComment(factory.createKeywordTypeNode(SyntaxKind.AnyKeyword), SyntaxKind.MultiLineCommentTrivia, "elided")]; |
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.
Hm, this is discarding all the parts we already actually got to before hitting the truncation limit - shouldn't this all be something more like
result.push(context.flags & NodeBuilderFlags.NoTruncation ? addSyntheticLeadingComment(factory.createKeywordTypeNode(SyntaxKind.AnyKeyword), SyntaxKind.MultiLineCommentTrivia, `... ${types.length - i} more elided ...`) : factory.createTypeReferenceNode(`... ${types.length - i} more ...`, /*typeArguments*/ undefined));
merged into the line below? This way we still get something like
A | B | C | /* ... 6 more elided ... */ any | undefined
out? (Part of the reason elision is a bit special here is because it endeavors to retain the last element, since elision only really makes sense in the middle.)
As written, this is just going to output
/* elided */ any
for the whole union, even if there's enough allotted space for a lot of the union.
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.
Including part of types then truncating with an any
seems to me like it would be incorrect and unnecessary, even for the last type. Instead, I would argue that if we are truncating and putting any
anyway, we might as well remove the whole thing and replace it for an any
.
Arguably, adding the types might serve as documentation, but it will be incorrect as we're not including all of the types, just some of 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.
Unnecessary for the type arithmetic? Yes. A | B | any
is just any. Unnecessary for the human that has to go and fill in the blank after the quickfix runs? Probably not! The more context we retain, the easier it is for the human that has to go and fix this fix's output with what's supposed to go there!
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 changed as requested but I have some concerns.
I understand your point about retaining context, however, I respectfully disagree because the amount of code generated might become too large to be useful. I fear even the comment might go unnoticed. If this happens, including partial types and then truncating with any can lead to confusion and inaccuracies.
I feel is more efficient to replace the entire section with any
to maintain clarity and correctness. This way, we avoid the risk of misinterpretation and ensure the code remains manageable.
i.e take a look at the test codeFixClassImplementInterfaceNoTruncation.ts
in the latest change, finding where the parameter type ends is already complicated. A user noticing the comment, or the introduction of any
seems likely to be missed.
I don't seem to find a way to create a declaration file with an error like truncation, so I've been testing it locally by removing the error itself. There's an ongoing thread about this here #54305 |
This reverts commit 90dade9.
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 like the consistency between quick info and the fix output now.
cc @rbuckton just to be aware of the NotEmittedTypeElement
node kind and it being public, in case he can think of anything we should do for a new node kind that we've just forgotten (surely we'd have some kind of typesafe thing yelling at us, though, right? 😅).
i.e take a look at the test codeFixClassImplementInterfaceNoTruncation.ts in the latest change, finding where the parameter type ends is already complicated.
Yeah, IMO, that's the point of setting NoTruncation
- you get the full fat, complex output - as much as we can give. It's possible we may want a middleground in the future, where we want the syntactic validity but not the extra long truncation length the flag also implies, but we can consider that separately. We may just wanna make the truncation length easily configurable in the future (though we need to remember that will affect node caching).
I fear even the comment might go unnoticed.
Actually, that's a good idea for another new feature/followup - maybe we should issue suggestions/errors on explicitly written any
(or unknown
) in unions with other members, since we know they trivially collapse the union to nothing! What could you possibly intend when you write A | B | any
, anyway?
I don't seem to find a way to create a declaration file with an error like truncation, so I've been testing it locally by removing the error itself
I was thinking of triggering it via a quickfix, since that's what the new test was doing - you have to tune the length of the expansion-generated nodes pretty precisely to get it to truncate inside the explicit object you want truncated, though (you need just enough length to be below the limit, and then the object literal we want to truncate within).
@@ -7054,6 +7055,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
|
|||
function createTypeNodesFromResolvedType(resolvedType: ResolvedType): TypeElement[] | undefined { | |||
if (checkTruncationLength(context)) { | |||
if (context.flags & NodeBuilderFlags.NoTruncation) { |
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.
Is there a test that shows what this looks like?
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.
Now that you mentioned, there's no test for this scenario. I removed this check, and nothing regressed. I'm not sure how to test it but if it's utterly necessary I can address that another PR.
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.
https://app.codecov.io/gh/microsoft/TypeScript/pull/59332?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=checks&utm_campaign=pr+comments&utm_term=microsoft implies that this code is covered somewhere in a test. Maybe add an assert and reverse engineer a specific test from that?
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.
After deeper analysis, the file hugeDeclarationOutputGetsTruncatedWithError.types
access this code.
Check line 18, for the last property zz
. It changes from "zz": { ...; }
to zz: { };
. This is a "type" tests, during emit the "elided" comment gets added: zz: { /*elided*/ };
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.
Why don't we add elided
in type 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 was working on it. Added.
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.
Just to confirm, you're saying that in *.types
files we will write "zz": { ...; }
but in emit we will write zz: { /*elided*/ };
? The thing I was interested in is whether or not we should do the latter for both, but maybe that's not what you were describing?
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.
No, I mean, before this change files in *.types
files and during emit we write "zz": { ...; }
.
After this change, *.types
files and emit write "zz": { }
, and .d.ts
files "zz": { /*elided*/ }
respectively.
See the test codeFixClassImplementInterfaceNoTruncationProperties.ts
for the emit example. hugeDeclarationOutputGetsTruncatedWithError.types
shows the *.types
change.
Fixes #59168 (comment)
Make completions and formatting handle truncation gracefully. In this PR, when truncation is reported instead tsserver will replace the type with
any
type. This should also solved the issue mentioned by @weswigham in this comment #58719 (comment) about force truncation after 1,000,000 characters.