"use strict"; const matchValue = require("match-value"); const asExpression = require("as-expression"); const FIELD_MAPPINGS = { attrset_expression: { binding_set: "MERGE" }, rec_attrset_expression: { binding_set: "MERGE" }, let_attrset_expression: { binding_set: "MERGE" }, let_expression: { binding_set: "MERGE" } }; function mergeNode(parent, child) { // NOTE: Mutates parent! let { type, ... childArgs } = child; Object.assign(parent, childArgs); } // TODO: Refactor this function convertStringNodeParts(node) { let fullText = node.text; let currentIndex = node.startIndex; let parts = []; function relative(index) { return index - node.startIndex; } for (let child of node.children) { if (child.type === "interpolation") { if (child.startIndex > currentIndex) { // We skipped some literal string let value = fullText.slice(relative(currentIndex), relative(child.startIndex)); parts.push({ type: "NixInterpolationLiteral", value: value }); } parts.push(convertNode(child)); currentIndex = child.endIndex; } } if (currentIndex < node.endIndex) { // Last bit of string literal at the end let value = fullText.slice(relative(currentIndex), relative(node.endIndex)); parts.push({ type: "NixInterpolationLiteral", value: value }); } return parts.map((part, i) => { // FIXME: Verify that this logic is correct, and also holds up for actual interpolated strings let isLiteralString = (part.type === "NixInterpolationLiteral"); if (isLiteralString) { let strippedString = part.value; if (i === 0 && isLiteralString) { // Strip prefix quote strippedString = strippedString.slice(1); } if (i === parts.length - 1 && isLiteralString) { // Strip suffix quote strippedString = strippedString.slice(0, -1); } return { ... part, value: strippedString }; } else { return part; } }); } function convertNode(node) { let { type } = node; let result = (node.isNamed) ? { type: type } : { type: "token", text: node.text } if (node.fields != null) { // console.log({ node, fields: node.fields, children: node.children }); for (let field of node.fields) { let children = node[field]; let fieldName = field.replace(/Nodes?$/, ""); result[fieldName] = asExpression(() => { if (children == null) { return null; } else if (Array.isArray(children)) { return children.map(convertNode); } else { return convertNode(children); } }); } let nodeFieldMappings = FIELD_MAPPINGS[node.type]; if (nodeFieldMappings != null) { node.children.forEach((child, i) => { let mapTo = nodeFieldMappings[child.type]; if (mapTo === "MERGE") { // This means we should elide the child node, and merge it into the node we're currently processing; this is used to deal with non-hidden layers of indirection mergeNode(result, convertNode(child)); } else if (mapTo != null) { // TODO: Support for multi-child fields? result[mapTo] = convertNode(child); } }); } } // Special case: if we don't provide a Babel-compatible top-level Program entry, traversal will fail if (type === "source_code") { return { type: "Program", sourceType: "script", body: [ result.expression ] }; } // Special case: this is just a wrapper for an identifier, and we don't really want to keep this wrapper if (type === "variable_expression") { return convertNode(node.nameNode); } // The below section is based on `alias` expressions throughout the grammar, and the rules in https://github.com/cstrahan/tree-sitter-nix/blob/83ee5993560bf15854c69b77d92e34456f8fb655/grammar.js#L53-L59 if (type === "identifier" || type === "attr_identifier") { // FIXME: attr_identifier gone? result.name = node.text; } else if (type === "integer_expression" || type === "float_expression") { result.value = node.text; } else if (type === "string_expression") { result.parts = convertStringNodeParts(node); } else if (type === "path_expression" || type === "hpath_expression") { result.path = node.text; } else if (type === "spath_expression") { // Strip the < and > result.path = node.text.slice(1, -1); } else if (type === "uri_expression") { result.uri = node.text; } if (type === "binary_expression" || type === "unary_expression") { // Unpack the anonymous token result.operator = result.operator.text; } if (type === "rec_attrset_expression") { result.recursive = true; } else if (type === "attrset") { result.recursive = false; } // FIXME: Is this code still needed after the parser updates? if (type === "inherit") { // We're inheriting from scope here, so the source expression is explicitly not set result.expression = result.expression ?? null; } else if (type === "inherit_from") { // Already set } if (type === "list_expression") { // TODO: Can this sort of renaming be done more neatly? result.elements = result.element; delete result.element; } // console.log(result); result.type = matchValue(result.type, { source_code: "NixProgram", token: "NixAnonymousToken", path_expression: "NixPathLiteral", hpath_expression: "NixHomePathLiteral", /* FIXME: Do we have interpolation support here yet? */ spath_expression: "NixEnvironmentPathLiteral", uri_expression: "NixURILiteral", integer_expression: "NixIntegerLiteral", float_expression: "NixFloatLiteral", string_expression: "NixStringLiteral", parenthesized_expression: "NixParenthesizedExpression", attrset_expression: "NixAttributeSet", rec_attrset_expression: "NixAttributeSet", binding_set: "NixAttributeSetBindings", let_expression: "NixLetIn", let_attrset_expression: "NixLetAttributeSet", identifier: "NixIdentifier", attr_identifier: "NixAttributeIdentifier", // FIXME: Gone? attrpath: "NixAttributePath", inherit: "NixInherit", inherit_from: "NixInherit", inherited_attrs: "NixInheritAttributes", attrs_inherited_from: "NixInheritAttributes", // FIXME: Gone? binding: "NixBinding", unary_expression: "NixUnaryOperation", binary_expression: "NixBinaryOperation", apply_expression: "NixFunctionCall", select_expression: "NixAttributeSelection", interpolation: "NixInterpolationExpression", list_expression: "NixListLiteral", with_expression: "NixWithExpression", if_expression: "NixConditional", // Function definitions function_expression: "NixFunctionDefinition", formals: "NixUnpackedAttributes", formal: "NixUnpackedAttribute", // _: (value) => value // FIXME: assert_expression? if_expression? unary_expression? has_attr_expression? apply_expression? indented_string_expression? binding_set? }); return result; } /* Produces an AST that's roughly shape-compatible with ESTree so that Babel can deal with it */ module.exports = function prepareAST(tree) { return convertNode(tree.rootNode); };