From 5a4c9755c3fc3ac2bc88dfcf50468909d99fc2f9 Mon Sep 17 00:00:00 2001
From: Qjuh <76154676+Qjuh@users.noreply.github.com>
Date: Sat, 11 Nov 2023 15:18:08 +0100
Subject: [PATCH] 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
---
apps/website/src/components/ExcerptText.tsx | 28 ++---
.../src/components/TableOfContentItems.tsx | 2 +-
.../src/mixins/ApiItemContainerMixin.ts | 105 +++++++++---------
.../api-extractor-model/src/model/ApiClass.ts | 2 +-
.../src/model/DeserializerContext.ts | 7 +-
.../src/model/HeritageType.ts | 6 +-
.../src/generators/ApiModelGenerator.ts | 34 ++++--
.../src/generators/ExcerptBuilder.ts | 21 ++--
8 files changed, 116 insertions(+), 89 deletions(-)
diff --git a/apps/website/src/components/ExcerptText.tsx b/apps/website/src/components/ExcerptText.tsx
index bd7b89592..68ee9c875 100644
--- a/apps/website/src/components/ExcerptText.tsx
+++ b/apps/website/src/components/ExcerptText.tsx
@@ -24,14 +24,12 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
return (
{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 (text in BuiltinDocumentationLinks) {
- const href = BuiltinDocumentationLinks[text as keyof typeof BuiltinDocumentationLinks];
+ if (token.text in BuiltinDocumentationLinks) {
+ const href = BuiltinDocumentationLinks[token.text as keyof typeof BuiltinDocumentationLinks];
return (
-
- {text}
+
+ {token.text}
);
}
@@ -45,20 +43,22 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
// dapi-types doesn't have routes for class members
// so we can assume this member is for an enum
if (meaning === 'member' && path && 'parent' in path) href += `/enum/${path.parent}#${path.component}`;
- else if (meaning === 'type') href += `#${text}`;
- else href += `/${meaning}/${text}`;
+ else if (meaning === 'type' || meaning === 'var') href += `#${token.text}`;
+ else href += `/${meaning}/${token.text}`;
return (
-
- {text}
+
+ {token.text}
);
}
- const item = model.resolveDeclarationReference(token.canonicalReference!, model).resolvedApiItem;
+ const item = token.canonicalReference
+ ? model.resolveDeclarationReference(token.canonicalReference!, model).resolvedApiItem
+ : null;
if (!item) {
- return text;
+ return token.text;
}
return (
@@ -68,12 +68,12 @@ export function ExcerptText({ model, excerpt }: ExcerptTextProps) {
key={`${item.displayName}-${item.containerKey}-${idx}`}
packageName={item.getAssociatedPackage()?.displayName.replace('@discordjs/', '')}
>
- {text}
+ {token.text}
);
}
- return text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, '');
+ return token.text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, '');
})}
);
diff --git a/apps/website/src/components/TableOfContentItems.tsx b/apps/website/src/components/TableOfContentItems.tsx
index 544b25c78..a1e333160 100644
--- a/apps/website/src/components/TableOfContentItems.tsx
+++ b/apps/website/src/components/TableOfContentItems.tsx
@@ -127,7 +127,7 @@ export function TableOfContentItems({ serializedMembers }: TableOfContentsItemPr
{eventItems}
diff --git a/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts b/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts
index 9da10d3eb..5e50629da 100644
--- a/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts
+++ b/packages/api-extractor-model/src/mixins/ApiItemContainerMixin.ts
@@ -1,9 +1,13 @@
+/* eslint-disable @typescript-eslint/no-loop-func */
// 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 { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
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 {
ApiItem,
apiItem_onParentChanged,
@@ -15,11 +19,12 @@ import {
import type { ApiClass } from '../model/ApiClass.js';
import type { ApiInterface } from '../model/ApiInterface.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 { IResolveDeclarationReferenceResult } from '../model/ModelReferenceResolver.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';
/**
@@ -37,9 +42,13 @@ export interface IApiItemContainerJson extends IApiItemJson {
preserveMemberOrder?: boolean;
}
+interface ExcerptTokenRangeInDeclaredItem {
+ item: ApiDeclaredItem;
+ range: IExcerptTokenRange;
+}
interface IMappedTypeParameters {
item: ApiItem;
- mappedTypeParameters: Map;
+ mappedTypeParameters: Map;
}
const _members: unique symbol = Symbol('ApiItemContainerMixin._members');
@@ -317,7 +326,7 @@ export function ApiItemContainerMixin(
let next: IMappedTypeParameters | undefined = { item: this, mappedTypeParameters: new Map() };
while (next?.item) {
- const membersToAdd: ApiItem[] = []; /*
+ const membersToAdd: ApiItem[] = []; //*
const typeParams = next.mappedTypeParameters;
const context: DeserializerContext = {
apiJsonFilename: '',
@@ -325,56 +334,46 @@ export function ApiItemContainerMixin(
toolVersion: '',
versionToDeserialize: ApiJsonSchemaVersion.LATEST,
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
// previously in the inheritance tree. If so, we know we won't inherit it, and thus
// 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
// 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
- /*
- if (
- ApiTypeParameterListMixin.isBaseClassOf(member) &&
- member.typeParameters.some((param) => typeParams.has(param.name))
- ) {
- const jsonObject: Partial = {};
- member.serializeInto(jsonObject);
- member = deserializerModule.Deserializer.deserialize(context, {
- ...jsonObject,
- typeParameters: (jsonObject as IApiTypeParameterListMixinJson).typeParameters.map(
- ({ typeParameterName, constraintTokenRange, defaultTypeTokenRange }) => ({
- typeParameterName: typeParams.get(typeParameterName) ?? typeParameterName,
- defaultTypeTokenRange,
- constraintTokenRange,
- }),
- ),
- } as IApiTypeParameterListMixinJson);
+
+ // 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))) {
+ const jsonObject: Partial = {};
+ member.serializeInto(jsonObject);
+ const excerptTokens = (jsonObject as IApiDeclaredItemJson).excerptTokens.map((token) => {
+ let x: ExcerptToken | undefined;
+ if (typeParams.has(token.text) && next?.item instanceof ApiDeclaredItem) {
+ const originalValue = typeParams.get(token.text)!;
+ x = originalValue.item.excerptTokens[originalValue.range.startIndex];
}
- if (ApiReturnTypeMixin.isBaseClassOf(member)) {
- const jsonObject: Partial = {};
- member.serializeInto(jsonObject);
- 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);
- } // */
+ const excerptToken: IExcerptToken = x ? { kind: x.kind, text: x.text } : token;
+ if (x?.canonicalReference !== undefined) {
+ excerptToken.canonicalReference = x.canonicalReference.toString();
+ }
+ 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);
}
} else if (!membersByKind.has(member.kind)) {
@@ -478,14 +477,20 @@ export function ApiItemContainerMixin(
continue;
}
- const mappedTypeParameters: Map = new Map();
+ const mappedTypeParameters: Map = new Map();
if (
(apiItem.kind === ApiItemKind.Class || apiItem.kind === ApiItemKind.Interface) &&
next.item.kind === ApiItemKind.Class
) {
- for (const [index, typeParameter] of extendsType.typeParameters?.entries() ?? []) {
- const key = (apiItem as ApiClass | ApiInterface).typeParameters[index]?.name ?? '';
- mappedTypeParameters.set(key, typeParameter);
+ for (const [index, key] of (apiItem as ApiClass | ApiInterface).typeParameters.entries() ?? []) {
+ const typeParameter = extendsType.typeParameters?.[index];
+ 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,
+ });
}
}
diff --git a/packages/api-extractor-model/src/model/ApiClass.ts b/packages/api-extractor-model/src/model/ApiClass.ts
index e65a88bd8..7f8e86ec1 100644
--- a/packages/api-extractor-model/src/model/ApiClass.ts
+++ b/packages/api-extractor-model/src/model/ApiClass.ts
@@ -44,7 +44,7 @@ export interface IApiClassOptions
}
export interface IExcerptTokenRangeWithTypeParameters extends IExcerptTokenRange {
- typeParameters: string[];
+ typeParameters: IExcerptTokenRange[];
}
export interface IApiClassJson
diff --git a/packages/api-extractor-model/src/model/DeserializerContext.ts b/packages/api-extractor-model/src/model/DeserializerContext.ts
index daf4de626..b5778c8f9 100644
--- a/packages/api-extractor-model/src/model/DeserializerContext.ts
+++ b/packages/api-extractor-model/src/model/DeserializerContext.ts
@@ -91,13 +91,18 @@ export enum ApiJsonSchemaVersion {
*/
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.
*
* IMPORTANT: When incrementing this number, consider whether `OLDEST_SUPPORTED` or `OLDEST_FORWARDS_COMPATIBLE`
* should be updated.
*/
- LATEST = V_1011,
+ LATEST = V_1012,
/**
* The oldest .api.json schema version that is still supported for backwards compatibility.
diff --git a/packages/api-extractor-model/src/model/HeritageType.ts b/packages/api-extractor-model/src/model/HeritageType.ts
index 1efe0f649..5332f4a9b 100644
--- a/packages/api-extractor-model/src/model/HeritageType.ts
+++ b/packages/api-extractor-model/src/model/HeritageType.ts
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// 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
@@ -38,9 +38,9 @@ export class HeritageType {
*/
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.typeParameters = typeParameters;
}
diff --git a/packages/api-extractor/src/generators/ApiModelGenerator.ts b/packages/api-extractor/src/generators/ApiModelGenerator.ts
index 94a8f7aa7..dc2d0b012 100644
--- a/packages/api-extractor/src/generators/ApiModelGenerator.ts
+++ b/packages/api-extractor/src/generators/ApiModelGenerator.ts
@@ -538,9 +538,6 @@ export class ApiModelGenerator {
if (apiClass === undefined) {
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[] = [];
@@ -557,9 +554,12 @@ export class ApiModelGenerator {
extendsTokenRange = ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
if (heritageClause.types.length > 0) {
extendsTokenRange.typeParameters.push(
- ...(heritageClause.types[0]?.typeArguments?.map((typeArgument) =>
- ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '',
- ) ?? []),
+ ...(heritageClause.types[0]?.typeArguments?.map((typeArgument) => {
+ const typeArgumentTokenRange = ExcerptBuilder.createEmptyTokenRange();
+ nodesToCapture.push({ node: typeArgument, tokenRange: typeArgumentTokenRange });
+
+ return typeArgumentTokenRange;
+ }) ?? []),
);
nodesToCapture.push({ node: heritageClause.types[0], tokenRange: extendsTokenRange });
}
@@ -568,9 +568,14 @@ export class ApiModelGenerator {
const implementsTokenRange: IExcerptTokenRangeWithTypeParameters =
ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
implementsTokenRange.typeParameters.push(
- ...(heritageClause.types[0]?.typeArguments?.map((typeArgument) =>
- ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '',
- ) ?? []),
+ ...(heritageType.typeArguments?.map((typeArgument) => {
+ const typeArgumentTokenRange = ExcerptBuilder.createEmptyTokenRange();
+ if (ts.isTypeReferenceNode(typeArgument)) {
+ nodesToCapture.push({ node: typeArgument, tokenRange: typeArgumentTokenRange });
+ }
+
+ return typeArgumentTokenRange;
+ }) ?? []),
);
implementsTokenRanges.push(implementsTokenRange);
nodesToCapture.push({ node: heritageType, tokenRange: implementsTokenRange });
@@ -893,9 +898,14 @@ export class ApiModelGenerator {
const extendsTokenRange: IExcerptTokenRangeWithTypeParameters =
ExcerptBuilder.createEmptyTokenRangeWithTypeParameters();
extendsTokenRange.typeParameters.push(
- ...(heritageClause.types[0]?.typeArguments?.map((typeArgument) =>
- ts.isTypeReferenceNode(typeArgument) ? typeArgument.typeName.getText() : '',
- ) ?? []),
+ ...(heritageType.typeArguments?.map((typeArgument) => {
+ const typeArgumentTokenRange = ExcerptBuilder.createEmptyTokenRange();
+ if (ts.isTypeReferenceNode(typeArgument)) {
+ nodesToCapture.push({ node: typeArgument, tokenRange: typeArgumentTokenRange });
+ }
+
+ return typeArgumentTokenRange;
+ }) ?? []),
);
extendsTokenRanges.push(extendsTokenRange);
nodesToCapture.push({ node: heritageType, tokenRange: extendsTokenRange });
diff --git a/packages/api-extractor/src/generators/ExcerptBuilder.ts b/packages/api-extractor/src/generators/ExcerptBuilder.ts
index 39ea83bf5..da6b0fd69 100644
--- a/packages/api-extractor/src/generators/ExcerptBuilder.ts
+++ b/packages/api-extractor/src/generators/ExcerptBuilder.ts
@@ -176,14 +176,17 @@ export class ExcerptBuilder {
if (span.kind === ts.SyntaxKind.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) {
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);
} else {
ExcerptBuilder._appendToken(excerptTokens, ExcerptTokenKind.Content, span.prefix);
@@ -209,7 +212,11 @@ export class ExcerptBuilder {
}
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;
}
@@ -335,7 +342,7 @@ export class ExcerptBuilder {
}
}
}
- }
+ } /*
private static _isDeclarationName(name: ts.Identifier): boolean {
return ExcerptBuilder._isDeclaration(name.parent) && name.parent.name === name;
@@ -366,5 +373,5 @@ export class ExcerptBuilder {
default:
return false;
}
- }
+ } // */
}