polymer-analyzer
Version:
Static analysis for Web Components
187 lines • 8.07 kB
JavaScript
/**
* @license
* Copyright (c) 2017 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 babel = require("@babel/types");
const doctrine = require("doctrine");
const source_range_1 = require("../model/source-range");
const ast_value_1 = require("./ast-value");
const esutil_1 = require("./esutil");
const function_1 = require("./function");
const jsdoc = require("./jsdoc");
class FunctionScanner {
scan(document, visit) {
return __awaiter(this, void 0, void 0, function* () {
const visitor = new FunctionVisitor(document);
yield visit(visitor);
return {
features: [...visitor.functions].sort((a, b) => source_range_1.comparePosition(a.sourceRange.start, b.sourceRange.start)),
};
});
}
}
exports.FunctionScanner = FunctionScanner;
class FunctionVisitor {
constructor(document) {
this.functions = new Set();
this.warnings = [];
this.document = document;
}
/**
* Scan standalone function declarations.
*/
enterFunctionDeclaration(node, _parent, path) {
this.initFunction(node, path, ast_value_1.getIdentifierName(node.id));
}
/**
* Scan object method declarations.
*/
enterObjectMethod(node, _parent, path) {
this.initFunction(node, path, ast_value_1.getIdentifierName(node.key));
}
/**
* Scan functions assigned to newly declared variables.
*/
enterVariableDeclaration(node, _parent, path) {
if (node.declarations.length !== 1) {
return; // Ambiguous.
}
const declaration = node.declarations[0];
const declarationValue = declaration.init;
if (declarationValue && babel.isFunction(declarationValue)) {
this.initFunction(declarationValue, path, ast_value_1.getIdentifierName(declaration.id));
}
}
/**
* Scan functions assigned to variables and object properties.
*/
enterAssignmentExpression(node, _parent, path) {
if (babel.isFunction(node.right)) {
this.initFunction(node.right, path, ast_value_1.getIdentifierName(node.left));
}
}
/**
* Scan functions defined inside of object literals.
*/
enterObjectExpression(_node, _parent, path) {
for (const propPath of esutil_1.getSimpleObjectPropPaths(path)) {
const prop = propPath.node;
const propValue = prop.value;
const name = esutil_1.getPropertyName(prop);
if (babel.isFunction(propValue)) {
this.initFunction(propValue, propPath, name);
continue;
}
const comment = esutil_1.getBestComment(propPath) || '';
const docs = jsdoc.parseJsdoc(comment);
if (jsdoc.getTag(docs, 'function')) {
this.initFunction(prop, propPath, name);
continue;
}
}
}
initFunction(node, path, analyzedName) {
const docs = jsdoc.parseJsdoc(esutil_1.getBestComment(path) || '');
// The @function annotation can override the name.
const functionTag = jsdoc.getTag(docs, 'function');
if (functionTag && functionTag.name) {
analyzedName = functionTag.name;
}
if (!analyzedName) {
// TODO(fks): Propagate a warning if name could not be determined
return;
}
if (!jsdoc.hasTag(docs, 'global') && !jsdoc.hasTag(docs, 'memberof') &&
!this.isExported(path)) {
// Without this check we would emit a lot of functions not worthy of
// inclusion. Since we don't do scope analysis, we can't tell when a
// function is actually part of an exposed API. Only include functions
// that are explicitly @global, or declared as part of some namespace
// with @memberof.
return;
}
// TODO(justinfagnani): remove polymerMixin support
if (jsdoc.hasTag(docs, 'mixinFunction') ||
jsdoc.hasTag(docs, 'polymerMixin')) {
// This is a mixin, not a normal function.
return;
}
const functionName = ast_value_1.getNamespacedIdentifier(analyzedName, docs);
const sourceRange = this.document.sourceRangeForNode(node);
const summaryTag = jsdoc.getTag(docs, 'summary');
const summary = (summaryTag && summaryTag.description) || '';
const description = docs.description;
let functionReturn = esutil_1.getReturnFromAnnotation(docs);
if (functionReturn === undefined && babel.isFunction(node)) {
functionReturn = esutil_1.inferReturnFromBody(node);
}
// TODO(justinfagnani): consolidate with similar param processing code in
// docs.ts
const functionParams = [];
const templateTypes = [];
for (const tag of docs.tags) {
if (tag.title === 'param') {
functionParams.push({
type: tag.type ? doctrine.type.stringify(tag.type) : 'N/A',
desc: tag.description || '',
name: tag.name || 'N/A'
});
}
else if (tag.title === 'template') {
for (let t of (tag.description || '').split(',')) {
t = t.trim();
if (t.length > 0) {
templateTypes.push(t);
}
}
}
}
// TODO(fks): parse params directly from `fn`, merge with docs.tags data
const specificName = functionName.slice(functionName.lastIndexOf('.') + 1);
this.functions.add(new function_1.ScannedFunction(functionName, description, summary, esutil_1.getOrInferPrivacy(specificName, docs), { language: 'js', node, containingDocument: this.document }, docs, sourceRange, functionParams, functionReturn, templateTypes));
}
isExported(path) {
const node = path.node;
if (babel.isObjectExpression(node)) {
// This function recurses up the AST until it finds an exported statement.
// That's a little crude, since being within an exported statement doesn't
// necessarily mean the function itself is exported. There are lots of
// cases where this fails, but a method defined on an object is a common
// one.
return false;
}
if (babel.isStatement(node)) {
const parent = path.parent;
if (parent && babel.isExportDefaultDeclaration(parent) ||
babel.isExportNamedDeclaration(parent)) {
return true;
}
return false;
}
const parentPath = path.parentPath;
if (parentPath == null) {
return false;
}
return this.isExported(parentPath);
}
}
//# sourceMappingURL=function-scanner.js.map
;