Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

feat: Support extends constraints on infer type #4018

Merged

Conversation

nissy-dev
Copy link
Contributor

@nissy-dev nissy-dev commented Dec 8, 2022

Summary

Part of #2400

This PR is initial implementation for "extends constraints on infer type" released in TS.4.7.

Test Plan

I add some parser tests and confirmed the prettier tests are almost passed.
The only following case is not passed, so I'm currently considering about how to pass it.

type X9<U, T> = T extends (infer U extends number ? U : T ) ? U : T

@netlify
Copy link

netlify bot commented Dec 8, 2022

Deploy Preview for docs-rometools ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit f5c64b5
🔍 Latest deploy log https://app.netlify.com/sites/docs-rometools/deploys/63aec6217424ba000a5e3852
😎 Deploy Preview https://deploy-preview-4018--docs-rometools.netlify.app/playground
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site settings.

@nissy-dev nissy-dev changed the title Support extends constraints on infer type feat: Support extends constraints on infer type Dec 8, 2022
Copy link
Contributor

@MichaReiser MichaReiser left a comment

Choose a reason for hiding this comment

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

I found some TypeScript test cases that the current implementation doesn't handle correctly

You can run cargo coverage --language=ts to run the typescript test cases (I hope our TS submodule is recent enough). Or you can paste the content of this file in the playground and see if it gets parsed correctly.

I suspect that the issue is related to the fact that #48112 uses speculative parsing:

https://github.com/microsoft/TypeScript/blob/01b9a2d5b002298d7bb76e1f2c90dcf8a7908545/src/compiler/parser.ts#L3974-L3989

It only parses out the constraint if conditional types are disallowed or the current token after parsing the type isn't a ? token.

xtask/codegen/js.ungram Outdated Show resolved Hide resolved
@nissy-dev nissy-dev force-pushed the support-extends-constraints-on-infer-type branch from 48f7993 to 4c655a0 Compare December 15, 2022 10:45
@nissy-dev
Copy link
Contributor Author

nissy-dev commented Dec 19, 2022

@MichaReiser
Thank you for your review! I updated codes based on your comments. Currently, I struggle with two edge case. The first case (EdgeCase1) is that it is not sufficient for the parser to check one token ahead. The second case (EdgeCase2) is that parse failed because of removing parentheses. I would appreciate if you could give me some advice.

type EdgeCase1<T> = T extends { [P in infer U extends keyof T ? 1 : 0]: 1; } ? 1 : 0;
type EdgeCase2<T> = T extends ((...a: any[]) => infer R extends string) ? R : never;

Log

  ✖ expected `?` but instead found `;`

  > 1 │ type EdgeCase2<T> = T extends (...a: any[]) => infer R extends string ? R : never;
      │                                                                            ^
    2 │

@MichaReiser MichaReiser added the A-Parser Area: parser label Dec 20, 2022
Comment on lines 92 to 93
/// TODO
pub(crate) allow_conditional_type: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

Something to explore once we get the implementation working.

Would it be possible to move this flag out of the ParserState and instead introduce a TypeContext that's passed to all type parsing methods similar to the ExpressionContext used when parsing expressions?

The benefit of a scoped context vs adding global state to the parser is that:

  • It doesn't increase the size of the parser snapshot (and, therefore, the size of creating a snapshot).
  • Explicitly passing the context makes it more evident where the new flag is used

crates/rome_js_formatter/src/ts/types/infer_type.rs Outdated Show resolved Hide resolved
crates/rome_js_parser/src/state.rs Outdated Show resolved Hide resolved
crates/rome_js_parser/src/syntax/typescript/types.rs Outdated Show resolved Hide resolved
@MichaReiser
Copy link
Contributor

@MichaReiser Thank you for your review! I updated codes based on your comments. Currently, I struggle with two edge case. The first case (EdgeCase1) is that it is not sufficient for the parser to check one token ahead. The second case (EdgeCase2) is that parse failed because of removing parentheses. I would appreciate if you could give me some advice.

type EdgeCase1<T> = T extends { [P in infer U extends keyof T ? 1 : 0]: 1; } ? 1 : 0;
type EdgeCase2<T> = T extends ((...a: any[]) => infer R extends string) ? R : never;

Log

  ✖ expected `?` but instead found `;`

  > 1 │ type EdgeCase2<T> = T extends (...a: any[]) => infer R extends string ? R : never;
      │                                                                            ^
    2 │

