build: package api-extractor and -model (#9920)

* fix(ExceptText): don't display import("d..-types/v10"). in return type

* Squashed 'packages/api-extractor-model/' content from commit 39ecb196c

git-subtree-dir: packages/api-extractor-model
git-subtree-split: 39ecb196ca210bdf84ba6c9cadb1bb93571849d7

* Squashed 'packages/api-extractor/' content from commit 341ad6c51

git-subtree-dir: packages/api-extractor
git-subtree-split: 341ad6c51b01656d4f73b74ad4bdb3095f9262c4

* feat(api-extractor): add api-extractor and -model

* fix: package.json docs script

* fix(SourcLink): use <> instead of function syntax

* fix: make packages private

* fix: rest params showing in docs, added labels

* fix: missed two files

* fix: cpy-cli & pnpm-lock

* fix: increase icon size

* fix: icon size again
This commit is contained in:
Qjuh
2023-11-07 21:53:36 +01:00
committed by GitHub
parent 95c0b1a59f
commit 5c0fad3b2d
251 changed files with 36017 additions and 296 deletions

View File

@@ -0,0 +1,251 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { ReleaseTag } from '@discordjs/api-extractor-model';
import * as tsdoc from '@microsoft/tsdoc';
import * as ts from 'typescript';
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
import { ResolverFailure } from '../analyzer/AstReferenceResolver.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js';
import type { Collector } from '../collector/Collector.js';
import { VisitorState } from '../collector/VisitorState.js';
export class DocCommentEnhancer {
private readonly _collector: Collector;
public constructor(collector: Collector) {
this._collector = collector;
}
public static analyze(collector: Collector): void {
const docCommentEnhancer: DocCommentEnhancer = new DocCommentEnhancer(collector);
docCommentEnhancer.analyze();
}
public analyze(): void {
for (const entity of this._collector.entities) {
if (
entity.astEntity instanceof AstSymbol &&
(entity.consumable ||
this._collector.extractorConfig.apiReportIncludeForgottenExports ||
this._collector.extractorConfig.docModelIncludeForgottenExports)
) {
entity.astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
this._analyzeApiItem(astDeclaration);
});
}
}
}
private _analyzeApiItem(astDeclaration: AstDeclaration): void {
const metadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
if (metadata.docCommentEnhancerVisitorState === VisitorState.Visited) {
return;
}
if (metadata.docCommentEnhancerVisitorState === VisitorState.Visiting) {
this._collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.CyclicInheritDoc,
`The @inheritDoc tag for "${astDeclaration.astSymbol.localName}" refers to its own declaration`,
astDeclaration,
);
return;
}
metadata.docCommentEnhancerVisitorState = VisitorState.Visiting;
if (metadata.tsdocComment?.inheritDocTag) {
this._applyInheritDoc(astDeclaration, metadata.tsdocComment, metadata.tsdocComment.inheritDocTag);
}
this._analyzeNeedsDocumentation(astDeclaration, metadata);
this._checkForBrokenLinks(astDeclaration, metadata);
metadata.docCommentEnhancerVisitorState = VisitorState.Visited;
}
private _analyzeNeedsDocumentation(astDeclaration: AstDeclaration, metadata: ApiItemMetadata): void {
if (astDeclaration.declaration.kind === ts.SyntaxKind.Constructor) {
// Constructors always do pretty much the same thing, so it's annoying to require people to write
// descriptions for them. Instead, if the constructor lacks a TSDoc summary, then API Extractor
// will auto-generate one.
metadata.undocumented = false;
// The class that contains this constructor
const classDeclaration: AstDeclaration = astDeclaration.parent!;
const configuration: tsdoc.TSDocConfiguration = this._collector.extractorConfig.tsdocConfiguration;
if (!metadata.tsdocComment) {
metadata.tsdocComment = new tsdoc.DocComment({ configuration });
}
if (!tsdoc.PlainTextEmitter.hasAnyTextContent(metadata.tsdocComment.summarySection)) {
metadata.tsdocComment.summarySection.appendNodesInParagraph([
new tsdoc.DocPlainText({ configuration, text: 'Constructs a new instance of the ' }),
new tsdoc.DocCodeSpan({
configuration,
code: classDeclaration.astSymbol.localName,
}),
new tsdoc.DocPlainText({ configuration, text: ' class' }),
]);
}
const apiItemMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(astDeclaration);
if (apiItemMetadata.effectiveReleaseTag === ReleaseTag.Internal) {
// If the constructor is marked as internal, then add a boilerplate notice for the containing class
const classMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(classDeclaration);
if (!classMetadata.tsdocComment) {
classMetadata.tsdocComment = new tsdoc.DocComment({ configuration });
}
if (classMetadata.tsdocComment.remarksBlock === undefined) {
classMetadata.tsdocComment.remarksBlock = new tsdoc.DocBlock({
configuration,
blockTag: new tsdoc.DocBlockTag({
configuration,
tagName: tsdoc.StandardTags.remarks.tagName,
}),
});
}
classMetadata.tsdocComment.remarksBlock.content.appendNode(
new tsdoc.DocParagraph({ configuration }, [
new tsdoc.DocPlainText({
configuration,
text:
`The constructor for this class is marked as internal. Third-party code should not` +
` call the constructor directly or create subclasses that extend the `,
}),
new tsdoc.DocCodeSpan({
configuration,
code: classDeclaration.astSymbol.localName,
}),
new tsdoc.DocPlainText({ configuration, text: ' class.' }),
]),
);
}
return;
}
if (metadata.tsdocComment) {
// Require the summary to contain at least 10 non-spacing characters
metadata.undocumented = !tsdoc.PlainTextEmitter.hasAnyTextContent(metadata.tsdocComment.summarySection, 10);
} else {
metadata.undocumented = true;
}
}
private _checkForBrokenLinks(astDeclaration: AstDeclaration, metadata: ApiItemMetadata): void {
if (!metadata.tsdocComment) {
return;
}
this._checkForBrokenLinksRecursive(astDeclaration, metadata.tsdocComment);
}
private _checkForBrokenLinksRecursive(astDeclaration: AstDeclaration, node: tsdoc.DocNode): void {
if (
node instanceof tsdoc.DocLinkTag &&
node.codeDestination && // Is it referring to the working package? If not, we don't do any link validation, because
// AstReferenceResolver doesn't support it yet (but ModelReferenceResolver does of course).
// Tracked by: https://github.com/microsoft/rushstack/issues/1195
(node.codeDestination.packageName === undefined ||
node.codeDestination.packageName === this._collector.workingPackage.name)
) {
const referencedAstDeclaration: AstDeclaration | ResolverFailure = this._collector.astReferenceResolver.resolve(
node.codeDestination,
);
if (referencedAstDeclaration instanceof ResolverFailure) {
this._collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.UnresolvedLink,
'The @link reference could not be resolved: ' + referencedAstDeclaration.reason,
astDeclaration,
);
}
}
for (const childNode of node.getChildNodes()) {
this._checkForBrokenLinksRecursive(astDeclaration, childNode);
}
}
/**
* Follow an `{@inheritDoc ___}` reference and copy the content that we find in the referenced comment.
*/
private _applyInheritDoc(
astDeclaration: AstDeclaration,
docComment: tsdoc.DocComment,
inheritDocTag: tsdoc.DocInheritDocTag,
): void {
if (!inheritDocTag.declarationReference) {
this._collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.UnresolvedInheritDocBase,
'The @inheritDoc tag needs a TSDoc declaration reference; signature matching is not supported yet',
astDeclaration,
);
return;
}
// Is it referring to the working package?
if (
!(
inheritDocTag.declarationReference.packageName === undefined ||
inheritDocTag.declarationReference.packageName === this._collector.workingPackage.name
)
) {
// It's referencing an external package, so skip this inheritDoc tag, since AstReferenceResolver doesn't
// support it yet. As a workaround, this tag will get handled later by api-documenter.
// Tracked by: https://github.com/microsoft/rushstack/issues/1195
return;
}
const referencedAstDeclaration: AstDeclaration | ResolverFailure = this._collector.astReferenceResolver.resolve(
inheritDocTag.declarationReference,
);
if (referencedAstDeclaration instanceof ResolverFailure) {
this._collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.UnresolvedInheritDocReference,
'The @inheritDoc reference could not be resolved: ' + referencedAstDeclaration.reason,
astDeclaration,
);
return;
}
this._analyzeApiItem(referencedAstDeclaration);
const referencedMetadata: ApiItemMetadata = this._collector.fetchApiItemMetadata(referencedAstDeclaration);
if (referencedMetadata.tsdocComment) {
this._copyInheritedDocs(docComment, referencedMetadata.tsdocComment);
}
}
/**
* Copy the content from `sourceDocComment` to `targetDocComment`.
*/
private _copyInheritedDocs(targetDocComment: tsdoc.DocComment, sourceDocComment: tsdoc.DocComment): void {
targetDocComment.summarySection = sourceDocComment.summarySection;
targetDocComment.remarksBlock = sourceDocComment.remarksBlock;
targetDocComment.params.clear();
for (const param of sourceDocComment.params) {
targetDocComment.params.add(param);
}
for (const typeParam of sourceDocComment.typeParams) {
targetDocComment.typeParams.add(typeParam);
}
targetDocComment.returnsBlock = sourceDocComment.returnsBlock;
targetDocComment.inheritDocTag = undefined;
}
}

