bson-transpilers
Advanced tools
Comparing version 0.0.0-next-0df16eac622fa26bc26ea5bc44d4af849a08e128 to 0.0.0-next-1037badfa5c12b52a8fa581424cf7e01654f809d
@@ -10,3 +10,3 @@ 'use strict'; | ||
BsonTranspilersTypeError, | ||
BsonTranspilersUnimplementedError | ||
BsonTranspilersUnimplementedError, | ||
} = require('../helper/error'); | ||
@@ -25,84 +25,89 @@ | ||
*/ | ||
module.exports = (ANTLRVisitor) => class CodeGenerationVisitor extends ANTLRVisitor { | ||
constructor() { | ||
super(); | ||
this.idiomatic = true; // PUBLIC | ||
this.clearImports(); | ||
this.state = { declarations: new DeclarationStore() }; | ||
} | ||
module.exports = (ANTLRVisitor) => | ||
class CodeGenerationVisitor extends ANTLRVisitor { | ||
constructor() { | ||
super(); | ||
this.idiomatic = true; // PUBLIC | ||
this.clearImports(); | ||
this.state = { declarations: new DeclarationStore() }; | ||
} | ||
clearImports() { | ||
this.requiredImports = {}; | ||
[300, 301, 302, 303, 304, 305, 306].forEach( | ||
(i) => (this.requiredImports[i] = []) | ||
); | ||
} | ||
/** | ||
* Start the compiler at this.startRule. | ||
* | ||
* "Return" methods are overridden only by the object generator. All | ||
* generators or visitors that translate code to code should not need to | ||
* override these methods. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {*} | ||
*/ | ||
returnResult(ctx) { | ||
const rule = `visit${this.startRule.replace(/^\w/, c => c.toUpperCase())}`; | ||
if (!this.startRule || !(rule in this)) { | ||
throw new BsonTranspilersInternalError( | ||
'Unimplemented Visitor: the entry rule for the compiler must be set' | ||
clearImports() { | ||
this.requiredImports = {}; | ||
[300, 301, 302, 303, 304, 305, 306].forEach( | ||
(i) => (this.requiredImports[i] = []) | ||
); | ||
} | ||
return this[rule](ctx); | ||
} | ||
returnResultWithDeclarations(ctx) { | ||
let result = this.returnResult(ctx); | ||
if (this.getState().declarations.length() > 0) { | ||
result = `${this.getState().declarations.toString() + '\n\n'}${result}`; | ||
/** | ||
* Start the compiler at this.startRule. | ||
* | ||
* "Return" methods are overridden only by the object generator. All | ||
* generators or visitors that translate code to code should not need to | ||
* override these methods. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {*} | ||
*/ | ||
returnResult(ctx) { | ||
const rule = `visit${this.startRule.replace(/^\w/, (c) => | ||
c.toUpperCase() | ||
)}`; | ||
if (!this.startRule || !(rule in this)) { | ||
throw new BsonTranspilersInternalError( | ||
'Unimplemented Visitor: the entry rule for the compiler must be set' | ||
); | ||
} | ||
return this[rule](ctx); | ||
} | ||
return result; | ||
} | ||
/** | ||
* PUBLIC: This is the entry point for the compiler. Each visitor must define | ||
* an attribute called "startNode". | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Boolean} useDeclarations - prepend the result string with declarations | ||
* @return {String} | ||
*/ | ||
start(ctx, useDeclarations = false) { | ||
return (useDeclarations ? this.returnResultWithDeclarations(ctx) : this.returnResult(ctx)).trim(); | ||
} | ||
returnResultWithDeclarations(ctx) { | ||
let result = this.returnResult(ctx); | ||
if (this.getState().declarations.length() > 0) { | ||
result = `${this.getState().declarations.toString() + '\n\n'}${result}`; | ||
} | ||
return result; | ||
} | ||
getState() { | ||
return this.state; | ||
} | ||
/** | ||
* PUBLIC: This is the entry point for the compiler. Each visitor must define | ||
* an attribute called "startNode". | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Boolean} useDeclarations - prepend the result string with declarations | ||
* @return {String} | ||
*/ | ||
start(ctx, useDeclarations = false) { | ||
return ( | ||
useDeclarations | ||
? this.returnResultWithDeclarations(ctx) | ||
: this.returnResult(ctx) | ||
).trim(); | ||
} | ||
clearDeclarations() { | ||
this.getState().declarations.clear(); | ||
} | ||
getState() { | ||
return this.state; | ||
} | ||
/** | ||
* PUBLIC: As code is generated, any classes that require imports are tracked | ||
* in this.Imports. Each class has a "code" defined in the symbol table. | ||
* The imports are then generated based on the output language templates. | ||
* @param {String} mode | ||
* @param {Boolean} driverSyntax (optional) | ||
* @return {String} - The list of imports in the target language. | ||
*/ | ||
getImports(mode, driverSyntax) { | ||
const importTemplate = this.Imports.import.template ? | ||
this.Imports.import.template : | ||
(s) => ( | ||
Object.values(s) | ||
.filter((a, i) => (Object.values(s).indexOf(a) === i)) | ||
.join('\n') | ||
); | ||
// Remove empty arrays because js [] is not falsey :( | ||
[300, 301, 302, 303, 304, 305, 306].forEach( | ||
(i) => { | ||
clearDeclarations() { | ||
this.getState().declarations.clear(); | ||
} | ||
/** | ||
* PUBLIC: As code is generated, any classes that require imports are tracked | ||
* in this.Imports. Each class has a "code" defined in the symbol table. | ||
* The imports are then generated based on the output language templates. | ||
* @param {String} mode | ||
* @param {Boolean} driverSyntax (optional) | ||
* @return {String} - The list of imports in the target language. | ||
*/ | ||
getImports(mode, driverSyntax) { | ||
const importTemplate = this.Imports.import.template | ||
? this.Imports.import.template | ||
: (s) => | ||
Object.values(s) | ||
.filter((a, i) => Object.values(s).indexOf(a) === i) | ||
.join('\n'); | ||
// Remove empty arrays because js [] is not falsey :( | ||
[300, 301, 302, 303, 304, 305, 306].forEach((i) => { | ||
if (i in this.requiredImports && this.requiredImports[i].length === 0) { | ||
@@ -112,771 +117,841 @@ this.requiredImports[i] = false; | ||
}); | ||
this.requiredImports.driver = !!driverSyntax; | ||
const imports = {}; | ||
for (const code of Object.keys(this.requiredImports)) { | ||
if ( | ||
this.requiredImports[code] && | ||
this.Imports[code] && | ||
this.Imports[code].template | ||
) { | ||
imports[code] = this.Imports[code].template(this.requiredImports[code], mode); | ||
this.requiredImports.driver = !!driverSyntax; | ||
const imports = {}; | ||
for (const code of Object.keys(this.requiredImports)) { | ||
if ( | ||
this.requiredImports[code] && | ||
this.Imports[code] && | ||
this.Imports[code].template | ||
) { | ||
imports[code] = this.Imports[code].template( | ||
this.requiredImports[code], | ||
mode | ||
); | ||
} | ||
} | ||
return importTemplate(imports); | ||
} | ||
return importTemplate(imports); | ||
} | ||
/** | ||
* Used by the generators. Makes a copy of the required imports so | ||
* can be rolled back after a recursion if needed. | ||
* | ||
* @return {Object} | ||
*/ | ||
deepCopyRequiredImports() { | ||
const copy = Object.assign({}, this.requiredImports); | ||
[300, 301, 302, 303, 304, 305, 306].forEach((i) => { | ||
copy[i] = Array.from(this.requiredImports[i]); | ||
}); | ||
return copy; | ||
} | ||
/** | ||
* Used by the generators. Makes a copy of the required imports so | ||
* can be rolled back after a recursion if needed. | ||
* | ||
* @return {Object} | ||
*/ | ||
deepCopyRequiredImports() { | ||
const copy = Object.assign({}, this.requiredImports); | ||
[300, 301, 302, 303, 304, 305, 306].forEach((i) => { | ||
copy[i] = Array.from(this.requiredImports[i]); | ||
}); | ||
return copy; | ||
} | ||
/** | ||
* If the compiler reaches a expression in the input language | ||
* that is not implemented yet. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
*/ | ||
unimplemented(ctx) { | ||
const name = this.renameNode(ctx.constructor.name); | ||
throw new BsonTranspilersUnimplementedError( | ||
`'${name}' not yet implemented` | ||
); | ||
} | ||
/** | ||
* If the compiler reaches a expression in the input language | ||
* that is not implemented yet. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
*/ | ||
unimplemented(ctx) { | ||
const name = this.renameNode(ctx.constructor.name); | ||
throw new BsonTranspilersUnimplementedError( | ||
`'${name}' not yet implemented` | ||
); | ||
} | ||
/** | ||
* Some grammar definitions are written so that comparisons will chain and add | ||
* nodes with a single child when the expression does *not* match. This is a | ||
* helper method (right now used just by Python) that skips nodes downwards | ||
* until a node with multiple children is found, or a node matches "goal". | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {String} goal - Optional: the name of the child to find. | ||
* @return {ParserRuleContext} | ||
*/ | ||
skipFakeNodesDown(ctx, goal) { /* eslint no-unused-vars: 0 */ | ||
return ctx; | ||
} | ||
_getType(ctx) { | ||
if (ctx.type !== undefined) { | ||
/** | ||
* Some grammar definitions are written so that comparisons will chain and add | ||
* nodes with a single child when the expression does *not* match. This is a | ||
* helper method (right now used just by Python) that skips nodes downwards | ||
* until a node with multiple children is found, or a node matches "goal". | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {String} goal - Optional: the name of the child to find. | ||
* @return {ParserRuleContext} | ||
*/ | ||
skipFakeNodesDown(ctx, goal) { | ||
/* eslint no-unused-vars: 0 */ | ||
return ctx; | ||
} | ||
if (!ctx.children) { | ||
_getType(ctx) { | ||
if (ctx.type !== undefined) { | ||
return ctx; | ||
} | ||
if (!ctx.children) { | ||
return null; | ||
} | ||
for (const c of ctx.children) { | ||
const typed = this._getType(c); | ||
if (typed) { | ||
return typed; | ||
} | ||
} | ||
return null; | ||
} | ||
for (const c of ctx.children) { | ||
const typed = this._getType(c); | ||
if (typed) { | ||
return typed; | ||
/** | ||
* Recursively descend down the tree looking for a node with the type set. | ||
* Returns the first child node with a type set. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {ParserRuleContext} | ||
*/ | ||
findTypedNode(ctx) { | ||
const typed = this._getType(ctx); | ||
if (!typed) { | ||
throw new BsonTranspilersInternalError( | ||
'Type not set on any child nodes' | ||
); | ||
} | ||
return typed; | ||
} | ||
return null; | ||
} | ||
/** | ||
* Recursively descend down the tree looking for a node with the type set. | ||
* Returns the first child node with a type set. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {ParserRuleContext} | ||
*/ | ||
findTypedNode(ctx) { | ||
const typed = this._getType(ctx); | ||
if (!typed) { | ||
throw new BsonTranspilersInternalError('Type not set on any child nodes'); | ||
/** | ||
* Get the 'originalType' of ctx, of if it's undefined, keep checking parent | ||
* nodes until an original type is found. Otherwise null. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {ParserRuleContext} | ||
*/ | ||
getParentOriginalType(ctx) { | ||
if (ctx.originalType !== undefined) { | ||
return ctx.originalType; | ||
} | ||
if (ctx.parentCtx) { | ||
return this.getParentOriginalType(ctx.parentCtx); | ||
} | ||
return null; | ||
} | ||
return typed; | ||
} | ||
/** | ||
* Get the 'originalType' of ctx, of if it's undefined, keep checking parent | ||
* nodes until an original type is found. Otherwise null. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {ParserRuleContext} | ||
*/ | ||
getParentOriginalType(ctx) { | ||
if (ctx.originalType !== undefined) { | ||
return ctx.originalType; | ||
} | ||
if (ctx.parentCtx) { | ||
return this.getParentOriginalType(ctx.parentCtx); | ||
} | ||
return null; | ||
} | ||
compareTypes(expectedType, type, ctx, result) { | ||
// If the types are exactly the same, just return. | ||
if ( | ||
expectedType.indexOf(type) !== -1 || | ||
expectedType.indexOf(type.id) !== -1 | ||
) { | ||
return result; | ||
} | ||
compareTypes(expectedType, type, ctx, result) { | ||
// If the types are exactly the same, just return. | ||
if (expectedType.indexOf(type) !== -1 || | ||
expectedType.indexOf(type.id) !== -1) { | ||
return result; | ||
} | ||
const numericTypes = [ | ||
this.Types._integer, this.Types._decimal, this.Types._hex, | ||
this.Types._octal, this.Types._long | ||
]; | ||
// If both expected and node are numeric literals, cast + return | ||
for (let i = 0; i < expectedType.length; i++) { | ||
if (numericTypes.indexOf(type) !== -1 && | ||
numericTypes.indexOf(expectedType[i]) !== -1) { | ||
// Need to visit the octal node always | ||
if (type.id === '_octal') { | ||
return this.leafHelper( | ||
expectedType[i], | ||
{ | ||
const numericTypes = [ | ||
this.Types._integer, | ||
this.Types._decimal, | ||
this.Types._hex, | ||
this.Types._octal, | ||
this.Types._long, | ||
]; | ||
// If both expected and node are numeric literals, cast + return | ||
for (let i = 0; i < expectedType.length; i++) { | ||
if ( | ||
numericTypes.indexOf(type) !== -1 && | ||
numericTypes.indexOf(expectedType[i]) !== -1 | ||
) { | ||
// Need to visit the octal node always | ||
if (type.id === '_octal') { | ||
return this.leafHelper(expectedType[i], { | ||
type: expectedType[i], | ||
originalType: type.id, | ||
getText: () => ( this.visit(ctx) ) | ||
} | ||
); | ||
getText: () => this.visit(ctx), | ||
}); | ||
} | ||
const child = this.skipFakeNodesDown(ctx); | ||
child.originalType = type; | ||
child.type = expectedType[i]; | ||
return this.leafHelper(expectedType[i], child); | ||
} | ||
const child = this.skipFakeNodesDown(ctx); | ||
child.originalType = type; | ||
child.type = expectedType[i]; | ||
return this.leafHelper(expectedType[i], child); | ||
} | ||
} | ||
// If the expected type is "numeric", accept the number basic & bson types | ||
if (expectedType.indexOf(this.Types._numeric) !== -1 && | ||
(numericTypes.indexOf(type) !== -1 || (type.code === 106 || | ||
type.code === 105 || type.code === 104))) { | ||
return result; | ||
// If the expected type is "numeric", accept the number basic & bson types | ||
if ( | ||
expectedType.indexOf(this.Types._numeric) !== -1 && | ||
(numericTypes.indexOf(type) !== -1 || | ||
type.code === 106 || | ||
type.code === 105 || | ||
type.code === 104) | ||
) { | ||
return result; | ||
} | ||
// If the expected type is any number, accept float/int/_numeric | ||
if ( | ||
numericTypes.some((t) => expectedType.indexOf(t) !== -1) && | ||
(type.code === 106 || | ||
type.code === 105 || | ||
type.code === 104 || | ||
type === this.Types._numeric) | ||
) { | ||
return result; | ||
} | ||
return null; | ||
} | ||
// If the expected type is any number, accept float/int/_numeric | ||
if ((numericTypes.some((t) => ( expectedType.indexOf(t) !== -1))) && | ||
(type.code === 106 || type.code === 105 || type.code === 104 || | ||
type === this.Types._numeric)) { | ||
return result; | ||
} | ||
return null; | ||
} | ||
/** | ||
* Convert between numeric types. Required so that we don't end up with | ||
* strange conversions like 'Int32(Double(2))', and can just generate '2'. | ||
* | ||
* @param {Array} expectedType - types to cast to. | ||
* @param {ParserRuleContext} ctx - ctx to cast from, if valid. | ||
* | ||
* @returns {String} - visited result, or null on error. | ||
*/ | ||
castType(expectedType, ctx) { | ||
const result = this.visit(ctx); | ||
const typedCtx = this.findTypedNode(ctx); | ||
let type = typedCtx.type; | ||
/** | ||
* Convert between numeric types. Required so that we don't end up with | ||
* strange conversions like 'Int32(Double(2))', and can just generate '2'. | ||
* | ||
* @param {Array} expectedType - types to cast to. | ||
* @param {ParserRuleContext} ctx - ctx to cast from, if valid. | ||
* | ||
* @returns {String} - visited result, or null on error. | ||
*/ | ||
castType(expectedType, ctx) { | ||
const result = this.visit(ctx); | ||
const typedCtx = this.findTypedNode(ctx); | ||
let type = typedCtx.type; | ||
let equal = this.compareTypes(expectedType, type, ctx, result); | ||
while (equal === null) { | ||
if (type.type === null) { | ||
return null; | ||
let equal = this.compareTypes(expectedType, type, ctx, result); | ||
while (equal === null) { | ||
if (type.type === null) { | ||
return null; | ||
} | ||
type = type.type; | ||
equal = this.compareTypes(expectedType, type, ctx, result); | ||
} | ||
type = type.type; | ||
equal = this.compareTypes(expectedType, type, ctx, result); | ||
return equal; | ||
} | ||
return equal; | ||
} | ||
/** | ||
* Validate each argument against the expected argument types defined in the | ||
* Symbol table. | ||
* | ||
* @param {Array} expected - An array of arrays where each subarray represents | ||
* possible argument types for that index. | ||
* @param {Array} args - empty if no args. | ||
* @param {String} name - The name of the function for error reporting. | ||
* @param {Object} namedArgs - Optional: if named arguments exist, this is the | ||
* mapping of name to default value. | ||
* | ||
* @returns {Array} - Array containing the generated output for each argument. | ||
*/ | ||
checkArguments(expected, args, name, namedArgs) { | ||
const argStr = []; | ||
if (args.length === 0) { | ||
if (expected.length === 0 || expected[0].indexOf(null) !== -1) { | ||
return argStr; | ||
} | ||
throw new BsonTranspilersArgumentError( | ||
`Argument count mismatch: '${name}' requires least one argument` | ||
); | ||
} | ||
if (args.length > expected.length) { | ||
throw new BsonTranspilersArgumentError( | ||
`Argument count mismatch: '${name}' expects ${ | ||
expected.length} args and got ${args.length}` | ||
); | ||
} | ||
for (let i = 0; i < expected.length; i++) { | ||
if (args[i] === undefined) { | ||
if (expected[i].indexOf(null) !== -1) { | ||
/** | ||
* Validate each argument against the expected argument types defined in the | ||
* Symbol table. | ||
* | ||
* @param {Array} expected - An array of arrays where each subarray represents | ||
* possible argument types for that index. | ||
* @param {Array} args - empty if no args. | ||
* @param {String} name - The name of the function for error reporting. | ||
* @param {Object} namedArgs - Optional: if named arguments exist, this is the | ||
* mapping of name to default value. | ||
* | ||
* @returns {Array} - Array containing the generated output for each argument. | ||
*/ | ||
checkArguments(expected, args, name, namedArgs) { | ||
const argStr = []; | ||
if (args.length === 0) { | ||
if (expected.length === 0 || expected[0].indexOf(null) !== -1) { | ||
return argStr; | ||
} | ||
throw new BsonTranspilersArgumentError( | ||
`Argument count mismatch: too few arguments passed to '${name}'` | ||
`Argument count mismatch: '${name}' requires least one argument` | ||
); | ||
} | ||
if (args.length > expected.length) { | ||
throw new BsonTranspilersArgumentError( | ||
`Argument count mismatch: '${name}' expects ${expected.length} args and got ${args.length}` | ||
); | ||
} | ||
for (let i = 0; i < expected.length; i++) { | ||
if (args[i] === undefined) { | ||
if (expected[i].indexOf(null) !== -1) { | ||
return argStr; | ||
} | ||
throw new BsonTranspilersArgumentError( | ||
`Argument count mismatch: too few arguments passed to '${name}'` | ||
); | ||
} | ||
const toCompare = this.checkNamedArgs(expected[i], args[i], namedArgs); | ||
const result = this.castType(...toCompare); | ||
if (result === null) { | ||
const typeStr = expected[i].map((e) => { | ||
const id = e && e.id ? e.id : e; | ||
return e ? id : '[optional]'; | ||
}).join(', '); | ||
const message = `Argument type mismatch: '${name}' expects types ${ | ||
typeStr} but got type ${this.findTypedNode(args[i]).type.id | ||
} for argument at index ${i}`; | ||
const toCompare = this.checkNamedArgs(expected[i], args[i], namedArgs); | ||
const result = this.castType(...toCompare); | ||
if (result === null) { | ||
const typeStr = expected[i] | ||
.map((e) => { | ||
const id = e && e.id ? e.id : e; | ||
return e ? id : '[optional]'; | ||
}) | ||
.join(', '); | ||
const message = `Argument type mismatch: '${name}' expects types ${typeStr} but got type ${ | ||
this.findTypedNode(args[i]).type.id | ||
} for argument at index ${i}`; | ||
throw new BsonTranspilersArgumentError(message); | ||
throw new BsonTranspilersArgumentError(message); | ||
} | ||
argStr.push(result); | ||
} | ||
argStr.push(result); | ||
return argStr; | ||
} | ||
return argStr; | ||
} | ||
/* | ||
* Overriden only by the object generator. | ||
*/ | ||
returnFunctionCallLhs(code, name) { | ||
return name; | ||
} | ||
returnFunctionCallLhsRhs(lhs, rhs, lhsType, l) { | ||
if (lhsType.argsTemplate) { | ||
rhs = lhsType.argsTemplate.bind(this.getState())(l, ...rhs); | ||
} else { | ||
rhs = `(${rhs.join(', ')})`; | ||
/* | ||
* Overriden only by the object generator. | ||
*/ | ||
returnFunctionCallLhs(code, name) { | ||
return name; | ||
} | ||
return `${lhs}${rhs}`; | ||
} | ||
returnAttributeAccess(lhs, rhs, type) { | ||
if (type && type.attr[rhs].template) { | ||
return type.attr[rhs].template(lhs, rhs); | ||
returnFunctionCallLhsRhs(lhs, rhs, lhsType, l) { | ||
if (lhsType.argsTemplate) { | ||
rhs = lhsType.argsTemplate.bind(this.getState())(l, ...rhs); | ||
} else { | ||
rhs = `(${rhs.join(', ')})`; | ||
} | ||
return `${lhs}${rhs}`; | ||
} | ||
return `${lhs}.${rhs}`; | ||
} | ||
returnParenthesis(expr) { | ||
return `(${expr})`; | ||
} | ||
returnSet(args, ctx) { | ||
return this.visitChildren(ctx); | ||
} | ||
/** | ||
* Generate a function call, diverting to process or emit methods if they | ||
* exist. | ||
* @param {ParserRuleContext} ctx | ||
* @return {*} | ||
*/ | ||
generateFunctionCall(ctx) { | ||
const funcNameNode = this.getFunctionCallName(ctx); | ||
const lhs = this.visit(funcNameNode); | ||
let l = lhs; | ||
let lhsType = this.findTypedNode(funcNameNode).type; | ||
if (typeof lhsType === 'string') { | ||
lhsType = this.Types[lhsType]; | ||
returnAttributeAccess(lhs, rhs, type) { | ||
if (type && type.attr[rhs].template) { | ||
return type.attr[rhs].template(lhs, rhs); | ||
} | ||
return `${lhs}.${rhs}`; | ||
} | ||
// Special case | ||
if (`process${lhsType.id}` in this) { | ||
return this[`process${lhsType.id}`](ctx); | ||
returnParenthesis(expr) { | ||
return `(${expr})`; | ||
} | ||
if (`emit${lhsType.id}` in this) { | ||
return this[`emit${lhsType.id}`](ctx); | ||
returnSet(args, ctx) { | ||
return this.visitChildren(ctx); | ||
} | ||
// Check if left-hand-side is callable | ||
ctx.type = lhsType.type; | ||
if (!lhsType.callable) { | ||
throw new BsonTranspilersTypeError(`${lhsType.id} is not callable`); | ||
} | ||
/** | ||
* Generate a function call, diverting to process or emit methods if they | ||
* exist. | ||
* @param {ParserRuleContext} ctx | ||
* @return {*} | ||
*/ | ||
generateFunctionCall(ctx) { | ||
const funcNameNode = this.getFunctionCallName(ctx); | ||
const lhs = this.visit(funcNameNode); | ||
let l = lhs; | ||
let lhsType = this.findTypedNode(funcNameNode).type; | ||
if (typeof lhsType === 'string') { | ||
lhsType = this.Types[lhsType]; | ||
} | ||
// Check arguments | ||
const expectedArgs = lhsType.args; | ||
const rhs = this.checkArguments( | ||
expectedArgs, this.getArguments(ctx), lhsType.id, lhsType.namedArgs | ||
); | ||
// Special case | ||
if (`process${lhsType.id}` in this) { | ||
return this[`process${lhsType.id}`](ctx); | ||
} | ||
if (`emit${lhsType.id}` in this) { | ||
return this[`emit${lhsType.id}`](ctx); | ||
} | ||
// Apply the arguments template | ||
if (lhsType.argsTemplate) { | ||
l = this.visit(this.getIfIdentifier(funcNameNode)); | ||
} | ||
const expr = this.returnFunctionCallLhsRhs(lhs, rhs, lhsType, l); | ||
const constructor = lhsType.callable === this.SYMBOL_TYPE.CONSTRUCTOR; | ||
// Check if left-hand-side is callable | ||
ctx.type = lhsType.type; | ||
if (!lhsType.callable) { | ||
throw new BsonTranspilersTypeError(`${lhsType.id} is not callable`); | ||
} | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(expr, !constructor, lhsType.code) | ||
: expr; | ||
} | ||
// Check arguments | ||
const expectedArgs = lhsType.args; | ||
const rhs = this.checkArguments( | ||
expectedArgs, | ||
this.getArguments(ctx), | ||
lhsType.id, | ||
lhsType.namedArgs | ||
); | ||
/** | ||
* Visit a symbol and error if undefined. Otherwise check symbol table and | ||
* replace if template exists. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {*} | ||
*/ | ||
generateIdentifier(ctx) { | ||
const name = this.visitChildren(ctx); | ||
ctx.type = this.Symbols[name]; | ||
if (ctx.type === undefined) { | ||
throw new BsonTranspilersReferenceError(`Symbol '${name}' is undefined`); | ||
} | ||
this.requiredImports[ctx.type.code] = true; | ||
// Apply the arguments template | ||
if (lhsType.argsTemplate) { | ||
l = this.visit(this.getIfIdentifier(funcNameNode)); | ||
} | ||
const expr = this.returnFunctionCallLhsRhs(lhs, rhs, lhsType, l); | ||
const constructor = lhsType.callable === this.SYMBOL_TYPE.CONSTRUCTOR; | ||
if (ctx.type.template) { | ||
return ctx.type.template(); | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(expr, !constructor, lhsType.code) | ||
: expr; | ||
} | ||
return this.returnFunctionCallLhs(ctx.type.code, name); | ||
} | ||
/** | ||
* Generate attribute access. Visitors are in charge of making sure that | ||
* if lhs type attribute access is not supported, an error is thrown. | ||
* | ||
* NOTE: If the attribute isn't defined on the lhsType, then it will check the | ||
* lhsType.type to see if defined. It will loop, checking the lhs type's type, | ||
* until the attribute exists or the .type is null. If the type is null, | ||
* and the class is a BSON class, then error. If it is a native type however, | ||
* do not error and just return the original attribute. This is to not annoy | ||
* people with attribute errors in languages where it wouldn't throw anyway. | ||
* It feels better to be strict about BSON than the whole language, but it's | ||
* up for debate. TODO: could set type checking to 'strict' or 'non-strict' in | ||
* the visitor, and then only error if we are compiling from a strictly typed | ||
* language. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {*} | ||
*/ | ||
generateAttributeAccess(ctx) { | ||
if ('emitAttributeAccess' in this) { | ||
return this.emitAttributeAccess(ctx); | ||
/** | ||
* Visit a symbol and error if undefined. Otherwise check symbol table and | ||
* replace if template exists. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {*} | ||
*/ | ||
generateIdentifier(ctx) { | ||
const name = this.visitChildren(ctx); | ||
ctx.type = this.Symbols[name]; | ||
if (ctx.type === undefined) { | ||
throw new BsonTranspilersReferenceError( | ||
`Symbol '${name}' is undefined` | ||
); | ||
} | ||
this.requiredImports[ctx.type.code] = true; | ||
if (ctx.type.template) { | ||
return ctx.type.template(); | ||
} | ||
return this.returnFunctionCallLhs(ctx.type.code, name); | ||
} | ||
const lhsNode = this.getAttributeLHS(ctx); | ||
const lhs = this.visit(lhsNode); | ||
const rhs = this.getAttributeRHS(ctx).getText(); | ||
let type = this.findTypedNode(lhsNode).type; | ||
if (typeof type === 'string') { | ||
type = this.Types[type]; | ||
} | ||
while (type !== null) { | ||
if (!(Object.prototype.hasOwnProperty.call(type.attr, rhs))) { | ||
if (type.id in this.BsonTypes && this.BsonTypes[type.id].id !== null) { // TODO: tell symbols vs types | ||
throw new BsonTranspilersAttributeError( | ||
`'${rhs}' not an attribute of ${type.id}` | ||
); | ||
/** | ||
* Generate attribute access. Visitors are in charge of making sure that | ||
* if lhs type attribute access is not supported, an error is thrown. | ||
* | ||
* NOTE: If the attribute isn't defined on the lhsType, then it will check the | ||
* lhsType.type to see if defined. It will loop, checking the lhs type's type, | ||
* until the attribute exists or the .type is null. If the type is null, | ||
* and the class is a BSON class, then error. If it is a native type however, | ||
* do not error and just return the original attribute. This is to not annoy | ||
* people with attribute errors in languages where it wouldn't throw anyway. | ||
* It feels better to be strict about BSON than the whole language, but it's | ||
* up for debate. TODO: could set type checking to 'strict' or 'non-strict' in | ||
* the visitor, and then only error if we are compiling from a strictly typed | ||
* language. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @return {*} | ||
*/ | ||
generateAttributeAccess(ctx) { | ||
if ('emitAttributeAccess' in this) { | ||
return this.emitAttributeAccess(ctx); | ||
} | ||
const lhsNode = this.getAttributeLHS(ctx); | ||
const lhs = this.visit(lhsNode); | ||
const rhs = this.getAttributeRHS(ctx).getText(); | ||
let type = this.findTypedNode(lhsNode).type; | ||
if (typeof type === 'string') { | ||
type = this.Types[type]; | ||
} | ||
while (type !== null) { | ||
if (!Object.prototype.hasOwnProperty.call(type.attr, rhs)) { | ||
if ( | ||
type.id in this.BsonTypes && | ||
this.BsonTypes[type.id].id !== null | ||
) { | ||
// TODO: tell symbols vs types | ||
throw new BsonTranspilersAttributeError( | ||
`'${rhs}' not an attribute of ${type.id}` | ||
); | ||
} | ||
type = type.type; | ||
if (typeof type === 'string') { | ||
type = this.Types[type]; | ||
} | ||
} else { | ||
break; | ||
} | ||
type = type.type; | ||
if (typeof type === 'string') { | ||
type = this.Types[type]; | ||
} | ||
} else { | ||
break; | ||
} | ||
} | ||
if (type === null) { | ||
ctx.type = this.Types._undefined; | ||
// TODO: how strict do we want to be? | ||
if (type === null) { | ||
ctx.type = this.Types._undefined; | ||
// TODO: how strict do we want to be? | ||
return this.returnAttributeAccess(lhs, rhs, type); | ||
} | ||
ctx.type = type.attr[rhs]; | ||
return this.returnAttributeAccess(lhs, rhs, type); | ||
} | ||
ctx.type = type.attr[rhs]; | ||
return this.returnAttributeAccess(lhs, rhs, type); | ||
} | ||
/** | ||
* This helper function checks for an emit method then applies the templates | ||
* if they exist for a function call node. Used primarily by process methods. | ||
* | ||
* @param {ParserRuleContext} ctx - The function call node | ||
* @param {Object} lhsType - The type | ||
* @param {Array} args - Arguments to the template | ||
* @param {String} defaultT - The default name if no template exists. | ||
* @param {String} defaultA - The default arguments if no argsTemplate exists. | ||
* @param {Boolean} skipNew - Optional: If true, never add new. | ||
* @param {Boolean} skipLhs - Optional: If true, don't add lhs to result. | ||
* | ||
* @return {*} | ||
*/ | ||
generateCall(ctx, lhsType, args, defaultT, defaultA, skipNew, skipLhs) { | ||
if (`emit${lhsType.id}` in this) { | ||
return this[`emit${lhsType.id}`](ctx); | ||
/** | ||
* This helper function checks for an emit method then applies the templates | ||
* if they exist for a function call node. Used primarily by process methods. | ||
* | ||
* @param {ParserRuleContext} ctx - The function call node | ||
* @param {Object} lhsType - The type | ||
* @param {Array} args - Arguments to the template | ||
* @param {String} defaultT - The default name if no template exists. | ||
* @param {String} defaultA - The default arguments if no argsTemplate exists. | ||
* @param {Boolean} skipNew - Optional: If true, never add new. | ||
* @param {Boolean} skipLhs - Optional: If true, don't add lhs to result. | ||
* | ||
* @return {*} | ||
*/ | ||
generateCall(ctx, lhsType, args, defaultT, defaultA, skipNew, skipLhs) { | ||
if (`emit${lhsType.id}` in this) { | ||
return this[`emit${lhsType.id}`](ctx); | ||
} | ||
const lhsArg = lhsType.template ? lhsType.template() : defaultT; | ||
const rhs = lhsType.argsTemplate | ||
? lhsType.argsTemplate.bind(this.getState())(lhsArg, ...args) | ||
: defaultA; | ||
const lhs = skipLhs ? '' : lhsArg; | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(`${lhs}${rhs}`, skipNew, lhsType.code) | ||
: `${lhs}${rhs}`; | ||
} | ||
const lhsArg = lhsType.template | ||
? lhsType.template() | ||
: defaultT; | ||
const rhs = lhsType.argsTemplate | ||
? lhsType.argsTemplate.bind(this.getState())(lhsArg, ...args) | ||
: defaultA; | ||
const lhs = skipLhs ? '' : lhsArg; | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(`${lhs}${rhs}`, skipNew, lhsType.code) | ||
: `${lhs}${rhs}`; | ||
} | ||
generateObjectLiteral(ctx) { | ||
if (this.idiomatic && 'emitIdiomaticObjectLiteral' in this) { | ||
return this.emitIdiomaticObjectLiteral(ctx); | ||
generateObjectLiteral(ctx) { | ||
if (this.idiomatic && 'emitIdiomaticObjectLiteral' in this) { | ||
return this.emitIdiomaticObjectLiteral(ctx); | ||
} | ||
const type = this.Types._object; | ||
if (`emit${type.id}` in this) { | ||
return this[`emit${type.id}`](ctx); | ||
} | ||
ctx.type = type; | ||
this.requiredImports[10] = true; | ||
ctx.indentDepth = this.findIndentDepth(ctx) + 1; | ||
let args = ''; | ||
const keysAndValues = this.getKeyValueList(ctx); | ||
if (ctx.type.argsTemplate) { | ||
args = ctx.type.argsTemplate.bind(this.getState())( | ||
this.getKeyValueList(ctx).map((k) => { | ||
return [this.getKeyStr(k), this.visit(this.getValue(k))]; | ||
}), | ||
ctx.indentDepth | ||
); | ||
} else { | ||
args = this.visit(keysAndValues); | ||
} | ||
ctx.indentDepth--; | ||
if (ctx.type.template) { | ||
return ctx.type.template.bind(this.getState())(args, ctx.indentDepth); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
const type = this.Types._object; | ||
if (`emit${type.id}` in this) { | ||
return this[`emit${type.id}`](ctx); | ||
} | ||
ctx.type = type; | ||
this.requiredImports[10] = true; | ||
ctx.indentDepth = this.findIndentDepth(ctx) + 1; | ||
let args = ''; | ||
const keysAndValues = this.getKeyValueList(ctx); | ||
if (ctx.type.argsTemplate) { | ||
args = ctx.type.argsTemplate.bind(this.getState())( | ||
this.getKeyValueList(ctx).map((k) => { | ||
return [this.getKeyStr(k), this.visit(this.getValue(k))]; | ||
}), | ||
ctx.indentDepth); | ||
} else { | ||
args = this.visit(keysAndValues); | ||
} | ||
ctx.indentDepth--; | ||
if (ctx.type.template) { | ||
return ctx.type.template.bind(this.getState())(args, ctx.indentDepth); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
generateArrayLiteral(ctx) { | ||
const type = this.Types._array; | ||
if (`emit${type.id}` in this) { | ||
return this[`emit${type.id}`](ctx); | ||
generateArrayLiteral(ctx) { | ||
const type = this.Types._array; | ||
if (`emit${type.id}` in this) { | ||
return this[`emit${type.id}`](ctx); | ||
} | ||
ctx.type = type; | ||
ctx.indentDepth = this.findIndentDepth(ctx) + 1; | ||
this.requiredImports[9] = true; | ||
let args = ''; | ||
const list = this.getList(ctx); | ||
const visitedElements = list.map((child) => this.visit(child)); | ||
if (ctx.type.argsTemplate) { | ||
// NOTE: not currently being used anywhere. | ||
args = visitedElements | ||
.map((arg, index) => { | ||
const last = !visitedElements[index + 1]; | ||
return ctx.type.argsTemplate.bind(this.getState())( | ||
arg, | ||
ctx.indentDepth, | ||
last | ||
); | ||
}) | ||
.join(''); | ||
} else { | ||
args = visitedElements.join(', '); | ||
} | ||
if (ctx.type.template) { | ||
return ctx.type.template(args, ctx.indentDepth); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
ctx.type = type; | ||
ctx.indentDepth = this.findIndentDepth(ctx) + 1; | ||
this.requiredImports[9] = true; | ||
let args = ''; | ||
const list = this.getList(ctx); | ||
const visitedElements = list.map((child) => ( this.visit(child) )); | ||
if (ctx.type.argsTemplate) { // NOTE: not currently being used anywhere. | ||
args = visitedElements.map((arg, index) => { | ||
const last = !visitedElements[index + 1]; | ||
return ctx.type.argsTemplate.bind(this.getState())(arg, ctx.indentDepth, last); | ||
}).join(''); | ||
} else { | ||
args = visitedElements.join(', '); | ||
} | ||
if (ctx.type.template) { | ||
return ctx.type.template(args, ctx.indentDepth); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
/** | ||
* Called from the process methods of numeric class constructors. | ||
* We know there will be a single (sometimes optional) argument that is | ||
* a number or string. | ||
* | ||
* Required because we want to pass the argument type to the template | ||
* so that we can determine if the generated number needs to be parsed or | ||
* casted. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @returns {String} | ||
*/ | ||
generateNumericClass(ctx) { | ||
const funcNameNode = this.getFunctionCallName(ctx); | ||
const lhsStr = this.visit(funcNameNode); | ||
let lhsType = this.findTypedNode(funcNameNode).type; | ||
if (typeof lhsType === 'string') { | ||
lhsType = this.Types[lhsType]; | ||
} | ||
ctx.type = lhsType.type; | ||
if (`emit${lhsType.id}` in this) { | ||
this[`emit${lhsType.id}`](ctx); | ||
} | ||
/** | ||
* Called from the process methods of numeric class constructors. | ||
* We know there will be a single (sometimes optional) argument that is | ||
* a number or string. | ||
* | ||
* Required because we want to pass the argument type to the template | ||
* so that we can determine if the generated number needs to be parsed or | ||
* casted. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @returns {String} | ||
*/ | ||
generateNumericClass(ctx) { | ||
const funcNameNode = this.getFunctionCallName(ctx); | ||
const lhsStr = this.visit(funcNameNode); | ||
let lhsType = this.findTypedNode(funcNameNode).type; | ||
if (typeof lhsType === 'string') { | ||
lhsType = this.Types[lhsType]; | ||
} | ||
ctx.type = lhsType.type; | ||
if (`emit${lhsType.id}` in this) { | ||
this[`emit${lhsType.id}`](ctx); | ||
} | ||
// Get the original type of the argument | ||
const expectedArgs = lhsType.args; | ||
let args = this.checkArguments( | ||
expectedArgs, this.getArguments(ctx), lhsType.id, lhsType.namedArgs | ||
); | ||
let argType; | ||
// Get the original type of the argument | ||
const expectedArgs = lhsType.args; | ||
let args = this.checkArguments( | ||
expectedArgs, | ||
this.getArguments(ctx), | ||
lhsType.id, | ||
lhsType.namedArgs | ||
); | ||
let argType; | ||
if (args.length === 0) { | ||
args = ['0']; | ||
argType = this.Types._integer; | ||
} else { | ||
const argNode = this.getArgumentAt(ctx, 0); | ||
const typed = this.findTypedNode(argNode); | ||
argType = typed.originalType !== undefined ? | ||
typed.originalType : | ||
typed.type; | ||
if (args.length === 0) { | ||
args = ['0']; | ||
argType = this.Types._integer; | ||
} else { | ||
const argNode = this.getArgumentAt(ctx, 0); | ||
const typed = this.findTypedNode(argNode); | ||
argType = | ||
typed.originalType !== undefined ? typed.originalType : typed.type; | ||
} | ||
return this.generateCall( | ||
ctx, | ||
lhsType, | ||
[args[0], argType.id], | ||
lhsStr, | ||
`(${args.join(', ')})` | ||
); | ||
} | ||
return this.generateCall( | ||
ctx, lhsType, [args[0], argType.id], lhsStr, `(${args.join(', ')})` | ||
); | ||
} | ||
/** | ||
* Same as generateCall but for type literals instead of function calls. | ||
* | ||
* @param {ParserRuleContext} ctx - The literal node | ||
* @param {Object} lhsType - The type | ||
* @param {Array} args - Arguments to the template | ||
* @param {String} defaultT - The default if no template exists. | ||
* @param {Boolean} skipNew - Optional: If true, never add new. | ||
* | ||
* @return {*} | ||
*/ | ||
generateLiteral(ctx, lhsType, args, defaultT, skipNew) { | ||
if (`emit${lhsType.id}` in this) { | ||
return this[`emit${lhsType.id}`](ctx); | ||
/** | ||
* Same as generateCall but for type literals instead of function calls. | ||
* | ||
* @param {ParserRuleContext} ctx - The literal node | ||
* @param {Object} lhsType - The type | ||
* @param {Array} args - Arguments to the template | ||
* @param {String} defaultT - The default if no template exists. | ||
* @param {Boolean} skipNew - Optional: If true, never add new. | ||
* | ||
* @return {*} | ||
*/ | ||
generateLiteral(ctx, lhsType, args, defaultT, skipNew) { | ||
if (`emit${lhsType.id}` in this) { | ||
return this[`emit${lhsType.id}`](ctx); | ||
} | ||
if (lhsType.template) { | ||
const str = lhsType.template(...args); | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(str, skipNew, lhsType.code) | ||
: str; | ||
} | ||
return defaultT; | ||
} | ||
if (lhsType.template) { | ||
const str = lhsType.template(...args); | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(str, skipNew, lhsType.code) | ||
: str; | ||
} | ||
return defaultT; | ||
} | ||
/** | ||
* Helper method for generating literals. Called from the visit methods for | ||
* literal nodes. | ||
* | ||
* @param {Object} setType - the type to set the literal to. | ||
* @param {ParserRuleContext} ctx - the tree node. | ||
* @return {*} | ||
*/ | ||
leafHelper(setType, ctx) { | ||
ctx.type = setType; | ||
this.requiredImports[ctx.type.code] = true; | ||
/** | ||
* Helper method for generating literals. Called from the visit methods for | ||
* literal nodes. | ||
* | ||
* @param {Object} setType - the type to set the literal to. | ||
* @param {ParserRuleContext} ctx - the tree node. | ||
* @return {*} | ||
*/ | ||
leafHelper(setType, ctx) { | ||
ctx.type = setType; | ||
this.requiredImports[ctx.type.code] = true; | ||
// Pass the original argument type to the template, not the casted type. | ||
const parentOriginalType = this.getParentOriginalType(ctx); | ||
const type = parentOriginalType === null ? ctx.type : parentOriginalType; | ||
// Pass the original argument type to the template, not the casted type. | ||
const parentOriginalType = this.getParentOriginalType(ctx); | ||
const type = parentOriginalType === null ? ctx.type : parentOriginalType; | ||
if (`process${ctx.type.id}` in this) { | ||
return this[`process${ctx.type.id}`](ctx); | ||
if (`process${ctx.type.id}` in this) { | ||
return this[`process${ctx.type.id}`](ctx); | ||
} | ||
if (`emit${ctx.type.id}` in this) { | ||
return this[`emit${ctx.type.id}`](ctx); | ||
} | ||
const children = ctx.getText(); | ||
return this.generateLiteral( | ||
ctx, | ||
ctx.type, | ||
[children, type.id], | ||
children, | ||
true | ||
); | ||
} | ||
if (`emit${ctx.type.id}` in this) { | ||
return this[`emit${ctx.type.id}`](ctx); | ||
} | ||
const children = ctx.getText(); | ||
return this.generateLiteral(ctx, ctx.type, [children, type.id], children, true); | ||
} | ||
findIndentDepth(ctx) { | ||
while (ctx.indentDepth === undefined) { | ||
ctx = ctx.parentCtx; | ||
if (ctx === undefined || ctx === null) { | ||
return -1; | ||
findIndentDepth(ctx) { | ||
while (ctx.indentDepth === undefined) { | ||
ctx = ctx.parentCtx; | ||
if (ctx === undefined || ctx === null) { | ||
return -1; | ||
} | ||
} | ||
return ctx.indentDepth; | ||
} | ||
return ctx.indentDepth; | ||
} | ||
/** | ||
* Process required for BSON regex types so we can validate the flags. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Object} type - The type of the LHS | ||
* @param {Object} symbolType - The type of the Symbol | ||
* @return {*} | ||
*/ | ||
generateBSONRegex(ctx, type, symbolType) { | ||
if (`emit${type.id}` in this) { | ||
return this[`emit${type.id}`](ctx); | ||
} | ||
ctx.type = type; | ||
/** | ||
* Process required for BSON regex types so we can validate the flags. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Object} type - The type of the LHS | ||
* @param {Object} symbolType - The type of the Symbol | ||
* @return {*} | ||
*/ | ||
generateBSONRegex(ctx, type, symbolType) { | ||
if (`emit${type.id}` in this) { | ||
return this[`emit${type.id}`](ctx); | ||
} | ||
ctx.type = type; | ||
const args = this.checkArguments( | ||
symbolType.args, this.getArguments(ctx), type.id, symbolType.namedArgs | ||
); | ||
const args = this.checkArguments( | ||
symbolType.args, | ||
this.getArguments(ctx), | ||
type.id, | ||
symbolType.namedArgs | ||
); | ||
const expectedFlags = this.Syntax.bsonRegexFlags | ||
? this.Syntax.bsonRegexFlags | ||
: { i: 'i', m: 'm', x: 'x', s: 's', l: 'l', u: 'u' }; | ||
const expectedFlags = this.Syntax.bsonRegexFlags | ||
? this.Syntax.bsonRegexFlags | ||
: { i: 'i', m: 'm', x: 'x', s: 's', l: 'l', u: 'u' }; | ||
let flags = null; | ||
const pattern = args[0]; | ||
if (args.length === 2) { | ||
flags = args[1]; | ||
for (let i = 1; i < flags.length - 1; i++) { | ||
if (!(flags[i] in expectedFlags)) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Invalid flag '${flags[i]}' passed to Regexp` | ||
); | ||
let flags = null; | ||
const pattern = args[0]; | ||
if (args.length === 2) { | ||
flags = args[1]; | ||
for (let i = 1; i < flags.length - 1; i++) { | ||
if (!(flags[i] in expectedFlags)) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Invalid flag '${flags[i]}' passed to Regexp` | ||
); | ||
} | ||
} | ||
flags = flags.replace(/[imxlsu]/g, (m) => expectedFlags[m]); | ||
} | ||
flags = flags.replace(/[imxlsu]/g, m => expectedFlags[m]); | ||
} | ||
return this.generateCall( | ||
ctx, symbolType, [pattern, flags], 'Regex', | ||
`(${pattern}${flags ? ', ' + flags : ''})` | ||
); | ||
} | ||
/** | ||
* Code is processed in every language because want to generate the scope as | ||
* a non-idiomatic document. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Object} type - The type of the LHS | ||
* @param {Object} symbolType - The type of the Symbol | ||
* @param {boolean} requireString - if the code argument must be a string. | ||
* @return {*} | ||
*/ | ||
generateBSONCode(ctx, type, symbolType, requireString) { | ||
if (`emit${type.id}` in this) { | ||
return this[`emit${type.id}`](ctx); | ||
} | ||
ctx.type = type; | ||
const argList = this.getArguments(ctx); | ||
if (!(argList.length === 1 || argList.length === 2)) { | ||
throw new BsonTranspilersArgumentError( | ||
'Argument count mismatch: Code requires one or two arguments' | ||
return this.generateCall( | ||
ctx, | ||
symbolType, | ||
[pattern, flags], | ||
'Regex', | ||
`(${pattern}${flags ? ', ' + flags : ''})` | ||
); | ||
} | ||
let code = ''; | ||
if (requireString) { | ||
const arg = this.getArgumentAt(ctx, 0); | ||
code = this.visit(arg); | ||
if (this.findTypedNode(arg).type !== this.Types._string) { | ||
/** | ||
* Code is processed in every language because want to generate the scope as | ||
* a non-idiomatic document. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Object} type - The type of the LHS | ||
* @param {Object} symbolType - The type of the Symbol | ||
* @param {boolean} requireString - if the code argument must be a string. | ||
* @return {*} | ||
*/ | ||
generateBSONCode(ctx, type, symbolType, requireString) { | ||
if (`emit${type.id}` in this) { | ||
return this[`emit${type.id}`](ctx); | ||
} | ||
ctx.type = type; | ||
const argList = this.getArguments(ctx); | ||
if (!(argList.length === 1 || argList.length === 2)) { | ||
throw new BsonTranspilersArgumentError( | ||
'Argument type mismatch: Code requires first argument to be a string' | ||
'Argument count mismatch: Code requires one or two arguments' | ||
); | ||
} | ||
} else { | ||
code = removeQuotes(this.getArgumentAt(ctx, 0).getText()); | ||
} | ||
let scope = undefined; | ||
let scopestr = ''; | ||
let code = ''; | ||
if (requireString) { | ||
const arg = this.getArgumentAt(ctx, 0); | ||
code = this.visit(arg); | ||
if (this.findTypedNode(arg).type !== this.Types._string) { | ||
throw new BsonTranspilersArgumentError( | ||
'Argument type mismatch: Code requires first argument to be a string' | ||
); | ||
} | ||
} else { | ||
code = removeQuotes(this.getArgumentAt(ctx, 0).getText()); | ||
} | ||
let scope = undefined; | ||
let scopestr = ''; | ||
if (argList.length === 2) { | ||
const idiomatic = this.idiomatic; | ||
this.idiomatic = false; | ||
const compareTo = this.checkNamedArgs( | ||
[this.Types._object], this.getArgumentAt(ctx, 1), symbolType.namedArgs | ||
); | ||
scope = this.castType(...compareTo); | ||
if (scope === null) { | ||
throw new BsonTranspilersArgumentError( | ||
'Code expects argument \'scope\' to be object' | ||
if (argList.length === 2) { | ||
const idiomatic = this.idiomatic; | ||
this.idiomatic = false; | ||
const compareTo = this.checkNamedArgs( | ||
[this.Types._object], | ||
this.getArgumentAt(ctx, 1), | ||
symbolType.namedArgs | ||
); | ||
scope = this.castType(...compareTo); | ||
if (scope === null) { | ||
throw new BsonTranspilersArgumentError( | ||
"Code expects argument 'scope' to be object" | ||
); | ||
} | ||
this.idiomatic = idiomatic; | ||
scopestr = `, ${scope}`; | ||
this.requiredImports[113] = true; | ||
this.requiredImports[10] = true; | ||
} | ||
this.idiomatic = idiomatic; | ||
scopestr = `, ${scope}`; | ||
this.requiredImports[113] = true; | ||
this.requiredImports[10] = true; | ||
return this.generateCall( | ||
ctx, | ||
symbolType, | ||
[code, scope], | ||
'Code', | ||
`(${code}${scopestr})` | ||
); | ||
} | ||
return this.generateCall( | ||
ctx, symbolType, [code, scope], 'Code', `(${code}${scopestr})` | ||
); | ||
} | ||
/** | ||
* Gets a process method because need to tell the template if | ||
* the argument is a number or a date. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @returns {String} - generated code | ||
*/ | ||
generateObjectIdFromTime(ctx) { | ||
const funcNameNode = this.getFunctionCallName(ctx); | ||
const lhsStr = this.visit(funcNameNode); | ||
let lhsType = this.findTypedNode(funcNameNode).type; | ||
if (typeof lhsType === 'string') { | ||
lhsType = this.Types[lhsType]; | ||
/** | ||
* Gets a process method because need to tell the template if | ||
* the argument is a number or a date. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @returns {String} - generated code | ||
*/ | ||
generateObjectIdFromTime(ctx) { | ||
const funcNameNode = this.getFunctionCallName(ctx); | ||
const lhsStr = this.visit(funcNameNode); | ||
let lhsType = this.findTypedNode(funcNameNode).type; | ||
if (typeof lhsType === 'string') { | ||
lhsType = this.Types[lhsType]; | ||
} | ||
const args = this.checkArguments( | ||
lhsType.args, | ||
this.getArguments(ctx), | ||
lhsType.id, | ||
lhsType.namedArgs | ||
); | ||
const isNumber = | ||
this.findTypedNode(this.getArgumentAt(ctx, 0)).type.code !== 200; | ||
return this.generateCall( | ||
ctx, | ||
lhsType, | ||
[args[0], isNumber], | ||
lhsStr, | ||
`(${args.join(', ')})`, | ||
true | ||
); | ||
} | ||
const args = this.checkArguments( | ||
lhsType.args, this.getArguments(ctx), lhsType.id, lhsType.namedArgs | ||
); | ||
const isNumber = this.findTypedNode( | ||
this.getArgumentAt(ctx, 0)).type.code !== 200; | ||
return this.generateCall( | ||
ctx, lhsType, [args[0], isNumber], lhsStr, `(${args.join(', ')})`, true | ||
); | ||
} | ||
generateFuncDefExpression() { | ||
throw new BsonTranspilersUnimplementedError( | ||
'Support for exporting functions to languages other than javascript is not yet available.' | ||
); | ||
} | ||
generateFuncDefExpression() { | ||
throw new BsonTranspilersUnimplementedError( | ||
'Support for exporting functions to languages other than javascript is not yet available.' | ||
); | ||
} | ||
/** | ||
* Overrides the ANTLR visitChildren method so that options can be set. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Object} options: | ||
* start - child index to start iterating at. | ||
* end - child index to end iterating after. | ||
* step - how many children to increment each step, 1 visits all children. | ||
* separator - a string separator to go between generated children. | ||
* ignore - an array of child indexes to skip. | ||
* children - the set of children to visit. | ||
* @returns {String} | ||
*/ | ||
visitChildren(ctx, options) { | ||
const opts = { | ||
start: 0, | ||
step: 1, | ||
separator: '', | ||
ignore: [], | ||
children: ctx.children, | ||
}; | ||
Object.assign(opts, options ? options : {}); | ||
opts.end = 'end' in opts ? opts.end : opts.children.length - 1; | ||
/** | ||
* Overrides the ANTLR visitChildren method so that options can be set. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Object} options: | ||
* start - child index to start iterating at. | ||
* end - child index to end iterating after. | ||
* step - how many children to increment each step, 1 visits all children. | ||
* separator - a string separator to go between generated children. | ||
* ignore - an array of child indexes to skip. | ||
* children - the set of children to visit. | ||
* @returns {String} | ||
*/ | ||
visitChildren(ctx, options) { | ||
const opts = { | ||
start: 0, step: 1, separator: '', ignore: [], children: ctx.children | ||
}; | ||
Object.assign(opts, options ? options : {}); | ||
opts.end = ('end' in opts) ? opts.end : opts.children.length - 1; | ||
let code = ''; | ||
for (let i = opts.start; i <= opts.end; i += opts.step) { | ||
if (opts.ignore.indexOf(i) === -1) { | ||
code = `${code}${this.visit(opts.children[i])}${ | ||
i === opts.end ? '' : opts.separator | ||
}`; | ||
} | ||
} | ||
return code; | ||
} | ||
let code = ''; | ||
for (let i = opts.start; i <= opts.end; i += opts.step) { | ||
if (opts.ignore.indexOf(i) === -1) { | ||
code = `${code}${this.visit( | ||
opts.children[i] | ||
)}${(i === opts.end) ? '' : opts.separator}`; | ||
/** | ||
* Visit a end-of-file symbol. Universal for all grammars. | ||
* * | ||
* @returns {String} | ||
*/ | ||
visitEof() { | ||
if (this.Syntax.eof.template) { | ||
return this.Syntax.eof.template(); | ||
} | ||
return ''; | ||
} | ||
return code; | ||
} | ||
/** | ||
* Visit a end-of-file symbol. Universal for all grammars. | ||
* * | ||
* @returns {String} | ||
*/ | ||
visitEof() { | ||
if (this.Syntax.eof.template) { | ||
return this.Syntax.eof.template(); | ||
/** | ||
* Visit a end-of-line symbol. Universal for all grammars. | ||
* * | ||
* @returns {String} | ||
*/ | ||
visitEos() { | ||
if (this.Syntax.eos.template) { | ||
return this.Syntax.eos.template(); | ||
} | ||
return '\n'; | ||
} | ||
return ''; | ||
} | ||
/** | ||
* Visit a end-of-line symbol. Universal for all grammars. | ||
* * | ||
* @returns {String} | ||
*/ | ||
visitEos() { | ||
if (this.Syntax.eos.template) { | ||
return this.Syntax.eos.template(); | ||
/** | ||
* Visit a leaf node and return a string. Universal for all grammars. | ||
* * | ||
* @param {ParserRuleContext} ctx | ||
* @returns {String} | ||
*/ | ||
visitTerminal(ctx) { | ||
return ctx.getText(); | ||
} | ||
return '\n'; | ||
} | ||
/** | ||
* Visit a leaf node and return a string. Universal for all grammars. | ||
* * | ||
* @param {ParserRuleContext} ctx | ||
* @returns {String} | ||
*/ | ||
visitTerminal(ctx) { | ||
return ctx.getText(); | ||
} | ||
}; | ||
}; |
@@ -5,6 +5,7 @@ 'use strict'; | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; |
@@ -46,3 +46,5 @@ 'use strict'; | ||
for (var i = 0; i < existing.length; i++) { | ||
const candidate = `${this.varTemplateRoot(templateID, varRoot)}${i > 0 ? i : ''}`; | ||
const candidate = `${this.varTemplateRoot(templateID, varRoot)}${ | ||
i > 0 ? i : '' | ||
}`; | ||
const current = this.vars[declaration(candidate)]; | ||
@@ -57,3 +59,5 @@ if (current !== undefined) { | ||
const varTemplateRoot = this.varTemplateRoot(templateID, varRoot); | ||
return Object.values(this.vars).filter(varName => varName.startsWith(varTemplateRoot)); | ||
return Object.values(this.vars).filter((varName) => | ||
varName.startsWith(varTemplateRoot) | ||
); | ||
} | ||
@@ -60,0 +64,0 @@ |
@@ -23,3 +23,8 @@ 'use strict'; | ||
syntaxError(recognizer, symbol, line, column, message, payload) { | ||
throw new BsonTranspilersSyntaxError(message, { symbol, line, column, payload }); | ||
throw new BsonTranspilersSyntaxError(message, { | ||
symbol, | ||
line, | ||
column, | ||
payload, | ||
}); | ||
} | ||
@@ -26,0 +31,0 @@ } |
@@ -5,4 +5,7 @@ 'use strict'; | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { super(); } | ||
}; | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; |
'use strict'; | ||
/* eslint complexity: 0 */ | ||
const {doubleQuoteStringify, removeQuotes} = require('../../helper/format'); | ||
const { doubleQuoteStringify, removeQuotes } = require('../../helper/format'); | ||
const { | ||
BsonTranspilersRuntimeError, | ||
BsonTranspilersUnimplementedError | ||
BsonTranspilersUnimplementedError, | ||
} = require('../../helper/error'); | ||
@@ -12,672 +12,784 @@ | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
// Operations that take the field name as an argument | ||
this.field_opts = [ | ||
'gt', 'lt', 'lte', 'gte', 'eq', 'ne', 'nin', 'in', 'not', 'exists', | ||
'type', 'all', 'size', 'elemMatch', 'mod', 'regex', | ||
'sum', 'avg', 'first', 'last', 'max', 'min', 'push', 'addToSet', | ||
'stdDevSamp', 'stdDevPop', | ||
'bitsAllSet', 'bitsAllClear', 'bitsAnySet', 'bitsAnyClear', | ||
'geoWithin', 'geoIntersects', 'near', 'nearSphere' | ||
]; | ||
// Operations that convert by {$op: value} => op(value) | ||
this.opts = [ | ||
'match', 'skip', 'limit', 'out', 'sortByCount', 'count', 'or', 'nor', | ||
'and' | ||
]; | ||
// Operations split by their import class | ||
this.builderImports = [ | ||
// Filter ops | ||
[ | ||
'all', 'and', 'bitsAllClear', 'bitsAllSet', 'bitsAnyClear', 'bitsAnySet', | ||
'elemMatch', 'eq', 'exists', 'expr', 'geoIntersects', 'geoWithin', | ||
'geoWithinBox', 'geoWithinCenter', 'geoWithinCenterSphere', 'geoWithinPolygon', | ||
'gt', 'gte', 'in', 'lt', 'lte', 'mod', 'ne', 'near', 'nearSphere', 'nin', | ||
'nor', 'not', 'or', 'regex', 'size', 'text', 'type', 'where', 'options' | ||
], | ||
// Agg ops | ||
[ | ||
'addFields', 'bucket', 'bucketAuto', 'count', 'facet', 'graphLookup', | ||
'group', 'limit', 'lookup', 'match', 'out', 'project', 'replaceRoot', | ||
'sample', 'skip', 'sort', 'sortByCount', 'unwind' | ||
], | ||
// Accumulator ops | ||
[ | ||
'addToSet', 'avg', 'first', 'last', 'max', 'min', 'push', | ||
'stdDevPop', 'stdDevSamp', 'sum' | ||
] | ||
].reduce((obj, list, index) => { | ||
list.forEach((op) => { | ||
obj[op] = index + 300; | ||
}); | ||
return obj; | ||
}, {}); | ||
} | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
// Operations that take the field name as an argument | ||
this.field_opts = [ | ||
'gt', | ||
'lt', | ||
'lte', | ||
'gte', | ||
'eq', | ||
'ne', | ||
'nin', | ||
'in', | ||
'not', | ||
'exists', | ||
'type', | ||
'all', | ||
'size', | ||
'elemMatch', | ||
'mod', | ||
'regex', | ||
'sum', | ||
'avg', | ||
'first', | ||
'last', | ||
'max', | ||
'min', | ||
'push', | ||
'addToSet', | ||
'stdDevSamp', | ||
'stdDevPop', | ||
'bitsAllSet', | ||
'bitsAllClear', | ||
'bitsAnySet', | ||
'bitsAnyClear', | ||
'geoWithin', | ||
'geoIntersects', | ||
'near', | ||
'nearSphere', | ||
]; | ||
// Operations that convert by {$op: value} => op(value) | ||
this.opts = [ | ||
'match', | ||
'skip', | ||
'limit', | ||
'out', | ||
'sortByCount', | ||
'count', | ||
'or', | ||
'nor', | ||
'and', | ||
]; | ||
// Operations split by their import class | ||
this.builderImports = [ | ||
// Filter ops | ||
[ | ||
'all', | ||
'and', | ||
'bitsAllClear', | ||
'bitsAllSet', | ||
'bitsAnyClear', | ||
'bitsAnySet', | ||
'elemMatch', | ||
'eq', | ||
'exists', | ||
'expr', | ||
'geoIntersects', | ||
'geoWithin', | ||
'geoWithinBox', | ||
'geoWithinCenter', | ||
'geoWithinCenterSphere', | ||
'geoWithinPolygon', | ||
'gt', | ||
'gte', | ||
'in', | ||
'lt', | ||
'lte', | ||
'mod', | ||
'ne', | ||
'near', | ||
'nearSphere', | ||
'nin', | ||
'nor', | ||
'not', | ||
'or', | ||
'regex', | ||
'size', | ||
'text', | ||
'type', | ||
'where', | ||
'options', | ||
], | ||
// Agg ops | ||
[ | ||
'addFields', | ||
'bucket', | ||
'bucketAuto', | ||
'count', | ||
'facet', | ||
'graphLookup', | ||
'group', | ||
'limit', | ||
'lookup', | ||
'match', | ||
'out', | ||
'project', | ||
'replaceRoot', | ||
'sample', | ||
'skip', | ||
'sort', | ||
'sortByCount', | ||
'unwind', | ||
], | ||
// Accumulator ops | ||
[ | ||
'addToSet', | ||
'avg', | ||
'first', | ||
'last', | ||
'max', | ||
'min', | ||
'push', | ||
'stdDevPop', | ||
'stdDevSamp', | ||
'sum', | ||
], | ||
].reduce((obj, list, index) => { | ||
list.forEach((op) => { | ||
obj[op] = index + 300; | ||
}); | ||
return obj; | ||
}, {}); | ||
} | ||
/** The rest of the functions in this file are for generating builders **/ | ||
/** The rest of the functions in this file are for generating builders **/ | ||
/** | ||
* Emit an "idiomatic" filter or aggregation, meaning use the builders | ||
* instead of a regular object if possible. | ||
* | ||
* @param {ObjectLiteralContext} ctx | ||
* @return {String} | ||
*/ | ||
emitIdiomaticObjectLiteral(ctx) { | ||
ctx.type = this.Types._object; | ||
ctx.indentDepth = this.findIndentDepth(ctx) + 1; | ||
let multiOps = false; | ||
let args = ''; | ||
const properties = this.getKeyValueList(ctx); | ||
if (properties.length) { | ||
args = properties.map((pair) => { | ||
const field = this.getKeyStr(pair); | ||
const value = this.getValue(pair); | ||
if (field.startsWith('$')) { | ||
const op = field.substr(1); | ||
if (this.builderImports[op]) { | ||
this.requiredImports[this.builderImports[op]].push(op); | ||
} | ||
if (op === 'regex') { | ||
multiOps = true; | ||
} | ||
if (`handle${op}` in this) { | ||
return this[`handle${op}`](this.getObjectChild(value), op, ctx); | ||
} | ||
if (this.field_opts.indexOf(op) !== -1) { | ||
// Assert that this isn't the top-level object | ||
if (!this.isSubObject(ctx)) { | ||
throw new BsonTranspilersRuntimeError(`$${op} cannot be top-level`); | ||
/** | ||
* Emit an "idiomatic" filter or aggregation, meaning use the builders | ||
* instead of a regular object if possible. | ||
* | ||
* @param {ObjectLiteralContext} ctx | ||
* @return {String} | ||
*/ | ||
emitIdiomaticObjectLiteral(ctx) { | ||
ctx.type = this.Types._object; | ||
ctx.indentDepth = this.findIndentDepth(ctx) + 1; | ||
let multiOps = false; | ||
let args = ''; | ||
const properties = this.getKeyValueList(ctx); | ||
if (properties.length) { | ||
args = properties.map((pair) => { | ||
const field = this.getKeyStr(pair); | ||
const value = this.getValue(pair); | ||
if (field.startsWith('$')) { | ||
const op = field.substr(1); | ||
if (this.builderImports[op]) { | ||
this.requiredImports[this.builderImports[op]].push(op); | ||
} | ||
return this.handleFieldOp(value, op, ctx); | ||
if (op === 'regex') { | ||
multiOps = true; | ||
} | ||
if (`handle${op}` in this) { | ||
return this[`handle${op}`](this.getObjectChild(value), op, ctx); | ||
} | ||
if (this.field_opts.indexOf(op) !== -1) { | ||
// Assert that this isn't the top-level object | ||
if (!this.isSubObject(ctx)) { | ||
throw new BsonTranspilersRuntimeError( | ||
`$${op} cannot be top-level` | ||
); | ||
} | ||
return this.handleFieldOp(value, op, ctx); | ||
} | ||
if (this.opts.indexOf(op) !== -1) { | ||
return `${field.substr(1)}(${this.visit(value)})`; | ||
} | ||
} | ||
if (this.opts.indexOf(op) !== -1) { | ||
return `${field.substr(1)}(${this.visit(value)})`; | ||
const valueStr = this.visit(value); | ||
// $-op filters need to rewind a level | ||
const child = this.getObjectChild(value); | ||
if (this.isFilter(child)) { | ||
return valueStr; | ||
} | ||
this.requiredImports[300].push('eq'); | ||
return `eq(${doubleQuoteStringify(field)}, ${valueStr})`; | ||
}); | ||
if (args.length > 1 && !multiOps) { | ||
this.requiredImports[300].push('and'); | ||
return `and(${args.join(', ')})`; | ||
} | ||
const valueStr = this.visit(value); | ||
// $-op filters need to rewind a level | ||
const child = this.getObjectChild(value); | ||
if (this.isFilter(child)) { | ||
return valueStr; | ||
return args[0]; | ||
} | ||
this.requiredImports[10] = true; | ||
return 'new Document()'; | ||
} | ||
/** | ||
* Generates idiomatic java for a $-operator that takes the field name | ||
* as the first argument. i.e. { field: { $op: value } } => op(field, value) | ||
* | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @param {String} op - The $-op | ||
* @param {ObjectLiteralContext} parent - The parent object's ctx | ||
* @returns {String} | ||
*/ | ||
handleFieldOp(ctx, op, parent) { | ||
const parentField = this.getParentKeyStr(parent); | ||
return `${op}(${doubleQuoteStringify(parentField)}, ${this.visit(ctx)})`; | ||
} | ||
/** | ||
* Determines if an object has a subfield that is a $-op. | ||
* | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @return {Boolean} | ||
*/ | ||
isFilter(ctx) { | ||
const properties = this.getKeyValueList(ctx); | ||
for (let i = 0; i < properties.length; i++) { | ||
const pair = properties[i]; | ||
const field = this.getKeyStr(pair); | ||
if (this.field_opts.indexOf(field.substr(1)) !== -1) { | ||
return true; | ||
} | ||
this.requiredImports[300].push('eq'); | ||
return `eq(${doubleQuoteStringify(field)}, ${valueStr})`; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Generates idiomatic java for a $-operator that requires a document that has | ||
* a single subfield whose value gets set to the builder argument. | ||
* { $op: { $subfield: value } } => op(value) | ||
* | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @param {String} op - The name of the $-op. | ||
* @param {String} subfield - The name of the subfield to require. | ||
* @param {Boolean} idiomatic - If the value should be generated as idiomatic. | ||
* @return {String} | ||
*/ | ||
handleSingleSubfield(ctx, op, subfield, idiomatic) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
let value = ''; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (field === subfield) { | ||
this.idiomatic = idiomatic; | ||
value = this.visit(this.getValue(pair)); | ||
this.idiomatic = true; | ||
} else { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unrecognized option to $${op}: ${field}` | ||
); | ||
} | ||
}); | ||
if (args.length > 1 && !multiOps) { | ||
this.requiredImports[300].push('and'); | ||
return `and(${args.join(', ')})`; | ||
if (value === '') { | ||
throw new BsonTranspilersRuntimeError( | ||
`Missing option '${subfield}' in $${op}` | ||
); | ||
} | ||
return args[0]; | ||
return `${op}(${value})`; | ||
} | ||
this.requiredImports[10] = true; | ||
return 'new Document()'; | ||
} | ||
/** | ||
* Generates idiomatic java for a $-operator that takes the field name | ||
* as the first argument. i.e. { field: { $op: value } } => op(field, value) | ||
* | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @param {String} op - The $-op | ||
* @param {ObjectLiteralContext} parent - The parent object's ctx | ||
* @returns {String} | ||
*/ | ||
handleFieldOp(ctx, op, parent) { | ||
const parentField = this.getParentKeyStr(parent); | ||
return `${op}(${doubleQuoteStringify(parentField)}, ${this.visit(ctx)})`; | ||
} | ||
/** | ||
* Generates idiomatic java for a $-operator that has some required options | ||
* and some options that get rolled into an Options object. | ||
* | ||
* { $op: { required: reqVal, optional: optionalVal } => | ||
* op(reqVal, new Options.optional()) | ||
* | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @param {String} op - The name of the $-op. | ||
* @param {Array} reqOpts - The list of required options. | ||
* @param {Array} optionalOpts - The list of optional options. | ||
* @param {Object} transforms - An mapping of original name to transformed | ||
* name. Includes both optional options and the Options object. | ||
* @return {string} | ||
*/ | ||
handleOptionsObject(ctx, op, reqOpts, optionalOpts, transforms) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const fields = {}; | ||
/** | ||
* Determines if an object has a subfield that is a $-op. | ||
* | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @return {Boolean} | ||
*/ | ||
isFilter(ctx) { | ||
const properties = this.getKeyValueList(ctx); | ||
for (let i = 0; i < properties.length; i++) { | ||
const pair = properties[i]; | ||
const field = this.getKeyStr(pair); | ||
if (this.field_opts.indexOf(field.substr(1)) !== -1) { | ||
return true; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if ( | ||
reqOpts.indexOf(field) !== -1 || | ||
optionalOpts.indexOf(field) !== -1 | ||
) { | ||
fields[field] = this.visit(this.getValue(pair)); | ||
} else { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unrecognized option to $${op}: ${field}` | ||
); | ||
} | ||
}); | ||
reqOpts.map((f) => { | ||
if (!(f in fields)) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Missing option '${f}' in $${op}` | ||
); | ||
} | ||
}); | ||
let options = ''; | ||
if (Object.keys(fields).length > reqOpts.length) { | ||
this.requiredImports[306].push(transforms[op]); | ||
options = `, new ${transforms[op]}()${Object.keys(fields) | ||
.filter((f) => { | ||
return optionalOpts.indexOf(f) !== -1; | ||
}) | ||
.map((k) => { | ||
if (transforms !== undefined && k in transforms) { | ||
return transforms[k](fields[k]); | ||
} | ||
return `.${k}(${fields[k]})`; | ||
}) | ||
.join('')}`; | ||
} | ||
return `${op}(${reqOpts | ||
.map((f) => { | ||
return fields[f]; | ||
}) | ||
.join(', ')}${options})`; | ||
} | ||
return false; | ||
} | ||
/** | ||
* Generates idiomatic java for a $-operator that requires a document that has | ||
* a single subfield whose value gets set to the builder argument. | ||
* { $op: { $subfield: value } } => op(value) | ||
* | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @param {String} op - The name of the $-op. | ||
* @param {String} subfield - The name of the subfield to require. | ||
* @param {Boolean} idiomatic - If the value should be generated as idiomatic. | ||
* @return {String} | ||
*/ | ||
handleSingleSubfield(ctx, op, subfield, idiomatic) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
let value = ''; | ||
/** | ||
* Generates idiomatic java for a $-operator that has some required options | ||
* and then multiple arbitrary fields. | ||
* | ||
* { $op: { required: reqVal, opt1: v1, opt2: v2...} } => op(reqVal, v1, v2...) | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @param {String} op - The name of the $-op. | ||
* @param {Array} reqOpts - The list of required options. | ||
* @param {Function} transform - A function that takes in the option name and | ||
* the value, then returns a string with the correct formatting. | ||
* @param {Boolean} idiomatic - If the value should be generated as idiomatic. | ||
* @return {string} | ||
*/ | ||
handleMultipleSubfields(ctx, op, reqOpts, transform, idiomatic) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const req = []; | ||
const fields = {}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (field === subfield) { | ||
this.idiomatic = idiomatic; | ||
value = this.visit(this.getValue(pair)); | ||
this.idiomatic = true; | ||
} else { | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (reqOpts.indexOf(field) !== -1) { | ||
req.push(this.visit(this.getValue(pair))); | ||
} else { | ||
this.idiomatic = idiomatic; | ||
fields[field] = this.visit(this.getValue(pair)); | ||
this.idiomatic = true; | ||
} | ||
}); | ||
if (req.length !== reqOpts.length) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unrecognized option to $${op}: ${field}` | ||
`Required option missing from ${op}` | ||
); | ||
} | ||
}); | ||
if (value === '') { | ||
throw new BsonTranspilersRuntimeError( | ||
`Missing option '${subfield}' in $${op}` | ||
const args = req.concat( | ||
Object.keys(fields).map((f) => { | ||
return transform(f, fields[f]); | ||
}) | ||
); | ||
return `${op}(${args.join(', ')})`; | ||
} | ||
return `${op}(${value})`; | ||
} | ||
/** | ||
* Generates idiomatic java for a $-operator that has some required options | ||
* and some options that get rolled into an Options object. | ||
* | ||
* { $op: { required: reqVal, optional: optionalVal } => | ||
* op(reqVal, new Options.optional()) | ||
* | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @param {String} op - The name of the $-op. | ||
* @param {Array} reqOpts - The list of required options. | ||
* @param {Array} optionalOpts - The list of optional options. | ||
* @param {Object} transforms - An mapping of original name to transformed | ||
* name. Includes both optional options and the Options object. | ||
* @return {string} | ||
*/ | ||
handleOptionsObject(ctx, op, reqOpts, optionalOpts, transforms) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const fields = {}; | ||
/** | ||
* Method for handling an idiomatic $project. | ||
* ctx must be a non-empty document. | ||
* | ||
* @param {ObjectLiteralContext} ctx | ||
* @return {String} | ||
*/ | ||
handleproject(ctx) { | ||
// Eventual todo: slice and elemMatch | ||
const properties = this.assertIsNonemptyObject(ctx, 'project'); | ||
const fields = {}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (reqOpts.indexOf(field) !== -1 || optionalOpts.indexOf(field) !== -1) { | ||
fields[field] = this.visit(this.getValue(pair)); | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
const original = this.getValue(pair).getText(); | ||
if (original.toLowerCase() === 'true' || original === '1') { | ||
if (field !== '_id') { | ||
// Skip because ID is included by default | ||
fields.includes = !fields.includes | ||
? `include(${doubleQuoteStringify(field)}` | ||
: `${fields.includes}, ${doubleQuoteStringify(field)}`; | ||
this.requiredImports[303].push('include'); | ||
} | ||
} else if (original.toLowerCase() === 'false' || original === '0') { | ||
if (field !== '_id') { | ||
fields.excludes = !fields.excludes | ||
? `exclude(${doubleQuoteStringify(field)}` | ||
: `${fields.excludes}, ${doubleQuoteStringify(field)}`; | ||
this.requiredImports[303].push('exclude'); | ||
} else { | ||
fields.excludeId = 'excludeId()'; | ||
this.requiredImports[303].push('excludeId'); | ||
} | ||
} else { | ||
const value = this.visit(this.getValue(pair)); | ||
fields.computed = !fields.computed | ||
? `computed(${doubleQuoteStringify(field)}, ${value})` | ||
: `${fields.computed}, computed(${doubleQuoteStringify( | ||
field | ||
)}, ${value})`; | ||
this.requiredImports[303].push('computed'); | ||
} | ||
}); | ||
if (fields.includes) { | ||
fields.includes = `${fields.includes})`; | ||
} | ||
if (fields.excludes) { | ||
fields.excludes = `${fields.excludes})`; | ||
} | ||
const elements = Object.values(fields); | ||
let projectStr; | ||
if (elements.length === 1) { | ||
projectStr = elements[0]; | ||
} else { | ||
projectStr = `fields(${elements.join(', ')})`; | ||
this.requiredImports[303].push('fields'); | ||
} | ||
return `project(${projectStr})`; | ||
} | ||
/** | ||
* Method for handling an idiomatic $not. | ||
* | ||
* { field: { $not: {$op: value} } } => not(op(field, value)) | ||
* | ||
* ctx must be a non-empty document. | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* $not field | ||
* @param {String} op - the operation, which in this case is "not" | ||
* @param {ObjectLiteralExpressionContext} parent - ctx of parent document. | ||
* @return {String} | ||
*/ | ||
handlenot(ctx, op, parent) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const val = this.getValue(properties[0]); | ||
const innerop = this.getKeyStr(properties[0]).substr(1); | ||
this.requiredImports[300].push(innerop); | ||
const inner = this.handleFieldOp(val, innerop, parent); | ||
return `${op}(${inner})`; | ||
} | ||
/** | ||
* Method for handling an idiomatic $mod. | ||
* | ||
* { field: { $mod: [arr1, arr2] } } => mod(field, arr1, arr2) | ||
* | ||
* ctx must be an array of length 2. | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* $mod field | ||
* @param {String} op - the operation, which in this case is "mod" | ||
* @param {ObjectLiteralExpressionContext} parent - ctx of parent document. | ||
* @return {String} | ||
*/ | ||
handlemod(ctx, op, parent) { | ||
const list = this.getList(ctx); | ||
if (list.length !== 2) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unrecognized option to $${op}: ${field}` | ||
'$mod requires an array of 2-elements' | ||
); | ||
} | ||
}); | ||
reqOpts.map((f) => { | ||
if (!(f in fields)) { | ||
throw new BsonTranspilersRuntimeError(`Missing option '${f}' in $${op}`); | ||
} | ||
}); | ||
const parentField = this.getParentKeyStr(parent); | ||
const inner = list.map((f) => { | ||
return this.visit(f); | ||
}); | ||
return `${op}(${doubleQuoteStringify(parentField)}, ${inner.join(', ')})`; | ||
} | ||
let options = ''; | ||
if (Object.keys(fields).length > reqOpts.length) { | ||
this.requiredImports[306].push(transforms[op]); | ||
/** | ||
* Method for handling an idiomatic $regex. | ||
* | ||
* { field: { $regex: regexstr, $options?: optsstr } } => | ||
* regex(regexstr, optsstr?) | ||
* | ||
* ctx must be a string | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* $regex field | ||
* @param {String} op - the operation, which in this case is "regex" | ||
* @param {ObjectLiteralExpressionContext} parent - ctx of parent document. | ||
* @return {String} | ||
*/ | ||
handleregex(ctx, op, parent) { | ||
const parentField = this.getParentKeyStr(parent); | ||
const regex = { r: '', o: '' }; | ||
options = `, new ${transforms[op]}()${Object.keys(fields).filter((f) => { | ||
return optionalOpts.indexOf(f) !== -1; | ||
}).map((k) => { | ||
if (transforms !== undefined && k in transforms) { | ||
return transforms[k](fields[k]); | ||
const properties = this.getKeyValueList(parent); | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (field === '$regex') { | ||
regex.r = this.visit(this.getValue(pair)); | ||
} | ||
return `.${k}(${fields[k]})`; | ||
}).join('')}`; | ||
if (field === '$options') { | ||
regex.o = `, ${this.visit(this.getValue(pair))}`; | ||
} | ||
}); | ||
return `${op}(${doubleQuoteStringify(parentField)}, ${regex.r}${ | ||
regex.o | ||
})`; | ||
} | ||
handleoptions() { | ||
return ''; | ||
} | ||
return `${op}(${reqOpts.map((f) => { | ||
return fields[f]; | ||
}).join(', ')}${options})`; | ||
} | ||
/** | ||
* Generates idiomatic java for a $-operator that has some required options | ||
* and then multiple arbitrary fields. | ||
* | ||
* { $op: { required: reqVal, opt1: v1, opt2: v2...} } => op(reqVal, v1, v2...) | ||
* @param {ObjectLiteralContext} ctx - The field of the $-op | ||
* @param {String} op - The name of the $-op. | ||
* @param {Array} reqOpts - The list of required options. | ||
* @param {Function} transform - A function that takes in the option name and | ||
* the value, then returns a string with the correct formatting. | ||
* @param {Boolean} idiomatic - If the value should be generated as idiomatic. | ||
* @return {string} | ||
*/ | ||
handleMultipleSubfields(ctx, op, reqOpts, transform, idiomatic) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const req = []; | ||
const fields = {}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (reqOpts.indexOf(field) !== -1) { | ||
req.push(this.visit(this.getValue(pair))); | ||
/** | ||
* Method for handling an idiomatic $where. | ||
* | ||
* { $where: <function def> } => where(<function as string>) | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* $where field | ||
* @return {String} | ||
*/ | ||
handlewhere(ctx) { | ||
let text; | ||
if (!('getParent' in ctx)) { | ||
text = ctx.getText(); | ||
} else { | ||
this.idiomatic = idiomatic; | ||
fields[field] = this.visit(this.getValue(pair)); | ||
this.idiomatic = true; | ||
text = ctx.getParent().getText(); | ||
} | ||
}); | ||
if (req.length !== reqOpts.length) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Required option missing from ${op}` | ||
); | ||
return `where(${doubleQuoteStringify(text)})`; | ||
} | ||
const args = req.concat(Object.keys(fields).map((f) => { | ||
return transform(f, fields[f]); | ||
})); | ||
return `${op}(${args.join(', ')})`; | ||
} | ||
/** | ||
* Method for handling an idiomatic $sort. | ||
* | ||
* { $sort: { f1: 1, f2: -1, f3: { $meta: 'textScore' } } } => | ||
* sort(ascending(f1), descending(f2), metaTextScore(f3)) | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* @return {string} | ||
*/ | ||
handlesort(ctx) { | ||
const properties = this.assertIsNonemptyObject(ctx, 'sort'); | ||
const fields = []; | ||
/** | ||
* Method for handling an idiomatic $project. | ||
* ctx must be a non-empty document. | ||
* | ||
* @param {ObjectLiteralContext} ctx | ||
* @return {String} | ||
*/ | ||
handleproject(ctx) { | ||
// Eventual todo: slice and elemMatch | ||
const properties = this.assertIsNonemptyObject(ctx, 'project'); | ||
const fields = {}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
const original = this.getValue(pair).getText(); | ||
if (original.toLowerCase() === 'true' || original === '1') { | ||
if (field !== '_id') { | ||
// Skip because ID is included by default | ||
fields.includes = !fields.includes ? | ||
`include(${doubleQuoteStringify(field)}` : | ||
`${fields.includes}, ${doubleQuoteStringify(field)}`; | ||
this.requiredImports[303].push('include'); | ||
} | ||
} else if (original.toLowerCase() === 'false' || original === '0') { | ||
if (field !== '_id') { | ||
fields.excludes = !fields.excludes ? | ||
`exclude(${doubleQuoteStringify(field)}` : | ||
`${fields.excludes}, ${doubleQuoteStringify(field)}`; | ||
this.requiredImports[303].push('exclude'); | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
const original = this.getValue(pair).getText(); | ||
if (original === '1') { | ||
fields.push(`ascending(${doubleQuoteStringify(field)})`); | ||
this.requiredImports[304].push('ascending'); | ||
} else if (original === '-1') { | ||
fields.push(`descending(${doubleQuoteStringify(field)})`); | ||
this.requiredImports[304].push('descending'); | ||
} else if ( | ||
original.match( | ||
new RegExp(/{(?:'|")?\$meta(?:'|")?:(?:'|")textScore*(?:'|")}/) | ||
) | ||
) { | ||
fields.push(`metaTextScore(${doubleQuoteStringify(field)})`); | ||
this.requiredImports[304].push('metaTextScore'); | ||
} else { | ||
fields.excludeId = 'excludeId()'; | ||
this.requiredImports[303].push('excludeId'); | ||
throw new BsonTranspilersRuntimeError( | ||
'$sort key ordering must be specified using a number or ' + | ||
"{$meta: 'textScore'}" | ||
); | ||
} | ||
}); | ||
let sortStr; | ||
if (fields.length > 1) { | ||
sortStr = `orderBy(${fields.join(', ')})`; | ||
this.requiredImports[304].push('orderBy'); | ||
} else { | ||
const value = this.visit(this.getValue(pair)); | ||
fields.computed = !fields.computed ? | ||
`computed(${doubleQuoteStringify(field)}, ${value})` : | ||
`${fields.computed}, computed(${doubleQuoteStringify(field)}, ${value})`; | ||
this.requiredImports[303].push('computed'); | ||
sortStr = fields[0]; | ||
} | ||
}); | ||
if (fields.includes) { | ||
fields.includes = `${fields.includes})`; | ||
return `sort(${sortStr})`; | ||
} | ||
if (fields.excludes) { | ||
fields.excludes = `${fields.excludes})`; | ||
} | ||
const elements = Object.values(fields); | ||
let projectStr; | ||
if (elements.length === 1) { | ||
projectStr = elements[0]; | ||
} else { | ||
projectStr = `fields(${elements.join(', ')})`; | ||
this.requiredImports[303].push('fields'); | ||
} | ||
return `project(${projectStr})`; | ||
} | ||
/** | ||
* Method for handling an idiomatic $not. | ||
* | ||
* { field: { $not: {$op: value} } } => not(op(field, value)) | ||
* | ||
* ctx must be a non-empty document. | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* $not field | ||
* @param {String} op - the operation, which in this case is "not" | ||
* @param {ObjectLiteralExpressionContext} parent - ctx of parent document. | ||
* @return {String} | ||
*/ | ||
handlenot(ctx, op, parent) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const val = this.getValue(properties[0]); | ||
const innerop = this.getKeyStr(properties[0]).substr(1); | ||
this.requiredImports[300].push(innerop); | ||
const inner = this.handleFieldOp(val, innerop, parent); | ||
return `${op}(${inner})`; | ||
} | ||
/** | ||
* Method for handling an idiomatic $mod. | ||
* | ||
* { field: { $mod: [arr1, arr2] } } => mod(field, arr1, arr2) | ||
* | ||
* ctx must be an array of length 2. | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* $mod field | ||
* @param {String} op - the operation, which in this case is "mod" | ||
* @param {ObjectLiteralExpressionContext} parent - ctx of parent document. | ||
* @return {String} | ||
*/ | ||
handlemod(ctx, op, parent) { | ||
const list = this.getList(ctx); | ||
if (list.length !== 2) { | ||
throw new BsonTranspilersRuntimeError( | ||
'$mod requires an array of 2-elements' | ||
handlegeoWithin(ctx, op, parent) { | ||
this.requiredImports[300].splice( | ||
this.requiredImports[300].indexOf('geoWithin'), | ||
1 | ||
); | ||
} | ||
const parentField = this.getParentKeyStr(parent); | ||
const inner = list.map((f) => { | ||
return this.visit(f); | ||
}); | ||
return `${op}(${doubleQuoteStringify(parentField)}, ${inner.join(', ')})`; | ||
} | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const parentField = doubleQuoteStringify(this.getParentKeyStr(parent)); | ||
const fields = {}; | ||
/** | ||
* Method for handling an idiomatic $regex. | ||
* | ||
* { field: { $regex: regexstr, $options?: optsstr } } => | ||
* regex(regexstr, optsstr?) | ||
* | ||
* ctx must be a string | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* $regex field | ||
* @param {String} op - the operation, which in this case is "regex" | ||
* @param {ObjectLiteralExpressionContext} parent - ctx of parent document. | ||
* @return {String} | ||
*/ | ||
handleregex(ctx, op, parent) { | ||
const parentField = this.getParentKeyStr(parent); | ||
const regex = {r: '', o: ''}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
fields[field] = this.getValue(pair); | ||
}); | ||
const properties = this.getKeyValueList(parent); | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (field === '$regex') { | ||
regex.r = this.visit(this.getValue(pair)); | ||
if (Object.keys(fields).length !== 1) { | ||
throw new BsonTranspilersRuntimeError( | ||
'$geoWithin takes an object with only 1 field' | ||
); | ||
} | ||
if (field === '$options') { | ||
regex.o = `, ${this.visit(this.getValue(pair))}`; | ||
const key = Object.keys(fields)[0]; | ||
switch (key) { | ||
case '$geometry': { | ||
this.requiredImports[300].push('geoWithin'); | ||
return `geoWithin(${parentField}, ${this.handlegeometry( | ||
fields[key] | ||
)})`; | ||
} | ||
case '$box': { | ||
this.requiredImports[300].push('geoWithinBox'); | ||
return `geoWithinBox(${parentField}, ${this.combineCoordinates( | ||
fields[key], | ||
2, | ||
'', | ||
true, | ||
(p) => { | ||
return this.combineCoordinates(p, 2, '', true, (p2) => { | ||
return this.visit(p2); | ||
}); | ||
} | ||
)})`; | ||
} | ||
case '$polygon': { | ||
this.requiredImports[300].push('geoWithinPolygon'); | ||
return `geoWithinPolygon(${parentField}, ${this.visit(fields[key])})`; | ||
} | ||
case '$centerSphere': | ||
case '$center': { | ||
const array = this.assertIsNonemptyArray(fields[key]); | ||
if (array.length !== 2) { | ||
throw new BsonTranspilersRuntimeError( | ||
`${key} takes array of length 2` | ||
); | ||
} | ||
const coordinates = this.combineCoordinates( | ||
array[0], | ||
2, | ||
'', | ||
true, | ||
(p) => { | ||
return this.visit(p); | ||
} | ||
); | ||
const geoop = `geoWithin${key[1].toUpperCase()}${key.substr(2)}`; | ||
this.requiredImports[300].push(geoop); | ||
return `${geoop}(${parentField}, ${coordinates}, ${this.visit( | ||
array[1] | ||
)})`; | ||
} | ||
default: { | ||
throw new BsonTranspilersRuntimeError( | ||
`unrecognized option ${key} to $geoWithin` | ||
); | ||
} | ||
} | ||
}); | ||
return `${op}(${doubleQuoteStringify(parentField)}, ${regex.r}${regex.o})`; | ||
} | ||
handleoptions() { | ||
return ''; | ||
} | ||
/** | ||
* Method for handling an idiomatic $where. | ||
* | ||
* { $where: <function def> } => where(<function as string>) | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* $where field | ||
* @return {String} | ||
*/ | ||
handlewhere(ctx) { | ||
let text; | ||
if (!('getParent' in ctx)) { | ||
text = ctx.getText(); | ||
} else { | ||
text = ctx.getParent().getText(); | ||
} | ||
return `where(${doubleQuoteStringify(text)})`; | ||
} | ||
/** | ||
* Method for handling an idiomatic $sort. | ||
* | ||
* { $sort: { f1: 1, f2: -1, f3: { $meta: 'textScore' } } } => | ||
* sort(ascending(f1), descending(f2), metaTextScore(f3)) | ||
* | ||
* @param {ObjectLiteralExpressionContext} ctx - the ctx of the value of the | ||
* @return {string} | ||
*/ | ||
handlesort(ctx) { | ||
const properties = this.assertIsNonemptyObject(ctx, 'sort'); | ||
const fields = []; | ||
handlenear(ctx, op, parent) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const parentField = doubleQuoteStringify(this.getParentKeyStr(parent)); | ||
const fields = {}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
const original = this.getValue(pair).getText(); | ||
if (original === '1') { | ||
fields.push(`ascending(${doubleQuoteStringify(field)})`); | ||
this.requiredImports[304].push('ascending'); | ||
} else if (original === '-1') { | ||
fields.push(`descending(${doubleQuoteStringify(field)})`); | ||
this.requiredImports[304].push('descending'); | ||
} else if (original.match( | ||
new RegExp(/{(?:'|")?\$meta(?:'|")?:(?:'|")textScore*(?:'|")}/) | ||
)) { | ||
fields.push(`metaTextScore(${doubleQuoteStringify(field)})`); | ||
this.requiredImports[304].push('metaTextScore'); | ||
} else { | ||
throw new BsonTranspilersRuntimeError( | ||
'$sort key ordering must be specified using a number or ' + | ||
'{$meta: \'textScore\'}' | ||
); | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
fields[field] = this.getValue(pair); | ||
}); | ||
['$geometry', '$minDistance', '$maxDistance'].map((k) => { | ||
if (!(k in fields)) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Missing required field ${k} in $${op}` | ||
); | ||
} | ||
}); | ||
if (Object.keys(fields).length !== 3) { | ||
throw new BsonTranspilersRuntimeError(`Too many fields to $${op}`); | ||
} | ||
}); | ||
let sortStr; | ||
if (fields.length > 1) { | ||
sortStr = `orderBy(${fields.join(', ')})`; | ||
this.requiredImports[304].push('orderBy'); | ||
} else { | ||
sortStr = fields[0]; | ||
return `${op}(${parentField}, ${this.handlegeometry( | ||
fields.$geometry | ||
)}, ${this.visit(fields.$maxDistance)}, ${this.visit( | ||
fields.$minDistance | ||
)})`; | ||
} | ||
return `sort(${sortStr})`; | ||
} | ||
handlegeoWithin(ctx, op, parent) { | ||
this.requiredImports[300].splice( | ||
this.requiredImports[300].indexOf('geoWithin'), 1 | ||
); | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const parentField = doubleQuoteStringify( | ||
this.getParentKeyStr(parent) | ||
); | ||
const fields = {}; | ||
handlenearSphere(ctx, op, parent) { | ||
return this.handlenear(ctx, op, parent); | ||
} | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
fields[field] = this.getValue(pair); | ||
}); | ||
generateposition(ctx) { | ||
const coordinates = this.assertIsNonemptyArray(ctx, 'geometry'); | ||
if (coordinates.length !== 2) { | ||
throw new BsonTranspilersRuntimeError('Position must be 2 coordinates'); | ||
} | ||
this.requiredImports[305].push('Position'); | ||
return `new Position(${this.visit(coordinates[0])}, ${this.visit( | ||
coordinates[1] | ||
)})`; | ||
} | ||
if (Object.keys(fields).length !== 1) { | ||
throw new BsonTranspilersRuntimeError( | ||
'$geoWithin takes an object with only 1 field' | ||
); | ||
assertIsNonemptyArray(ctx, op) { | ||
const array = this.getArray(ctx); | ||
if (!array || this.getList(array).length === 0) { | ||
throw new BsonTranspilersRuntimeError( | ||
`$${op} requires a non-empty array` | ||
); | ||
} | ||
return this.getList(array); | ||
} | ||
const key = Object.keys(fields)[0]; | ||
switch (key) { | ||
case ('$geometry'): { | ||
this.requiredImports[300].push('geoWithin'); | ||
return `geoWithin(${parentField}, ${this.handlegeometry(fields[key])})`; | ||
assertIsNonemptyObject(ctx, op) { | ||
if (this.getObject(ctx)) { | ||
ctx = this.getObject(ctx); | ||
} | ||
case ('$box'): { | ||
this.requiredImports[300].push('geoWithinBox'); | ||
return `geoWithinBox(${parentField}, ${ | ||
this.combineCoordinates(fields[key], 2, '', true, (p) => { | ||
return this.combineCoordinates(p, 2, '', true, (p2) => { | ||
return this.visit(p2); | ||
}); | ||
})})`; | ||
} | ||
case ('$polygon'): { | ||
this.requiredImports[300].push('geoWithinPolygon'); | ||
return `geoWithinPolygon(${parentField}, ${this.visit(fields[key])})`; | ||
} | ||
case ('$centerSphere'): | ||
case ('$center'): { | ||
const array = this.assertIsNonemptyArray(fields[key]); | ||
if (array.length !== 2) { | ||
throw new BsonTranspilersRuntimeError(`${key} takes array of length 2`); | ||
} | ||
const coordinates = this.combineCoordinates( | ||
array[0], 2, '', true, (p) => { return this.visit(p); } | ||
); | ||
const geoop = `geoWithin${key[1].toUpperCase()}${ | ||
key.substr(2) | ||
}`; | ||
this.requiredImports[300].push(geoop); | ||
return `${geoop}(${parentField}, ${coordinates}, ${this.visit(array[1])})`; | ||
} | ||
default: { | ||
const kv = this.getKeyValueList(ctx); | ||
if (kv.length === 0) { | ||
throw new BsonTranspilersRuntimeError( | ||
`unrecognized option ${key} to $geoWithin` | ||
`$${op} requires a non-empty document` | ||
); | ||
} | ||
return kv; | ||
} | ||
} | ||
handlenear(ctx, op, parent) { | ||
const properties = this.assertIsNonemptyObject(ctx, op); | ||
const parentField = doubleQuoteStringify( | ||
this.getParentKeyStr(parent) | ||
); | ||
const fields = {}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
fields[field] = this.getValue(pair); | ||
}); | ||
['$geometry', '$minDistance', '$maxDistance'].map((k) => { | ||
if (!(k in fields)) { | ||
combineCoordinates(ctx, length, className, noArray, innerFunc) { | ||
if (!noArray) { | ||
this.requiredImports[9] = true; | ||
} | ||
const points = this.assertIsNonemptyArray(ctx, 'geometry'); | ||
if (points.length < length) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Missing required field ${k} in $${op}` | ||
`${ | ||
className ? className : '$geometry inner array' | ||
} must have at least ${length} elements (has ${points.length})` | ||
); | ||
} | ||
}); | ||
if (Object.keys(fields).length !== 3) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Too many fields to $${op}` | ||
); | ||
let pointstr = points | ||
.map((p) => { | ||
if (!innerFunc) { | ||
return this.generateposition(p); | ||
} | ||
return innerFunc(p); | ||
}) | ||
.join(', '); | ||
pointstr = noArray ? pointstr : `Arrays.asList(${pointstr})`; | ||
if (!className) { | ||
return pointstr; | ||
} | ||
this.requiredImports[305].push(className); | ||
return `new ${className}(${pointstr})`; | ||
} | ||
return `${op}(${parentField}, ${ | ||
this.handlegeometry(fields.$geometry) | ||
}, ${this.visit(fields.$maxDistance)}, ${ | ||
this.visit(fields.$minDistance) | ||
})`; | ||
} | ||
handlenearSphere(ctx, op, parent) { | ||
return this.handlenear(ctx, op, parent); | ||
} | ||
generatepoint(ctx) { | ||
return `new Point(${this.generateposition(ctx)})`; | ||
} | ||
generateposition(ctx) { | ||
const coordinates = this.assertIsNonemptyArray(ctx, 'geometry'); | ||
if (coordinates.length !== 2) { | ||
throw new BsonTranspilersRuntimeError( | ||
'Position must be 2 coordinates' | ||
); | ||
generatemultipoint(ctx) { | ||
return this.combineCoordinates(ctx, 1, 'MultiPoint'); | ||
} | ||
this.requiredImports[305].push('Position'); | ||
return `new Position(${ | ||
this.visit(coordinates[0])}, ${this.visit(coordinates[1]) | ||
})`; | ||
} | ||
assertIsNonemptyArray(ctx, op) { | ||
const array = this.getArray(ctx); | ||
if (!array || this.getList(array).length === 0) { | ||
throw new BsonTranspilersRuntimeError( | ||
`$${op} requires a non-empty array` | ||
); | ||
generatelinestring(ctx) { | ||
return this.combineCoordinates(ctx, 2, 'LineString'); | ||
} | ||
return this.getList(array); | ||
} | ||
assertIsNonemptyObject(ctx, op) { | ||
if (this.getObject(ctx)) { | ||
ctx = this.getObject(ctx); | ||
generatemultilinestring(ctx) { | ||
return this.combineCoordinates(ctx, 1, 'MultiLineString', false, (p) => { | ||
return this.combineCoordinates(p, 2); | ||
}); | ||
} | ||
const kv = this.getKeyValueList(ctx); | ||
if (kv.length === 0) { | ||
throw new BsonTranspilersRuntimeError( | ||
`$${op} requires a non-empty document` | ||
); | ||
} | ||
return kv; | ||
} | ||
combineCoordinates(ctx, length, className, noArray, innerFunc) { | ||
if (!noArray) { | ||
this.requiredImports[9] = true; | ||
} | ||
const points = this.assertIsNonemptyArray(ctx, 'geometry'); | ||
if (points.length < length) { | ||
throw new BsonTranspilersRuntimeError( | ||
`${ | ||
className ? className : '$geometry inner array' | ||
} must have at least ${length} elements (has ${points.length})` | ||
generatepolygon(ctx) { | ||
const polyCoords = this.combineCoordinates( | ||
ctx, | ||
1, | ||
'PolygonCoordinates', | ||
true, | ||
(p) => { | ||
return this.combineCoordinates(p, 4); | ||
} | ||
); | ||
return `new Polygon(${polyCoords})`; | ||
} | ||
let pointstr = points.map((p) => { | ||
if (!innerFunc) { | ||
return this.generateposition(p); | ||
} | ||
return innerFunc(p); | ||
}).join(', '); | ||
pointstr = noArray ? pointstr : `Arrays.asList(${pointstr})`; | ||
if (!className) { | ||
return pointstr; | ||
} | ||
this.requiredImports[305].push(className); | ||
return `new ${className}(${pointstr})`; | ||
} | ||
generatepoint(ctx) { | ||
return `new Point(${this.generateposition(ctx)})`; | ||
} | ||
generatemultipoint(ctx) { | ||
return this.combineCoordinates(ctx, 1, 'MultiPoint'); | ||
} | ||
generatelinestring(ctx) { | ||
return this.combineCoordinates(ctx, 2, 'LineString'); | ||
} | ||
generatemultilinestring(ctx) { | ||
return this.combineCoordinates( | ||
ctx, | ||
1, | ||
'MultiLineString', | ||
false, | ||
(p) => { return this.combineCoordinates(p, 2); } | ||
); | ||
} | ||
generatepolygon(ctx) { | ||
const polyCoords = this.combineCoordinates( | ||
ctx, | ||
1, | ||
'PolygonCoordinates', | ||
true, | ||
(p) => { return this.combineCoordinates(p, 4); } | ||
); | ||
return `new Polygon(${polyCoords})`; | ||
} | ||
generatemultipolygon(ctx) { | ||
return this.combineCoordinates( | ||
ctx, | ||
1, | ||
'MultiPolygon', | ||
false, | ||
(p) => { | ||
generatemultipolygon(ctx) { | ||
return this.combineCoordinates(ctx, 1, 'MultiPolygon', false, (p) => { | ||
return this.combineCoordinates( | ||
@@ -688,159 +800,186 @@ p, | ||
true, | ||
(p2) => { return this.combineCoordinates(p2, 4); } | ||
(p2) => { | ||
return this.combineCoordinates(p2, 4); | ||
} | ||
); | ||
} | ||
); | ||
} | ||
}); | ||
} | ||
generategeometrycollection(ctx) { | ||
const geometries = this.assertIsNonemptyArray(ctx, 'geometry'); | ||
return `new GeometryCollection(Arrays.asList(${ | ||
geometries.map((g) => { | ||
const obj = this.getObject(g); | ||
if (!obj) { | ||
generategeometrycollection(ctx) { | ||
const geometries = this.assertIsNonemptyArray(ctx, 'geometry'); | ||
return `new GeometryCollection(Arrays.asList(${geometries | ||
.map((g) => { | ||
const obj = this.getObject(g); | ||
if (!obj) { | ||
throw new BsonTranspilersRuntimeError( | ||
'$GeometryCollection requires objects' | ||
); | ||
} | ||
return this.handlegeometry(obj); | ||
}) | ||
.join(', ')}))`; | ||
} | ||
handlegeometry(ctx) { | ||
const properties = this.assertIsNonemptyObject(ctx, 'geometry'); | ||
const fields = {}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (field === 'type') { | ||
fields.type = removeQuotes(this.visit(this.getValue(pair))); | ||
} else if (field === 'coordinates') { | ||
fields.coordinates = this.getValue(pair); | ||
} else if (field === 'crs') { | ||
throw new BsonTranspilersUnimplementedError( | ||
'Coordinate reference systems not currently supported' | ||
); | ||
} else { | ||
throw new BsonTranspilersRuntimeError( | ||
'$GeometryCollection requires objects' | ||
`Unrecognized option to $geometry: ${field}` | ||
); | ||
} | ||
return this.handlegeometry(obj); | ||
}).join(', ') | ||
}))`; | ||
} | ||
handlegeometry(ctx) { | ||
const properties = this.assertIsNonemptyObject(ctx, 'geometry'); | ||
const fields = {}; | ||
properties.forEach((pair) => { | ||
const field = this.getKeyStr(pair); | ||
if (field === 'type') { | ||
fields.type = removeQuotes(this.visit(this.getValue(pair))); | ||
} else if (field === 'coordinates') { | ||
fields.coordinates = this.getValue(pair); | ||
} else if (field === 'crs') { | ||
throw new BsonTranspilersUnimplementedError( | ||
'Coordinate reference systems not currently supported' | ||
); | ||
} else { | ||
}); | ||
if (!fields.type || !fields.coordinates) { | ||
throw new BsonTranspilersRuntimeError('Missing option to $geometry'); | ||
} | ||
if ( | ||
!this.getArray(fields.coordinates) || | ||
this.getList(this.getArray(fields.coordinates)).length === 0 | ||
) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unrecognized option to $geometry: ${field}` | ||
'Invalid coordinates option for $geometry' | ||
); | ||
} | ||
}); | ||
if (!fields.type || !fields.coordinates) { | ||
if (`generate${fields.type.toLowerCase()}` in this) { | ||
this.requiredImports[305].push(fields.type); | ||
return this[`generate${fields.type.toLowerCase()}`](fields.coordinates); | ||
} | ||
throw new BsonTranspilersRuntimeError( | ||
'Missing option to $geometry' | ||
`Unrecognized GeoJSON type "${fields.type}"` | ||
); | ||
} | ||
if (!this.getArray(fields.coordinates) || | ||
this.getList(this.getArray(fields.coordinates)).length === 0) { | ||
throw new BsonTranspilersRuntimeError( | ||
'Invalid coordinates option for $geometry' | ||
handlesample(ctx) { | ||
return this.handleSingleSubfield(ctx, 'sample', 'size', true); | ||
} | ||
handlereplaceRoot(ctx) { | ||
return this.handleSingleSubfield(ctx, 'replaceRoot', 'newRoot', false); | ||
} | ||
handlegraphLookup(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'graphLookup', | ||
['from', 'startWith', 'connectFromField', 'connectToField', 'as'], | ||
['maxDepth', 'depthField', 'restrictSearchWithMatch'], | ||
{ graphLookup: 'GraphLookupOptions' } | ||
); | ||
} | ||
if (`generate${fields.type.toLowerCase()}` in this) { | ||
this.requiredImports[305].push(fields.type); | ||
return this[`generate${fields.type.toLowerCase()}`](fields.coordinates); | ||
handlelookup(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'lookup', | ||
['from', 'localField', 'foreignField', 'as'], | ||
[], | ||
{ lookup: 'LookupOptions' } | ||
); | ||
} | ||
throw new BsonTranspilersRuntimeError( | ||
`Unrecognized GeoJSON type "${fields.type}"` | ||
); | ||
} | ||
handlesample(ctx) { | ||
return this.handleSingleSubfield(ctx, 'sample', 'size', true); | ||
} | ||
handlereplaceRoot(ctx) { | ||
return this.handleSingleSubfield(ctx, 'replaceRoot', 'newRoot', false); | ||
} | ||
handlegraphLookup(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'graphLookup', | ||
['from', 'startWith', 'connectFromField', 'connectToField', 'as'], | ||
['maxDepth', 'depthField', 'restrictSearchWithMatch'], | ||
{ graphLookup: 'GraphLookupOptions' } | ||
); | ||
} | ||
handlelookup(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'lookup', | ||
['from', 'localField', 'foreignField', 'as'], | ||
[], | ||
{ lookup: 'LookupOptions' } | ||
); | ||
} | ||
handlebucket(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'bucket', | ||
['groupBy', 'boundaries'], | ||
['default', 'output'], | ||
{ | ||
bucket: 'BucketOptions', | ||
default: (k) => { return `.defaultBucket(${k})`; } | ||
} | ||
); | ||
} | ||
handlebucketAuto(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'bucketAuto', | ||
['groupBy', 'buckets'], | ||
['granularity', 'output'], | ||
{ | ||
bucketAuto: 'BucketAutoOptions', | ||
granularity: (k) => { | ||
return `.granularity(BucketGranularity.fromString(${k}))`; | ||
handlebucket(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'bucket', | ||
['groupBy', 'boundaries'], | ||
['default', 'output'], | ||
{ | ||
bucket: 'BucketOptions', | ||
default: (k) => { | ||
return `.defaultBucket(${k})`; | ||
}, | ||
} | ||
); | ||
} | ||
handlebucketAuto(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'bucketAuto', | ||
['groupBy', 'buckets'], | ||
['granularity', 'output'], | ||
{ | ||
bucketAuto: 'BucketAutoOptions', | ||
granularity: (k) => { | ||
return `.granularity(BucketGranularity.fromString(${k}))`; | ||
}, | ||
} | ||
); | ||
} | ||
handletext(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'text', | ||
['$search'], | ||
['$language', '$caseSensitive', '$diacriticSensitive'], | ||
{ | ||
text: 'TextSearchOptions', | ||
$language: (k) => { | ||
return `.language(${k})`; | ||
}, | ||
$caseSensitive: (k) => { | ||
return `.caseSensitive(${k})`; | ||
}, | ||
$diacriticSensitive: (k) => { | ||
return `.diacriticSensitive(${k})`; | ||
}, | ||
} | ||
); | ||
} | ||
handleunwind(ctx) { | ||
const copy = this.deepCopyRequiredImports(); | ||
const value = this.visit(ctx.parentCtx); | ||
this.requiredImports = copy; | ||
if (this.findTypedNode(ctx.parentCtx).type.id === '_string') { | ||
return `unwind(${value})`; | ||
} | ||
); | ||
} | ||
handletext(ctx) { | ||
return this.handleOptionsObject( | ||
ctx, | ||
'text', | ||
['$search'], | ||
['$language', '$caseSensitive', '$diacriticSensitive'], | ||
{ | ||
text: 'TextSearchOptions', | ||
$language: (k) => { return `.language(${k})`; }, | ||
$caseSensitive: (k) => { return `.caseSensitive(${k})`; }, | ||
$diacriticSensitive: (k) => { return `.diacriticSensitive(${k})`; } | ||
} | ||
); | ||
} | ||
handleunwind(ctx) { | ||
const copy = this.deepCopyRequiredImports(); | ||
const value = this.visit(ctx.parentCtx); | ||
this.requiredImports = copy; | ||
if (this.findTypedNode(ctx.parentCtx).type.id === '_string') { | ||
return `unwind(${value})`; | ||
return this.handleOptionsObject( | ||
ctx, | ||
'unwind', | ||
['path'], | ||
['includeArrayIndex', 'preserveNullAndEmptyArrays'], | ||
{ unwind: 'UnwindOptions' } | ||
); | ||
} | ||
return this.handleOptionsObject( | ||
ctx, | ||
'unwind', | ||
['path'], | ||
['includeArrayIndex', 'preserveNullAndEmptyArrays'], | ||
{ unwind: 'UnwindOptions' } | ||
); | ||
} | ||
handlegroup(ctx) { | ||
return this.handleMultipleSubfields(ctx, 'group', ['_id'], (f, v) => { | ||
return v; | ||
}, true); | ||
} | ||
handlefacet(ctx) { | ||
this.requiredImports[306].push('Facet'); | ||
return this.handleMultipleSubfields(ctx, 'facet', [], (f, v) => { | ||
return `new Facet(${doubleQuoteStringify(f)}, ${v})`; | ||
}, true); | ||
} | ||
handleaddFields(ctx) { | ||
this.requiredImports[306].push('Field'); | ||
return this.handleMultipleSubfields(ctx, 'addFields', [], (f, v) => { | ||
return `new Field(${doubleQuoteStringify(f)}, ${v})`; | ||
}, false); | ||
} | ||
}; | ||
handlegroup(ctx) { | ||
return this.handleMultipleSubfields( | ||
ctx, | ||
'group', | ||
['_id'], | ||
(f, v) => { | ||
return v; | ||
}, | ||
true | ||
); | ||
} | ||
handlefacet(ctx) { | ||
this.requiredImports[306].push('Facet'); | ||
return this.handleMultipleSubfields( | ||
ctx, | ||
'facet', | ||
[], | ||
(f, v) => { | ||
return `new Facet(${doubleQuoteStringify(f)}, ${v})`; | ||
}, | ||
true | ||
); | ||
} | ||
handleaddFields(ctx) { | ||
this.requiredImports[306].push('Field'); | ||
return this.handleMultipleSubfields( | ||
ctx, | ||
'addFields', | ||
[], | ||
(f, v) => { | ||
return `new Field(${doubleQuoteStringify(f)}, ${v})`; | ||
}, | ||
false | ||
); | ||
} | ||
}; |
@@ -5,13 +5,14 @@ 'use strict'; | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
generateFuncDefExpression(ctx) { | ||
const source = ctx.start.source[1].strdata; | ||
const startIndex = ctx.start.start; | ||
const stopIndex = ctx.stop.stop; | ||
return source.slice(startIndex, stopIndex + 1); | ||
} | ||
}; | ||
generateFuncDefExpression(ctx) { | ||
const source = ctx.start.source[1].strdata; | ||
const startIndex = ctx.start.start; | ||
const stopIndex = ctx.stop.stop; | ||
return source.slice(startIndex, stopIndex + 1); | ||
} | ||
}; |
@@ -8,7 +8,6 @@ 'use strict'; | ||
BsonTranspilersRuntimeError, | ||
BsonTranspilersUnimplementedError | ||
BsonTranspilersUnimplementedError, | ||
} = require('../../helper/error'); | ||
const { removeQuotes } = require('../../helper/format'); | ||
/** | ||
@@ -21,47 +20,66 @@ * This is a Visitor that visits the tree generated by the ECMAScript.g4 grammar. | ||
*/ | ||
module.exports = (CodeGenerationVisitor) => class Visitor extends CodeGenerationVisitor { | ||
constructor() { | ||
super(); | ||
this.startRule = 'program'; // Name of the ANTLR rule to start | ||
module.exports = (CodeGenerationVisitor) => | ||
class Visitor extends CodeGenerationVisitor { | ||
constructor() { | ||
super(); | ||
this.startRule = 'program'; // Name of the ANTLR rule to start | ||
// Throw UnimplementedError for nodes with expressions that we don't support | ||
this.visitThisExpression = | ||
this.visitDeleteExpression = | ||
this.visitVoidExpression = | ||
this.visitTypeofExpression = | ||
this.visitInExpression = | ||
this.visitInstanceofExpression = | ||
this.visitAssignmentExpression = | ||
this.visitAssignmentOperatorExpression = | ||
this.visitMemberIndexExpression = | ||
this.visitTernaryExpression = | ||
this.visitFunctionDeclaration = | ||
this.visitVariableStatement = | ||
this.visitIfStatement = | ||
this.visitDoWhileStatement = | ||
this.visitWhileStatement = | ||
this.visitForStatement = | ||
this.visitForVarStatement = | ||
this.visitForInStatement = | ||
this.visitForVarInStatement = | ||
this.visitContinueStatement = | ||
this.visitBreakStatement = | ||
this.visitReturnStatement = | ||
this.visitWithStatement = | ||
this.visitLabelledStatement = | ||
this.visitSwitchStatement = | ||
this.visitThrowStatement = | ||
this.visitTryStatement = | ||
this.visitDebuggerStatement = | ||
this.unimplemented; | ||
} | ||
// Throw UnimplementedError for nodes with expressions that we don't support | ||
this.visitThisExpression = | ||
this.visitDeleteExpression = | ||
this.visitVoidExpression = | ||
this.visitTypeofExpression = | ||
this.visitInExpression = | ||
this.visitInstanceofExpression = | ||
this.visitAssignmentExpression = | ||
this.visitAssignmentOperatorExpression = | ||
this.visitMemberIndexExpression = | ||
this.visitTernaryExpression = | ||
this.visitFunctionDeclaration = | ||
this.visitVariableStatement = | ||
this.visitIfStatement = | ||
this.visitDoWhileStatement = | ||
this.visitWhileStatement = | ||
this.visitForStatement = | ||
this.visitForVarStatement = | ||
this.visitForInStatement = | ||
this.visitForVarInStatement = | ||
this.visitContinueStatement = | ||
this.visitBreakStatement = | ||
this.visitReturnStatement = | ||
this.visitWithStatement = | ||
this.visitLabelledStatement = | ||
this.visitSwitchStatement = | ||
this.visitThrowStatement = | ||
this.visitTryStatement = | ||
this.visitDebuggerStatement = | ||
this.unimplemented; | ||
} | ||
/* | ||
* | ||
* Visit Methods | ||
* | ||
*/ | ||
/* | ||
* | ||
* Visit Methods | ||
* | ||
*/ | ||
visitProgram(ctx) { | ||
if (ctx.getChildCount() < 2) { | ||
visitProgram(ctx) { | ||
if (ctx.getChildCount() < 2) { | ||
throw new BsonTranspilersRuntimeError( | ||
'Expression contains 0 statements. Input should be a single statement' | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitSourceElements(ctx) { | ||
if (ctx.sourceElement().length !== 1) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Expression contains ${ | ||
ctx.sourceElement().length | ||
} statements. Input should be a single statement` | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitEmptyStatement() { | ||
throw new BsonTranspilersRuntimeError( | ||
@@ -71,639 +89,665 @@ 'Expression contains 0 statements. Input should be a single statement' | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitSourceElements(ctx) { | ||
if (ctx.sourceElement().length !== 1) { | ||
throw new BsonTranspilersRuntimeError(`Expression contains ${ | ||
ctx.sourceElement().length} statements. Input should be a single statement`); | ||
visitFuncCallExpression(ctx) { | ||
return this.generateFunctionCall(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitEmptyStatement() { | ||
throw new BsonTranspilersRuntimeError( | ||
'Expression contains 0 statements. Input should be a single statement' | ||
); | ||
} | ||
visitIdentifierExpression(ctx) { | ||
return this.generateIdentifier(ctx); | ||
} | ||
visitFuncCallExpression(ctx) { | ||
return this.generateFunctionCall(ctx); | ||
} | ||
visitGetAttributeExpression(ctx) { | ||
return this.generateAttributeAccess(ctx); | ||
} | ||
visitIdentifierExpression(ctx) { | ||
return this.generateIdentifier(ctx); | ||
} | ||
visitObjectLiteral(ctx) { | ||
return this.generateObjectLiteral(ctx); | ||
} | ||
visitGetAttributeExpression(ctx) { | ||
return this.generateAttributeAccess(ctx); | ||
} | ||
visitArrayLiteral(ctx) { | ||
return this.generateArrayLiteral(ctx); | ||
} | ||
visitObjectLiteral(ctx) { | ||
return this.generateObjectLiteral(ctx); | ||
} | ||
visitNullLiteral(ctx) { | ||
return this.leafHelper(this.Types._null, ctx); | ||
} | ||
visitArrayLiteral(ctx) { | ||
return this.generateArrayLiteral(ctx); | ||
} | ||
visitUndefinedLiteral(ctx) { | ||
return this.leafHelper(this.Types._undefined, ctx); | ||
} | ||
visitNullLiteral(ctx) { | ||
return this.leafHelper(this.Types._null, ctx); | ||
} | ||
visitBooleanLiteral(ctx) { | ||
return this.leafHelper(this.Types._bool, ctx); | ||
} | ||
visitUndefinedLiteral(ctx) { | ||
return this.leafHelper(this.Types._undefined, ctx); | ||
} | ||
visitStringLiteral(ctx) { | ||
return this.leafHelper(this.Types._string, ctx); | ||
} | ||
visitBooleanLiteral(ctx) { | ||
return this.leafHelper(this.Types._bool, ctx); | ||
} | ||
visitRegularExpressionLiteral(ctx) { | ||
return this.leafHelper(this.Types._regex, ctx); | ||
} | ||
visitStringLiteral(ctx) { | ||
return this.leafHelper(this.Types._string, ctx); | ||
} | ||
visitIntegerLiteral(ctx) { | ||
return this.leafHelper(this.Types._long, ctx); | ||
} | ||
visitRegularExpressionLiteral(ctx) { | ||
return this.leafHelper(this.Types._regex, ctx); | ||
} | ||
visitDecimalLiteral(ctx) { | ||
return this.leafHelper(this.Types._decimal, ctx); | ||
} | ||
visitIntegerLiteral(ctx) { | ||
return this.leafHelper(this.Types._long, ctx); | ||
} | ||
visitHexIntegerLiteral(ctx) { | ||
return this.leafHelper(this.Types._hex, ctx); | ||
} | ||
visitDecimalLiteral(ctx) { | ||
return this.leafHelper(this.Types._decimal, ctx); | ||
} | ||
visitOctalIntegerLiteral(ctx) { | ||
return this.leafHelper(this.Types._octal, ctx); | ||
} | ||
visitHexIntegerLiteral(ctx) { | ||
return this.leafHelper(this.Types._hex, ctx); | ||
} | ||
visitElision(ctx) { | ||
ctx.type = this.Types._undefined; | ||
if (ctx.type.template) { | ||
return ctx.type.template(); | ||
} | ||
return 'null'; | ||
} | ||
visitOctalIntegerLiteral(ctx) { | ||
return this.leafHelper(this.Types._octal, ctx); | ||
} | ||
visitElision(ctx) { | ||
ctx.type = this.Types._undefined; | ||
if (ctx.type.template) { | ||
return ctx.type.template(); | ||
visitNewExpression(ctx) { | ||
// Skip new because already included in function calls for constructors. | ||
ctx.singleExpression().wasNew = true; // for dates only | ||
const res = this.visit(ctx.singleExpression()); | ||
ctx.type = this.findTypedNode(ctx.singleExpression()).type; | ||
return res; | ||
} | ||
return 'null'; | ||
} | ||
visitNewExpression(ctx) { | ||
// Skip new because already included in function calls for constructors. | ||
ctx.singleExpression().wasNew = true; // for dates only | ||
const res = this.visit(ctx.singleExpression()); | ||
ctx.type = this.findTypedNode(ctx.singleExpression()).type; | ||
return res; | ||
} | ||
visitRelationalExpression(ctx) { | ||
ctx.type = this.Types._boolean; | ||
const lhs = this.visit(ctx.singleExpression()[0]); | ||
const rhs = this.visit(ctx.singleExpression()[1]); | ||
const op = this.visit(ctx.children[1]); | ||
if (this.Syntax.equality) { | ||
return this.Syntax.equality.template(lhs, op, rhs); | ||
visitRelationalExpression(ctx) { | ||
ctx.type = this.Types._boolean; | ||
const lhs = this.visit(ctx.singleExpression()[0]); | ||
const rhs = this.visit(ctx.singleExpression()[1]); | ||
const op = this.visit(ctx.children[1]); | ||
if (this.Syntax.equality) { | ||
return this.Syntax.equality.template(lhs, op, rhs); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitEqualityExpression(ctx) { | ||
ctx.type = this.Types._boolean; | ||
const lhs = this.visit(ctx.singleExpression()[0]); | ||
const rhs = this.visit(ctx.singleExpression()[1]); | ||
const op = this.visit(ctx.children[1]); | ||
if (this.Syntax.equality) { | ||
return this.Syntax.equality.template(lhs, op, rhs); | ||
visitEqualityExpression(ctx) { | ||
ctx.type = this.Types._boolean; | ||
const lhs = this.visit(ctx.singleExpression()[0]); | ||
const rhs = this.visit(ctx.singleExpression()[1]); | ||
const op = this.visit(ctx.children[1]); | ||
if (this.Syntax.equality) { | ||
return this.Syntax.equality.template(lhs, op, rhs); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitLogicalAndExpression(ctx) { | ||
if (this.Syntax.and) { | ||
return this.Syntax.and.template( | ||
ctx.singleExpression().map((t) => (this.visit(t))) | ||
); | ||
visitLogicalAndExpression(ctx) { | ||
if (this.Syntax.and) { | ||
return this.Syntax.and.template( | ||
ctx.singleExpression().map((t) => this.visit(t)) | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitLogicalOrExpression(ctx) { | ||
if (this.Syntax.or) { | ||
return this.Syntax.or.template( | ||
ctx.singleExpression().map((t) => ( this.visit(t) )) | ||
); | ||
visitLogicalOrExpression(ctx) { | ||
if (this.Syntax.or) { | ||
return this.Syntax.or.template( | ||
ctx.singleExpression().map((t) => this.visit(t)) | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitNotExpression(ctx) { | ||
if (this.Syntax.not) { | ||
return this.Syntax.not.template( | ||
this.visit(ctx.singleExpression()) | ||
); | ||
visitNotExpression(ctx) { | ||
if (this.Syntax.not) { | ||
return this.Syntax.not.template(this.visit(ctx.singleExpression())); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitBitAndExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
visitBitAndExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitBitXOrExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
visitBitXOrExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitBitOrExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
visitBitOrExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitBitNotExpression(ctx) { | ||
if (this.Syntax.unary.template) { | ||
return this.Syntax.unary.template( | ||
'~', | ||
this.visit(ctx.singleExpression()) | ||
); | ||
visitBitNotExpression(ctx) { | ||
if (this.Syntax.unary.template) { | ||
return this.Syntax.unary.template( | ||
'~', | ||
this.visit(ctx.singleExpression()) | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitUnaryPlusExpression(ctx) { | ||
if (this.Syntax.unary.template) { | ||
return this.Syntax.unary.template( | ||
'+', | ||
this.visit(ctx.singleExpression()) | ||
); | ||
visitUnaryPlusExpression(ctx) { | ||
if (this.Syntax.unary.template) { | ||
return this.Syntax.unary.template( | ||
'+', | ||
this.visit(ctx.singleExpression()) | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitUnaryMinusExpression(ctx) { | ||
if (this.Syntax.unary.template) { | ||
return this.Syntax.unary.template( | ||
'-', | ||
this.visit(ctx.singleExpression()) | ||
); | ||
visitUnaryMinusExpression(ctx) { | ||
if (this.Syntax.unary.template) { | ||
return this.Syntax.unary.template( | ||
'-', | ||
this.visit(ctx.singleExpression()) | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitAdditiveExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
visitAdditiveExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitMultiplicativeExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
visitMultiplicativeExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitBitShiftExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
visitBitShiftExpression(ctx) { | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitParenthesizedExpression(ctx) { | ||
if (this.Syntax.parens.template) { | ||
const kids = this.visit(ctx.expressionSequence()); | ||
return this.Syntax.parens.template(kids); | ||
visitParenthesizedExpression(ctx) { | ||
if (this.Syntax.parens.template) { | ||
const kids = this.visit(ctx.expressionSequence()); | ||
return this.Syntax.parens.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitFuncDefExpression(ctx) { | ||
return this.generateFuncDefExpression(ctx); | ||
} | ||
visitFuncDefExpression(ctx) { | ||
return this.generateFuncDefExpression(ctx); | ||
} | ||
/* | ||
* | ||
* Process Methods | ||
* | ||
*/ | ||
/* | ||
* | ||
* Process Methods | ||
* | ||
*/ | ||
/* Numerical process methods */ | ||
processNumber(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
/* Numerical process methods */ | ||
processNumber(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
processInt32(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
processInt32(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
processDouble(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
processDouble(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
/** | ||
* Check arguments then execute in the same way as regex literals. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processRegExp(ctx) { | ||
this.checkArguments( | ||
this.Symbols.RegExp.args, this.getArguments(ctx), 'RegExp' | ||
); | ||
return this.process_regex(ctx); | ||
} | ||
/** | ||
* This looks like non-camelcase because the name of the basic type is "_regex" | ||
* and the process methods are constructed with "Process" + <type name>. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
process_regex(ctx) { // eslint-disable-line camelcase | ||
ctx.type = this.Types._regex; | ||
let pattern; | ||
let flags; | ||
try { | ||
const regexobj = this.executeJavascript(ctx.getText()); | ||
pattern = regexobj.source; | ||
flags = regexobj.flags; | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
/** | ||
* Check arguments then execute in the same way as regex literals. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processRegExp(ctx) { | ||
this.checkArguments( | ||
this.Symbols.RegExp.args, | ||
this.getArguments(ctx), | ||
'RegExp' | ||
); | ||
return this.process_regex(ctx); | ||
} | ||
let targetflags = flags.replace(/[imuyg]/g, m => this.Syntax.regexFlags[m]); | ||
targetflags = targetflags === '' ? | ||
'' : | ||
`${targetflags.split('').sort().join('')}`; | ||
/** | ||
* This looks like non-camelcase because the name of the basic type is "_regex" | ||
* and the process methods are constructed with "Process" + <type name>. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
process_regex(ctx) { | ||
// eslint-disable-line camelcase | ||
ctx.type = this.Types._regex; | ||
let pattern; | ||
let flags; | ||
return this.generateLiteral(ctx, ctx.type, [pattern, targetflags], 'RegExp'); | ||
} | ||
try { | ||
const regexobj = this.executeJavascript(ctx.getText()); | ||
pattern = regexobj.source; | ||
flags = regexobj.flags; | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
/** | ||
* Process BSON regexps because we need to verify the flags are valid. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {string} | ||
*/ | ||
processBSONRegExp(ctx) { | ||
return this.generateBSONRegex( | ||
ctx, this.Types.BSONRegExpType, this.Symbols.BSONRegExp | ||
); | ||
} | ||
let targetflags = flags.replace( | ||
/[imuyg]/g, | ||
(m) => this.Syntax.regexFlags[m] | ||
); | ||
targetflags = | ||
targetflags === '' ? '' : `${targetflags.split('').sort().join('')}`; | ||
/** | ||
* The arguments to Code can be either a string or actual javascript code. | ||
* Manually check arguments here because first argument can be any JS, and we | ||
* don't want to ever visit that node. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processCodeFromJS(ctx) { | ||
return this.generateBSONCode(ctx, this.Types.Code, this.Symbols.Code, false); | ||
} | ||
/** | ||
* ObjectId needs preprocessing because it needs to be executed. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processObjectId(ctx) { | ||
ctx.type = this.Types.ObjectId; | ||
const symbolType = this.Symbols.ObjectId; | ||
const argsList = this.getArguments(ctx); | ||
this.checkArguments(symbolType.args, argsList, 'ObjectId'); | ||
let hexstr; | ||
try { | ||
hexstr = this.executeJavascript(`new ${ctx.getText()}`).toHexString(); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
return this.generateLiteral( | ||
ctx, | ||
ctx.type, | ||
[pattern, targetflags], | ||
'RegExp' | ||
); | ||
} | ||
const args = argsList.length === 0 ? [] : [hexstr]; | ||
return this.generateCall( | ||
ctx, symbolType, args, 'ObjectId', `(${hexstr})` | ||
); | ||
} | ||
/** | ||
* Long needs preprocessing because it needs to be executed. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processLong(ctx) { | ||
ctx.type = this.Types.Long; | ||
const symbolType = this.Symbols.Long; | ||
let longstr; | ||
this.checkArguments(symbolType.args, this.getArguments(ctx), 'Long'); | ||
try { | ||
longstr = this.executeJavascript(`new ${ctx.getText()}`).toString(); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
/** | ||
* Process BSON regexps because we need to verify the flags are valid. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {string} | ||
*/ | ||
processBSONRegExp(ctx) { | ||
return this.generateBSONRegex( | ||
ctx, | ||
this.Types.BSONRegExpType, | ||
this.Symbols.BSONRegExp | ||
); | ||
} | ||
return this.generateCall( | ||
ctx, symbolType, [longstr, '_long'], 'Long', `(${longstr})` | ||
); | ||
} | ||
processLongfromBits(ctx) { | ||
if ('emitLongfromBits' in this) { | ||
return this.emitLongfromBits(ctx); | ||
/** | ||
* The arguments to Code can be either a string or actual javascript code. | ||
* Manually check arguments here because first argument can be any JS, and we | ||
* don't want to ever visit that node. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processCodeFromJS(ctx) { | ||
return this.generateBSONCode( | ||
ctx, | ||
this.Types.Code, | ||
this.Symbols.Code, | ||
false | ||
); | ||
} | ||
return this.processLong(ctx); | ||
} | ||
/** | ||
* Decimal128 needs preprocessing because it needs to be executed. Check | ||
* argument length manually because 'Buffer' not supported. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processDecimal128(ctx) { | ||
ctx.type = this.Types.Decimal128; | ||
const symbolType = this.Symbols.Decimal128; | ||
let decstr; | ||
const argList = this.getArguments(ctx); | ||
/** | ||
* ObjectId needs preprocessing because it needs to be executed. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processObjectId(ctx) { | ||
ctx.type = this.Types.ObjectId; | ||
const symbolType = this.Symbols.ObjectId; | ||
const argsList = this.getArguments(ctx); | ||
if (argList.length !== 1) { | ||
throw new BsonTranspilersArgumentError( | ||
'Argument count mismatch: Decimal128 requires one argument' | ||
this.checkArguments(symbolType.args, argsList, 'ObjectId'); | ||
let hexstr; | ||
try { | ||
hexstr = this.executeJavascript(`new ${ctx.getText()}`).toHexString(); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
const args = argsList.length === 0 ? [] : [hexstr]; | ||
return this.generateCall( | ||
ctx, | ||
symbolType, | ||
args, | ||
'ObjectId', | ||
`(${hexstr})` | ||
); | ||
} | ||
try { | ||
decstr = this.executeJavascript(`new ${ctx.getText()}`).toString(); | ||
} catch (error) { | ||
// TODO: this isn't quite right because it catches all type errors. | ||
if (error.name === 'TypeError' || error.code === 'ERR_INVALID_ARG_TYPE') { | ||
throw new BsonTranspilersArgumentError(error.message); | ||
/** | ||
* Long needs preprocessing because it needs to be executed. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processLong(ctx) { | ||
ctx.type = this.Types.Long; | ||
const symbolType = this.Symbols.Long; | ||
let longstr; | ||
this.checkArguments(symbolType.args, this.getArguments(ctx), 'Long'); | ||
try { | ||
longstr = this.executeJavascript(`new ${ctx.getText()}`).toString(); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
return this.generateCall( | ||
ctx, | ||
symbolType, | ||
[longstr, '_long'], | ||
'Long', | ||
`(${longstr})` | ||
); | ||
} | ||
return this.generateCall( | ||
ctx, symbolType, [decstr], 'Decimal128', `(${decstr})` | ||
); | ||
} | ||
/** | ||
* This is a bit weird because we can just convert to string directly. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processLongtoString(ctx) { | ||
ctx.type = this.Types._string; | ||
const long = ctx.singleExpression().singleExpression(); | ||
let longstr; | ||
this.checkArguments( | ||
[[this.Types._numeric, null]], this.getArguments(ctx), 'Long toString' | ||
); | ||
try { | ||
longstr = this.executeJavascript(`new ${long.getText()}`).toString(); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
processLongfromBits(ctx) { | ||
if ('emitLongfromBits' in this) { | ||
return this.emitLongfromBits(ctx); | ||
} | ||
return this.processLong(ctx); | ||
} | ||
return ctx.type.template ? ctx.type.template(longstr) : `'${longstr}'`; | ||
} | ||
/** | ||
* Preprocessed because different target languages need different info out | ||
* of the constructed date, so we want to execute it. Passes a constructed | ||
* date object to the template or generator. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processDate(ctx) { | ||
const isStr = !(ctx.getText().includes('ISODate') || ctx.wasNew); | ||
const symbolType = this.Symbols.Date; | ||
/** | ||
* Decimal128 needs preprocessing because it needs to be executed. Check | ||
* argument length manually because 'Buffer' not supported. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processDecimal128(ctx) { | ||
ctx.type = this.Types.Decimal128; | ||
const symbolType = this.Symbols.Decimal128; | ||
let decstr; | ||
const argList = this.getArguments(ctx); | ||
ctx.type = this.Types.Date; | ||
if (isStr) { | ||
ctx.type = this.Types._string; | ||
this.requiredImports[201] = true; | ||
} | ||
const argsList = this.getArguments(ctx); | ||
let date = null; | ||
if (argsList.length !== 0) { | ||
try { | ||
this.checkArguments(this.Symbols.Date.args, argsList, 'Date'); | ||
} catch (e) { | ||
if (argList.length !== 1) { | ||
throw new BsonTranspilersArgumentError( | ||
'Invalid argument to Date: requires no args, one string/number, or up to 7 numbers' | ||
'Argument count mismatch: Decimal128 requires one argument' | ||
); | ||
} | ||
let text = ctx.getText(); | ||
text = text.startsWith('new ') ? text : `new ${text}`; | ||
try { | ||
date = this.executeJavascript(text); | ||
decstr = this.executeJavascript(`new ${ctx.getText()}`).toString(); | ||
} catch (error) { | ||
// TODO: this isn't quite right because it catches all type errors. | ||
if ( | ||
error.name === 'TypeError' || | ||
error.code === 'ERR_INVALID_ARG_TYPE' | ||
) { | ||
throw new BsonTranspilersArgumentError(error.message); | ||
} | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
return this.generateCall( | ||
ctx, | ||
symbolType, | ||
[decstr], | ||
'Decimal128', | ||
`(${decstr})` | ||
); | ||
} | ||
const dargs = `Date(${date | ||
? this.Types._string.template(date.toUTCString()) | ||
: ''})`; | ||
return this.generateCall( | ||
ctx, symbolType, [date, isStr], '', dargs, isStr, true | ||
); | ||
} | ||
processObjectIdCreateFromTime(ctx) { | ||
return this.generateObjectIdFromTime(ctx); | ||
} | ||
/** | ||
* This is a bit weird because we can just convert to string directly. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processLongtoString(ctx) { | ||
ctx.type = this.Types._string; | ||
const long = ctx.singleExpression().singleExpression(); | ||
let longstr; | ||
this.checkArguments( | ||
[[this.Types._numeric, null]], | ||
this.getArguments(ctx), | ||
'Long toString' | ||
); | ||
processBinary() { | ||
throw new BsonTranspilersUnimplementedError('Binary type not supported'); | ||
} | ||
try { | ||
longstr = this.executeJavascript(`new ${long.getText()}`).toString(); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
return ctx.type.template ? ctx.type.template(longstr) : `'${longstr}'`; | ||
} | ||
/* | ||
* | ||
* Helper Methods | ||
* | ||
*/ | ||
/** | ||
* Preprocessed because different target languages need different info out | ||
* of the constructed date, so we want to execute it. Passes a constructed | ||
* date object to the template or generator. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processDate(ctx) { | ||
const isStr = !(ctx.getText().includes('ISODate') || ctx.wasNew); | ||
const symbolType = this.Symbols.Date; | ||
/** | ||
* Takes in the constructor name of a node and returns a human-readable | ||
* node name. Used for error reporting, must be defined by all visitors. | ||
* | ||
* @param {String} name | ||
* @return {String} | ||
*/ | ||
renameNode(name) { | ||
return name ? name.replace('_stmt', '') : 'Expression'; | ||
} | ||
ctx.type = this.Types.Date; | ||
if (isStr) { | ||
ctx.type = this.Types._string; | ||
this.requiredImports[201] = true; | ||
} | ||
/** | ||
* There are no named arguments in javascript, so we can just return directly. | ||
* @param {Array} expected | ||
* @param {ParserRuleContext} node | ||
* @return {Array} | ||
*/ | ||
checkNamedArgs(expected, node) { | ||
return [expected, node]; | ||
} | ||
const argsList = this.getArguments(ctx); | ||
let date = null; | ||
/** | ||
* Execute javascript in a sandbox. | ||
* | ||
* @param {String} input | ||
* @return {*} result of execution | ||
*/ | ||
executeJavascript(input) { | ||
const sandbox = { | ||
RegExp: RegExp, | ||
BSONRegExp: bson.BSONRegExp, | ||
// Binary: bson.Binary, | ||
DBRef: bson.DBRef, | ||
Decimal128: bson.Decimal128, | ||
Double: bson.Double, | ||
Int32: bson.Int32, | ||
Long: bson.Long, | ||
Int64: bson.Long, | ||
Map: bson.Map, | ||
MaxKey: bson.MaxKey, | ||
MinKey: bson.MinKey, | ||
ObjectID: bson.ObjectId, | ||
ObjectId: bson.ObjectId, | ||
BSONSymbol: bson.BSONSymbol, | ||
Timestamp: bson.Timestamp, | ||
Code: function(c, s) { | ||
return new bson.Code(c, s); | ||
}, | ||
Date: function(s) { | ||
const args = Array.from(arguments); | ||
if (argsList.length !== 0) { | ||
try { | ||
this.checkArguments(this.Symbols.Date.args, argsList, 'Date'); | ||
} catch (e) { | ||
throw new BsonTranspilersArgumentError( | ||
'Invalid argument to Date: requires no args, one string/number, or up to 7 numbers' | ||
); | ||
} | ||
if (args.length === 1) { | ||
return new Date(s); | ||
let text = ctx.getText(); | ||
text = text.startsWith('new ') ? text : `new ${text}`; | ||
try { | ||
date = this.executeJavascript(text); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
} | ||
const dargs = `Date(${ | ||
date ? this.Types._string.template(date.toUTCString()) : '' | ||
})`; | ||
return this.generateCall( | ||
ctx, | ||
symbolType, | ||
[date, isStr], | ||
'', | ||
dargs, | ||
isStr, | ||
true | ||
); | ||
} | ||
return new Date(Date.UTC(...args)); | ||
}, | ||
Buffer: Buffer, | ||
__result: {} | ||
}; | ||
const res = vm.runInContext('__result = ' + input, vm.createContext(sandbox)); | ||
return res; | ||
} | ||
processObjectIdCreateFromTime(ctx) { | ||
return this.generateObjectIdFromTime(ctx); | ||
} | ||
/* | ||
* Accessor Functions. | ||
* | ||
* These MUST be defined by every visitor. Each function is a wrapper around | ||
* a tree node. They are required so that the CodeGenerationVisitor and the | ||
* Generators can access tree elements without needing to know which tree they | ||
* are visiting or the ANTLR name of the node. | ||
*/ | ||
processBinary() { | ||
throw new BsonTranspilersUnimplementedError('Binary type not supported'); | ||
} | ||
getArguments(ctx) { | ||
if (!('arguments' in ctx) || | ||
/* | ||
* | ||
* Helper Methods | ||
* | ||
*/ | ||
/** | ||
* Takes in the constructor name of a node and returns a human-readable | ||
* node name. Used for error reporting, must be defined by all visitors. | ||
* | ||
* @param {String} name | ||
* @return {String} | ||
*/ | ||
renameNode(name) { | ||
return name ? name.replace('_stmt', '') : 'Expression'; | ||
} | ||
/** | ||
* There are no named arguments in javascript, so we can just return directly. | ||
* @param {Array} expected | ||
* @param {ParserRuleContext} node | ||
* @return {Array} | ||
*/ | ||
checkNamedArgs(expected, node) { | ||
return [expected, node]; | ||
} | ||
/** | ||
* Execute javascript in a sandbox. | ||
* | ||
* @param {String} input | ||
* @return {*} result of execution | ||
*/ | ||
executeJavascript(input) { | ||
const sandbox = { | ||
RegExp: RegExp, | ||
BSONRegExp: bson.BSONRegExp, | ||
// Binary: bson.Binary, | ||
DBRef: bson.DBRef, | ||
Decimal128: bson.Decimal128, | ||
Double: bson.Double, | ||
Int32: bson.Int32, | ||
Long: bson.Long, | ||
Int64: bson.Long, | ||
Map: bson.Map, | ||
MaxKey: bson.MaxKey, | ||
MinKey: bson.MinKey, | ||
ObjectID: bson.ObjectId, | ||
ObjectId: bson.ObjectId, | ||
BSONSymbol: bson.BSONSymbol, | ||
Timestamp: bson.Timestamp, | ||
Code: function (c, s) { | ||
return new bson.Code(c, s); | ||
}, | ||
Date: function (s) { | ||
const args = Array.from(arguments); | ||
if (args.length === 1) { | ||
return new Date(s); | ||
} | ||
return new Date(Date.UTC(...args)); | ||
}, | ||
Buffer: Buffer, | ||
__result: {}, | ||
}; | ||
const res = vm.runInContext( | ||
'__result = ' + input, | ||
vm.createContext(sandbox) | ||
); | ||
return res; | ||
} | ||
/* | ||
* Accessor Functions. | ||
* | ||
* These MUST be defined by every visitor. Each function is a wrapper around | ||
* a tree node. They are required so that the CodeGenerationVisitor and the | ||
* Generators can access tree elements without needing to know which tree they | ||
* are visiting or the ANTLR name of the node. | ||
*/ | ||
getArguments(ctx) { | ||
if ( | ||
!('arguments' in ctx) || | ||
!('argumentList' in ctx.arguments()) || | ||
!ctx.arguments().argumentList()) { | ||
return []; | ||
!ctx.arguments().argumentList() | ||
) { | ||
return []; | ||
} | ||
return ctx.arguments().argumentList().singleExpression(); | ||
} | ||
return ctx.arguments().argumentList().singleExpression(); | ||
} | ||
getArgumentAt(ctx, i) { | ||
return this.getArguments(ctx)[i]; | ||
} | ||
getArgumentAt(ctx, i) { | ||
return this.getArguments(ctx)[i]; | ||
} | ||
getFunctionCallName(ctx) { | ||
return ctx.singleExpression(); | ||
} | ||
getIfIdentifier(ctx) { | ||
if ('identifierName' in ctx) { | ||
getFunctionCallName(ctx) { | ||
return ctx.singleExpression(); | ||
} | ||
return ctx; | ||
} | ||
getAttributeLHS(ctx) { | ||
return ctx.singleExpression(); | ||
} | ||
getIfIdentifier(ctx) { | ||
if ('identifierName' in ctx) { | ||
return ctx.singleExpression(); | ||
} | ||
return ctx; | ||
} | ||
getAttributeRHS(ctx) { | ||
return ctx.identifierName(); | ||
} | ||
getAttributeLHS(ctx) { | ||
return ctx.singleExpression(); | ||
} | ||
getList(ctx) { | ||
if (!('elementList' in ctx) || !ctx.elementList()) { | ||
return []; | ||
getAttributeRHS(ctx) { | ||
return ctx.identifierName(); | ||
} | ||
const elisions = ctx.elementList().elision(); | ||
const elements = ctx.elementList().singleExpression(); | ||
return ctx.elementList().children.filter((c) => { | ||
return elisions.indexOf(c) !== -1 || elements.indexOf(c) !== -1; | ||
}); | ||
} | ||
getArray(ctx) { | ||
if (!('arrayLiteral' in ctx)) { | ||
return false; | ||
getList(ctx) { | ||
if (!('elementList' in ctx) || !ctx.elementList()) { | ||
return []; | ||
} | ||
const elisions = ctx.elementList().elision(); | ||
const elements = ctx.elementList().singleExpression(); | ||
return ctx.elementList().children.filter((c) => { | ||
return elisions.indexOf(c) !== -1 || elements.indexOf(c) !== -1; | ||
}); | ||
} | ||
return ctx.arrayLiteral(); | ||
} | ||
getObject(ctx) { | ||
if (!('objectLiteral' in ctx)) { | ||
return false; | ||
getArray(ctx) { | ||
if (!('arrayLiteral' in ctx)) { | ||
return false; | ||
} | ||
return ctx.arrayLiteral(); | ||
} | ||
return ctx.objectLiteral(); | ||
} | ||
getKeyValueList(ctx) { | ||
if ('propertyNameAndValueList' in ctx && ctx.propertyNameAndValueList()) { | ||
return ctx.propertyNameAndValueList().propertyAssignment(); | ||
getObject(ctx) { | ||
if (!('objectLiteral' in ctx)) { | ||
return false; | ||
} | ||
return ctx.objectLiteral(); | ||
} | ||
return []; | ||
} | ||
getKeyStr(ctx) { | ||
return removeQuotes(this.visit(ctx.propertyName())); | ||
} | ||
getKeyValueList(ctx) { | ||
if ('propertyNameAndValueList' in ctx && ctx.propertyNameAndValueList()) { | ||
return ctx.propertyNameAndValueList().propertyAssignment(); | ||
} | ||
return []; | ||
} | ||
getValue(ctx) { | ||
return ctx.singleExpression(); | ||
} | ||
getKeyStr(ctx) { | ||
return removeQuotes(this.visit(ctx.propertyName())); | ||
} | ||
isSubObject(ctx) { | ||
return 'propertyName' in ctx.parentCtx.parentCtx; | ||
} | ||
getValue(ctx) { | ||
return ctx.singleExpression(); | ||
} | ||
getParentKeyStr(ctx) { | ||
// For a given sub document, get its key. | ||
return this.getKeyStr(ctx.parentCtx.parentCtx); | ||
} | ||
isSubObject(ctx) { | ||
return 'propertyName' in ctx.parentCtx.parentCtx; | ||
} | ||
getObjectChild(ctx) { | ||
return ctx.getChild(0); | ||
} | ||
}; | ||
getParentKeyStr(ctx) { | ||
// For a given sub document, get its key. | ||
return this.getKeyStr(ctx.parentCtx.parentCtx); | ||
} | ||
getObjectChild(ctx) { | ||
return ctx.getChild(0); | ||
} | ||
}; |
@@ -7,3 +7,3 @@ 'use strict'; | ||
BsonTranspilersTypeError, | ||
BsonTranspilersRuntimeError | ||
BsonTranspilersRuntimeError, | ||
} = require('../../helper/error'); | ||
@@ -15,225 +15,256 @@ const { removeQuotes } = require('../../helper/format'); | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
this.IGNORE = 'a unique thing'; | ||
} | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
this.IGNORE = 'a unique thing'; | ||
} | ||
start(ctx) { | ||
return this.returnResult(ctx); | ||
} | ||
/** | ||
* Overrides the helper function to instantiate the object instead of | ||
* concatenating the strings. | ||
* | ||
* @param {ParserRuleContext} ctx - The function call node | ||
* @param {Object} lhsType - The type | ||
* @param {Array} args - Arguments to the template | ||
* | ||
* @return {String} | ||
*/ | ||
generateCall(ctx, lhsType, args) { | ||
if (`emit${lhsType.id}` in this) { | ||
return this[`emit${lhsType.id}`](ctx, ...args); | ||
start(ctx) { | ||
return this.returnResult(ctx); | ||
} | ||
const lhs = this.visit(this.getFunctionCallName(ctx)); | ||
return this.returnFunctionCallLhsRhs(lhs, args, lhsType); | ||
} | ||
/** | ||
* Don't concatenate child nodes, return them as array. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Object} options | ||
* @return {Array} | ||
*/ | ||
visitChildren(ctx, options) { | ||
const opts = { | ||
start: 0, step: 1, separator: '', ignore: [], children: ctx.children | ||
}; | ||
Object.assign(opts, options ? options : {}); | ||
opts.end = ('end' in opts) ? opts.end : opts.children.length - 1; | ||
const code = []; | ||
for (let i = opts.start; i <= opts.end; i += opts.step) { | ||
if (opts.ignore.indexOf(i) === -1) { | ||
code.push(this.visit( | ||
opts.children[i] | ||
)); | ||
/** | ||
* Overrides the helper function to instantiate the object instead of | ||
* concatenating the strings. | ||
* | ||
* @param {ParserRuleContext} ctx - The function call node | ||
* @param {Object} lhsType - The type | ||
* @param {Array} args - Arguments to the template | ||
* | ||
* @return {String} | ||
*/ | ||
generateCall(ctx, lhsType, args) { | ||
if (`emit${lhsType.id}` in this) { | ||
return this[`emit${lhsType.id}`](ctx, ...args); | ||
} | ||
const lhs = this.visit(this.getFunctionCallName(ctx)); | ||
return this.returnFunctionCallLhsRhs(lhs, args, lhsType); | ||
} | ||
const result = code.filter((c) => c !== this.IGNORE); | ||
if (result.length === 1) { | ||
return result[0]; | ||
} | ||
return result; | ||
} | ||
returnFunctionCallRhs(rhs) { | ||
return rhs; | ||
} | ||
/** | ||
* Don't concatenate child nodes, return them as array. | ||
* | ||
* @param {ParserRuleContext} ctx | ||
* @param {Object} options | ||
* @return {Array} | ||
*/ | ||
visitChildren(ctx, options) { | ||
const opts = { | ||
start: 0, | ||
step: 1, | ||
separator: '', | ||
ignore: [], | ||
children: ctx.children, | ||
}; | ||
Object.assign(opts, options ? options : {}); | ||
opts.end = 'end' in opts ? opts.end : opts.children.length - 1; | ||
returnFunctionCallLhs(code, name) { | ||
const types = { | ||
100: bson.Code, 101: bson.ObjectId, 102: bson.Binary, 103: bson.DBRef, | ||
104: bson.Double, 105: bson.Int32, 106: bson.Long, 107: bson.MinKey, | ||
108: bson.MaxKey, 109: bson.BSONRegExp, 110: bson.Timestamp, | ||
111: bson.BSONSymbol, 112: bson.Decimal128, 200: Date, 8: RegExp, 2: Number, | ||
10: Object | ||
}; | ||
const result = types[code]; | ||
if (result === undefined) { | ||
throw new BsonTranspilersReferenceError(`Cannot instantiate ${name} with code=${code}`); | ||
const code = []; | ||
for (let i = opts.start; i <= opts.end; i += opts.step) { | ||
if (opts.ignore.indexOf(i) === -1) { | ||
code.push(this.visit(opts.children[i])); | ||
} | ||
} | ||
const result = code.filter((c) => c !== this.IGNORE); | ||
if (result.length === 1) { | ||
return result[0]; | ||
} | ||
return result; | ||
} | ||
return result; | ||
} | ||
returnFunctionCallLhsRhs(lhs, args, lhsType) { | ||
if (args.length === 1 && args[0] === undefined) { | ||
args = []; | ||
returnFunctionCallRhs(rhs) { | ||
return rhs; | ||
} | ||
if (lhsType && lhsType.argsTemplate) { | ||
return lhsType.argsTemplate.bind(this.getState())(lhs, ...args); | ||
returnFunctionCallLhs(code, name) { | ||
const types = { | ||
100: bson.Code, | ||
101: bson.ObjectId, | ||
102: bson.Binary, | ||
103: bson.DBRef, | ||
104: bson.Double, | ||
105: bson.Int32, | ||
106: bson.Long, | ||
107: bson.MinKey, | ||
108: bson.MaxKey, | ||
109: bson.BSONRegExp, | ||
110: bson.Timestamp, | ||
111: bson.BSONSymbol, | ||
112: bson.Decimal128, | ||
200: Date, | ||
8: RegExp, | ||
2: Number, | ||
10: Object, | ||
}; | ||
const result = types[code]; | ||
if (result === undefined) { | ||
throw new BsonTranspilersReferenceError( | ||
`Cannot instantiate ${name} with code=${code}` | ||
); | ||
} | ||
return result; | ||
} | ||
let expr; | ||
try { | ||
if (lhsType.callable === this.SYMBOL_TYPE.CONSTRUCTOR) { | ||
expr = new lhs(...args); | ||
} else { | ||
expr = lhs(...args); | ||
returnFunctionCallLhsRhs(lhs, args, lhsType) { | ||
if (args.length === 1 && args[0] === undefined) { | ||
args = []; | ||
} | ||
} catch (e) { | ||
if (e.message.includes('constructor')) { | ||
try { | ||
if (lhsType && lhsType.argsTemplate) { | ||
return lhsType.argsTemplate.bind(this.getState())(lhs, ...args); | ||
} | ||
let expr; | ||
try { | ||
if (lhsType.callable === this.SYMBOL_TYPE.CONSTRUCTOR) { | ||
expr = new lhs(...args); | ||
} else { | ||
expr = lhs(...args); | ||
} catch (e2) { | ||
e2.message = `Error constructing type: ${e2.message}`; | ||
throw e2; | ||
} | ||
} else { | ||
e.message = `Error constructing type: ${e.message}`; | ||
throw e; | ||
} catch (e) { | ||
if (e.message.includes('constructor')) { | ||
try { | ||
expr = lhs(...args); | ||
} catch (e2) { | ||
e2.message = `Error constructing type: ${e2.message}`; | ||
throw e2; | ||
} | ||
} else { | ||
e.message = `Error constructing type: ${e.message}`; | ||
throw e; | ||
} | ||
} | ||
return expr; | ||
} | ||
return expr; | ||
} | ||
returnAttributeAccess(lhs, rhs, type) { | ||
if (type === null) { | ||
throw new BsonTranspilersTypeError(`Error: ${rhs} is undefined and cannot be called`); | ||
} | ||
let expr = lhs[rhs]; | ||
if (type.attr[rhs].template) { | ||
expr = type.attr[rhs].template(lhs, rhs); | ||
returnAttributeAccess(lhs, rhs, type) { | ||
if (type === null) { | ||
throw new BsonTranspilersTypeError( | ||
`Error: ${rhs} is undefined and cannot be called` | ||
); | ||
} | ||
let expr = lhs[rhs]; | ||
if (type.attr[rhs].template) { | ||
expr = type.attr[rhs].template(lhs, rhs); | ||
if (typeof expr === 'function') { | ||
return function () { | ||
return expr(...arguments); | ||
}; | ||
} | ||
} | ||
if (typeof expr === 'function') { | ||
return function() { | ||
return expr(...arguments); | ||
return function () { | ||
return lhs[rhs](...arguments); | ||
}; | ||
} | ||
return expr; | ||
} | ||
if (typeof expr === 'function') { | ||
return function() { | ||
return lhs[rhs](...arguments); | ||
}; | ||
returnParenthesis(expr) { | ||
return expr; | ||
} | ||
return expr; | ||
} | ||
returnParenthesis(expr) { | ||
return expr; | ||
} | ||
returnSet(args) { | ||
return args; | ||
} | ||
returnSet(args) { | ||
return args; | ||
} | ||
returnComparison(ctx) { | ||
return ctx.children.reduce((s, node, i, arr) => { | ||
if (i === arr.length - 1) { | ||
// Always visit the last element | ||
return s; | ||
} | ||
if (i % 2 === 0) { | ||
// Only ops | ||
return s; | ||
} | ||
const op = this.visit(node); | ||
if ( | ||
typeof op === 'object' && | ||
op.length === 2 && | ||
op.every((k) => ['in', 'is', 'not'].indexOf(k) !== -1) | ||
) { | ||
return this.Syntax.equality.template(s, '!=', this.visit(arr[i + 1])); | ||
} | ||
if (['>', '<', '<=', '>=', '<>', '==', '!=', 'is'].indexOf(op) !== -1) { | ||
return this.Syntax.equality.template(s, op, this.visit(arr[i + 1])); | ||
} | ||
if (op === 'in' || op === 'notin') { | ||
return this.Syntax.in.template.bind(this.state)( | ||
s, | ||
op, | ||
this.visit(arr[i + 1]) | ||
); | ||
} | ||
throw new BsonTranspilersRuntimeError(`Unrecognized operation ${op}`); | ||
}, this.visit(ctx.children[0])); | ||
} | ||
returnComparison(ctx) { | ||
return ctx.children.reduce((s, node, i, arr) => { | ||
if (i === arr.length - 1) { // Always visit the last element | ||
return s; | ||
} | ||
if (i % 2 === 0) { // Only ops | ||
return s; | ||
} | ||
const op = this.visit(node); | ||
if (typeof op === 'object' && op.length === 2 && op.every((k) => (['in', 'is', 'not'].indexOf(k) !== -1))) { | ||
return this.Syntax.equality.template(s, '!=', this.visit(arr[i + 1])); | ||
} | ||
if (['>', '<', '<=', '>=', '<>', '==', '!=', 'is'].indexOf(op) !== -1) { | ||
return this.Syntax.equality.template(s, op, this.visit(arr[i + 1])); | ||
} | ||
if (op === 'in' || op === 'notin') { | ||
return this.Syntax.in.template.bind(this.state)(s, op, this.visit(arr[i + 1])); | ||
} | ||
throw new BsonTranspilersRuntimeError(`Unrecognized operation ${op}`); | ||
}, this.visit(ctx.children[0])); | ||
} | ||
emitLongfromBits(ctx) { | ||
ctx.type = this.Types.Long; | ||
const symbolType = this.Symbols.Long.attr.fromBits; | ||
const rhs = this.checkArguments( | ||
symbolType.args, | ||
this.getArguments(ctx), | ||
'Long.fromBits' | ||
); | ||
return bson.Long.fromBits(...rhs); | ||
} | ||
emitLongfromBits(ctx) { | ||
ctx.type = this.Types.Long; | ||
const symbolType = this.Symbols.Long.attr.fromBits; | ||
const rhs = this.checkArguments( | ||
symbolType.args, this.getArguments(ctx), 'Long.fromBits' | ||
); | ||
return bson.Long.fromBits(...rhs); | ||
} | ||
emitRegex(ctx, pattern, flags) { | ||
return new bson.BSONRegExp(pattern, flags); | ||
} | ||
emitRegex(ctx, pattern, flags) { | ||
return new bson.BSONRegExp(pattern, flags); | ||
} | ||
emit_array(ctx) { | ||
ctx.type = this.Types._array; | ||
this.requiredImports[9] = true; | ||
return this.getList(ctx).map((child) => this.visit(child)); | ||
} | ||
emit_array(ctx) { | ||
ctx.type = this.Types._array; | ||
this.requiredImports[9] = true; | ||
return this.getList(ctx).map((child) => ( this.visit(child) )); | ||
} | ||
emit_object(ctx) { | ||
ctx.type = this.Types._object; | ||
this.requiredImports[10] = true; | ||
const object = {}; | ||
this.getKeyValueList(ctx).map((k) => { | ||
const key = this.getKeyStr(k); | ||
if (key === '$where') { | ||
object[key] = removeQuotes(this.getValue(k).getText()); | ||
} else { | ||
object[key] = this.visit(this.getValue(k)); | ||
} | ||
}); | ||
return object; | ||
} | ||
emit_object(ctx) { | ||
ctx.type = this.Types._object; | ||
this.requiredImports[10] = true; | ||
const object = {}; | ||
this.getKeyValueList(ctx).map((k) => { | ||
const key = this.getKeyStr(k); | ||
if (key === '$where') { | ||
object[key] = removeQuotes(this.getValue(k).getText()); | ||
} else { | ||
object[key] = this.visit(this.getValue(k)); | ||
emitdatetime(ctx, date, isString) { | ||
if (date === null && isString) { | ||
return Date(); | ||
} else if (date === null) { | ||
return new Date(); | ||
} | ||
}); | ||
return object; | ||
} | ||
emitdatetime(ctx, date, isString) { | ||
if (date === null && isString) { | ||
return Date(); | ||
} else if (date === null) { | ||
return new Date(); | ||
return date; | ||
} | ||
return date; | ||
} | ||
emitDate(ctx, date, isString) { | ||
if (date === null && isString) { | ||
return Date(); | ||
} else if (date === null) { | ||
return new Date(); | ||
emitDate(ctx, date, isString) { | ||
if (date === null && isString) { | ||
return Date(); | ||
} else if (date === null) { | ||
return new Date(); | ||
} | ||
return date; | ||
} | ||
return date; | ||
} | ||
/* Numbers need emit methods because they also take type args */ | ||
/* Numbers need emit methods because they also take type args */ | ||
emitNumber(ctx, arg) { | ||
return Number(arg); | ||
} | ||
emitNumber(ctx, arg) { | ||
return Number(arg); | ||
} | ||
emitint(ctx, arg) { | ||
return new bson.Int32(arg); | ||
} | ||
emitint(ctx, arg) { | ||
return new bson.Int32(arg); | ||
} | ||
emitfloat(ctx, arg) { | ||
return new bson.Double(arg); | ||
} | ||
}; | ||
emitfloat(ctx, arg) { | ||
return new bson.Double(arg); | ||
} | ||
}; |
@@ -6,9 +6,10 @@ 'use strict'; | ||
const PHPUtils = require('./PHPUtils'); | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
// Common functions used by templates | ||
this.state.utils = new PHPUtils(); | ||
} | ||
}; | ||
// Common functions used by templates | ||
this.state.utils = new PHPUtils(); | ||
} | ||
}; |
@@ -8,4 +8,3 @@ 'use strict'; | ||
class PHPUtils { | ||
constructor() { | ||
} | ||
constructor() {} | ||
@@ -58,3 +57,3 @@ /** | ||
if ( | ||
(str.charAt(0) === '\'' && str.charAt(str.length - 1) === '\'') || | ||
(str.charAt(0) === "'" && str.charAt(str.length - 1) === "'") || | ||
(str.charAt(0) === '"' && str.charAt(str.length - 1) === '"') | ||
@@ -61,0 +60,0 @@ ) { |
@@ -5,6 +5,7 @@ 'use strict'; | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; |
@@ -8,3 +8,3 @@ 'use strict'; | ||
BsonTranspilersInternalError, | ||
BsonTranspilersUnimplementedError | ||
BsonTranspilersUnimplementedError, | ||
} = require('../../helper/error'); | ||
@@ -20,734 +20,805 @@ const { removeQuotes } = require('../../helper/format'); | ||
*/ | ||
module.exports = (CodeGenerationVisitor) => class Visitor extends CodeGenerationVisitor { | ||
constructor() { | ||
super(); | ||
this.startRule = 'file_input'; // Name of the ANTLR rule to start | ||
module.exports = (CodeGenerationVisitor) => | ||
class Visitor extends CodeGenerationVisitor { | ||
constructor() { | ||
super(); | ||
this.startRule = 'file_input'; // Name of the ANTLR rule to start | ||
// Throw UnimplementedError for nodes with expressions that we don't support | ||
this.visitDel_stmt = | ||
this.visitPass_stmt = | ||
this.visitFlow_stmt = | ||
this.visitImport_stmt = | ||
this.visitGlobal_stmt = | ||
this.visitNonlocal_stmt = | ||
this.visitAssert_stmt = | ||
this.visitIf_stmt = | ||
this.visitWhile_stmt = | ||
this.visitFor_stmt = | ||
this.visitTry_stmt = | ||
this.visitWith_stmt = | ||
this.visitFuncdef = | ||
this.visitClassdef = | ||
this.visitDecorated = | ||
this.visitAsync_stmt = | ||
this.visitComp_iter = | ||
this.visitStar_expr = | ||
this.visitInline_if = | ||
this.visitAssign_stmt = | ||
this.visitEllipsesAtom = | ||
this.visitAugassign = | ||
this.visitImag_literal = | ||
this.unimplemented; | ||
} | ||
// Throw UnimplementedError for nodes with expressions that we don't support | ||
this.visitDel_stmt = | ||
this.visitPass_stmt = | ||
this.visitFlow_stmt = | ||
this.visitImport_stmt = | ||
this.visitGlobal_stmt = | ||
this.visitNonlocal_stmt = | ||
this.visitAssert_stmt = | ||
this.visitIf_stmt = | ||
this.visitWhile_stmt = | ||
this.visitFor_stmt = | ||
this.visitTry_stmt = | ||
this.visitWith_stmt = | ||
this.visitFuncdef = | ||
this.visitClassdef = | ||
this.visitDecorated = | ||
this.visitAsync_stmt = | ||
this.visitComp_iter = | ||
this.visitStar_expr = | ||
this.visitInline_if = | ||
this.visitAssign_stmt = | ||
this.visitEllipsesAtom = | ||
this.visitAugassign = | ||
this.visitImag_literal = | ||
this.unimplemented; | ||
} | ||
/* | ||
* | ||
* Visit Methods | ||
* | ||
*/ | ||
/* | ||
* | ||
* Visit Methods | ||
* | ||
*/ | ||
visitFile_input(ctx) { | ||
if (ctx.stmt().length !== 1) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Expression contains ${ | ||
ctx.stmt().length | ||
} statements. Input should be a single statement` | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitFile_input(ctx) { | ||
if (ctx.stmt().length !== 1) { | ||
throw new BsonTranspilersRuntimeError(`Expression contains ${ | ||
ctx.stmt().length} statements. Input should be a single statement`); | ||
visitFunctionCall(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
return this.generateFunctionCall(ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitFunctionCall(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
visitIdentifier(ctx) { | ||
return this.generateIdentifier(ctx); | ||
} | ||
return this.generateFunctionCall(ctx); | ||
} | ||
visitIdentifier(ctx) { | ||
return this.generateIdentifier(ctx); | ||
} | ||
visitAttributeAccess(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
return this.generateAttributeAccess(ctx); | ||
} | ||
visitAttributeAccess(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
visitObject_literal(ctx) { | ||
this.testForComprehension(ctx.dictorsetmaker()); | ||
return this.generateObjectLiteral(ctx); | ||
} | ||
return this.generateAttributeAccess(ctx); | ||
} | ||
visitObject_literal(ctx) { | ||
this.testForComprehension(ctx.dictorsetmaker()); | ||
return this.generateObjectLiteral(ctx); | ||
} | ||
visitArray_literal(ctx) { | ||
this.testForComprehension(ctx.testlist_comp()); | ||
return this.generateArrayLiteral(ctx); | ||
} | ||
visitArray_literal(ctx) { | ||
this.testForComprehension(ctx.testlist_comp()); | ||
return this.generateArrayLiteral(ctx); | ||
} | ||
visitExpr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
visitExpr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitXor_expr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
visitXor_expr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitAnd_expr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
visitAnd_expr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map((m) => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
if (this.Syntax.binary.template) { | ||
const kids = ctx.children.map(m => this.visit(m)); | ||
return this.Syntax.binary.template(kids); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitShift_expr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
visitShift_expr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
const args = ctx.children.map((n) => this.visit(n)); | ||
if (this.Syntax.binary.template) { | ||
return this.Syntax.binary.template(args); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
const args = ctx.children.map((n) => this.visit(n)); | ||
if (this.Syntax.binary.template) { | ||
return this.Syntax.binary.template(args); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitArith_expr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
visitArith_expr(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
const args = ctx.children.map((n) => this.visit(n)); | ||
if (this.Syntax.binary.template) { | ||
return this.Syntax.binary.template(args); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
const args = ctx.children.map((n) => this.visit(n)); | ||
if (this.Syntax.binary.template) { | ||
return this.Syntax.binary.template(args); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
/* So far, this only exists in Python so it hasn't been moved to | ||
* CodeGenerationVisitor. However, if another input or output language has a | ||
* set implementation, should move this to the shared visitor. */ | ||
visitSet_literal(ctx) { | ||
ctx.type = this.Types._array; | ||
ctx.indentDepth = this.findIndentDepth(ctx) + 1; | ||
this.requiredImports[9] = true; | ||
let args = []; | ||
const list = ctx.testlist_comp(); | ||
this.testForComprehension(list); | ||
let join; | ||
if (list) { | ||
// Sets of 1 item is the same as the item itself, but keep parens for math | ||
if (list.children.length === 1) { | ||
return this.returnParenthesis(this.visit(list.children[0])); | ||
} | ||
const visitedChildren = list.children.map((child) => { | ||
return this.visit(child); | ||
}); | ||
const visitedElements = visitedChildren.filter((arg) => { | ||
return arg !== ','; | ||
}); | ||
if (ctx.type.argsTemplate) { // NOTE: not currently being used anywhere. | ||
args = visitedElements.map((arg, index) => { | ||
const last = !visitedElements[index + 1]; | ||
return ctx.type.argsTemplate.bind(this.getState())(arg, ctx.indentDepth, last); | ||
/* So far, this only exists in Python so it hasn't been moved to | ||
* CodeGenerationVisitor. However, if another input or output language has a | ||
* set implementation, should move this to the shared visitor. */ | ||
visitSet_literal(ctx) { | ||
ctx.type = this.Types._array; | ||
ctx.indentDepth = this.findIndentDepth(ctx) + 1; | ||
this.requiredImports[9] = true; | ||
let args = []; | ||
const list = ctx.testlist_comp(); | ||
this.testForComprehension(list); | ||
let join; | ||
if (list) { | ||
// Sets of 1 item is the same as the item itself, but keep parens for math | ||
if (list.children.length === 1) { | ||
return this.returnParenthesis(this.visit(list.children[0])); | ||
} | ||
const visitedChildren = list.children.map((child) => { | ||
return this.visit(child); | ||
}); | ||
join = ''; | ||
} else { | ||
args = visitedElements; | ||
join = ', '; | ||
const visitedElements = visitedChildren.filter((arg) => { | ||
return arg !== ','; | ||
}); | ||
if (ctx.type.argsTemplate) { | ||
// NOTE: not currently being used anywhere. | ||
args = visitedElements.map((arg, index) => { | ||
const last = !visitedElements[index + 1]; | ||
return ctx.type.argsTemplate.bind(this.getState())( | ||
arg, | ||
ctx.indentDepth, | ||
last | ||
); | ||
}); | ||
join = ''; | ||
} else { | ||
args = visitedElements; | ||
join = ', '; | ||
} | ||
} | ||
if (ctx.type.template) { | ||
return ctx.type.template(args.join(join), ctx.indentDepth); | ||
} | ||
return this.returnSet(args, ctx); | ||
} | ||
if (ctx.type.template) { | ||
return ctx.type.template(args.join(join), ctx.indentDepth); | ||
} | ||
return this.returnSet(args, ctx); | ||
} | ||
visitStringAtom(ctx) { | ||
ctx.type = this.Types._string; | ||
this.requiredImports[ctx.type.code] = true; | ||
// Pass the original argument type to the template, not the casted type. | ||
const type = ctx.originalType === undefined ? ctx.type : ctx.originalType; | ||
visitStringAtom(ctx) { | ||
ctx.type = this.Types._string; | ||
this.requiredImports[ctx.type.code] = true; | ||
// Pass the original argument type to the template, not the casted type. | ||
const type = ctx.originalType === undefined ? ctx.type : ctx.originalType; | ||
let result = this.visitChildren(ctx); | ||
result = result.replace(/^([rubf]?[rubf]["']|'''|"""|'|")/gi, ''); | ||
result = result.replace(/(["]{3}|["]|[']{3}|['])$/, ''); | ||
return this.generateLiteral( | ||
ctx, ctx.type, [result, type.id], `'${result}'`, true | ||
); | ||
} | ||
visitInteger_literal(ctx) { | ||
return this.leafHelper(this.Types._long, ctx); | ||
} | ||
visitOct_literal(ctx) { | ||
return this.leafHelper(this.Types._octal, ctx); | ||
} | ||
visitHex_literal(ctx) { | ||
return this.leafHelper(this.Types._hex, ctx); | ||
} | ||
visitBin_literal(ctx) { | ||
return this.leafHelper(this.Types._bin, ctx); | ||
} | ||
visitFloat_literal(ctx) { | ||
return this.leafHelper(this.Types._decimal, ctx); | ||
} | ||
visitBoolean_literal(ctx) { | ||
return this.leafHelper(this.Types._bool, ctx); | ||
} | ||
visitNone_literal(ctx) { | ||
return this.leafHelper(this.Types._null, ctx); | ||
} | ||
visitExpr_stmt(ctx) { | ||
if ( | ||
('assign_stmt' in ctx && ctx.assign_stmt() !== null) || | ||
('augassign' in ctx && ctx.augassign() !== null) || | ||
('annassign' in ctx && ctx.annassign() !== null) | ||
) { | ||
throw new BsonTranspilersUnimplementedError( | ||
'Assignment not yet implemented' | ||
let result = this.visitChildren(ctx); | ||
result = result.replace(/^([rubf]?[rubf]["']|'''|"""|'|")/gi, ''); | ||
result = result.replace(/(["]{3}|["]|[']{3}|['])$/, ''); | ||
return this.generateLiteral( | ||
ctx, | ||
ctx.type, | ||
[result, type.id], | ||
`'${result}'`, | ||
true | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitFactor(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
visitInteger_literal(ctx) { | ||
return this.leafHelper(this.Types._long, ctx); | ||
} | ||
// For the expression "+1", set the type to the child's type. | ||
const op = this.visit(ctx.children[0]); | ||
const factor = this.visit(ctx.factor()); | ||
ctx.type = this.findTypedNode(ctx.factor()).type; | ||
if (this.Syntax.unary.template) { | ||
return this.Syntax.unary.template( | ||
op, | ||
factor | ||
); | ||
visitOct_literal(ctx) { | ||
return this.leafHelper(this.Types._octal, ctx); | ||
} | ||
return `${op}${factor}`; | ||
} | ||
visitTerm(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
visitHex_literal(ctx) { | ||
return this.leafHelper(this.Types._hex, ctx); | ||
} | ||
const args = ctx.children.map((n) => this.visit(n)); | ||
if (this.Syntax.binary.template) { | ||
return this.Syntax.binary.template(args); | ||
visitBin_literal(ctx) { | ||
return this.leafHelper(this.Types._bin, ctx); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
visitPower(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
visitFloat_literal(ctx) { | ||
return this.leafHelper(this.Types._decimal, ctx); | ||
} | ||
const lhs = this.visit(ctx.atom()); | ||
const rhs = this.visit(ctx.factor()); | ||
if (this.Syntax.binary.template) { | ||
return this.Syntax.binary.template([lhs, '**', rhs]); | ||
visitBoolean_literal(ctx) { | ||
return this.leafHelper(this.Types._bool, ctx); | ||
} | ||
return `${lhs} ** ${rhs}`; | ||
} | ||
visitAnd_test(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
visitNone_literal(ctx) { | ||
return this.leafHelper(this.Types._null, ctx); | ||
} | ||
const children = ctx.not_test().map((t) => ( this.visit(t) )); | ||
if (this.Syntax.and) { | ||
return this.Syntax.and.template(children); | ||
} | ||
return children.join(' and '); | ||
} | ||
visitOr_test(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
visitExpr_stmt(ctx) { | ||
if ( | ||
('assign_stmt' in ctx && ctx.assign_stmt() !== null) || | ||
('augassign' in ctx && ctx.augassign() !== null) || | ||
('annassign' in ctx && ctx.annassign() !== null) | ||
) { | ||
throw new BsonTranspilersUnimplementedError( | ||
'Assignment not yet implemented' | ||
); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
const children = ctx.and_test().map((t) => (this.visit(t))); | ||
if (this.Syntax.or) { | ||
return this.Syntax.or.template(children); | ||
visitFactor(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
// For the expression "+1", set the type to the child's type. | ||
const op = this.visit(ctx.children[0]); | ||
const factor = this.visit(ctx.factor()); | ||
ctx.type = this.findTypedNode(ctx.factor()).type; | ||
if (this.Syntax.unary.template) { | ||
return this.Syntax.unary.template(op, factor); | ||
} | ||
return `${op}${factor}`; | ||
} | ||
return children.join(' or '); | ||
} | ||
visitNot_test(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
visitTerm(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
const args = ctx.children.map((n) => this.visit(n)); | ||
if (this.Syntax.binary.template) { | ||
return this.Syntax.binary.template(args); | ||
} | ||
return this.visitChildren(ctx); | ||
} | ||
const child = this.visit(ctx.children[1]); | ||
if (this.Syntax.not) { | ||
return this.Syntax.not.template(child); | ||
visitPower(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
const lhs = this.visit(ctx.atom()); | ||
const rhs = this.visit(ctx.factor()); | ||
if (this.Syntax.binary.template) { | ||
return this.Syntax.binary.template([lhs, '**', rhs]); | ||
} | ||
return `${lhs} ** ${rhs}`; | ||
} | ||
} | ||
returnComparison(ctx) { | ||
let skip = false; | ||
return ctx.children.reduce((str, e, i, arr) => { | ||
if (skip) { // Skip for 'in' statements because swallows rhs | ||
skip = false; | ||
return str; | ||
visitAnd_test(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
if (i === arr.length - 1) { // Always visit the last element | ||
return `${str}${this.visit(e)}`; | ||
const children = ctx.not_test().map((t) => this.visit(t)); | ||
if (this.Syntax.and) { | ||
return this.Syntax.and.template(children); | ||
} | ||
if (i % 2 === 0) { // Only ops | ||
return str; | ||
return children.join(' and '); | ||
} | ||
visitOr_test(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
const op = this.visit(e); | ||
if (op === '==' || op === '!=' || op === 'is' || op === 'isnot') { | ||
skip = true; | ||
if (this.Syntax.equality) { | ||
return `${str}${this.Syntax.equality.template( | ||
this.visit(arr[i - 1]), op, this.visit(arr[i + 1]))}`; | ||
} | ||
return `${str} === ${this.visit(arr[i - 1])} ${op} ${this.visit(arr[i + 1])}`; | ||
const children = ctx.and_test().map((t) => this.visit(t)); | ||
if (this.Syntax.or) { | ||
return this.Syntax.or.template(children); | ||
} | ||
if (op === 'in' || op === 'notin') { | ||
skip = true; | ||
if (this.Syntax.in) { | ||
return `${str}${this.Syntax.in.template.bind(this.state)( | ||
this.visit(arr[i - 1]), op, this.visit(arr[i + 1]))}`; | ||
} | ||
return `${str} ${this.visit(arr[i - 1])} ${op} ${this.visit(arr[i + 1])}`; | ||
return children.join(' or '); | ||
} | ||
visitNot_test(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
return `${str}${this.visit(arr[i - 1])} ${op} `; | ||
}, ''); | ||
} | ||
const child = this.visit(ctx.children[1]); | ||
if (this.Syntax.not) { | ||
return this.Syntax.not.template(child); | ||
} | ||
} | ||
visitComparison(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
returnComparison(ctx) { | ||
let skip = false; | ||
return ctx.children.reduce((str, e, i, arr) => { | ||
if (skip) { | ||
// Skip for 'in' statements because swallows rhs | ||
skip = false; | ||
return str; | ||
} | ||
if (i === arr.length - 1) { | ||
// Always visit the last element | ||
return `${str}${this.visit(e)}`; | ||
} | ||
if (i % 2 === 0) { | ||
// Only ops | ||
return str; | ||
} | ||
const op = this.visit(e); | ||
if (op === '==' || op === '!=' || op === 'is' || op === 'isnot') { | ||
skip = true; | ||
if (this.Syntax.equality) { | ||
return `${str}${this.Syntax.equality.template( | ||
this.visit(arr[i - 1]), | ||
op, | ||
this.visit(arr[i + 1]) | ||
)}`; | ||
} | ||
return `${str} === ${this.visit(arr[i - 1])} ${op} ${this.visit( | ||
arr[i + 1] | ||
)}`; | ||
} | ||
if (op === 'in' || op === 'notin') { | ||
skip = true; | ||
if (this.Syntax.in) { | ||
return `${str}${this.Syntax.in.template.bind(this.state)( | ||
this.visit(arr[i - 1]), | ||
op, | ||
this.visit(arr[i + 1]) | ||
)}`; | ||
} | ||
return `${str} ${this.visit(arr[i - 1])} ${op} ${this.visit( | ||
arr[i + 1] | ||
)}`; | ||
} | ||
return `${str}${this.visit(arr[i - 1])} ${op} `; | ||
}, ''); | ||
} | ||
return this.returnComparison(ctx); | ||
} | ||
visitIndexAccess(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
visitComparison(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
return this.returnComparison(ctx); | ||
} | ||
throw new BsonTranspilersUnimplementedError('Indexing not currently supported'); | ||
} | ||
/* | ||
* | ||
* Process Methods | ||
* | ||
*/ | ||
visitIndexAccess(ctx) { | ||
if (ctx.getChildCount() === 1) { | ||
return this.visitChildren(ctx); | ||
} | ||
throw new BsonTranspilersUnimplementedError( | ||
'Indexing not currently supported' | ||
); | ||
} | ||
processint(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
/* | ||
* | ||
* Process Methods | ||
* | ||
*/ | ||
processfloat(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
processint(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
processInt64(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
processfloat(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
processfrom_native(ctx) { | ||
ctx.type = this.Types.BSONRegExp; | ||
const symbolType = this.Symbols.Regex; | ||
const argList = this.getArguments(ctx); | ||
if (argList.length !== 1) { | ||
throw new BsonTranspilersArgumentError('RegExp.from_native requires one argument'); | ||
processInt64(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
const pythonFlags = { | ||
256: '', 2: 'i', 128: '', 4: 'l', 8: 'm', 16: 's', 64: 'x' | ||
}; | ||
const native = this.skipFakeNodesDown(this.getArgumentAt(ctx, 0)); | ||
const args = this.findPatternAndFlags(native, pythonFlags, this.Syntax.bsonRegexFlags); | ||
return this.generateCall( | ||
ctx, symbolType, args, 'Regex', | ||
`(${args[0]}${args[1] ? ', ' + args[1] : ''})` | ||
); | ||
} | ||
processfrom_native(ctx) { | ||
ctx.type = this.Types.BSONRegExp; | ||
const symbolType = this.Symbols.Regex; | ||
processcompile(ctx) { | ||
ctx.type = this.Types._regex; | ||
const pythonFlags = { | ||
256: '', 2: 'i', 128: '', 4: '', 8: 'm', 16: '', 64: '' | ||
}; | ||
const args = this.findPatternAndFlags(ctx, pythonFlags, this.Syntax.regexFlags); | ||
return this.generateLiteral(ctx, ctx.type, args, 'RegExp'); | ||
} | ||
const argList = this.getArguments(ctx); | ||
if (argList.length !== 1) { | ||
throw new BsonTranspilersArgumentError( | ||
'RegExp.from_native requires one argument' | ||
); | ||
} | ||
const pythonFlags = { | ||
256: '', | ||
2: 'i', | ||
128: '', | ||
4: 'l', | ||
8: 'm', | ||
16: 's', | ||
64: 'x', | ||
}; | ||
const native = this.skipFakeNodesDown(this.getArgumentAt(ctx, 0)); | ||
const args = this.findPatternAndFlags( | ||
native, | ||
pythonFlags, | ||
this.Syntax.bsonRegexFlags | ||
); | ||
processRegex(ctx) { | ||
return this.generateBSONRegex(ctx, this.Types.Regex, this.Symbols.Regex); | ||
} | ||
return this.generateCall( | ||
ctx, | ||
symbolType, | ||
args, | ||
'Regex', | ||
`(${args[0]}${args[1] ? ', ' + args[1] : ''})` | ||
); | ||
} | ||
processCode(ctx) { | ||
return this.generateBSONCode(ctx, this.Types.Code, this.Symbols.Code, true); | ||
} | ||
processcompile(ctx) { | ||
ctx.type = this.Types._regex; | ||
const pythonFlags = { | ||
256: '', | ||
2: 'i', | ||
128: '', | ||
4: '', | ||
8: 'm', | ||
16: '', | ||
64: '', | ||
}; | ||
const args = this.findPatternAndFlags( | ||
ctx, | ||
pythonFlags, | ||
this.Syntax.regexFlags | ||
); | ||
return this.generateLiteral(ctx, ctx.type, args, 'RegExp'); | ||
} | ||
processdatetime(ctx) { | ||
ctx.type = this.Types.Date; | ||
ctx.wasNew = true; // Always true for non-js | ||
const symbolType = this.Symbols.datetime; | ||
let date = null; | ||
processRegex(ctx) { | ||
return this.generateBSONRegex(ctx, this.Types.Regex, this.Symbols.Regex); | ||
} | ||
const argsList = this.getArguments(ctx); | ||
if (argsList.length !== 0) { | ||
if (argsList.length < 3) { | ||
throw new BsonTranspilersArgumentError( | ||
`Wrong number of arguments to datetime: needs at at least 3, got ${ | ||
argsList.length}` | ||
); | ||
} | ||
processCode(ctx) { | ||
return this.generateBSONCode( | ||
ctx, | ||
this.Types.Code, | ||
this.Symbols.Code, | ||
true | ||
); | ||
} | ||
try { | ||
this.checkArguments(symbolType.args, argsList, 'datetime'); | ||
} catch (e) { | ||
throw new BsonTranspilersArgumentError( | ||
`Invalid argument to datetime: requires no args or up to 7 numbers. ${ | ||
e.message}` | ||
); | ||
} | ||
processdatetime(ctx) { | ||
ctx.type = this.Types.Date; | ||
ctx.wasNew = true; // Always true for non-js | ||
const symbolType = this.Symbols.datetime; | ||
let date = null; | ||
const argvals = argsList.map((k) => { | ||
let v; | ||
const argsList = this.getArguments(ctx); | ||
if (argsList.length !== 0) { | ||
if (argsList.length < 3) { | ||
throw new BsonTranspilersArgumentError( | ||
`Wrong number of arguments to datetime: needs at at least 3, got ${argsList.length}` | ||
); | ||
} | ||
try { | ||
v = parseInt(k.getText(), 10); | ||
this.checkArguments(symbolType.args, argsList, 'datetime'); | ||
} catch (e) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unable to convert datetime argument to integer: ${k.getText()}` | ||
throw new BsonTranspilersArgumentError( | ||
`Invalid argument to datetime: requires no args or up to 7 numbers. ${e.message}` | ||
); | ||
} | ||
if (isNaN(v)) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unable to convert datetime argument to integer: ${k.getText()}` | ||
const argvals = argsList.map((k) => { | ||
let v; | ||
try { | ||
v = parseInt(k.getText(), 10); | ||
} catch (e) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unable to convert datetime argument to integer: ${k.getText()}` | ||
); | ||
} | ||
if (isNaN(v)) { | ||
throw new BsonTranspilersRuntimeError( | ||
`Unable to convert datetime argument to integer: ${k.getText()}` | ||
); | ||
} | ||
return v; | ||
}); | ||
/* month is 0-based in JS, 1-based in everything else (afaict) */ | ||
argvals[1]--; | ||
/* date is 1900-based in JS, 0-based in python */ | ||
argvals[0] -= 1900; | ||
try { | ||
date = new Date(Date.UTC(...argvals)); | ||
} catch (e) { | ||
throw new BsonTranspilersInternalError( | ||
`Unable to construct date from arguments: ${e.message}` | ||
); | ||
} | ||
return v; | ||
}); | ||
/* month is 0-based in JS, 1-based in everything else (afaict) */ | ||
argvals[1]--; | ||
/* date is 1900-based in JS, 0-based in python */ | ||
argvals[0] -= 1900; | ||
try { | ||
date = new Date(Date.UTC(...argvals)); | ||
} catch (e) { | ||
throw new BsonTranspilersInternalError( | ||
`Unable to construct date from arguments: ${e.message}` | ||
); | ||
} | ||
const dargs = `Date(${ | ||
date ? this.Types._string.template(date.toUTCString()) : '' | ||
})`; | ||
return this.generateCall( | ||
ctx, | ||
symbolType, | ||
[date, false], | ||
'', | ||
dargs, | ||
false, | ||
true | ||
); | ||
} | ||
const dargs = `Date(${date | ||
? this.Types._string.template(date.toUTCString()) | ||
: ''})`; | ||
return this.generateCall( | ||
ctx, symbolType, [date, false], '', dargs, false, true | ||
); | ||
} | ||
processObjectIdfrom_datetime(ctx) { | ||
return this.generateObjectIdFromTime(ctx); | ||
} | ||
processObjectIdfrom_datetime(ctx) { | ||
return this.generateObjectIdFromTime(ctx); | ||
} | ||
processBinary() { | ||
throw new BsonTranspilersUnimplementedError('Binary type not supported'); | ||
} | ||
processBinary() { | ||
throw new BsonTranspilersUnimplementedError('Binary type not supported'); | ||
} | ||
/* | ||
* | ||
* Helper Methods | ||
* | ||
*/ | ||
/* | ||
* | ||
* Helper Methods | ||
* | ||
*/ | ||
findPatternAndFlags(ctx, pythonFlags, targetFlags) { | ||
let pattern; | ||
findPatternAndFlags(ctx, pythonFlags, targetFlags) { | ||
let pattern; | ||
const symbolType = this.Symbols.re.attr.compile; | ||
const argList = this.getArguments(ctx); | ||
const args = this.checkArguments(symbolType.args, argList, symbolType.id, symbolType.namedArgs); | ||
const symbolType = this.Symbols.re.attr.compile; | ||
const argList = this.getArguments(ctx); | ||
const args = this.checkArguments( | ||
symbolType.args, | ||
argList, | ||
symbolType.id, | ||
symbolType.namedArgs | ||
); | ||
// Compile regex without flags | ||
const raw = this.getArgumentAt(ctx, 0).getText(); | ||
let str = raw.replace(/^([rubf]?[rubf]["']|'''|"""|'|")/gi, ''); | ||
str = str.replace(/(["]{3}|["]|[']{3}|['])$/, ''); | ||
const input = `new RegExp(${raw.substr(-1)}${str}${raw.substr(-1)})`; | ||
try { | ||
const sandbox = { | ||
RegExp: RegExp | ||
}; | ||
const regexobj = vm.runInContext('__result = ' + input, vm.createContext(sandbox)); | ||
pattern = regexobj.source; | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
// Compile regex without flags | ||
const raw = this.getArgumentAt(ctx, 0).getText(); | ||
let str = raw.replace(/^([rubf]?[rubf]["']|'''|"""|'|")/gi, ''); | ||
str = str.replace(/(["]{3}|["]|[']{3}|['])$/, ''); | ||
const input = `new RegExp(${raw.substr(-1)}${str}${raw.substr(-1)})`; | ||
try { | ||
const sandbox = { | ||
RegExp: RegExp, | ||
}; | ||
const regexobj = vm.runInContext( | ||
'__result = ' + input, | ||
vm.createContext(sandbox) | ||
); | ||
pattern = regexobj.source; | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
// Convert flags | ||
if (args.length === 1) { | ||
return [pattern, targetFlags.u]; | ||
} | ||
// Convert flags | ||
if (args.length === 1) { | ||
return [pattern, targetFlags.u]; | ||
} | ||
let flagsArg = argList[1]; | ||
flagsArg = this.skipFakeNodesDown(this.checkNamedArgs( | ||
[this.Types._integer], flagsArg, symbolType.namedArgs | ||
)[1]); | ||
let visited; | ||
if ('expr' in flagsArg.parentCtx) { // combine bitwise flags | ||
visited = flagsArg.xor_expr().map(f => this.visit(f)); | ||
} else { | ||
visited = [this.visit(flagsArg)]; | ||
} | ||
let flagsArg = argList[1]; | ||
flagsArg = this.skipFakeNodesDown( | ||
this.checkNamedArgs( | ||
[this.Types._integer], | ||
flagsArg, | ||
symbolType.namedArgs | ||
)[1] | ||
); | ||
let visited; | ||
if ('expr' in flagsArg.parentCtx) { | ||
// combine bitwise flags | ||
visited = flagsArg.xor_expr().map((f) => this.visit(f)); | ||
} else { | ||
visited = [this.visit(flagsArg)]; | ||
} | ||
const translated = visited | ||
.map(f => pythonFlags[f]) | ||
.filter(f => f !== undefined); | ||
const translated = visited | ||
.map((f) => pythonFlags[f]) | ||
.filter((f) => f !== undefined); | ||
if (visited.indexOf('256') === -1) { // default is unicode without re.A | ||
translated.push('u'); | ||
} | ||
if (visited.indexOf('256') === -1) { | ||
// default is unicode without re.A | ||
translated.push('u'); | ||
} | ||
const target = translated | ||
.map(m => targetFlags[m]) | ||
.filter(f => f !== undefined); | ||
const target = translated | ||
.map((m) => targetFlags[m]) | ||
.filter((f) => f !== undefined); | ||
const flags = target.sort().join(''); | ||
return [pattern, flags]; | ||
} | ||
const flags = target.sort().join(''); | ||
return [pattern, flags]; | ||
} | ||
/** | ||
* Want to throw unimplemented for comprehensions instead of reference errors. | ||
* @param {ParserRuleContext} ctx | ||
*/ | ||
testForComprehension(ctx) { | ||
if (ctx === null || ctx === undefined) { | ||
return; | ||
} | ||
if ( | ||
('comp_for' in ctx && ctx.comp_for() !== null) || | ||
/** | ||
* Want to throw unimplemented for comprehensions instead of reference errors. | ||
* @param {ParserRuleContext} ctx | ||
*/ | ||
testForComprehension(ctx) { | ||
if (ctx === null || ctx === undefined) { | ||
return; | ||
} | ||
if ( | ||
('comp_for' in ctx && ctx.comp_for() !== null) || | ||
('comp_if' in ctx && ctx.comp_if() !== null) | ||
) { | ||
throw new BsonTranspilersUnimplementedError( | ||
'Comprehensions not yet implemented' | ||
); | ||
) { | ||
throw new BsonTranspilersUnimplementedError( | ||
'Comprehensions not yet implemented' | ||
); | ||
} | ||
} | ||
} | ||
getParentUntil(ctx, name, steps) { | ||
steps = steps === undefined ? 0 : steps; | ||
let res = ctx; | ||
let found = false; | ||
const stack = []; | ||
while (res !== undefined && res !== null && !found) { | ||
if (name in res) { | ||
const goal = res[name](); | ||
if (goal === stack[stack.length - 1]) { | ||
found = true; | ||
getParentUntil(ctx, name, steps) { | ||
steps = steps === undefined ? 0 : steps; | ||
let res = ctx; | ||
let found = false; | ||
const stack = []; | ||
while (res !== undefined && res !== null && !found) { | ||
if (name in res) { | ||
const goal = res[name](); | ||
if (goal === stack[stack.length - 1]) { | ||
found = true; | ||
break; | ||
} | ||
} | ||
stack.push(res); | ||
res = res.parentCtx; | ||
} | ||
return found ? stack[stack.length - 1 - steps] : false; | ||
} | ||
skipFakeNodesDown(ctx, goal) { | ||
let res = ctx; | ||
while (res.children !== undefined && res.children.length === 1) { | ||
res = res.children[0]; | ||
if (goal && goal in res) { | ||
res = res[goal](); | ||
break; | ||
} | ||
} | ||
stack.push(res); | ||
res = res.parentCtx; | ||
if (res.children === undefined) { | ||
return res.parentCtx; | ||
} | ||
return res; | ||
} | ||
return found ? stack[stack.length - 1 - steps] : false; | ||
} | ||
skipFakeNodesDown(ctx, goal) { | ||
let res = ctx; | ||
while (res.children !== undefined && res.children.length === 1) { | ||
res = res.children[0]; | ||
if (goal && goal in res) { | ||
res = res[goal](); | ||
break; | ||
skipFakeNodesUp(ctx, goal) { | ||
let res = ctx.parentCtx; | ||
while ( | ||
res !== undefined && | ||
res !== null && | ||
res.children !== undefined && | ||
res.children.length === 1 | ||
) { | ||
if (goal && goal in res) { | ||
res = res[goal](); | ||
break; | ||
} | ||
res = res.parentCtx; | ||
} | ||
return res; | ||
} | ||
if (res.children === undefined) { | ||
return res.parentCtx; | ||
/** | ||
* Takes in the constructor name of a node and returns a human-readable | ||
* node name. Used for error reporting, must be defined by all visitors. | ||
* | ||
* @param {String} name | ||
* @return {String} | ||
*/ | ||
renameNode(name) { | ||
return name ? name.replace('_stmt', '') : 'Expression'; | ||
} | ||
return res; | ||
} | ||
skipFakeNodesUp(ctx, goal) { | ||
let res = ctx.parentCtx; | ||
while (res !== undefined && res !== null && res.children !== undefined && | ||
res.children.length === 1) { | ||
if (goal && goal in res) { | ||
res = res[goal](); | ||
break; | ||
/** | ||
* If a named argument is passed in, then check against the 'namedArgs' array | ||
* instead of positionally. | ||
* | ||
* @param {Array} expected | ||
* @param {ParserRuleContext} node | ||
* @param {Object} namedArgs | ||
* @return {Array} | ||
*/ | ||
checkNamedArgs(expected, node, namedArgs) { | ||
const child = this.skipFakeNodesDown(node); | ||
if (namedArgs && 'test' in child && child.test().length > 1) { | ||
const name = child.test()[0].getText(); | ||
const value = child.test()[1]; | ||
const expectedType = namedArgs[name]; | ||
if (expectedType === undefined) { | ||
throw new BsonTranspilersArgumentError( | ||
`Unknown named argument '${name}'` | ||
); | ||
} | ||
return [expectedType.type, value]; | ||
} | ||
res = res.parentCtx; | ||
return [expected, node]; | ||
} | ||
return res; | ||
} | ||
/** | ||
* Takes in the constructor name of a node and returns a human-readable | ||
* node name. Used for error reporting, must be defined by all visitors. | ||
* | ||
* @param {String} name | ||
* @return {String} | ||
*/ | ||
renameNode(name) { | ||
return name ? name.replace('_stmt', '') : 'Expression'; | ||
} | ||
/* | ||
* | ||
* Accessor Functions. | ||
* | ||
* These MUST be defined by every visitor. Each function is a wrapper around | ||
* a tree node. They are required so that the CodeGenerationVisitor and the | ||
* Generators can access tree elements without needing to know which tree they | ||
* are visiting or the ANTLR name of the node. | ||
* | ||
*/ | ||
/** | ||
* If a named argument is passed in, then check against the 'namedArgs' array | ||
* instead of positionally. | ||
* | ||
* @param {Array} expected | ||
* @param {ParserRuleContext} node | ||
* @param {Object} namedArgs | ||
* @return {Array} | ||
*/ | ||
checkNamedArgs(expected, node, namedArgs) { | ||
const child = this.skipFakeNodesDown(node); | ||
if (namedArgs && 'test' in child && child.test().length > 1) { | ||
const name = child.test()[0].getText(); | ||
const value = child.test()[1]; | ||
const expectedType = namedArgs[name]; | ||
if (expectedType === undefined) { | ||
throw new BsonTranspilersArgumentError( | ||
`Unknown named argument '${name}'` | ||
); | ||
getArguments(ctx) { | ||
const trailer = ctx.paren_trailer(); | ||
if (!('arglist' in trailer) || trailer.arglist() === null) { | ||
return []; | ||
} | ||
return [expectedType.type, value]; | ||
return trailer.arglist().argument(); | ||
} | ||
return [expected, node]; | ||
} | ||
/* | ||
* | ||
* Accessor Functions. | ||
* | ||
* These MUST be defined by every visitor. Each function is a wrapper around | ||
* a tree node. They are required so that the CodeGenerationVisitor and the | ||
* Generators can access tree elements without needing to know which tree they | ||
* are visiting or the ANTLR name of the node. | ||
* | ||
*/ | ||
getArgumentAt(ctx, i) { | ||
return this.getArguments(ctx)[i]; | ||
} | ||
getArguments(ctx) { | ||
const trailer = ctx.paren_trailer(); | ||
if (!('arglist' in trailer) || trailer.arglist() === null) { | ||
return []; | ||
getFunctionCallName(ctx) { | ||
return ctx.atom(); | ||
} | ||
return trailer.arglist().argument(); | ||
} | ||
getArgumentAt(ctx, i) { | ||
return this.getArguments(ctx)[i]; | ||
} | ||
getIfIdentifier(ctx) { | ||
if ('identifier' in ctx) { | ||
return ctx.identifier(); | ||
} | ||
return ctx; | ||
} | ||
getFunctionCallName(ctx) { | ||
return ctx.atom(); | ||
} | ||
getAttributeLHS(ctx) { | ||
return ctx.atom(); | ||
} | ||
getIfIdentifier(ctx) { | ||
if ('identifier' in ctx) { | ||
return ctx.identifier(); | ||
getAttributeRHS(ctx) { | ||
return ctx.dot_trailer().identifier(); | ||
} | ||
return ctx; | ||
} | ||
getAttributeLHS(ctx) { | ||
return ctx.atom(); | ||
} | ||
getList(ctx) { | ||
if (!('testlist_comp' in ctx) || !ctx.testlist_comp()) { | ||
return []; | ||
} | ||
return ctx.testlist_comp().test(); | ||
} | ||
getAttributeRHS(ctx) { | ||
return ctx.dot_trailer().identifier(); | ||
} | ||
getArray(ctx) { | ||
return this.skipFakeNodesDown(ctx, 'array_literal'); | ||
} | ||
getList(ctx) { | ||
if (!('testlist_comp' in ctx) || !ctx.testlist_comp()) { | ||
getObject(ctx) { | ||
return this.skipFakeNodesDown(ctx, 'object_literal'); | ||
} | ||
getKeyValueList(ctx) { | ||
if ('dictorsetmaker' in ctx && ctx.dictorsetmaker()) { | ||
const properties = ctx.dictorsetmaker().test(); | ||
return properties | ||
.map((key, i) => { | ||
if (i % 2 === 0) { | ||
return [key, properties[i + 1]]; | ||
} | ||
return null; | ||
}) | ||
.filter((k) => k !== null); | ||
} | ||
return []; | ||
} | ||
return ctx.testlist_comp().test(); | ||
} | ||
getArray(ctx) { | ||
return this.skipFakeNodesDown(ctx, 'array_literal'); | ||
} | ||
getKeyStr(k) { | ||
return removeQuotes(this.visit(k[0])); | ||
} | ||
getObject(ctx) { | ||
return this.skipFakeNodesDown(ctx, 'object_literal'); | ||
} | ||
getValue(k) { | ||
return k[1]; | ||
} | ||
getKeyValueList(ctx) { | ||
if ('dictorsetmaker' in ctx && ctx.dictorsetmaker()) { | ||
const properties = ctx.dictorsetmaker().test(); | ||
return properties | ||
.map((key, i) => { | ||
if (i % 2 === 0) { | ||
return [ | ||
key, | ||
properties[i + 1] | ||
]; | ||
} | ||
return null; | ||
}) | ||
.filter((k) => (k !== null)); | ||
isSubObject(ctx) { | ||
return this.getParentUntil(ctx.parentCtx, 'dictorsetmaker', 1); | ||
} | ||
return []; | ||
} | ||
getKeyStr(k) { | ||
return removeQuotes(this.visit(k[0])); | ||
} | ||
getParentKeyStr(ctx) { | ||
// TODO: fix for long list | ||
// For a given sub document, get its key. | ||
const topNode = this.getParentUntil(ctx.parentCtx, 'dictorsetmaker', 1); | ||
const objNode = topNode.parentCtx; | ||
const index = objNode.test().indexOf(topNode); | ||
const keyNode = objNode.test()[index - 1]; | ||
const key = this.visit(keyNode); | ||
return removeQuotes(key); | ||
} | ||
getValue(k) { | ||
return k[1]; | ||
} | ||
isSubObject(ctx) { | ||
return this.getParentUntil(ctx.parentCtx, 'dictorsetmaker', 1); | ||
} | ||
getParentKeyStr(ctx) { // TODO: fix for long list | ||
// For a given sub document, get its key. | ||
const topNode = this.getParentUntil(ctx.parentCtx, 'dictorsetmaker', 1); | ||
const objNode = topNode.parentCtx; | ||
const index = objNode.test().indexOf(topNode); | ||
const keyNode = objNode.test()[index - 1]; | ||
const key = this.visit(keyNode); | ||
return removeQuotes(key); | ||
} | ||
getObjectChild(ctx) { | ||
return this.skipFakeNodesDown(ctx); | ||
} | ||
}; | ||
getObjectChild(ctx) { | ||
return this.skipFakeNodesDown(ctx); | ||
} | ||
}; |
@@ -5,6 +5,7 @@ 'use strict'; | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; |
@@ -5,6 +5,7 @@ 'use strict'; | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; |
@@ -5,6 +5,7 @@ 'use strict'; | ||
*/ | ||
module.exports = (Visitor) => class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; | ||
module.exports = (Visitor) => | ||
class Generator extends Visitor { | ||
constructor() { | ||
super(); | ||
} | ||
}; |
@@ -7,3 +7,3 @@ 'use strict'; | ||
BsonTranspilersRuntimeError, | ||
BsonTranspilersUnimplementedError | ||
BsonTranspilersUnimplementedError, | ||
} = require('../../helper/error'); | ||
@@ -19,134 +19,138 @@ | ||
*/ | ||
module.exports = (JavascriptVisitor) => class Visitor extends JavascriptVisitor { | ||
constructor() { | ||
super(); | ||
} | ||
module.exports = (JavascriptVisitor) => | ||
class Visitor extends JavascriptVisitor { | ||
constructor() { | ||
super(); | ||
} | ||
processNumberLong(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
executeJavascript(input) { | ||
const sandbox = { | ||
RegExp: RegExp, | ||
DBRef: bson.DBRef, | ||
Map: bson.Map, | ||
MaxKey: bson.MaxKey, | ||
MinKey: bson.MinKey, | ||
ObjectId: bson.ObjectId, | ||
BSONSymbol: bson.BSONSymbol, | ||
Timestamp: bson.Timestamp, | ||
Decimal128: bson.Decimal128, | ||
Long: bson.Long, | ||
Int32: bson.Int32, | ||
Double: bson.Double, | ||
Code: function(c, s) { | ||
return new bson.Code(c, s); | ||
}, | ||
NumberDecimal: function(s) { | ||
if (s === undefined) { | ||
s = '0'; | ||
} | ||
return bson.Decimal128.fromString(s.toString()); | ||
}, | ||
NumberInt: function(s) { | ||
return parseInt(s, 10); | ||
}, | ||
NumberLong: function(v) { | ||
if (v === undefined) { | ||
v = 0; | ||
} | ||
return bson.Long.fromNumber(v); | ||
}, | ||
ISODate: function(s) { | ||
return new Date(s); | ||
}, | ||
Date: function(s) { | ||
const args = Array.from(arguments); | ||
if (args.length === 1) { | ||
processNumberLong(ctx) { | ||
return this.generateNumericClass(ctx); | ||
} | ||
executeJavascript(input) { | ||
const sandbox = { | ||
RegExp: RegExp, | ||
DBRef: bson.DBRef, | ||
Map: bson.Map, | ||
MaxKey: bson.MaxKey, | ||
MinKey: bson.MinKey, | ||
ObjectId: bson.ObjectId, | ||
BSONSymbol: bson.BSONSymbol, | ||
Timestamp: bson.Timestamp, | ||
Decimal128: bson.Decimal128, | ||
Long: bson.Long, | ||
Int32: bson.Int32, | ||
Double: bson.Double, | ||
Code: function (c, s) { | ||
return new bson.Code(c, s); | ||
}, | ||
NumberDecimal: function (s) { | ||
if (s === undefined) { | ||
s = '0'; | ||
} | ||
return bson.Decimal128.fromString(s.toString()); | ||
}, | ||
NumberInt: function (s) { | ||
return parseInt(s, 10); | ||
}, | ||
NumberLong: function (v) { | ||
if (v === undefined) { | ||
v = 0; | ||
} | ||
return bson.Long.fromNumber(v); | ||
}, | ||
ISODate: function (s) { | ||
return new Date(s); | ||
} | ||
}, | ||
Date: function (s) { | ||
const args = Array.from(arguments); | ||
return new Date(Date.UTC(...args)); | ||
}, | ||
Buffer: Buffer, | ||
__result: {} | ||
}; | ||
const res = vm.runInContext('__result = ' + input, vm.createContext(sandbox)); | ||
return res; | ||
} | ||
if (args.length === 1) { | ||
return new Date(s); | ||
} | ||
/** | ||
* BinData needs extra processing because we need to check that the arg is | ||
* valid base64. | ||
* | ||
* TODO: figure out if it ever makes sense to support Binary. | ||
*/ | ||
processBinData() { | ||
throw new BsonTranspilersUnimplementedError('BinData type not supported'); | ||
} | ||
return new Date(Date.UTC(...args)); | ||
}, | ||
Buffer: Buffer, | ||
__result: {}, | ||
}; | ||
const res = vm.runInContext( | ||
'__result = ' + input, | ||
vm.createContext(sandbox) | ||
); | ||
return res; | ||
} | ||
/** | ||
* Needs preprocessing because must be executed in javascript. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processNumberDecimal(ctx) { | ||
ctx.type = this.Types.NumberDecimal; | ||
const symbolType = this.Symbols.NumberDecimal; | ||
let decstr; | ||
try { | ||
decstr = this.executeJavascript(`new ${ctx.getText()}`).toString(); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
/** | ||
* BinData needs extra processing because we need to check that the arg is | ||
* valid base64. | ||
* | ||
* TODO: figure out if it ever makes sense to support Binary. | ||
*/ | ||
processBinData() { | ||
throw new BsonTranspilersUnimplementedError('BinData type not supported'); | ||
} | ||
if ('emitNumberDecimal' in this) { | ||
return this.emitNumberDecimal(ctx, decstr); | ||
/** | ||
* Needs preprocessing because must be executed in javascript. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processNumberDecimal(ctx) { | ||
ctx.type = this.Types.NumberDecimal; | ||
const symbolType = this.Symbols.NumberDecimal; | ||
let decstr; | ||
try { | ||
decstr = this.executeJavascript(`new ${ctx.getText()}`).toString(); | ||
} catch (error) { | ||
throw new BsonTranspilersRuntimeError(error.message); | ||
} | ||
if ('emitNumberDecimal' in this) { | ||
return this.emitNumberDecimal(ctx, decstr); | ||
} | ||
const lhs = symbolType.template | ||
? symbolType.template() | ||
: this.returnFunctionCallLhs(symbolType.code, 'NumberDecimal'); | ||
const res = this.returnFunctionCallLhsRhs(lhs, [decstr], symbolType, lhs); | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(res, false, ctx.type.code) | ||
: this.returnFunctionCallLhsRhs(lhs, [decstr], symbolType, lhs); | ||
} | ||
const lhs = symbolType.template | ||
? symbolType.template() | ||
: this.returnFunctionCallLhs(symbolType.code, 'NumberDecimal'); | ||
const res = this.returnFunctionCallLhsRhs(lhs, [decstr], symbolType, lhs); | ||
/** | ||
* Needs preprocessing because ISODate is treated exactly like Date, but always | ||
* is invoked as an object. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processISODate(ctx) { | ||
ctx.wasNew = true; | ||
return this.processDate(ctx); | ||
} | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(res, false, ctx.type.code) | ||
: this.returnFunctionCallLhsRhs(lhs, [decstr], symbolType, lhs); | ||
} | ||
/** | ||
* Also accepts no arguments. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processCode(ctx) { | ||
ctx.type = this.Types.Code; | ||
const symbolType = this.Symbols.Code; | ||
const lhs = symbolType.template | ||
? symbolType.template() | ||
: this.returnFunctionCallLhs(symbolType.code, 'Code'); | ||
/** | ||
* Needs preprocessing because ISODate is treated exactly like Date, but always | ||
* is invoked as an object. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processISODate(ctx) { | ||
ctx.wasNew = true; | ||
return this.processDate(ctx); | ||
} | ||
if (this.getArguments(ctx).length === 0) { | ||
const code = this.returnFunctionCallLhsRhs(lhs, [], symbolType, lhs); | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(code, false, ctx.type.code) | ||
: code; | ||
} | ||
/** | ||
* Also accepts no arguments. | ||
* | ||
* @param {FuncCallExpressionContext} ctx | ||
* @return {String} | ||
*/ | ||
processCode(ctx) { | ||
ctx.type = this.Types.Code; | ||
const symbolType = this.Symbols.Code; | ||
const lhs = symbolType.template | ||
? symbolType.template() | ||
: this.returnFunctionCallLhs(symbolType.code, 'Code'); | ||
if (this.getArguments(ctx).length === 0) { | ||
const code = this.returnFunctionCallLhsRhs(lhs, [], symbolType, lhs); | ||
return this.Syntax.new.template | ||
? this.Syntax.new.template(code, false, ctx.type.code) | ||
: code; | ||
return this.generateBSONCode(ctx, ctx.type, symbolType, true); | ||
} | ||
return this.generateBSONCode(ctx, ctx.type, symbolType, true); | ||
} | ||
}; | ||
}; |
@@ -48,3 +48,3 @@ 'use strict'; | ||
path.join(inputLang, 'types.yaml'), | ||
path.join(inputLang, 'symbols.yaml') | ||
path.join(inputLang, 'symbols.yaml'), | ||
]; | ||
@@ -64,3 +64,14 @@ const contents = files.reduce((str, file) => { | ||
const inputLangs = ['shell']; | ||
const outputLangs = ['java', 'shell', 'python', 'csharp', 'javascript', 'object', 'ruby', 'go', 'rust', 'php']; | ||
const outputLangs = [ | ||
'java', | ||
'shell', | ||
'python', | ||
'csharp', | ||
'javascript', | ||
'object', | ||
'ruby', | ||
'go', | ||
'rust', | ||
'php', | ||
]; | ||
if (!fs.existsSync(dir)) { | ||
@@ -67,0 +78,0 @@ fs.mkdirSync(dir); |
@@ -29,2 +29,2 @@ { | ||
} | ||
} | ||
} |
# Contributing to bson-transpilers | ||
## Setting up your environment | ||
The `bson-transpilers` package uses | ||
@@ -10,2 +11,3 @@ [ANTLR4](https://github.com/antlr/antlr4/blob/master/doc/javascript-target.md) | ||
Make sure you have Java installed: | ||
```shell | ||
@@ -16,7 +18,8 @@ $ brew cask install java | ||
_I strongly suggest using an IDE that will help you visualize ANTLR trees (JetBrains has a good plugin). | ||
Otherwise you can use the java version of the grammar and compile it with | ||
`javac <Language>*.java && grun <Language> <StartRule> -gui`. | ||
[This might be helpful](https://github.com/antlr/antlr4/blob/master/doc/getting-started.md)._ | ||
Otherwise you can use the java version of the grammar and compile it with | ||
`javac <Language>*.java && grun <Language> <StartRule> -gui`. | ||
[This might be helpful](https://github.com/antlr/antlr4/blob/master/doc/getting-started.md)._ | ||
Make sure you have run the following from the root directory: | ||
```shell | ||
@@ -27,2 +30,3 @@ $ npm run bootstrap | ||
Then compile and run tests locally with: | ||
```shell | ||
@@ -34,6 +38,7 @@ $ npm run compile && npm run test | ||
output and input languages. If none are provided, everything will run. | ||
- __INPUT=:__ comma-separated input languages you want to test | ||
- __OUTPUT=:__ comma-separated output languages you want to test. Also called "target" language. | ||
- __MODE=:__ comma-separated names of the test files (without .yaml) that you want to run | ||
- **INPUT=:** comma-separated input languages you want to test | ||
- **OUTPUT=:** comma-separated output languages you want to test. Also called "target" language. | ||
- **MODE=:** comma-separated names of the test files (without .yaml) that you want to run | ||
```shell | ||
@@ -46,3 +51,5 @@ OUTPUT=csharp INPUT=shell MODE=native,bson npm run test | ||
See also the original presentation: https://drive.google.com/file/d/1jvwtR3k9oBUzIjL4z_VtpHvdWahfcjTK/view | ||
## Compilation Stages | ||
Similar to how many transpilers work, this package parses the input | ||
@@ -53,2 +60,3 @@ string into a tree and then generates code from the tree using the [Visitor | ||
### Step 1: Parsing | ||
Parsing and tree generation is handled by ANTLR4. | ||
@@ -69,4 +77,4 @@ The grammar files are located in the `grammars` folder, and the javascript | ||
### Step 2: Visitor | ||
### Step 2: Visitor | ||
<img alt="visiting-tree" width="60%" align="right" src="/img-docs/visiting-tree.png"/> | ||
@@ -101,4 +109,4 @@ | ||
### Step 3: Generator | ||
### Step 3: Generator | ||
The other half of the tree visitation stage. Each ouput language will | ||
@@ -109,7 +117,8 @@ have a Generator class defined in `codegeneration/<ouput language>/Generator.js`. | ||
So for example, translating between JS and Python, the order of inheritance will be: | ||
1. `lib/antlr/ECMAScriptVisitor.js` ["empty" superclass, specific to the tree built by ANTLR] | ||
2. `codegeneration/CodeGenerationVisitor.js` ["generic" visitor, shared between all languages] | ||
3. `codegeneration/javascript/Visitor.js` [specific to input language] | ||
4. `codegeneration/python/Generator.js` [specific to output language] | ||
1. `lib/antlr/ECMAScriptVisitor.js` ["empty" superclass, specific to the tree built by ANTLR] | ||
2. `codegeneration/CodeGenerationVisitor.js` ["generic" visitor, shared between all languages] | ||
3. `codegeneration/javascript/Visitor.js` [specific to input language] | ||
4. `codegeneration/python/Generator.js` [specific to output language] | ||
For nodes that cannot be translated using | ||
@@ -127,2 +136,3 @@ templates, the Generator class will define a method called `emit<type>` which | ||
### Symbols | ||
Each input language has it's own set of symbols that are part of the | ||
@@ -136,2 +146,3 @@ language. The majority of symbols supported in the input languages are BSON types | ||
#### Symbol Metadata | ||
The visitor uses the symbol table to determine if a symbol is undefined, but the | ||
@@ -142,3 +153,2 @@ symbol table also stores some metadata so the visitor can do type and other validity checks. The symbols | ||
```yml | ||
@@ -164,16 +174,17 @@ Decimal128: | ||
``` | ||
<br> | ||
| Field | Data | | ||
| ----------|:-------------| | ||
| id | The name of the attribute. Mostly used for error reporting and the `emit` or `process` method names. | | ||
| callable | Used for determining if the symbol can be part of a function call. There are three types of symbols: <br><br>`*func`: a function call. If the symbol is found as the "left-hand-side" of a function call, it is valid. <br><br>`*constructor`: also a function call, but may require a `new` keyword if the output language requires it. <br><br>`*var`: a variable. Indicates to the transpiler that the symbol cannot be invoked, i.e. `<symbol>()` is invalid.| | ||
| args | Used for type checking. If the symbol is callable, i.e. a `*func` or `*constructor`, then the expected arguments are defined here as an array. So for example, if the function takes in a string as the first arg, and an optional second arg that can be a object or array, args will look like <br>`[ [*StringType], [*ObjectType, *ArrayType, null] ]`. Null indicates optional.| | ||
| type | The type that the symbol evaluates to. If it is a variable, it will be the type of the variable. If it is a function, it will be the return type. See the [Types](#types) section.| | ||
| attr | Used for determining if attribute access is valid. This field is also a symbol table, but a namespace-prefixed one. So for the example above, `Decimal128.fromString` is a valid attribute, so we need to define the symbol `fromString` in the same way we defined the `Decimal128` symbol.| | ||
| template | Used for code generation. See the [Templates](#templates) section.| | ||
| argsTemplate | Used for code generation. See the [Templates](#templates) section.| | ||
| Field | Data | | ||
| ------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| id | The name of the attribute. Mostly used for error reporting and the `emit` or `process` method names. | | ||
| callable | Used for determining if the symbol can be part of a function call. There are three types of symbols: <br><br>`*func`: a function call. If the symbol is found as the "left-hand-side" of a function call, it is valid. <br><br>`*constructor`: also a function call, but may require a `new` keyword if the output language requires it. <br><br>`*var`: a variable. Indicates to the transpiler that the symbol cannot be invoked, i.e. `<symbol>()` is invalid. | | ||
| args | Used for type checking. If the symbol is callable, i.e. a `*func` or `*constructor`, then the expected arguments are defined here as an array. So for example, if the function takes in a string as the first arg, and an optional second arg that can be a object or array, args will look like <br>`[ [*StringType], [*ObjectType, *ArrayType, null] ]`. Null indicates optional. | | ||
| type | The type that the symbol evaluates to. If it is a variable, it will be the type of the variable. If it is a function, it will be the return type. See the [Types](#types) section. | | ||
| attr | Used for determining if attribute access is valid. This field is also a symbol table, but a namespace-prefixed one. So for the example above, `Decimal128.fromString` is a valid attribute, so we need to define the symbol `fromString` in the same way we defined the `Decimal128` symbol. | | ||
| template | Used for code generation. See the [Templates](#templates) section. | | ||
| argsTemplate | Used for code generation. See the [Templates](#templates) section. | | ||
### Types | ||
### Types | ||
Each input language also has a set of types that are part of the language. | ||
@@ -184,4 +195,3 @@ The set of types that are universal for all languages (i.e. "primitives", | ||
Types that are specific to the input language are defined in `symbols/<input | ||
language>/types.yaml`. These include BSON types, i.e. classes like `ObjectId`, and | ||
Types that are specific to the input language are defined in `symbols/<input language>/types.yaml`. These include BSON types, i.e. classes like `ObjectId`, and | ||
language-specific types like `RegExp` and `Date`. The types are defined in the same | ||
@@ -195,3 +205,3 @@ pattern as the symbols and contain the same metadata as the symbols. | ||
and is a constructor, so `ObjectId()` is valid. The **type** `ObjectId` has | ||
attributes like `ObjectId().toString()` and is *a variable*, so `ObjectId()()` | ||
attributes like `ObjectId().toString()` and is _a variable_, so `ObjectId()()` | ||
is not valid and will error with `ObjectId() is not callable` or similar error. | ||
@@ -203,2 +213,3 @@ You can kind of think of types as instantiated symbols, if that's helpful. | ||
## Templates | ||
The symbol table includes an additional piece of metadata, called a `template`. | ||
@@ -216,2 +227,3 @@ These are functions that accept strings and return strings, and are responsible for | ||
## Project Structure | ||
<img alt="indexjs" width="60%" align="right" src="/img-docs/indexjs.jpg"/> | ||
@@ -225,2 +237,3 @@ | ||
To construct a transpiler, `index.js` needs 4 components: | ||
- `lib/antlr/<ANTLR tree visitor` The ANTLR-generated visitor for the generated parse tree. | ||
@@ -231,13 +244,13 @@ - `codegeneration/CodeGenerationVisitor.js` The "generic" visitor for all input languages. | ||
- `lib/symbol-table/<input language>to<ouput language>.js` - The symbol table for | ||
the input+output combination. | ||
the input+output combination. | ||
#### TL;DR | ||
- __CodeGenerationVisitor:__ place to store repeated code between all visitors. | ||
- __Visitor:__ visits nodes; processes input language via | ||
`processs` methods and sends information to either output language's template found in the `Symbol | ||
Template` or an `emit` method in the Generator`. | ||
- __Generator:__ processes output language via `emit` methods, which take in tree nodes and return strings. | ||
- __Symbol Table:__ a directory of the defined symbols, types and their metadata, including templates. | ||
- __Symbol Template:__ does string manipulation to provide output. | ||
- **CodeGenerationVisitor:** place to store repeated code between all visitors. | ||
- **Visitor:** visits nodes; processes input language via | ||
`processs` methods and sends information to either output language's template found in the `Symbol Template` or an `emit` method in the Generator`. | ||
- **Generator:** processes output language via `emit` methods, which take in tree nodes and return strings. | ||
- **Symbol Table:** a directory of the defined symbols, types and their metadata, including templates. | ||
- **Symbol Template:** does string manipulation to provide output. | ||
The class hierarchy of any transpiler is | ||
@@ -247,12 +260,14 @@ ANTLRVisitor <-- CodeGenerationVisitor <-- [input-language-specific] Visitor <-- [output-language-specific] Generator. | ||
## A primer on the different types of methods | ||
| Method name | Summary | | ||
|--------------|-----------| | ||
| `visit<*>` | Always auto-generated by ANTLR, overridden by language-specific visitors. Controls the flow of the tree visitation. | | ||
| `get<*>` | Wrapper methods around nodes, defined by language-specific visitors. They exist because different grammars call equivalent nodes different names. | | ||
| `generate<*>` | Shared logic between all input languages. Defined in `CodeGenerationVisitor` and called from language-specific visitor methods. | | ||
| `return<*>` | Separates string generation from object generation. Defined in `CodeGenerationVisitor` or `object/Generator`. | | ||
| `process<*>` | Special case for entire input language. Defined in visitors, called programmatically within the visitor. | | ||
| `emit<*>` | Special case for entire output language. Defined in generators, called programmatically by the visitors. | | ||
| Method name | Summary | | ||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `visit<*>` | Always auto-generated by ANTLR, overridden by language-specific visitors. Controls the flow of the tree visitation. | | ||
| `get<*>` | Wrapper methods around nodes, defined by language-specific visitors. They exist because different grammars call equivalent nodes different names. | | ||
| `generate<*>` | Shared logic between all input languages. Defined in `CodeGenerationVisitor` and called from language-specific visitor methods. | | ||
| `return<*>` | Separates string generation from object generation. Defined in `CodeGenerationVisitor` or `object/Generator`. | | ||
| `process<*>` | Special case for entire input language. Defined in visitors, called programmatically within the visitor. | | ||
| `emit<*>` | Special case for entire output language. Defined in generators, called programmatically by the visitors. | | ||
### Tests | ||
Tests are written in YAML. Each yaml file requires two keys, first 'runner' which is a | ||
@@ -273,19 +288,33 @@ function that runs each individual test. The function signature is always `(it, expect, input, output, transpiler, test)` | ||
1. Create a directory in `symbols` directory for your output language: | ||
```shell | ||
mkdir symbols/<output lang> | ||
``` | ||
2. Create a `templates.yaml` file to store your language's templates. Inside | ||
you'll probably want to copy the contents from the `symbols/sample_templates.yaml` file. | ||
That file also includes comments on which template functions require unusual arguments. | ||
```shell | ||
cp symbols/sample_template.yaml symbols/<output lang>/templates.yaml | ||
``` | ||
3. Add your new language to the `compile-symbol-table.js` file: | ||
```js | ||
const outputLangs = ['java', 'shell', 'python', 'csharp', 'javascript', 'mylanguage']; | ||
const outputLangs = [ | ||
'java', | ||
'shell', | ||
'python', | ||
'csharp', | ||
'javascript', | ||
'mylanguage', | ||
]; | ||
``` | ||
4. You should now run `npm run compile` to generate a complete symbol table. | ||
This will be generated in `lib/symbol-table/javascriptto<output lang>` and | ||
`lib/symbol-table/shellto<output lang>`. | ||
`lib/symbol-table/shellto<output lang>`. | ||
5. You will have to require the generated symbol tables in `index.js`: | ||
```js | ||
@@ -309,18 +338,24 @@ const javascript<output lang>symbols = require('lib/symbol-table/javascriptto<output lang>') | ||
``` | ||
6. We still don't have a `Generator.js` file required above, so that won't | ||
quite work yet. So next, create a new directory in `codegeneration` for your | ||
output language: | ||
output language: | ||
```shell | ||
mkidr codegeneration/<output lang> | ||
``` | ||
7. And create a generator file: | ||
```shell | ||
touch codegeneration/<output lang>/Generator.js | ||
``` | ||
8. Most of the Generators are empty nowadays because the work has been moved into | ||
the templates. I would expect that 90% of the time, what you're trying to do can | ||
be done in the templates without changing the template signature. However, if you | ||
really can't do it in the templates, you can copy any of the generators that exist | ||
and fill in any emit methods you require. The class should be created with a super | ||
class that is passed to the function, like so: | ||
the templates. I would expect that 90% of the time, what you're trying to do can | ||
be done in the templates without changing the template signature. However, if you | ||
really can't do it in the templates, you can copy any of the generators that exist | ||
and fill in any emit methods you require. The class should be created with a super | ||
class that is passed to the function, like so: | ||
```js | ||
@@ -340,21 +375,26 @@ /* | ||
``` | ||
9. Next thing is tests! You must go through each test file and add the results of | ||
compiling each input into your output language under the `output` field. | ||
compiling each input into your output language under the `output` field. | ||
```yaml | ||
Document: | ||
- input: | ||
javascript: "{x: '1'}" | ||
shell: "{x: '1'}" | ||
python: "{'x': '1'}" | ||
output: | ||
javascript: "{\n 'x': '1'\n}" | ||
python: "{\n 'x': '1'\n}" | ||
java: "eq(\"x\", \"1\")" | ||
csharp: "new BsonDocument(\"x\", \"1\")" | ||
shell: "{\n 'x': '1'\n}" | ||
<your output language>: ... | ||
Document: | ||
- input: | ||
javascript: "{x: '1'}" | ||
shell: "{x: '1'}" | ||
python: "{'x': '1'}" | ||
output: | ||
javascript: "{\n 'x': '1'\n}" | ||
python: "{\n 'x': '1'\n}" | ||
java: 'eq("x", "1")' | ||
csharp: 'new BsonDocument("x", "1")' | ||
shell: "{\n 'x': '1'\n}" | ||
<your output language>: ... | ||
``` | ||
Make sure to add your output language in the outputLanguages array at the beginning | ||
of `run-yaml.test.js`, and to the list near the end of `functions.test.js`. | ||
## Adding an Input Language | ||
TODO! |
@@ -19,2 +19,4 @@ 'use strict'; | ||
https.get(downloadUrl, (response) => response.pipe(fs.createWriteStream(outputFile))); | ||
https.get(downloadUrl, (response) => | ||
response.pipe(fs.createWriteStream(outputFile)) | ||
); |
@@ -8,9 +8,7 @@ 'use strict'; | ||
*/ | ||
const doubleQuoteStringify = function(str) { | ||
const doubleQuoteStringify = function (str) { | ||
let newStr = str.toString(); | ||
if ( | ||
( | ||
newStr.charAt(0) === '\'' && newStr.charAt(newStr.length - 1) === '\'' | ||
) || | ||
(newStr.charAt(0) === "'" && newStr.charAt(newStr.length - 1) === "'") || | ||
(newStr.charAt(0) === '"' && newStr.charAt(newStr.length - 1) === '"') | ||
@@ -30,9 +28,7 @@ ) { | ||
*/ | ||
const singleQuoteStringify = function(str) { | ||
const singleQuoteStringify = function (str) { | ||
let newStr = str.toString(); | ||
if ( | ||
( | ||
newStr.charAt(0) === '\'' && newStr.charAt(newStr.length - 1) === '\'' | ||
) || | ||
(newStr.charAt(0) === "'" && newStr.charAt(newStr.length - 1) === "'") || | ||
(newStr.charAt(0) === '"' && newStr.charAt(newStr.length - 1) === '"') | ||
@@ -52,3 +48,3 @@ ) { | ||
*/ | ||
const removeQuotes = function(str) { | ||
const removeQuotes = function (str) { | ||
let newStr = str.toString(); | ||
@@ -58,3 +54,3 @@ | ||
(newStr.charAt(0) === '"' && newStr.charAt(newStr.length - 1) === '"') || | ||
(newStr.charAt(0) === '\'' && newStr.charAt(newStr.length - 1) === '\'') | ||
(newStr.charAt(0) === "'" && newStr.charAt(newStr.length - 1) === "'") | ||
) { | ||
@@ -70,3 +66,3 @@ newStr = newStr.substr(1, newStr.length - 2); | ||
singleQuoteStringify, | ||
removeQuotes | ||
removeQuotes, | ||
}; |
85
index.js
@@ -6,3 +6,4 @@ 'use strict'; | ||
const JavascriptANTLRVisitor = require('./lib/antlr/ECMAScriptVisitor').ECMAScriptVisitor; | ||
const JavascriptANTLRVisitor = | ||
require('./lib/antlr/ECMAScriptVisitor').ECMAScriptVisitor; | ||
@@ -12,3 +13,3 @@ const ErrorListener = require('./codegeneration/ErrorListener.js'); | ||
BsonTranspilersInternalError, | ||
BsonTranspilersUnimplementedError | ||
BsonTranspilersUnimplementedError, | ||
} = require('./helper/error'); | ||
@@ -73,4 +74,12 @@ | ||
* then an error should be thrown. Can be empty, but must exist. */ | ||
['BasicTypes', 'BsonTypes', 'NativeTypes', 'SymbolTypes', 'BsonTypes', | ||
'BsonSymbols', 'NativeSymbols', 'SymbolTypes'].map((k) => { | ||
[ | ||
'BasicTypes', | ||
'BsonTypes', | ||
'NativeTypes', | ||
'SymbolTypes', | ||
'BsonTypes', | ||
'BsonSymbols', | ||
'NativeSymbols', | ||
'SymbolTypes', | ||
].map((k) => { | ||
if (!(k in doc)) { | ||
@@ -88,3 +97,3 @@ throw new BsonTranspilersInternalError( | ||
Syntax: doc.Syntax, | ||
Imports: doc.Imports | ||
Imports: doc.Imports, | ||
}); | ||
@@ -95,5 +104,4 @@ | ||
const tree = loadTree(input, transpiler.startRule); | ||
transpiler.idiomatic = idiomatic === undefined ? | ||
transpiler.idiomatic : | ||
idiomatic; | ||
transpiler.idiomatic = | ||
idiomatic === undefined ? transpiler.idiomatic : idiomatic; | ||
if (!driverSyntax) { | ||
@@ -121,8 +129,13 @@ transpiler.clearImports(); | ||
Object.keys(input).map((k) => { | ||
result[k] = (k === 'options' || k === 'exportMode') ? input[k] : compile(input[k], idiomatic, true); | ||
result[k] = | ||
k === 'options' || k === 'exportMode' | ||
? input[k] | ||
: compile(input[k], idiomatic, true); | ||
}); | ||
if (!('options' in result) || | ||
!('uri' in result.options) || | ||
!('database' in result.options) || | ||
!('collection' in result.options)) { | ||
if ( | ||
!('options' in result) || | ||
!('uri' in result.options) || | ||
!('database' in result.options) || | ||
!('collection' in result.options) | ||
) { | ||
throw new BsonTranspilersInternalError( | ||
@@ -134,3 +147,3 @@ 'Missing required metadata to generate drivers syntax' | ||
throw new BsonTranspilersInternalError( | ||
'Need to pass \'aggregation\' or \'filter\' when compiling with driver syntax' | ||
"Need to pass 'aggregation' or 'filter' when compiling with driver syntax" | ||
); | ||
@@ -148,3 +161,3 @@ } | ||
return transpiler.getImports(mode, driverSyntax); | ||
} | ||
}, | ||
}; | ||
@@ -157,3 +170,5 @@ }; | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getJavaGenerator, | ||
@@ -164,3 +179,5 @@ shelljavasymbols | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getPythonGenerator, | ||
@@ -171,3 +188,5 @@ shellpythonsymbols | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getCsharpGenerator, | ||
@@ -178,3 +197,5 @@ shellcsharpsymbols | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getJavascriptGenerator, | ||
@@ -185,3 +206,5 @@ shelljavascriptsymbols | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getObjectGenerator, | ||
@@ -192,3 +215,5 @@ shellobjectsymbols | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getRubyGenerator, | ||
@@ -199,3 +224,5 @@ shellrubysymbols | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getGoGenerator, | ||
@@ -206,3 +233,5 @@ shellgosymbols | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getRustGenerator, | ||
@@ -213,10 +242,12 @@ shellrustsymbols | ||
loadJSTree, | ||
getShellVisitor(getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor))), | ||
getShellVisitor( | ||
getJavascriptVisitor(getCodeGenerationVisitor(JavascriptANTLRVisitor)) | ||
), | ||
getPhpGenerator, | ||
shellphpsymbols | ||
) | ||
), | ||
}, | ||
getTree: { | ||
shell: loadJSTree | ||
} | ||
shell: loadJSTree, | ||
}, | ||
}; |
{ | ||
"name": "bson-transpilers", | ||
"version": "0.0.0-next-0df16eac622fa26bc26ea5bc44d4af849a08e128", | ||
"version": "0.0.0-next-1037badfa5c12b52a8fa581424cf7e01654f809d", | ||
"apiVersion": "0.0.1", | ||
@@ -35,3 +35,3 @@ "description": "Source to source compilers using ANTLR", | ||
"devDependencies": { | ||
"@mongodb-js/eslint-config-compass": "0.0.0-next-0df16eac622fa26bc26ea5bc44d4af849a08e128", | ||
"@mongodb-js/eslint-config-compass": "0.0.0-next-1037badfa5c12b52a8fa581424cf7e01654f809d", | ||
"chai": "^4.3.4", | ||
@@ -51,3 +51,3 @@ "depcheck": "^1.4.1", | ||
}, | ||
"gitHead": "0df16eac622fa26bc26ea5bc44d4af849a08e128" | ||
"gitHead": "1037badfa5c12b52a8fa581424cf7e01654f809d" | ||
} |
118
README.md
# BSON-Transpilers | ||
[![npm version][1]][2] [![build status][3]][4] | ||
@@ -11,2 +12,3 @@ [![downloads][5]][6] | ||
**NOTES:** | ||
1. A version of the code with support for `python` with corresponding test cases has been removed to avoid bundling and supporting unused code, however it can be still be found in https://github.com/mongodb-js/compass/tree/80cf701e44cd966207f956fac69e8233861b1cd5/packages/bson-transpilers. | ||
@@ -23,3 +25,3 @@ 2. The `shell` output is disabled as is essentially the only input in use and it produces code that is compatible only with legacy `mongo` shell not the new `mongosh` shell. See [COMPASS-4930](https://jira.mongodb.org/browse/COMPASS-4930) for some additional context. | ||
const string =` | ||
const string = ` | ||
{ item: "book", qty: Int32(10), tags: ["red", "blank"], dim_cm: [14, Int32("81")] }`; | ||
@@ -39,50 +41,56 @@ | ||
## API | ||
### compiledString = transpiler\[inputLang\]\[outputLang\].compile(codeString) | ||
Output a compiled string given input and output languages. | ||
- __inputLang:__ Input language of the code string. `shell` and `javascript` | ||
- **inputLang:** Input language of the code string. `shell` and `javascript` | ||
are currently supported. | ||
- __outputLang:__ The language you would like the output to be. `java`, | ||
- **outputLang:** The language you would like the output to be. `java`, | ||
`python`, `shell`, `javascript`, and `csharp` are currently supported. | ||
- __codeString:__ The code string you would like to be compiled to your | ||
- **codeString:** The code string you would like to be compiled to your | ||
selected output language. | ||
### importsString = transpiler\[inputLang\]\[outputLang\].getImports(mode, driverSyntax) | ||
Output a string containing the set of import statements for the generated code | ||
to compile. These are all the packages that the compiled code could use so that | ||
the transpiler output will be runnable. | ||
- __inputLang:__ Input language of the code string. `shell` and `javascript` | ||
- **inputLang:** Input language of the code string. `shell` and `javascript` | ||
are currently supported. | ||
- __outputLang:__ The language you would like the output to be. `java`, | ||
- **outputLang:** The language you would like the output to be. `java`, | ||
`python`, `shell`, `javascript`, and `csharp` are currently supported. | ||
- __mode:__ Either 'Query' for the `.find()` method or 'Pipeline' for `.aggregate()`. | ||
- __driverSyntax:__ Whether or not you want to include Driver Syntax into your output string. | ||
- **mode:** Either 'Query' for the `.find()` method or 'Pipeline' for `.aggregate()`. | ||
- **driverSyntax:** Whether or not you want to include Driver Syntax into your output string. | ||
### catch (error) | ||
Any transpiler errors that occur will be thrown. To catch them, wrap the | ||
`transpiler` in a `try/catch` block. | ||
- __error.message:__ Message `bson-transpilers` will send back letting you know | ||
- **error.message:** Message `bson-transpilers` will send back letting you know | ||
the transpiler error. | ||
- __error.stack:__ The usual error stacktrace. | ||
- __error.code:__ [Error code]() that `bson-transpilers` adds to the error object to | ||
- **error.stack:** The usual error stacktrace. | ||
- **error.code:** [Error code]() that `bson-transpilers` adds to the error object to | ||
help you distinguish error types. | ||
- __error.line:__ If it is a syntax error, will have the line. | ||
- __error.column:__ If it is a syntax error, will have the column. | ||
- __error.symbol:__ If it is a syntax error, will have the symbol associated with the error. | ||
- **error.line:** If it is a syntax error, will have the line. | ||
- **error.column:** If it is a syntax error, will have the column. | ||
- **error.symbol:** If it is a syntax error, will have the symbol associated with the error. | ||
### State | ||
The `CodeGenerationVisitor` class manages a global state which is bound to the `argsTemplate` functions. This state is intended to be used as a solution for the `argsTemplate` functions to communicate with the `DriverTemplate` function. For example: | ||
The `CodeGenerationVisitor` class manages a global state which is bound to the `argsTemplate` functions. This state is intended to be used as a solution for the `argsTemplate` functions to communicate with the `DriverTemplate` function. For example: | ||
```yaml | ||
ObjectIdEqualsArgsTemplate: &ObjectIdEqualsArgsTemplate !!js/function > | ||
(_) => { | ||
this.oneLineStatement = "Hello World"; | ||
return ''; | ||
} | ||
ObjectIdEqualsArgsTemplate: !!js/function &ObjectIdEqualsArgsTemplate > | ||
(_) => { | ||
this.oneLineStatement = "Hello World"; | ||
return ''; | ||
} | ||
DriverTemplate: &DriverTemplate !!js/function > | ||
(_spec) => { | ||
return this.oneLineStatement; | ||
} | ||
DriverTemplate: !!js/function &DriverTemplate > | ||
(_spec) => { | ||
return this.oneLineStatement; | ||
} | ||
``` | ||
@@ -93,10 +101,11 @@ | ||
#### DeclarationStore | ||
A more practical use-case of state is to accumulate variable declarations throughout the `argsTemplate` to be rendered by the `DriverTemplate`. That is, the motivation for using `DeclarationStore` is to prepend the driver syntax with variable declarations rather than using non-idiomatic solutions such as closures. | ||
The `DeclarationStore` class maintains an internal state concerning variable declarations. For example, | ||
A more practical use-case of state is to accumulate variable declarations throughout the `argsTemplate` to be rendered by the `DriverTemplate`. That is, the motivation for using `DeclarationStore` is to prepend the driver syntax with variable declarations rather than using non-idiomatic solutions such as closures. | ||
The `DeclarationStore` class maintains an internal state concerning variable declarations. For example, | ||
```javascript | ||
// within the args template | ||
(arg) => { | ||
return this.declarations.add("Temp", "objectID", (varName) => { | ||
return this.declarations.add('Temp', 'objectID', (varName) => { | ||
return [ | ||
@@ -106,9 +115,9 @@ `${varName}, err := primitive.ObjectIDFromHex(${arg})`, | ||
' log.Fatal(err)', | ||
'}' | ||
].join('\n') | ||
}) | ||
} | ||
'}', | ||
].join('\n'); | ||
}); | ||
}; | ||
``` | ||
Note that each use of the same variable name will result in an increment being added to the declaration statement. For example, if the variable name `objectIDForTemp` is used two times the resulting declaration statements will use `objectIDForTemp` for the first declaration and `objectID2ForTemp` for the second declaration. The `add` method returns the incremented variable name, and is therefore what would be expected as the right-hand side of the statement defined by the `argsTemplate` function. | ||
Note that each use of the same variable name will result in an increment being added to the declaration statement. For example, if the variable name `objectIDForTemp` is used two times the resulting declaration statements will use `objectIDForTemp` for the first declaration and `objectID2ForTemp` for the second declaration. The `add` method returns the incremented variable name, and is therefore what would be expected as the right-hand side of the statement defined by the `argsTemplate` function. | ||
@@ -119,9 +128,11 @@ The instance of the `DeclarationStore` constructed by the transpiler class is passed into the driver, syntax via state, for use: | ||
(spec) => { | ||
const comment = '// some comment' | ||
const client = 'client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(cs.String()))' | ||
return "#{comment}\n\n#{client}\n\n${this.declarations.toString()}" | ||
} | ||
const comment = '// some comment'; | ||
const client = | ||
'client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(cs.String()))'; | ||
return '#{comment}\n\n#{client}\n\n${this.declarations.toString()}'; | ||
}; | ||
``` | ||
### Errors | ||
There are a few different error classes thrown by `bson-transpilers`, each with | ||
@@ -131,3 +142,5 @@ their own error code: | ||
#### BsonTranspilersArgumentError | ||
###### code: E_BSONTRANSPILERS_ARGUMENT | ||
This will occur when you're using a method with a wrong number of arguments, or | ||
@@ -155,3 +168,5 @@ the arguments are of the wrong type. | ||
#### BsonTranspilersAttributeError | ||
###### code: E_BSONTRANSPILERS_ATTRIBUTE | ||
Will be thrown if an invalid method or property is used on a BSON object. For | ||
@@ -163,10 +178,12 @@ example, since `new DBRef()` doesn't have a method `.foo()`, transpiler will | ||
// ✘: method foo doesn't exist, so this will throw a BsonTranspilersAttributeError . | ||
new DBRef('newCollection', new ObjectId()).foo() | ||
new DBRef('newCollection', new ObjectId()).foo(); | ||
// ✔: this won't throw, since .toString() method exists | ||
new DBRef('newCollection', new ObjectId()).toString(10) | ||
new DBRef('newCollection', new ObjectId()).toString(10); | ||
``` | ||
#### BsonTranspilersSyntaxError | ||
###### code: E_BSONTRANSPILERS_SYNTAX | ||
This will throw if you have a syntax error. For example missing a colon in | ||
@@ -188,3 +205,5 @@ Object assignment, or forgetting a comma in array definition: | ||
#### BsonTranspilersTypeError | ||
###### code: E_BSONTRANSPILERS_TYPE | ||
This error will occur if a symbol is treated as the wrong type. For example, if | ||
@@ -195,9 +214,12 @@ a non-function is called: | ||
// ✘: MAX_VALUE is a constant, not a function | ||
Long.MAX_VALUE() | ||
Long.MAX_VALUE(); | ||
// ✔: MAX_VALUE without a call will not throw | ||
Long.MAX_VALUE | ||
Long.MAX_VALUE; | ||
``` | ||
#### BsonTranspilersUnimplementedError | ||
###### code: E_BSONTRANSPILERS_UNIMPLEMENTED | ||
If there is a feature in the input code that is not currently supported by the | ||
@@ -207,3 +229,5 @@ transpiler. | ||
#### BsonTranspilersRuntimeError | ||
###### code: E_BSONTRANSPILERS_RUNTIME | ||
A generic runtime error will be thrown for all errors that are not covered by the | ||
@@ -215,10 +239,12 @@ above list of errors. These are usually constructor requirements, for example | ||
// ✘: these are not proper 'RegExp()' flags, a BsonTranspilersRuntimeError will be thrown. | ||
new RegExp('ab+c', 'beep') | ||
new RegExp('ab+c', 'beep'); | ||
// ✔: 'im' are proper 'RegExp()' flags | ||
new RegExp('ab+c', 'im') | ||
new RegExp('ab+c', 'im'); | ||
``` | ||
#### BsonTranspilersInternalError | ||
###### code: E_BSONTRANSPILERS_INTERNAL | ||
In the case where something has gone wrong within compilation, and an error has | ||
@@ -228,2 +254,3 @@ occured. If you see this error, please create [an issue](https://github.com/mongodb-js/bson-transpilers/issues) on Github! | ||
# Install | ||
```shell | ||
@@ -234,2 +261,3 @@ npm install -S bson-transpilers | ||
## Contributing | ||
Head over to the readme [on contributing](./CONTRIBUTING.md) to find out more | ||
@@ -239,2 +267,3 @@ information on project structure and setting up your environment. | ||
# Authors | ||
- [aherlihy](https://github.com/aherlihy) - Anna Herlihy <herlihyap@gmail.com> | ||
@@ -245,4 +274,5 @@ - [alenakhineika](https://github.com/alenakhineika) - Alena Khineika <alena.khineika@mongodb.com> | ||
# License | ||
[Apache-2.0](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) | ||
[Apache-2.0](<https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)>) | ||
[1]: https://img.shields.io/npm/v/bson-transpilers.svg?style=flat-square | ||
@@ -249,0 +279,0 @@ [2]: https://npmjs.org/package/bson-transpilers |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1391100
49
16297
267