feat(api-extractor): replace type parameters with their actual values on inherited members (#9939)

* refactor: use tokenRange for typeParams in heritage

* fix: correct type param replacement
This commit is contained in:
Qjuh
2023-11-11 15:18:08 +01:00
committed by GitHub
parent 5b0aa92c81
commit 5a4c9755c3
8 changed files with 116 additions and 89 deletions

View File

@@ -24,14 +24,12 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
return ( return (
<span> <span>
{excerpt.spannedTokens.map((token, idx) => { {excerpt.spannedTokens.map((token, idx) => {
// TODO: Real fix in api-extractor needed
const text = token.text.replaceAll('\n', '').replaceAll(/\s{2}$/g, '');
if (token.kind === ExcerptTokenKind.Reference) { if (token.kind === ExcerptTokenKind.Reference) {
if (text in BuiltinDocumentationLinks) { if (token.text in BuiltinDocumentationLinks) {
const href = BuiltinDocumentationLinks[text as keyof typeof BuiltinDocumentationLinks]; const href = BuiltinDocumentationLinks[token.text as keyof typeof BuiltinDocumentationLinks];
return ( return (
<DocumentationLink key={`${text}-${idx}`} href={href}> <DocumentationLink key={`${token.text}-${idx}`} href={href}>
{text} {token.text}
</DocumentationLink> </DocumentationLink>
); );
} }
@@ -45,20 +43,22 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
// dapi-types doesn't have routes for class members // dapi-types doesn't have routes for class members
// so we can assume this member is for an enum // so we can assume this member is for an enum
if (meaning === 'member' && path && 'parent' in path) href += `/enum/${path.parent}#${path.component}`; if (meaning === 'member' && path && 'parent' in path) href += `/enum/${path.parent}#${path.component}`;
else if (meaning === 'type') href += `#${text}`; else if (meaning === 'type' || meaning === 'var') href += `#${token.text}`;
else href += `/${meaning}/${text}`; else href += `/${meaning}/${token.text}`;
return ( return (
<DocumentationLink key={`${text}-${idx}`} href={href}> <DocumentationLink key={`${token.text}-${idx}`} href={href}>
{text} {token.text}
</DocumentationLink> </DocumentationLink>
); );
} }
const item = model.resolveDeclarationReference(token.canonicalReference!, model).resolvedApiItem; const item = token.canonicalReference
? model.resolveDeclarationReference(token.canonicalReference!, model).resolvedApiItem
: null;
if (!item) { if (!item) {
return text; return token.text;
} }
return ( return (
@@ -68,12 +68,12 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
key={`${item.displayName}-${item.containerKey}-${idx}`} key={`${item.displayName}-${item.containerKey}-${idx}`}
packageName={item.getAssociatedPackage()?.displayName.replace('@discordjs/', '')} packageName={item.getAssociatedPackage()?.displayName.replace('@discordjs/', '')}
> >
{text} {token.text}
</ItemLink> </ItemLink>
); );
} }
return text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, ''); return token.text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, '');
})} })}
</span> </span>
); );

View File

