243 lines
13 KiB
JavaScript
243 lines
13 KiB
JavaScript
"use strict";
|
|
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
|
|
// See LICENSE in the project root for license information.
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.ValidationEnhancer = void 0;
|
|
const path = __importStar(require("path"));
|
|
const ts = __importStar(require("typescript"));
|
|
const AstSymbol_1 = require("../analyzer/AstSymbol");
|
|
const api_extractor_model_1 = require("@microsoft/api-extractor-model");
|
|
const AstNamespaceImport_1 = require("../analyzer/AstNamespaceImport");
|
|
class ValidationEnhancer {
|
|
static analyze(collector) {
|
|
const alreadyWarnedEntities = new Set();
|
|
for (const entity of collector.entities) {
|
|
if (!(entity.consumable ||
|
|
collector.extractorConfig.apiReportIncludeForgottenExports ||
|
|
collector.extractorConfig.docModelIncludeForgottenExports)) {
|
|
continue;
|
|
}
|
|
if (entity.astEntity instanceof AstSymbol_1.AstSymbol) {
|
|
// A regular exported AstSymbol
|
|
const astSymbol = entity.astEntity;
|
|
astSymbol.forEachDeclarationRecursive((astDeclaration) => {
|
|
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
|
|
});
|
|
const symbolMetadata = collector.fetchSymbolMetadata(astSymbol);
|
|
ValidationEnhancer._checkForInternalUnderscore(collector, entity, astSymbol, symbolMetadata);
|
|
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
|
|
}
|
|
else if (entity.astEntity instanceof AstNamespaceImport_1.AstNamespaceImport) {
|
|
// A namespace created using "import * as ___ from ___"
|
|
const astNamespaceImport = entity.astEntity;
|
|
const astModuleExportInfo = astNamespaceImport.fetchAstModuleExportInfo(collector);
|
|
for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) {
|
|
if (namespaceMemberAstEntity instanceof AstSymbol_1.AstSymbol) {
|
|
const astSymbol = namespaceMemberAstEntity;
|
|
astSymbol.forEachDeclarationRecursive((astDeclaration) => {
|
|
ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
|
|
});
|
|
const symbolMetadata = collector.fetchSymbolMetadata(astSymbol);
|
|
// (Don't apply ValidationEnhancer._checkForInternalUnderscore() for AstNamespaceImport members)
|
|
ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static _checkForInternalUnderscore(collector, collectorEntity, astSymbol, symbolMetadata) {
|
|
let needsUnderscore = false;
|
|
if (symbolMetadata.maxEffectiveReleaseTag === api_extractor_model_1.ReleaseTag.Internal) {
|
|
if (!astSymbol.parentAstSymbol) {
|
|
// If it's marked as @internal and has no parent, then it needs an underscore.
|
|
// We use maxEffectiveReleaseTag because a merged declaration would NOT need an underscore in a case like this:
|
|
//
|
|
// /** @public */
|
|
// export enum X { }
|
|
//
|
|
// /** @internal */
|
|
// export namespace X { }
|
|
//
|
|
// (The above normally reports an error "ae-different-release-tags", but that may be suppressed.)
|
|
needsUnderscore = true;
|
|
}
|
|
else {
|
|
// If it's marked as @internal and the parent isn't obviously already @internal, then it needs an underscore.
|
|
//
|
|
// For example, we WOULD need an underscore for a merged declaration like this:
|
|
//
|
|
// /** @internal */
|
|
// export namespace X {
|
|
// export interface _Y { }
|
|
// }
|
|
//
|
|
// /** @public */
|
|
// export class X {
|
|
// /** @internal */
|
|
// public static _Y(): void { } // <==== different from parent
|
|
// }
|
|
const parentSymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
|
|
if (parentSymbolMetadata.maxEffectiveReleaseTag > api_extractor_model_1.ReleaseTag.Internal) {
|
|
needsUnderscore = true;
|
|
}
|
|
}
|
|
}
|
|
if (needsUnderscore) {
|
|
for (const exportName of collectorEntity.exportNames) {
|
|
if (exportName[0] !== '_') {
|
|
collector.messageRouter.addAnalyzerIssue("ae-internal-missing-underscore" /* ExtractorMessageId.InternalMissingUnderscore */, `The name "${exportName}" should be prefixed with an underscore` +
|
|
` because the declaration is marked as @internal`, astSymbol, { exportName });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
static _checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata) {
|
|
if (astSymbol.isExternal) {
|
|
// For now, don't report errors for external code. If the developer cares about it, they should run
|
|
// API Extractor separately on the external project
|
|
return;
|
|
}
|
|
// Normally we will expect all release tags to be the same. Arbitrarily we choose the maxEffectiveReleaseTag
|
|
// as the thing they should all match.
|
|
const expectedEffectiveReleaseTag = symbolMetadata.maxEffectiveReleaseTag;
|
|
// This is set to true if we find a declaration whose release tag is different from expectedEffectiveReleaseTag
|
|
let mixedReleaseTags = false;
|
|
// This is set to false if we find a declaration that is not a function/method overload
|
|
let onlyFunctionOverloads = true;
|
|
// This is set to true if we find a declaration that is @internal
|
|
let anyInternalReleaseTags = false;
|
|
for (const astDeclaration of astSymbol.astDeclarations) {
|
|
const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
|
|
const effectiveReleaseTag = apiItemMetadata.effectiveReleaseTag;
|
|
switch (astDeclaration.declaration.kind) {
|
|
case ts.SyntaxKind.FunctionDeclaration:
|
|
case ts.SyntaxKind.MethodDeclaration:
|
|
break;
|
|
default:
|
|
onlyFunctionOverloads = false;
|
|
}
|
|
if (effectiveReleaseTag !== expectedEffectiveReleaseTag) {
|
|
mixedReleaseTags = true;
|
|
}
|
|
if (effectiveReleaseTag === api_extractor_model_1.ReleaseTag.Internal) {
|
|
anyInternalReleaseTags = true;
|
|
}
|
|
}
|
|
if (mixedReleaseTags) {
|
|
if (!onlyFunctionOverloads) {
|
|
collector.messageRouter.addAnalyzerIssue("ae-different-release-tags" /* ExtractorMessageId.DifferentReleaseTags */, 'This symbol has another declaration with a different release tag', astSymbol);
|
|
}
|
|
if (anyInternalReleaseTags) {
|
|
collector.messageRouter.addAnalyzerIssue("ae-internal-mixed-release-tag" /* ExtractorMessageId.InternalMixedReleaseTag */, `Mixed release tags are not allowed for "${astSymbol.localName}" because one of its declarations` +
|
|
` is marked as @internal`, astSymbol);
|
|
}
|
|
}
|
|
}
|
|
static _checkReferences(collector, astDeclaration, alreadyWarnedEntities) {
|
|
const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
|
|
const declarationReleaseTag = apiItemMetadata.effectiveReleaseTag;
|
|
for (const referencedEntity of astDeclaration.referencedAstEntities) {
|
|
let collectorEntity;
|
|
let referencedReleaseTag;
|
|
let localName;
|
|
if (referencedEntity instanceof AstSymbol_1.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 = referencedEntity.rootAstSymbol;
|
|
if (rootSymbol.isExternal) {
|
|
continue;
|
|
}
|
|
collectorEntity = collector.tryGetCollectorEntity(rootSymbol);
|
|
localName = (collectorEntity === null || collectorEntity === void 0 ? void 0 : collectorEntity.nameForEmit) || rootSymbol.localName;
|
|
const referencedMetadata = collector.fetchSymbolMetadata(referencedEntity);
|
|
referencedReleaseTag = referencedMetadata.maxEffectiveReleaseTag;
|
|
}
|
|
else if (referencedEntity instanceof AstNamespaceImport_1.AstNamespaceImport) {
|
|
collectorEntity = collector.tryGetCollectorEntity(referencedEntity);
|
|
// TODO: Currently the "import * as ___ from ___" syntax does not yet support doc comments
|
|
referencedReleaseTag = api_extractor_model_1.ReleaseTag.Public;
|
|
localName = (collectorEntity === null || collectorEntity === void 0 ? void 0 : collectorEntity.nameForEmit) || referencedEntity.localName;
|
|
}
|
|
else {
|
|
continue;
|
|
}
|
|
if (collectorEntity && collectorEntity.consumable) {
|
|
if (api_extractor_model_1.ReleaseTag.compare(declarationReleaseTag, referencedReleaseTag) > 0) {
|
|
collector.messageRouter.addAnalyzerIssue("ae-incompatible-release-tags" /* ExtractorMessageId.IncompatibleReleaseTags */, `The symbol "${astDeclaration.astSymbol.localName}"` +
|
|
` is marked as ${api_extractor_model_1.ReleaseTag.getTagName(declarationReleaseTag)},` +
|
|
` but its signature references "${localName}"` +
|
|
` which is marked as ${api_extractor_model_1.ReleaseTag.getTagName(referencedReleaseTag)}`, astDeclaration);
|
|
}
|
|
}
|
|
else {
|
|
const entryPointFilename = path.basename(collector.workingPackage.entryPointSourceFile.fileName);
|
|
if (!alreadyWarnedEntities.has(referencedEntity)) {
|
|
alreadyWarnedEntities.add(referencedEntity);
|
|
if (referencedEntity instanceof AstSymbol_1.AstSymbol &&
|
|
ValidationEnhancer._isEcmaScriptSymbol(referencedEntity)) {
|
|
// The main usage scenario for ECMAScript symbols is to attach private data to a JavaScript object,
|
|
// so as a special case, we do NOT report them as forgotten exports.
|
|
}
|
|
else {
|
|
collector.messageRouter.addAnalyzerIssue("ae-forgotten-export" /* ExtractorMessageId.ForgottenExport */, `The symbol "${localName}" needs to be exported by the entry point ${entryPointFilename}`, astDeclaration);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Detect an AstSymbol that refers to an ECMAScript symbol declaration such as:
|
|
//
|
|
// const mySymbol: unique symbol = Symbol('mySymbol');
|
|
static _isEcmaScriptSymbol(astSymbol) {
|
|
if (astSymbol.astDeclarations.length !== 1) {
|
|
return false;
|
|
}
|
|
// We are matching a form like this:
|
|
//
|
|
// - VariableDeclaration:
|
|
// - Identifier: pre=[mySymbol]
|
|
// - ColonToken: pre=[:] sep=[ ]
|
|
// - TypeOperator:
|
|
// - UniqueKeyword: pre=[unique] sep=[ ]
|
|
// - SymbolKeyword: pre=[symbol]
|
|
const astDeclaration = astSymbol.astDeclarations[0];
|
|
if (ts.isVariableDeclaration(astDeclaration.declaration)) {
|
|
const variableTypeNode = astDeclaration.declaration.type;
|
|
if (variableTypeNode) {
|
|
for (const token of variableTypeNode.getChildren()) {
|
|
if (token.kind === ts.SyntaxKind.SymbolKeyword) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
exports.ValidationEnhancer = ValidationEnhancer;
|
|
//# sourceMappingURL=ValidationEnhancer.js.map
|