-
Notifications
You must be signed in to change notification settings - Fork 604
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] Fix #3593 Incorrect canonical reference to aliased class in .api.json #3602
[api-extractor] Fix #3593 Incorrect canonical reference to aliased class in .api.json #3602
Conversation
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.
Thanks for the draft PR. Left some initial comments. I need to dive into this deeper, hoping to get to that this weekend. Test cases look good. 👍
apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts
Outdated
Show resolved
Hide resolved
apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts
Outdated
Show resolved
Hide resolved
Hey, sorry for the delay @fwienber! Hoping to take a look tomorrow 👍 |
No worries! Looks like we live in time zones that unfortunately have minimal working hours overlap (CEST = UTC+2 here)... |
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 nameForEmit
part LGTM, left a few comments about the namespace imports piece. LMK if I can help clarify anything!
Maybe newlineKind: 'os' should be the default for api-extractor config (ExtractorConfig.ts) anyway, but for now, let's make it explicit in the test configuration. Another possible scenario is that the tests are run under Windows, but git has been configured to used 'lf'. Then, using 'os' line endings would still lead to a git diff. But detecting the git setting is probably beyond what the test runner should do.
"Incorrect canonical reference to aliased class in .api.json" microsoft#3593 Fix first scenario from microsoft#3593. Added test scenario "docReferencesAlias", inspired by the test repo referenced in the GitHub issue: `Item` and `Options` are default exports of two separate source files, and `index` re-exports both, renaming `Options` to `renamed_Options` (preserving the "namespacing" information of the directory). `DeclarationReferenceGenerator` now receives a function to resolve a Symbol to its emit name. `ApiModelGenerator` fetches this function from `Collector`, which now maintains another map from symbol to `CollectorEntity`. If for a given symbol, there is a `CollectorEntity`, then its `emitName` is returned, the symbol's name otherwise. While this change fixes the `export { __ as __ }` scenario, for _some_ reason, it changes the output of one other test scenario, namely "ambientNameConflict". In its `*.api.json` file, some reference to the class re-exported as `MyPromise`, the name clash of localFile's `Promise` and the global `Promise` now leads to `Promise_2` being used instead of `Promise`, the old expected result that was also wrong, because it should have been `MyPromise`, and `MyPromise` should appear in `*.api.json`, but that is something completely different, so I'll ignore the problem for now and check in the changed expected `*.api.json` of "ambientNameConflict".
"Incorrect canonical reference to aliased class in .api.json" microsoft#3593 This fixes the other aliasing, namely through (nested) namespaces. Added a test scenario "docReferencesNamespaceAlias", which consists of nested namespaces that have their own index.d.ts, re-exporting the default export of each sibling file and their respective sub-namespace (top-level -> 'renamed' -> 'sub'). The *.api.json canonicalReferences now correctly use the nested namespace "fully-qualified name". Again, some other test result changed unexpectedly, maybe revealing an existing bug / wrong expected result. The scenarios are "exportImportStarAs2", "importEquals" and "includeForgottenExports". The pattern is that before, namespace and class were separate tokens, but are now aggregated to one dot-separated expression. I hope that these are improvements, but I am not sure, so this should be reviewed. Implementation: --------------- When DeclarationReferenceGenerator#_getParentReference() looks for a parent symbol, it must also resolve namespace parent symbols the Collector derived from 'import * as ___'. To that end, Collector now has a method getExportingNamespace() that pulls namespace imports from the CollectorEntities of a namespace symbol (if present). For that, namespace imports are now also registered in the _entitiesBySymbol Map. In DeclarationReferenceGenerator, the handling of namespaces had to be improved. _getNavigationToSymbol() now detects namespaces, even behind an alias, and then always returns "." as separator. To find the parent, it also considers namespaces now. _symbolToDeclarationReference() takes care not to follow an alias that leads to a *-imported namespace, because the target is the imported source file that no longer allows to retrieve the namespace name.
032e401
to
ef387a7
Compare
bump |
Hey @fwienber - I opened fwienber#2 for some proposed changes to this PR. LMK what you think - feel free to merge in/modify as you see fit. I don't actually have merge powers in this repository - I'm just a contributor - we'll need @octogonz for that. We'll also definitely want him to take a look at this PR... |
// First case: the symbol is exported via a namespace | ||
const exportingNamespace: ts.Symbol | undefined = this._collector.getExportingNamespace(symbol); | ||
if (exportingNamespace) { | ||
return this._symbolToDeclarationReference(exportingNamespace, exportingNamespace.flags, true); | ||
} |
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.
What if a symbol is exported from both a namespace and the entry point? Consider the following:
// index.ts
import * as i1 from './internal';
import { SomeClass } from './internal';
export { i1, SomeClass }
// internal.ts
export class SomeClass {}
What should the declaration reference of SomeClass
be? Today, API Extractor will actually write two items to the doc model for SomeClass
:
my-package!SomeClass:class
as a child of theApiEntryPoint
.my-package!i1.SomeClass:class
as a child of theApiNamespace
my-package!i1
.
(Note that today API Extractor will actually write only the latter to the doc model, but this is a recent bug from #3552 and relatively trivial to fix. I'm working on it.)
Ideally API Extractor would only write SomeClass
once to the doc model... so which declaration reference is right? Today, your PR generates my-package!i1.SomeClass:class
for reference tokens, in part due to the logic highlighted by this comment.
Another situation - suppose a symbol is exported from multiple namespaces, in some convoluted way like this:
// index.ts
import * as i1 from './intermediate1';
import * as i2 from './intermediate2';
export { i1, i2 }
// intermediate1.ts
import * as internal from './internal';
export { internal }
// intermediate2.ts
import * as internal from './internal';
export { internal }
// internal.ts
export class SomeClass {}
What should the declaration reference of SomeClass
be? Again, today, API Extractor writes two items for SomeClass
:
my-package!i1.internal.SomeClass
my-package!i2.internal.SomeClass
And today, your PR is indeterministic in which one it generates for reference tokens.
I think fundamentally this is another instance of #1308. I'm not sure what the right behavior is. But I do think we want the behavior to be somewhat deterministic and robust to shuffling of API items. One idea is to choose the shortest ID, and among IDs of the same length, choose the one that comes first alphabetically. That being said, I'm not sure if this open question should block this PR.
@octogonz - what do you 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.
Seems like a bad idea in the first place to expose the same module under two different names. However, if you want to support this use case, maybe the developer should have some means to specify which alias is the "canonical" one and thus has precedence. Could be a new api-extractor configuration or even a new TSDoc tag.
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 idea is to choose the shortest ID, and among IDs of the same length, choose the one that comes first alphabetically.
This sounds good to me. Maybe we could also provide a TSDoc tag that allows the choice to be explicitly specified, similar to suggestions for the other ae-ambiguous-ids issues.
Some proposed changes to microsoft#3602
@zelliott thanks for the PR, I merged it without changes! |
Sure! I'm taking another look at this PR right now. Do you mind sharing a code example of the issue you're describing? I can try to help with this as well - but probably worth doing in a follow-up PR as this one is somewhat large as-is. |
It was in my "real world" example, but should be easy to add to some scenario. Just a minute! |
Okay, just go to |
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 this looks good to me! Regarding the @link
and @inheritDoc
stuff - I'll take a look, but I don't see it as blocking this PR.
build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.json
Outdated
Show resolved
Hide resolved
apps/api-extractor/src/generators/DeclarationReferenceGenerator.ts
Outdated
Show resolved
Hide resolved
As suggested by @zelliott, the logic when to fall back to Navigation.Exports (`.`) must use `consumable`, not `exported`. Updated the improved build-tests/api-extractor-scenarios results.
…extractor-fix-3593-reference-alias # Conflicts: # apps/api-extractor/src/collector/CollectorEntity.ts # build-tests/api-extractor-scenarios/etc/exportImportStarAs2/api-extractor-scenarios.api.json # build-tests/api-extractor-scenarios/etc/namespaceImports/api-extractor-scenarios.api.json # build-tests/api-extractor-scenarios/etc/namespaceImports2/api-extractor-scenarios.api.json
…ngConsumableParent()" method because it does nontrivial work
...icrosoft/api-extractor/fwienber-api-extractor-fix-3593-reference-alias_2022-08-29-07-53.json
Show resolved
Hide resolved
…r-fix-3593-reference-alias [api-extractor] Fix microsoft#3593 Incorrect canonical reference to aliased class in .api.json
Summary
api-extractor currently does not support packages with multiple entry points.
As a workaround, multiple entry points can be aggregated through a single entry point, directly or indirectly re-exporting them.
This works as long as their original names are used. However, when trying to rename exports, for example because they are grouped in directories and when "flattening", name-clashes must be avoided, api-extractor produces document model (
*.api.json
) files with incorrect canonical references. Both renaming and moving a module into a namespace do not work:export { Foo as subpath_Foo } from "./subpath/Foo"
import * as subpath from "./subpath"; export { subpath }
(this is the workaround api-extractor suggests forexport * as subpath from "./subpath"
not yet being supported)In both cases, the
subpath
information is lost in canonical references toFoo
.For details and an example, see #3593.
Fixes #3593
Details
After a pointer by @zelliott, extended
Collector
to offer a new function to retrieve the "emit name" of a givents.Symbol
. TheemitName
property can be fetched from aCollectorEntity
and is rename-aware.DeclarationReferenceGenerator
calls this function to use the correct emit name if the referenced symbol has been renamed via a re-export. This fixes the first part of the bug.Concerning the second part of the bug, namespaced re-exports:
When
DeclarationReferenceGenerator#_getParentReference()
looks for a parent symbol, it must also resolve namespace parent symbols the Collector derived fromimport * as ___
. To that end,Collector
now has a functiongetExportingNamespace()
that finds namespace imports of the given symbol by looking up the parent namespace in the correspondingCollectorEntity
(if present).This new function is then used by
DeclarationReferenceGenerator
to compute the correct parentDeclarationReference
. Also there, handling of namespaces had to be improved.How it was tested
Added two test scenarios, one for each part of the bug:
docReferencesAlias
uses renaming,docReferencesNamespaceAlias
uses namespaces for re-export.When building the
build-tests
, there is no git diff, so the expected*.api.json
files with the correct canonical references are now generated.