polymer-analyzer
Version:
Static analysis for Web Components
216 lines • 7.25 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 doctrine = require("doctrine");
const model_1 = require("../model/model");
/**
* Given a JSDoc string (minus opening/closing comment delimiters), extract its
* description and tags.
*/
function parseJsdoc(docs) {
docs = removeLeadingAsterisks(docs);
const d = doctrine.parse(docs, {
unwrap: false,
// lineNumbers: true,
preserveWhitespace: true,
});
// Strip any leading and trailing newline characters in the
// description of multiline comments for readibility.
// TODO(rictic): figure out if we can trim() here or not. Something something
// markdown?
const description = d.description && d.description.replace(/^\n+|\n+$/g, '');
return { description, tags: parseCustomTags(d.tags) };
}
exports.parseJsdoc = parseJsdoc;
// Tags with a name: @title name description
const tagsWithNames = new Set([
'appliesMixin',
'demo',
'hero',
'mixinFunction',
'polymerBehavior',
'pseudoElement'
]);
const firstWordAndRest = /^\s*(\S*)\s*(.*)$/;
function parseCustomTags(tags) {
return tags.map((tag) => {
if (tag.description != null && tagsWithNames.has(tag.title)) {
const match = firstWordAndRest.exec(tag.description);
if (match != null) {
const name = match[1];
const description = match[2];
return Object.assign({}, tag, { name,
description });
}
}
return tag;
});
}
/**
* removes leading *, and any space before it
*/
function removeLeadingAsterisks(description) {
return description.split('\n')
.map(function (line) {
// remove leading '\s*' from each line
const match = line.trim().match(/^[\s]*\*\s?(.*)$/);
return match ? match[1] : line;
})
.join('\n');
}
exports.removeLeadingAsterisks = removeLeadingAsterisks;
function hasTag(jsdoc, title) {
return getTag(jsdoc, title) !== undefined;
}
exports.hasTag = hasTag;
/**
* Finds the first JSDoc tag matching `title`.
*/
function getTag(jsdoc, title) {
return jsdoc && jsdoc.tags && jsdoc.tags.find((t) => t.title === title);
}
exports.getTag = getTag;
function unindent(text) {
if (!text) {
return text;
}
const lines = text.replace(/\t/g, ' ').split('\n');
const indent = lines.reduce(function (prev, line) {
if (/^\s*$/.test(line)) {
return prev; // Completely ignore blank lines.
}
const lineIndent = line.match(/^(\s*)/)[0].length;
if (prev === null) {
return lineIndent;
}
return lineIndent < prev ? lineIndent : prev;
}, 0);
return lines
.map(function (l) {
return l.substr(indent);
})
.join('\n');
}
exports.unindent = unindent;
function isAnnotationEmpty(docs) {
return docs === undefined ||
docs.tags.length === 0 && docs.description.trim() === '';
}
exports.isAnnotationEmpty = isAnnotationEmpty;
const privacyTags = new Set(['public', 'private', 'protected']);
function getPrivacy(jsdoc) {
return jsdoc && jsdoc.tags &&
jsdoc.tags.filter((t) => privacyTags.has(t.title))
.map((t) => t.title)[0];
}
exports.getPrivacy = getPrivacy;
/**
* Returns the mixin applications, in the form of ScannedReferences, for the
* jsdocs of class.
*
* The references are returned in presumed order of application - from furthest
* up the prototype chain to closest to the subclass.
*/
function getMixinApplications(document, node, docs, warnings, path) {
// TODO(justinfagnani): remove @mixes support
const appliesMixinAnnotations = docs.tags.filter((tag) => tag.title === 'appliesMixin' || tag.title === 'mixes');
return appliesMixinAnnotations
.map((annotation) => {
const mixinId = annotation.name;
// TODO(justinfagnani): we need source ranges for jsdoc
// annotations
const sourceRange = document.sourceRangeForNode(node);
if (mixinId === undefined) {
warnings.push(new model_1.Warning({
code: 'class-mixes-annotation-no-id',
message: '@appliesMixin annotation with no identifier. Usage `@appliesMixin MixinName`',
severity: model_1.Severity.WARNING,
sourceRange,
parsedDocument: document
}));
return;
}
return new model_1.ScannedReference('element-mixin', mixinId, sourceRange, undefined, path);
})
.filter((m) => m !== undefined);
}
exports.getMixinApplications = getMixinApplications;
function extractDemos(jsdoc) {
if (!jsdoc || !jsdoc.tags) {
return [];
}
const demos = [];
const demoUrls = new Set();
for (const tag of jsdoc.tags.filter((tag) => tag.title === 'demo' && tag.name)) {
const demoUrl = tag.name;
if (demoUrls.has(demoUrl)) {
continue;
}
demoUrls.add(demoUrl);
demos.push({
desc: tag.description || undefined,
path: demoUrl,
});
}
return demos;
}
exports.extractDemos = extractDemos;
function join(...jsdocs) {
return {
description: jsdocs.map((jsdoc) => jsdoc && jsdoc.description || '')
.join('\n\n')
.trim(),
tags: jsdocs.map((jsdoc) => jsdoc && jsdoc.tags || [])
.reduce((acc, tags) => acc.concat(tags)),
};
}
exports.join = join;
/**
* Assume that if the same symbol is documented in multiple places, the longer
* description is probably the intended one.
*
* TODO(rictic): unify logic with join(...)'s above.
*/
function pickBestDescription(...descriptions) {
let description = '';
for (const desc of descriptions) {
if (desc && desc.length > description.length) {
description = desc;
}
}
return description;
}
exports.pickBestDescription = pickBestDescription;
/**
* Extracts the description from a jsdoc annotation and uses
* known descriptive tags if no explicit description is set.
*/
function getDescription(jsdocAnn) {
if (jsdocAnn.description) {
return jsdocAnn.description;
}
// These tags can be used to describe a field.
// e.g.:
// /** @type {string} the name of the animal */
// this.name = name || 'Rex';
const tagSet = new Set(['public', 'private', 'protected', 'type']);
for (const tag of jsdocAnn.tags) {
if (tagSet.has(tag.title) && tag.description) {
return tag.description;
}
}
}
exports.getDescription = getDescription;
//# sourceMappingURL=jsdoc.js.map
;