I looked into this more closely because it wasn't clear to me why the parsing is failing, and I created a draft PR that uses try_parse and allows conditional places in a few more places. In my opinion it would be good to explore if the DisallowConditionalTypes flag would be moved into a new TypesContext that works similar to ExpressionContext and is passed down everywhere where we parse types. It makes it easier to debug and reason about the code (with the downside of it needing to be passed explicitly)

Case 1

You can use the try_parse function for speculative parsing when a fixed lookahead isn't sufficient (as it is in this case because you need to look if the type is followed by a ? token and the type could be of multiple tokens).

Case 2

Not entirely sure but I think the issue here is that you missed to allow conditional types.

@MichaReiser
Copy link
Contributor

!bench_parser

@MichaReiser
Copy link
Contributor

Thanks for updating the PR.

Do you plan to refactor the code to use a TypeContext instead of extending the ParserState?

There are a few tests failing that should pass

  • type Equals = A extends () => B extends C ? D : E ? F : G;
  • type Equals<A, B, C, D, E, F, G> = A extends (x: B extends C ? D : E) => 0 ? F : G; This one fails for typescript too but should be easy to fix by wrapping the parse_ts_type call in parse_ts_parenthesized_type with an allow()

@github-actions
Copy link

Parser Benchmark Results

group                                 main                                   pr
-----                                 ----                                   --
parser/big5-added.json                1.02    255.6±8.00µs    66.1 MB/sec    1.00    251.6±9.27µs    67.1 MB/sec
parser/canada.json                    1.00    131.7±5.82ms    16.3 MB/sec    1.02    134.5±7.79ms    16.0 MB/sec
parser/checker.ts                     1.01    165.3±5.63ms    15.7 MB/sec    1.00    163.8±5.31ms    15.9 MB/sec
parser/compiler.js                    1.00     95.1±2.44ms    11.0 MB/sec    1.01     95.8±2.85ms    10.9 MB/sec
parser/d3.min.js                      1.01     57.7±2.46ms     4.5 MB/sec    1.00     57.1±2.48ms     4.6 MB/sec
parser/db.json                        1.01      6.4±0.20ms    27.9 MB/sec    1.00      6.3±0.22ms    28.3 MB/sec
parser/dojo.js                        1.01      4.7±0.11ms    14.7 MB/sec    1.00      4.6±0.13ms    14.8 MB/sec
parser/eucjp.json                     1.01   424.7±12.61µs    92.2 MB/sec    1.00   419.5±16.00µs    93.4 MB/sec
parser/ios.d.ts                       1.01    142.5±5.10ms    13.1 MB/sec    1.00    141.0±4.43ms    13.2 MB/sec
parser/jquery.min.js                  1.02     14.8±0.50ms     5.6 MB/sec    1.00     14.5±0.46ms     5.7 MB/sec
parser/math.js                        1.01    115.4±4.57ms     5.6 MB/sec    1.00    114.8±3.98ms     5.6 MB/sec
parser/package-lock.json              1.00      2.6±0.09ms    52.9 MB/sec    1.00      2.6±0.07ms    53.1 MB/sec
parser/parser.ts                      1.02      3.3±0.09ms    14.6 MB/sec    1.00      3.3±0.09ms    14.8 MB/sec
parser/pixi.min.js                    1.03     73.4±2.87ms     6.0 MB/sec    1.00     71.2±2.24ms     6.2 MB/sec
parser/react-dom.production.min.js    1.00     20.3±1.15ms     5.7 MB/sec    1.01     20.5±1.08ms     5.6 MB/sec
parser/react.production.min.js        1.04  1040.6±54.70µs     5.9 MB/sec    1.00  1005.2±35.11µs     6.1 MB/sec
parser/router.ts                      1.00  1308.9±33.94µs    23.5 MB/sec    1.00  1307.9±45.11µs    23.5 MB/sec
parser/tex-chtml-full.js              1.01    155.4±4.52ms     5.9 MB/sec    1.00    153.8±4.54ms     5.9 MB/sec
parser/three.min.js                   1.02     81.0±2.80ms     7.3 MB/sec    1.00     79.7±2.73ms     7.4 MB/sec
parser/typescript.js                  1.01   650.6±12.95ms    14.6 MB/sec    1.00   647.1±12.37ms    14.7 MB/sec
parser/vue.global.prod.js             1.00     25.1±1.20ms     4.8 MB/sec    1.04     26.1±1.23ms     4.6 MB/sec

