Skip to content

Commit

Permalink
Implement the missing ae-forgotten-export analysis for AstNamespaceIm…
Browse files Browse the repository at this point in the history
…port references
  • Loading branch information
octogonz committed Jun 30, 2021
1 parent 51acb54 commit 12799aa
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 66 deletions.
8 changes: 7 additions & 1 deletion apps/api-extractor/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@
"name": "scenario",
"program": "${workspaceFolder}/lib/start.js",
"cwd": "${workspaceFolder}/../../build-tests/api-extractor-scenarios",
"args": ["--debug", "run", "--local", "--config", "./temp/configs/api-extractor-typeof.json"],
"args": [
"--debug",
"run",
"--local",
"--config",
"./temp/configs/api-extractor-exportImportStarAs2.json"
],
"sourceMaps": true
}
]
Expand Down
10 changes: 9 additions & 1 deletion apps/api-extractor/src/analyzer/AstNamespaceImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

import * as ts from 'typescript';

import { AstModule } from './AstModule';
import { AstModule, AstModuleExportInfo } from './AstModule';
import { AstSyntheticEntity } from './AstEntity';
import { Collector } from '../collector/Collector';

export interface IAstNamespaceImportOptions {
readonly astModule: AstModule;
Expand Down Expand Up @@ -78,4 +79,11 @@ export class AstNamespaceImport extends AstSyntheticEntity {
// abstract
return this.namespaceName;
}

public fetchAstModuleExportInfo(collector: Collector): AstModuleExportInfo {
const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(
this.astModule
);
return astModuleExportInfo;
}
}
7 changes: 2 additions & 5 deletions apps/api-extractor/src/collector/Collector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ export class Collector {

const astModuleExportInfo: AstModuleExportInfo =
this.astSymbolTable.fetchAstModuleExportInfo(astEntryPoint);

for (const [exportName, astEntity] of astModuleExportInfo.exportedLocalEntities) {
this._createCollectorEntity(astEntity, exportName);

Expand Down Expand Up @@ -440,18 +441,14 @@ export class Collector {
}

if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = this.astSymbolTable.fetchAstModuleExportInfo(
astEntity.astModule
);
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(this);

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

// TODO - create entity for module export
}
}
}
Expand Down
147 changes: 94 additions & 53 deletions apps/api-extractor/src/enhancers/ValidationEnhancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,52 @@ import { CollectorEntity } from '../collector/CollectorEntity';
import { ExtractorMessageId } from '../api/ExtractorMessageId';
import { ReleaseTag } from '@microsoft/api-extractor-model';
import { AstNamespaceImport } from '../analyzer/AstNamespaceImport';
import { AstModuleExportInfo } from '../analyzer/AstModule';
import { AstEntity } from '../analyzer/AstEntity';

export class ValidationEnhancer {
public static analyze(collector: Collector): void {
const alreadyWarnedSymbols: Set<AstSymbol> = new Set<AstSymbol>();
const alreadyWarnedEntities: Set<AstEntity> = new Set<AstEntity>();

for (const entity of collector.entities) {
if (entity.astEntity instanceof AstSymbol) {
if (entity.consumable) {
entity.astEntity.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedSymbols);
});

const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(entity.astEntity);
ValidationEnhancer._checkForInternalUnderscore(collector, entity, entity.astEntity, symbolMetadata);
ValidationEnhancer._checkForInconsistentReleaseTags(collector, entity.astEntity, symbolMetadata);
}
if (!entity.consumable) {
continue;
}

if (entity.astEntity instanceof AstNamespaceImport) {
// TODO [MA]: validation for local module import
if (entity.astEntity instanceof AstSymbol) {
// A regular exported AstSymbol

const astSymbol: AstSymbol = entity.astEntity;

astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
});

const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
ValidationEnhancer._checkForInternalUnderscore(collector, entity, astSymbol, symbolMetadata);
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
} else if (entity.astEntity instanceof AstNamespaceImport) {
// A namespace created using "import * as ___ from ___"
const astNamespaceImport: AstNamespaceImport = entity.astEntity;

const astModuleExportInfo: AstModuleExportInfo =
astNamespaceImport.fetchAstModuleExportInfo(collector);

for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) {
if (namespaceMemberAstEntity instanceof AstSymbol) {
const astSymbol: AstSymbol = namespaceMemberAstEntity;

astSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
});

const symbolMetadata: SymbolMetadata = collector.fetchSymbolMetadata(astSymbol);

