Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-extractor] Add options to include forgotten exports in API report and doc model files #3552

Merged
merged 9 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions apps/api-extractor/src/api/ExtractorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,10 @@ interface IExtractorConfigParameters {
apiReportEnabled: boolean;
reportFilePath: string;
reportTempFilePath: string;
apiReportIncludeForgottenExports: boolean;
docModelEnabled: boolean;
apiJsonFilePath: string;
docModelIncludeForgottenExports: boolean;
rollupEnabled: boolean;
untrimmedFilePath: string;
alphaTrimmedFilePath: string;
Expand Down Expand Up @@ -246,11 +248,15 @@ export class ExtractorConfig {
public readonly reportFilePath: string;
/** The `reportTempFolder` path combined with the `reportFileName`. */
public readonly reportTempFilePath: string;
/** {@inheritDoc IConfigApiReport.includeForgottenExports} */
public readonly apiReportIncludeForgottenExports: boolean;

/** {@inheritDoc IConfigDocModel.enabled} */
public readonly docModelEnabled: boolean;
/** {@inheritDoc IConfigDocModel.apiJsonFilePath} */
public readonly apiJsonFilePath: string;
/** {@inheritDoc IConfigDocModel.includeForgottenExports} */
public readonly docModelIncludeForgottenExports: boolean;

/** {@inheritDoc IConfigDtsRollup.enabled} */
public readonly rollupEnabled: boolean;
Expand Down Expand Up @@ -307,8 +313,10 @@ export class ExtractorConfig {
this.apiReportEnabled = parameters.apiReportEnabled;
this.reportFilePath = parameters.reportFilePath;
this.reportTempFilePath = parameters.reportTempFilePath;
this.apiReportIncludeForgottenExports = parameters.apiReportIncludeForgottenExports;
this.docModelEnabled = parameters.docModelEnabled;
this.apiJsonFilePath = parameters.apiJsonFilePath;
this.docModelIncludeForgottenExports = parameters.docModelIncludeForgottenExports;
this.rollupEnabled = parameters.rollupEnabled;
this.untrimmedFilePath = parameters.untrimmedFilePath;
this.alphaTrimmedFilePath = parameters.alphaTrimmedFilePath;
Expand Down Expand Up @@ -848,6 +856,7 @@ export class ExtractorConfig {
let apiReportEnabled: boolean = false;
let reportFilePath: string = '';
let reportTempFilePath: string = '';
let apiReportIncludeForgottenExports: boolean = false;
if (configObject.apiReport) {
apiReportEnabled = !!configObject.apiReport.enabled;

Expand Down Expand Up @@ -879,17 +888,20 @@ export class ExtractorConfig {

reportFilePath = path.join(reportFolder, reportFilename);
reportTempFilePath = path.join(reportTempFolder, reportFilename);
apiReportIncludeForgottenExports = !!configObject.apiReport.includeForgottenExports;
}

let docModelEnabled: boolean = false;
let apiJsonFilePath: string = '';
let docModelIncludeForgottenExports: boolean = false;
if (configObject.docModel) {
docModelEnabled = !!configObject.docModel.enabled;
apiJsonFilePath = ExtractorConfig._resolvePathWithTokens(
'apiJsonFilePath',
configObject.docModel.apiJsonFilePath,
tokenContext
);
docModelIncludeForgottenExports = !!configObject.docModel.includeForgottenExports;
}

let tsdocMetadataEnabled: boolean = false;
Expand Down Expand Up @@ -993,8 +1005,10 @@ export class ExtractorConfig {
apiReportEnabled,
reportFilePath,
reportTempFilePath,
apiReportIncludeForgottenExports,
docModelEnabled,
apiJsonFilePath,
docModelIncludeForgottenExports,
rollupEnabled,
untrimmedFilePath,
alphaTrimmedFilePath,
Expand Down
2 changes: 1 addition & 1 deletion apps/api-extractor/src/api/ExtractorMessageId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const enum ExtractorMessageId {
IncompatibleReleaseTags = 'ae-incompatible-release-tags',

/**
* "___ is exported by the package, but it is missing a release tag (`@alpha`, `@beta`, `@public`, or `@internal`)."
* "___ is part of the package's API, but it is missing a release tag (`@alpha`, `@beta`, `@public`, or `@internal`)."
*/
MissingReleaseTag = 'ae-missing-release-tag',

Expand Down
24 changes: 23 additions & 1 deletion apps/api-extractor/src/api/IConfigFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ export interface IConfigApiReport {
* prepend a folder token such as `<projectFolder>`.
*/
reportTempFolder?: string;

/**
* Whether "forgotten exports" should be included in the API report file.
*
* @remarks
* Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See
* https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more.
*
* @defaultValue `false`
*/
includeForgottenExports?: boolean;
}

/**
Expand All @@ -120,6 +131,17 @@ export interface IConfigDocModel {
* prepend a folder token such as `<projectFolder>`.
*/
apiJsonFilePath?: string;

/**
* Whether "forgotten exports" should be included in the doc model file.
*
* @remarks
* Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See
* https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more.
*
* @defaultValue `false`
*/
includeForgottenExports?: boolean;
}

/**
Expand Down Expand Up @@ -376,7 +398,7 @@ export interface IConfigFile {
testMode?: boolean;

/**
* Specifies how API Extractor sorts members of an enum when generating api.json.
* Specifies how API Extractor sorts members of an enum when generating the .api.json file.
*
* @remarks
* By default, the output files will be sorted alphabetically, which is "by-name".
Expand Down
89 changes: 49 additions & 40 deletions apps/api-extractor/src/collector/Collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,28 +246,23 @@ export class Collector {
this.workingPackage.tsdocComment = this.workingPackage.tsdocParserContext!.docComment;
}

const exportedAstEntities: AstEntity[] = [];

// Create a CollectorEntity for each top-level export

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);

exportedAstEntities.push(astEntity);
processedAstEntities.push(astEntity);
}

// Create a CollectorEntity for each indirectly referenced export.
// Note that we do this *after* the above loop, so that references to exported AstSymbols
// are encountered first as exports.
const alreadySeenAstSymbols: Set<AstSymbol> = new Set<AstSymbol>();
for (const exportedAstEntity of exportedAstEntities) {
this._createEntityForIndirectReferences(exportedAstEntity, alreadySeenAstSymbols);

if (exportedAstEntity instanceof AstSymbol) {
this.fetchSymbolMetadata(exportedAstEntity);
// 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);
}
}

Expand Down Expand Up @@ -414,7 +409,11 @@ export class Collector {
return overloadIndex;
}

private _createCollectorEntity(astEntity: AstEntity, exportedName: string | undefined): CollectorEntity {
private _createCollectorEntity(
astEntity: AstEntity,
exportName?: string,
parent?: CollectorEntity
): CollectorEntity {
let entity: CollectorEntity | undefined = this._entitiesByAstEntity.get(astEntity);

if (!entity) {
Expand All @@ -425,50 +424,54 @@ export class Collector {
this._collectReferenceDirectives(astEntity);
}

if (exportedName) {
entity.addExportName(exportedName);
if (exportName) {
if (parent) {
entity.addLocalExportName(exportName, parent);
} else {
entity.addExportName(exportName);
}
}

return entity;
}

private _createEntityForIndirectReferences(
astEntity: AstEntity,
alreadySeenAstEntities: Set<AstEntity>
): void {
if (alreadySeenAstEntities.has(astEntity)) {
return;
}
private _recursivelyCreateEntities(astEntity: AstEntity, alreadySeenAstEntities: Set<AstEntity>): void {
octogonz marked this conversation as resolved.
Show resolved Hide resolved
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 symbols is nested inside a namespace, only the root-level namespace
// get a collector entity
// 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, undefined);
this._createCollectorEntity(referencedAstEntity);
}
octogonz marked this conversation as resolved.
Show resolved Hide resolved
} else {
this._createCollectorEntity(referencedAstEntity, undefined);
this._createCollectorEntity(referencedAstEntity);
}

this._createEntityForIndirectReferences(referencedAstEntity, alreadySeenAstEntities);
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 exportedEntity of astModuleExportInfo.exportedLocalEntities.values()) {
// Create a CollectorEntity for each top-level export of AstImportInternal entity
const entity: CollectorEntity = this._createCollectorEntity(exportedEntity, undefined);
entity.addAstNamespaceImports(astEntity);

this._createEntityForIndirectReferences(exportedEntity, alreadySeenAstEntities);
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);
}
}
}
Expand Down Expand Up @@ -812,16 +815,22 @@ export class Collector {
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
// 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) {
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
if (astSymbol.rootAstSymbol.localName !== '_default') {
this.messageRouter.addAnalyzerIssue(
ExtractorMessageId.MissingReleaseTag,
`"${entity.astEntity.localName}" is exported by the package, but it is missing ` +
`"${entity.astEntity.localName}" is part of the package's API, but it is missing ` +
`a release tag (@alpha, @beta, @public, or @internal)`,
astSymbol
);
Expand Down
Loading