'use strict'; const React = require("react"); const createReactClass = require("create-react-class"); /* FIXME: Replace all inline simpleness determinations with calls to .isSimple */ function stringifyNode(node) { if (node.type === "Identifier") { return node.name; } else if (node.type === "AssignmentPattern") { return `${stringifyNode(node.left)} = ${stringifyNode(node.right)}`; } else if (node.type === "Literal") { return node.raw; } else if (node.type === "MemberExpression") { /* FIXME: Handle dynamic properties. */ if (node.computed === true) { return `${stringifyNode(node.object)}[${stringifyNode(node.property)}]`; } else { return `${stringifyNode(node.object)}.${stringifyNode(node.property)}`; } } else if (node.type === "ThisExpression") { return "this"; } else if (node.type === "ContinueStatement") { return "continue"; } else if (node.type === "LogicalExpression" || node.type === "BinaryExpression") { return `${stringifyNode(node.left)} ${node.operator} ${stringifyNode(node.right)}`; } else if (node.type === "UnaryExpression" || node.type === "UpdateExpression") { /* FIXME: Add spacing for typeof etc. but not for symbol operators like ++? */ if (node.prefix === true) { return `${node.operator} ${stringifyNode(node.argument)}`; } else { return `${stringifyNode(node.argument)} ${node.operator}`; } } else if (node.type === "CallExpression") { return `${stringifyNode(node.callee)}(${node.arguments.map((arg) => stringifyNode(arg)).join(", ")})`; } else if (node.type === "ObjectExpression") { if (node.properties.length > 0) { throw new Error("Can only stringify zero-property object literals"); } else { return "{}"; } } else if (node.type === "ArrayExpression") { if (node.elements.length > 0) { throw new Error("Can only stringify zero-element array literals"); } else { return "[]"; } } else if (node.type === "ConditionalExpression") { return `(${stringifyNode(node.test)} ? ${stringifyNode(node.consequent)} : ${stringifyNode(node.alternate)})`; } else if (node.type === "AssignmentExpression") { /* FIXME: Other assignment operators - does that include things like += ? */ return `${stringifyNode(node.left)} = ${stringifyNode(node.right)}`; } else { throw new Error(`Unrecognized node type: ${node.type}`); } } function collectNestedExpressions(root, type, attribute) { let collected = [ root ]; let last = root; while (last[attribute] != null && last[attribute].type === type) { collected.push(last[attribute]); last = last[attribute]; } return [collected, last]; } module.exports = function ({dependencies}) { let FunctionDeclarationItem = createReactClass({ displayName: "FunctionDeclarationItem", render: function () { let GenericAstItem = dependencies.get("GenericAstItem"); let AstItem = dependencies.get("AstItem"); if (this.props.data.id == null || this.props.data.id.type !== "Identifier") { return (); } else { let functionName = this.props.data.id.name; let stringifiedParameters = this.props.data.params.map((param) => { return stringifyNode(param); }); let itemProps = { abbreviated: true, hasFunctionScope: true, // TODO: Should this be hardcoded like this? hasBlockScope: false, type: this.props.data.type, preview: `function ${functionName} (${stringifiedParameters.join(", ")})`, scopeId: this.props.data._scopeId, scopeType: "function", children: { body: this.props.data.body } }; return ( ); } } }); let CallExpressionItem = createReactClass({ displayName: "CallExpressionItem", render: function () { let AstItem = dependencies.get("AstItem"); let showArguments = false; let stringifiedArguments = this.props.data.arguments.map((arg) => { try { return stringifyNode(arg); } catch (err) { /* FIXME: Either catch specific error, or use a .isSimpleNode method on AST nodes to determine whether we can stringify this call expression */ showArguments = true; return `<${arg.type}>`; } }); let itemProps = { abbreviated: true, type: this.props.data.type, preview: `${stringifyNode(this.props.data.callee)}(${stringifiedArguments.join(", ")})`, childSequences: { arguments: (showArguments === true ? this.props.data.arguments : null) } }; return ( ); } }); let MemberExpressionItem = createReactClass({ displayName: "MemberExpressionItem", render: function () { let AstItem = dependencies.get("AstItem"); let GenericAstItem = dependencies.get("GenericAstItem"); /* The below is for handling recursive MemberExpressions, eg. for foo.bar.baz */ /* FIXME: How to handle the 'computed' property? */ // let [collected, last] = collectNestedExpressions(this.props.data, "MemberExpression", "object"); let hasNonLiteralProperty = (this.props.data.property.type !== "Identifier"); let hasNonLiteralObject = (this.props.data.object.type !== "Identifier"); if (hasNonLiteralObject || hasNonLiteralProperty) { return (); } else { let itemProps = { abbreviated: true, type: this.props.data.type, preview: stringifyNode(this.props.data) }; return ( ); } } }); let LiteralItem = createReactClass({ displayName: "LiteralItem", render: function () { let AstItem = dependencies.get("AstItem"); let itemProps = { abbreviated: true, type: this.props.data.type, preview: stringifyNode(this.props.data) }; return ( ); } }); let ObjectPropertyItem = createReactClass({ displayName: "ObjectPropertyItem", render: function () { let AstItem = dependencies.get("AstItem"); let GenericAstItem = dependencies.get("GenericAstItem"); if (this.props.data.computed === true || this.props.data.method === true || this.props.data.shorthand === true) { return (); } else { let preview, complexValue; if (this.props.data.value.type === "Literal") { preview = `${stringifyNode(this.props.data.key)}: ${this.props.data.value.raw}`; complexValue = null; } else { preview = `${stringifyNode(this.props.data.key)}: <${this.props.data.value.type}>`; complexValue = this.props.data.value; } let itemProps = { abbreviated: true, type: this.props.data.type, preview: preview, children: { value: complexValue } }; return ( ); } } }); let VariableDeclaratorItem = createReactClass({ displayName: "VariableDeclaratorItem", render: function () { let GenericAstItem = dependencies.get("GenericAstItem"); let AstItem = dependencies.get("AstItem"); if (this.props.data.id.type !== "Identifier") { return (); } else { let preview; let simpleValue = false; if (this.props.data.init != null) { try { let stringifiedValue = stringifyNode(this.props.data.init); simpleValue = true; preview = `${this.props.data._kind} ${this.props.data.id.name} = ${stringifiedValue}`; } catch (err) { /* FIXME: Error type */ preview = `${this.props.data._kind} ${this.props.data.id.name} = <${this.props.data.init.type}>`; } } else { preview = `${this.props.data._kind} ${this.props.data.id.name}`; } let itemProps = { abbreviated: true, type: this.props.data.type, preview: preview, children: { init: (simpleValue === false) ? this.props.data.init : null } }; return ( ); } } }); let VariableDeclarationItem = createReactClass({ displayName: "VariableDeclarationItem", render: function () { let AstItem = dependencies.get("AstItem"); let itemProps = { abbreviated: true, type: this.props.data.type, // preview: `(${this.props.data.kind})`, childSequences: { declarations: this.props.data.declarations } }; return ( ); } }); let AssignmentExpressionItem = createReactClass({ displayName: "AssignmentExpressionItem", render: function () { let AstItem = dependencies.get("AstItem"); let preview, simpleValue, leftHand, rightHand, collectedAssignments; if (this.props.data.right.type === "AssignmentExpression") { /* We want to collapse multiple nested AssignmentExpressions into one (eg. `foo = bar = baz`) */ let [collected, last] = collectNestedExpressions(this.props.data, "AssignmentExpression", "right"); leftHand = collected.map((node) => { return stringifyNode(node.left); }).join(" = "); rightHand = last.right; collectedAssignments = collected; } else { leftHand = stringifyNode(this.props.data.left); rightHand = this.props.data.right; } if (rightHand.isSimple()) { preview = `${leftHand} = ${stringifyNode(rightHand)}`; simpleValue = true; } else { preview = `${leftHand} = <${rightHand.type}>`; simpleValue = false; } let itemProps = { abbreviated: true, type: "AssignmentExpression (nested)", preview: preview, children: { value: (simpleValue === false ? this.props.data.right : null) }, childSequences: { assignments: (collectedAssignments != null ? collectedAssignments.map((node) => node.left) : null) } }; return ( ); } }); let ObjectExpressionItem = createReactClass({ displayName: "ObjectExpressionItem", render: function () { let AstItem = dependencies.get("AstItem"); let stringifiedProperties = this.props.data.properties.map((property) => { if (property.computed === true) { /* FIXME */ return "[computed]"; } else if (property.value.isSimple()) { return `${stringifyNode(property.key)}: ${property.value.raw}`; } else { return stringifyNode(property.key); } }); let propertyPreviewLimit = 3; let shortenedStringifiedProperties; if (stringifiedProperties.length > propertyPreviewLimit) { shortenedStringifiedProperties = stringifiedProperties.slice(0, propertyPreviewLimit).concat([`(... ${stringifiedProperties.length - propertyPreviewLimit} more)`]); } else { shortenedStringifiedProperties = stringifiedProperties; } let itemProps = { abbreviated: true, type: this.props.data.type, preview: `{ ${shortenedStringifiedProperties.join(", ")} }`, childSequences: { properties: this.props.data.properties } }; return ( ); } }); let FixedKeywordItem = createReactClass({ displayName: "FixedKeywordItem", render: function () { let AstItem = dependencies.get("AstItem"); let itemProps = { abbreviated: true, type: this.props.data.type, preview: stringifyNode(this.props.data) }; return ( ); } }); let OperatorExpressionItem = createReactClass({ displayName: "OperatorExpressionItem", render: function () { let AstItem = dependencies.get("AstItem"); let GenericAstItem = dependencies.get("GenericAstItem"); try { let itemProps = { abbreviated: true, type: this.props.data.type, preview: stringifyNode(this.props.data) }; return ( ); } catch (err) { /* FIXME: Error type */ return (); } } }); let IfStatementItem = createReactClass({ displayName: "IfStatementItem", render: function () { let AstItem = dependencies.get("AstItem"); let GenericAstItem = dependencies.get("GenericAstItem"); if (this.props.data.test.isSimple()) { let stringifiedTest = stringifyNode(this.props.data.test); let itemProps = { abbreviated: true, type: this.props.data.type, preview: `if (${stringifiedTest}) ...`, children: { consequent: this.props.data.consequent, /* FIXME: else-if chains */ alternate: this.props.data.alternate } }; return ( ); } else { return (); } } }); let ReturnStatementItem = createReactClass({ displayName: "ReturnStatementItem", render: function () { let AstItem = dependencies.get("AstItem"); let GenericAstItem = dependencies.get("GenericAstItem"); if (this.props.data.isSimple()) { let itemProps = { abbreviated: true, type: this.props.data.type, preview: `return ${stringifyNode(this.props.data.argument)}` }; return ( ); } else { return (); } } }); let ExpressionStatementItem = createReactClass({ displayName: "ExpressionStatementItem", render: function () { let AstItem = dependencies.get("AstItem"); let GenericAstItem = dependencies.get("GenericAstItem"); if (this.props.data.isSimple()) { let itemProps = { abbreviated: true, type: this.props.data.type, preview: stringifyNode(this.props.data.expression) }; return ( ); } else { return (); } } }); let IdentifierItem = createReactClass({ displayName: "IdentifierItem", render: function () { let AstItem = dependencies.get("AstItem"); let itemProps = { abbreviated: true, type: this.props.data.type, preview: this.props.data.name }; return ( ); } }); let abbreviatedTypeMap = { FunctionDeclaration: FunctionDeclarationItem, FunctionExpression: FunctionDeclarationItem, MemberExpression: MemberExpressionItem, Identifier: IdentifierItem, Literal: LiteralItem, Property: ObjectPropertyItem, CallExpression: CallExpressionItem, VariableDeclarator: VariableDeclaratorItem, VariableDeclaration: VariableDeclarationItem, AssignmentExpression: AssignmentExpressionItem, ObjectExpression: ObjectExpressionItem, ThisExpression: FixedKeywordItem, ContinueStatement: FixedKeywordItem, IfStatement: IfStatementItem, ConditionalExpression: IfStatementItem, UpdateExpression: OperatorExpressionItem, UnaryExpression: OperatorExpressionItem, BinaryExpression: OperatorExpressionItem, LogicalExpression: OperatorExpressionItem, ReturnStatement: ReturnStatementItem, ExpressionStatement: ExpressionStatementItem }; dependencies.set("AbbreviatedAstItem", createReactClass({ displayName: "AbbreviatedAstItem", render: function () { let GenericAstItem = dependencies.get("GenericAstItem"); if (abbreviatedTypeMap[this.props.data.type] != null) { let AbbreviatedItemType = abbreviatedTypeMap[this.props.data.type]; return (); } else { return (); } } })); };