You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

539 lines
15 KiB
JavaScript

'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 (<GenericAstItem {...this.props} />);
} 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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
});
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 (<GenericAstItem {...this.props} />);
} else {
let itemProps = {
abbreviated: true,
type: this.props.data.type,
preview: stringifyNode(this.props.data)
};
return (
<AstItem {...itemProps} data={this.props.data} />
);
}
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
});
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 (<GenericAstItem {...this.props} />);
} 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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
}
});
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 (<GenericAstItem {...this.props} />);
} 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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
} catch (err) {
/* FIXME: Error type */
return (<GenericAstItem {...this.props} />);
}
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
} else {
return (<GenericAstItem {...this.props} />);
}
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
} else {
return (<GenericAstItem {...this.props} />);
}
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
} else {
return (<GenericAstItem {...this.props} />);
}
}
});
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 (
<AstItem {...itemProps} data={this.props.data} />
);
}
});
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 (<AbbreviatedItemType {...this.props} />);
} else {
return (<GenericAstItem {...this.props} />);
}
}
}));
};