@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
187 lines (185 loc) • 8.79 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 */
const ts = require("typescript");
const ReleaseTag_1 = require("../aedoc/ReleaseTag");
const Markup_1 = require("../markup/Markup");
const AstMethod_1 = require("./AstMethod");
const AstProperty_1 = require("./AstProperty");
const AstItem_1 = require("./AstItem");
const AstItemContainer_1 = require("./AstItemContainer");
const TypeScriptHelpers_1 = require("../TypeScriptHelpers");
const PrettyPrinter_1 = require("../PrettyPrinter");
/**
* This class is part of the AstItem abstract syntax tree. It represents a class,
* interface, or type literal expression.
*/
class AstStructuredType extends AstItemContainer_1.default {
constructor(options) {
super(options);
this._processedMemberNames = new Set();
this._setterNames = new Set();
this._classLikeDeclaration = options.declaration;
this.type = this.typeChecker.getDeclaredTypeOfSymbol(this.declarationSymbol);
if (this.declarationSymbol.flags & ts.SymbolFlags.Interface) {
this.kind = AstItem_1.AstItemKind.Interface;
}
else if (this.declarationSymbol.flags & ts.SymbolFlags.TypeLiteral) {
this.kind = AstItem_1.AstItemKind.TypeLiteral;
}
else {
this.kind = AstItem_1.AstItemKind.Class;
}
for (const memberDeclaration of this._classLikeDeclaration.members) {
const memberSymbol = TypeScriptHelpers_1.default.tryGetSymbolForDeclaration(memberDeclaration);
if (memberSymbol) {
this._processMember(memberSymbol, memberDeclaration);
}
else {
// If someone put an extra semicolon after their function, we don't care about that
if (memberDeclaration.kind !== ts.SyntaxKind.SemicolonClassElement) {
// If there is some other non-semantic junk, add a warning so we can investigate it
this.reportWarning(PrettyPrinter_1.default.formatFileAndLineNumber(memberDeclaration)
+ `: No semantic information for "${memberDeclaration.getText()}"`);
}
}
}
// If there is a getter and no setter, mark it as readonly.
for (const member of this.getSortedMemberItems()) {
const memberSymbol = TypeScriptHelpers_1.default.tryGetSymbolForDeclaration(member.getDeclaration());
if (memberSymbol && (memberSymbol.flags === ts.SymbolFlags.GetAccessor)) {
if (!this._setterNames.has(member.name)) {
member.isReadOnly = true;
}
}
}
// Check for heritage clauses (implements and extends)
if (this._classLikeDeclaration.heritageClauses) {
for (const heritage of this._classLikeDeclaration.heritageClauses) {
const typeText = heritage.types && heritage.types.length
&& heritage.types[0].expression
? heritage.types[0].expression.getText() : undefined;
if (heritage.token === ts.SyntaxKind.ExtendsKeyword) {
this.extends = typeText;
}
else if (heritage.token === ts.SyntaxKind.ImplementsKeyword) {
this.implements = typeText;
}
}
}
// Check for type parameters
if (this._classLikeDeclaration.typeParameters && this._classLikeDeclaration.typeParameters.length) {
if (!this.typeParameters) {
this.typeParameters = [];
}
for (const param of this._classLikeDeclaration.typeParameters) {
this.typeParameters.push(param.getText());
}
}
// Throw errors for setters that don't have a corresponding getter
this._setterNames.forEach((setterName) => {
if (!this.getMemberItem(setterName)) {
// Normally we treat API design changes as warnings rather than errors. However,
// a missing getter is bizarre enough that it's reasonable to assume it's a mistake,
// not a conscious design choice.
this.reportError(`The "${setterName}" property has a setter, but no a getter`);
}
});
}
/**
* @virtual
*/
visitTypeReferencesForAstItem() {
super.visitTypeReferencesForAstItem();
// Collect type references from the base classes
if (this._classLikeDeclaration && this._classLikeDeclaration.heritageClauses) {
for (const clause of this._classLikeDeclaration.heritageClauses) {
this.visitTypeReferencesForNode(clause);
}
}
}
/**
* Returns a line of text such as "class MyClass extends MyBaseClass", excluding the
* curly braces and body. The name "MyClass" will be the public name seen by external
* callers, not the declared name of the class; @see AstItem.name documentation for details.
*/
getDeclarationLine() {
let result = '';
if (this.kind !== AstItem_1.AstItemKind.TypeLiteral) {
result += (this.declarationSymbol.flags & ts.SymbolFlags.Interface)
? 'interface ' : 'class ';
result += this.name;
if (this._classLikeDeclaration.typeParameters) {
result += '<';
result += this._classLikeDeclaration.typeParameters
.map((param) => param.getText())
.join(', ');
result += '>';
}
if (this._classLikeDeclaration.heritageClauses) {
result += ' ';
result += this._classLikeDeclaration.heritageClauses
.map((clause) => clause.getText())
.join(', ');
}
}
return result;
}
onCompleteInitialization() {
super.onCompleteInitialization();
// Is the constructor internal?
for (const member of this.getSortedMemberItems()) {
if (member.kind === AstItem_1.AstItemKind.Constructor) {
if (member.documentation.releaseTag === ReleaseTag_1.ReleaseTag.Internal) {
// Add a boilerplate notice for classes with internal constructors
this.documentation.remarks.unshift(...Markup_1.Markup.createTextElements(`The constructor for this class is marked as internal. Third-party code`
+ ` should not call the constructor directly or create subclasses that extend the ${this.name} class.`), Markup_1.Markup.PARAGRAPH);
}
}
}
}
_processMember(memberSymbol, memberDeclaration) {
if (memberDeclaration.modifiers) {
for (let i = 0; i < memberDeclaration.modifiers.length; i++) {
const modifier = memberDeclaration.modifiers[i];
if (modifier.kind === ts.SyntaxKind.PrivateKeyword) {
return;
}
}
}
if (this._processedMemberNames.has(memberSymbol.name)) {
if (memberSymbol.flags === ts.SymbolFlags.SetAccessor) {
// In case of setters, just add them to a list to check later if they have a getter
this._setterNames.add(memberSymbol.name);
}
// Throw an error for duplicate names, because we use names as identifiers
// @todo #261549 Define an AEDoc tag to allow defining an identifier for overloaded methods eg. @overload method2
return;
}
// Proceed to add the member
this._processedMemberNames.add(memberSymbol.name);
const memberOptions = {
context: this.context,
declaration: memberDeclaration,
declarationSymbol: memberSymbol,
jsdocNode: memberDeclaration
};
if (memberSymbol.flags & (ts.SymbolFlags.Method |
ts.SymbolFlags.Constructor |
ts.SymbolFlags.Signature |
ts.SymbolFlags.Function)) {
this.addMemberItem(new AstMethod_1.default(memberOptions));
}
else if (memberSymbol.flags & (ts.SymbolFlags.Property |
ts.SymbolFlags.GetAccessor)) {
this.addMemberItem(new AstProperty_1.default(memberOptions));
}
else {
this.reportWarning(`Unsupported member: ${memberSymbol.name}`);
}
}
}
exports.default = AstStructuredType;
//# sourceMappingURL=AstStructuredType.js.map