mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-14 18:43:31 +01:00
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:
294
packages/api-extractor/src/enhancers/ValidationEnhancer.ts
Normal file
294
packages/api-extractor/src/enhancers/ValidationEnhancer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user