// (Don't apply ValidationEnhancer._checkForInternalUnderscore() for AstNamespaceImport members)

ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
}
}
}
}
}
Expand Down Expand Up @@ -163,63 +189,78 @@ export class ValidationEnhancer {
private static _checkReferences(
collector: Collector,
astDeclaration: AstDeclaration,
alreadyWarnedSymbols: Set<AstSymbol>
alreadyWarnedEntities: Set<AstEntity>
): void {
const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
const declarationReleaseTag: ReleaseTag = apiItemMetadata.effectiveReleaseTag;

for (const referencedEntity of astDeclaration.referencedAstEntities) {
let collectorEntity: CollectorEntity | undefined;
let referencedReleaseTag: ReleaseTag;
let localName: string;

if (referencedEntity instanceof AstSymbol) {
// If this is e.g. a member of a namespace, then we need to be checking the top-level scope to see
// whether it's exported.
//
// TODO: Technically we should also check each of the nested scopes along the way.
const rootSymbol: AstSymbol = referencedEntity.rootAstSymbol;

if (!rootSymbol.isExternal) {
// TODO: consider exported by local module import
const collectorEntity: CollectorEntity | undefined = collector.tryGetCollectorEntity(rootSymbol);

if (collectorEntity && collectorEntity.consumable) {
const referencedMetadata: SymbolMetadata = collector.fetchSymbolMetadata(referencedEntity);
const referencedReleaseTag: ReleaseTag = referencedMetadata.maxEffectiveReleaseTag;

if (ReleaseTag.compare(declarationReleaseTag, referencedReleaseTag) > 0) {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.IncompatibleReleaseTags,
`The symbol "${astDeclaration.astSymbol.localName}"` +
` is marked as ${ReleaseTag.getTagName(declarationReleaseTag)},` +
` but its signature references "${referencedEntity.localName}"` +
` which is marked as ${ReleaseTag.getTagName(referencedReleaseTag)}`,
astDeclaration
);
}
if (rootSymbol.isExternal) {
continue;
}

localName = rootSymbol.localName;

collectorEntity = collector.tryGetCollectorEntity(rootSymbol);

const referencedMetadata: SymbolMetadata = collector.fetchSymbolMetadata(referencedEntity);
referencedReleaseTag = referencedMetadata.maxEffectiveReleaseTag;
} else if (referencedEntity instanceof AstNamespaceImport) {
collectorEntity = collector.tryGetCollectorEntity(referencedEntity);

// TODO: Currently the "import * as ___ from ___" syntax does not yet support doc comments
referencedReleaseTag = ReleaseTag.Public;

localName = referencedEntity.localName;
} else {
continue;
}

if (collectorEntity && collectorEntity.consumable) {
if (ReleaseTag.compare(declarationReleaseTag, referencedReleaseTag) > 0) {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.IncompatibleReleaseTags,
`The symbol "${astDeclaration.astSymbol.localName}"` +
` is marked as ${ReleaseTag.getTagName(declarationReleaseTag)},` +
` but its signature references "${referencedEntity.localName}"` +
` which is marked as ${ReleaseTag.getTagName(referencedReleaseTag)}`,
astDeclaration
);
}
} else {
const entryPointFilename: string = path.basename(
collector.workingPackage.entryPointSourceFile.fileName
);

if (!alreadyWarnedEntities.has(referencedEntity)) {
alreadyWarnedEntities.add(referencedEntity);

if (
referencedEntity instanceof AstSymbol &&
ValidationEnhancer._isEcmaScriptSymbol(referencedEntity)
) {
// The main usage scenario for ECMAScript symbols is to attach private data to a JavaScript object,
// so as a special case, we do NOT report them as forgotten exports.
} else {
const entryPointFilename: string = path.basename(
collector.workingPackage.entryPointSourceFile.fileName
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.ForgottenExport,
`The symbol "${localName}" needs to be exported by the entry point ${entryPointFilename}`,
astDeclaration
);

if (!alreadyWarnedSymbols.has(referencedEntity)) {
alreadyWarnedSymbols.add(referencedEntity);

// The main usage scenario for ECMAScript symbols is to attach private data to a JavaScript object,
// so as a special case, we do NOT report them as forgotten exports.
if (!ValidationEnhancer._isEcmaScriptSymbol(referencedEntity)) {
collector.messageRouter.addAnalyzerIssue(
ExtractorMessageId.ForgottenExport,
`The symbol "${rootSymbol.localName}" needs to be exported` +
` by the entry point ${entryPointFilename}`,
astDeclaration
);
}
}
}
}
}

if (referencedEntity instanceof AstNamespaceImport) {
// TODO [MA]: add validation for local import
}
}
}

Expand Down
14 changes: 14 additions & 0 deletions apps/api-extractor/src/generators/ApiModelGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ export class ApiModelGenerator {
}

if (astEntity instanceof AstNamespaceImport) {
// Note that a single API item can belong to two different AstNamespaceImport namespaces. For example:
//
// // file.ts defines "thing()"
// import * as example1 from "./file";
// import * as example2 from "./file";
//
// // ...so here we end up with example1.thing() and example2.thing()
// export { example1, example2 }
//
// The current logic does not try to associate "thing()" with a specific parent. Instead
// the API documentation will show duplicated entries for example1.thing() and example2.thing()./
//
// This could be improved in the future, but it requires a stable mechanism for choosing an associated parent.
// For thoughts about this: https://github.com/microsoft/rushstack/issues/1308
this._processAstModule(astEntity.astModule, exportedName, parentApiItem);
return;
}
Expand Down
4 changes: 1 addition & 3 deletions apps/api-extractor/src/generators/ApiReportGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,7 @@ export class ApiReportGenerator {
}

if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(
astEntity.astModule
);
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);

if (entity.nameForEmit === undefined) {
// This should never happen
Expand Down
4 changes: 1 addition & 3 deletions apps/api-extractor/src/generators/DtsRollupGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,7 @@ export class DtsRollupGenerator {
}

if (astEntity instanceof AstNamespaceImport) {
const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(
astEntity.astModule
);
const astModuleExportInfo: AstModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector);

if (entity.nameForEmit === undefined) {
// This should never happen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
```ts

// Warning: (ae-forgotten-export) The symbol "forgottenNs" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
function exportedApi(): forgottenNs.ForgottenClass;

Expand Down

0 comments on commit 12799aa

Please sign in to comment.