@microsoft/api-extractor
Version:
Validatation, documentation, and auditing for the exported API of a TypeScript package
422 lines (420 loc) • 19.4 kB
JavaScript
/* tslint:disable:no-bitwise */
/* tslint:disable:no-constant-condition */
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var ts = require("typescript");
var ApiDocumentation_1 = require("./ApiDocumentation");
var TypeScriptHelpers_1 = require("../TypeScriptHelpers");
var DocElementParser_1 = require("../DocElementParser");
var PackageJsonHelpers_1 = require("../PackageJsonHelpers");
var ApiDefinitionReference_1 = require("../ApiDefinitionReference");
/**
* Indicates the type of definition represented by a ApiItem object.
*/
var ApiItemKind;
(function (ApiItemKind) {
/**
* A TypeScript class.
*/
ApiItemKind[ApiItemKind["Class"] = 0] = "Class";
/**
* A TypeScript enum.
*/
ApiItemKind[ApiItemKind["Enum"] = 1] = "Enum";
/**
* A TypeScript value on an enum.
*/
ApiItemKind[ApiItemKind["EnumValue"] = 2] = "EnumValue";
/**
* A TypeScript function.
*/
ApiItemKind[ApiItemKind["Function"] = 3] = "Function";
/**
* A TypeScript interface.
*/
ApiItemKind[ApiItemKind["Interface"] = 4] = "Interface";
/**
* A TypeScript method.
*/
ApiItemKind[ApiItemKind["Method"] = 5] = "Method";
/**
* A TypeScript package.
*/
ApiItemKind[ApiItemKind["Package"] = 6] = "Package";
/**
* A TypeScript parameter.
*/
ApiItemKind[ApiItemKind["Parameter"] = 7] = "Parameter";
/**
* A TypeScript property.
*/
ApiItemKind[ApiItemKind["Property"] = 8] = "Property";
/**
* A TypeScript type literal expression, i.e. which defines an anonymous interface.
*/
ApiItemKind[ApiItemKind["TypeLiteral"] = 9] = "TypeLiteral";
/**
* A Typescript class constructor function.
*/
ApiItemKind[ApiItemKind["Constructor"] = 10] = "Constructor";
})(ApiItemKind = exports.ApiItemKind || (exports.ApiItemKind = {}));
/**
* The state of completing the ApiItem's doc comment references inside a recursive call to ApiItem.resolveReferences().
*/
var InitializationState;
(function (InitializationState) {
/**
* The references of this ApiItem have not begun to be completed.
*/
InitializationState[InitializationState["Incomplete"] = 0] = "Incomplete";
/**
* The refernces of this ApiItem are in the process of being completed.
* If we encounter this state again during completing, a circular dependency
* has occured.
*/
InitializationState[InitializationState["Completing"] = 1] = "Completing";
/**
* The references of this ApiItem 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.
var typingsScopeNames = ['@types'];
/**
* ApiItem 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 ApiItem to extract a
* simplified tree which correponds to the major topics for our API documentation.
*/
var ApiItem = (function () {
function ApiItem(options) {
/**
* A superset of memberItems. Includes memberItems and also other ApiItems that
* comprise this ApiItem.
*
* Ex: if this ApiItem is an ApiFunction, then in it's innerItems would
* consist of ApiParameters.
* Ex: if this ApiItem is an ApiMember that is a type literal, then it's
* innerItems would contain ApiProperties.
*/
this.innerItems = [];
/**
* True if this ApiItem either itself has missing type information or one
* of it's innerItems is missing type information.
*
* Ex: if this ApiItem is an ApiMethod and has no type on the return value, then
* we consider the ApiItem as 'itself' missing type informations and this property
* is set to true.
* Ex: If this ApiItem is an ApiMethod and one of its innerItems is an ApiParameter
* that has no type specified, then we say an innerItem of this ApiMethod is missing
* type information and this property is set to true.
*/
this.hasIncompleteTypes = false;
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.extractor = options.extractor;
this.typeChecker = this.extractor.typeChecker;
this.declarationSymbol = options.declarationSymbol;
this.exportSymbol = options.exportSymbol || this.declarationSymbol;
this.name = this.exportSymbol.name || '???';
var originalJsDoc = '';
if (this.jsdocNode) {
originalJsDoc = TypeScriptHelpers_1.default.getJsDocComments(this.jsdocNode, this.reportError);
}
this.documentation = new ApiDocumentation_1.default(originalJsDoc, this.extractor.docItemLoader, this.extractor, this.reportError, this.warnings);
}
/**
* Called after the constructor to finish the analysis.
*/
ApiItem.prototype.visitTypeReferencesForApiItem = function () {
// (virtual)
};
/**
* Return the compiler's underlying Declaration object
* @todo Generally ApiItem classes don't expose ts API objects; we should add
* an appropriate member to avoid the need for this.
*/
ApiItem.prototype.getDeclaration = function () {
return this.declaration;
};
/**
* Return the compiler's underlying Symbol object that contains semantic information about the item
* @todo Generally ApiItem classes don't expose ts API objects; we should add
* an appropriate member to avoid the need for this.
*/
ApiItem.prototype.getDeclarationSymbol = function () {
return this.declarationSymbol;
};
/**
* Whether this APiItem should have documentation or not. If false, then
* ApiItem.missingDocumentation will never be set.
*/
ApiItem.prototype.shouldHaveDocumentation = function () {
return true;
};
/**
* 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.
*/
ApiItem.prototype.followAliases = function (symbol) {
var current = symbol;
while (true) {
if (!(current.flags & ts.SymbolFlags.Alias)) {
break;
}
var 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 ApiItem.
*/
ApiItem.prototype.reportError = function (message) {
this.extractor.reportError(message, this._errorNode.getSourceFile(), this._errorNode.getStart());
};
/**
* Adds a warning to the ApiItem.warnings list. These warnings will be emtted in the API file
* produced by ApiFileGenerator.
*/
ApiItem.prototype.reportWarning = function (message) {
this.warnings.push(message);
};
/**
* This function assumes all references from this ApiItem have been resolved and we can now safely create
* the documentation.
*/
ApiItem.prototype.onCompleteInitialization = function () {
this.documentation.completeInitialization(this.warnings);
// TODO: this.visitTypeReferencesForNode(this);
var summaryTextCondensed = DocElementParser_1.default.getAsText(this.documentation.summary, this.reportError).replace(/\s\s/g, ' ');
this.needsDocumentation = this.shouldHaveDocumentation() && summaryTextCondensed.length <= 10;
this.supportedName = (this.kind === ApiItemKind.Package) || ApiItem._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 === ApiItemKind.Package) {
if (this.documentation.apiTag !== ApiDocumentation_1.ApiTag.None) {
var tag = '@' + ApiDocumentation_1.ApiTag[this.documentation.apiTag].toLowerCase();
this.reportError("The " + tag + " tag is not allowed on the package, which is always public");
}
}
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('inheritdoc source item is deprecated. ' +
'Must provide @deprecated message or remove @inheritdoc inline tag.');
}
};
/**
* This function is a second stage that happens after Extractor.analyze() calls ApiItem constructor to build up
* the abstract syntax tree. In this second stage, we are creating the documentation for each ApiItem.
*
* This function makes sure we create the documentation for each ApiItem in the correct order.
* In the event that a circular dependency occurs, an error is reported. For example, if ApiItemOne has
* an \@inheritdoc referencing ApiItemTwo, and ApiItemTwo has an \@inheritdoc refercing ApiItemOne then
* we have a circular dependency and an error will be reported.
*/
ApiItem.prototype.completeInitialization = function () {
switch (this._state) {
case InitializationState.Completed:
return;
case InitializationState.Incomplete:
this._state = InitializationState.Completing;
this.onCompleteInitialization();
this._state = InitializationState.Completed;
for (var _i = 0, _a = this.innerItems; _i < _a.length; _i++) {
var innerItem = _a[_i];
innerItem.completeInitialization();
}
return;
case InitializationState.Completing:
this.reportError('circular reference');
return;
default:
throw new Error('ApiItem state is invalid');
}
};
/**
* A procedure for determining if this ApiItem is missing type
* information. We first check if the ApiItem itself is missing
* any type information and if not then we check each of it's
* innerItems for missing types.
*
* Ex: On the ApiItem 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 ApiParameter that is missing
* a type. Or for an ApiMember that is a type literal, there may be an
* ApiProperty that is missing type information.
*/
ApiItem.prototype.hasAnyIncompleteTypes = function () {
if (this.hasIncompleteTypes) {
return true;
}
for (var _i = 0, _a = this.innerItems; _i < _a.length; _i++) {
var innerItem = _a[_i];
if (innerItem.hasIncompleteTypes) {
return true;
}
}
return false;
};
/**
* This is called by ApiItems 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.
*/
ApiItem.prototype.visitTypeReferencesForNode = function (node) {
if (node.kind === ts.SyntaxKind.Block ||
(node.kind >= ts.SyntaxKind.JSDocTypeExpression && node.kind <= ts.SyntaxKind.JSDocNeverKeyword)) {
// Don't traverse into code blocks or JSDoc items; we only care about the function signature
return;
}
if (node.kind === ts.SyntaxKind.TypeReference) {
var typeReference = node;
this._analyzeTypeReference(typeReference);
}
// Recurse the tree
for (var _i = 0, _a = node.getChildren(); _i < _a.length; _i++) {
var childNode = _a[_i];
this.visitTypeReferencesForNode(childNode);
}
};
/**
* This is a helper for visitTypeReferencesForNode(). It analyzes a single TypeReferenceNode.
*/
ApiItem.prototype._analyzeTypeReference = function (typeReferenceNode) {
var symbol = this.extractor.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
var currentSymbol = this.followAliases(symbol);
if (!currentSymbol.declarations || !currentSymbol.declarations.length) {
// This is a degenerate case that happens sometimes
return;
}
var 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"
var typeReferencePackagePath = PackageJsonHelpers_1.default.tryFindPackagePathUpwards(sourceFile.path);
// Example: "@microsoft/sp-core-library"
var 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.extractor.package.name;
}
else {
typeReferencePackageName = PackageJsonHelpers_1.default.readPackageName(typeReferencePackagePath);
typingsScopeNames.every(function (typingScopeName) {
if (typeReferencePackageName.indexOf(typingScopeName) > -1) {
typeReferencePackageName = typeReferencePackageName.replace(typingScopeName + '/', '');
// returning true breaks the every loop
return true;
}
});
}
// Read the name/version from package.json -- that tells you what package the symbol
// belongs to. If it is your own ApiPackage.name/version, then you know it's a local symbol.
var currentPackageName = this.extractor.package.name;
var typeName = typeReferenceNode.typeName.getText();
if (!typeReferencePackagePath || typeReferencePackageName === currentPackageName) {
// The type is defined in this project. Did the person remember to export it?
var exportedLocalName = this.extractor.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
var scopedPackageName = ApiDefinitionReference_1.default.parseScopedPackageName(typeReferencePackageName);
var 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) {
var exportMemberName = currentSymbol.name.split('.');
apiDefinitionRefParts.exportName = exportMemberName.pop();
apiDefinitionRefParts.memberName = exportMemberName.pop();
}
else {
apiDefinitionRefParts.exportName = currentSymbol.name;
}
var apiDefinitionRef = ApiDefinitionReference_1.default.createFromParts(apiDefinitionRefParts);
// Attempt to resolve the type by checking the node modules
var referenceResolutionWarnings = [];
var resolvedApiItem = this.extractor.docItemLoader.resolveJsonReferences(apiDefinitionRef, referenceResolutionWarnings);
if (resolvedApiItem) {
// [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;
}
};
return ApiItem;
}());
/**
* Names of API items should only contain letters, numbers and underscores.
*/
ApiItem._allowedNameRegex = /^[a-zA-Z_]+[a-zA-Z_0-9]*$/;
exports.default = ApiItem;
//# sourceMappingURL=ApiItem.js.map