Skip to content
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

[api-extractor] API report shows spurious diffs because compiler emits inferred types inconsistently #1958

Open
1 of 2 tasks
Tracked by #290
dmichon-msft opened this issue Jun 23, 2020 · 13 comments
Open
1 of 2 tasks
Tracked by #290
Labels
needs design The next step is for someone to propose the details of an approach for solving the problem

Comments

@dmichon-msft
Copy link
Contributor

dmichon-msft commented Jun 23, 2020

Is this a feature or a bug?

  • Feature
  • Bug

Please describe the actual behavior.
TypeScript does not define the output order of the constituents of a union type. As a result of this, the output from API-extractor is inconsistent between fresh builds and incremental builds. From discussion with the TypeScript team, this is related to the order of loading of types and whether or not certain types need to be instantiated to type check the set of files that were built during the compilation.

The default sort order for union type constituents is based on the internal debug id, which is assigned when the type is instantiated.

If the issue is a bug, how can we reproduce it? Please provide detailed steps and include a GitHub branch if applicable. Your issue will get resolved faster if you can make it easy to investigate.
https://github.com/dmichon-msft/union-type-order-demo
Repro steps:

  1. Increment value of incrementStep1 in index.ts. Run npm run build. Observe order of export const test declaration.
  2. Increment value of incrementStep2 in B.ts. Run npm run build. Observe order of export const test declaration.
  3. Repeat (1) if necessary.

The order will change depending on which files needed recompilation.
The order directly matches that which is output in lib/index.d.ts, which is what changes.

What is the expected behavior?
Since API extractor is concerned with substantive changes, it should define a standard (potentially configurable, but not required) sort order for the constituents of union types, regardless of the order output by TypeScript. This should be similar to the sorting of interface members that api-extractor already performs.

If this is a bug, please provide the tool version, Node.js version, and OS.

  • Tool: api-extractor
  • Tool Version: 7.8.12, with TypeScript 3.7.5
  • Node Version: 10.19.0
    • Is this a LTS version? Yes
    • Have you tested on a LTS version? Yes
  • OS: Windows 10

Edited 6/23@1:29 PM: simplified repro.

@octogonz
Copy link
Collaborator

octogonz commented Jun 23, 2020

@dmichon-msft This repro doesn't seem to work.

1. Increment value of `incrementStep1` in `index.ts`

index.ts looks like this -- I don't see incrementStep1 in there:

export * from './A';
export * from './B';
export * from './C';

The README.md in the repo instead says:

Make a change to A.ts (e.g. add an optional property to IKeyed). Run npm run build. Observe order of
export const test declaration.

Add what property? What should I expect to observe exactly?

Please give explicit instructions.

@octogonz octogonz added the needs more info We can't proceed because we need a better repro or an answer to a question label Jun 23, 2020
@dmichon-msft
Copy link
Contributor Author

Oops, forgot to push when I simplified the repro.
The states are:

export declare const test: ObjectWithProps<Pick<ITest, "a" | "b" | "d" | "c">>;

and

export declare const test: ObjectWithProps<Pick<ITest, "d" | "c" | "b" | "a">>;

Further investigation has concluded that this repro is specifically dependent on inferring a type that contains a union. Providing an explicit typedef solves the underlying instability issue.

@octogonz
Copy link
Collaborator

octogonz commented Jun 23, 2020

Providing an explicit typedef solves the underlying instability issue.

(BTW I thought your ESLint rules require explicit type declarations?)

That said, if the compiler's output depends non-deterministically on the order in which an incremental build occurs -- that seems like a design flaw in the incremental build feature. Implementing a deterministic algorithm might be difficult work. But that doesn't make it acceptable to remove the guarantee of determinism.

If we remove that guarantee, then essentially ANY type expression can shuffle around arbitrarily during compilation (not just unions), which would imply that API Extractor has the burden of normalizing every detail of every type expression. This seems very much like the compiler's responsibility.

@octogonz
Copy link
Collaborator

To clarify a bit: Since there are many equivalent ways to infer a type, it's unsurprising that a type expression might shuffle around a bit due to modifications of its inputs. That part doesn't bother me, or seem like much of a problem for API Extractor's reporting.

The problematic part of this repro is:

  1. npm run build
  2. The compiler outputs "d" | "c" | "b" | "a"
  3. rd /s lib
  4. npm run build
  5. The compiler outputs "a" | "b" | "d" | "c"

Are we really considering this to be a correctly functioning incremental build implementation?

CC @RyanCavanaugh @DanielRosenwasser for thoughts

@RyanCavanaugh
Copy link
Member

Yes, this is functioning correctly. The types are equivalent and we have never instructed anyone to take a dependency on the text representation of a union type, nor would we.

@octogonz
Copy link
Collaborator

octogonz commented Jun 25, 2020

