polymer-analyzer
Version:
Static analysis for Web Components
330 lines • 12.8 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const jsonschema = require("jsonschema");
function generateAnalysis(input, urlResolver, filter) {
const _filter = filter || ((_) => true);
const members = {
elements: new Set(iFilter(input.getFeatures({ kind: 'element' }), _filter)),
mixins: new Set(iFilter(input.getFeatures({ kind: 'element-mixin' }), _filter)),
namespaces: new Set(iFilter(input.getFeatures({ kind: 'namespace' }), _filter)),
functions: new Set(iFilter(input.getFeatures({ kind: 'function' }), _filter)),
polymerBehaviors: new Set(iFilter(input.getFeatures({ kind: 'behavior' }), _filter)),
classes: new Set()
};
const allClasses = iFilter(input.getFeatures({ kind: 'class' }), _filter);
for (const class_ of allClasses) {
if (members.elements.has(class_) ||
members.mixins.has(class_) ||
members.polymerBehaviors.has(class_)) {
continue;
}
members.classes.add(class_);
}
return buildAnalysis(members, urlResolver);
}
exports.generateAnalysis = generateAnalysis;
function buildAnalysis(members, urlResolver) {
// Build mapping of namespaces
const namespaces = new Map();
for (const namespace of members.namespaces) {
namespaces.set(namespace.name, serializeNamespace(namespace, urlResolver));
}
const analysis = {
schema_version: '1.0.0',
};
for (const namespace of namespaces.values()) {
const namespaceName = getNamespaceName(namespace.name);
const parent = namespaces.get(namespaceName) || analysis;
parent.namespaces = parent.namespaces || [];
parent.namespaces.push(namespace);
}
for (const element of members.elements) {
const namespaceName = getNamespaceName(element.className);
const namespace = namespaces.get(namespaceName) || analysis;
namespace.elements = namespace.elements || [];
namespace.elements.push(serializeElement(element, urlResolver));
}
for (const mixin of members.mixins) {
const namespaceName = getNamespaceName(mixin.name);
const namespace = namespaces.get(namespaceName) || analysis;
namespace.mixins = namespace.mixins || [];
namespace.mixins.push(serializeElementMixin(mixin, urlResolver));
}
for (const function_ of members.functions) {
const namespaceName = getNamespaceName(function_.name);
const namespace = namespaces.get(namespaceName) || analysis;
namespace.functions = namespace.functions || [];
namespace.functions.push(serializeFunction(function_, urlResolver));
}
// TODO(usergenic): Consider moving framework-specific code to separate file.
for (const behavior of members.polymerBehaviors) {
const namespaceName = getNamespaceName(behavior.className);
const namespace = namespaces.get(namespaceName) || analysis;
namespace.metadata = namespace.metadata || {};
namespace.metadata.polymer = namespace.metadata.polymer || {};
namespace.metadata.polymer.behaviors =
namespace.metadata.polymer.behaviors || [];
namespace.metadata.polymer.behaviors.push(serializePolymerBehaviorAsElementMixin(behavior, urlResolver));
}
for (const class_ of members.classes) {
const namespaceName = getNamespaceName(class_.name);
const namespace = namespaces.get(namespaceName) || analysis;
namespace.classes = namespace.classes || [];
namespace.classes.push(serializeClass(class_, urlResolver));
}
return analysis;
}
function getNamespaceName(name) {
if (name == null) {
return undefined;
}
const lastDotIndex = name.lastIndexOf('.');
if (lastDotIndex === -1) {
return undefined;
}
return name.substring(0, lastDotIndex);
}
const validator = new jsonschema.Validator();
const schema = require('../analysis.schema.json');
class ValidationError extends Error {
constructor(result) {
const message = `Unable to validate serialized Polymer analysis. ` +
`Got ${result.errors.length} errors: ` +
`${result.errors.map((err) => ' ' + (err.message || err))
.join('\n')}`;
super(message);
this.errors = result.errors;
}
}
exports.ValidationError = ValidationError;
/**
* Throws if the given object isn't a valid AnalyzedPackage according to
* the JSON schema.
*/
function validateAnalysis(analyzedPackage) {
const result = validator.validate(analyzedPackage, schema);
if (result.throwError) {
throw result.throwError;
}
if (result.errors.length > 0) {
throw new ValidationError(result);
}
if (!/^1\.\d+\.\d+$/.test(analyzedPackage.schema_version)) {
throw new Error(`Invalid schema_version in AnalyzedPackage. ` +
`Expected 1.x.x, got ${analyzedPackage.schema_version}`);
}
}
exports.validateAnalysis = validateAnalysis;
function serializeNamespace(namespace, urlResolver) {
const metadata = {
name: namespace.name,
description: namespace.description,
summary: namespace.summary,
sourceRange: sourceRangeRelativeTo(namespace.sourceRange, undefined, urlResolver)
};
return metadata;
}
function serializeFunction(fn, urlResolver) {
const metadata = {
name: fn.name,
description: fn.description,
summary: fn.summary,
sourceRange: sourceRangeRelativeTo(fn.sourceRange, undefined, urlResolver),
privacy: fn.privacy,
};
if (fn.params) {
metadata.params = fn.params;
}
if (fn.return) {
metadata.return = fn.return;
}
return metadata;
}
function serializeClass(class_, urlResolver) {
let path;
let relativeUrl;
if (class_.sourceRange) {
path = class_.sourceRange.file;
relativeUrl = urlResolver.relative(class_.sourceRange.file);
}
const properties = [...class_.properties.values()].map((p) => serializeProperty(class_, path, urlResolver, p));
const methods = [...class_.methods.values()].map((m) => serializeMethod(class_, path, urlResolver, m));
const staticMethods = [...class_.staticMethods.values()].map((m) => serializeMethod(class_, path, urlResolver, m));
const serialized = {
description: class_.description || '',
summary: class_.summary || '',
path: relativeUrl,
properties: properties,
methods: methods,
staticMethods: staticMethods,
demos: (class_.demos ||
[]).map(({ path, desc }) => ({ url: path, description: desc || '' })),
metadata: class_.emitMetadata(),
sourceRange: sourceRangeRelativeTo(class_.sourceRange, path, urlResolver),
privacy: class_.privacy,
superclass: class_.superClass ? class_.superClass.identifier : undefined,
};
if (class_.name) {
serialized.name = class_.name;
}
return serialized;
}
function serializeElementLike(elementOrMixin, urlResolver) {
const class_ = serializeClass(elementOrMixin, urlResolver);
let path;
if (elementOrMixin.sourceRange) {
path = elementOrMixin.sourceRange.file;
}
class_.attributes =
Array.from(elementOrMixin.attributes.values())
.map((a) => serializeAttribute(elementOrMixin, path, urlResolver, a));
class_.events = Array.from(elementOrMixin.events.values())
.map((e) => serializeEvent(elementOrMixin, path, e));
Object.assign(class_, {
styling: {
cssVariables: [],
selectors: [],
},
slots: elementOrMixin.slots.map((s) => {
return {
description: '',
name: s.name,
range: sourceRangeRelativeTo(s.range, path, urlResolver)
};
}),
});
return class_;
}
function serializeElement(element, urlResolver) {
const metadata = serializeElementLike(element, urlResolver);
metadata.tagname = element.tagName;
// TODO(justinfagnani): Mixins should be able to have mixins too
if (element.mixins.length > 0) {
metadata.mixins = element.mixins.map((m) => m.identifier);
}
metadata.superclass = 'HTMLElement';
if (element.superClass) {
metadata.superclass = element.superClass.identifier;
}
return metadata;
}
function serializeElementMixin(mixin, urlResolver) {
const metadata = serializeElementLike(mixin, urlResolver);
metadata.name = mixin.name;
metadata.privacy = mixin.privacy;
if (mixin.mixins.length > 0) {
metadata.mixins = mixin.mixins.map((m) => m.identifier);
}
return metadata;
}
function serializePolymerBehaviorAsElementMixin(behavior, urlResolver) {
const metadata = serializeElementLike(behavior, urlResolver);
metadata.name = behavior.className;
metadata.privacy = behavior.privacy;
if (behavior.mixins.length > 0) {
metadata.mixins = behavior.mixins.map((m) => m.identifier);
}
return metadata;
}
function serializeProperty(class_, elementPath, urlResolver, resolvedProperty) {
const property = {
name: resolvedProperty.name,
type: resolvedProperty.type || '?',
description: resolvedProperty.description || '',
privacy: resolvedProperty.privacy,
sourceRange: sourceRangeRelativeTo(resolvedProperty.sourceRange, elementPath, urlResolver),
metadata: class_.emitPropertyMetadata(resolvedProperty),
};
if (resolvedProperty.default) {
property.defaultValue = resolvedProperty.default;
}
if (resolvedProperty.inheritedFrom) {
property.inheritedFrom = resolvedProperty.inheritedFrom;
}
return property;
}
function serializeAttribute(resolvedElement, elementPath, urlResolver, resolvedAttribute) {
const attribute = {
name: resolvedAttribute.name,
description: resolvedAttribute.description || '',
sourceRange: sourceRangeRelativeTo(resolvedAttribute.sourceRange, elementPath, urlResolver),
metadata: resolvedElement.emitAttributeMetadata(resolvedAttribute),
};
if (resolvedAttribute.type) {
attribute.type = resolvedAttribute.type;
}
if (resolvedAttribute.inheritedFrom != null) {
attribute.inheritedFrom = resolvedAttribute.inheritedFrom;
}
return attribute;
}
function serializeMethod(class_, elementPath, urlResolver, resolvedMethod) {
const method = {
name: resolvedMethod.name,
description: resolvedMethod.description || '',
privacy: resolvedMethod.privacy,
sourceRange: sourceRangeRelativeTo(resolvedMethod.sourceRange, elementPath, urlResolver),
metadata: class_.emitMethodMetadata(resolvedMethod),
};
if (resolvedMethod.params) {
method.params = Array.from(resolvedMethod.params);
}
if (resolvedMethod.return) {
method.return = resolvedMethod.return;
}
if (resolvedMethod.inheritedFrom != null) {
method.inheritedFrom = resolvedMethod.inheritedFrom;
}
return method;
}
function serializeEvent(resolvedElement, _elementPath, resolvedEvent) {
const event = {
type: 'CustomEvent',
name: resolvedEvent.name,
description: resolvedEvent.description || '',
metadata: resolvedElement.emitEventMetadata(resolvedEvent),
};
if (resolvedEvent.inheritedFrom != null) {
event.inheritedFrom = resolvedEvent.inheritedFrom;
}
return event;
}
function sourceRangeRelativeTo(sourceRange, from, urlResolver) {
if (sourceRange === undefined) {
return;
}
if (from === sourceRange.file) {
return { start: sourceRange.start, end: sourceRange.end };
}
let file;
if (from === undefined) {
file = urlResolver.relative(sourceRange.file);
}
else {
file = urlResolver.relative(from, sourceRange.file);
}
return { file, start: sourceRange.start, end: sourceRange.end };
}
// TODO(rictic): figure out why type inference goes wrong with more general
// types here.
function* iFilter(iter, f) {
for (const val of iter) {
if (f(val)) {
yield val;
}
}
}
//# sourceMappingURL=generate-analysis.js.map
;