polymer-analyzer
Version:
Static analysis for Web Components
305 lines (303 loc) • 13.2 kB
JavaScript
;
/**
* @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 estraverse = require("estraverse");
const ast_value_1 = require("../javascript/ast-value");
const esutil_1 = require("../javascript/esutil");
const jsdoc = require("../javascript/jsdoc");
const model_1 = require("../model/model");
const declaration_property_handlers_1 = require("./declaration-property-handlers");
const declaration_property_handlers_2 = require("./declaration-property-handlers");
const docs = require("./docs");
const expression_scanner_1 = require("./expression-scanner");
const js_utils_1 = require("./js-utils");
const polymer_element_1 = require("./polymer-element");
class PolymerElementScanner {
scan(document, visit) {
return __awaiter(this, void 0, void 0, function* () {
const visitor = new ElementVisitor(document);
yield visit(visitor);
return { features: visitor.features, warnings: visitor.warnings };
});
}
}
exports.PolymerElementScanner = PolymerElementScanner;
class ElementVisitor {
constructor(document) {
this.features = [];
/**
* The element being built during a traversal;
*/
this.element = null;
this.propertyHandlers = null;
this.classDetected = false;
this.warnings = [];
this.document = document;
}
enterClassDeclaration(node, _) {
this.classDetected = true;
const className = node.id.name;
const docs = jsdoc.parseJsdoc(esutil_1.getAttachedComment(node) || '');
this.element = new polymer_element_1.ScannedPolymerElement({
astNode: node,
description: docs.description,
events: esutil_1.getEventComments(node),
sourceRange: this.document.sourceRangeForNode(node), className,
privacy: esutil_1.getOrInferPrivacy(className, docs),
abstract: jsdoc.hasTag(docs, 'abstract'),
attributes: new Map(),
properties: [],
behaviors: [],
extends: undefined,
jsdoc: docs,
listeners: [],
methods: new Map(),
staticMethods: new Map(),
mixins: [],
observers: [],
superClass: undefined,
tagName: undefined
});
this.propertyHandlers =
declaration_property_handlers_2.declarationPropertyHandlers(this.element, this.document);
}
leaveClassDeclaration(_, _parent) {
for (const property of this.element.properties.values()) {
docs.annotate(property);
}
// TODO(justinfagnani): this looks wrong, class definitions can be nested
// so a definition in a method in a Polymer() declaration would end the
// declaration early. We should track which class induced the current
// element and finish the element when leaving _that_ class.
this.element = null;
this.propertyHandlers = null;
this.classDetected = false;
}
enterAssignmentExpression(node, _) {
if (!this.element) {
return;
}
const left = node.left;
if (left && left.object && left.object.type !== 'ThisExpression') {
return;
}
const prop = left.property;
if (prop && prop.name && this.propertyHandlers) {
const name = prop.name;
if (name in this.propertyHandlers) {
this.propertyHandlers[name](node.right);
}
}
}
enterMethodDefinition(node, _parent) {
const element = this.element;
if (!element) {
return;
}
const prop = Object.assign({}, node, {
method: true,
shorthand: false,
computed: false,
});
if (node.kind === 'get') {
const returnStatement = node.value.body.body[0];
const argument = returnStatement.argument;
const propDesc = js_utils_1.toScannedPolymerProperty(prop, this.document.sourceRangeForNode(node), this.document);
docs.annotate(propDesc);
// We only support observers and behaviors getters that return array
// literals.
if ((propDesc.name === 'behaviors' || propDesc.name === 'observers') &&
!Array.isArray(argument.elements)) {
return;
}
if (propDesc.name === 'behaviors') {
argument.elements.forEach((argNode) => {
const result = declaration_property_handlers_1.getBehaviorAssignmentOrWarning(argNode, this.document);
if (result.kind === 'warning') {
element.warnings.push(result.warning);
}
else {
element.behaviorAssignments.push(result.assignment);
}
});
return;
}
if (propDesc.name === 'observers') {
argument.elements.forEach((elementObject) => {
const parseResult = expression_scanner_1.parseExpressionInJsStringLiteral(this.document, elementObject, 'callExpression');
element.warnings = element.warnings.concat(parseResult.warnings);
let expressionText = undefined;
if (elementObject.type === 'Literal') {
expressionText = elementObject.raw;
}
element.observers.push({
javascriptNode: elementObject,
expression: expressionText,
parsedExpression: parseResult.databinding
});
});
return;
}
element.addProperty(propDesc);
return;
}
if (node.kind === 'method') {
const methodDesc = esutil_1.toScannedMethod(prop, this.document.sourceRangeForNode(node), this.document);
docs.annotate(methodDesc);
element.addMethod(methodDesc);
}
}
enterCallExpression(node, parent) {
// When dealing with a class, enterCallExpression is called after the
// parsing actually starts
if (this.classDetected) {
return estraverse.VisitorOption.Skip;
}
const callee = node.callee;
if (callee.type === 'Identifier') {
if (callee.name === 'Polymer') {
const rawDescription = esutil_1.getAttachedComment(parent);
let className = undefined;
if (parent.type === 'AssignmentExpression') {
className = ast_value_1.getIdentifierName(parent.left);
}
else if (parent.type === 'VariableDeclarator') {
className = ast_value_1.getIdentifierName(parent.id);
}
const jsDoc = jsdoc.parseJsdoc(rawDescription || '');
this.element = new polymer_element_1.ScannedPolymerElement({
className,
astNode: node,
description: jsDoc.description,
events: esutil_1.getEventComments(parent),
sourceRange: this.document.sourceRangeForNode(node.arguments[0]),
privacy: esutil_1.getOrInferPrivacy('', jsDoc),
abstract: jsdoc.hasTag(jsDoc, 'abstract'),
attributes: new Map(),
properties: [],
behaviors: [],
extends: undefined,
jsdoc: jsDoc,
listeners: [],
methods: new Map(),
staticMethods: new Map(),
mixins: [],
observers: [],
superClass: undefined,
tagName: undefined
});
this.element.description = (this.element.description || '').trim();
this.propertyHandlers =
declaration_property_handlers_2.declarationPropertyHandlers(this.element, this.document);
}
}
}
leaveCallExpression(node, _) {
const callee = node.callee;
const args = node.arguments;
if (callee.type === 'Identifier' && args.length === 1 &&
args[0].type === 'ObjectExpression') {
if (callee.name === 'Polymer') {
if (this.element) {
this.features.push(this.element);
this.element = null;
this.propertyHandlers = null;
}
}
}
}
enterObjectExpression(node, _) {
// When dealing with a class, there is no single object that we can
// parse to retrieve all properties.
if (this.classDetected) {
return estraverse.VisitorOption.Skip;
}
const element = this.element;
if (element) {
const getters = {};
const setters = {};
const definedProperties = {};
for (const prop of node.properties) {
const name = esutil_1.objectKeyToString(prop.key);
if (!name) {
element.warnings.push(new model_1.Warning({
message: `Can't determine name for property key from expression with type ${prop.key
.type}.`,
code: 'cant-determine-property-name',
severity: model_1.Severity.WARNING,
sourceRange: this.document.sourceRangeForNode(prop.key),
parsedDocument: this.document
}));
continue;
}
if (!this.propertyHandlers) {
continue;
}
if (name in this.propertyHandlers) {
this.propertyHandlers[name](prop.value);
continue;
}
try {
const scannedPolymerProperty = js_utils_1.toScannedPolymerProperty(prop, this.document.sourceRangeForNode(prop), this.document);
if (prop.kind === 'get') {
getters[scannedPolymerProperty.name] = scannedPolymerProperty;
}
else if (prop.kind === 'set') {
setters[scannedPolymerProperty.name] = scannedPolymerProperty;
}
else if (prop.method === true || esutil_1.isFunctionType(prop.value)) {
const scannedPolymerMethod = esutil_1.toScannedMethod(prop, this.document.sourceRangeForNode(prop), this.document);
element.addMethod(scannedPolymerMethod);
}
else {
element.addProperty(scannedPolymerProperty);
}
}
catch (e) {
if (e instanceof model_1.WarningCarryingException) {
element.warnings.push(e.warning);
continue;
}
throw e;
}
}
Object.keys(getters).forEach((name) => {
const prop = getters[name];
definedProperties[prop.name] = prop;
prop.readOnly = !!setters[prop.name];
});
Object.keys(setters).forEach((name) => {
const prop = setters[name];
if (!(prop.name in definedProperties)) {
definedProperties[prop.name] = prop;
}
});
Object.keys(definedProperties).forEach((name) => {
const prop = definedProperties[name];
element.addProperty(prop);
});
return estraverse.VisitorOption.Skip;
}
}
}
//# sourceMappingURL=polymer-element-scanner.js.map