"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" ) {
// 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" ,
binary _expression : "NixBinaryOperation" ,
apply _expression : "NixFunctionCall" ,
select _expression : "NixAttributeSelection" ,
interpolation : "NixInterpolationExpression" ,
list _expression : "NixListLiteral" ,
with _expression : "NixWithExpression" ,
// 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 ) ;
} ;