-
Notifications
You must be signed in to change notification settings - Fork 625
[api-extractor-model] Add new ApiItemContainerMixin.findMembersWithInheritance method #3469
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
Conversation
(Let's focus on getting PRs 3483, 3484, 3435 merged first, then remind me and I'll take a look at this.) |
0e56853
to
5ea39df
Compare
It sounds okay to me. The most interesting case is:
This design choice focuses on having documentation pages (API items) for inherited items. We might contrast with a design that focuses on showing every possible item even if we can't document it: For each API item, API Extractor might use the compiler engine to collect every inherited member, and add supplementary trivia fields to
If 🤔 And I suppose there's a counterargument that, if an API is actually important to end users, then we should find a way to define an In any case, such a feature could be a complementary to what you're implementing in this PR.
I think I agree with The most interesting gap would be something like: package-a export interface A { } package-b export interface B extends A { } package-c export interface C extends B { } ...where we generate
Ideally we should make an
I seem to remember that DocFX itself already has a way to include inherited members. (?) It's best not to modify |
As soon as anyone is available to do that work. The |
364dbf3
to
179e2e0
Compare
af2b89e
to
1d8eaf1
Compare
const typeParameters: IYamlParameter[] = this._populateYamlTypeParameters(uid, apiItem); | ||
if (typeParameters.length) { | ||
yamlItem.syntax = { typeParameters }; |
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.
This code was previously only called for ApiInterface
items, I think this was a bug? Now it's called for both ApiClass
and ApiInterface
items.
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.
This was introduced by PR #1317. The PR notes say that "type parameters are supported by docfx" but doesn't give any reason why the YAML is only populated for interfaces.
@rbuckton was this simply an oversight? If we start emitting type parameters for classes in DocFX YAML, is there any risk of a regression?
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 reverted this change from this PR because it's no longer really related (I'm not even touching YamlDocumenter.ts
anymore. It'd still be nice to fix this, it looks like an oversight, I'll open a separate 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.
Opened #3546.
@@ -192,11 +192,11 @@ | |||
"text": "export interface Constraint " | |||
} | |||
], | |||
"extendsTokenRanges": [], |
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.
In a future PR, I'd like to never write empty token ranges to API doc models.
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 serializer currently takes the approach of always writing every field, even if the contents are empty. The original motivation was to make the JSON file format somewhat self-documenting, such that someone could write a correct loader for it without having to look at the source code. It seemed more important back then, because there was no api-extractor-model
library to validate the deserialization, just a JSON schema and some TypeScript interfaces to access the JSON objects.
We could consider a different approach of trying to make the .api.json files as small as possible. But that would only make sense if we applied it across all fields, not just the token ranges.
1d8eaf1
to
3dab715
Compare
Okay @octogonz, I think I've responded to all of your open comments on this PR. I've also pulled out all api-documenter changes necessary for actually showing inherited members into a separate child branch. This child branch includes:
How do we want to go about reviewing and merging these branches/PRs? |
Let's make a second PR for |
Created zelliott#3. |
* inherited members. If true, the `FindMembersWithInheritanceMessageCallback` callback | ||
* will be called with messages explaining the errors in more detail. | ||
*/ | ||
maybeIncompleteMembers: boolean; |
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.
missingData
? incompleteResult
?
If I saw maybeIncompleteMembers=true
without any docs, from the name alone I'm not sure it would be obvious that a problem occurred, or that Members
is referring to the FindMembersWithInheritanceResult.members
output. (Maybe I'm being obtuse though heheh.)
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.
Renamed to maybeIncompleteResult
, let me know if you'd prefer incompleteResult
. My thought behind "maybe" is that it's possible that even if an error is hit finding inherited members, the inherited members are still complete. Consider the following:
type A = {}
interface B extends A {}
findMembersWithInheritance
doesn't support finding inherited members from type aliases like A
, but fortunately in this case it doesn't really matter, as A
has no members.
c082a44
to
682c5b5
Compare
682c5b5
to
c1ab8e0
Compare
common/changes/@microsoft/api-documenter/inheritance-docs_2022-07-15-17-26.json
Outdated
Show resolved
Hide resolved
common/changes/@microsoft/api-extractor-model/inheritance-docs_2022-06-14-22-47.json
Outdated
Show resolved
Hide resolved
common/changes/@microsoft/api-extractor/inheritance-docs_2022-06-14-22-47.json
Outdated
Show resolved
Hide resolved
Okay, I think this PR is ready for another look. Some open questions:
|
* | ||
* When called on `B`, this method will return `B.a` with type `T` as opposed to type | ||
* `number`, although the latter is more accurate. | ||
*/ |
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.
@zelliott I moved your doc comment to interface ApiItemContainerMixin
which is the publicly visible signature. (The class MixedClass
declaration is inside a local function scope.)
if (extendsTypes === undefined) { | ||
messages.push({ | ||
messageId: FindApiItemsMessageId.UnsupportedKind, | ||
text: `Item ${next.displayName} is of unsupported kind ${next.kind}.` |
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 understand this logic. The condition extendsTypes === undefined
means that the current thing is not a class or an interface; it is some other container such as ApiEnum
. If I call apiEnum.findMembersWithInheritance()
, I would expect it to return the enum members. The fact that there aren't any "inherited" members seems unimportant. Whereas this message seems to imply that there is some kind of problem: "Item apiEnum is of unsupported kind Enum"
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 fact that there aren't any "inherited" members seems unimportant.
Hm, I suppose I was thinking that if someone calls apiEnum.findMembersWithInheritance()
the message might be useful in communicating essentially "Hey, just so you know, you asked for inherited members, but ApiEnums do not support inheritance". But I can see your point of view, so I'm happy to add a check at the top of this method that basically just defers to get members()
if the API item is neither an ApiClass
or ApiInterface
.
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 feel strongly either way though, so I changed it in #3545 to just return the members without any messages or incompleteness.
if (!firstReferenceToken) { | ||
messages.push({ | ||
messageId: FindApiItemsMessageId.UnexpectedExcerptTokens, | ||
text: `Encountered unexpected excerpt tokens in ${next.displayName}. Excerpt: ${extendsType.excerpt.text}.` |
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.
!firstReferenceToken
occurs if the excerpt does not contain any token with a canonicalReference
. The analyzer can generate canonical references for a very wide variety of declarations, including things that don't get an ApiItem
, so it's interesting to consider how this situation might arise. Here's one example:
/** @public */
export declare class MyClass extends (class { }) {
}
We can imagine other edge cases that should have references, but DeclarationReferenceGenerator hasn't implemented support (maybe an augmented type or somesuch).
Is "Encountered unexpected excerpt tokens" the best way to describe these cases? Here's a couple alternative ideas:
- Safe approach: "A canonical reference was not found in the extends clause" accurately reports the implementation logic that failed
- Friendly approach: "The extends clause does not refer to type that can be analyzed" tries to provide a meaningful interpretation of the situation that will hopefully be correct
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 preferred your "safe approach" to your "friendly approach" because I thought the latter was too generic (i.e. why can't the type be analyzed - is it because there was no reference, is it because there was no associated ApiModel
, is it because declaration resolution failed, etc). See https://github.com/microsoft/rushstack/pull/3545/files#diff-f0516a1941ffe40f35eb325cb731d6d3af1663276d61824a4a6da636d91d278eR382-R383.
if (!apiModel) { | ||
messages.push({ | ||
messageId: FindApiItemsMessageId.MissingApiModel, | ||
text: `Unable to get the associated model of ${next.displayName}.` |
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.
This might arise if the ApiItem
object was newly constructed but not added to an ApiModel
yet. The problem isn't that we can't get the model, it's that no model has been associated. Maybe we could word it like:
"Unable to analyze references of API item ${next.displayName} because it is not associated with an ApiModel."
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.
Summary
This PR includes the api-extractor-model changes necessary for addressing #3429. All api-documenter changes for actually showing the inherited members as well as testing this change are in the child PR zelliott#3.
Details
Changes include
ApiItemContainerMixin. findMembersWithInheritance
which is the meat of this PR. This method walks an item's inheritance tree and finds all immediate and inherited members. It has comprehensive comments explaining its behavior and various edge cases. I'll also go into various scenarios later in this PR description.IFindApiItemsResult
type that can be used as a generic return type for future "find operations" performed by api-extractor-model.Support levels for various types of inheritance
There are many different flavors of inheritance in TypeScript and this PR does not support all of them. Here is this PR's support level by inheritance scenario:
Full support
class A extends B
orinterface A extends B
)interface A extends B, C
)Partial support
class A extends B<number>
). Types of inherited members are relative to their defining class, not inherited class.No support
Open questions
I'd expect these questions to be resolved before transitioning this PR from draft --> ready:
findMembersWithInheritance
the right approach here or are we essentially "re-implementing inheritance" and we're better off using the type checker in api-extractor to encode the appropriate inheritance information to the API model Answer: @octogonz and I decided this is the right approach for now and can be extended to support more types of inheritance in the futureshowInheritedMembers
config setting is only available via theapi-documenter generate
workflow, and the test project uses theapi-documenter markdown
workflow. It's non trivial to change fromgenerate
tomarkdown
. I've manually tested the change on my end by manually and it looks good, but I'd like actual checked in artifacts. Answer: Will add a new api-documenter-scenarios test project in a child PR.YamlDocumenter
? Answer: No.How it was tested
Tested in the child PR zelliott#3.