@straits/babel
Version:
Babel straits syntax plugin
382 lines (324 loc) • 10.4 kB
JavaScript
'use strict';
const assert = require('assert');
const template = require('@babel/template').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
const debug = {
/*
group() { return console.group( ...arguments ); },
log() { return console.error( ...arguments ); },
groupEnd() { return console.groupEnd( ...arguments ); },
*/
group(){},
log(){},
groupEnd(){},
};
// turning `_Straits` within strings into `.*`
// NOTE: if a string had `_Straits` originally, that'd be screwed up, escape those, maybe?
function cleanString( str ) {
return str
.replace(/\._Straits\.?/g, `.*` )
.replace(/_StraitsProvider:/g, `use traits * from` );
}
function generateStraitsFunctions( path ) {
// testTraitSet( traitSet )
// it makes sure that all the `use traits * from` statements have a valid object as expression
const testTraitBuilder = template(`
function TEST_TRAIT_SET( traitSet ) {
if( ! traitSet || typeof traitSet === 'boolean' || typeof traitSet === 'number' || typeof traitSet === 'string' ) {
throw new Error(\`\${traitSet} cannot be used as a trait set.\`);
}
}
`);
// getSymbol( targetSymName, ...traits )
// looks for `targetSymName` inside `traits`, and returns the trait, if found
const getSymbolBuilder = template(`
function GET_SYMBOL( targetSymName, ...traitSets ) {
let symbol;
traitSets.forEach( traitSet=>{
const sym = traitSet[targetSymName];
if( typeof sym === 'symbol' ) {
if( !! symbol && symbol !== sym ) {
throw new Error(\`Symbol \${targetSymName} offered by multiple trait sets.\`);
}
symbol = sym;
}
});
if( ! symbol ) {
throw new Error(\`No trait set is providing symbol \${targetSymName}.\`);
}
return symbol;
}
`);
// implSymbol( target, sym, value ): `target.*[sym] = value`
const implSymbolBuilder = template(`
function IMPL_SYMBOL( target, sym, value ) {
Object.defineProperty( target, sym, {value, configurable:true} );
return target[sym];
}
`);
// generating identifiers for the above functions
const identifiers = {
testTraitSet: path.scope.generateUidIdentifier(`testTraitSet`),
getSymbol: path.scope.generateUidIdentifier(`getSymbol`),
implSymbol: path.scope.generateUidIdentifier(`implSymbol`),
};
// adding to the AST the code for the above functions
path.unshiftContainer('body', testTraitBuilder({ TEST_TRAIT_SET:identifiers.testTraitSet }) );
path.unshiftContainer('body', getSymbolBuilder({ GET_SYMBOL:identifiers.getSymbol }) );
path.unshiftContainer('body', implSymbolBuilder({ IMPL_SYMBOL:identifiers.implSymbol }) );
// returning the identifiers
return identifiers;
}
class UseTraitStatement {
constructor( path, expr ) {
this.path = path; // the AST node representing this statement
this.expr = expr; // the trait set expression
this.scope = null;
}
}
class StraitsExpression {
constructor( path, targetPath, symbolName ) {
this.path = path;
this.targetPath = targetPath;
this.symbolName = symbolName;
this.assignmentPath = null;
this.scope = null;
}
get target() {
return this.targetPath.node;
}
get assignmentValue() {
return this.assignmentPath.node.right;
}
}
class StraitsScope {
constructor( path, traitSets ) {
this.path = path; // path where the scope begins
this.traitSets = new Set(traitSets); // the traitSets affecting this scope
this.symbols = new Map(); // the symbols resolved in this scope
}
}
module.exports = function( arg ) {
return {
pre() {
assert( ! this.straits );
this.straits = {
useTraitStatements: [],
straitsExpressions: [],
straitsAssignments: [],
scopeStack: [],
currentScope: new StraitsScope(),
};
},
visitor: {
Program: {
enter( path ) {
debug.log(`-----START PROGRAM-----`);
debug.group();
},
exit( path ) {
debug.groupEnd();
debug.log(`----- END PROGRAM-----`);
// TODO: explain why we're doing this here, rather than in `post`.
// IIRC, the Visitor keep running after `exit` on the new nodes we create...
// But so? Maybe babel6 was different?
const {straits} = this;
delete this.straits;
assert( straits.scopeStack.length === 0 );
// if we didn't find any `use traits * from` statements, we can terminate immediately
if( straits.useTraitStatements.length === 0 ) {
assert( straits.straitsExpressions.length === 0 );
return;
}
// generating global functions
const traitFns = generateStraitsFunctions( path );
// writing a `testTraitSet` call where each `use trait` statement is
straits.useTraitStatements.forEach( (uts)=>{
uts.path.insertBefore(
t.expressionStatement(
t.callExpression(
traitFns.testTraitSet,
[
uts.expr
]
)
)
);
});
// for each `.*` usage, let's resolve the symbol and replace the expression
{
const resolveSymbol = (se)=>{
const {scope, symbolName} = se;
const {symbols} = scope;
if( symbols.has(symbolName) ) {
return symbols.get( symbolName );
}
// if the symbol was not used before, let's resolve it...
const symbolIdentifier = path.scope.generateUidIdentifier( symbolName );
symbols.set( symbolName, symbolIdentifier );
// adding the `getSymbol( symName, ...traitSets )` line
scope.path.insertBefore(
t.variableDeclaration(
`const`,
[
t.variableDeclarator(
symbolIdentifier,
t.callExpression(
traitFns.getSymbol,
[
t.stringLiteral(symbolName),
...Array.from( scope.traitSets ).map( uts=>uts.expr )
]
)
)
]
)
);
return symbolIdentifier;
};
straits.straitsExpressions.forEach( (se)=>{
const symbolIdentifier = resolveSymbol( se );
se.path.replaceWith(
t.memberExpression(
se.target,
symbolIdentifier,
true
)
);
});
straits.straitsAssignments.reverse().forEach( (se)=>{
const symbolIdentifier = resolveSymbol( se );
se.assignmentPath.replaceWith(
t.callExpression(
traitFns.implSymbol,
[
se.target,
symbolIdentifier,
se.assignmentValue,
]
)
);
});
}
// removing everything that is unneeded...
{
straits.useTraitStatements.forEach( (uts)=>{
uts.path.remove();
});
}
}
},
BlockStatement: {
enter( path ) {
if( ! this.straits ) { return; }
const {straits} = this;
straits.scopeStack.push( straits.currentScope );
debug.log(`{`);
debug.group();
},
exit( path ) {
if( ! this.straits ) { return; }
const {straits} = this;
straits.currentScope = straits.scopeStack.pop();
debug.groupEnd();
debug.log(`}`);
}
},
// `use straits * from EXPR`
LabeledStatement( path ) {
if( ! this.straits ) { return; }
const {straits} = this;
const node = path.node;
if( node.label.name !== '_StraitsProvider' ) {
return;
}
assert( path.parent.type === 'BlockStatement' || path.parent.type === 'Program', `"use traits * from" must be placed in a block, or in the outermost scope.` );
assert( path.node.body.type === 'ExpressionStatement', `\`use traits\` requires an expression.` );
debug.log( `use traits * from ${generate(path.node.body.expression).code};` );
const useTraitStatement = new UseTraitStatement(
path,
path.node.body.expression
);
straits.currentScope = new StraitsScope( path, straits.currentScope.traitSets );
straits.currentScope.traitSets.add( useTraitStatement );
useTraitStatement.scope = straits.currentScope;
straits.useTraitStatements.push( useTraitStatement );
},
// a.*b
Identifier( path ) {
if( ! this.straits ) { return; }
const {straits} = this;
const node = path.node;
if( node.name !== '_Straits' ) {
return;
}
if( straits.currentScope.traitSets.size === 0 ) {
throw path.buildCodeFrameError(`.* used outside a \`use traits\` scope.`);
}
// `a.*b` is represented in the AST as `a._Straits.b`
// straitsOperatorPath is the `(...)._Straits` expression
// traitPath is the `(...).${symbol}` one
// parentPath is the expression above `traitPath`. is that an assignment?
const straitsOperatorPath = path.parentPath;
const traitPath = (()=>{
const straitsOperator = straitsOperatorPath.node;
assert( straitsOperator.type === 'MemberExpression' );
assert( straitsOperator.computed === false );
const prop = straitsOperator.property;
assert( prop === node );
return straitsOperatorPath.parentPath;
})();
const parentPath = traitPath.parentPath;
{
const traitParent = traitPath.node;
assert( traitParent.type === 'MemberExpression' );
assert( traitParent.object === straitsOperatorPath.node );
}
debug.log( `.*${generate(traitPath.node.property).code}` );
const straitsExpression = new StraitsExpression(
traitPath,
straitsOperatorPath.get('object'),
traitPath.node.property.name
);
straitsExpression.scope = straits.currentScope;
// if `a.*b = c`
if( parentPath.type === 'AssignmentExpression' && parentPath.node.operator === '=' && parentPath.node.left === traitPath.node ) {
straitsExpression.assignmentPath = parentPath;
straits.straitsAssignments.push( straitsExpression );
}
else {
straits.straitsExpressions.push( straitsExpression );
}
},
StringLiteral( literalPath ) {
const str = literalPath.node.value;
const newStr = cleanString( str );
if( newStr === str ) {
return;
}
literalPath.replaceWith(
t.stringLiteral(
newStr
)
);
},
TemplateElement( elementPath ) {
const {value, tail} = elementPath.node;
const newValue = {
raw: cleanString( value.raw ),
cooked: cleanString( value.cooked ),
};
if( newValue.raw === value.raw || newValue.cooked === value.cooked ) {
return;
}
elementPath.replaceWith(
t.templateElement(
newValue,
tail
)
);
},
},
};
};