polymer-analyzer
Version:
Static analysis for Web Components
221 lines (219 loc) • 8.28 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const jsdoc = require("../javascript/jsdoc");
const model_1 = require("../model/model");
class LocalId {
constructor(name, range) {
this.name = name;
this.range = range;
}
}
exports.LocalId = LocalId;
function addProperty(target, prop) {
target.properties.set(prop.name, prop);
const attributeName = propertyToAttributeName(prop.name);
// Don't produce attributes or events for nonpublic properties, properties
// that aren't in Polymer's `properties` block (i.e. not published),
// or properties whose names can't be converted into attribute names.
if ((prop.privacy && prop.privacy !== 'public') || !attributeName ||
!prop.published) {
return;
}
target.attributes.set(attributeName, {
name: attributeName,
sourceRange: prop.sourceRange,
description: prop.description,
type: prop.type,
changeEvent: prop.notify ? `${attributeName}-changed` : undefined
});
if (prop.notify) {
const name = `${attributeName}-changed`;
target.events.set(name, {
name,
description: `Fired when the \`${prop.name}\` property changes.`,
sourceRange: prop.sourceRange,
astNode: prop.astNode,
warnings: [],
params: []
});
}
}
exports.addProperty = addProperty;
function addMethod(target, method) {
target.methods.set(method.name, method);
}
exports.addMethod = addMethod;
/**
* The metadata for a single polymer element
*/
class ScannedPolymerElement extends model_1.ScannedElement {
constructor(options) {
super();
this.properties = new Map();
this.methods = new Map();
this.observers = [];
this.listeners = [];
this.behaviorAssignments = [];
// Indicates if an element is a pseudo element
this.pseudo = false;
this.abstract = false;
this.tagName = options.tagName;
this.className = options.className;
this.superClass = options.superClass;
this.mixins = options.mixins;
this.extends = options.extends;
this.jsdoc = options.jsdoc;
this.description = options.description || '';
this.attributes = options.attributes;
this.observers = options.observers;
this.listeners = options.listeners;
this.behaviorAssignments = options.behaviors;
this.events = options.events;
this.abstract = options.abstract;
this.privacy = options.privacy;
this.astNode = options.astNode;
this.sourceRange = options.sourceRange;
if (options.properties) {
options.properties.forEach((p) => this.addProperty(p));
}
if (options.methods) {
options.methods.forEach((m) => this.addMethod(m));
}
const summaryTag = jsdoc.getTag(this.jsdoc, 'summary');
this.summary =
(summaryTag !== undefined && summaryTag.description != null) ?
summaryTag.description :
'';
}
addProperty(prop) {
addProperty(this, prop);
}
addMethod(method) {
addMethod(this, method);
}
resolve(document) {
return new PolymerElement(this, document);
}
}
exports.ScannedPolymerElement = ScannedPolymerElement;
class PolymerElement extends model_1.Element {
constructor(scannedElement, document) {
super(scannedElement, document);
this.observers = [];
this.listeners = [];
this.behaviorAssignments = [];
this.localIds = [];
this.kinds.add('polymer-element');
this.observers = Array.from(scannedElement.observers);
this.listeners = Array.from(scannedElement.listeners);
this.behaviorAssignments = Array.from(scannedElement.behaviorAssignments);
this.scriptElement = scannedElement.scriptElement;
const domModules = scannedElement.tagName == null ?
new Set() :
document.getFeatures({
kind: 'dom-module',
id: scannedElement.tagName,
imported: true,
externalPackages: true
});
let domModule = undefined;
if (domModules.size === 1) {
// TODO(rictic): warn if this isn't true.
domModule = domModules.values().next().value;
}
if (domModule) {
this.description = this.description || domModule.comment || '';
this.domModule = domModule.node;
this.slots = this.slots.concat(domModule.slots);
this.localIds = domModule.localIds.slice();
}
if (scannedElement.pseudo) {
this.kinds.add('pseudo-element');
}
}
emitPropertyMetadata(property) {
const polymerMetadata = {};
const polymerMetadataFields = ['notify', 'observer', 'readOnly'];
for (const field of polymerMetadataFields) {
if (field in property) {
polymerMetadata[field] = property[field];
}
}
return { polymer: polymerMetadata };
}
_getSuperclassAndMixins(document, init) {
const prototypeChain = super._getSuperclassAndMixins(document, init);
const { warnings, behaviors } = getBehaviors(init.behaviorAssignments, document);
this.warnings.push(...warnings);
prototypeChain.push(...behaviors);
return prototypeChain;
}
}
exports.PolymerElement = PolymerElement;
/**
* Implements Polymer core's translation of property names to attribute names.
*
* Returns null if the property name cannot be so converted.
*/
function propertyToAttributeName(propertyName) {
// Polymer core will not map a property name that starts with an uppercase
// character onto an attribute.
if (propertyName[0].toUpperCase() === propertyName[0]) {
return null;
}
return propertyName.replace(/([A-Z])/g, (_, c1) => `-${c1.toLowerCase()}`);
}
function getBehaviors(behaviorAssignments, document) {
const warnings = [];
const behaviors = [];
for (const behavior of behaviorAssignments) {
const foundBehaviors = document.getFeatures({
kind: 'behavior',
id: behavior.name,
imported: true,
externalPackages: true
});
if (foundBehaviors.size === 0) {
warnings.push(new model_1.Warning({
message: `Unable to resolve behavior ` +
`\`${behavior.name}\`. Did you import it? Is it annotated with ` +
`?`,
severity: model_1.Severity.WARNING,
code: 'unknown-polymer-behavior',
sourceRange: behavior.sourceRange,
parsedDocument: document.parsedDocument
}));
// Skip processing this behavior.
continue;
}
if (foundBehaviors.size > 1) {
warnings.push(new model_1.Warning({
message: `Found more than one behavior named ${behavior.name}.`,
severity: model_1.Severity.WARNING,
code: 'multiple-polymer-behaviors',
sourceRange: behavior.sourceRange,
parsedDocument: document.parsedDocument
}));
// Don't skip processing this behavior, just take the most recently
// declared instance.
}
const foundBehavior = Array.from(foundBehaviors)[foundBehaviors.size - 1];
behaviors.push(foundBehavior);
}
return { warnings, behaviors };
}
exports.getBehaviors = getBehaviors;
//# sourceMappingURL=polymer-element.js.map