@@ -127,7 +127,7 @@ export function TableOfContentItems({ serializedMembers }: TableOfContentsItemPr
<div className="flex flex-row place-items-center gap-4"> <div className="flex flex-row place-items-center gap-4">
<VscSymbolEvent size={20} /> <VscSymbolEvent size={20} />
<div className="p-3 pl-0"> <div className="p-3 pl-0">
<span className="font-semibold">Properties</span> <span className="font-semibold">Events</span>
</div> </div>
</div> </div>
{eventItems} {eventItems}

View File

@@ -1,9 +1,13 @@
/* eslint-disable @typescript-eslint/no-loop-func */
// 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 { 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 { ApiDeclaredItem } from '../index.js'; import type { IExcerptToken, IExcerptTokenRange } from '../index.js';
import { ApiDeclaredItem } from '../index.js';
import type { IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js';
import { import {
ApiItem, ApiItem,
apiItem_onParentChanged, apiItem_onParentChanged,
@@ -15,11 +19,12 @@ import {
import type { ApiClass } from '../model/ApiClass.js'; import type { ApiClass } from '../model/ApiClass.js';
import type { ApiInterface } from '../model/ApiInterface.js'; import type { ApiInterface } from '../model/ApiInterface.js';
import type { ApiModel } from '../model/ApiModel.js'; import type { ApiModel } from '../model/ApiModel.js';
import type { DeserializerContext } from '../model/DeserializerContext.js'; import { ApiJsonSchemaVersion, type DeserializerContext } from '../model/DeserializerContext.js';
import type { HeritageType } from '../model/HeritageType.js'; import type { HeritageType } from '../model/HeritageType.js';
import type { IResolveDeclarationReferenceResult } from '../model/ModelReferenceResolver.js'; import type { IResolveDeclarationReferenceResult } from '../model/ModelReferenceResolver.js';
import { ApiNameMixin } from './ApiNameMixin.js'; import { ApiNameMixin } from './ApiNameMixin.js';
import { type ExcerptToken, ExcerptTokenKind } from './Excerpt.js'; import type { ExcerptToken } from './Excerpt.js';
import { ExcerptTokenKind } from './Excerpt.js';
import { type IFindApiItemsResult, type IFindApiItemsMessage, FindApiItemsMessageId } from './IFindApiItemsResult.js'; import { type IFindApiItemsResult, type IFindApiItemsMessage, FindApiItemsMessageId } from './IFindApiItemsResult.js';
/** /**
@@ -37,9 +42,13 @@ export interface IApiItemContainerJson extends IApiItemJson {
preserveMemberOrder?: boolean; preserveMemberOrder?: boolean;
} }
interface ExcerptTokenRangeInDeclaredItem {
item: ApiDeclaredItem;
range: IExcerptTokenRange;
}
interface IMappedTypeParameters { interface IMappedTypeParameters {
item: ApiItem; item: ApiItem;
mappedTypeParameters: Map<string, string>; mappedTypeParameters: Map<string, ExcerptTokenRangeInDeclaredItem>;
} }
const _members: unique symbol = Symbol('ApiItemContainerMixin._members'); const _members: unique symbol = Symbol('ApiItemContainerMixin._members');
@@ -317,7 +326,7 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
let next: IMappedTypeParameters | undefined = { item: this, mappedTypeParameters: new Map() }; let next: IMappedTypeParameters | undefined = { item: this, mappedTypeParameters: new Map() };
while (next?.item) { while (next?.item) {
const membersToAdd: ApiItem[] = []; /* const membersToAdd: ApiItem[] = []; //*
const typeParams = next.mappedTypeParameters; const typeParams = next.mappedTypeParameters;
const context: DeserializerContext = { const context: DeserializerContext = {
apiJsonFilename: '', apiJsonFilename: '',
@@ -325,56 +334,46 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
toolVersion: '', toolVersion: '',
versionToDeserialize: ApiJsonSchemaVersion.LATEST, versionToDeserialize: ApiJsonSchemaVersion.LATEST,
tsdocConfiguration: new TSDocConfiguration(), tsdocConfiguration: new TSDocConfiguration(),
}; */ }; // */
// eslint-disable-next-line @typescript-eslint/consistent-type-imports, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const deserializerModule: typeof import('../model/Deserializer') = require('../model/Deserializer');
// For each member, check to see if we've already seen a member with the same name // For each member, check to see if we've already seen a member with the same name
// previously in the inheritance tree. If so, we know we won't inherit it, and thus // previously in the inheritance tree. If so, we know we won't inherit it, and thus
// do not add it to our `membersToAdd` array. // do not add it to our `membersToAdd` array.
for (const member of next.item.members) { for (let member of next.item.members) {
// We add the to-be-added members to an intermediate array instead of immediately // We add the to-be-added members to an intermediate array instead of immediately
// to the maps themselves to support method overloads with the same name. // to the maps themselves to support method overloads with the same name.
if (ApiNameMixin.isBaseClassOf(member)) {
if (!membersByName.has(member.name)) { // This was supposed to replace type parameters with their assigned values in inheritance, but doesn't work yet
// This was supposed to replace type parameters with their assigned values in inheritance, but doesn't work yet //*
/* if (member instanceof ApiDeclaredItem && member.excerptTokens.some((token) => typeParams.has(token.text))) {
if ( const jsonObject: Partial<IApiItemJson> = {};
ApiTypeParameterListMixin.isBaseClassOf(member) && member.serializeInto(jsonObject);
member.typeParameters.some((param) => typeParams.has(param.name)) const excerptTokens = (jsonObject as IApiDeclaredItemJson).excerptTokens.map((token) => {
) { let x: ExcerptToken | undefined;
const jsonObject: Partial<IApiItemJson> = {}; if (typeParams.has(token.text) && next?.item instanceof ApiDeclaredItem) {
member.serializeInto(jsonObject); const originalValue = typeParams.get(token.text)!;
member = deserializerModule.Deserializer.deserialize(context, { x = originalValue.item.excerptTokens[originalValue.range.startIndex];
...jsonObject,
typeParameters: (jsonObject as IApiTypeParameterListMixinJson).typeParameters.map(
({ typeParameterName, constraintTokenRange, defaultTypeTokenRange }) => ({
typeParameterName: typeParams.get(typeParameterName) ?? typeParameterName,
defaultTypeTokenRange,
constraintTokenRange,
}),
),
} as IApiTypeParameterListMixinJson);
} }
if (ApiReturnTypeMixin.isBaseClassOf(member)) { const excerptToken: IExcerptToken = x ? { kind: x.kind, text: x.text } : token;
const jsonObject: Partial<IApiItemJson> = {}; if (x?.canonicalReference !== undefined) {
member.serializeInto(jsonObject); excerptToken.canonicalReference = x.canonicalReference.toString();
member = deserializerModule.Deserializer.deserialize(context, { }
...(jsonObject as IApiReturnTypeMixinJson),
excerptTokens: (jsonObject as IApiDeclaredItemJson).excerptTokens.map((token) =>
token.kind === ExcerptTokenKind.Content
? {
kind: ExcerptTokenKind.Content,
text: [...typeParams.keys()].reduce(
(tok, typ) => tok.replaceAll(new RegExp(`\b${typ}\b`, 'g'), typeParams.get(typ)!),
token.text,
),
}
: token,
),
} as IApiReturnTypeMixinJson);
member[apiItem_onParentChanged](next.item);
} // */
return excerptToken;
});
member = deserializerModule.Deserializer.deserialize(context, {
...jsonObject,
excerptTokens,
} as IApiDeclaredItemJson);
member[apiItem_onParentChanged](next.item);
}
if (ApiNameMixin.isBaseClassOf(member)) {
if (!membersByName.has(member.name)) {
membersToAdd.push(member); membersToAdd.push(member);
} }
} else if (!membersByKind.has(member.kind)) { } else if (!membersByKind.has(member.kind)) {
@@ -478,14 +477,20 @@ export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
continue; continue;
} }
const mappedTypeParameters: Map<string, string> = new Map(); const mappedTypeParameters: Map<string, ExcerptTokenRangeInDeclaredItem> = new Map();
if ( if (
(apiItem.kind === ApiItemKind.Class || apiItem.kind === ApiItemKind.Interface) && (apiItem.kind === ApiItemKind.Class || apiItem.kind === ApiItemKind.Interface) &&
next.item.kind === ApiItemKind.Class next.item.kind === ApiItemKind.Class
) { ) {
for (const [index, typeParameter] of extendsType.typeParameters?.entries() ?? []) { for (const [index, key] of (apiItem as ApiClass | ApiInterface).typeParameters.entries() ?? []) {
const key = (apiItem as ApiClass | ApiInterface).typeParameters[index]?.name ?? ''; const typeParameter = extendsType.typeParameters?.[index];
mappedTypeParameters.set(key, typeParameter); if (typeParameter)
mappedTypeParameters.set(key.name, { item: next.item as ApiDeclaredItem, range: typeParameter });
else if (key.defaultTypeExcerpt)
mappedTypeParameters.set(key.name, {
item: apiItem as ApiDeclaredItem,
range: key.defaultTypeExcerpt.tokenRange,
});
} }
} }

View File

@@ -44,7 +44,7 @@ export interface IApiClassOptions
} }
export interface IExcerptTokenRangeWithTypeParameters extends IExcerptTokenRange { export interface IExcerptTokenRangeWithTypeParameters extends IExcerptTokenRange {
typeParameters: string[]; typeParameters: IExcerptTokenRange[];
} }
export interface IApiClassJson export interface IApiClassJson

View File

@@ -91,13 +91,18 @@ export enum ApiJsonSchemaVersion {
*/ */
V_1011 = 1_011, V_1011 = 1_011,
/**
* Add a `fileLine`and `fileColumn` field to track source code location
*/
V_1012 = 1_012,
/** /**
* The current latest .api.json schema version. * The current latest .api.json schema version.
* *
* IMPORTANT: When incrementing this number, consider whether `OLDEST_SUPPORTED` or `OLDEST_FORWARDS_COMPATIBLE` * IMPORTANT: When incrementing this number, consider whether `OLDEST_SUPPORTED` or `OLDEST_FORWARDS_COMPATIBLE`
* should be updated. * should be updated.
*/ */
LATEST = V_1011, LATEST = V_1012,
/** /**
* The oldest .api.json schema version that is still supported for backwards compatibility. * The oldest .api.json schema version that is still supported for backwards compatibility.

View File

@@ -1,7 +1,7 @@
// 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 type { Excerpt } from '../mixins/Excerpt.js'; import type { Excerpt, IExcerptTokenRange } from '../mixins/Excerpt.js';
/** /**
* Represents a type referenced via an "extends" or "implements" heritage clause for a TypeScript class * Represents a type referenced via an "extends" or "implements" heritage clause for a TypeScript class
@@ -38,9 +38,9 @@ export class HeritageType {
*/ */
public readonly excerpt: Excerpt; public readonly excerpt: Excerpt;
public readonly typeParameters?: string[]; public readonly typeParameters?: IExcerptTokenRange[];
public constructor(excerpt: Excerpt, typeParameters: string[]) { public constructor(excerpt: Excerpt, typeParameters: IExcerptTokenRange[]) {
this.excerpt = excerpt; this.excerpt = excerpt;
this.typeParameters = typeParameters; this.typeParameters = typeParameters;
} }

View File

@@ -538,9 +538,6 @@ export class ApiModelGenerator {
if (apiClass === undefined) { if (apiClass === undefined) {
const classDeclaration: ts.ClassDeclaration = astDeclaration.declaration as ts.ClassDeclaration; const classDeclaration: ts.ClassDeclaration = astDeclaration.declaration as ts.ClassDeclaration;
if (name === 'ActionRow') {
console.dir(classDeclaration.heritageClauses?.[0]?.types[0]?.typeArguments, { depth: 3 });
}
const nodesToCapture: IExcerptBuilderNodeToCapture[] = []; const nodesToCapture: IExcerptBuilderNodeToCapture[] = [];
@@ -557,9 +554,12 @@ export class ApiModelGenerator {
extendsTokenRange = ExcerptBuilder.createEmptyTokenRangeWithTypeParameters(); extendsTokenRange = ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
if (heritageClause.types.length > 0) { if (heritageClause.types.length > 0) {
extendsTokenRange.typeParameters.push( extendsTokenRange.typeParameters.push(
...(heritageClause.types[0]?.typeArguments?.map((typeArgument) => ...(heritageClause.types[0]?.typeArguments?.map((typeArgument) => {
ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '', const typeArgumentTokenRange = ExcerptBuilder.createEmptyTokenRange();
) ?? []), nodesToCapture.push({ node: typeArgument, tokenRange: typeArgumentTokenRange });
return typeArgumentTokenRange;
}) ?? []),
); );
nodesToCapture.push({ node: heritageClause.types[0], tokenRange: extendsTokenRange }); nodesToCapture.push({ node: heritageClause.types[0], tokenRange: extendsTokenRange });
} }
@@ -568,9 +568,14 @@ export class ApiModelGenerator {
const implementsTokenRange: IExcerptTokenRangeWithTypeParameters = const implementsTokenRange: IExcerptTokenRangeWithTypeParameters =
ExcerptBuilder.createEmptyTokenRangeWithTypeParameters(); ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
implementsTokenRange.typeParameters.push( implementsTokenRange.typeParameters.push(
...(heritageClause.types[0]?.typeArguments?.map((typeArgument) => ...(heritageType.typeArguments?.map((typeArgument) => {
ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '', const typeArgumentTokenRange = ExcerptBuilder.createEmptyTokenRange();
) ?? []), if (ts.isTypeReferenceNode(typeArgument)) {
nodesToCapture.push({ node: typeArgument, tokenRange: typeArgumentTokenRange });
}
return typeArgumentTokenRange;
}) ?? []),
); );
implementsTokenRanges.push(implementsTokenRange); implementsTokenRanges.push(implementsTokenRange);
nodesToCapture.push({ node: heritageType, tokenRange: implementsTokenRange }); nodesToCapture.push({ node: heritageType, tokenRange: implementsTokenRange });
@@ -893,9 +898,14 @@ export class ApiModelGenerator {
const extendsTokenRange: IExcerptTokenRangeWithTypeParameters = const extendsTokenRange: IExcerptTokenRangeWithTypeParameters =
ExcerptBuilder.createEmptyTokenRangeWithTypeParameters(); ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
extendsTokenRange.typeParameters.push( extendsTokenRange.typeParameters.push(
...(heritageClause.types[0]?.typeArguments?.map((typeArgument) => ...(heritageType.typeArguments?.map((typeArgument) => {
ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '', const typeArgumentTokenRange = ExcerptBuilder.createEmptyTokenRange();
) ?? []), if (ts.isTypeReferenceNode(typeArgument)) {
nodesToCapture.push({ node: typeArgument, tokenRange: typeArgumentTokenRange });
}
return typeArgumentTokenRange;
}) ?? []),
); );
extendsTokenRanges.push(extendsTokenRange); extendsTokenRanges.push(extendsTokenRange);
nodesToCapture.push({ node: heritageType, tokenRange: extendsTokenRange }); nodesToCapture.push({ node: heritageType, tokenRange: extendsTokenRange });

View File

@@ -176,14 +176,17 @@ export class ExcerptBuilder {
if (span.kind === ts.SyntaxKind.Identifier) { if (span.kind === ts.SyntaxKind.Identifier) {
const name: ts.Identifier = span.node as ts.Identifier; const name: ts.Identifier = span.node as ts.Identifier;
if (!ExcerptBuilder._isDeclarationName(name)) { canonicalReference = state.referenceGenerator.getDeclarationReferenceForIdentifier(name);
canonicalReference = state.referenceGenerator.getDeclarationReferenceForIdentifier(name);
}
} }
if (canonicalReference) { if (canonicalReference) {
ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Reference, span.prefix, canonicalReference); ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Reference, span.prefix, canonicalReference);
} else if (ExcerptBuilder.isPrimitiveKeyword(span.node)) { } else if (
ExcerptBuilder.isPrimitiveKeyword(span.node) ||
(span.node.kind === ts.SyntaxKind.Identifier &&
((ts.isTypeReferenceNode(span.node.parent) && span.node.parent.typeName === span.node) ||
(ts.isTypeParameterDeclaration(span.node.parent) && span.node.parent.name === span.node)))
) {
ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Reference, span.prefix); ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Reference, span.prefix);
} else { } else {
ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Content, span.prefix); ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Content, span.prefix);
@@ -209,7 +212,11 @@ export class ExcerptBuilder {
} }
if (span.separator) { if (span.separator) {
ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Content, span.separator); ExcerptBuilder._appendToken(
excerptTokens,
ExcerptTokenKind.Content,
span.separator.replaceAll('\n', '').replaceAll(/\s{2}/g, ' '),
);
state.lastAppendedTokenIsSeparator = true; state.lastAppendedTokenIsSeparator = true;
} }
@@ -335,7 +342,7 @@ export class ExcerptBuilder {
} }
} }
} }
} } /*
private static _isDeclarationName(name: ts.Identifier): boolean { private static _isDeclarationName(name: ts.Identifier): boolean {
return ExcerptBuilder._isDeclaration(name.parent) && name.parent.name === name; return ExcerptBuilder._isDeclaration(name.parent) && name.parent.name === name;
@@ -366,5 +373,5 @@ export class ExcerptBuilder {
default: default:
return false; return false;
} }
} } // */
} }