Files
discord.js/packages/api-extractor/src/analyzer/AstReferenceResolver.ts
Qjuh 5c0fad3b2d 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
2023-11-07 21:53:36 +01:00

297 lines
9.9 KiB
TypeScript

// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as tsdoc from '@microsoft/tsdoc';
import * as ts from 'typescript';
import type { Collector } from '../collector/Collector.js';
import type { DeclarationMetadata } from '../collector/DeclarationMetadata.js';
import type { WorkingPackage } from '../collector/WorkingPackage.js';
import type { AstDeclaration } from './AstDeclaration.js';
import type { AstEntity } from './AstEntity.js';
import type { AstModule } from './AstModule.js';
import { AstSymbol } from './AstSymbol.js';
import type { AstSymbolTable } from './AstSymbolTable.js';
/**
* Used by `AstReferenceResolver` to report a failed resolution.
*
* @privateRemarks
* This class is similar to an `Error` object, but the intent of `ResolverFailure` is to describe
* why a reference could not be resolved. This information could be used to throw an actual `Error` object,
* but normally it is handed off to the `MessageRouter` instead.
*/
export class ResolverFailure {
/**
* Details about why the failure occurred.
*/
public readonly reason: string;
public constructor(reason: string) {
this.reason = reason;
}
}
/**
* This resolves a TSDoc declaration reference by walking the `AstSymbolTable` compiler state.
*
* @remarks
*
* This class is analogous to `ModelReferenceResolver` from the `@microsoft/api-extractor-model` project,
* which resolves declaration references by walking the hierarchy loaded from an .api.json file.
*/
export class AstReferenceResolver {
private readonly _collector: Collector;
private readonly _astSymbolTable: AstSymbolTable;
private readonly _workingPackage: WorkingPackage;
public constructor(collector: Collector) {
this._collector = collector;
this._astSymbolTable = collector.astSymbolTable;
this._workingPackage = collector.workingPackage;
}
public resolve(declarationReference: tsdoc.DocDeclarationReference): AstDeclaration | ResolverFailure {
// Is it referring to the working package?
if (
declarationReference.packageName !== undefined &&
declarationReference.packageName !== this._workingPackage.name
) {
return new ResolverFailure('External package references are not supported');
}
// Is it a path-based import?
if (declarationReference.importPath) {
return new ResolverFailure('Import paths are not supported');
}
const astModule: AstModule = this._astSymbolTable.fetchAstModuleFromWorkingPackage(
this._workingPackage.entryPointSourceFile,
);
if (declarationReference.memberReferences.length === 0) {
return new ResolverFailure('Package references are not supported');
}
const rootMemberReference: tsdoc.DocMemberReference = declarationReference.memberReferences[0]!;
const exportName: ResolverFailure | string = this._getMemberReferenceIdentifier(rootMemberReference);
if (exportName instanceof ResolverFailure) {
return exportName;
}
const rootAstEntity: AstEntity | undefined = this._astSymbolTable.tryGetExportOfAstModule(exportName, astModule);
if (rootAstEntity === undefined) {
return new ResolverFailure(`The package "${this._workingPackage.name}" does not have an export "${exportName}"`);
}
if (!(rootAstEntity instanceof AstSymbol)) {
return new ResolverFailure('This type of declaration is not supported yet by the resolver');
}
let currentDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration(
rootAstEntity.astDeclarations,
rootMemberReference,
rootAstEntity.localName,
);
if (currentDeclaration instanceof ResolverFailure) {
return currentDeclaration;
}
for (let index = 1; index < declarationReference.memberReferences.length; ++index) {
const memberReference: tsdoc.DocMemberReference = declarationReference.memberReferences[index]!;
const memberName: ResolverFailure | string = this._getMemberReferenceIdentifier(memberReference);
if (memberName instanceof ResolverFailure) {
return memberName;
}
const matchingChildren: readonly AstDeclaration[] = currentDeclaration.findChildrenWithName(memberName);
if (matchingChildren.length === 0) {
return new ResolverFailure(`No member was found with name "${memberName}"`);
}
const selectedDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration(
matchingChildren,
memberReference,
memberName,
);
if (selectedDeclaration instanceof ResolverFailure) {
return selectedDeclaration;
}
currentDeclaration = selectedDeclaration;
}
return currentDeclaration;
}
private _getMemberReferenceIdentifier(memberReference: tsdoc.DocMemberReference): ResolverFailure | string {
if (memberReference.memberSymbol !== undefined) {
return new ResolverFailure('ECMAScript symbol selectors are not supported');
}
if (memberReference.memberIdentifier === undefined) {
return new ResolverFailure('The member identifier is missing in the root member reference');
}
return memberReference.memberIdentifier.identifier;
}
private _selectDeclaration(
astDeclarations: readonly AstDeclaration[],
memberReference: tsdoc.DocMemberReference,
astSymbolName: string,
): AstDeclaration | ResolverFailure {
const memberSelector: tsdoc.DocMemberSelector | undefined = memberReference.selector;
if (memberSelector === undefined) {
if (astDeclarations.length === 1) {
return astDeclarations[0]!;
} else {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(astDeclarations);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`The reference is ambiguous because "${astSymbolName}"` +
` has more than one declaration; you need to add a TSDoc member reference selector`,
);
}
}
switch (memberSelector.selectorKind) {
case tsdoc.SelectorKind.System:
return this._selectUsingSystemSelector(astDeclarations, memberSelector, astSymbolName);
case tsdoc.SelectorKind.Index:
return this._selectUsingIndexSelector(astDeclarations, memberSelector, astSymbolName);
default:
return new ResolverFailure(`The selector "${memberSelector.selector}" is not a supported selector type`);
}
}
private _selectUsingSystemSelector(
astDeclarations: readonly AstDeclaration[],
memberSelector: tsdoc.DocMemberSelector,
astSymbolName: string,
): AstDeclaration | ResolverFailure {
const selectorName: string = memberSelector.selector;
let selectorSyntaxKind: ts.SyntaxKind;
switch (selectorName) {
case 'class':
selectorSyntaxKind = ts.SyntaxKind.ClassDeclaration;
break;
case 'enum':
selectorSyntaxKind = ts.SyntaxKind.EnumDeclaration;
break;
case 'function':
selectorSyntaxKind = ts.SyntaxKind.FunctionDeclaration;
break;
case 'interface':
selectorSyntaxKind = ts.SyntaxKind.InterfaceDeclaration;
break;
case 'namespace':
selectorSyntaxKind = ts.SyntaxKind.ModuleDeclaration;
break;
case 'type':
selectorSyntaxKind = ts.SyntaxKind.TypeAliasDeclaration;
break;
case 'variable':
selectorSyntaxKind = ts.SyntaxKind.VariableDeclaration;
break;
default:
return new ResolverFailure(`Unsupported system selector "${selectorName}"`);
}
const matches: AstDeclaration[] = astDeclarations.filter((x) => x.declaration.kind === selectorSyntaxKind);
if (matches.length === 0) {
return new ResolverFailure(
`A declaration for "${astSymbolName}" was not found that matches the TSDoc selector "${selectorName}"`,
);
}
if (matches.length > 1) {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(matches);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`More than one declaration "${astSymbolName}" matches the TSDoc selector "${selectorName}"`,
);
}
return matches[0]!;
}
private _selectUsingIndexSelector(
astDeclarations: readonly AstDeclaration[],
memberSelector: tsdoc.DocMemberSelector,
astSymbolName: string,
): AstDeclaration | ResolverFailure {
const selectorOverloadIndex: number = Number.parseInt(memberSelector.selector, 10);
const matches: AstDeclaration[] = [];
for (const astDeclaration of astDeclarations) {
const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration);
if (overloadIndex === selectorOverloadIndex) {
matches.push(astDeclaration);
}
}
if (matches.length === 0) {
return new ResolverFailure(
`An overload for "${astSymbolName}" was not found that matches the` +
` TSDoc selector ":${selectorOverloadIndex}"`,
);
}
if (matches.length > 1) {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(matches);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`More than one declaration for "${astSymbolName}" matches the TSDoc selector ":${selectorOverloadIndex}"`,
);
}
return matches[0]!;
}
/**
* This resolves an ambiguous match in the case where the extra matches are all ancillary declarations,
* except for one match that is the main declaration.
*/
private _tryDisambiguateAncillaryMatches(matches: readonly AstDeclaration[]): AstDeclaration | undefined {
let result: AstDeclaration | undefined;
for (const match of matches) {
const declarationMetadata: DeclarationMetadata = this._collector.fetchDeclarationMetadata(match);
if (!declarationMetadata.isAncillary) {
if (result) {
return undefined; // more than one match
}
result = match;
}
}
return result;
}
}