docs: allow @mixes TSDoc tag for documenting mixins (#10545)

This commit is contained in:
Qjuh
2024-10-16 02:31:04 +02:00
committed by GitHub
parent 960a80dbae
commit 62fb9de9c9
5 changed files with 109 additions and 29 deletions

View File

@@ -2,7 +2,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information. // 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 type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { InternalError } from '@rushstack/node-core-library'; import { InternalError } from '@rushstack/node-core-library';
import type { IExcerptToken, IExcerptTokenRange } from '../index.js'; import type { IExcerptToken, IExcerptTokenRange } from '../index.js';
@@ -42,6 +50,11 @@ export interface IApiItemContainerJson extends IApiItemJson {
preserveMemberOrder?: boolean; preserveMemberOrder?: boolean;
} }
interface Mixin {
declarationReference: DocDeclarationReference;
typeParameters: IExcerptTokenRange[];
}
interface ExcerptTokenRangeInDeclaredItem { interface ExcerptTokenRangeInDeclaredItem {
item: ApiDeclaredItem; item: ApiDeclaredItem;
range: IExcerptTokenRange; range: IExcerptTokenRange;
@@ -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. // Interfaces can extend multiple interfaces, so iterate through all of them.
// Also Classes can have multiple mixins
const extendedItems: IMappedTypeParameters[] = []; const extendedItems: IMappedTypeParameters[] = [];
let extendsTypes: readonly HeritageType[] | undefined; let extendsTypes: readonly (HeritageType | Mixin)[] | undefined;
switch (next.item.kind) { switch (next.item.kind) {
case ApiItemKind.Class: { case ApiItemKind.Class: {
const apiClass: ApiClass = next.item as ApiClass; 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; break;
} }
@@ -425,30 +476,38 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
} }
for (const extendsType of extendsTypes) { for (const extendsType of extendsTypes) {
// We want to find the reference token associated with the actual inherited declaration. let canonicalReference: DeclarationReference | DocDeclarationReference;
// In every case we support, this is the first reference token. For example: 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 {}
// ^ // ^
// export class A extends B.C {} // 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; const firstReferenceToken: ExcerptToken | undefined = extendsType.excerpt.spannedTokens.find(
}, (token: ExcerptToken) => {
); return token.kind === ExcerptTokenKind.Reference && token.canonicalReference;
},
);
if (!firstReferenceToken) { if (!firstReferenceToken) {
messages.push({ messages.push({
messageId: FindApiItemsMessageId.ExtendsClauseMissingReference, messageId: FindApiItemsMessageId.ExtendsClauseMissingReference,
text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.item.displayName} because no canonical reference was found`, text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.item.displayName} because no canonical reference was found`,
}); });
maybeIncompleteResult = true; maybeIncompleteResult = true;
continue; continue;
}
canonicalReference = firstReferenceToken.canonicalReference!;
} else {
// extendsType is a Mixin
canonicalReference = extendsType.declarationReference;
} }
const apiModel: ApiModel | undefined = this.getAssociatedModel(); const apiModel: ApiModel | undefined = this.getAssociatedModel();
@@ -461,10 +520,9 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
continue; continue;
} }
const canonicalReference: DeclarationReference = firstReferenceToken.canonicalReference!;
const apiItemResult: IResolveDeclarationReferenceResult = apiModel.resolveDeclarationReference( const apiItemResult: IResolveDeclarationReferenceResult = apiModel.resolveDeclarationReference(
canonicalReference, canonicalReference,
undefined, this,
); );
const apiItem: ApiItem | undefined = apiItemResult.resolvedApiItem; const apiItem: ApiItem | undefined = apiItemResult.resolvedApiItem;

View File

@@ -293,7 +293,7 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration(); 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); const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromObject(jsonObject.metadata.tsdocConfig);
if (tsdocConfigFile.hasErrors) { if (tsdocConfigFile.hasErrors) {
throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary()); throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary());
@@ -420,6 +420,8 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
for (const key of Object.keys(item)) { for (const key of Object.keys(item)) {
if (key === 'dependencies') { if (key === 'dependencies') {
result[MinifyJSONMapping.dependencies] = item.dependencies; result[MinifyJSONMapping.dependencies] = item.dependencies;
} else if (key === 'tsdocConfig') {
result[MinifyJSONMapping.tsdocConfig] = item.tsdocConfig;
} else } else
result[MinifyJSONMapping[key as keyof typeof MinifyJSONMapping]] = result[MinifyJSONMapping[key as keyof typeof MinifyJSONMapping]] =
typeof item[key] === 'object' ? mapper(item[key]) : item[key]; typeof item[key] === 'object' ? mapper(item[key]) : item[key];
@@ -440,6 +442,8 @@ export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumented
for (const key of Object.keys(item)) { for (const key of Object.keys(item)) {
if (key === MinifyJSONMapping.dependencies) { if (key === MinifyJSONMapping.dependencies) {
result.dependencies = item[MinifyJSONMapping.dependencies]; result.dependencies = item[MinifyJSONMapping.dependencies];
} else if (key === MinifyJSONMapping.tsdocConfig) {
result.tsdocConfig = item[MinifyJSONMapping.tsdocConfig];
} else } else
result[ result[
Object.keys(MinifyJSONMapping).find( Object.keys(MinifyJSONMapping).find(

View File

@@ -32,6 +32,10 @@
{ {
"tagName": "@preapproved", "tagName": "@preapproved",
"syntaxKind": "modifier" "syntaxKind": "modifier"
},
{
"tagName": "@mixes",
"syntaxKind": "block"
} }
], ],

View File

@@ -9,6 +9,11 @@ import { SharedChatInputCommandSubcommands } from './mixins/SharedSubcommands.js
/** /**
* A builder that creates API-compatible JSON data for chat input commands. * 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( export class ChatInputCommandBuilder extends Mixin(
CommandBuilder<RESTPostAPIChatInputApplicationCommandsJSONBody>, CommandBuilder<RESTPostAPIChatInputApplicationCommandsJSONBody>,

View File

@@ -0,0 +1,9 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
"tagDefinitions": [
{
"tagName": "@mixes",
"syntaxKind": "block"
}
]
}