polymer-analyzer
Version:
Static analysis for Web Components
849 lines • 36 kB
JavaScript
"use strict";
/**
* @license
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const generator_1 = require("@babel/generator");
const babel = require("@babel/types");
const doctrine = require("doctrine");
const model_1 = require("../model/model");
const declaration_property_handlers_1 = require("../polymer/declaration-property-handlers");
const polymer_element_1 = require("../polymer/polymer-element");
const polymer2_config_1 = require("../polymer/polymer2-config");
const polymer2_mixin_scanner_1 = require("../polymer/polymer2-mixin-scanner");
const astValue = require("./ast-value");
const ast_value_1 = require("./ast-value");
const esutil = require("./esutil");
const esutil_1 = require("./esutil");
const jsdoc = require("./jsdoc");
/**
* Find and classify classes from source code.
*
* Currently this has a bunch of Polymer stuff baked in that shouldn't be here
* in order to support generating only one feature for stuff that's essentially
* more specific kinds of classes, like Elements, PolymerElements, Mixins, etc.
*
* In a future change we'll add a mechanism whereby plugins can claim and
* specialize classes.
*/
class ClassScanner {
scan(document, visit) {
return __awaiter(this, void 0, void 0, function* () {
const classFinder = new ClassFinder(document);
const elementDefinitionFinder = new CustomElementsDefineCallFinder(document);
const prototypeMemberFinder = new PrototypeMemberFinder(document);
yield visit(prototypeMemberFinder);
const mixinFinder = new polymer2_mixin_scanner_1.MixinVisitor(document, prototypeMemberFinder);
// Find all classes and all calls to customElements.define()
yield Promise.all([
visit(classFinder),
visit(elementDefinitionFinder),
visit(mixinFinder),
]);
const mixins = mixinFinder.mixins;
const elementDefinitionsByClassName = new Map();
// For classes that show up as expressions in the second argument position
// of a customElements.define call.
const elementDefinitionsByClassExpression = new Map();
for (const defineCall of elementDefinitionFinder.calls) {
// MaybeChainedIdentifier is invented below. It's like Identifier, but it
// includes 'Polymer.Element' as a name.
if (defineCall.class_.type === 'MaybeChainedIdentifier') {
elementDefinitionsByClassName.set(defineCall.class_.name, defineCall);
}
else {
elementDefinitionsByClassExpression.set(defineCall.class_, defineCall);
}
}
// TODO(rictic): emit ElementDefineCallFeatures for define calls that don't
// map to any local classes?
const mixinClassExpressions = new Set();
for (const mixin of mixins) {
if (mixin.classAstNode) {
mixinClassExpressions.add(mixin.classAstNode);
}
}
// Next we want to distinguish custom elements from other classes.
const customElements = [];
const normalClasses = [];
const classMap = new Map();
for (const class_ of classFinder.classes) {
if (class_.astNode.language === 'js' &&
mixinClassExpressions.has(class_.astNode.node)) {
// This class is a mixin and has already been handled as such.
continue;
}
if (class_.name) {
classMap.set(class_.name, class_);
}
// Class expressions inside the customElements.define call
if (babel.isClassExpression(class_.astNode.node)) {
const definition = elementDefinitionsByClassExpression.get(class_.astNode.node);
if (definition) {
customElements.push({ class_, definition });
continue;
}
}
// Classes whose names are referenced in a same-file customElements.define
const definition = elementDefinitionsByClassName.get(class_.name) ||
elementDefinitionsByClassName.get(class_.localName);
if (definition) {
customElements.push({ class_, definition });
continue;
}
// Classes explicitly defined as elements in their jsdoc tags.
// TODO(justinfagnani): remove @polymerElement support
if (jsdoc.hasTag(class_.jsdoc, 'customElement') ||
jsdoc.hasTag(class_.jsdoc, 'polymerElement')) {
customElements.push({ class_ });
continue;
}
// Classes that aren't custom elements, or at least, aren't obviously.
normalClasses.push(class_);
}
for (const [name, members] of prototypeMemberFinder.members) {
if (classMap.has(name)) {
const cls = classMap.get(name);
cls.finishInitialization(members.methods, members.properties);
}
}
const scannedFeatures = [];
for (const element of customElements) {
scannedFeatures.push(this._makeElementFeature(element, document));
}
for (const scannedClass of normalClasses) {
scannedFeatures.push(scannedClass);
}
for (const mixin of mixins) {
scannedFeatures.push(mixin);
}
const collapsedClasses = this.collapseEphemeralSuperclasses(scannedFeatures, classFinder);
return {
features: collapsedClasses,
warnings: [
...elementDefinitionFinder.warnings,
...classFinder.warnings,
...mixinFinder.warnings,
]
};
});
}
/**
* Handle the pattern where a class's superclass is declared as a separate
* variable, usually so that mixins can be applied in a way that is compatible
* with the Closure compiler. We consider a class ephemeral if:
*
* 1) It is the superclass of one or more classes.
* 2) It is declared using a const, let, or var.
* 3) It is annotated as @private.
*/
collapseEphemeralSuperclasses(allClasses, classFinder) {
const possibleEphemeralsById = new Map();
const classesBySuperClassId = new Map();
for (const cls of allClasses) {
if (cls.name === undefined) {
continue;
}
if (classFinder.fromVariableDeclarators.has(cls) &&
cls.privacy === 'private') {
possibleEphemeralsById.set(cls.name, cls);
}
if (cls.superClass !== undefined) {
const superClassId = cls.superClass.identifier;
const childClasses = classesBySuperClassId.get(superClassId);
if (childClasses === undefined) {
classesBySuperClassId.set(superClassId, [cls]);
}
else {
childClasses.push(cls);
}
}
}
const ephemerals = new Set();
for (const [superClassId, childClasses] of classesBySuperClassId) {
const superClass = possibleEphemeralsById.get(superClassId);
if (superClass === undefined) {
continue;
}
let isEphemeral = false;
for (const childClass of childClasses) {
// Feature properties are readonly, hence this hacky cast. We could also
// make a new feature, but then we'd need a good way to clone a feature.
// It's pretty safe because we're still in the construction phase for
// scanned classes, so we know nothing else could be relying on the
// previous value yet.
childClass.superClass = superClass.superClass;
childClass.mixins.push(...superClass.mixins);
isEphemeral = true;
}
if (isEphemeral) {
ephemerals.add(superClass);
}
}
return allClasses.filter((cls) => !ephemerals.has(cls));
}
_makeElementFeature(element, document) {
const class_ = element.class_;
const astNode = element.class_.astNode;
const docs = element.class_.jsdoc;
const customElementTag = jsdoc.getTag(class_.jsdoc, 'customElement');
let tagName = undefined;
if (element.definition &&
element.definition.tagName.type === 'string-literal') {
tagName = element.definition.tagName.value;
}
else if (customElementTag && customElementTag.description) {
tagName = customElementTag.description;
}
else if (babel.isClassExpression(astNode.node) ||
babel.isClassDeclaration(astNode.node)) {
tagName = polymer2_config_1.getIsValue(astNode.node);
}
let warnings = [];
let scannedElement;
let methods = new Map();
let staticMethods = new Map();
let observers = [];
// This will cover almost all classes, except those defined only by
// applying a mixin. e.g. const MyElem = Mixin(HTMLElement)
if (babel.isClassExpression(astNode.node) ||
babel.isClassDeclaration(astNode.node)) {
const observersResult = this._getObservers(astNode.node, document);
observers = [];
if (observersResult) {
observers = observersResult.observers;
warnings = warnings.concat(observersResult.warnings);
}
const polymerProps = polymer2_config_1.getPolymerProperties(astNode.node, document);
for (const prop of polymerProps) {
const constructorProp = class_.properties.get(prop.name);
let finalProp;
if (constructorProp) {
finalProp = polymer_element_1.mergePropertyDeclarations(constructorProp, prop);
}
else {
finalProp = prop;
}
class_.properties.set(prop.name, finalProp);
}
methods = esutil_1.getMethods(astNode.node, document);
staticMethods = esutil_1.getStaticMethods(astNode.node, document);
}
const extends_ = getExtendsTypeName(docs);
// TODO(justinfagnani): Infer mixin applications and superclass from AST.
scannedElement = new polymer_element_1.ScannedPolymerElement({
className: class_.name,
tagName,
astNode,
statementAst: class_.statementAst,
properties: [...class_.properties.values()],
methods,
staticMethods,
observers,
events: astNode.language === 'js' ?
esutil.getEventComments(astNode.node) :
new Map(),
attributes: new Map(),
behaviors: [],
extends: extends_,
listeners: [],
description: class_.description,
sourceRange: class_.sourceRange,
superClass: class_.superClass,
jsdoc: class_.jsdoc,
abstract: class_.abstract,
mixins: class_.mixins,
privacy: class_.privacy,
isLegacyFactoryCall: false,
});
if (babel.isClassExpression(astNode.node) ||
babel.isClassDeclaration(astNode.node)) {
const observedAttributes = this._getObservedAttributes(astNode.node, document);
if (observedAttributes != null) {
// If a class defines observedAttributes, it overrides what the base
// classes defined.
// TODO(justinfagnani): define and handle composition patterns.
scannedElement.attributes.clear();
for (const attr of observedAttributes) {
scannedElement.attributes.set(attr.name, attr);
}
}
}
warnings.forEach((w) => scannedElement.warnings.push(w));
return scannedElement;
}
_getObservers(node, document) {
const returnedValue = polymer2_config_1.getStaticGetterValue(node, 'observers');
if (returnedValue) {
return declaration_property_handlers_1.extractObservers(returnedValue, document);
}
}
_getObservedAttributes(node, document) {
const returnedValue = polymer2_config_1.getStaticGetterValue(node, 'observedAttributes');
if (returnedValue && babel.isArrayExpression(returnedValue)) {
return this._extractAttributesFromObservedAttributes(returnedValue, document);
}
}
/**
* Extract attributes from the array expression inside a static
* observedAttributes method.
*
* e.g.
* static get observedAttributes() {
* return [
* /** @type {boolean} When given the element is totally inactive *\/
* 'disabled',
* /** @type {boolean} When given the element is expanded *\/
* 'open'
* ];
* }
*/
_extractAttributesFromObservedAttributes(arry, document) {
const results = [];
for (const expr of arry.elements) {
const value = astValue.expressionToValue(expr);
if (value && typeof value === 'string') {
let description = '';
let type = null;
const comment = esutil.getAttachedComment(expr);
if (comment) {
const annotation = jsdoc.parseJsdoc(comment);
description = annotation.description || description;
const tags = annotation.tags || [];
for (const tag of tags) {
if (tag.title === 'type') {
type = type || doctrine.type.stringify(tag.type);
}
// TODO(justinfagnani): this appears wrong, any tag could have a
// description do we really let any tag's description override
// the previous?
description = description || tag.description || '';
}
}
const attribute = {
name: value,
description: description,
sourceRange: document.sourceRangeForNode(expr),
astNode: { language: 'js', containingDocument: document, node: expr },
warnings: [],
};
if (type) {
attribute.type = type;
}
results.push(attribute);
}
}
return results;
}
}
exports.ClassScanner = ClassScanner;
class PrototypeMemberFinder {
constructor(document) {
this.members = new model_1.MapWithDefault(() => ({
methods: new Map(),
properties: new Map()
}));
this._document = document;
}
enterExpressionStatement(node) {
if (babel.isAssignmentExpression(node.expression)) {
this._createMemberFromAssignment(node.expression, getJSDocAnnotationForNode(node));
}
else if (babel.isMemberExpression(node.expression)) {
this._createMemberFromMemberExpression(node.expression, getJSDocAnnotationForNode(node));
}
}
_createMemberFromAssignment(node, jsdocAnn) {
if (!babel.isMemberExpression(node.left) ||
!babel.isMemberExpression(node.left.object) ||
!babel.isIdentifier(node.left.property)) {
return;
}
const leftExpr = node.left.object;
const leftProperty = node.left.property;
const cls = ast_value_1.getIdentifierName(leftExpr.object);
if (!cls || ast_value_1.getIdentifierName(leftExpr.property) !== 'prototype') {
return;
}
if (babel.isFunctionExpression(node.right)) {
const prop = this._createMethodFromExpression(leftProperty.name, node.right, jsdocAnn);
if (prop) {
this._addMethodToClass(cls, prop);
}
}
else {
const method = this._createPropertyFromExpression(leftProperty.name, node, jsdocAnn);
if (method) {
this._addPropertyToClass(cls, method);
}
}
}
_addMethodToClass(cls, member) {
const classMembers = this.members.get(cls);
classMembers.methods.set(member.name, member);
}
_addPropertyToClass(cls, member) {
const classMembers = this.members.get(cls);
classMembers.properties.set(member.name, member);
}
_createMemberFromMemberExpression(node, jsdocAnn) {
const left = node.object;
// we only want `something.prototype.member`
if (!babel.isIdentifier(node.property) || !babel.isMemberExpression(left) ||
ast_value_1.getIdentifierName(left.property) !== 'prototype') {
return;
}
const cls = ast_value_1.getIdentifierName(left.object);
if (!cls) {
return;
}
if (jsdoc.hasTag(jsdocAnn, 'function')) {
const method = this._createMethodFromExpression(node.property.name, node, jsdocAnn);
if (method) {
this._addMethodToClass(cls, method);
}
}
else {
const prop = this._createPropertyFromExpression(node.property.name, node, jsdocAnn);
if (prop) {
this._addPropertyToClass(cls, prop);
}
}
}
_createPropertyFromExpression(name, node, jsdocAnn) {
let description;
let type;
let readOnly = false;
const privacy = esutil_1.getOrInferPrivacy(name, jsdocAnn);
const sourceRange = this._document.sourceRangeForNode(node);
const warnings = [];
if (jsdocAnn) {
description = jsdoc.getDescription(jsdocAnn);
readOnly = jsdoc.hasTag(jsdocAnn, 'readonly');
}
let detectedType;
if (babel.isAssignmentExpression(node)) {
detectedType =
esutil_1.getClosureType(node.right, jsdocAnn, sourceRange, this._document);
}
else {
detectedType =
esutil_1.getClosureType(node, jsdocAnn, sourceRange, this._document);
}
if (detectedType.successful) {
type = detectedType.value;
}
else {
warnings.push(detectedType.error);
type = '?';
}
return {
name,
astNode: { language: 'js', containingDocument: this._document, node },
type,
jsdoc: jsdocAnn,
sourceRange,
description,
privacy,
warnings,
readOnly,
};
}
_createMethodFromExpression(name, node, jsdocAnn) {
let description;
let ret;
const privacy = esutil_1.getOrInferPrivacy(name, jsdocAnn);
const params = new Map();
if (jsdocAnn) {
description = jsdoc.getDescription(jsdocAnn);
ret = esutil_1.getReturnFromAnnotation(jsdocAnn);
if (babel.isFunctionExpression(node)) {
(node.params || []).forEach((nodeParam) => {
const param = esutil_1.toMethodParam(nodeParam, jsdocAnn);
params.set(param.name, param);
});
}
else {
for (const tag of (jsdocAnn.tags || [])) {
if (tag.title !== 'param' || !tag.name) {
continue;
}
let tagType;
let tagDescription;
if (tag.type) {
tagType = doctrine.type.stringify(tag.type);
}
if (tag.description) {
tagDescription = tag.description;
}
params.set(tag.name, { name: tag.name, type: tagType, description: tagDescription });
}
}
}
if (ret === undefined && babel.isFunctionExpression(node)) {
ret = esutil_1.inferReturnFromBody(node);
}
return {
name,
type: ret !== undefined ? ret.type : undefined,
description,
sourceRange: this._document.sourceRangeForNode(node),
warnings: [],
astNode: { language: 'js', containingDocument: this._document, node },
jsdoc: jsdocAnn,
params: Array.from(params.values()),
return: ret,
privacy
};
}
}
exports.PrototypeMemberFinder = PrototypeMemberFinder;
/**
* Finds all classes and matches them up with their best jsdoc comment.
*/
class ClassFinder {
constructor(document) {
this.classes = [];
this.warnings = [];
this.fromVariableDeclarators = new Set();
this.alreadyMatched = new Set();
this._document = document;
}
enterAssignmentExpression(node, _parent, path) {
this.handleGeneralAssignment(astValue.getIdentifierName(node.left), node.right, path);
}
enterVariableDeclarator(node, _parent, path) {
if (node.init) {
this.handleGeneralAssignment(astValue.getIdentifierName(node.id), node.init, path);
}
}
enterFunctionDeclaration(node, _parent, path) {
this.handleGeneralAssignment(astValue.getIdentifierName(node.id), node.body, path);
}
/** Generalizes over variable declarators and assignment expressions. */
handleGeneralAssignment(assignedName, value, path) {
const doc = jsdoc.parseJsdoc(esutil.getBestComment(path) || '');
if (babel.isClassExpression(value)) {
const name = assignedName ||
value.id && astValue.getIdentifierName(value.id) || undefined;
this._classFound(name, doc, value, path);
}
else if (jsdoc.hasTag(doc, 'constructor') ||
// TODO(justinfagnani): remove @polymerElement support
jsdoc.hasTag(doc, 'customElement') ||
jsdoc.hasTag(doc, 'polymerElement')) {
this._classFound(assignedName, doc, value, path);
}
}
enterClassExpression(node, parent, path) {
// Class expressions may be on the right hand side of assignments, so
// we may have already handled this expression from the parent or
// grandparent node. Class declarations can't be on the right hand side of
// assignments, so they'll definitely only be handled once.
if (this.alreadyMatched.has(node)) {
return;
}
const name = node.id ? astValue.getIdentifierName(node.id) : undefined;
const comment = esutil.getAttachedComment(node) ||
esutil.getAttachedComment(parent) || '';
this._classFound(name, jsdoc.parseJsdoc(comment), node, path);
}
enterClassDeclaration(node, parent, path) {
const name = astValue.getIdentifierName(node.id);
const comment = esutil.getAttachedComment(node) ||
esutil.getAttachedComment(parent) || '';
this._classFound(name, jsdoc.parseJsdoc(comment), node, path);
}
_classFound(name, doc, astNode, path) {
const namespacedName = name && ast_value_1.getNamespacedIdentifier(name, doc);
const warnings = [];
const properties = extractPropertiesFromClass(astNode, this._document);
const methods = esutil_1.getMethods(astNode, this._document);
const constructorMethod = esutil_1.getConstructorMethod(astNode, this._document);
const scannedClass = new model_1.ScannedClass(namespacedName, name, { language: 'js', containingDocument: this._document, node: astNode }, esutil.getCanonicalStatement(path), doc, (doc.description || '').trim(), this._document.sourceRangeForNode(astNode), properties, methods, constructorMethod, esutil_1.getStaticMethods(astNode, this._document), this._getExtends(astNode, doc, warnings, this._document, path), jsdoc.getMixinApplications(this._document, astNode, doc, warnings, path), esutil_1.getOrInferPrivacy(namespacedName || '', doc), warnings, jsdoc.hasTag(doc, 'abstract'), jsdoc.extractDemos(doc));
this.classes.push(scannedClass);
if (babel.isVariableDeclarator(path.node)) {
this.fromVariableDeclarators.add(scannedClass);
}
if (babel.isClassExpression(astNode)) {
this.alreadyMatched.add(astNode);
}
}
/**
* Returns the name of the superclass, if any.
*/
_getExtends(node, docs, warnings, document, path) {
const extendsId = getExtendsTypeName(docs);
// prefer @extends annotations over extends clauses
if (extendsId !== undefined) {
// TODO(justinfagnani): we need source ranges for jsdoc annotations
const sourceRange = document.sourceRangeForNode(node);
if (extendsId == null) {
warnings.push(new model_1.Warning({
code: 'class-extends-annotation-no-id',
message: '@extends annotation with no identifier',
severity: model_1.Severity.WARNING,
sourceRange,
parsedDocument: this._document
}));
}
else {
return new model_1.ScannedReference('class', extendsId, sourceRange, undefined, path);
}
}
else if (babel.isClassDeclaration(node) || babel.isClassExpression(node)) {
// If no @extends tag, look for a superclass.
const superClass = node.superClass;
if (superClass != null) {
let extendsId = ast_value_1.getIdentifierName(superClass);
if (extendsId != null) {
if (extendsId.startsWith('window.')) {
extendsId = extendsId.substring('window.'.length);
}
const sourceRange = document.sourceRangeForNode(superClass);
return new model_1.ScannedReference('class', extendsId, sourceRange, {
language: 'js',
node: node.superClass,
containingDocument: document
}, path);
}
}
}
}
}
/** Finds calls to customElements.define() */
class CustomElementsDefineCallFinder {
constructor(document) {
this.warnings = [];
this.calls = [];
this._document = document;
}
enterCallExpression(node) {
const callee = astValue.getIdentifierName(node.callee);
if (!(callee === 'window.customElements.define' ||
callee === 'customElements.define')) {
return;
}
const tagNameExpression = this._getTagNameExpression(node.arguments[0]);
if (tagNameExpression == null) {
return;
}
const elementClassExpression = this._getElementClassExpression(node.arguments[1]);
if (elementClassExpression == null) {
return;
}
this.calls.push({ tagName: tagNameExpression, class_: elementClassExpression });
}
_getTagNameExpression(expression) {
if (expression == null) {
return;
}
const tryForLiteralString = astValue.expressionToValue(expression);
if (tryForLiteralString != null &&
typeof tryForLiteralString === 'string') {
return {
type: 'string-literal',
value: tryForLiteralString,
sourceRange: this._document.sourceRangeForNode(expression)
};
}
if (babel.isMemberExpression(expression)) {
// Might be something like MyElement.is
const isPropertyNameIs = (babel.isIdentifier(expression.property) &&
expression.property.name === 'is') ||
(astValue.expressionToValue(expression.property) === 'is');
const className = astValue.getIdentifierName(expression.object);
if (isPropertyNameIs && className) {
return {
type: 'is',
className,
classNameSourceRange: this._document.sourceRangeForNode(expression.object)
};
}
}
this.warnings.push(new model_1.Warning({
code: 'cant-determine-element-tagname',
message: `Unable to evaluate this expression down to a definitive string ` +
`tagname.`,
severity: model_1.Severity.WARNING,
sourceRange: this._document.sourceRangeForNode(expression),
parsedDocument: this._document
}));
return undefined;
}
_getElementClassExpression(elementDefn) {
if (elementDefn == null) {
return null;
}
const className = astValue.getIdentifierName(elementDefn);
if (className) {
return {
type: 'MaybeChainedIdentifier',
name: className,
sourceRange: this._document.sourceRangeForNode(elementDefn)
};
}
if (babel.isClassExpression(elementDefn)) {
return elementDefn;
}
this.warnings.push(new model_1.Warning({
code: 'cant-determine-element-class',
message: `Unable to evaluate this expression down to a class reference.`,
severity: model_1.Severity.WARNING,
sourceRange: this._document.sourceRangeForNode(elementDefn),
parsedDocument: this._document,
}));
return null;
}
}
function extractPropertiesFromClass(astNode, document) {
const properties = new Map();
if (!babel.isClass(astNode)) {
return properties;
}
const construct = esutil_1.getConstructorClassMethod(astNode);
if (construct) {
const props = extractPropertiesFromConstructor(construct, document);
for (const prop of props.values()) {
properties.set(prop.name, prop);
}
}
for (const prop of esutil
.extractPropertiesFromClassOrObjectBody(astNode, document)
.values()) {
const existing = properties.get(prop.name);
if (!existing) {
properties.set(prop.name, prop);
}
else {
properties.set(prop.name, {
name: prop.name,
astNode: prop.astNode,
type: prop.type || existing.type,
jsdoc: prop.jsdoc,
sourceRange: prop.sourceRange,
description: prop.description || existing.description,
privacy: prop.privacy || existing.privacy,
warnings: prop.warnings,
readOnly: prop.readOnly === undefined ?
existing.readOnly : prop.readOnly
});
}
}
return properties;
}
exports.extractPropertiesFromClass = extractPropertiesFromClass;
function extractPropertyFromExpressionStatement(statement, document) {
let name;
let astNode;
let defaultValue;
if (babel.isAssignmentExpression(statement.expression)) {
// statements like:
// /** @public The foo. */
// this.foo = baz;
name = getPropertyNameOnThisExpression(statement.expression.left);
astNode = statement.expression.left;
defaultValue = generator_1.default(statement.expression.right).code;
}
else if (babel.isMemberExpression(statement.expression)) {
// statements like:
// /** @public The foo. */
// this.foo;
name = getPropertyNameOnThisExpression(statement.expression);
astNode = statement;
}
else {
return null;
}
if (name === undefined) {
return null;
}
const annotation = getJSDocAnnotationForNode(statement);
if (!annotation) {
return null;
}
return {
name,
astNode: { language: 'js', containingDocument: document, node: astNode },
type: getTypeFromAnnotation(annotation),
default: defaultValue,
jsdoc: annotation,
sourceRange: document.sourceRangeForNode(astNode),
description: jsdoc.getDescription(annotation),
privacy: esutil_1.getOrInferPrivacy(name, annotation),
warnings: [],
readOnly: jsdoc.hasTag(annotation, 'const'),
};
}
function extractPropertiesFromConstructor(method, document) {
const properties = new Map();
for (const statement of method.body.body) {
if (!babel.isExpressionStatement(statement)) {
continue;
}
const prop = extractPropertyFromExpressionStatement(statement, document);
if (!prop) {
continue;
}
properties.set(prop.name, prop);
}
return properties;
}
function getJSDocAnnotationForNode(node) {
const comment = esutil.getAttachedComment(node);
const jsdocAnn = comment === undefined ? undefined : jsdoc.parseJsdoc(comment);
if (!jsdocAnn || jsdocAnn.tags.length === 0) {
// The comment only counts if there's a jsdoc annotation in there
// somewhere.
// Otherwise it's just an assignment, maybe to a property in a
// super class or something.
return undefined;
}
return jsdocAnn;
}
function getTypeFromAnnotation(jsdocAnn) {
const typeTag = jsdoc.getTag(jsdocAnn, 'type');
let type = undefined;
if (typeTag && typeTag.type) {
type = doctrine.type.stringify(typeTag.type);
}
return type;
}
function getPropertyNameOnThisExpression(node) {
if (!babel.isMemberExpression(node) || node.computed ||
!babel.isThisExpression(node.object) ||
!babel.isIdentifier(node.property)) {
return;
}
return node.property.name;
}
/**
* Return the type name from the first @extends annotation. Supports either
* `@extends {SuperClass}` or `@extends SuperClass` forms.
*/
function getExtendsTypeName(docs) {
const tag = jsdoc.getTag(docs, 'extends');
if (!tag) {
return undefined;
}
return tag.type ? doctrine.type.stringify(tag.type) : tag.name;
}
//# sourceMappingURL=class-scanner.js.map