/** * Copyright 2017 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const esprima = require('esprima'); const ESTreeWalker = require('../../ESTreeWalker'); const Documentation = require('./Documentation'); class JSOutline { constructor(text) { this.classes = []; /** @type {!Map} */ this.inheritance = new Map(); this.errors = []; this._eventsByClassName = new Map(); this._currentClassName = null; this._currentClassMembers = []; this._text = text; const ast = esprima.parseScript(this._text, {loc: true, range: true}); const walker = new ESTreeWalker(node => { if (node.type === 'ClassDeclaration') this._onClassDeclaration(node); else if (node.type === 'MethodDefinition') this._onMethodDefinition(node); else if (node.type === 'AssignmentExpression') this._onAssignmentExpression(node); }); walker.walk(ast); this._flushClassIfNeeded(); this._recreateClassesWithEvents(); } _onClassDeclaration(node) { this._flushClassIfNeeded(); this._currentClassName = this._extractText(node.id); const superClass = this._extractText(node.superClass); if (superClass) this.inheritance.set(this._currentClassName, superClass); } _onMethodDefinition(node) { console.assert(this._currentClassName !== null); console.assert(node.value.type === 'FunctionExpression'); const methodName = this._extractText(node.key); if (node.kind === 'get') { const property = Documentation.Member.createProperty(methodName); this._currentClassMembers.push(property); return; } // Async functions have return value. let hasReturn = node.value.async; // Extract properties from constructor. if (node.kind === 'constructor') { // Extract properties from constructor. const walker = new ESTreeWalker(node => { if (node.type !== 'AssignmentExpression') return; node = node.left; if (node.type === 'MemberExpression' && node.object && node.object.type === 'ThisExpression' && node.property && node.property.type === 'Identifier') this._currentClassMembers.push(Documentation.Member.createProperty(node.property.name)); }); walker.walk(node); } else if (!hasReturn) { const walker = new ESTreeWalker(node => { if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression') return ESTreeWalker.SkipSubtree; if (node.type === 'ReturnStatement') hasReturn = hasReturn || !!node.argument; }); walker.walk(node.value.body); } const args = []; for (const param of node.value.params) { if (param.type === 'AssignmentPattern') args.push(new Documentation.Argument(param.left.name)); else if (param.type === 'RestElement') args.push(new Documentation.Argument('...' + param.argument.name)); else if (param.type === 'Identifier') args.push(new Documentation.Argument(param.name)); else if (param.type === 'ObjectPattern') args.push(new Documentation.Argument('options')); else this.errors.push(`JS Parsing issue: unsupported syntax to define parameter in ${this._currentClassName}.${methodName}(): ${this._extractText(param)}`); } const method = Documentation.Member.createMethod(methodName, args, hasReturn, node.value.async); this._currentClassMembers.push(method); return ESTreeWalker.SkipSubtree; } _onAssignmentExpression(node) { if (node.left.type !== 'MemberExpression' || node.right.type !== 'ObjectExpression') return; if (node.left.object.type !== 'Identifier' || node.left.property.type !== 'Identifier' || node.left.property.name !== 'Events') return; const className = node.left.object.name; let events = this._eventsByClassName.get(className); if (!events) { events = []; this._eventsByClassName.set(className, events); } for (const property of node.right.properties) { if (property.type !== 'Property' || property.key.type !== 'Identifier' || property.value.type !== 'Literal') continue; events.push(Documentation.Member.createEvent(property.value.value)); } } _flushClassIfNeeded() { if (this._currentClassName === null) return; const jsClass = new Documentation.Class(this._currentClassName, this._currentClassMembers); this.classes.push(jsClass); this._currentClassName = null; this._currentClassMembers = []; } _recreateClassesWithEvents() { this.classes = this.classes.map(cls => { const events = this._eventsByClassName.get(cls.name) || []; const members = cls.membersArray.concat(events); return new Documentation.Class(cls.name, members); }); } _extractText(node) { if (!node) return null; const text = this._text.substring(node.range[0], node.range[1]).trim(); return text; } } /** * @param {!Array} classes * @param {!Map} inheritance * @return {!Array} */ function recreateClassesWithInheritance(classes, inheritance) { const classesByName = new Map(classes.map(cls => [cls.name, cls])); return classes.map(cls => { const membersMap = new Map(); for (let wp = cls; wp; wp = classesByName.get(inheritance.get(wp.name))) { for (const member of wp.membersArray) { // Member was overridden. const memberId = member.type + ':' + member.name; if (membersMap.has(memberId)) continue; // Do not inherit constructors if (wp !== cls && member.name === 'constructor' && member.type === 'method') continue; membersMap.set(memberId, member); } } return new Documentation.Class(cls.name, Array.from(membersMap.values())); }); } /** * @param {!Array} sources * @return {!Promise<{documentation: !Documentation, errors: !Array}>} */ module.exports = async function(sources) { const classes = []; const errors = []; const inheritance = new Map(); for (const source of sources) { const outline = new JSOutline(source.text()); classes.push(...outline.classes); errors.push(...outline.errors); for (const entry of outline.inheritance) inheritance.set(entry[0], entry[1]); } const documentation = new Documentation(recreateClassesWithInheritance(classes, inheritance)); return { documentation, errors }; };