UNPKG

13.1 kBJavaScriptView Raw
1"use strict";
2// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
3// See LICENSE in the project root for license information.
4var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5 if (k2 === undefined) k2 = k;
6 var desc = Object.getOwnPropertyDescriptor(m, k);
7 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8 desc = { enumerable: true, get: function() { return m[k]; } };
9 }
10 Object.defineProperty(o, k2, desc);
11}) : (function(o, m, k, k2) {
12 if (k2 === undefined) k2 = k;
13 o[k2] = m[k];
14}));
15var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16 Object.defineProperty(o, "default", { enumerable: true, value: v });
17}) : function(o, v) {
18 o["default"] = v;
19});
20var __importStar = (this && this.__importStar) || function (mod) {
21 if (mod && mod.__esModule) return mod;
22 var result = {};
23 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
24 __setModuleDefault(result, mod);
25 return result;
26};
27Object.defineProperty(exports, "__esModule", { value: true });
28exports.ValidationEnhancer = void 0;
29const path = __importStar(require("path"));
30const ts = __importStar(require("typescript"));
31const AstSymbol_1 = require("../analyzer/AstSymbol");
32const api_extractor_model_1 = require("@microsoft/api-extractor-model");
33const AstNamespaceImport_1 = require("../analyzer/AstNamespaceImport");
34class ValidationEnhancer {
35 static analyze(collector) {
36 const alreadyWarnedEntities = new Set();
37 for (const entity of collector.entities) {
38 if (!entity.consumable) {
39 continue;
40 }
41 if (entity.astEntity instanceof AstSymbol_1.AstSymbol) {
42 // A regular exported AstSymbol
43 const astSymbol = entity.astEntity;
44 astSymbol.forEachDeclarationRecursive((astDeclaration) => {
45 ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
46 });
47 const symbolMetadata = collector.fetchSymbolMetadata(astSymbol);
48 ValidationEnhancer._checkForInternalUnderscore(collector, entity, astSymbol, symbolMetadata);
49 ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
50 }
51 else if (entity.astEntity instanceof AstNamespaceImport_1.AstNamespaceImport) {
52 // A namespace created using "import * as ___ from ___"
53 const astNamespaceImport = entity.astEntity;
54 const astModuleExportInfo = astNamespaceImport.fetchAstModuleExportInfo(collector);
55 for (const namespaceMemberAstEntity of astModuleExportInfo.exportedLocalEntities.values()) {
56 if (namespaceMemberAstEntity instanceof AstSymbol_1.AstSymbol) {
57 const astSymbol = namespaceMemberAstEntity;
58 astSymbol.forEachDeclarationRecursive((astDeclaration) => {
59 ValidationEnhancer._checkReferences(collector, astDeclaration, alreadyWarnedEntities);
60 });
61 const symbolMetadata = collector.fetchSymbolMetadata(astSymbol);
62 // (Don't apply ValidationEnhancer._checkForInternalUnderscore() for AstNamespaceImport members)
63 ValidationEnhancer._checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata);
64 }
65 }
66 }
67 }
68 }
69 static _checkForInternalUnderscore(collector, collectorEntity, astSymbol, symbolMetadata) {
70 let needsUnderscore = false;
71 if (symbolMetadata.maxEffectiveReleaseTag === api_extractor_model_1.ReleaseTag.Internal) {
72 if (!astSymbol.parentAstSymbol) {
73 // If it's marked as @internal and has no parent, then it needs and underscore.
74 // We use maxEffectiveReleaseTag because a merged declaration would NOT need an underscore in a case like this:
75 //
76 // /** @public */
77 // export enum X { }
78 //
79 // /** @internal */
80 // export namespace X { }
81 //
82 // (The above normally reports an error "ae-different-release-tags", but that may be suppressed.)
83 needsUnderscore = true;
84 }
85 else {
86 // If it's marked as @internal and the parent isn't obviously already @internal, then it needs an underscore.
87 //
88 // For example, we WOULD need an underscore for a merged declaration like this:
89 //
90 // /** @internal */
91 // export namespace X {
92 // export interface _Y { }
93 // }
94 //
95 // /** @public */
96 // export class X {
97 // /** @internal */
98 // public static _Y(): void { } // <==== different from parent
99 // }
100 const parentSymbolMetadata = collector.fetchSymbolMetadata(astSymbol);
101 if (parentSymbolMetadata.maxEffectiveReleaseTag > api_extractor_model_1.ReleaseTag.Internal) {
102 needsUnderscore = true;
103 }
104 }
105 }
106 if (needsUnderscore) {
107 for (const exportName of collectorEntity.exportNames) {
108 if (exportName[0] !== '_') {
109 collector.messageRouter.addAnalyzerIssue("ae-internal-missing-underscore" /* InternalMissingUnderscore */, `The name "${exportName}" should be prefixed with an underscore` +
110 ` because the declaration is marked as @internal`, astSymbol, { exportName });
111 }
112 }
113 }
114 }
115 static _checkForInconsistentReleaseTags(collector, astSymbol, symbolMetadata) {
116 if (astSymbol.isExternal) {
117 // For now, don't report errors for external code. If the developer cares about it, they should run
118 // API Extractor separately on the external project
119 return;
120 }
121 // Normally we will expect all release tags to be the same. Arbitrarily we choose the maxEffectiveReleaseTag
122 // as the thing they should all match.
123 const expectedEffectiveReleaseTag = symbolMetadata.maxEffectiveReleaseTag;
124 // This is set to true if we find a declaration whose release tag is different from expectedEffectiveReleaseTag
125 let mixedReleaseTags = false;
126 // This is set to false if we find a declaration that is not a function/method overload
127 let onlyFunctionOverloads = true;
128 // This is set to true if we find a declaration that is @internal
129 let anyInternalReleaseTags = false;
130 for (const astDeclaration of astSymbol.astDeclarations) {
131 const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
132 const effectiveReleaseTag = apiItemMetadata.effectiveReleaseTag;
133 switch (astDeclaration.declaration.kind) {
134 case ts.SyntaxKind.FunctionDeclaration:
135 case ts.SyntaxKind.MethodDeclaration:
136 break;
137 default:
138 onlyFunctionOverloads = false;
139 }
140 if (effectiveReleaseTag !== expectedEffectiveReleaseTag) {
141 mixedReleaseTags = true;
142 }
143 if (effectiveReleaseTag === api_extractor_model_1.ReleaseTag.Internal) {
144 anyInternalReleaseTags = true;
145 }
146 }
147 if (mixedReleaseTags) {
148 if (!onlyFunctionOverloads) {
149 collector.messageRouter.addAnalyzerIssue("ae-different-release-tags" /* DifferentReleaseTags */, 'This symbol has another declaration with a different release tag', astSymbol);
150 }
151 if (anyInternalReleaseTags) {
152 collector.messageRouter.addAnalyzerIssue("ae-internal-mixed-release-tag" /* InternalMixedReleaseTag */, `Mixed release tags are not allowed for "${astSymbol.localName}" because one of its declarations` +
153 ` is marked as @internal`, astSymbol);
154 }
155 }
156 }
157 static _checkReferences(collector, astDeclaration, alreadyWarnedEntities) {
158 const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration);
159 const declarationReleaseTag = apiItemMetadata.effectiveReleaseTag;
160 for (const referencedEntity of astDeclaration.referencedAstEntities) {
161 let collectorEntity;
162 let referencedReleaseTag;
163 let localName;
164 if (referencedEntity instanceof AstSymbol_1.AstSymbol) {
165 // If this is e.g. a member of a namespace, then we need to be checking the top-level scope to see
166 // whether it's exported.
167 //
168 // TODO: Technically we should also check each of the nested scopes along the way.
169 const rootSymbol = referencedEntity.rootAstSymbol;
170 if (rootSymbol.isExternal) {
171 continue;
172 }
173 localName = rootSymbol.localName;
174 collectorEntity = collector.tryGetCollectorEntity(rootSymbol);
175 const referencedMetadata = collector.fetchSymbolMetadata(referencedEntity);
176 referencedReleaseTag = referencedMetadata.maxEffectiveReleaseTag;
177 }
178 else if (referencedEntity instanceof AstNamespaceImport_1.AstNamespaceImport) {
179 collectorEntity = collector.tryGetCollectorEntity(referencedEntity);
180 // TODO: Currently the "import * as ___ from ___" syntax does not yet support doc comments
181 referencedReleaseTag = api_extractor_model_1.ReleaseTag.Public;
182 localName = referencedEntity.localName;
183 }
184 else {
185 continue;
186 }
187 if (collectorEntity && collectorEntity.consumable) {
188 if (api_extractor_model_1.ReleaseTag.compare(declarationReleaseTag, referencedReleaseTag) > 0) {
189 collector.messageRouter.addAnalyzerIssue("ae-incompatible-release-tags" /* IncompatibleReleaseTags */, `The symbol "${astDeclaration.astSymbol.localName}"` +
190 ` is marked as ${api_extractor_model_1.ReleaseTag.getTagName(declarationReleaseTag)},` +
191 ` but its signature references "${referencedEntity.localName}"` +
192 ` which is marked as ${api_extractor_model_1.ReleaseTag.getTagName(referencedReleaseTag)}`, astDeclaration);
193 }
194 }
195 else {
196 const entryPointFilename = path.basename(collector.workingPackage.entryPointSourceFile.fileName);
197 if (!alreadyWarnedEntities.has(referencedEntity)) {
198 alreadyWarnedEntities.add(referencedEntity);
199 if (referencedEntity instanceof AstSymbol_1.AstSymbol &&
200 ValidationEnhancer._isEcmaScriptSymbol(referencedEntity)) {
201 // The main usage scenario for ECMAScript symbols is to attach private data to a JavaScript object,
202 // so as a special case, we do NOT report them as forgotten exports.
203 }
204 else {
205 collector.messageRouter.addAnalyzerIssue("ae-forgotten-export" /* ForgottenExport */, `The symbol "${localName}" needs to be exported by the entry point ${entryPointFilename}`, astDeclaration);
206 }
207 }
208 }
209 }
210 }
211 // Detect an AstSymbol that refers to an ECMAScript symbol declaration such as:
212 //
213 // const mySymbol: unique symbol = Symbol('mySymbol');
214 static _isEcmaScriptSymbol(astSymbol) {
215 if (astSymbol.astDeclarations.length !== 1) {
216 return false;
217 }
218 // We are matching a form like this:
219 //
220 // - VariableDeclaration:
221 // - Identifier: pre=[mySymbol]
222 // - ColonToken: pre=[:] sep=[ ]
223 // - TypeOperator:
224 // - UniqueKeyword: pre=[unique] sep=[ ]
225 // - SymbolKeyword: pre=[symbol]
226 const astDeclaration = astSymbol.astDeclarations[0];
227 if (ts.isVariableDeclaration(astDeclaration.declaration)) {
228 const variableTypeNode = astDeclaration.declaration.type;
229 if (variableTypeNode) {
230 for (const token of variableTypeNode.getChildren()) {
231 if (token.kind === ts.SyntaxKind.SymbolKeyword) {
232 return true;
233 }
234 }
235 }
236 }
237 return false;
238 }
239}
240exports.ValidationEnhancer = ValidationEnhancer;
241//# sourceMappingURL=ValidationEnhancer.js.map
\No newline at end of file