Skip to content

Commit

Permalink
docs: allow @mixes TSDoc tag for documenting mixins (#10545)
Browse files Browse the repository at this point in the history
  • Loading branch information
Qjuh authored Oct 16, 2024
1 parent 960a80d commit 62fb9de
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 29 deletions.
114 changes: 86 additions & 28 deletions packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { TSDocConfiguration } from '@microsoft/tsdoc';
import {
type DocNode,
type DocPlainText,
DocDeclarationReference,
DocNodeKind,
TSDocConfiguration,
DocMemberReference,
DocMemberIdentifier,
} from '@microsoft/tsdoc';
import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { InternalError } from '@rushstack/node-core-library';
import type { IExcerptToken, IExcerptTokenRange } from '../index.js';
Expand Down Expand Up @@ -42,6 +50,11 @@ export interface IApiItemContainerJson extends IApiItemJson {
preserveMemberOrder?: boolean;
}

interface Mixin {
declarationReference: DocDeclarationReference;
typeParameters: IExcerptTokenRange[];
}

interface ExcerptTokenRangeInDeclaredItem {
item: ApiDeclaredItem;
range: IExcerptTokenRange;
Expand Down Expand Up @@ -393,14 +406,52 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
}
}

const findPlainTextNode = (node: DocNode): DocPlainText | undefined => {
switch (node.kind) {
case DocNodeKind.PlainText:
return node as DocPlainText;
default:
for (const child of node.getChildNodes()) {
const result = findPlainTextNode(child);
if (result) return result;
}
}

return undefined;
};

// Interfaces can extend multiple interfaces, so iterate through all of them.
// Also Classes can have multiple mixins
const extendedItems: IMappedTypeParameters[] = [];
let extendsTypes: readonly HeritageType[] | undefined;
let extendsTypes: readonly (HeritageType | Mixin)[] | undefined;

switch (next.item.kind) {
case ApiItemKind.Class: {
const apiClass: ApiClass = next.item as ApiClass;
extendsTypes = apiClass.extendsType ? [apiClass.extendsType] : [];
const configuration = apiClass.tsdocComment?.configuration ?? new TSDocConfiguration();
const mixins =
apiClass.tsdocComment?.customBlocks
.filter(
(block) => block.blockTag.tagName === '@mixes', // &&
// block.getChildNodes().some((node) => node.kind === DocNodeKind.PlainText),
)
.map(findPlainTextNode)
.filter((block) => block !== undefined)
.map((block) => ({
declarationReference: new DocDeclarationReference({
configuration,
memberReferences: block.text.split('.').map(
(part, index) =>
new DocMemberReference({
configuration,
hasDot: index > 0,
memberIdentifier: new DocMemberIdentifier({ configuration, identifier: part }),
}),
),
}),
typeParameters: [] as IExcerptTokenRange[],
})) ?? [];
extendsTypes = apiClass.extendsType ? [apiClass.extendsType, ...mixins] : [...mixins];
break;
}

Expand All @@ -425,30 +476,38 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
}

for (const extendsType of extendsTypes) {
// We want to find the reference token associated with the actual inherited declaration.
// In every case we support, this is the first reference token. For example:
//
// ```
// export class A extends B {}
// ^
// export class A extends B<C> {}
// ^
// export class A extends B.C {}
// ^^^
// ```
const firstReferenceToken: ExcerptToken | undefined = extendsType.excerpt.spannedTokens.find(
(token: ExcerptToken) => {
return token.kind === ExcerptTokenKind.Reference && token.canonicalReference;
},
);
let canonicalReference: DeclarationReference | DocDeclarationReference;
if ('excerpt' in extendsType) {
// We want to find the reference token associated with the actual inherited declaration.
// In every case we support, this is the first reference token. For example:
//
// ```
// export class A extends B {}
// ^
// export class A extends B<C> {}
// ^
// export class A extends B.C {}
// ^^^
// ```
const firstReferenceToken: ExcerptToken | undefined = extendsType.excerpt.spannedTokens.find(
(token: ExcerptToken) => {
return token.kind === ExcerptTokenKind.Reference && token.canonicalReference;
},
);

if (!firstReferenceToken) {
messages.push({
messageId: FindApiItemsMessageId.ExtendsClauseMissingReference,
text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.item.displayName} because no canonical reference was found`,
});
maybeIncompleteResult = true;
continue;
}

if (!firstReferenceToken) {
messages.push({
messageId: FindApiItemsMessageId.ExtendsClauseMissingReference,
text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.item.displayName} because no canonical reference was found`,
});
maybeIncompleteResult = true;
continue;
canonicalReference = firstReferenceToken.canonicalReference!;
} else {
// extendsType is a Mixin
canonicalReference = extendsType.declarationReference;
}

const apiModel: ApiModel | undefined = this.getAssociatedModel();
Expand All @@ -461,10 +520,9 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
continue;
}

const canonicalReference: DeclarationReference = firstReferenceToken.canonicalReference!;
const apiItemResult: IResolveDeclarationReferenceResult = apiModel.resolveDeclarationReference(
canonicalReference,
undefined,
this,
);

const apiItem: ApiItem | undefined = apiItemResult.resolvedApiItem;
Expand Down
6 changes: 5 additions & 1 deletion packages/api-extractor-model/src/model/ApiPackage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented

const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration();

if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004 && 'tsdocConfiguration' in jsonObject) {
if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004 && 'tsdocConfig' in jsonObject.metadata) {
const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromObject(jsonObject.metadata.tsdocConfig);
if (tsdocConfigFile.hasErrors) {
throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary());
Expand Down Expand Up @@ -420,6 +420,8 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
for (const key of Object.keys(item)) {
if (key === 'dependencies') {
result[MinifyJSONMapping.dependencies] = item.dependencies;
} else if (key === 'tsdocConfig') {
result[MinifyJSONMapping.tsdocConfig] = item.tsdocConfig;
} else
result[MinifyJSONMapping[key as keyof typeof MinifyJSONMapping]] =
typeof item[key] === 'object' ? mapper(item[key]) : item[key];
Expand All @@ -440,6 +442,8 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
for (const key of Object.keys(item)) {
if (key === MinifyJSONMapping.dependencies) {
result.dependencies = item[MinifyJSONMapping.dependencies];
} else if (key === MinifyJSONMapping.tsdocConfig) {
result.tsdocConfig = item[MinifyJSONMapping.tsdocConfig];
} else
result[
Object.keys(MinifyJSONMapping).find(
Expand Down
4 changes: 4 additions & 0 deletions packages/api-extractor/extends/tsdoc-base.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
{
"tagName": "@preapproved",
"syntaxKind": "modifier"
},
{
"tagName": "@mixes",
"syntaxKind": "block"
}
],

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { SharedChatInputCommandSubcommands } from './mixins/SharedSubcommands.js

/**
* A builder that creates API-compatible JSON data for chat input commands.
*
* @mixes CommandBuilder<RESTPostAPIChatInputApplicationCommandsJSONBody>
* @mixes SharedChatInputCommandOptions
* @mixes SharedNameAndDescription
* @mixes SharedChatInputCommandSubcommands
*/
export class ChatInputCommandBuilder extends Mixin(
CommandBuilder<RESTPostAPIChatInputApplicationCommandsJSONBody>,
Expand Down
9 changes: 9 additions & 0 deletions packages/builders/tsdoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"tagDefinitions": [
{
"tagName": "@mixes",
"syntaxKind": "block"
}
]
}

0 comments on commit 62fb9de

Please sign in to comment.