mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-15 02:53: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:
96
packages/api-extractor/src/collector/ApiItemMetadata.ts
Normal file
96
packages/api-extractor/src/collector/ApiItemMetadata.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
import type { ReleaseTag } from '@discordjs/api-extractor-model';
|
||||
import type * as tsdoc from '@microsoft/tsdoc';
|
||||
import { VisitorState } from './VisitorState.js';
|
||||
|
||||
/**
|
||||
* Constructor parameters for `ApiItemMetadata`.
|
||||
*/
|
||||
export interface IApiItemMetadataOptions {
|
||||
declaredReleaseTag: ReleaseTag;
|
||||
effectiveReleaseTag: ReleaseTag;
|
||||
isEventProperty: boolean;
|
||||
isOverride: boolean;
|
||||
isPreapproved: boolean;
|
||||
isSealed: boolean;
|
||||
isVirtual: boolean;
|
||||
releaseTagSameAsParent: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the Collector's additional analysis for an `AstDeclaration`. This object is assigned to
|
||||
* `AstDeclaration.apiItemMetadata` but consumers must always obtain it by calling `Collector.fetchApiItemMetadata()`.
|
||||
*
|
||||
* @remarks
|
||||
* Note that ancillary declarations share their `ApiItemMetadata` with the main declaration,
|
||||
* whereas a separate `DeclarationMetadata` object is created for each declaration.
|
||||
*
|
||||
* Consider this example:
|
||||
* ```ts
|
||||
* export declare class A {
|
||||
* get b(): string;
|
||||
* set b(value: string);
|
||||
* }
|
||||
* export declare namespace A { }
|
||||
* ```
|
||||
*
|
||||
* In this example, there are two "symbols": `A` and `b`
|
||||
*
|
||||
* There are four "declarations": `A` class, `A` namespace, `b` getter, `b` setter
|
||||
*
|
||||
* There are three "API items": `A` class, `A` namespace, `b` property. The property getter is the main declaration
|
||||
* for `b`, and the setter is the "ancillary" declaration.
|
||||
*/
|
||||
export class ApiItemMetadata {
|
||||
/**
|
||||
* This is the release tag that was explicitly specified in the original doc comment, if any.
|
||||
*/
|
||||
public readonly declaredReleaseTag: ReleaseTag;
|
||||
|
||||
/**
|
||||
* The "effective" release tag is a normalized value that is based on `declaredReleaseTag`,
|
||||
* but may be inherited from a parent, or corrected if the declared value was somehow invalid.
|
||||
* When actually trimming .d.ts files or generating docs, API Extractor uses the "effective" value
|
||||
* instead of the "declared" value.
|
||||
*/
|
||||
public readonly effectiveReleaseTag: ReleaseTag;
|
||||
|
||||
// If true, then it would be redundant to show this release tag
|
||||
public readonly releaseTagSameAsParent: boolean;
|
||||
|
||||
// NOTE: In the future, the Collector may infer or error-correct some of these states.
|
||||
// Generators should rely on these instead of tsdocComment.modifierTagSet.
|
||||
public readonly isEventProperty: boolean;
|
||||
|
||||
public readonly isOverride: boolean;
|
||||
|
||||
public readonly isSealed: boolean;
|
||||
|
||||
public readonly isVirtual: boolean;
|
||||
|
||||
public readonly isPreapproved: boolean;
|
||||
|
||||
/**
|
||||
* This is the TSDoc comment for the declaration. It may be modified (or constructed artificially) by
|
||||
* the DocCommentEnhancer.
|
||||
*/
|
||||
public tsdocComment: tsdoc.DocComment | undefined;
|
||||
|
||||
// Assigned by DocCommentEnhancer
|
||||
public undocumented: boolean = true;
|
||||
|
||||
public docCommentEnhancerVisitorState: VisitorState = VisitorState.Unvisited;
|
||||
|
||||
public constructor(options: IApiItemMetadataOptions) {
|
||||
this.declaredReleaseTag = options.declaredReleaseTag;
|
||||
this.effectiveReleaseTag = options.effectiveReleaseTag;
|
||||
this.releaseTagSameAsParent = options.releaseTagSameAsParent;
|
||||
this.isEventProperty = options.isEventProperty;
|
||||
this.isOverride = options.isOverride;
|
||||
this.isSealed = options.isSealed;
|
||||
this.isVirtual = options.isVirtual;
|
||||
this.isPreapproved = options.isPreapproved;
|
||||
}
|
||||
}
|
||||
953
packages/api-extractor/src/collector/Collector.ts
Normal file
953
packages/api-extractor/src/collector/Collector.ts
Normal file
@@ -0,0 +1,953 @@
|
||||
// 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 { PackageJsonLookup, Sort, InternalError } from '@rushstack/node-core-library';
|
||||
import * as ts from 'typescript';
|
||||
import { PackageDocComment } from '../aedoc/PackageDocComment.js';
|
||||
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
|
||||
import type { AstEntity } from '../analyzer/AstEntity.js';
|
||||
import { AstImport } from '../analyzer/AstImport.js';
|
||||
import type { AstModule, AstModuleExportInfo } from '../analyzer/AstModule.js';
|
||||
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport.js';
|
||||
import { AstReferenceResolver } from '../analyzer/AstReferenceResolver.js';
|
||||
import { AstSymbol } from '../analyzer/AstSymbol.js';
|
||||
import { AstSymbolTable } from '../analyzer/AstSymbolTable.js';
|
||||
import { TypeScriptHelpers } from '../analyzer/TypeScriptHelpers.js';
|
||||
import { TypeScriptInternals, type IGlobalVariableAnalyzer } from '../analyzer/TypeScriptInternals.js';
|
||||
import { ExtractorConfig } from '../api/ExtractorConfig.js';
|
||||
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
|
||||
import { ApiItemMetadata, type IApiItemMetadataOptions } from './ApiItemMetadata.js';
|
||||
import { CollectorEntity } from './CollectorEntity.js';
|
||||
import { type DeclarationMetadata, InternalDeclarationMetadata } from './DeclarationMetadata.js';
|
||||
import type { MessageRouter } from './MessageRouter.js';
|
||||
import type { SourceMapper } from './SourceMapper.js';
|
||||
import { SymbolMetadata } from './SymbolMetadata.js';
|
||||
import { WorkingPackage } from './WorkingPackage.js';
|
||||
|
||||
/**
|
||||
* Options for Collector constructor.
|
||||
*/
|
||||
export interface ICollectorOptions {
|
||||
extractorConfig: ExtractorConfig;
|
||||
|
||||
messageRouter: MessageRouter;
|
||||
|
||||
/**
|
||||
* Configuration for the TypeScript compiler. The most important options to set are:
|
||||
*
|
||||
* - target: ts.ScriptTarget.ES5
|
||||
* - module: ts.ModuleKind.CommonJS
|
||||
* - moduleResolution: ts.ModuleResolutionKind.NodeJs
|
||||
* - rootDir: inputFolder
|
||||
*/
|
||||
program: ts.Program;
|
||||
|
||||
sourceMapper: SourceMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Collector` manages the overall data set that is used by `ApiModelGenerator`,
|
||||
* `DtsRollupGenerator`, and `ApiReportGenerator`. Starting from the working package's entry point,
|
||||
* the `Collector` collects all exported symbols, determines how to import any symbols they reference,
|
||||
* assigns unique names, and sorts everything into a normalized alphabetical ordering.
|
||||
*/
|
||||
export class Collector {
|
||||
public readonly program: ts.Program;
|
||||
|
||||
public readonly typeChecker: ts.TypeChecker;
|
||||
|
||||
public readonly globalVariableAnalyzer: IGlobalVariableAnalyzer;
|
||||
|
||||
public readonly astSymbolTable: AstSymbolTable;
|
||||
|
||||
public readonly astReferenceResolver: AstReferenceResolver;
|
||||
|
||||
public readonly packageJsonLookup: PackageJsonLookup;
|
||||
|
||||
public readonly messageRouter: MessageRouter;
|
||||
|
||||
public readonly workingPackage: WorkingPackage;
|
||||
|
||||
public readonly extractorConfig: ExtractorConfig;
|
||||
|
||||
public readonly sourceMapper: SourceMapper;
|
||||
|
||||
/**
|
||||
* The `ExtractorConfig.bundledPackages` names in a set.
|
||||
*/
|
||||
public readonly bundledPackageNames: ReadonlySet<string>;
|
||||
|
||||
private readonly _program: ts.Program;
|
||||
|
||||
private readonly _tsdocParser: tsdoc.TSDocParser;
|
||||
|
||||
private _astEntryPoint: AstModule | undefined;
|
||||
|
||||
private readonly _entities: CollectorEntity[] = [];
|
||||
|
||||
private readonly _entitiesByAstEntity: Map<AstEntity, CollectorEntity> = new Map<AstEntity, CollectorEntity>();
|
||||
|
||||
private readonly _entitiesBySymbol: Map<ts.Symbol, CollectorEntity> = new Map<ts.Symbol, CollectorEntity>();
|
||||
|
||||
private readonly _starExportedExternalModulePaths: string[] = [];
|
||||
|
||||
private readonly _dtsTypeReferenceDirectives: Set<string> = new Set<string>();
|
||||
|
||||
private readonly _dtsLibReferenceDirectives: Set<string> = new Set<string>();
|
||||
|
||||
// Used by getOverloadIndex()
|
||||
private readonly _cachedOverloadIndexesByDeclaration: Map<AstDeclaration, number>;
|
||||
|
||||
public constructor(options: ICollectorOptions) {
|
||||
this.packageJsonLookup = new PackageJsonLookup();
|
||||
|
||||
this._program = options.program;
|
||||
this.extractorConfig = options.extractorConfig;
|
||||
this.sourceMapper = options.sourceMapper;
|
||||
|
||||
const entryPointSourceFile: ts.SourceFile | undefined = options.program.getSourceFile(
|
||||
this.extractorConfig.mainEntryPointFilePath,
|
||||
);
|
||||
|
||||
if (!entryPointSourceFile) {
|
||||
throw new Error('Unable to load file: ' + this.extractorConfig.mainEntryPointFilePath);
|
||||
}
|
||||
|
||||
if (!this.extractorConfig.packageFolder || !this.extractorConfig.packageJson) {
|
||||
throw new Error('Unable to find a package.json file for the project being analyzed');
|
||||
}
|
||||
|
||||
this.workingPackage = new WorkingPackage({
|
||||
packageFolder: this.extractorConfig.packageFolder,
|
||||
packageJson: this.extractorConfig.packageJson,
|
||||
entryPointSourceFile,
|
||||
});
|
||||
|
||||
this.messageRouter = options.messageRouter;
|
||||
|
||||
this.program = options.program;
|
||||
this.typeChecker = options.program.getTypeChecker();
|
||||
this.globalVariableAnalyzer = TypeScriptInternals.getGlobalVariableAnalyzer(this.program);
|
||||
|
||||
this._tsdocParser = new tsdoc.TSDocParser(this.extractorConfig.tsdocConfiguration);
|
||||
|
||||
this.bundledPackageNames = new Set<string>(this.extractorConfig.bundledPackages);
|
||||
|
||||
this.astSymbolTable = new AstSymbolTable(
|
||||
this.program,
|
||||
this.typeChecker,
|
||||
this.packageJsonLookup,
|
||||
this.bundledPackageNames,
|
||||
this.messageRouter,
|
||||
);
|
||||
this.astReferenceResolver = new AstReferenceResolver(this);
|
||||
|
||||
this._cachedOverloadIndexesByDeclaration = new Map<AstDeclaration, number>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of names (e.g. "example-library") that should appear in a reference like this:
|
||||
*
|
||||
* ```
|
||||
* /// <reference types="example-library" />
|
||||
* ```
|
||||
*/
|
||||
public get dtsTypeReferenceDirectives(): ReadonlySet<string> {
|
||||
return this._dtsTypeReferenceDirectives;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of names (e.g. "runtime-library") that should appear in a reference like this:
|
||||
*
|
||||
* ```
|
||||
* /// <reference lib="runtime-library" />
|
||||
* ```
|
||||
*/
|
||||
public get dtsLibReferenceDirectives(): ReadonlySet<string> {
|
||||
return this._dtsLibReferenceDirectives;
|
||||
}
|
||||
|
||||
public get entities(): readonly CollectorEntity[] {
|
||||
return this._entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of module specifiers (e.g. `"@rushstack/node-core-library/lib/FileSystem"`) that should be emitted
|
||||
* as star exports (e.g. `export * from "@rushstack/node-core-library/lib/FileSystem"`).
|
||||
*/
|
||||
public get starExportedExternalModulePaths(): readonly string[] {
|
||||
return this._starExportedExternalModulePaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the analysis.
|
||||
*/
|
||||
public analyze(): void {
|
||||
if (this._astEntryPoint) {
|
||||
throw new Error('DtsRollupGenerator.analyze() was already called');
|
||||
}
|
||||
|
||||
// This runs a full type analysis, and then augments the Abstract Syntax Tree (i.e. declarations)
|
||||
// with semantic information (i.e. symbols). The "diagnostics" are a subset of the everyday
|
||||
// compile errors that would result from a full compilation.
|
||||
for (const diagnostic of this._program.getSemanticDiagnostics()) {
|
||||
this.messageRouter.addCompilerDiagnostic(diagnostic);
|
||||
}
|
||||
|
||||
const sourceFiles: readonly ts.SourceFile[] = this.program.getSourceFiles();
|
||||
|
||||
if (this.messageRouter.showDiagnostics) {
|
||||
this.messageRouter.logDiagnosticHeader('Root filenames');
|
||||
for (const fileName of this.program.getRootFileNames()) {
|
||||
this.messageRouter.logDiagnostic(fileName);
|
||||
}
|
||||
|
||||
this.messageRouter.logDiagnosticFooter();
|
||||
|
||||
this.messageRouter.logDiagnosticHeader('Files analyzed by compiler');
|
||||
for (const sourceFile of sourceFiles) {
|
||||
this.messageRouter.logDiagnostic(sourceFile.fileName);
|
||||
}
|
||||
|
||||
this.messageRouter.logDiagnosticFooter();
|
||||
}
|
||||
|
||||
// We can throw this error earlier in CompilerState.ts, but intentionally wait until after we've logged the
|
||||
// associated diagnostic message above to make debugging easier for developers.
|
||||
// Typically there will be many such files -- to avoid too much noise, only report the first one.
|
||||
const badSourceFile: ts.SourceFile | undefined = sourceFiles.find(
|
||||
({ fileName }) => !ExtractorConfig.hasDtsFileExtension(fileName),
|
||||
);
|
||||
if (badSourceFile) {
|
||||
this.messageRouter.addAnalyzerIssueForPosition(
|
||||
ExtractorMessageId.WrongInputFileType,
|
||||
'Incorrect file type; API Extractor expects to analyze compiler outputs with the .d.ts file extension. ' +
|
||||
'Troubleshooting tips: https://api-extractor.com/link/dts-error',
|
||||
badSourceFile,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
// Build the entry point
|
||||
const entryPointSourceFile: ts.SourceFile = this.workingPackage.entryPointSourceFile;
|
||||
|
||||
const astEntryPoint: AstModule = this.astSymbolTable.fetchAstModuleFromWorkingPackage(entryPointSourceFile);
|
||||
this._astEntryPoint = astEntryPoint;
|
||||
|
||||
const packageDocCommentTextRange: ts.TextRange | undefined = PackageDocComment.tryFindInSourceFile(
|
||||
entryPointSourceFile,
|
||||
this,
|
||||
);
|
||||
|
||||
if (packageDocCommentTextRange) {
|
||||
const range: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(
|
||||
entryPointSourceFile.text,
|
||||
packageDocCommentTextRange.pos,
|
||||
packageDocCommentTextRange.end,
|
||||
);
|
||||
|
||||
this.workingPackage.tsdocParserContext = this._tsdocParser.parseRange(range);
|
||||
|
||||
this.messageRouter.addTsdocMessages(this.workingPackage.tsdocParserContext, entryPointSourceFile);
|
||||
|
||||
this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment;
|
||||
}
|
||||
|
||||
const astModuleExportInfo: AstModuleExportInfo = this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint);
|
||||
|
||||
// Create a CollectorEntity for each top-level export.
|
||||
const processedAstEntities: AstEntity[] = [];
|
||||
for (const [exportName, astEntity] of astModuleExportInfo.exportedLocalEntities) {
|
||||
this._createCollectorEntity(astEntity, exportName);
|
||||
processedAstEntities.push(astEntity);
|
||||
}
|
||||
|
||||
// Recursively create the remaining CollectorEntities after the top-level entities
|
||||
// have been processed.
|
||||
const alreadySeenAstEntities: Set<AstEntity> = new Set<AstEntity>();
|
||||
for (const astEntity of processedAstEntities) {
|
||||
this._recursivelyCreateEntities(astEntity, alreadySeenAstEntities);
|
||||
if (astEntity instanceof AstSymbol) {
|
||||
this.fetchSymbolMetadata(astEntity);
|
||||
}
|
||||
}
|
||||
|
||||
this._makeUniqueNames();
|
||||
|
||||
for (const starExportedExternalModule of astModuleExportInfo.starExportedExternalModules) {
|
||||
if (starExportedExternalModule.externalModulePath !== undefined) {
|
||||
this._starExportedExternalModulePaths.push(starExportedExternalModule.externalModulePath);
|
||||
}
|
||||
}
|
||||
|
||||
Sort.sortBy(this._entities, (x) => x.getSortKey());
|
||||
Sort.sortSet(this._dtsTypeReferenceDirectives);
|
||||
Sort.sortSet(this._dtsLibReferenceDirectives);
|
||||
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
|
||||
this._starExportedExternalModulePaths.sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given ts.Identifier that is part of an AstSymbol that we analyzed, return the CollectorEntity that
|
||||
* it refers to. Returns undefined if it doesn't refer to anything interesting.
|
||||
*
|
||||
* @remarks
|
||||
* Throws an Error if the ts.Identifier is not part of node tree that was analyzed.
|
||||
*/
|
||||
public tryGetEntityForNode(identifier: ts.Identifier | ts.ImportTypeNode): CollectorEntity | undefined {
|
||||
const astEntity: AstEntity | undefined = this.astSymbolTable.tryGetEntityForNode(identifier);
|
||||
if (astEntity) {
|
||||
return this._entitiesByAstEntity.get(astEntity);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given analyzed ts.Symbol, return the CollectorEntity that it refers to. Returns undefined if it
|
||||
* doesn't refer to anything interesting.
|
||||
*/
|
||||
public tryGetEntityForSymbol(symbol: ts.Symbol): CollectorEntity | undefined {
|
||||
return this._entitiesBySymbol.get(symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the associated `CollectorEntity` for the given `astEntity`, if one was created during analysis.
|
||||
*/
|
||||
public tryGetCollectorEntity(astEntity: AstEntity): CollectorEntity | undefined {
|
||||
return this._entitiesByAstEntity.get(astEntity);
|
||||
}
|
||||
|
||||
public fetchSymbolMetadata(astSymbol: AstSymbol): SymbolMetadata {
|
||||
if (astSymbol.symbolMetadata === undefined) {
|
||||
this._fetchSymbolMetadata(astSymbol);
|
||||
}
|
||||
|
||||
return astSymbol.symbolMetadata as SymbolMetadata;
|
||||
}
|
||||
|
||||
public fetchDeclarationMetadata(astDeclaration: AstDeclaration): DeclarationMetadata {
|
||||
if (astDeclaration.declarationMetadata === undefined) {
|
||||
// Fetching the SymbolMetadata always constructs the DeclarationMetadata
|
||||
this._fetchSymbolMetadata(astDeclaration.astSymbol);
|
||||
}
|
||||
|
||||
return astDeclaration.declarationMetadata as DeclarationMetadata;
|
||||
}
|
||||
|
||||
public fetchApiItemMetadata(astDeclaration: AstDeclaration): ApiItemMetadata {
|
||||
if (astDeclaration.apiItemMetadata === undefined) {
|
||||
// Fetching the SymbolMetadata always constructs the ApiItemMetadata
|
||||
this._fetchSymbolMetadata(astDeclaration.astSymbol);
|
||||
}
|
||||
|
||||
return astDeclaration.apiItemMetadata as ApiItemMetadata;
|
||||
}
|
||||
|
||||
public tryFetchMetadataForAstEntity(astEntity: AstEntity): SymbolMetadata | undefined {
|
||||
if (astEntity instanceof AstSymbol) {
|
||||
return this.fetchSymbolMetadata(astEntity);
|
||||
}
|
||||
|
||||
if (astEntity instanceof AstImport && astEntity.astSymbol) {
|
||||
return this.fetchSymbolMetadata(astEntity.astSymbol);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public isAncillaryDeclaration(astDeclaration: AstDeclaration): boolean {
|
||||
const declarationMetadata: DeclarationMetadata = this.fetchDeclarationMetadata(astDeclaration);
|
||||
return declarationMetadata.isAncillary;
|
||||
}
|
||||
|
||||
public getNonAncillaryDeclarations(astSymbol: AstSymbol): readonly AstDeclaration[] {
|
||||
const result: AstDeclaration[] = [];
|
||||
for (const astDeclaration of astSymbol.astDeclarations) {
|
||||
const declarationMetadata: DeclarationMetadata = this.fetchDeclarationMetadata(astDeclaration);
|
||||
if (!declarationMetadata.isAncillary) {
|
||||
result.push(astDeclaration);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the leading underscore, for example: "_Example" --\> "example*Example*_"
|
||||
*
|
||||
* @remarks
|
||||
* This causes internal definitions to sort alphabetically case-insensitive, then case-sensitive, and
|
||||
* initially ignoring the underscore prefix, while still deterministically comparing it.
|
||||
* The star is used as a delimiter because it is not a legal identifier character.
|
||||
*/
|
||||
public static getSortKeyIgnoringUnderscore(identifier: string | undefined): string {
|
||||
if (!identifier) return '';
|
||||
|
||||
let parts: string[];
|
||||
|
||||
if (identifier.startsWith('_')) {
|
||||
const withoutUnderscore: string = identifier.slice(1);
|
||||
parts = [withoutUnderscore.toLowerCase(), '*', withoutUnderscore, '*', '_'];
|
||||
} else {
|
||||
parts = [identifier.toLowerCase(), '*', identifier];
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* For function-like signatures, this returns the TSDoc "overload index" which can be used to identify
|
||||
* a specific overload.
|
||||
*/
|
||||
public getOverloadIndex(astDeclaration: AstDeclaration): number {
|
||||
const allDeclarations: readonly AstDeclaration[] = astDeclaration.astSymbol.astDeclarations;
|
||||
if (allDeclarations.length === 1) {
|
||||
return 1; // trivial case
|
||||
}
|
||||
|
||||
let overloadIndex: number | undefined = this._cachedOverloadIndexesByDeclaration.get(astDeclaration);
|
||||
|
||||
if (overloadIndex === undefined) {
|
||||
// TSDoc index selectors are positive integers counting from 1
|
||||
let nextIndex = 1;
|
||||
for (const other of allDeclarations) {
|
||||
// Filter out other declarations that are not overloads. For example, an overloaded function can also
|
||||
// be a namespace.
|
||||
if (other.declaration.kind === astDeclaration.declaration.kind) {
|
||||
this._cachedOverloadIndexesByDeclaration.set(other, nextIndex);
|
||||
++nextIndex;
|
||||
}
|
||||
}
|
||||
|
||||
overloadIndex = this._cachedOverloadIndexesByDeclaration.get(astDeclaration);
|
||||
}
|
||||
|
||||
if (overloadIndex === undefined) {
|
||||
// This should never happen
|
||||
throw new InternalError('Error calculating overload index for declaration');
|
||||
}
|
||||
|
||||
return overloadIndex;
|
||||
}
|
||||
|
||||
private _createCollectorEntity(astEntity: AstEntity, exportName?: string, parent?: CollectorEntity): CollectorEntity {
|
||||
let entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity);
|
||||
|
||||
if (!entity) {
|
||||
entity = new CollectorEntity(astEntity);
|
||||
|
||||
this._entitiesByAstEntity.set(astEntity, entity);
|
||||
if (astEntity instanceof AstSymbol) {
|
||||
this._entitiesBySymbol.set(astEntity.followedSymbol, entity);
|
||||
} else if (astEntity instanceof AstNamespaceImport) {
|
||||
this._entitiesBySymbol.set(astEntity.symbol, entity);
|
||||
}
|
||||
|
||||
this._entities.push(entity);
|
||||
this._collectReferenceDirectives(astEntity);
|
||||
}
|
||||
|
||||
if (exportName) {
|
||||
if (parent) {
|
||||
entity.addLocalExportName(exportName, parent);
|
||||
} else {
|
||||
entity.addExportName(exportName);
|
||||
}
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private _recursivelyCreateEntities(astEntity: AstEntity, alreadySeenAstEntities: Set<AstEntity>): void {
|
||||
if (alreadySeenAstEntities.has(astEntity)) return;
|
||||
alreadySeenAstEntities.add(astEntity);
|
||||
|
||||
if (astEntity instanceof AstSymbol) {
|
||||
astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
|
||||
for (const referencedAstEntity of astDeclaration.referencedAstEntities) {
|
||||
if (referencedAstEntity instanceof AstSymbol) {
|
||||
// We only create collector entities for root-level symbols. For example, if a symbol is
|
||||
// nested inside a namespace, only the namespace gets a collector entity. Note that this
|
||||
// is not true for AstNamespaceImports below.
|
||||
if (referencedAstEntity.parentAstSymbol === undefined) {
|
||||
this._createCollectorEntity(referencedAstEntity);
|
||||
}
|
||||
} else {
|
||||
this._createCollectorEntity(referencedAstEntity);
|
||||
}
|
||||
|
||||
this._recursivelyCreateEntities(referencedAstEntity, alreadySeenAstEntities);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (astEntity instanceof AstNamespaceImport) {
|
||||
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this);
|
||||
const parentEntity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity);
|
||||
if (!parentEntity) {
|
||||
// This should never happen, as we've already created entities for all AstNamespaceImports.
|
||||
throw new InternalError(
|
||||
`Failed to get CollectorEntity for AstNamespaceImport with namespace name "${astEntity.namespaceName}"`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const [localExportName, localAstEntity] of astModuleExportInfo.exportedLocalEntities) {
|
||||
// Create a CollectorEntity for each local export within an AstNamespaceImport entity.
|
||||
this._createCollectorEntity(localAstEntity, localExportName, parentEntity);
|
||||
this._recursivelyCreateEntities(localAstEntity, alreadySeenAstEntities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a unique name for each item in the package typings file.
|
||||
*/
|
||||
private _makeUniqueNames(): void {
|
||||
// The following examples illustrate the nameForEmit heuristics:
|
||||
//
|
||||
// Example 1:
|
||||
// class X { } <--- nameForEmit should be "A" to simplify things and reduce possibility of conflicts
|
||||
// export { X as A };
|
||||
//
|
||||
// Example 2:
|
||||
// class X { } <--- nameForEmit should be "X" because choosing A or B would be nondeterministic
|
||||
// export { X as A };
|
||||
// export { X as B };
|
||||
//
|
||||
// Example 3:
|
||||
// class X { } <--- nameForEmit should be "X_1" because Y has a stronger claim to the name
|
||||
// export { X as A };
|
||||
// export { X as B };
|
||||
// class Y { } <--- nameForEmit should be "X"
|
||||
// export { Y as X };
|
||||
|
||||
// Set of names that should NOT be used when generating a unique nameForEmit
|
||||
const usedNames: Set<string> = new Set<string>();
|
||||
|
||||
// First collect the names of explicit package exports, and perform a sanity check.
|
||||
for (const entity of this._entities) {
|
||||
for (const exportName of entity.exportNames) {
|
||||
if (usedNames.has(exportName)) {
|
||||
// This should be impossible
|
||||
throw new InternalError(`A package cannot have two exports with the name "${exportName}"`);
|
||||
}
|
||||
|
||||
usedNames.add(exportName);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that each entity has a unique nameForEmit
|
||||
for (const entity of this._entities) {
|
||||
// What name would we ideally want to emit it as?
|
||||
let idealNameForEmit: string;
|
||||
|
||||
// If this entity is exported exactly once, then we prefer the exported name
|
||||
if (entity.singleExportName !== undefined && entity.singleExportName !== ts.InternalSymbolName.Default) {
|
||||
idealNameForEmit = entity.singleExportName;
|
||||
} else {
|
||||
// otherwise use the local name
|
||||
idealNameForEmit = entity.astEntity.localName;
|
||||
}
|
||||
|
||||
if (idealNameForEmit.includes('.')) {
|
||||
// For an ImportType with a namespace chain, only the top namespace is imported.
|
||||
idealNameForEmit = idealNameForEmit.split('.')[0]!;
|
||||
}
|
||||
|
||||
// If the idealNameForEmit happens to be the same as one of the exports, then we're safe to use that...
|
||||
if (
|
||||
entity.exportNames.has(idealNameForEmit) && // ...except that if it conflicts with a global name, then the global name wins
|
||||
!this.globalVariableAnalyzer.hasGlobalName(idealNameForEmit) && // ...also avoid "default" which can interfere with "export { default } from 'some-module;'"
|
||||
idealNameForEmit !== 'default'
|
||||
) {
|
||||
entity.nameForEmit = idealNameForEmit;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate a unique name based on idealNameForEmit
|
||||
let suffix = 1;
|
||||
let nameForEmit: string = idealNameForEmit;
|
||||
|
||||
// Choose a name that doesn't conflict with usedNames or a global name
|
||||
while (
|
||||
nameForEmit === 'default' ||
|
||||
usedNames.has(nameForEmit) ||
|
||||
this.globalVariableAnalyzer.hasGlobalName(nameForEmit)
|
||||
) {
|
||||
nameForEmit = `${idealNameForEmit}_${++suffix}`;
|
||||
}
|
||||
|
||||
entity.nameForEmit = nameForEmit;
|
||||
usedNames.add(nameForEmit);
|
||||
}
|
||||
}
|
||||
|
||||
private _fetchSymbolMetadata(astSymbol: AstSymbol): void {
|
||||
if (astSymbol.symbolMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When we solve an astSymbol, then we always also solve all of its parents and all of its declarations.
|
||||
// The parent is solved first.
|
||||
if (astSymbol.parentAstSymbol && astSymbol.parentAstSymbol.symbolMetadata === undefined) {
|
||||
this._fetchSymbolMetadata(astSymbol.parentAstSymbol);
|
||||
}
|
||||
|
||||
// Construct the DeclarationMetadata objects, and detect any ancillary declarations
|
||||
this._calculateDeclarationMetadataForDeclarations(astSymbol);
|
||||
|
||||
// Calculate the ApiItemMetadata objects
|
||||
for (const astDeclaration of astSymbol.astDeclarations) {
|
||||
this._calculateApiItemMetadata(astDeclaration);
|
||||
}
|
||||
|
||||
// The most public effectiveReleaseTag for all declarations
|
||||
let maxEffectiveReleaseTag: ReleaseTag = ReleaseTag.None;
|
||||
|
||||
for (const astDeclaration of astSymbol.astDeclarations) {
|
||||
// We know we solved this above
|
||||
const apiItemMetadata: ApiItemMetadata = astDeclaration.apiItemMetadata as ApiItemMetadata;
|
||||
|
||||
const effectiveReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;
|
||||
|
||||
if (effectiveReleaseTag > maxEffectiveReleaseTag) {
|
||||
maxEffectiveReleaseTag = effectiveReleaseTag;
|
||||
}
|
||||
}
|
||||
|
||||
// Update this last when we're sure no exceptions were thrown
|
||||
astSymbol.symbolMetadata = new SymbolMetadata({
|
||||
maxEffectiveReleaseTag,
|
||||
});
|
||||
}
|
||||
|
||||
private _calculateDeclarationMetadataForDeclarations(astSymbol: AstSymbol): void {
|
||||
// Initialize DeclarationMetadata for each declaration
|
||||
for (const astDeclaration of astSymbol.astDeclarations) {
|
||||
if (astDeclaration.declarationMetadata) {
|
||||
throw new InternalError('AstDeclaration.declarationMetadata is not expected to have been initialized yet');
|
||||
}
|
||||
|
||||
const metadata: InternalDeclarationMetadata = new InternalDeclarationMetadata();
|
||||
metadata.tsdocParserContext = this._parseTsdocForAstDeclaration(astDeclaration);
|
||||
|
||||
astDeclaration.declarationMetadata = metadata;
|
||||
}
|
||||
|
||||
// Detect ancillary declarations
|
||||
for (const astDeclaration of astSymbol.astDeclarations) {
|
||||
// For a getter/setter pair, make the setter ancillary to the getter
|
||||
if (astDeclaration.declaration.kind === ts.SyntaxKind.SetAccessor) {
|
||||
let foundGetter = false;
|
||||
for (const getterAstDeclaration of astDeclaration.astSymbol.astDeclarations) {
|
||||
if (getterAstDeclaration.declaration.kind === ts.SyntaxKind.GetAccessor) {
|
||||
// Associate it with the getter
|
||||
this._addAncillaryDeclaration(getterAstDeclaration, astDeclaration);
|
||||
|
||||
foundGetter = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundGetter) {
|
||||
this.messageRouter.addAnalyzerIssue(
|
||||
ExtractorMessageId.MissingGetter,
|
||||
`The property "${astDeclaration.astSymbol.localName}" has a setter but no getter.`,
|
||||
astDeclaration,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _addAncillaryDeclaration(mainAstDeclaration: AstDeclaration, ancillaryAstDeclaration: AstDeclaration): void {
|
||||
const mainMetadata: InternalDeclarationMetadata =
|
||||
mainAstDeclaration.declarationMetadata as InternalDeclarationMetadata;
|
||||
const ancillaryMetadata: InternalDeclarationMetadata =
|
||||
ancillaryAstDeclaration.declarationMetadata as InternalDeclarationMetadata;
|
||||
|
||||
if (mainMetadata.ancillaryDeclarations.includes(ancillaryAstDeclaration)) {
|
||||
return; // already added
|
||||
}
|
||||
|
||||
if (mainAstDeclaration.astSymbol !== ancillaryAstDeclaration.astSymbol) {
|
||||
throw new InternalError(
|
||||
'Invalid call to _addAncillaryDeclaration() because declarations do not belong to the same symbol',
|
||||
);
|
||||
}
|
||||
|
||||
if (mainMetadata.isAncillary) {
|
||||
throw new InternalError('Invalid call to _addAncillaryDeclaration() because the target is ancillary itself');
|
||||
}
|
||||
|
||||
if (ancillaryMetadata.isAncillary) {
|
||||
throw new InternalError(
|
||||
'Invalid call to _addAncillaryDeclaration() because source is already ancillary to another declaration',
|
||||
);
|
||||
}
|
||||
|
||||
if (mainAstDeclaration.apiItemMetadata || ancillaryAstDeclaration.apiItemMetadata) {
|
||||
throw new InternalError(
|
||||
'Invalid call to _addAncillaryDeclaration() because the API item metadata has already been constructed',
|
||||
);
|
||||
}
|
||||
|
||||
ancillaryMetadata.isAncillary = true;
|
||||
mainMetadata.ancillaryDeclarations.push(ancillaryAstDeclaration);
|
||||
}
|
||||
|
||||
private _calculateApiItemMetadata(astDeclaration: AstDeclaration): void {
|
||||
const declarationMetadata: InternalDeclarationMetadata =
|
||||
astDeclaration.declarationMetadata as InternalDeclarationMetadata;
|
||||
if (declarationMetadata.isAncillary) {
|
||||
if (astDeclaration.declaration.kind === ts.SyntaxKind.SetAccessor && declarationMetadata.tsdocParserContext) {
|
||||
this.messageRouter.addAnalyzerIssue(
|
||||
ExtractorMessageId.SetterWithDocs,
|
||||
`The doc comment for the property "${astDeclaration.astSymbol.localName}"` +
|
||||
` must appear on the getter, not the setter.`,
|
||||
astDeclaration,
|
||||
);
|
||||
}
|
||||
|
||||
// We never calculate ApiItemMetadata for an ancillary declaration; instead, it is assigned when
|
||||
// the main declaration is processed.
|
||||
return;
|
||||
}
|
||||
|
||||
const options: IApiItemMetadataOptions = {
|
||||
declaredReleaseTag: ReleaseTag.None,
|
||||
effectiveReleaseTag: ReleaseTag.None,
|
||||
isEventProperty: false,
|
||||
isOverride: false,
|
||||
isSealed: false,
|
||||
isVirtual: false,
|
||||
isPreapproved: false,
|
||||
releaseTagSameAsParent: false,
|
||||
};
|
||||
|
||||
const parserContext: tsdoc.ParserContext | undefined = declarationMetadata.tsdocParserContext;
|
||||
if (parserContext) {
|
||||
const modifierTagSet: tsdoc.StandardModifierTagSet = parserContext.docComment.modifierTagSet;
|
||||
|
||||
let declaredReleaseTag: ReleaseTag = ReleaseTag.None;
|
||||
let extraReleaseTags = false;
|
||||
|
||||
if (modifierTagSet.isPublic()) {
|
||||
declaredReleaseTag = ReleaseTag.Public;
|
||||
}
|
||||
|
||||
if (modifierTagSet.isBeta()) {
|
||||
if (declaredReleaseTag === ReleaseTag.None) {
|
||||
declaredReleaseTag = ReleaseTag.Beta;
|
||||
} else {
|
||||
extraReleaseTags = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (modifierTagSet.isAlpha()) {
|
||||
if (declaredReleaseTag === ReleaseTag.None) {
|
||||
declaredReleaseTag = ReleaseTag.Alpha;
|
||||
} else {
|
||||
extraReleaseTags = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (modifierTagSet.isInternal()) {
|
||||
if (declaredReleaseTag === ReleaseTag.None) {
|
||||
declaredReleaseTag = ReleaseTag.Internal;
|
||||
} else {
|
||||
extraReleaseTags = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (extraReleaseTags && !astDeclaration.astSymbol.isExternal) {
|
||||
// for now, don't report errors for external code
|
||||
this.messageRouter.addAnalyzerIssue(
|
||||
ExtractorMessageId.ExtraReleaseTag,
|
||||
'The doc comment should not contain more than one release tag',
|
||||
astDeclaration,
|
||||
);
|
||||
}
|
||||
|
||||
options.declaredReleaseTag = declaredReleaseTag;
|
||||
|
||||
options.isEventProperty = modifierTagSet.isEventProperty();
|
||||
options.isOverride = modifierTagSet.isOverride();
|
||||
options.isSealed = modifierTagSet.isSealed();
|
||||
options.isVirtual = modifierTagSet.isVirtual();
|
||||
const preapprovedTag: tsdoc.TSDocTagDefinition | undefined =
|
||||
this.extractorConfig.tsdocConfiguration.tryGetTagDefinition('@preapproved');
|
||||
|
||||
if (preapprovedTag && modifierTagSet.hasTag(preapprovedTag)) {
|
||||
// This feature only makes sense for potentially big declarations.
|
||||
switch (astDeclaration.declaration.kind) {
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
case ts.SyntaxKind.EnumDeclaration:
|
||||
case ts.SyntaxKind.InterfaceDeclaration:
|
||||
case ts.SyntaxKind.ModuleDeclaration:
|
||||
if (declaredReleaseTag === ReleaseTag.Internal) {
|
||||
options.isPreapproved = true;
|
||||
} else {
|
||||
this.messageRouter.addAnalyzerIssue(
|
||||
ExtractorMessageId.PreapprovedBadReleaseTag,
|
||||
`The @preapproved tag cannot be applied to "${astDeclaration.astSymbol.localName}"` +
|
||||
` without an @internal release tag`,
|
||||
astDeclaration,
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
this.messageRouter.addAnalyzerIssue(
|
||||
ExtractorMessageId.PreapprovedUnsupportedType,
|
||||
`The @preapproved tag cannot be applied to "${astDeclaration.astSymbol.localName}"` +
|
||||
` because it is not a supported declaration type`,
|
||||
astDeclaration,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This needs to be set regardless of whether or not a parserContext exists
|
||||
if (astDeclaration.parent) {
|
||||
const parentApiItemMetadata: ApiItemMetadata = this.fetchApiItemMetadata(astDeclaration.parent);
|
||||
options.effectiveReleaseTag =
|
||||
options.declaredReleaseTag === ReleaseTag.None
|
||||
? parentApiItemMetadata.effectiveReleaseTag
|
||||
: options.declaredReleaseTag;
|
||||
|
||||
options.releaseTagSameAsParent = parentApiItemMetadata.effectiveReleaseTag === options.effectiveReleaseTag;
|
||||
} else {
|
||||
options.effectiveReleaseTag = options.declaredReleaseTag;
|
||||
}
|
||||
|
||||
if (options.effectiveReleaseTag === ReleaseTag.None) {
|
||||
if (!astDeclaration.astSymbol.isExternal) {
|
||||
// for now, don't report errors for external code
|
||||
// Don't report missing release tags for forgotten exports (unless we're including forgotten exports
|
||||
// in either the API report or doc model).
|
||||
const astSymbol: AstSymbol = astDeclaration.astSymbol;
|
||||
const entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astSymbol.rootAstSymbol);
|
||||
if (
|
||||
entity &&
|
||||
(entity.consumable ||
|
||||
this.extractorConfig.apiReportIncludeForgottenExports ||
|
||||
this.extractorConfig.docModelIncludeForgottenExports) && // We also don't report errors for the default export of an entry point, since its doc comment
|
||||
// isn't easy to obtain from the .d.ts file
|
||||
astSymbol.rootAstSymbol.localName !== '_default'
|
||||
) {
|
||||
this.messageRouter.addAnalyzerIssue(
|
||||
ExtractorMessageId.MissingReleaseTag,
|
||||
`"${entity.astEntity.localName}" is part of the package's API, but it is missing ` +
|
||||
`a release tag (@alpha, @beta, @public, or @internal)`,
|
||||
astSymbol,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
options.effectiveReleaseTag = ReleaseTag.Public;
|
||||
}
|
||||
|
||||
const apiItemMetadata: ApiItemMetadata = new ApiItemMetadata(options);
|
||||
if (parserContext) {
|
||||
apiItemMetadata.tsdocComment = parserContext.docComment;
|
||||
}
|
||||
|
||||
astDeclaration.apiItemMetadata = apiItemMetadata;
|
||||
|
||||
// Lastly, share the result with any ancillary declarations
|
||||
for (const ancillaryDeclaration of declarationMetadata.ancillaryDeclarations) {
|
||||
ancillaryDeclaration.apiItemMetadata = apiItemMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
private _parseTsdocForAstDeclaration(astDeclaration: AstDeclaration): tsdoc.ParserContext | undefined {
|
||||
const declaration: ts.Declaration = astDeclaration.declaration;
|
||||
let nodeForComment: ts.Node = declaration;
|
||||
|
||||
if (ts.isVariableDeclaration(declaration)) {
|
||||
// Variable declarations are special because they can be combined into a list. For example:
|
||||
//
|
||||
// /** A */ export /** B */ const /** C */ x = 1, /** D **/ [ /** E */ y, z] = [3, 4];
|
||||
//
|
||||
// The compiler will only emit comments A and C in the .d.ts file, so in general there isn't a well-defined
|
||||
// way to document these parts. API Extractor requires you to break them into separate exports like this:
|
||||
//
|
||||
// /** A */ export const x = 1;
|
||||
//
|
||||
// But _getReleaseTagForDeclaration() still receives a node corresponding to "x", so we need to walk upwards
|
||||
// and find the containing statement in order for getJSDocCommentRanges() to read the comment that we expect.
|
||||
const statement: ts.VariableStatement | undefined = TypeScriptHelpers.findFirstParent(
|
||||
declaration,
|
||||
ts.SyntaxKind.VariableStatement,
|
||||
) as ts.VariableStatement | undefined;
|
||||
if (
|
||||
statement !== undefined && // For a compound declaration, fall back to looking for C instead of A
|
||||
statement.declarationList.declarations.length === 1
|
||||
) {
|
||||
nodeForComment = statement;
|
||||
}
|
||||
}
|
||||
|
||||
const sourceFileText: string = declaration.getSourceFile().text;
|
||||
const ranges: ts.CommentRange[] = TypeScriptInternals.getJSDocCommentRanges(nodeForComment, sourceFileText) ?? [];
|
||||
|
||||
if (ranges.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// We use the JSDoc comment block that is closest to the definition, i.e.
|
||||
// the last one preceding it
|
||||
const range: ts.TextRange = ranges[ranges.length - 1]!;
|
||||
|
||||
const tsdocTextRange: tsdoc.TextRange = tsdoc.TextRange.fromStringRange(sourceFileText, range.pos, range.end);
|
||||
|
||||
const parserContext: tsdoc.ParserContext = this._tsdocParser.parseRange(tsdocTextRange);
|
||||
|
||||
this.messageRouter.addTsdocMessages(parserContext, declaration.getSourceFile(), astDeclaration);
|
||||
|
||||
// We delete the @privateRemarks block as early as possible, to ensure that it never leaks through
|
||||
// into one of the output files.
|
||||
parserContext.docComment.privateRemarks = undefined;
|
||||
|
||||
return parserContext;
|
||||
}
|
||||
|
||||
private _collectReferenceDirectives(astEntity: AstEntity): void {
|
||||
if (astEntity instanceof AstSymbol) {
|
||||
const sourceFiles: ts.SourceFile[] = astEntity.astDeclarations.map((astDeclaration) =>
|
||||
astDeclaration.declaration.getSourceFile(),
|
||||
);
|
||||
this._collectReferenceDirectivesFromSourceFiles(sourceFiles);
|
||||
return;
|
||||
}
|
||||
|
||||
if (astEntity instanceof AstNamespaceImport) {
|
||||
const sourceFiles: ts.SourceFile[] = [astEntity.astModule.sourceFile];
|
||||
this._collectReferenceDirectivesFromSourceFiles(sourceFiles);
|
||||
}
|
||||
}
|
||||
|
||||
private _collectReferenceDirectivesFromSourceFiles(sourceFiles: ts.SourceFile[]): void {
|
||||
const seenFilenames: Set<string> = new Set<string>();
|
||||
|
||||
for (const sourceFile of sourceFiles) {
|
||||
if (sourceFile?.fileName && !seenFilenames.has(sourceFile.fileName)) {
|
||||
seenFilenames.add(sourceFile.fileName);
|
||||
|
||||
for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) {
|
||||
const name: string = sourceFile.text.slice(typeReferenceDirective.pos, typeReferenceDirective.end);
|
||||
this._dtsTypeReferenceDirectives.add(name);
|
||||
}
|
||||
|
||||
for (const libReferenceDirective of sourceFile.libReferenceDirectives) {
|
||||
const name: string = sourceFile.text.slice(libReferenceDirective.pos, libReferenceDirective.end);
|
||||
this._dtsLibReferenceDirectives.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
241
packages/api-extractor/src/collector/CollectorEntity.ts
Normal file
241
packages/api-extractor/src/collector/CollectorEntity.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
import { Sort } from '@rushstack/node-core-library';
|
||||
import * as ts from 'typescript';
|
||||
import type { AstEntity } from '../analyzer/AstEntity.js';
|
||||
import { AstSymbol } from '../analyzer/AstSymbol.js';
|
||||
import { Collector } from './Collector.js';
|
||||
|
||||
/**
|
||||
* This is a data structure used by the Collector to track an AstEntity that may be emitted in the *.d.ts file.
|
||||
*
|
||||
* @remarks
|
||||
* The additional contextual state beyond AstSymbol is:
|
||||
* - Whether it's an export of this entry point or not
|
||||
* - The nameForEmit, which may get renamed by DtsRollupGenerator._makeUniqueNames()
|
||||
* - The export name (or names, if the same symbol is exported multiple times)
|
||||
*/
|
||||
export class CollectorEntity {
|
||||
/**
|
||||
* The AstEntity that this entry represents.
|
||||
*/
|
||||
public readonly astEntity: AstEntity;
|
||||
|
||||
private _exportNames: Set<string> = new Set();
|
||||
|
||||
private _exportNamesSorted: boolean = false;
|
||||
|
||||
private _singleExportName: string | undefined = undefined;
|
||||
|
||||
private _localExportNamesByParent: Map<CollectorEntity, Set<string>> = new Map();
|
||||
|
||||
private _nameForEmit: string | undefined = undefined;
|
||||
|
||||
private _sortKey: string | undefined = undefined;
|
||||
|
||||
public constructor(astEntity: AstEntity) {
|
||||
this.astEntity = astEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* The declaration name that will be emitted in the .d.ts rollup, .api.md, and .api.json files. Generated by
|
||||
* `Collector._makeUniqueNames`. Be aware that the declaration may be renamed to avoid conflicts with (1)
|
||||
* global names (e.g. `Promise`) and (2) if local, other local names across different files.
|
||||
*/
|
||||
public get nameForEmit(): string | undefined {
|
||||
return this._nameForEmit;
|
||||
}
|
||||
|
||||
public set nameForEmit(value: string | undefined) {
|
||||
this._nameForEmit = value;
|
||||
this._sortKey = undefined; // invalidate the cached value
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of export names if this symbol is exported from the entry point.
|
||||
*
|
||||
* @remarks
|
||||
* Note that a given symbol may be exported more than once:
|
||||
* ```
|
||||
* class X { }
|
||||
* export { X }
|
||||
* export { X as Y }
|
||||
* ```
|
||||
*/
|
||||
public get exportNames(): ReadonlySet<string> {
|
||||
if (!this._exportNamesSorted) {
|
||||
Sort.sortSet(this._exportNames);
|
||||
this._exportNamesSorted = true;
|
||||
}
|
||||
|
||||
return this._exportNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* If exportNames contains only one string, then singleExportName is that string.
|
||||
* In all other cases, it is undefined.
|
||||
*/
|
||||
public get singleExportName(): string | undefined {
|
||||
return this._singleExportName;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is true if exportNames contains only one string, and the declaration can be exported using the inline syntax
|
||||
* such as "export class X \{ \}" instead of "export \{ X \}".
|
||||
*/
|
||||
public get shouldInlineExport(): boolean {
|
||||
// We don't inline an AstImport
|
||||
return (
|
||||
this.astEntity instanceof AstSymbol && // We don't inline a symbol with more than one exported name
|
||||
this._singleExportName !== undefined &&
|
||||
this._singleExportName !== ts.InternalSymbolName.Default && // We can't inline a symbol whose emitted name is different from the export name
|
||||
(this._nameForEmit === undefined || this._nameForEmit === this._singleExportName)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that this entity is exported from the package entry point. Compare to `CollectorEntity.exported`.
|
||||
*/
|
||||
public get exportedFromEntryPoint(): boolean {
|
||||
return this.exportNames.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that this entity is exported from its parent module (i.e. either the package entry point or
|
||||
* a local namespace). Compare to `CollectorEntity.consumable`.
|
||||
*
|
||||
* @remarks
|
||||
* In the example below:
|
||||
*
|
||||
* ```ts
|
||||
* declare function add(): void;
|
||||
* declare namespace calculator {
|
||||
* export {
|
||||
* add
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Namespace `calculator` is neither exported nor consumable, function `add` is exported (from `calculator`)
|
||||
* but not consumable.
|
||||
*/
|
||||
public get exported(): boolean {
|
||||
// Exported from top-level?
|
||||
if (this.exportedFromEntryPoint) return true;
|
||||
|
||||
// Exported from parent?
|
||||
for (const localExportNames of this._localExportNamesByParent.values()) {
|
||||
if (localExportNames.size > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that it is possible for a consumer of the API to "consume" this entity, either by importing
|
||||
* it directly or via a namespace. If an entity is not consumable, then API Extractor will report an
|
||||
* `ae-forgotten-export` warning. Compare to `CollectorEntity.exported`.
|
||||
*
|
||||
* @remarks
|
||||
* An API item is consumable if:
|
||||
*
|
||||
* 1. It is exported from the top-level entry point OR
|
||||
* 2. It is exported from a consumable parent entity.
|
||||
*
|
||||
* For an example of #2, consider how `AstNamespaceImport` entities are processed. A generated rollup.d.ts
|
||||
* might look like this:
|
||||
*
|
||||
* ```ts
|
||||
* declare function add(): void;
|
||||
* declare namespace calculator {
|
||||
* export {
|
||||
* add
|
||||
* }
|
||||
* }
|
||||
* export { calculator }
|
||||
* ```
|
||||
*
|
||||
* In this example, `add` is exported via the consumable `calculator` namespace.
|
||||
*/
|
||||
public get consumable(): boolean {
|
||||
// Exported from top-level?
|
||||
if (this.exportedFromEntryPoint) return true;
|
||||
|
||||
// Exported from consumable parent?
|
||||
for (const [parent, localExportNames] of this._localExportNamesByParent) {
|
||||
if (localExportNames.size > 0 && parent.consumable) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first consumable parent that exports this entity. If there is none, returns
|
||||
* `undefined`.
|
||||
*/
|
||||
public getFirstExportingConsumableParent(): CollectorEntity | undefined {
|
||||
for (const [parent, localExportNames] of this._localExportNamesByParent) {
|
||||
if (parent.consumable && localExportNames.size > 0) {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new export name to the entity.
|
||||
*/
|
||||
public addExportName(exportName: string): void {
|
||||
if (!this._exportNames.has(exportName)) {
|
||||
this._exportNamesSorted = false;
|
||||
this._exportNames.add(exportName);
|
||||
|
||||
if (this._exportNames.size === 1) {
|
||||
this._singleExportName = exportName;
|
||||
} else {
|
||||
this._singleExportName = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new local export name to the entity.
|
||||
*
|
||||
* @remarks
|
||||
* In the example below:
|
||||
*
|
||||
* ```ts
|
||||
* declare function add(): void;
|
||||
* declare namespace calculator {
|
||||
* export {
|
||||
* add
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* `add` is the local export name for the `CollectorEntity` for `add`.
|
||||
*/
|
||||
public addLocalExportName(localExportName: string, parent: CollectorEntity): void {
|
||||
const localExportNames: Set<string> = this._localExportNamesByParent.get(parent) ?? new Set();
|
||||
localExportNames.add(localExportName);
|
||||
|
||||
this._localExportNamesByParent.set(parent, localExportNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* A sorting key used by DtsRollupGenerator._makeUniqueNames()
|
||||
*/
|
||||
public getSortKey(): string {
|
||||
if (!this._sortKey) {
|
||||
this._sortKey = Collector.getSortKeyIgnoringUnderscore(this.nameForEmit ?? this.astEntity.localName);
|
||||
}
|
||||
|
||||
return this._sortKey;
|
||||
}
|
||||
}
|
||||
49
packages/api-extractor/src/collector/DeclarationMetadata.ts
Normal file
49
packages/api-extractor/src/collector/DeclarationMetadata.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
import type * as tsdoc from '@microsoft/tsdoc';
|
||||
import type { AstDeclaration } from '../analyzer/AstDeclaration.js';
|
||||
|
||||
/**
|
||||
* Stores the Collector's additional analysis for a specific `AstDeclaration` signature. This object is assigned to
|
||||
* `AstDeclaration.declarationMetadata` but consumers must always obtain it by calling
|
||||
* `Collector.fetchDeclarationMetadata()`.
|
||||
*
|
||||
* Note that ancillary declarations share their `ApiItemMetadata` with the main declaration,
|
||||
* whereas a separate `DeclarationMetadata` object is created for each declaration.
|
||||
*/
|
||||
export abstract class DeclarationMetadata {
|
||||
/**
|
||||
* The ParserContext from when the TSDoc comment was parsed from the source code.
|
||||
* If the source code did not contain a doc comment, then this will be undefined.
|
||||
*
|
||||
* Note that if an ancillary declaration has a doc comment, it is tracked here, whereas
|
||||
* `ApiItemMetadata.tsdocComment` corresponds to documentation for the main declaration.
|
||||
*/
|
||||
public abstract readonly tsdocParserContext: tsdoc.ParserContext | undefined;
|
||||
|
||||
/**
|
||||
* If true, then this declaration is treated as part of another declaration.
|
||||
*/
|
||||
public abstract readonly isAncillary: boolean;
|
||||
|
||||
/**
|
||||
* A list of other declarations that are treated as being part of this declaration. For example, a property
|
||||
* getter/setter pair will be treated as a single API item, with the setter being treated as ancillary to the getter.
|
||||
*
|
||||
* If the `ancillaryDeclarations` array is non-empty, then `isAncillary` will be false for this declaration,
|
||||
* and `isAncillary` will be true for all the array items.
|
||||
*/
|
||||
public abstract readonly ancillaryDeclarations: readonly AstDeclaration[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Used internally by the `Collector` to build up `DeclarationMetadata`.
|
||||
*/
|
||||
export class InternalDeclarationMetadata extends DeclarationMetadata {
|
||||
public tsdocParserContext: tsdoc.ParserContext | undefined = undefined;
|
||||
|
||||
public isAncillary: boolean = false;
|
||||
|
||||
public ancillaryDeclarations: AstDeclaration[] = [];
|
||||
}
|
||||
648
packages/api-extractor/src/collector/MessageRouter.ts
Normal file
648
packages/api-extractor/src/collector/MessageRouter.ts
Normal file
@@ -0,0 +1,648 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
import type * as tsdoc from '@microsoft/tsdoc';
|
||||
import { Sort, InternalError } from '@rushstack/node-core-library';
|
||||
import colors from 'colors';
|
||||
import * as ts from 'typescript';
|
||||
import { AstDeclaration } from '../analyzer/AstDeclaration.js';
|
||||
import type { AstSymbol } from '../analyzer/AstSymbol.js';
|
||||
import { ConsoleMessageId } from '../api/ConsoleMessageId.js';
|
||||
import { ExtractorLogLevel } from '../api/ExtractorLogLevel.js';
|
||||
import {
|
||||
ExtractorMessage,
|
||||
ExtractorMessageCategory,
|
||||
type IExtractorMessageOptions,
|
||||
type IExtractorMessageProperties,
|
||||
} from '../api/ExtractorMessage.js';
|
||||
import { type ExtractorMessageId, allExtractorMessageIds } from '../api/ExtractorMessageId.js';
|
||||
import type { IExtractorMessagesConfig, IConfigMessageReportingRule } from '../api/IConfigFile.js';
|
||||
import type { ISourceLocation, SourceMapper } from './SourceMapper.js';
|
||||
|
||||
interface IReportingRule {
|
||||
addToApiReportFile: boolean;
|
||||
logLevel: ExtractorLogLevel;
|
||||
}
|
||||
|
||||
export interface IMessageRouterOptions {
|
||||
messageCallback: ((message: ExtractorMessage) => void) | undefined;
|
||||
messagesConfig: IExtractorMessagesConfig;
|
||||
showDiagnostics: boolean;
|
||||
showVerboseMessages: boolean;
|
||||
sourceMapper: SourceMapper;
|
||||
tsdocConfiguration: tsdoc.TSDocConfiguration;
|
||||
workingPackageFolder: string | undefined;
|
||||
}
|
||||
|
||||
export interface IBuildJsonDumpObjectOptions {
|
||||
/**
|
||||
* {@link MessageRouter.buildJsonDumpObject} will omit any objects keys with these names.
|
||||
*/
|
||||
keyNamesToOmit?: string[];
|
||||
}
|
||||
|
||||
export class MessageRouter {
|
||||
public static readonly DIAGNOSTICS_LINE: string = '============================================================';
|
||||
|
||||
private readonly _workingPackageFolder: string | undefined;
|
||||
|
||||
private readonly _messageCallback: ((message: ExtractorMessage) => void) | undefined;
|
||||
|
||||
// All messages
|
||||
private readonly _messages: ExtractorMessage[];
|
||||
|
||||
// For each AstDeclaration, the messages associated with it. This is used when addToApiReportFile=true
|
||||
private readonly _associatedMessagesForAstDeclaration: Map<AstDeclaration, ExtractorMessage[]>;
|
||||
|
||||
private readonly _sourceMapper: SourceMapper;
|
||||
|
||||
private readonly _tsdocConfiguration: tsdoc.TSDocConfiguration;
|
||||
|
||||
// Normalized representation of the routing rules from api-extractor.json
|
||||
private _reportingRuleByMessageId: Map<string, IReportingRule> = new Map<string, IReportingRule>();
|
||||
|
||||
private _compilerDefaultRule: IReportingRule = {
|
||||
logLevel: ExtractorLogLevel.None,
|
||||
addToApiReportFile: false,
|
||||
};
|
||||
|
||||
private _extractorDefaultRule: IReportingRule = {
|
||||
logLevel: ExtractorLogLevel.None,
|
||||
addToApiReportFile: false,
|
||||
};
|
||||
|
||||
private _tsdocDefaultRule: IReportingRule = { logLevel: ExtractorLogLevel.None, addToApiReportFile: false };
|
||||
|
||||
public errorCount: number = 0;
|
||||
|
||||
public warningCount: number = 0;
|
||||
|
||||
/**
|
||||
* See {@link IExtractorInvokeOptions.showVerboseMessages}
|
||||
*/
|
||||
public readonly showVerboseMessages: boolean;
|
||||
|
||||
/**
|
||||
* See {@link IExtractorInvokeOptions.showDiagnostics}
|
||||
*/
|
||||
public readonly showDiagnostics: boolean;
|
||||
|
||||
public constructor(options: IMessageRouterOptions) {
|
||||
this._workingPackageFolder = options.workingPackageFolder;
|
||||
this._messageCallback = options.messageCallback;
|
||||
|
||||
this._messages = [];
|
||||
this._associatedMessagesForAstDeclaration = new Map<AstDeclaration, ExtractorMessage[]>();
|
||||
this._sourceMapper = options.sourceMapper;
|
||||
this._tsdocConfiguration = options.tsdocConfiguration;
|
||||
|
||||
// showDiagnostics implies showVerboseMessages
|
||||
this.showVerboseMessages = options.showVerboseMessages || options.showDiagnostics;
|
||||
this.showDiagnostics = options.showDiagnostics;
|
||||
|
||||
this._applyMessagesConfig(options.messagesConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the api-extractor.json configuration and build up the tables of routing rules.
|
||||
*/
|
||||
private _applyMessagesConfig(messagesConfig: IExtractorMessagesConfig): void {
|
||||
if (messagesConfig.compilerMessageReporting) {
|
||||
for (const messageId of Object.getOwnPropertyNames(messagesConfig.compilerMessageReporting)) {
|
||||
const reportingRule: IReportingRule = MessageRouter._getNormalizedRule(
|
||||
messagesConfig.compilerMessageReporting[messageId]!,
|
||||
);
|
||||
|
||||
if (messageId === 'default') {
|
||||
this._compilerDefaultRule = reportingRule;
|
||||
} else if (/^TS\d+$/.test(messageId)) {
|
||||
this._reportingRuleByMessageId.set(messageId, reportingRule);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Error in API Extractor config: The messages.compilerMessageReporting table contains` +
|
||||
` an invalid entry "${messageId}". The identifier format is "TS" followed by an integer.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messagesConfig.extractorMessageReporting) {
|
||||
for (const messageId of Object.getOwnPropertyNames(messagesConfig.extractorMessageReporting)) {
|
||||
const reportingRule: IReportingRule = MessageRouter._getNormalizedRule(
|
||||
messagesConfig.extractorMessageReporting[messageId]!,
|
||||
);
|
||||
|
||||
if (messageId === 'default') {
|
||||
this._extractorDefaultRule = reportingRule;
|
||||
} else if (!messageId.startsWith('ae-')) {
|
||||
throw new Error(
|
||||
`Error in API Extractor config: The messages.extractorMessageReporting table contains` +
|
||||
` an invalid entry "${messageId}". The name should begin with the "ae-" prefix.`,
|
||||
);
|
||||
} else if (allExtractorMessageIds.has(messageId)) {
|
||||
this._reportingRuleByMessageId.set(messageId, reportingRule);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Error in API Extractor config: The messages.extractorMessageReporting table contains` +
|
||||
` an unrecognized identifier "${messageId}". Is it spelled correctly?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messagesConfig.tsdocMessageReporting) {
|
||||
for (const messageId of Object.getOwnPropertyNames(messagesConfig.tsdocMessageReporting)) {
|
||||
const reportingRule: IReportingRule = MessageRouter._getNormalizedRule(
|
||||
messagesConfig.tsdocMessageReporting[messageId]!,
|
||||
);
|
||||
|
||||
if (messageId === 'default') {
|
||||
this._tsdocDefaultRule = reportingRule;
|
||||
} else if (!messageId.startsWith('tsdoc-')) {
|
||||
throw new Error(
|
||||
`Error in API Extractor config: The messages.tsdocMessageReporting table contains` +
|
||||
` an invalid entry "${messageId}". The name should begin with the "tsdoc-" prefix.`,
|
||||
);
|
||||
} else if (this._tsdocConfiguration.isKnownMessageId(messageId)) {
|
||||
this._reportingRuleByMessageId.set(messageId, reportingRule);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Error in API Extractor config: The messages.tsdocMessageReporting table contains` +
|
||||
` an unrecognized identifier "${messageId}". Is it spelled correctly?`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static _getNormalizedRule(rule: IConfigMessageReportingRule): IReportingRule {
|
||||
return {
|
||||
logLevel: rule.logLevel || 'none',
|
||||
addToApiReportFile: rule.addToApiReportFile ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
public get messages(): readonly ExtractorMessage[] {
|
||||
return this._messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a diagnostic message reported by the TypeScript compiler
|
||||
*/
|
||||
public addCompilerDiagnostic(diagnostic: ts.Diagnostic): void {
|
||||
switch (diagnostic.category) {
|
||||
case ts.DiagnosticCategory.Suggestion:
|
||||
case ts.DiagnosticCategory.Message:
|
||||
return; // ignore noise
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const messageText: string = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
||||
const options: IExtractorMessageOptions = {
|
||||
category: ExtractorMessageCategory.Compiler,
|
||||
messageId: `TS${diagnostic.code}`,
|
||||
text: messageText,
|
||||
};
|
||||
|
||||
if (diagnostic.file) {
|
||||
// NOTE: Since compiler errors pertain to issues specific to the .d.ts files,
|
||||
// we do not apply source mappings for them.
|
||||
const sourceFile: ts.SourceFile = diagnostic.file;
|
||||
const sourceLocation: ISourceLocation = this._sourceMapper.getSourceLocation({
|
||||
sourceFile,
|
||||
pos: diagnostic.start ?? 0,
|
||||
useDtsLocation: true,
|
||||
});
|
||||
options.sourceFilePath = sourceLocation.sourceFilePath;
|
||||
options.sourceFileLine = sourceLocation.sourceFileLine;
|
||||
options.sourceFileColumn = sourceLocation.sourceFileColumn;
|
||||
}
|
||||
|
||||
this._messages.push(new ExtractorMessage(options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message from the API Extractor analysis
|
||||
*/
|
||||
public addAnalyzerIssue(
|
||||
messageId: ExtractorMessageId,
|
||||
messageText: string,
|
||||
astDeclarationOrSymbol: AstDeclaration | AstSymbol,
|
||||
properties?: IExtractorMessageProperties,
|
||||
): void {
|
||||
let astDeclaration: AstDeclaration;
|
||||
if (astDeclarationOrSymbol instanceof AstDeclaration) {
|
||||
astDeclaration = astDeclarationOrSymbol;
|
||||
} else {
|
||||
astDeclaration = astDeclarationOrSymbol.astDeclarations[0]!;
|
||||
}
|
||||
|
||||
const extractorMessage: ExtractorMessage = this.addAnalyzerIssueForPosition(
|
||||
messageId,
|
||||
messageText,
|
||||
astDeclaration.declaration.getSourceFile(),
|
||||
astDeclaration.declaration.getStart(),
|
||||
properties,
|
||||
);
|
||||
|
||||
this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add all messages produced from an invocation of the TSDoc parser, assuming they refer to
|
||||
* code in the specified source file.
|
||||
*/
|
||||
public addTsdocMessages(
|
||||
parserContext: tsdoc.ParserContext,
|
||||
sourceFile: ts.SourceFile,
|
||||
astDeclaration?: AstDeclaration,
|
||||
): void {
|
||||
for (const message of parserContext.log.messages) {
|
||||
const options: IExtractorMessageOptions = {
|
||||
category: ExtractorMessageCategory.TSDoc,
|
||||
messageId: message.messageId,
|
||||
text: message.unformattedText,
|
||||
};
|
||||
|
||||
const sourceLocation: ISourceLocation = this._sourceMapper.getSourceLocation({
|
||||
sourceFile,
|
||||
pos: message.textRange.pos,
|
||||
});
|
||||
options.sourceFilePath = sourceLocation.sourceFilePath;
|
||||
options.sourceFileLine = sourceLocation.sourceFileLine;
|
||||
options.sourceFileColumn = sourceLocation.sourceFileColumn;
|
||||
|
||||
const extractorMessage: ExtractorMessage = new ExtractorMessage(options);
|
||||
|
||||
if (astDeclaration) {
|
||||
this._associateMessageWithAstDeclaration(extractorMessage, astDeclaration);
|
||||
}
|
||||
|
||||
this._messages.push(extractorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collects the primitive members (numbers, strings, arrays, etc) into an object that
|
||||
* is JSON serializable. This is used by the "--diagnostics" feature to dump the state of configuration objects.
|
||||
*
|
||||
* @returns a JSON serializable object (possibly including `null` values)
|
||||
* or `undefined` if the input cannot be represented as JSON
|
||||
*/
|
||||
|
||||
public static buildJsonDumpObject(input: any, options?: IBuildJsonDumpObjectOptions): any | undefined {
|
||||
const ioptions = options ?? {};
|
||||
|
||||
const keyNamesToOmit: Set<string> = new Set(ioptions.keyNamesToOmit);
|
||||
|
||||
return MessageRouter._buildJsonDumpObject(input, keyNamesToOmit);
|
||||
}
|
||||
|
||||
private static _buildJsonDumpObject(input: any, keyNamesToOmit: Set<string>): any | undefined {
|
||||
if (input === null || input === undefined) {
|
||||
return null; // JSON uses null instead of undefined
|
||||
}
|
||||
|
||||
switch (typeof input) {
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
return input;
|
||||
case 'object':
|
||||
if (Array.isArray(input)) {
|
||||
const outputArray: any[] = [];
|
||||
for (const element of input) {
|
||||
const serializedElement: any = MessageRouter._buildJsonDumpObject(element, keyNamesToOmit);
|
||||
if (serializedElement !== undefined) {
|
||||
outputArray.push(serializedElement);
|
||||
}
|
||||
}
|
||||
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const outputObject: object = {};
|
||||
for (const key of Object.getOwnPropertyNames(input)) {
|
||||
if (keyNamesToOmit.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value: any = input[key];
|
||||
|
||||
const serializedValue: any = MessageRouter._buildJsonDumpObject(value, keyNamesToOmit);
|
||||
|
||||
if (serializedValue !== undefined) {
|
||||
(outputObject as any)[key] = serializedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return outputObject;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record this message in _associatedMessagesForAstDeclaration
|
||||
*/
|
||||
private _associateMessageWithAstDeclaration(
|
||||
extractorMessage: ExtractorMessage,
|
||||
astDeclaration: AstDeclaration,
|
||||
): void {
|
||||
let associatedMessages: ExtractorMessage[] | undefined =
|
||||
this._associatedMessagesForAstDeclaration.get(astDeclaration);
|
||||
|
||||
if (!associatedMessages) {
|
||||
associatedMessages = [];
|
||||
this._associatedMessagesForAstDeclaration.set(astDeclaration, associatedMessages);
|
||||
}
|
||||
|
||||
associatedMessages.push(extractorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message for a location in an arbitrary source file.
|
||||
*/
|
||||
public addAnalyzerIssueForPosition(
|
||||
messageId: ExtractorMessageId,
|
||||
messageText: string,
|
||||
sourceFile: ts.SourceFile,
|
||||
pos: number,
|
||||
properties?: IExtractorMessageProperties,
|
||||
): ExtractorMessage {
|
||||
const options: IExtractorMessageOptions = {
|
||||
category: ExtractorMessageCategory.Extractor,
|
||||
messageId,
|
||||
text: messageText,
|
||||
properties,
|
||||
};
|
||||
|
||||
const sourceLocation: ISourceLocation = this._sourceMapper.getSourceLocation({
|
||||
sourceFile,
|
||||
pos,
|
||||
});
|
||||
options.sourceFilePath = sourceLocation.sourceFilePath;
|
||||
options.sourceFileLine = sourceLocation.sourceFileLine;
|
||||
options.sourceFileColumn = sourceLocation.sourceFileColumn;
|
||||
|
||||
const extractorMessage: ExtractorMessage = new ExtractorMessage(options);
|
||||
|
||||
this._messages.push(extractorMessage);
|
||||
return extractorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used when writing the API report file. It looks up any messages that were configured to get emitted
|
||||
* in the API report file and returns them. It also records that they were emitted, which suppresses them from
|
||||
* being shown on the console.
|
||||
*/
|
||||
public fetchAssociatedMessagesForReviewFile(astDeclaration: AstDeclaration): ExtractorMessage[] {
|
||||
const messagesForApiReportFile: ExtractorMessage[] = [];
|
||||
|
||||
const associatedMessages: ExtractorMessage[] = this._associatedMessagesForAstDeclaration.get(astDeclaration) ?? [];
|
||||
for (const associatedMessage of associatedMessages) {
|
||||
// Make sure we didn't already report this message for some reason
|
||||
if (!associatedMessage.handled) {
|
||||
// Is this message type configured to go in the API report file?
|
||||
const reportingRule: IReportingRule = this._getRuleForMessage(associatedMessage);
|
||||
if (reportingRule.addToApiReportFile) {
|
||||
// Include it in the result, and record that it went to the API report file
|
||||
messagesForApiReportFile.push(associatedMessage);
|
||||
associatedMessage.handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._sortMessagesForOutput(messagesForApiReportFile);
|
||||
return messagesForApiReportFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns all remaining messages that were flagged with `addToApiReportFile`, but which were not
|
||||
* retreieved using `fetchAssociatedMessagesForReviewFile()`.
|
||||
*/
|
||||
public fetchUnassociatedMessagesForReviewFile(): ExtractorMessage[] {
|
||||
const messagesForApiReportFile: ExtractorMessage[] = [];
|
||||
|
||||
for (const unassociatedMessage of this.messages) {
|
||||
// Make sure we didn't already report this message for some reason
|
||||
if (!unassociatedMessage.handled) {
|
||||
// Is this message type configured to go in the API report file?
|
||||
const reportingRule: IReportingRule = this._getRuleForMessage(unassociatedMessage);
|
||||
if (reportingRule.addToApiReportFile) {
|
||||
// Include it in the result, and record that it went to the API report file
|
||||
messagesForApiReportFile.push(unassociatedMessage);
|
||||
unassociatedMessage.handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._sortMessagesForOutput(messagesForApiReportFile);
|
||||
return messagesForApiReportFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns the list of remaining messages that were not already processed by
|
||||
* `fetchAssociatedMessagesForReviewFile()` or `fetchUnassociatedMessagesForReviewFile()`.
|
||||
* These messages will be shown on the console.
|
||||
*/
|
||||
public handleRemainingNonConsoleMessages(): void {
|
||||
const messagesForLogger: ExtractorMessage[] = [];
|
||||
|
||||
for (const message of this.messages) {
|
||||
// Make sure we didn't already report this message
|
||||
if (!message.handled) {
|
||||
messagesForLogger.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
this._sortMessagesForOutput(messagesForLogger);
|
||||
|
||||
for (const message of messagesForLogger) {
|
||||
this._handleMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
public logError(messageId: ConsoleMessageId, message: string, properties?: IExtractorMessageProperties): void {
|
||||
this._handleMessage(
|
||||
new ExtractorMessage({
|
||||
category: ExtractorMessageCategory.Console,
|
||||
messageId,
|
||||
text: message,
|
||||
properties,
|
||||
logLevel: ExtractorLogLevel.Error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public logWarning(messageId: ConsoleMessageId, message: string, properties?: IExtractorMessageProperties): void {
|
||||
this._handleMessage(
|
||||
new ExtractorMessage({
|
||||
category: ExtractorMessageCategory.Console,
|
||||
messageId,
|
||||
text: message,
|
||||
properties,
|
||||
logLevel: ExtractorLogLevel.Warning,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public logInfo(messageId: ConsoleMessageId, message: string, properties?: IExtractorMessageProperties): void {
|
||||
this._handleMessage(
|
||||
new ExtractorMessage({
|
||||
category: ExtractorMessageCategory.Console,
|
||||
messageId,
|
||||
text: message,
|
||||
properties,
|
||||
logLevel: ExtractorLogLevel.Info,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public logVerbose(messageId: ConsoleMessageId, message: string, properties?: IExtractorMessageProperties): void {
|
||||
this._handleMessage(
|
||||
new ExtractorMessage({
|
||||
category: ExtractorMessageCategory.Console,
|
||||
messageId,
|
||||
text: message,
|
||||
properties,
|
||||
logLevel: ExtractorLogLevel.Verbose,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public logDiagnosticHeader(title: string): void {
|
||||
this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE);
|
||||
this.logDiagnostic(`DIAGNOSTIC: ` + title);
|
||||
this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE);
|
||||
}
|
||||
|
||||
public logDiagnosticFooter(): void {
|
||||
this.logDiagnostic(MessageRouter.DIAGNOSTICS_LINE + '\n');
|
||||
}
|
||||
|
||||
public logDiagnostic(message: string): void {
|
||||
if (this.showDiagnostics) {
|
||||
this.logVerbose(ConsoleMessageId.Diagnostics, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the calling application a chance to handle the `ExtractorMessage`, and if not, display it on the console.
|
||||
*/
|
||||
private _handleMessage(message: ExtractorMessage): void {
|
||||
// Don't tally messages that were already "handled" by writing them into the API report
|
||||
if (message.handled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign the ExtractorMessage.logLevel; the message callback may adjust it below
|
||||
if (message.category === ExtractorMessageCategory.Console) {
|
||||
// Console messages have their category log level assigned via logInfo(), logVerbose(), etc.
|
||||
} else {
|
||||
const reportingRule: IReportingRule = this._getRuleForMessage(message);
|
||||
message.logLevel = reportingRule.logLevel;
|
||||
}
|
||||
|
||||
// If there is a callback, allow it to modify and/or handle the message
|
||||
if (this._messageCallback) {
|
||||
this._messageCallback(message);
|
||||
}
|
||||
|
||||
// Update the statistics
|
||||
switch (message.logLevel) {
|
||||
case ExtractorLogLevel.Error:
|
||||
++this.errorCount;
|
||||
break;
|
||||
case ExtractorLogLevel.Warning:
|
||||
++this.warningCount;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (message.handled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The messageCallback did not handle the message, so perform default handling
|
||||
message.handled = true;
|
||||
|
||||
if (message.logLevel === ExtractorLogLevel.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
let messageText: string;
|
||||
if (message.category === ExtractorMessageCategory.Console) {
|
||||
messageText = message.text;
|
||||
} else {
|
||||
messageText = message.formatMessageWithLocation(this._workingPackageFolder);
|
||||
}
|
||||
|
||||
switch (message.logLevel) {
|
||||
case ExtractorLogLevel.Error:
|
||||
console.error(colors.red('Error: ' + messageText));
|
||||
break;
|
||||
case ExtractorLogLevel.Warning:
|
||||
console.warn(colors.yellow('Warning: ' + messageText));
|
||||
break;
|
||||
case ExtractorLogLevel.Info:
|
||||
console.log(messageText);
|
||||
break;
|
||||
case ExtractorLogLevel.Verbose:
|
||||
if (this.showVerboseMessages) {
|
||||
console.log(colors.cyan(messageText));
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid logLevel value: ${JSON.stringify(message.logLevel)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given message, determine the IReportingRule based on the rule tables.
|
||||
*/
|
||||
private _getRuleForMessage(message: ExtractorMessage): IReportingRule {
|
||||
const reportingRule: IReportingRule | undefined = this._reportingRuleByMessageId.get(message.messageId);
|
||||
if (reportingRule) {
|
||||
return reportingRule;
|
||||
}
|
||||
|
||||
switch (message.category) {
|
||||
case ExtractorMessageCategory.Compiler:
|
||||
return this._compilerDefaultRule;
|
||||
case ExtractorMessageCategory.Extractor:
|
||||
return this._extractorDefaultRule;
|
||||
case ExtractorMessageCategory.TSDoc:
|
||||
return this._tsdocDefaultRule;
|
||||
case ExtractorMessageCategory.Console:
|
||||
throw new InternalError('ExtractorMessageCategory.Console is not supported with IReportingRule');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts an array of messages according to a reasonable ordering
|
||||
*/
|
||||
private _sortMessagesForOutput(messages: ExtractorMessage[]): void {
|
||||
messages.sort((a, b) => {
|
||||
let diff: number;
|
||||
// First sort by file name
|
||||
diff = Sort.compareByValue(a.sourceFilePath, b.sourceFilePath);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Then sort by line number
|
||||
diff = Sort.compareByValue(a.sourceFileLine, b.sourceFileLine);
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
// Then sort by messageId
|
||||
return Sort.compareByValue(a.messageId, b.messageId);
|
||||
});
|
||||
}
|
||||
}
|
||||
258
packages/api-extractor/src/collector/SourceMapper.ts
Normal file
258
packages/api-extractor/src/collector/SourceMapper.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
// 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 { FileSystem, InternalError, JsonFile, NewlineKind } from '@rushstack/node-core-library';
|
||||
import { SourceMapConsumer, type RawSourceMap, type MappingItem, type Position } from 'source-map';
|
||||
import type ts from 'typescript';
|
||||
|
||||
interface ISourceMap {
|
||||
// SourceMapConsumer.originalPositionFor() is useless because the mapping contains numerous gaps,
|
||||
// and the API provides no way to find the nearest match. So instead we extract all the mapping items
|
||||
// and search them using SourceMapper._findNearestMappingItem().
|
||||
mappingItems: MappingItem[];
|
||||
|
||||
sourceMapConsumer: SourceMapConsumer;
|
||||
}
|
||||
|
||||
interface IOriginalFileInfo {
|
||||
// Whether the .ts file exists
|
||||
fileExists: boolean;
|
||||
|
||||
// This is used to check whether the guessed position is out of bounds.
|
||||
// Since column/line numbers are 1-based, the 0th item in this array is unused.
|
||||
maxColumnForLine: number[];
|
||||
}
|
||||
|
||||
export interface ISourceLocation {
|
||||
/**
|
||||
* The column number in the source file. The first column number is 1.
|
||||
*/
|
||||
sourceFileColumn: number;
|
||||
|
||||
/**
|
||||
* The line number in the source file. The first line number is 1.
|
||||
*/
|
||||
sourceFileLine: number;
|
||||
|
||||
/**
|
||||
* The absolute path to the source file.
|
||||
*/
|
||||
sourceFilePath: string;
|
||||
}
|
||||
|
||||
export interface IGetSourceLocationOptions {
|
||||
/**
|
||||
* The position within the source file to get the source location from.
|
||||
*/
|
||||
pos: number;
|
||||
|
||||
/**
|
||||
* The source file to get the source location from.
|
||||
*/
|
||||
sourceFile: ts.SourceFile;
|
||||
|
||||
/**
|
||||
* If `false` or not provided, then we attempt to follow source maps in order to resolve the
|
||||
* location to the original `.ts` file. If resolution isn't possible for some reason, we fall
|
||||
* back to the `.d.ts` location.
|
||||
*
|
||||
* If `true`, then we don't bother following source maps, and the location refers to the `.d.ts`
|
||||
* location.
|
||||
*/
|
||||
useDtsLocation?: boolean;
|
||||
}
|
||||
|
||||
export class SourceMapper {
|
||||
// Map from .d.ts file path --> ISourceMap if a source map was found, or null if not found
|
||||
private _sourceMapByFilePath: Map<string, ISourceMap | null> = new Map<string, ISourceMap | null>();
|
||||
|
||||
// Cache the FileSystem.exists() result for mapped .ts files
|
||||
private _originalFileInfoByPath: Map<string, IOriginalFileInfo> = new Map<string, IOriginalFileInfo>();
|
||||
|
||||
/**
|
||||
* Given a `.d.ts` source file and a specific position within the file, return the corresponding
|
||||
* `ISourceLocation`.
|
||||
*/
|
||||
public getSourceLocation(options: IGetSourceLocationOptions): ISourceLocation {
|
||||
const lineAndCharacter: ts.LineAndCharacter = options.sourceFile.getLineAndCharacterOfPosition(options.pos);
|
||||
const sourceLocation: ISourceLocation = {
|
||||
sourceFilePath: options.sourceFile.fileName,
|
||||
sourceFileLine: lineAndCharacter.line + 1,
|
||||
sourceFileColumn: lineAndCharacter.character + 1,
|
||||
};
|
||||
|
||||
if (options.useDtsLocation) {
|
||||
return sourceLocation;
|
||||
}
|
||||
|
||||
const mappedSourceLocation: ISourceLocation | undefined = this._getMappedSourceLocation(sourceLocation);
|
||||
return mappedSourceLocation ?? sourceLocation;
|
||||
}
|
||||
|
||||
private _getMappedSourceLocation(sourceLocation: ISourceLocation): ISourceLocation | undefined {
|
||||
const { sourceFilePath, sourceFileLine, sourceFileColumn } = sourceLocation;
|
||||
|
||||
if (!FileSystem.exists(sourceFilePath)) {
|
||||
// Sanity check
|
||||
throw new InternalError('The referenced path was not found: ' + sourceFilePath);
|
||||
}
|
||||
|
||||
const sourceMap: ISourceMap | null = this._getSourceMap(sourceFilePath);
|
||||
if (!sourceMap) return;
|
||||
|
||||
const nearestMappingItem: MappingItem | undefined = SourceMapper._findNearestMappingItem(sourceMap.mappingItems, {
|
||||
line: sourceFileLine,
|
||||
column: sourceFileColumn,
|
||||
});
|
||||
|
||||
if (!nearestMappingItem) return;
|
||||
|
||||
const mappedFilePath: string = path.resolve(path.dirname(sourceFilePath), nearestMappingItem.source);
|
||||
|
||||
// Does the mapped filename exist? Use a cache to remember the answer.
|
||||
let originalFileInfo: IOriginalFileInfo | undefined = this._originalFileInfoByPath.get(mappedFilePath);
|
||||
if (originalFileInfo === undefined) {
|
||||
originalFileInfo = {
|
||||
fileExists: FileSystem.exists(mappedFilePath),
|
||||
maxColumnForLine: [],
|
||||
};
|
||||
|
||||
if (originalFileInfo.fileExists) {
|
||||
// Read the file and measure the length of each line
|
||||
originalFileInfo.maxColumnForLine = FileSystem.readFile(mappedFilePath, {
|
||||
convertLineEndings: NewlineKind.Lf,
|
||||
})
|
||||
.split('\n')
|
||||
.map((x) => x.length + 1); // +1 since columns are 1-based
|
||||
originalFileInfo.maxColumnForLine.unshift(0); // Extra item since lines are 1-based
|
||||
}
|
||||
|
||||
this._originalFileInfoByPath.set(mappedFilePath, originalFileInfo);
|
||||
}
|
||||
|
||||
// Don't translate coordinates to a file that doesn't exist
|
||||
if (!originalFileInfo.fileExists) return;
|
||||
|
||||
// The nearestMappingItem anchor may be above/left of the real position, due to gaps in the mapping. Calculate
|
||||
// the delta and apply it to the original position.
|
||||
const guessedPosition: Position = {
|
||||
line: nearestMappingItem.originalLine + sourceFileLine - nearestMappingItem.generatedLine,
|
||||
column: nearestMappingItem.originalColumn + sourceFileColumn - nearestMappingItem.generatedColumn,
|
||||
};
|
||||
|
||||
// Verify that the result is not out of bounds, in cause our heuristic failed
|
||||
if (
|
||||
guessedPosition.line >= 1 &&
|
||||
guessedPosition.line < originalFileInfo.maxColumnForLine.length &&
|
||||
guessedPosition.column >= 1 &&
|
||||
guessedPosition.column <= originalFileInfo.maxColumnForLine[guessedPosition.line]!
|
||||
) {
|
||||
return {
|
||||
sourceFilePath: mappedFilePath,
|
||||
sourceFileLine: guessedPosition.line,
|
||||
sourceFileColumn: guessedPosition.column,
|
||||
};
|
||||
} else {
|
||||
// The guessed position was out of bounds, so use the nearestMappingItem position instead.
|
||||
return {
|
||||
sourceFilePath: mappedFilePath,
|
||||
sourceFileLine: nearestMappingItem.originalLine,
|
||||
sourceFileColumn: nearestMappingItem.originalColumn,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _getSourceMap(sourceFilePath: string): ISourceMap | null {
|
||||
let sourceMap: ISourceMap | null | undefined = this._sourceMapByFilePath.get(sourceFilePath);
|
||||
|
||||
if (sourceMap === undefined) {
|
||||
// Normalize the path and redo the lookup
|
||||
const normalizedPath: string = FileSystem.getRealPath(sourceFilePath);
|
||||
|
||||
sourceMap = this._sourceMapByFilePath.get(normalizedPath);
|
||||
if (sourceMap === undefined) {
|
||||
// Given "folder/file.d.ts", check for a corresponding "folder/file.d.ts.map"
|
||||
const sourceMapPath: string = normalizedPath + '.map';
|
||||
if (FileSystem.exists(sourceMapPath)) {
|
||||
// Load up the source map
|
||||
const rawSourceMap: RawSourceMap = JsonFile.load(sourceMapPath) as RawSourceMap;
|
||||
|
||||
const sourceMapConsumer: SourceMapConsumer = new SourceMapConsumer(rawSourceMap);
|
||||
const mappingItems: MappingItem[] = [];
|
||||
|
||||
// Extract the list of mapping items
|
||||
sourceMapConsumer.eachMapping(
|
||||
(mappingItem: MappingItem) => {
|
||||
mappingItems.push({
|
||||
...mappingItem,
|
||||
// The "source-map" package inexplicably uses 1-based line numbers but 0-based column numbers.
|
||||
// Fix that up proactively so we don't have to deal with it later.
|
||||
generatedColumn: mappingItem.generatedColumn + 1,
|
||||
originalColumn: mappingItem.originalColumn + 1,
|
||||
});
|
||||
},
|
||||
this,
|
||||
SourceMapConsumer.GENERATED_ORDER,
|
||||
);
|
||||
|
||||
sourceMap = { sourceMapConsumer, mappingItems };
|
||||
} else {
|
||||
// No source map for this filename
|
||||
sourceMap = null;
|
||||
}
|
||||
|
||||
this._sourceMapByFilePath.set(normalizedPath, sourceMap);
|
||||
if (sourceFilePath !== normalizedPath) {
|
||||
// Add both keys to the map
|
||||
this._sourceMapByFilePath.set(sourceFilePath, sourceMap);
|
||||
}
|
||||
} else {
|
||||
// Copy the result from the normalized to the non-normalized key
|
||||
this._sourceMapByFilePath.set(sourceFilePath, sourceMap);
|
||||
}
|
||||
}
|
||||
|
||||
return sourceMap;
|
||||
}
|
||||
|
||||
// The `mappingItems` array is sorted by generatedLine/generatedColumn (GENERATED_ORDER).
|
||||
// The _findNearestMappingItem() lookup is a simple binary search that returns the previous item
|
||||
// if there is no exact match.
|
||||
private static _findNearestMappingItem(mappingItems: MappingItem[], position: Position): MappingItem | undefined {
|
||||
if (mappingItems.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let startIndex = 0;
|
||||
let endIndex: number = mappingItems.length - 1;
|
||||
|
||||
while (startIndex <= endIndex) {
|
||||
const middleIndex: number = startIndex + Math.floor((endIndex - startIndex) / 2);
|
||||
|
||||
const diff: number = SourceMapper._compareMappingItem(mappingItems[middleIndex]!, position);
|
||||
|
||||
if (diff < 0) {
|
||||
startIndex = middleIndex + 1;
|
||||
} else if (diff > 0) {
|
||||
endIndex = middleIndex - 1;
|
||||
} else {
|
||||
// Exact match
|
||||
return mappingItems[middleIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find an exact match, then endIndex < startIndex.
|
||||
// Take endIndex because it's the smaller value.
|
||||
return mappingItems[endIndex];
|
||||
}
|
||||
|
||||
private static _compareMappingItem(mappingItem: MappingItem, position: Position): number {
|
||||
const diff: number = mappingItem.generatedLine - position.line;
|
||||
if (diff !== 0) {
|
||||
return diff;
|
||||
}
|
||||
|
||||
return mappingItem.generatedColumn - position.column;
|
||||
}
|
||||
}
|
||||
25
packages/api-extractor/src/collector/SymbolMetadata.ts
Normal file
25
packages/api-extractor/src/collector/SymbolMetadata.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
import type { ReleaseTag } from '@discordjs/api-extractor-model';
|
||||
|
||||
/**
|
||||
* Constructor parameters for `SymbolMetadata`.
|
||||
*/
|
||||
export interface ISymbolMetadataOptions {
|
||||
maxEffectiveReleaseTag: ReleaseTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the Collector's additional analysis for an `AstSymbol`. This object is assigned to `AstSymbol.metadata`
|
||||
* but consumers must always obtain it by calling `Collector.fetchSymbolMetadata()`.
|
||||
*/
|
||||
export class SymbolMetadata {
|
||||
// For all declarations associated with this symbol, this is the
|
||||
// `ApiItemMetadata.effectiveReleaseTag` value that is most public.
|
||||
public readonly maxEffectiveReleaseTag: ReleaseTag;
|
||||
|
||||
public constructor(options: ISymbolMetadataOptions) {
|
||||
this.maxEffectiveReleaseTag = options.maxEffectiveReleaseTag;
|
||||
}
|
||||
}
|
||||
24
packages/api-extractor/src/collector/VisitorState.ts
Normal file
24
packages/api-extractor/src/collector/VisitorState.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
/**
|
||||
* Keeps track of a directed graph traversal that needs to detect cycles.
|
||||
*/
|
||||
export enum VisitorState {
|
||||
/**
|
||||
* We have not visited the node yet.
|
||||
*/
|
||||
Unvisited = 0,
|
||||
|
||||
/**
|
||||
* We have visited the node, but have not finished traversing its references yet.
|
||||
* If we reach a node that is already in the `Visiting` state, this means we have
|
||||
* encountered a cyclic reference.
|
||||
*/
|
||||
Visiting = 1,
|
||||
|
||||
/**
|
||||
* We are finished vising the node and all its references.
|
||||
*/
|
||||
Visited = 2,
|
||||
}
|
||||
78
packages/api-extractor/src/collector/WorkingPackage.ts
Normal file
78
packages/api-extractor/src/collector/WorkingPackage.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
import type * as tsdoc from '@microsoft/tsdoc';
|
||||
import type { INodePackageJson } from '@rushstack/node-core-library';
|
||||
import type * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Constructor options for WorkingPackage
|
||||
*/
|
||||
export interface IWorkingPackageOptions {
|
||||
entryPointSourceFile: ts.SourceFile;
|
||||
packageFolder: string;
|
||||
packageJson: INodePackageJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the working package for a particular invocation of API Extractor.
|
||||
*
|
||||
* @remarks
|
||||
* API Extractor tries to model the world as a collection of NPM packages, such that each
|
||||
* .d.ts file belongs to at most one package. When API Extractor is invoked on a project,
|
||||
* we refer to that project as being the "working package". There is exactly one
|
||||
* "working package" for the duration of this analysis. Any files that do not belong to
|
||||
* the working package are referred to as "external": external declarations belonging to
|
||||
* external packages.
|
||||
*
|
||||
* If API Extractor is invoked on a standalone .d.ts file, the "working package" may not
|
||||
* have an actual package.json file on disk, but we still refer to it in concept.
|
||||
*/
|
||||
export class WorkingPackage {
|
||||
/**
|
||||
* Returns the folder for the package.json file of the working package.
|
||||
*
|
||||
* @remarks
|
||||
* If the entry point is `C:\Folder\project\src\index.ts` and the nearest package.json
|
||||
* is `C:\Folder\project\package.json`, then the packageFolder is `C:\Folder\project`
|
||||
*/
|
||||
public readonly packageFolder: string;
|
||||
|
||||
/**
|
||||
* The parsed package.json file for the working package.
|
||||
*/
|
||||
public readonly packageJson: INodePackageJson;
|
||||
|
||||
/**
|
||||
* The entry point being processed during this invocation of API Extractor.
|
||||
*
|
||||
* @remarks
|
||||
* The working package may have multiple entry points; however, today API Extractor
|
||||
* only processes a single entry point during an invocation. This will be improved
|
||||
* in the future.
|
||||
*/
|
||||
public readonly entryPointSourceFile: ts.SourceFile;
|
||||
|
||||
/**
|
||||
* The `@packageDocumentation` comment, if any, for the working package.
|
||||
*/
|
||||
public tsdocComment: tsdoc.DocComment | undefined;
|
||||
|
||||
/**
|
||||
* Additional parser information for `WorkingPackage.tsdocComment`.
|
||||
*/
|
||||
public tsdocParserContext: tsdoc.ParserContext | undefined;
|
||||
|
||||
public constructor(options: IWorkingPackageOptions) {
|
||||
this.packageFolder = options.packageFolder;
|
||||
this.packageJson = options.packageJson;
|
||||
this.entryPointSourceFile = options.entryPointSourceFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full name of the working package.
|
||||
*/
|
||||
public get name(): string {
|
||||
return this.packageJson.name;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user