The types are equivalent and we have never instructed anyone to take a dependency on the text representation of a union type, nor would we.

@RyanCavanaugh Then we need to normalize the expression afterwards. (This scenario is not about type equivalence, but rather about a text file being processed for reporting and documentation purposes.)

Question: If the compiler now produces random outputs when invoked on the same input file, how random is it? Is it merely union type members shuffling around? Can entire expressions be replaced with a semantically equivalent expression? Can functions be reordered? Can line numbers be added/removed that will shift the source maps?

Basically we need to know what kind of normalization is needed. Today API Extractor makes efforts to faithfully preserve stylistic aspects of its input as much as possible.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jun 25, 2020

Relevant things that could happen to my knowledge:

  • Union types are printed in an arbitrary order
  • Type aliases may or may not be used to represent equivalent types that were produced from inference. If multiple type aliases for the same type exist, which alias is used (if any) is arbitrary
  • Properties in an inferred object type may be printed in an arbitrary order

@octogonz
Copy link
Collaborator

Does this only happen with inferred types? Will explicitly declared types always get emitted consistently?

@RyanCavanaugh
Copy link
Member

Yeah, if we're able to re-use the existing text of a node, we always will

@octogonz
Copy link
Collaborator

  • Union types are printed in an arbitrary order
  • Properties in an inferred object type may be printed in an arbitrary order

For this, it sounds pretty straightforward to normalize by sorting them alphabetically. (Might even be cheap and useful for the compiler to do that.)

Type aliases may or may not be used to represent equivalent types that were produced from inference. If multiple type aliases for the same type exist, which alias is used (if any) is arbitrary

🤔 Does a normal form even exist for this problem?

Or suppose we wanted to keep the previously reported expression if nothing has changed. Is there even a well defined test to determine if two type expressions are exactly equivalent?

@octogonz octogonz removed the needs more info We can't proceed because we need a better repro or an answer to a question label Jul 2, 2020
@octogonz octogonz changed the title [api-extractor] Normalize order of union type constituents [api-extractor] API report shows spurious diffs because compiler emits inferred types inconsistently Jul 2, 2020
@octogonz octogonz added the needs design The next step is for someone to propose the details of an approach for solving the problem label Jul 2, 2020
@octogonz
Copy link
Collaborator

octogonz commented Jul 2, 2020

Possible solutions:

  • The simplest complete solution is for @rushstack/eslint-config to warn about usage of inferred types. We already do that.

  • It would be nice for API Extractor itself to warn about inferred types, but this is not easy because API Extractor analyses .d.ts files. We would need to use .d.ts maps and inspect the corresponding .ts files.

  • Another idea is for API Extractor to sort subexpressions that are a union of string types ("a" | "b" | "d" | "c") but NOT try to normalize any other inferred type subexpression. This would technically resolve @dmichon-msft's original problem, but it implies more general support. I wouldn't want to implement this without evidence that string unions are somehow an important/common special case.

  • Lastly, we could simply update the website to document that API reporting doesn't work reliably with inferred types, and leave it at that. Based on what @RyanCavanaugh said, I'm coming to think emitted inferred types are simply (1) not easy to track in an API report and thus (2) not a best practice for a public API.

@iclanton do you have thoughts?

mshustov added a commit to mshustov/kibana that referenced this issue Sep 7, 2020
mshustov added a commit to elastic/kibana that referenced this issue Sep 8, 2020
* disable incremental builds for public type generation.

They generate non-deterministic types microsoft/rushstack#1958

* do not infer public types
mshustov added a commit to mshustov/kibana that referenced this issue Sep 8, 2020
* disable incremental builds for public type generation.

They generate non-deterministic types microsoft/rushstack#1958

* do not infer public types
mshustov added a commit to elastic/kibana that referenced this issue Sep 8, 2020
* disable incremental builds for public type generation.

They generate non-deterministic types microsoft/rushstack#1958

* do not infer public types
no23reason added a commit to no23reason/gooddata-ui-sdk that referenced this issue Oct 23, 2020
This needs the COmmonJS builds to not be incremental
See microsoft/rushstack#1958 (comment)
for details.

JIRA: RAIL-1815
no23reason added a commit to no23reason/gooddata-ui-sdk that referenced this issue Oct 23, 2020
This needs the CommonJS builds to not be incremental
See microsoft/rushstack#1958 (comment)
for details.

JIRA: RAIL-1815
@maximzavadskiy
Copy link

We are currently using "incremental": false in tsconfig.json to handle this problem

@relm923
Copy link
Contributor

relm923 commented Jul 23, 2024

Using TS project references and composite builds makes this issue for more pronounced. Union ordering and object properties shuffle ordering depending on build order leaning to false positive diffs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs design The next step is for someone to propose the details of an approach for solving the problem
Projects
Status: AE/AD
5 participants