@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
503 lines (501 loc) • 22.9 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
/* tslint:disable:no-bitwise */
/* tslint:disable:no-constant-condition */
const ts = require("typescript");
const ApiDocumentation_1 = require("../aedoc/ApiDocumentation");
const ReleaseTag_1 = require("../aedoc/ReleaseTag");
const TypeScriptHelpers_1 = require("../TypeScriptHelpers");
const Markup_1 = require("../markup/Markup");
const ApiDefinitionReference_1 = require("../ApiDefinitionReference");
/**
* Indicates the type of definition represented by a AstItem object.
*/
var AstItemKind;
(function (AstItemKind) {
/**
* A TypeScript class.
*/
AstItemKind[AstItemKind["Class"] = 0] = "Class";
/**
* A TypeScript enum.
*/
AstItemKind[AstItemKind["Enum"] = 1] = "Enum";
/**
* A TypeScript value on an enum.
*/
AstItemKind[AstItemKind["EnumValue"] = 2] = "EnumValue";
/**
* A TypeScript function.
*/
AstItemKind[AstItemKind["Function"] = 3] = "Function";
/**
* A TypeScript interface.
*/
AstItemKind[AstItemKind["Interface"] = 4] = "Interface";
/**
* A TypeScript method.
*/
AstItemKind[AstItemKind["Method"] = 5] = "Method";
/**
* A TypeScript package.
*/
AstItemKind[AstItemKind["Package"] = 6] = "Package";
/**
* A TypeScript parameter.
*/
AstItemKind[AstItemKind["Parameter"] = 7] = "Parameter";
/**
* A TypeScript property.
*/
AstItemKind[AstItemKind["Property"] = 8] = "Property";
/**
* A TypeScript type literal expression, i.e. which defines an anonymous interface.
*/
AstItemKind[AstItemKind["TypeLiteral"] = 9] = "TypeLiteral";
/**
* A Typescript class constructor function.
*/
AstItemKind[AstItemKind["Constructor"] = 10] = "Constructor";
/**
* A Typescript namespace.
*/
AstItemKind[AstItemKind["Namespace"] = 11] = "Namespace";
/**
* A Typescript BlockScopedVariable.
*/
AstItemKind[AstItemKind["ModuleVariable"] = 12] = "ModuleVariable";
})(AstItemKind = exports.AstItemKind || (exports.AstItemKind = {}));
/**
* The state of completing the AstItem's doc comment references inside a recursive call to AstItem.resolveReferences().
*/
var InitializationState;
(function (InitializationState) {
/**
* The references of this AstItem have not begun to be completed.
*/
InitializationState[InitializationState["Incomplete"] = 0] = "Incomplete";
/**
* The references of this AstItem are in the process of being completed.
* If we encounter this state again during completing, a circular dependency
* has occurred.
*/
InitializationState[InitializationState["Completing"] = 1] = "Completing";
/**
* The references of this AstItem have all been completed and the documentation can
* now safely be created.
*/
InitializationState[InitializationState["Completed"] = 2] = "Completed";
})(InitializationState || (InitializationState = {}));
// Names of NPM scopes that contain packages that provide typings for the real package.
// The TypeScript compiler's typings design doesn't seem to handle scoped NPM packages,
// so the transformation will always be simple, like this:
// "@types/example" --> "example"
// NOT like this:
// "@types/@contoso/example" --> "@contoso/example"
// "@contosotypes/example" --> "@contoso/example"
// Eventually this constant should be provided by the gulp task that invokes the compiler.
const typingsScopeNames = ['@types'];
/**
* AstItem is an abstract base that represents TypeScript API definitions such as classes,
* interfaces, enums, properties, functions, and variables. Rather than directly using the
* abstract syntax tree from the TypeScript Compiler API, we use AstItem to extract a
* simplified tree which correponds to the major topics for our API documentation.
*/
class AstItem {
constructor(options) {
/**
* A superset of memberItems. Includes memberItems and also other AstItems that
* comprise this AstItem.
*
* Ex: if this AstItem is an AstFunction, then in it's innerItems would
* consist of AstParameters.
* Ex: if this AstItem is an AstMember that is a type literal, then it's
* innerItems would contain ApiProperties.
*/
this.innerItems = [];
/**
* True if this AstItem either itself has missing type information or one
* of it's innerItems is missing type information.
*
* Ex: if this AstItem is an AstMethod and has no type on the return value, then
* we consider the AstItem as 'itself' missing type informations and this property
* is set to true.
* Ex: If this AstItem is an AstMethod and one of its innerItems is an AstParameter
* that has no type specified, then we say an innerItem of this AstMethod is missing
* type information and this property is set to true.
*/
this.hasIncompleteTypes = false;
/**
* The release tag for this item, which may be inherited from a parent.
* By contrast, ApiDocumentation.releaseTag merely tracks the release tag that was
* explicitly applied to this item, and does not consider inheritance.
* @remarks
* This is calculated during completeInitialization() and should not be used beforehand.
*/
this.inheritedReleaseTag = ReleaseTag_1.ReleaseTag.None;
/**
* The deprecated message for this item, which may be inherited from a parent.
* By contrast, ApiDocumentation.deprecatedMessage merely tracks the message that was
* explicitly applied to this item, and does not consider inheritance.
* @remarks
* This is calculated during completeInitialization() and should not be used beforehand.
*/
this.inheritedDeprecatedMessage = [];
this.reportError = this.reportError.bind(this);
this.jsdocNode = options.jsdocNode;
this.declaration = options.declaration;
this._errorNode = options.declaration;
this._state = InitializationState.Incomplete;
this.warnings = [];
this.context = options.context;
this.typeChecker = this.context.typeChecker;
this.declarationSymbol = options.declarationSymbol;
this.exportSymbol = options.exportSymbol || this.declarationSymbol;
this.name = this.exportSymbol.name || '???';
let originalJsdoc = '';
if (this.jsdocNode) {
originalJsdoc = TypeScriptHelpers_1.default.getJsdocComments(this.jsdocNode, this.reportError);
}
this.documentation = new ApiDocumentation_1.default(originalJsdoc, this.context.docItemLoader, this.context, this.reportError, this.warnings);
}
/**
* Called by AstItemContainer.addMemberItem(). Other code should NOT call this method.
*/
notifyAddedToContainer(parentContainer) {
if (this._parentContainer) {
// This would indicate a program bug
throw new Error('The API item has already been added to another container: ' + this._parentContainer.name);
}
this._parentContainer = parentContainer;
}
/**
* Called after the constructor to finish the analysis.
*/
visitTypeReferencesForAstItem() {
// (virtual)
}
/**
* Return the compiler's underlying Declaration object
* @todo Generally AstItem classes don't expose ts API objects; we should add
* an appropriate member to avoid the need for this.
*/
getDeclaration() {
return this.declaration;
}
/**
* Return the compiler's underlying Symbol object that contains semantic information about the item
* @todo Generally AstItem classes don't expose ts API objects; we should add
* an appropriate member to avoid the need for this.
*/
getDeclarationSymbol() {
return this.declarationSymbol;
}
/**
* Whether this APiItem should have documentation or not. If false, then
* AstItem.missingDocumentation will never be set.
*/
shouldHaveDocumentation() {
return true;
}
/**
* The AstItemContainer that this member belongs to, or undefined if there is none.
*/
get parentContainer() {
return this._parentContainer;
}
/**
* This function is a second stage that happens after ExtractorContext.analyze() calls AstItem constructor to build up
* the abstract syntax tree. In this second stage, we are creating the documentation for each AstItem.
*
* This function makes sure we create the documentation for each AstItem in the correct order.
* In the event that a circular dependency occurs, an error is reported. For example, if AstItemOne has
* an \@inheritdoc referencing AstItemTwo, and AstItemTwo has an \@inheritdoc referencing AstItemOne then
* we have a circular dependency and an error will be reported.
*/
completeInitialization() {
switch (this._state) {
case InitializationState.Completed:
return;
case InitializationState.Incomplete:
this._state = InitializationState.Completing;
this.onCompleteInitialization();
this._state = InitializationState.Completed;
for (const innerItem of this.innerItems) {
innerItem.completeInitialization();
}
return;
case InitializationState.Completing:
this.reportError('circular reference');
return;
default:
throw new Error('AstItem state is invalid');
}
}
/**
* A procedure for determining if this AstItem is missing type
* information. We first check if the AstItem itself is missing
* any type information and if not then we check each of it's
* innerItems for missing types.
*
* Ex: On the AstItem itself, there may be missing type information
* on the return value or missing type declaration of itself
* (const name;).
* Ex: For each innerItem, there may be an AstParameter that is missing
* a type. Or for an AstMember that is a type literal, there may be an
* AstProperty that is missing type information.
*/
hasAnyIncompleteTypes() {
if (this.hasIncompleteTypes) {
return true;
}
for (const innerItem of this.innerItems) {
if (innerItem.hasIncompleteTypes) {
return true;
}
}
return false;
}
/**
* This traverses any type aliases to find the original place where an item was defined.
* For example, suppose a class is defined as "export default class MyClass { }"
* but exported from the package's index.ts like this:
*
* export { default as _MyClass } from './MyClass';
*
* In this example, calling followAliases() on the _MyClass symbol will return the
* original definition of MyClass, traversing any intermediary places where the
* symbol was imported and re-exported.
*/
followAliases(symbol) {
let current = symbol;
while (true) {
if (!(current.flags & ts.SymbolFlags.Alias)) {
break;
}
const currentAlias = this.typeChecker.getAliasedSymbol(current);
if (!currentAlias || currentAlias === current) {
break;
}
current = currentAlias;
}
return current;
}
/**
* Reports an error through the ApiErrorHandler interface that was registered with the Extractor,
* adding the filename and line number information for the declaration of this AstItem.
*/
reportError(message) {
this.context.reportError(message, this._errorNode.getSourceFile(), this._errorNode.getStart());
}
/**
* Adds a warning to the AstItem.warnings list. These warnings will be emitted in the API file
* produced by ApiFileGenerator.
*/
reportWarning(message) {
this.warnings.push(message);
}
/**
* This function assumes all references from this AstItem have been resolved and we can now safely create
* the documentation.
*/
onCompleteInitialization() {
this.documentation.completeInitialization(this.warnings);
// Calculate the inherited release tag
if (this.documentation.releaseTag !== ReleaseTag_1.ReleaseTag.None) {
this.inheritedReleaseTag = this.documentation.releaseTag;
}
else if (this.parentContainer) {
this.inheritedReleaseTag = this.parentContainer.inheritedReleaseTag;
}
// Calculate the inherited deprecation message
if (this.documentation.deprecatedMessage.length) {
this.inheritedDeprecatedMessage = this.documentation.deprecatedMessage;
}
else if (this.parentContainer) {
this.inheritedDeprecatedMessage = this.parentContainer.inheritedDeprecatedMessage;
}
// TODO: this.visitTypeReferencesForNode(this);
const summaryTextCondensed = Markup_1.Markup.extractTextContent(this.documentation.summary).replace(/\s\s/g, ' ');
this.needsDocumentation = this.shouldHaveDocumentation() && summaryTextCondensed.length <= 10;
this.supportedName = (this.kind === AstItemKind.Package) || AstItem._allowedNameRegex.test(this.name);
if (!this.supportedName) {
this.warnings.push(`The name "${this.name}" contains unsupported characters; ` +
'API names should use only letters, numbers, and underscores');
}
if (this.kind === AstItemKind.Package) {
if (this.documentation.releaseTag !== ReleaseTag_1.ReleaseTag.None) {
const tag = '@' + ReleaseTag_1.ReleaseTag[this.documentation.releaseTag].toLowerCase();
this.reportError(`The ${tag} tag is not allowed on the package, which is always considered to be `);
}
}
if (this.documentation.preapproved) {
if (!(this.getDeclaration().kind & (ts.SyntaxKind.InterfaceDeclaration | ts.SyntaxKind.ClassDeclaration))) {
this.reportError('The @preapproved tag may only be applied to classes and interfaces');
this.documentation.preapproved = false;
}
}
if (this.documentation.isDocInheritedDeprecated && this.documentation.deprecatedMessage.length === 0) {
this.reportError('The @inheritdoc target has been marked as @deprecated. ' +
'Add a @deprecated message here, or else remove the @inheritdoc relationship.');
}
if (this.name.substr(0, 1) === '_') {
if (this.documentation.releaseTag !== ReleaseTag_1.ReleaseTag.Internal
&& this.documentation.releaseTag !== ReleaseTag_1.ReleaseTag.None) {
this.reportWarning('The underscore prefix ("_") should only be used with definitions'
+ ' that are explicitly marked as @internal');
}
}
else {
if (this.documentation.releaseTag === ReleaseTag_1.ReleaseTag.Internal) {
this.reportWarning('Because this definition is explicitly marked as @internal, an underscore prefix ("_")'
+ ' should be added to its name');
}
}
// Is it missing a release tag?
if (this.documentation.releaseTag === ReleaseTag_1.ReleaseTag.None) {
// Only warn about top-level exports
if (this.parentContainer && this.parentContainer.kind === AstItemKind.Package) {
// Don't warn about items that failed to parse.
if (!this.documentation.failedToParse) {
// If there is no release tag, and this is a top-level export of the package, then
// report an error
this.reportError(`A release tag (, , , ) must be specified`
+ ` for ${this.name}`);
}
}
}
}
/**
* This is called by AstItems to visit the types that appear in an expression. For example,
* if a Public API function returns a class that is defined in this package, but not exported,
* this is a problem. visitTypeReferencesForNode() finds all TypeReference child nodes under the
* specified node and analyzes each one.
*/
visitTypeReferencesForNode(node) {
if (node.kind === ts.SyntaxKind.Block ||
(node.kind >= ts.SyntaxKind.JSDocTypeExpression && node.kind <= ts.SyntaxKind.NeverKeyword)) {
// Don't traverse into code blocks or JSDoc items; we only care about the function signature
return;
}
if (node.kind === ts.SyntaxKind.TypeReference) {
const typeReference = node;
this._analyzeTypeReference(typeReference);
}
// Recurse the tree
for (const childNode of node.getChildren()) {
this.visitTypeReferencesForNode(childNode);
}
}
/**
* This is a helper for visitTypeReferencesForNode(). It analyzes a single TypeReferenceNode.
*/
_analyzeTypeReference(typeReferenceNode) {
const symbol = this.context.typeChecker.getSymbolAtLocation(typeReferenceNode.typeName);
if (!symbol) {
// Is this bad?
return;
}
if (symbol.flags & ts.SymbolFlags.TypeParameter) {
// Don't analyze e.g. "T" in "Set<T>"
return;
}
// Follow the aliases all the way to the ending SourceFile
const currentSymbol = this.followAliases(symbol);
if (!currentSymbol.declarations || !currentSymbol.declarations.length) {
// This is a degenerate case that happens sometimes
return;
}
const sourceFile = currentSymbol.declarations[0].getSourceFile();
// Walk upwards from that directory until you find a directory containing package.json,
// this is where the referenced type is located.
// Example: "c:\users\<username>\sp-client\spfx-core\sp-core-library"
const typeReferencePackagePath = this.context.packageJsonLookup
.tryGetPackageFolder(sourceFile.fileName);
// Example: "@microsoft/sp-core-library"
let typeReferencePackageName = '';
// If we can not find a package path, we consider the type to be part of the current project's package.
// One case where this happens is when looking for a type that is a symlink
if (!typeReferencePackagePath) {
typeReferencePackageName = this.context.package.name;
}
else {
typeReferencePackageName = this.context.packageJsonLookup
.getPackageName(typeReferencePackagePath);
typingsScopeNames.every(typingScopeName => {
if (typeReferencePackageName.indexOf(typingScopeName) > -1) {
typeReferencePackageName = typeReferencePackageName.replace(typingScopeName + '/', '');
// returning true breaks the every loop
return true;
}
return false;
});
}
// Read the name/version from package.json -- that tells you what package the symbol
// belongs to. If it is your own AstPackage.name/version, then you know it's a local symbol.
const currentPackageName = this.context.package.name;
const typeName = typeReferenceNode.typeName.getText();
if (!typeReferencePackagePath || typeReferencePackageName === currentPackageName) {
// The type is defined in this project. Did the person remember to export it?
const exportedLocalName = this.context.package.tryGetExportedSymbolName(currentSymbol);
if (exportedLocalName) {
// [CASE 1] Local/Exported
// Yes; the type is properly exported.
// TODO: In the future, here we can check for issues such as a @public type
// referencing an @internal type.
return;
}
else {
// [CASE 2] Local/Unexported
// No; issue a warning
this.reportWarning(`The type "${typeName}" needs to be exported by the package`
+ ` (e.g. added to index.ts)`);
return;
}
}
// External
// Attempt to load from docItemLoader
const scopedPackageName = ApiDefinitionReference_1.default.parseScopedPackageName(typeReferencePackageName);
const apiDefinitionRefParts = {
scopeName: scopedPackageName.scope,
packageName: scopedPackageName.package,
exportName: '',
memberName: ''
};
// the currentSymbol.name is the name of an export, if it contains a '.' then the substring
// after the period is the member name
if (currentSymbol.name.indexOf('.') > -1) {
const exportMemberName = currentSymbol.name.split('.');
apiDefinitionRefParts.exportName = exportMemberName.pop() || '';
apiDefinitionRefParts.memberName = exportMemberName.pop() || '';
}
else {
apiDefinitionRefParts.exportName = currentSymbol.name;
}
const apiDefinitionRef = ApiDefinitionReference_1.default.createFromParts(apiDefinitionRefParts);
// Attempt to resolve the type by checking the node modules
const referenceResolutionWarnings = [];
const resolvedAstItem = this.context.docItemLoader.resolveJsonReferences(apiDefinitionRef, referenceResolutionWarnings);
if (resolvedAstItem) {
// [CASE 3] External/Resolved
// This is a reference to a type from an external package, and it was resolved.
return;
}
else {
// [CASE 4] External/Unresolved
// For cases when we can't find the external package, we are going to write a report
// at the bottom of the *api.ts file. We do this because we do not yet support references
// to items like react:Component.
// For now we are going to silently ignore these errors.
return;
}
}
}
/**
* Names of API items should only contain letters, numbers and underscores.
*/
AstItem._allowedNameRegex = /^[a-zA-Z_]+[a-zA-Z_0-9]*$/;
exports.default = AstItem;
//# sourceMappingURL=AstItem.js.map