@nissy-dev
Copy link
Contributor Author

nissy-dev commented Dec 23, 2022

@MichaReiser
Thank you for your reviews and i'm so sorry for a late response. I really appreciate the time you spent on this. I learned a lot about how to handle parser contexts and use try_parse from your PR.

I would like to do the refactoring and fix the failing tests if possible, but it will be the day after tomorrow before I can start. This task might have been a bit difficult for me. Therefore, I am willing to have you continue the task on your end.

@MichaReiser
Copy link
Contributor

@MichaReiser

Thank you for your reviews and i'm so sorry for a late response. I really appreciate the time you spent on this. I learned a lot about how to handle parser contexts and use try_parse from your PR.

I would like to do the refactoring and fix the failing tests if possible, but it will be the day after tomorrow before I can start. This task might have been a bit difficult for me. Therefore, I am willing to have you continue the task on your end.

That's alright. We're all taking a few days off. Enjoy the holidays

@nissy-dev
Copy link
Contributor Author

nissy-dev commented Dec 25, 2022

There are a few tests failing that should pass

I could fix these tests and pass all prettier tests!

Do you plan to refactor the code to use a TypeContext instead of extending the ParserState?

I tried to refactor in a few days and made PR nissy-dev#6.
If the PR is not so different from your thought, I will merge it into this PR and continue to review.