View File

@@ -0,0 +1,294 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { ReleaseTag, releaseTagCompare, releaseTagGetTagName } from '@discordjs/api-extractor-model';
import * as ts from 'typescript';
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
import type { AstEntity } from '../analyzer/AstEntity.js';
import type { AstModuleExportInfo } from '../analyzer/AstModule.js';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
import { AstSymbol } from '../analyzer/AstSymbol.js';
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
import type { ApiItemMetadata } from '../collector/ApiItemMetadata.js';
import type { Collector } from '../collector/Collector.js';
import type { CollectorEntity } from '../collector/CollectorEntity.js';
import type { SymbolMetadata } from '../collector/SymbolMetadata.js';
export class ValidationEnhancer {
public static analyze(collector: Collector): void {
const alreadyWarnedEntities: Set<AstEntity> = new Set<AstEntity>();
for (const entity of collector.entities) {
if (
!(
entity.consumable ||
collector.extractorConfig.apiReportIncludeForgottenExports ||
collector.extractorConfig.docModelIncludeForgottenExports
)
) {
continue;
}
if (entity.astEntity instanceof AstSymbol) {
// A regular exported AstSymbol
const astSymbol: AstSymbol = entity.astEntity;
astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
});
const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
ValidationEnhancer._checkForInternalUnderscore(collector, entity, astSymbol, symbolMetadata);
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
} else if (entity.astEntity instanceof AstNamespaceImport) {
// A namespace created using "import * as ___ from ___"
const astNamespaceImport: AstNamespaceImport = entity.astEntity;
const astModuleExportInfo: AstModuleExportInfo = astNamespaceImport.fetchAstModuleExportInfo(collector);
for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) {
if (namespaceMemberAstEntity instanceof AstSymbol) {
const astSymbol: AstSymbol = namespaceMemberAstEntity;
astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
});
const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
// (Don't apply ValidationEnhancer._checkForInternalUnderscore() for AstNamespaceImport members)
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
}
}
}
}
}
private static _checkForInternalUnderscore(
collector: Collector,
collectorEntity: CollectorEntity,
astSymbol: AstSymbol,
symbolMetadata: SymbolMetadata,
): void {
let needsUnderscore = false;
if (symbolMetadata.maxEffectiveReleaseTag === ReleaseTag.Internal) {
if (astSymbol.parentAstSymbol) {
// If it's marked as @internal and the parent isn't obviously already @internal, then it needs an underscore.
//
// For example, we WOULD need an underscore for a merged declaration like this:
//
// /** @internal */
// export namespace X {
// export interface _Y { }
// }
//
// /** @public */
// export class X {
// /** @internal */
// public static _Y(): void { } // <==== different from parent
// }
const parentSymbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
if (parentSymbolMetadata.maxEffectiveReleaseTag > ReleaseTag.Internal) {
needsUnderscore = true;
}
} else {
// If it's marked as @internal and has no parent, then it needs an underscore.
// We use maxEffectiveReleaseTag because a merged declaration would NOT need an underscore in a case like this:
//
// /** @public */
// export enum X { }
//
// /** @internal */
// export namespace X { }
//
// (The above normally reports an error "ae-different-release-tags", but that may be suppressed.)
needsUnderscore = true;
}
}
if (needsUnderscore) {
for (const exportName of collectorEntity.exportNames) {
if (!exportName.startsWith('_')) {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.InternalMissingUnderscore,
`The name "${exportName}" should be prefixed with an underscore` +
` because the declaration is marked as @internal`,
astSymbol,
{ exportName },
);
}
}
}
}
private static _checkForInconsistentReleaseTags(
collector: Collector,
astSymbol: AstSymbol,
symbolMetadata: SymbolMetadata,
): void {
if (astSymbol.isExternal) {
// For now, don't report errors for external code. If the developer cares about it, they should run
// API Extractor separately on the external project
return;
}
// Normally we will expect all release tags to be the same. Arbitrarily we choose the maxEffectiveReleaseTag
// as the thing they should all match.
const expectedEffectiveReleaseTag: ReleaseTag = symbolMetadata.maxEffectiveReleaseTag;
// This is set to true if we find a declaration whose release tag is different from expectedEffectiveReleaseTag
let mixedReleaseTags = false;
// This is set to false if we find a declaration that is not a function/method overload
let onlyFunctionOverloads = true;
// This is set to true if we find a declaration that is @internal
let anyInternalReleaseTags = false;
for (const astDeclaration of astSymbol.astDeclarations) {
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
const effectiveReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
switch (astDeclaration.declaration.kind) {
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.MethodDeclaration:
break;
default:
onlyFunctionOverloads = false;
}
if (effectiveReleaseTag !== expectedEffectiveReleaseTag) {
mixedReleaseTags = true;
}
if (effectiveReleaseTag === ReleaseTag.Internal) {
anyInternalReleaseTags = true;
}
}
if (mixedReleaseTags) {
if (!onlyFunctionOverloads) {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.DifferentReleaseTags,
'This symbol has another declaration with a different release tag',
astSymbol,
);
}
if (anyInternalReleaseTags) {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.InternalMixedReleaseTag,
`Mixed release tags are not allowed for "${astSymbol.localName}" because one of its declarations` +
` is marked as @internal`,
astSymbol,
);
}
}
}
private static _checkReferences(
collector: Collector,
astDeclaration: AstDeclaration,
alreadyWarnedEntities: Set<AstEntity>,
): void {
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
const declarationReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
for (const referencedEntity of astDeclaration.referencedAstEntities) {
let collectorEntity: CollectorEntity | undefined;
let referencedReleaseTag: ReleaseTag;
let localName: string;
if (referencedEntity instanceof AstSymbol) {
// If this is e.g. a member of a namespace, then we need to be checking the top-level scope to see
// whether it's exported.
//
// Technically we should also check each of the nested scopes along the way.
const rootSymbol: AstSymbol = referencedEntity.rootAstSymbol;
if (rootSymbol.isExternal) {
continue;
}
collectorEntity = collector.tryGetCollectorEntity(rootSymbol);
localName = collectorEntity?.nameForEmit ?? rootSymbol.localName;
const referencedMetadata: SymbolMetadata = collector.fetchSymbolMetadata(referencedEntity);
referencedReleaseTag = referencedMetadata.maxEffectiveReleaseTag;
} else if (referencedEntity instanceof AstNamespaceImport) {
collectorEntity = collector.tryGetCollectorEntity(referencedEntity);
referencedReleaseTag = ReleaseTag.Public;
localName = collectorEntity?.nameForEmit ?? referencedEntity.localName;
} else {
continue;
}
if (collectorEntity && collectorEntity.consumable) {
if (releaseTagCompare(declarationReleaseTag, referencedReleaseTag) > 0) {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.IncompatibleReleaseTags,
`The symbol "${astDeclaration.astSymbol.localName}"` +
` is marked as ${releaseTagGetTagName(declarationReleaseTag)},` +
` but its signature references "${localName}"` +
` which is marked as ${releaseTagGetTagName(referencedReleaseTag)}`,
astDeclaration,
);
}
} else {
const entryPointFilename: string = path.basename(collector.workingPackage.entryPointSourceFile.fileName);
if (!alreadyWarnedEntities.has(referencedEntity)) {
alreadyWarnedEntities.add(referencedEntity);
if (referencedEntity instanceof AstSymbol && ValidationEnhancer._isEcmaScriptSymbol(referencedEntity)) {
// The main usage scenario for ECMAScript symbols is to attach private data to a JavaScript object,
// so as a special case, we do NOT report them as forgotten exports.
} else {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.ForgottenExport,
`The symbol "${localName}" needs to be exported by the entry point ${entryPointFilename}`,
astDeclaration,
);
}
}
}
}
}
// Detect an AstSymbol that refers to an ECMAScript symbol declaration such as:
//
// const mySymbol: unique symbol = Symbol('mySymbol');
private static _isEcmaScriptSymbol(astSymbol: AstSymbol): boolean {
if (astSymbol.astDeclarations.length !== 1) {
return false;
}
// We are matching a form like this:
//
// - VariableDeclaration:
// - Identifier: pre=[mySymbol]
// - ColonToken: pre=[:] sep=[ ]
// - TypeOperator:
// - UniqueKeyword: pre=[unique] sep=[ ]
// - SymbolKeyword: pre=[symbol]
const astDeclaration: AstDeclaration = astSymbol.astDeclarations[0]!;
if (ts.isVariableDeclaration(astDeclaration.declaration)) {
const variableTypeNode: ts.TypeNode | undefined = astDeclaration.declaration.type;
if (variableTypeNode) {
for (const token of variableTypeNode.getChildren()) {
if (token.kind === ts.SyntaxKind.SymbolKeyword) {
return true;
}
}
}
}
return false;
}
}