UNPKG

polymer-analyzer

Version:
459 lines (457 loc) 20.7 kB
"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 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 mixinFinder = new polymer2_mixin_scanner_1.MixinVisitor(document); const elementDefinitionFinder = new CustomElementsDefineCallFinder(document); // 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 = []; for (const class_ of classFinder.classes) { if (mixinClassExpressions.has(class_.astNode)) { // This class is a mixin and has already been handled as such. continue; } // Class expressions inside the customElements.define call if (class_.astNode.type === 'ClassExpression') { const definition = elementDefinitionsByClassExpression.get(class_.astNode); 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_); } 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); } return { features: scannedFeatures, warnings: [ ...elementDefinitionFinder.warnings, ...classFinder.warnings, ...mixinFinder.warnings, ] }; }); } _makeElementFeature(element, document) { const class_ = element.class_; const astNode = element.class_.astNode; const docs = element.class_.jsdoc; let tagName = undefined; // TODO(rictic): support `@customElements explicit-tag-name` from jsdoc if (element.definition && element.definition.tagName.type === 'string-literal') { tagName = element.definition.tagName.value; } else if (astNode.type === 'ClassExpression' || astNode.type === 'ClassDeclaration') { tagName = polymer2_config_1.getIsValue(astNode); } let warnings = []; let scannedElement; let properties = []; 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 (astNode.type === 'ClassExpression' || astNode.type === 'ClassDeclaration') { const observersResult = this._getObservers(astNode, document); observers = []; if (observersResult) { observers = observersResult.observers; warnings = warnings.concat(observersResult.warnings); } properties = polymer2_config_1.getPolymerProperties(astNode, document); methods = esutil_1.getMethods(astNode, document); staticMethods = esutil_1.getStaticMethods(astNode, document); } const extendsTag = jsdoc.getTag(docs, 'extends'); const extends_ = extendsTag !== undefined ? extendsTag.name : undefined; // TODO(justinfagnani): Infer mixin applications and superclass from AST. scannedElement = new polymer_element_1.ScannedPolymerElement({ className: class_.name, tagName, astNode, properties, methods, staticMethods, observers, events: esutil.getEventComments(astNode), 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 }); if (astNode.type === 'ClassExpression' || astNode.type === 'ClassDeclaration') { const observedAttributes = this._getObservedAttributes(astNode, 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 && returnedValue.type === 'ArrayExpression') { 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: expr, warnings: [], }; if (type) { attribute.type = type; } results.push(attribute); } } return results; } } exports.ClassScanner = ClassScanner; /** * Finds all classes and matches them up with their best jsdoc comment. */ class ClassFinder { constructor(document) { this.classes = []; this.warnings = []; this.alreadyMatched = new Set(); this._document = document; } enterAssignmentExpression(node, parent) { this.handleGeneralAssignment(astValue.getIdentifierName(node.left), node.right, node, parent); } enterVariableDeclarator(node, parent) { if (node.init) { this.handleGeneralAssignment(astValue.getIdentifierName(node.id), node.init, node, parent); } } /** Generalizes over variable declarators and assignment expressions. */ handleGeneralAssignment(assignedName, value, assignment, statement) { const comment = esutil.getAttachedComment(value) || esutil.getAttachedComment(assignment) || esutil.getAttachedComment(statement) || ''; const doc = jsdoc.parseJsdoc(comment); if (value.type === 'ClassExpression') { const name = assignedName || value.id && astValue.getIdentifierName(value.id); this._classFound(name, doc, value); } else { // TODO(justinfagnani): remove @polymerElement support if (jsdoc.hasTag(doc, 'customElement') || jsdoc.hasTag(doc, 'polymerElement')) { this._classFound(assignedName, doc, value); } } } enterClassExpression(node, parent) { // 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); } enterClassDeclaration(node, parent) { const name = astValue.getIdentifierName(node.id); const comment = esutil.getAttachedComment(node) || esutil.getAttachedComment(parent) || ''; this._classFound(name, jsdoc.parseJsdoc(comment), node); } _classFound(name, doc, astNode) { const namespacedName = name && ast_value_1.getNamespacedIdentifier(name, doc); const warnings = []; // TODO(rictic): scan the constructor and look for assignments to this.foo // to determine properties. this.classes.push(new model_1.ScannedClass(namespacedName, name, astNode, doc, (doc.description || '').trim(), this._document.sourceRangeForNode(astNode), new Map(), esutil_1.getMethods(astNode, this._document), esutil_1.getStaticMethods(astNode, this._document), this._getExtends(astNode, doc, warnings, this._document), jsdoc.getMixinApplications(this._document, astNode, doc, warnings), esutil_1.getOrInferPrivacy(namespacedName || '', doc), warnings, jsdoc.hasTag(doc, 'abstract'), jsdoc.extractDemos(doc))); if (astNode.type === 'ClassExpression') { this.alreadyMatched.add(astNode); } } /** * Returns the name of the superclass, if any. */ _getExtends(node, docs, warnings, document) { const extendsAnnotations = docs.tags.filter((tag) => tag.title === 'extends'); // prefer @extends annotations over extends clauses if (extendsAnnotations.length > 0) { const extendsId = extendsAnnotations[0].name; // 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(extendsId, sourceRange); } } else if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') { // If no @extends tag, look for a superclass. const superClass = node.superClass; if (superClass != null) { const extendsId = ast_value_1.getIdentifierName(superClass); if (extendsId != null) { const sourceRange = document.sourceRangeForNode(superClass); return new model_1.ScannedReference(extendsId, sourceRange); } } } } } /** 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 (expression.type === 'MemberExpression') { // Might be something like MyElement.is const isPropertyNameIs = (expression.property.type === 'Identifier' && 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 (elementDefn.type === 'ClassExpression') { 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; } } //# sourceMappingURL=class-scanner.js.map