/// ```javascript
/// type Type<A> = A extends ((a: string) => infer B extends string) ? B : never; // true
/// ```
pub(crate) fn is_includes_inferred_return_types_with_extends_constraints(
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 referred to prettier/prettier#13275

@MichaReiser
Copy link
Contributor

There are a few tests failing that should pass

I could fix these tests and pass all prettier tests!

Do you plan to refactor the code to use a TypeContext instead of extending the ParserState?

I tried to refactor in a few days and made PR nissy-dev#6. If the PR is not so different from your thought, I will merge it into this PR and continue to review.

This PR and your other PR looks good. The only thing that we need to be careful about is to make sure to pass context through correctly and only use TypeContext::default in positions where a type starts a complete new context (because it is parenthesized)

@nissy-dev
Copy link
Contributor Author

The only thing that we need to be careful about is to make sure to pass context through correctly and only use TypeContext::default in positions where a type starts a complete new context (because it is parenthesized)

If the scope of the refactoring is up to making sure the context is passed correctly, I think it is better to separate PR about the refactoring task. This is because there are too many functions which the context should be passed and this refactoring affects many logic.

And then, it is really hard for me to pass context through correctly. I tried, but I couldn't do that. I would like you to do this refactoring on your end if possible.

Comment on lines 761 to 781
match node.kind() {
JsSyntaxKind::TS_FUNCTION_TYPE => {
let function_type = TsFunctionType::unwrap_cast(node.clone());
if let Ok(AnyTsReturnType::AnyTsType(AnyTsType::TsInferType(infer_type))) =
function_type.return_type()
{
if infer_type.constraint().is_some() {
return true;
}
}
}
JsSyntaxKind::TS_CONSTRUCTOR_TYPE => {
let constructor_type = TsConstructorType::unwrap_cast(node.clone());
if let Ok(AnyTsType::TsInferType(infer_type)) = constructor_type.return_type() {
if infer_type.constraint().is_some() {
return true;
}
}
}
_ => (),
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit

Suggested change
match node.kind() {
JsSyntaxKind::TS_FUNCTION_TYPE => {
let function_type = TsFunctionType::unwrap_cast(node.clone());
if let Ok(AnyTsReturnType::AnyTsType(AnyTsType::TsInferType(infer_type))) =
function_type.return_type()
{
if infer_type.constraint().is_some() {
return true;
}
}
}
JsSyntaxKind::TS_CONSTRUCTOR_TYPE => {
let constructor_type = TsConstructorType::unwrap_cast(node.clone());
if let Ok(AnyTsType::TsInferType(infer_type)) = constructor_type.return_type() {
if infer_type.constraint().is_some() {
return true;
}
}
}
_ => (),
}
let return_type = match node.kind() {
JsSyntaxKind::TS_FUNCTION_TYPE => {
let function_type = TsFunctionType::unwrap_cast(node.clone());
function_type.return_type()
}
JsSyntaxKind::TS_CONSTRUCTOR_TYPE => {
let constructor_type = TsConstructorType::unwrap_cast(node.clone());
constructor_type.return_type()
}
_ => {
return false;
}
};
match return_type {
Ok(AnyTsType::TsInferType(infer_type)) => infer_type.constraint().is_some(),
_ => false
}

@MichaReiser
Copy link
Contributor

That sounds good to me. Thank you @nissy-dev for another excellent parser contribution!

@MichaReiser MichaReiser force-pushed the support-extends-constraints-on-infer-type branch from 51a7efe to f5c64b5 Compare December 30, 2022 11:06
@MichaReiser
Copy link
Contributor

!bench_parser

@github-actions
Copy link

Parser Benchmark Results

group                                 main                                   pr
-----                                 ----                                   --
parser/big5-added.json                1.02    193.1±0.55µs    87.5 MB/sec    1.00    189.4±0.30µs    89.2 MB/sec
parser/canada.json                    1.01     88.3±2.16ms    24.3 MB/sec    1.00     87.8±2.78ms    24.4 MB/sec
parser/checker.ts                     1.00    114.7±1.49ms    22.7 MB/sec    1.02    117.3±1.59ms    22.2 MB/sec
parser/compiler.js                    1.00     65.9±0.86ms    15.9 MB/sec    1.04     68.3±0.71ms    15.3 MB/sec
parser/d3.min.js                      1.00     40.3±0.32ms     6.5 MB/sec    1.05     42.3±0.35ms     6.2 MB/sec
parser/db.json                        1.00      4.8±0.02ms    37.1 MB/sec    1.00      4.9±0.02ms    36.9 MB/sec
parser/dojo.js                        1.00      3.7±0.01ms    18.8 MB/sec    1.05      3.8±0.00ms    17.9 MB/sec
parser/eucjp.json                     1.00    329.7±0.28µs   118.8 MB/sec    1.00    329.6±0.19µs   118.8 MB/sec
parser/ios.d.ts                       1.00     95.7±1.19ms    19.5 MB/sec    1.04     99.6±1.77ms    18.7 MB/sec
parser/jquery.min.js                  1.00     11.0±0.03ms     7.5 MB/sec    1.05     11.6±0.03ms     7.1 MB/sec
parser/math.js                        1.00     78.1±0.84ms     8.3 MB/sec    1.05     82.2±0.86ms     7.9 MB/sec
parser/package-lock.json              1.00      2.0±0.01ms    68.3 MB/sec    1.00      2.0±0.01ms    68.5 MB/sec
parser/parser.ts                      1.00      2.6±0.01ms    18.8 MB/sec    1.04      2.7±0.01ms    18.0 MB/sec
parser/pixi.min.js                    1.00     50.5±0.70ms     8.7 MB/sec    1.03     52.0±0.73ms     8.4 MB/sec
parser/react-dom.production.min.js    1.00     14.7±0.03ms     7.8 MB/sec    1.05     15.5±0.07ms     7.4 MB/sec
parser/react.production.min.js        1.00    737.0±9.81µs     8.3 MB/sec    1.06    777.8±2.23µs     7.9 MB/sec
parser/router.ts                      1.00   1001.8±1.71µs    30.7 MB/sec    1.04   1046.3±6.15µs    29.4 MB/sec
parser/tex-chtml-full.js              1.00    110.0±2.13ms     8.3 MB/sec    1.00    110.6±1.03ms     8.2 MB/sec
parser/three.min.js                   1.00     54.6±0.63ms    10.8 MB/sec    1.05     57.3±0.64ms    10.2 MB/sec
parser/typescript.js                  1.00    438.1±4.36ms    21.7 MB/sec    1.04    456.4±5.08ms    20.8 MB/sec
parser/vue.global.prod.js             1.00     17.9±0.07ms     6.7 MB/sec    1.08     19.2±0.11ms     6.3 MB/sec

@MichaReiser MichaReiser added the L-JavaScript Langauge: JavaScript label Dec 30, 2022
@MichaReiser MichaReiser added this to the Next milestone Dec 30, 2022
@MichaReiser MichaReiser merged commit c7e0fa6 into rome:main Dec 30, 2022
@nissy-dev nissy-dev deleted the support-extends-constraints-on-infer-type branch January 8, 2023 00:31
@Conaclos Conaclos mentioned this pull request Feb 21, 2023
4 tasks
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A-Parser Area: parser L-JavaScript Langauge: JavaScript
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants