decaffeinate-parser
Advanced tools
Comparing version 1.0.2 to 1.1.0
{ | ||
"name": "decaffeinate-parser", | ||
"version": "1.0.2", | ||
"version": "1.1.0", | ||
"description": "A better AST for CoffeeScript, inspired by CoffeeScriptRedux.", | ||
"main": "lib/parser.js", | ||
"jsnext:main": "src/parser.js", | ||
"main": "dist/decaffeinate-parser.umd.js", | ||
"jsnext:main": "dist/decaffeinate-parser.es6.js", | ||
"scripts": { | ||
"prepublish": "babel --stage 0 src -d lib", | ||
"test": "mocha --compilers js:babel/register" | ||
"build": "rollup -c rollup.config.es6.js && rollup -c rollup.config.umd.js", | ||
"prepublish": "npm run build", | ||
"test": "mocha --compilers js:babel-register" | ||
}, | ||
@@ -26,5 +27,11 @@ "keywords": [ | ||
"devDependencies": { | ||
"babel": "^5.8.23", | ||
"babel": "^6.3.26", | ||
"babel-preset-es2015": "^6.3.13", | ||
"babel-preset-es2015-rollup": "^1.1.1", | ||
"babel-register": "^6.4.3", | ||
"binary-search": "^1.2.0", | ||
"coffee-script-redux": "^2.0.0-beta8", | ||
"mocha": "^2.3.3", | ||
"mocha": "^2.3.4", | ||
"rollup": "^0.25.0", | ||
"rollup-plugin-babel": "^2.3.9", | ||
"string-repeat": "^1.1.1" | ||
@@ -35,2 +42,2 @@ }, | ||
} | ||
} | ||
} |
@@ -16,4 +16,8 @@ # decaffeinate-parser | ||
* Single-line functions, `if` statements, etc. have blocks for bodies. | ||
* String interpolation nodes are modeled after `TemplateLiteral` from ES6 rather | ||
than being a series of nested `ConcatOp` nodes. | ||
* Triple-quoted strings have the node type `Herestring` rather than `String`. | ||
* Virtual nodes (such as the `LogicalNotOp` generated by an `unless`) are | ||
marked as such with a `virtual: true` property. | ||
* `do` is handled with the `DoOp` node type. | ||
* `for-in` loops do not have an implicit `step` property. | ||
@@ -20,0 +24,0 @@ * Ranges of programs with indented blocks are correct. |
1317
src/parser.js
@@ -0,10 +1,14 @@ | ||
import * as CoffeeScript from 'coffee-script'; | ||
import ParseContext from './util/ParseContext'; | ||
import isChainedComparison from './util/isChainedComparison'; | ||
import lineColumnMapper from './util/lineColumnMapper'; | ||
import isInterpolatedString from './util/isInterpolatedString'; | ||
import locationsEqual from './util/locationsEqual'; | ||
import parseLiteral from './util/parseLiteral'; | ||
import trimNonMatchingParentheses from './util/trimNonMatchingParentheses'; | ||
import type from './util/type'; | ||
import { nodes as csParse } from 'coffee-script'; | ||
import { patchCoffeeScript } from './ext/coffee-script'; | ||
/** | ||
* @param {string} source | ||
* @param {{coffeeScriptParser: function(string): Object}} options | ||
* @param {{coffeeScript: {nodes: function(string): Object, tokens: function(string): Array}}} options | ||
* @returns {Program} | ||
@@ -24,4 +28,5 @@ */ | ||
const parseCoffeeScript = options.coffeeScriptParser || csParse; | ||
return /** @type Program */ convert(parseCoffeeScript(source), source, lineColumnMapper(source)); | ||
const CS = options.coffeeScript || CoffeeScript; | ||
patchCoffeeScript(CS); | ||
return /** @type Program */ convert(ParseContext.fromSource(source, CS.tokens, CS.nodes)); | ||
} | ||
@@ -84,645 +89,899 @@ | ||
/** | ||
* @param {Object} node | ||
* @param {string} source | ||
* @param {function(number, number): number} mapper | ||
* @param ancestors | ||
* @param {ParseContext} context | ||
* @returns {Node} | ||
*/ | ||
function convert(node, source, mapper, ancestors=[]) { | ||
if (ancestors.length === 0) { | ||
return makeNode('Program', node.locationData, { | ||
body: makeNode('Block', node.locationData, { | ||
statements: convertChild(node.expressions) | ||
}) | ||
}); | ||
} | ||
function convert(context) { | ||
const { source, lineMap: mapper } = context; | ||
return convertNode(context.ast); | ||
switch (type(node)) { | ||
case 'Value': | ||
let value = convertChild(node.base); | ||
node.properties.forEach(prop => { | ||
value = accessOpForProperty(value, prop, node.base.locationData); | ||
if (value.type === 'MemberAccessOp' && value.expression.type === 'MemberAccessOp') { | ||
if (value.expression.memberName === 'prototype' && value.expression.raw.slice(-2) === '::') { | ||
// Un-expand shorthand prototype access. | ||
value = { | ||
type: 'ProtoMemberAccessOp', | ||
line: value.line, | ||
column: value.column, | ||
range: value.range, | ||
raw: value.raw, | ||
expression: value.expression.expression, | ||
memberName: value.memberName | ||
}; | ||
/** | ||
* @param {Object} node | ||
* @param ancestors | ||
* @returns {Node} | ||
*/ | ||
function convertNode(node, ancestors = []) { | ||
if (ancestors.length === 0) { | ||
let programNode = makeNode('Program', node.locationData, { | ||
body: makeNode('Block', node.locationData, { | ||
statements: convertChild(node.expressions) | ||
}) | ||
}); | ||
Object.defineProperty(programNode, 'context', { | ||
value: context, | ||
enumerable: false | ||
}); | ||
return programNode; | ||
} | ||
if (node.locationData) { | ||
trimNonMatchingParentheses(source, node.locationData, mapper); | ||
} | ||
switch (type(node)) { | ||
case 'Value': | ||
let value = convertChild(node.base); | ||
node.properties.forEach(prop => { | ||
value = accessOpForProperty(value, prop, node.base.locationData); | ||
if (value.type === 'MemberAccessOp' && value.expression.type === 'MemberAccessOp') { | ||
if (value.expression.memberName === 'prototype' && value.expression.raw.slice(-2) === '::') { | ||
// Un-expand shorthand prototype access. | ||
value = { | ||
type: 'ProtoMemberAccessOp', | ||
line: value.line, | ||
column: value.column, | ||
range: value.range, | ||
raw: value.raw, | ||
expression: value.expression.expression, | ||
memberName: value.memberName | ||
}; | ||
} | ||
} | ||
}); | ||
return value; | ||
case 'Literal': | ||
if (node.value === 'this') { | ||
return makeNode('This', node.locationData); | ||
} else { | ||
let start = mapper(node.locationData.first_line, node.locationData.first_column); | ||
let end = mapper(node.locationData.last_line, node.locationData.last_column) + 1; | ||
let raw = source.slice(start, end); | ||
let literal = parseLiteral(raw, start); | ||
if (!literal) { | ||
if (raw[0] === '`' && raw[raw.length - 1] === '`') { | ||
return makeNode('JavaScript', node.locationData, { data: node.value }); | ||
} | ||
return makeNode('Identifier', node.locationData, { data: node.value }); | ||
} else if (literal.type === 'error') { | ||
if (literal.error.type === 'unbalanced-quotes') { | ||
// This is probably part of an interpolated string. | ||
literal = parseLiteral(node.value); | ||
return makeNode('String', node.locationData, { data: parseLiteral(node.value).data }); | ||
} | ||
throw new Error(literal.error.message); | ||
} else if (literal.type === 'string') { | ||
return makeNode('String', node.locationData, { data: literal.data }); | ||
} else if (literal.type === 'int') { | ||
return makeNode('Int', node.locationData, { data: literal.data }); | ||
} else if (literal.type === 'float') { | ||
return makeNode('Float', node.locationData, { data: literal.data }); | ||
} else if (literal.type === 'Herestring') { | ||
return makeNode('Herestring', node.locationData, { data: literal.data, padding: literal.padding }); | ||
} else { | ||
throw new Error(`unknown literal type for value: ${JSON.stringify(literal)}`); | ||
} | ||
} | ||
}); | ||
return value; | ||
case 'Literal': | ||
if (node.value === 'this') { | ||
return makeNode('This', node.locationData); | ||
} else { | ||
const data = parseLiteral(node.value); | ||
if (typeof data === 'string') { | ||
return makeNode('String', node.locationData, {data}); | ||
} else if (typeof data === 'number') { | ||
return makeNode(nodeTypeForLiteral(data), node.locationData, { data }); | ||
} else if (typeof data === 'undefined') { | ||
return makeNode('Identifier', node.locationData, { data: node.value }); | ||
case 'Call': | ||
if (node.isNew) { | ||
return makeNode('NewOp', expandLocationLeftThrough(node.locationData, 'new'), { | ||
ctor: convertChild(node.variable), | ||
arguments: convertChild(node.args) | ||
}); | ||
} else if (node.isSuper) { | ||
if (node.args.length === 1 && type(node.args[0]) === 'Splat' && locationsEqual(node.args[0].locationData, node.locationData)) { | ||
// Virtual splat argument. | ||
return makeNode('FunctionApplication', node.locationData, { | ||
function: makeNode('Super', node.locationData), | ||
arguments: [{ | ||
type: 'Spread', | ||
virtual: true, | ||
expression: { | ||
type: 'Identifier', | ||
data: 'arguments', | ||
virtual: true | ||
} | ||
}] | ||
}); | ||
} | ||
const superLocationData = { | ||
first_line: node.locationData.first_line, | ||
first_column: node.locationData.first_column, | ||
last_line: node.locationData.first_line, | ||
last_column: node.locationData.first_column + 'super'.length - 1 | ||
}; | ||
return makeNode('FunctionApplication', node.locationData, { | ||
function: makeNode('Super', superLocationData), | ||
arguments: convertChild(node.args) | ||
}); | ||
} else { | ||
throw new Error(`unknown literal type for value: ${data}`); | ||
const result = makeNode(node.soak ? 'SoakedFunctionApplication' : 'FunctionApplication', node.locationData, { | ||
function: convertChild(node.variable), | ||
arguments: convertChild(node.args) | ||
}); | ||
if (node.do) { | ||
result.type = 'DoOp'; | ||
result.expression = result.function; | ||
// The argument to `do` may not always be a function literal. | ||
if (result.expression.parameters) { | ||
result.expression.parameters = result.expression.parameters.map((param, i) => { | ||
const arg = result.arguments[i]; | ||
if (arg.type === 'Identifier' && arg.data === param.data) { | ||
return param; | ||
} | ||
return makeNode('DefaultParam', locationContainingNodes(node.args[i], node.variable.params[i]), { | ||
param, | ||
default: arg | ||
}); | ||
}); | ||
} | ||
delete result.function; | ||
delete result.arguments; | ||
} | ||
return result; | ||
} | ||
} | ||
case 'Call': | ||
if (node.isNew) { | ||
return makeNode('NewOp', expandLocationLeftThrough(node.locationData, 'new'), { | ||
ctor: convertChild(node.variable), | ||
arguments: convertChild(node.args) | ||
}); | ||
} else if (node.isSuper) { | ||
if (node.args.length === 1 && type(node.args[0]) === 'Splat' && node.args[0].locationData === node.locationData) { | ||
// Virtual splat argument, ignore it. | ||
node.args = []; | ||
case 'Op': | ||
const op = convertOperator(node); | ||
if (op.type === 'PlusOp') { | ||
if (isInterpolatedString(node, context)) { | ||
op.type = 'ConcatOp'; | ||
let parentOp = ancestors.reduce((memo, ancestor) => type(ancestor) === 'Op' ? ancestor : memo, null); | ||
if (!parentOp || !isInterpolatedString(parentOp, context)) { | ||
return createTemplateLiteral(op); | ||
} | ||
} | ||
} | ||
return makeNode('SuperFunctionApplication', node.locationData, { | ||
arguments: convertChild(node.args) | ||
if (isChainedComparison(node) && !isChainedComparison(ancestors[ancestors.length - 1])) { | ||
return makeNode('ChainedComparisonOp', node.locationData, { | ||
expression: op | ||
}); | ||
} | ||
return op; | ||
case 'Assign': | ||
if (node.context === 'object') { | ||
return makeNode('ObjectInitialiserMember', node.locationData, { | ||
key: convertChild(node.variable), | ||
expression: convertChild(node.value) | ||
}); | ||
} else if (node.context && node.context.slice(-1) === '=') { | ||
return makeNode('CompoundAssignOp', node.locationData, { | ||
assignee: convertChild(node.variable), | ||
expression: convertChild(node.value), | ||
op: binaryOperatorNodeType(node.context.slice(0, -1)) | ||
}) | ||
} else { | ||
return makeNode('AssignOp', node.locationData, { | ||
assignee: convertChild(node.variable), | ||
expression: convertChild(node.value) | ||
}); | ||
} | ||
case 'Obj': | ||
return makeNode('ObjectInitialiser', node.locationData, { | ||
members: node.properties.map(property => { | ||
if (type(property) === 'Value') { | ||
// shorthand property | ||
const keyValue = convertChild(property); | ||
return makeNode('ObjectInitialiserMember', property.locationData, { | ||
key: keyValue, | ||
expression: keyValue | ||
}); | ||
} | ||
return convertChild(property); | ||
}).filter(node => node) | ||
}); | ||
} else { | ||
return makeNode(node.soak ? 'SoakedFunctionApplication' : 'FunctionApplication', node.locationData, { | ||
function: convertChild(node.variable), | ||
arguments: convertChild(node.args) | ||
case 'Arr': | ||
return makeNode('ArrayInitialiser', node.locationData, { | ||
members: convertChild(node.objects) | ||
}); | ||
} | ||
case 'Op': | ||
const op = convertOperator(node); | ||
if (isChainedComparison(node) && !isChainedComparison(ancestors[ancestors.length - 1])) { | ||
return makeNode('ChainedComparisonOp', node.locationData, { | ||
expression: op | ||
case 'Parens': | ||
if (type(node.body) === 'Block') { | ||
const expressions = node.body.expressions; | ||
if (expressions.length === 1) { | ||
return convertChild(expressions[0]); | ||
} else { | ||
const lastExpression = expressions[expressions.length - 1]; | ||
let result = convertChild(lastExpression); | ||
for (let i = expressions.length - 2; i >= 0; i--) { | ||
let left = expressions[i]; | ||
result = makeNode('SeqOp', locationContainingNodes(left, lastExpression), { | ||
left: convertChild(left), | ||
right: result | ||
}); | ||
} | ||
return result; | ||
} | ||
} else { | ||
return convertChild(node.body); | ||
} | ||
case 'If': | ||
let conditional = makeNode('Conditional', node.locationData, { | ||
isUnless: Boolean(node.condition.inverted), | ||
condition: convertChild(node.condition), | ||
consequent: convertChild(node.body), | ||
alternate: convertChild(node.elseBody) | ||
}); | ||
} | ||
return op; | ||
if (conditional.condition.range[0] > conditional.consequent.range[0]) { | ||
conditional.consequent.inline = true; | ||
} | ||
return conditional; | ||
case 'Assign': | ||
if (node.context === 'object') { | ||
return makeNode('ObjectInitialiserMember', node.locationData, { | ||
key: convertChild(node.variable), | ||
expression: convertChild(node.value) | ||
case 'Code': | ||
const fnType = node.bound ? 'BoundFunction' : | ||
node.isGenerator ? 'GeneratorFunction' : 'Function'; | ||
return makeNode(fnType, node.locationData, { | ||
body: convertChild(node.body), | ||
parameters: convertChild(node.params) | ||
}); | ||
} else if (node.context && node.context.slice(-1) === '=') { | ||
return makeNode('CompoundAssignOp', node.locationData, { | ||
assignee: convertChild(node.variable), | ||
expression: convertChild(node.value), | ||
op: binaryOperatorNodeType(node.context.slice(0, -1)) | ||
}) | ||
} else { | ||
return makeNode('AssignOp', node.locationData, { | ||
assignee: convertChild(node.variable), | ||
expression: convertChild(node.value) | ||
}); | ||
} | ||
case 'Obj': | ||
return makeNode('ObjectInitialiser', node.locationData, { | ||
members: node.properties.map(property => { | ||
if (type(property) === 'Value') { | ||
// shorthand property | ||
const keyValue = convertChild(property); | ||
return makeNode('ObjectInitialiserMember', property.locationData, { | ||
key: keyValue, | ||
expression: keyValue | ||
}); | ||
case 'Param': | ||
const param = convertChild(node.name); | ||
if (node.value) { | ||
return makeNode('DefaultParam', node.locationData, { | ||
default: convertChild(node.value), | ||
param | ||
}); | ||
} | ||
if (node.splat) { | ||
return makeNode('Rest', node.locationData, { | ||
expression: param | ||
}); | ||
} | ||
return param; | ||
case 'Block': | ||
if (node.expressions.length === 0) { | ||
return null; | ||
} else { | ||
const block = makeNode('Block', node.locationData, { | ||
statements: convertChild(node.expressions) | ||
}); | ||
block.inline = false; | ||
for (let i = block.range[0] - 1; i >= 0; i--) { | ||
const char = source[i]; | ||
if (char === '\n') { | ||
break; | ||
} else if (char !== ' ' && char !== '\t') { | ||
block.inline = true; | ||
break; | ||
} | ||
} | ||
return block; | ||
} | ||
return convertChild(property); | ||
}).filter(node => node) | ||
}); | ||
case 'Bool': | ||
return makeNode('Bool', node.locationData, { | ||
data: JSON.parse(node.val) | ||
}); | ||
case 'Arr': | ||
return makeNode('ArrayInitialiser', node.locationData, { | ||
members: convertChild(node.objects) | ||
}); | ||
case 'Null': | ||
return makeNode('Null', node.locationData); | ||
case 'Parens': | ||
if (type(node.body) === 'Block') { | ||
const expressions = node.body.expressions; | ||
if (expressions.length === 1) { | ||
return convertChild(expressions[0]); | ||
case 'Undefined': | ||
return makeNode('Undefined', node.locationData); | ||
case 'Return': | ||
return makeNode('Return', node.locationData, { | ||
expression: node.expression ? convertChild(node.expression) : null | ||
}); | ||
case 'For': | ||
if (locationsEqual(node.body.locationData, node.locationData)) { | ||
node.body.locationData = locationContainingNodes(...node.body.expressions); | ||
} | ||
node.locationData = locationWithLastPosition(node.locationData, node.body.locationData); | ||
if (node.object) { | ||
return makeNode('ForOf', node.locationData, { | ||
keyAssignee: convertChild(node.index), | ||
valAssignee: convertChild(node.name), | ||
body: convertChild(node.body), | ||
target: convertChild(node.source), | ||
filter: convertChild(node.guard), | ||
isOwn: node.own | ||
}); | ||
} else { | ||
const lastExpression = expressions[expressions.length - 1]; | ||
let result = convertChild(lastExpression); | ||
for (let i = expressions.length - 2; i >= 0; i--) { | ||
let left = expressions[i]; | ||
result = makeNode('SeqOp', locationContainingNodes(left, lastExpression), { | ||
left: convertChild(left), | ||
right: result | ||
}); | ||
} | ||
return result; | ||
return makeNode('ForIn', node.locationData, { | ||
keyAssignee: convertChild(node.index), | ||
valAssignee: convertChild(node.name), | ||
body: convertChild(node.body), | ||
target: convertChild(node.source), | ||
filter: convertChild(node.guard), | ||
step: convertChild(node.step) | ||
}); | ||
} | ||
} else { | ||
return convertChild(node.body); | ||
} | ||
case 'If': | ||
if (type(node.condition) === 'Op' && node.condition.operator === '!') { | ||
if (node.locationData === node.condition.locationData) { | ||
// Virtual node for `unless` condition. | ||
node.condition.locationData = null; | ||
case 'While': | ||
const result = makeNode('While', locationContainingNodes(node, node.condition, node.body), { | ||
condition: convertChild(node.condition), | ||
guard: convertChild(node.guard), | ||
body: convertChild(node.body), | ||
isUntil: node.condition.inverted === true | ||
}); | ||
if (result.raw.indexOf('loop') === 0) { | ||
result.condition = { | ||
type: 'Bool', | ||
data: true, | ||
virtual: true | ||
}; | ||
} | ||
} | ||
return makeNode('Conditional', node.locationData, { | ||
condition: convertChild(node.condition), | ||
consequent: convertChild(node.body), | ||
alternate: convertChild(node.elseBody) | ||
}); | ||
return result; | ||
case 'Code': | ||
const fnType = node.bound ? 'BoundFunction' : | ||
node.isGenerator ? 'GeneratorFunction' : 'Function'; | ||
return makeNode(fnType, node.locationData, { | ||
body: convertChild(node.body), | ||
parameters: convertChild(node.params) | ||
}); | ||
case 'Existence': | ||
return makeNode('UnaryExistsOp', node.locationData, { | ||
expression: convertChild(node.expression) | ||
}); | ||
case 'Param': | ||
const param = convertChild(node.name); | ||
if (node.value) { | ||
return makeNode('DefaultParam', node.locationData, { | ||
default: convertChild(node.value), | ||
param | ||
case 'Class': | ||
const nameNode = node.variable ? convertChild(node.variable) : null; | ||
let ctor = null; | ||
let boundMembers = []; | ||
const body = (!node.body || node.body.expressions.length === 0) ? null : makeNode('Block', node.body.locationData, { | ||
statements: node.body.expressions.reduce((statements, expr) => { | ||
if (type(expr) === 'Value' && type(expr.base) === 'Obj') { | ||
expr.base.properties.forEach(property => { | ||
let key; | ||
let value; | ||
switch (type(property)) { | ||
case 'Value': | ||
// shorthand property | ||
key = value = convertChild(property); | ||
break; | ||
case 'Comment': | ||
return; | ||
default: | ||
key = convertChild(property.variable); | ||
value = convertChild(property.value); | ||
break; | ||
} | ||
if (key.data === 'constructor') { | ||
statements.push(ctor = makeNode('Constructor', property.locationData, { | ||
expression: value | ||
})); | ||
} else if (key.type === 'MemberAccessOp' && key.expression.type === 'This') { | ||
statements.push(makeNode('AssignOp', property.locationData, { | ||
assignee: key, | ||
expression: value | ||
})); | ||
} else { | ||
statements.push(makeNode('ClassProtoAssignOp', property.locationData, { | ||
assignee: key, | ||
expression: value | ||
})); | ||
} | ||
if (value.type === 'BoundFunction') { | ||
boundMembers.push(statements[statements.length - 1]); | ||
} | ||
}); | ||
} else { | ||
statements.push(convertChild(expr)); | ||
} | ||
return statements; | ||
}, []) | ||
}); | ||
} | ||
if (node.splat) { | ||
return makeNode('Rest', node.locationData, { | ||
expression: param | ||
return makeNode('Class', node.locationData, { | ||
name: nameNode, | ||
nameAssignee: nameNode, | ||
body, | ||
boundMembers, | ||
parent: node.parent ? convertChild(node.parent) : null, | ||
ctor | ||
}); | ||
} | ||
return param; | ||
case 'Block': | ||
if (node.expressions.length === 0) { | ||
return null; | ||
} else { | ||
return makeNode('Block', node.locationData, { | ||
statements: convertChild(node.expressions) | ||
case 'Switch': | ||
return makeNode('Switch', node.locationData, { | ||
expression: convertChild(node.subject), | ||
cases: node.cases.map(([conditions, body]) => { | ||
if (!Array.isArray(conditions)) { | ||
conditions = [conditions]; | ||
} | ||
const loc = expandLocationLeftThrough( | ||
locationContainingNodes(conditions[0], body), | ||
'when ' | ||
); | ||
return makeNode('SwitchCase', loc, { | ||
conditions: convertChild(conditions), | ||
consequent: convertChild(body) | ||
}) | ||
}).filter(node => node), | ||
alternate: convertChild(node.otherwise) | ||
}); | ||
} | ||
case 'Bool': | ||
return makeNode('Bool', node.locationData, { | ||
data: JSON.parse(node.val) | ||
}); | ||
case 'Splat': | ||
return makeNode('Spread', node.locationData, { | ||
expression: convertChild(node.name) | ||
}); | ||
case 'Null': | ||
return makeNode('Null', node.locationData); | ||
case 'Throw': | ||
return makeNode('Throw', node.locationData, { | ||
expression: convertChild(node.expression) | ||
}); | ||
case 'Undefined': | ||
return makeNode('Undefined', node.locationData); | ||
case 'Try': | ||
return makeNode('Try', node.locationData, { | ||
body: convertChild(node.attempt), | ||
catchAssignee: convertChild(node.errorVariable), | ||
catchBody: convertChild(node.recovery), | ||
finallyBody: convertChild(node.ensure) | ||
}); | ||
case 'Return': | ||
return makeNode('Return', node.locationData, { | ||
expression: node.expression ? convertChild(node.expression) : null | ||
}); | ||
case 'Range': | ||
return makeNode('Range', node.locationData, { | ||
left: convertChild(node.from), | ||
right: convertChild(node.to), | ||
isInclusive: !node.exclusive | ||
}); | ||
case 'For': | ||
if (node.body.locationData === node.locationData) { | ||
node.body.locationData = locationContainingNodes(...node.body.expressions); | ||
} | ||
node.locationData = locationWithLastPosition(node.locationData, node.body.locationData); | ||
if (node.object) { | ||
return makeNode('ForOf', node.locationData, { | ||
keyAssignee: convertChild(node.index), | ||
valAssignee: convertChild(node.name), | ||
body: convertChild(node.body), | ||
target: convertChild(node.source), | ||
filter: convertChild(node.guard), | ||
isOwn: node.own | ||
case 'In': | ||
return makeNode('InOp', node.locationData, { | ||
left: convertChild(node.object), | ||
right: convertChild(node.array), | ||
isNot: node.negated === true | ||
}); | ||
} else { | ||
return makeNode('ForIn', node.locationData, { | ||
keyAssignee: convertChild(node.index), | ||
valAssignee: convertChild(node.name), | ||
body: convertChild(node.body), | ||
target: convertChild(node.source), | ||
filter: convertChild(node.guard), | ||
step: convertChild(node.step) | ||
}); | ||
} | ||
case 'While': | ||
return makeNode('While', locationContainingNodes(node, node.condition, node.body), { | ||
condition: convertChild(node.condition), | ||
body: convertChild(node.body) | ||
}); | ||
case 'Expansion': | ||
return makeNode('Expansion', node.locationData); | ||
case 'Existence': | ||
return makeNode('UnaryExistsOp', node.locationData, { | ||
expression: convertChild(node.expression) | ||
}); | ||
case 'Comment': | ||
return null; | ||
case 'Class': | ||
const nameNode = node.variable ? convertChild(node.variable) : null; | ||
case 'Extends': | ||
return makeNode('ExtendsOp', node.locationData, { | ||
left: convertChild(node.child), | ||
right: convertChild(node.parent) | ||
}); | ||
let ctor = null; | ||
let boundMembers = []; | ||
const body = (!node.body || node.body.expressions.length === 0) ? null : makeNode('Block', node.body.locationData, { | ||
statements: node.body.expressions.reduce((statements, expr) => { | ||
if (type(expr) === 'Value' && type(expr.base) === 'Obj') { | ||
expr.base.properties.forEach(property => { | ||
let key; | ||
let value; | ||
switch (type(property)) { | ||
case 'Value': | ||
// shorthand property | ||
key = value = convertChild(property); | ||
break; | ||
default: | ||
throw new Error(`unknown node type: ${type(node)}\n${JSON.stringify(node, null, 2)}`); | ||
break; | ||
} | ||
case 'Comment': | ||
return; | ||
function convertChild(child) { | ||
if (!child) { | ||
return null; | ||
} else if (Array.isArray(child)) { | ||
return child.map(convertChild).filter(node => node); | ||
} else { | ||
return convertNode(child, [...ancestors, node]); | ||
} | ||
} | ||
default: | ||
key = convertChild(property.variable); | ||
value = convertChild(property.value); | ||
break; | ||
function makeNode(type, loc, attrs = {}) { | ||
const result = {type}; | ||
if (loc) { | ||
const start = mapper(loc.first_line, loc.first_column); | ||
const end = mapper(loc.last_line, loc.last_column) + 1; | ||
result.line = loc.first_line + 1; | ||
result.column = loc.first_column + 1; | ||
result.range = [start, end]; | ||
} else { | ||
result.virtual = true; | ||
} | ||
for (let key in attrs) { | ||
if (attrs.hasOwnProperty(key)) { | ||
let value = attrs[key]; | ||
result[key] = value; | ||
if (value && result.range) { | ||
(Array.isArray(value) ? value : [value]).forEach(node => { | ||
if (node.range) { | ||
// Expand the range to contain all the children. | ||
if (result.range[0] > node.range[0]) { | ||
result.range[0] = node.range[0]; | ||
} | ||
if (result.range[1] < node.range[1]) { | ||
result.range[1] = node.range[1]; | ||
} | ||
} | ||
if (key.data === 'constructor') { | ||
statements.push(ctor = makeNode('Constructor', property.locationData, { | ||
expression: value | ||
})); | ||
} else if (key.type === 'MemberAccessOp' && key.expression.type === 'This') { | ||
statements.push(makeNode('AssignOp', property.locationData, { | ||
assignee: key, | ||
expression: value | ||
})); | ||
} else { | ||
statements.push(makeNode('ClassProtoAssignOp', property.locationData, { | ||
assignee: key, | ||
expression: value | ||
})); | ||
} | ||
if (value.type === 'BoundFunction') { | ||
boundMembers.push(statements[statements.length - 1]); | ||
} | ||
}); | ||
} else { | ||
statements.push(convertChild(expr)); | ||
} | ||
return statements; | ||
}, []) | ||
}); | ||
} | ||
} | ||
// Shrink to be within the size of the source. | ||
if (result.range) { | ||
if (result.range[0] < 0) { | ||
result.range[0] = 0; | ||
} | ||
if (result.range[1] > source.length) { | ||
result.range[1] = source.length; | ||
} | ||
result.raw = source.slice(result.range[0], result.range[1]); | ||
} | ||
return result; | ||
} | ||
return makeNode('Class', node.locationData, { | ||
name: nameNode, | ||
nameAssignee: nameNode, | ||
body, | ||
boundMembers, | ||
parent: node.parent ? convertChild(node.parent) : null, | ||
ctor | ||
}); | ||
function createTemplateLiteral(op) { | ||
let stringStartTokenIndex = context.indexOfTokenAtOffset(op.range[0]); | ||
for (; stringStartTokenIndex >= 0; stringStartTokenIndex--) { | ||
if (context.tokenAtIndex(stringStartTokenIndex).type === 'STRING_START') { | ||
break; | ||
} | ||
} | ||
let stringEndTokenIndex = context.indexOfEndTokenForStartTokenAtIndex(stringStartTokenIndex); | ||
if (stringEndTokenIndex === null) { | ||
throw new Error('cannot find interpolation end for node'); | ||
} | ||
op.type = 'TemplateLiteral'; | ||
op.range = [ | ||
context.tokenAtIndex(stringStartTokenIndex).range[0], | ||
context.tokenAtIndex(stringEndTokenIndex).range[1] | ||
]; | ||
op.raw = source.slice(...op.range); | ||
case 'Switch': | ||
return makeNode('Switch', node.locationData, { | ||
expression: convertChild(node.subject), | ||
cases: node.cases.map(([conditions, body]) => { | ||
if (!Array.isArray(conditions)) { | ||
conditions = [conditions]; | ||
} | ||
const loc = expandLocationLeftThrough( | ||
locationContainingNodes(conditions[0], body), | ||
'when ' | ||
); | ||
return makeNode('SwitchCase', loc, { | ||
conditions: convertChild(conditions), | ||
consequent: convertChild(body) | ||
}) | ||
}).filter(node => node), | ||
alternate: convertChild(node.otherwise) | ||
}); | ||
let elements = []; | ||
case 'Splat': | ||
return makeNode('Spread', node.locationData, { | ||
expression: convertChild(node.name) | ||
}); | ||
function addElements({ left, right }) { | ||
if (left.type === 'ConcatOp') { | ||
addElements(left); | ||
} else { | ||
elements.push(left); | ||
} | ||
elements.push(right); | ||
} | ||
addElements(op); | ||
case 'Throw': | ||
return makeNode('Throw', node.locationData, { | ||
expression: convertChild(node.expression) | ||
}); | ||
let quasis = []; | ||
let expressions = []; | ||
let quote = op.raw.slice(0, 3) === '"""' ? '"""' : '"'; | ||
case 'Try': | ||
return makeNode('Try', node.locationData, { | ||
body: convertChild(node.attempt), | ||
catchAssignee: convertChild(node.errorVariable), | ||
catchBody: convertChild(node.recovery), | ||
finallyBody: convertChild(node.ensure) | ||
}); | ||
function buildFirstQuasi() { | ||
// Find the start of the first interpolation, i.e. "#{a}". | ||
// ^ | ||
let startOfInterpolation = op.range[0]; | ||
while (source[startOfInterpolation] !== '#') { | ||
startOfInterpolation += 1; | ||
} | ||
let range = [op.range[0], startOfInterpolation]; | ||
return buildQuasi(range); | ||
} | ||
case 'Range': | ||
return makeNode('Range', node.locationData, { | ||
left: convertChild(node.from), | ||
right: convertChild(node.to), | ||
isInclusive: !node.exclusive | ||
}); | ||
function buildLastQuasi() { | ||
// Find the close of the last interpolation, i.e. "a#{b}". | ||
// ^ | ||
let endOfInterpolation = op.range[1]; | ||
while (source[endOfInterpolation] !== '}') { | ||
endOfInterpolation -= 1; | ||
} | ||
return buildQuasi([endOfInterpolation + 1, op.range[1]]); | ||
} | ||
case 'In': | ||
return makeNode('InOp', node.locationData, { | ||
left: convertChild(node.object), | ||
right: convertChild(node.array) | ||
}); | ||
function buildQuasi(range) { | ||
let loc = mapper.invert(range[0]); | ||
return { | ||
type: 'String', | ||
data: '', | ||
raw: source.slice(...range), | ||
line: loc.line + 1, | ||
column: loc.column + 1, | ||
range | ||
}; | ||
} | ||
case 'Expansion': | ||
return makeNode('Expansion', node.locationData); | ||
function quotesMatch(string) { | ||
let leftTripleQuoted = string.slice(0, 3) === '"""'; | ||
let rightTripleQuoted = string.slice(-3) === '"""'; | ||
case 'Comment': | ||
return null; | ||
if (string.slice(-4) === '\\"""') { | ||
// Don't count escaped quotes. | ||
rightTripleQuoted = false; | ||
} | ||
case 'Extends': | ||
return makeNode('ExtendsOp', node.locationData, { | ||
left: convertChild(node.child), | ||
right: convertChild(node.parent) | ||
}); | ||
if (leftTripleQuoted !== rightTripleQuoted) { | ||
// Unbalanced. | ||
return false; | ||
} else if (leftTripleQuoted && rightTripleQuoted) { | ||
// We're set as long as we didn't double count. | ||
return string.length >= 6; | ||
} | ||
default: | ||
throw new Error(`unknown node type: ${type(node)}\n${JSON.stringify(node, null, 2)}`); | ||
break; | ||
} | ||
let leftSingleQuoted = string.slice(0, 1) === '"'; | ||
let rightSingleQuoted = string.slice(-1) === '"'; | ||
function convertChild(child) { | ||
if (!child) { | ||
return null; | ||
} else if (Array.isArray(child)) { | ||
return child.map(convertChild).filter(node => node); | ||
} else { | ||
return convert(child, source, mapper, [...ancestors, node]); | ||
} | ||
} | ||
if (string.slice(-2) === '\\"') { | ||
// Don't count escaped quotes. | ||
rightSingleQuoted = false; | ||
} | ||
function makeNode(type, loc, attrs={}) { | ||
const result = { type }; | ||
if (loc) { | ||
const start = mapper(loc.first_line, loc.first_column); | ||
const end = mapper(loc.last_line, loc.last_column) + 1; | ||
result.line = loc.first_line + 1; | ||
result.column = loc.first_column + 1; | ||
result.range = [start, end]; | ||
} else { | ||
result.virtual = true; | ||
} | ||
for (let key in attrs) { | ||
if (attrs.hasOwnProperty(key)) { | ||
let value = attrs[key]; | ||
result[key] = value; | ||
if (value && result.range) { | ||
(Array.isArray(value) ? value : [value]).forEach(node => { | ||
if (node.range) { | ||
// Expand the range to contain all the children. | ||
if (result.range[0] > node.range[0]) { | ||
result.range[0] = node.range[0]; | ||
if (leftSingleQuoted !== rightSingleQuoted) { | ||
// Unbalanced. | ||
return false; | ||
} else if (leftSingleQuoted && rightSingleQuoted) { | ||
// We're set as long as we didn't double count. | ||
return string.length >= 2; | ||
} | ||
} | ||
elements.forEach((element, i) => { | ||
if (i === 0) { | ||
if (element.type === 'String') { | ||
if (element.range[0] === op.range[0]) { | ||
// This string is not interpolated, it's part of the string interpolation. | ||
if (element.data === '' && element.raw.length > quote.length) { | ||
// CoffeeScript includes the `#` in the raw value of a leading | ||
// empty quasi string, but it shouldn't be there. | ||
element = buildFirstQuasi(); | ||
} | ||
if (result.range[1] < node.range[1]) { | ||
result.range[1] = node.range[1]; | ||
} | ||
quasis.push(element); | ||
return; | ||
} | ||
}); | ||
} | ||
} | ||
if (element.type === 'String' && !quotesMatch(element.raw)) { | ||
quasis.push(element); | ||
} else { | ||
if (quasis.length === 0) { | ||
// This element is interpolated and is first, i.e. "#{a}". | ||
quasis.push(buildFirstQuasi()); | ||
} else if (quasis.length < expressions.length + 1) { | ||
let borderIndex = source.lastIndexOf('}#{', element.range[0]); | ||
quasis.push(buildQuasi([borderIndex + 1, borderIndex + 1])); | ||
} | ||
expressions.push(element); | ||
} | ||
}); | ||
if (quasis.length < expressions.length + 1) { | ||
quasis.push(buildLastQuasi()); | ||
} | ||
op.quasis = quasis; | ||
op.expressions = expressions; | ||
delete op.left; | ||
delete op.right; | ||
return op; | ||
} | ||
// Shrink to be within the size of the source. | ||
if (result.range) { | ||
if (result.range[0] < 0) { | ||
result.range[0] = 0; | ||
} | ||
if (result.range[1] > source.length) { | ||
result.range[1] = source.length; | ||
} | ||
result.raw = source.slice(result.range[0], result.range[1]); | ||
} | ||
return result; | ||
} | ||
/** | ||
* @param expression converted base | ||
* @param prop CS node to convert | ||
* @param loc CS location data for original base | ||
*/ | ||
function accessOpForProperty(expression, prop, loc) { | ||
switch (type(prop)) { | ||
case 'Access': | ||
return makeNode(prop.soak ? 'SoakedMemberAccessOp' : 'MemberAccessOp', mergeLocations(loc, prop.locationData), { | ||
expression, | ||
memberName: prop.name.value | ||
}); | ||
/** | ||
* @param expression converted base | ||
* @param prop CS node to convertNode | ||
* @param loc CS location data for original base | ||
*/ | ||
function accessOpForProperty(expression, prop, loc) { | ||
switch (type(prop)) { | ||
case 'Access': | ||
return makeNode(prop.soak ? 'SoakedMemberAccessOp' : 'MemberAccessOp', mergeLocations(loc, prop.locationData), { | ||
expression, | ||
memberName: prop.name.value | ||
}); | ||
case 'Index': | ||
return makeNode(prop.soak ? 'SoakedDynamicMemberAccessOp' : 'DynamicMemberAccessOp', expandLocationRightThrough(mergeLocations(loc, prop.locationData), ']'), { | ||
expression, | ||
indexingExpr: convert(prop.index, source, mapper, [...ancestors, node, prop]) | ||
}); | ||
case 'Index': | ||
return makeNode(prop.soak ? 'SoakedDynamicMemberAccessOp' : 'DynamicMemberAccessOp', expandLocationRightThrough(mergeLocations(loc, prop.locationData), ']'), { | ||
expression, | ||
indexingExpr: convertNode(prop.index, [...ancestors, node, prop]) | ||
}); | ||
case 'Slice': | ||
return makeNode('Slice', expandLocationRightThrough(mergeLocations(loc, prop.locationData), ']'), { | ||
expression, | ||
left: convertChild(prop.range.from), | ||
right: convertChild(prop.range.to), | ||
isInclusive: !prop.range.exclusive | ||
}); | ||
case 'Slice': | ||
return makeNode('Slice', expandLocationRightThrough(mergeLocations(loc, prop.locationData), ']'), { | ||
expression, | ||
left: convertChild(prop.range.from), | ||
right: convertChild(prop.range.to), | ||
isInclusive: !prop.range.exclusive | ||
}); | ||
default: | ||
throw new Error(`unknown property type: ${type(prop)}\n${JSON.stringify(prop, null, 2)}`) | ||
default: | ||
throw new Error(`unknown property type: ${type(prop)}\n${JSON.stringify(prop, null, 2)}`) | ||
} | ||
} | ||
} | ||
function binaryOperatorNodeType(operator) { | ||
switch (operator) { | ||
case '===': | ||
return 'EQOp'; | ||
case '!==': | ||
return 'NEQOp'; | ||
function binaryOperatorNodeType(operator) { | ||
switch (operator) { | ||
case '===': | ||
return 'EQOp'; | ||
case '&&': | ||
return 'LogicalAndOp'; | ||
case '!==': | ||
return 'NEQOp'; | ||
case '||': | ||
return 'LogicalOrOp'; | ||
case '&&': | ||
return 'LogicalAndOp'; | ||
case '+': | ||
return 'PlusOp'; | ||
case '||': | ||
return 'LogicalOrOp'; | ||
case '-': | ||
return 'SubtractOp'; | ||
case '+': | ||
return 'PlusOp'; | ||
case '*': | ||
return 'MultiplyOp'; | ||
case '-': | ||
return 'SubtractOp'; | ||
case '/': | ||
return 'DivideOp'; | ||
case '*': | ||
return 'MultiplyOp'; | ||
case '%': | ||
return 'RemOp'; | ||
case '/': | ||
return 'DivideOp'; | ||
case '&': | ||
return 'BitAndOp'; | ||
case '%': | ||
return 'RemOp'; | ||
case '|': | ||
return 'BitOrOp'; | ||
case '%%': | ||
return 'ModuloOp'; | ||
case '^': | ||
return 'BitXorOp'; | ||
case '&': | ||
return 'BitAndOp'; | ||
case '<': | ||
return 'LTOp'; | ||
case '|': | ||
return 'BitOrOp'; | ||
case '>': | ||
return 'GTOp'; | ||
case '^': | ||
return 'BitXorOp'; | ||
case '<=': | ||
return 'LTEOp'; | ||
case '<': | ||
return 'LTOp'; | ||
case '>=': | ||
return 'GTEOp'; | ||
case '>': | ||
return 'GTOp'; | ||
case 'in': | ||
return 'OfOp'; | ||
case '<=': | ||
return 'LTEOp'; | ||
case '?': | ||
return 'ExistsOp'; | ||
case '>=': | ||
return 'GTEOp'; | ||
case 'instanceof': | ||
return 'InstanceofOp'; | ||
case 'in': | ||
return 'OfOp'; | ||
case '<<': | ||
return 'LeftShiftOp'; | ||
case '?': | ||
return 'ExistsOp'; | ||
case '>>': | ||
return 'SignedRightShiftOp'; | ||
case 'instanceof': | ||
return 'InstanceofOp'; | ||
case '>>>': | ||
return 'UnsignedRightShiftOp'; | ||
case '<<': | ||
return 'LeftShiftOp'; | ||
case '**': | ||
return 'ExpOp'; | ||
case '>>': | ||
return 'SignedRightShiftOp'; | ||
case '//': | ||
return 'FloorDivideOp'; | ||
case '>>>': | ||
return 'UnsignedRightShiftOp'; | ||
default: | ||
return null; | ||
case '**': | ||
return 'ExpOp'; | ||
case '//': | ||
return 'FloorDivideOp'; | ||
default: | ||
return null; | ||
} | ||
} | ||
} | ||
function convertOperator(op) { | ||
let nodeType; | ||
function convertOperator(op) { | ||
let nodeType; | ||
if (op.second) { | ||
nodeType = binaryOperatorNodeType(op.operator); | ||
if (op.second) { | ||
nodeType = binaryOperatorNodeType(op.operator); | ||
if (!nodeType) { | ||
throw new Error(`unknown binary operator: ${op.operator}`); | ||
} | ||
if (!nodeType) { | ||
throw new Error(`unknown binary operator: ${op.operator}`); | ||
} | ||
return makeNode(nodeType, op.locationData, { | ||
left: convert(op.first, source, mapper, [...ancestors, op]), | ||
right: convert(op.second, source, mapper, [...ancestors, op]) | ||
}); | ||
} else { | ||
switch (op.operator) { | ||
case '+': | ||
nodeType = 'UnaryPlusOp'; | ||
break; | ||
let result = makeNode(nodeType, op.locationData, { | ||
left: convertNode(op.first, [...ancestors, op]), | ||
right: convertNode(op.second, [...ancestors, op]) | ||
}); | ||
if (result.type === 'InstanceofOp' || result.type === 'OfOp') { | ||
result.isNot = op.inverted === true; | ||
} | ||
return result; | ||
} else { | ||
switch (op.operator) { | ||
case '+': | ||
nodeType = 'UnaryPlusOp'; | ||
break; | ||
case '-': | ||
nodeType = 'UnaryNegateOp'; | ||
break; | ||
case '-': | ||
nodeType = 'UnaryNegateOp'; | ||
break; | ||
case 'typeof': | ||
nodeType = 'TypeofOp'; | ||
break; | ||
case 'typeof': | ||
nodeType = 'TypeofOp'; | ||
break; | ||
case '!': | ||
nodeType = 'LogicalNotOp'; | ||
break; | ||
case '!': | ||
nodeType = 'LogicalNotOp'; | ||
break; | ||
case '~': | ||
nodeType = 'BitNotOp'; | ||
break; | ||
case '~': | ||
nodeType = 'BitNotOp'; | ||
break; | ||
case '--': | ||
nodeType = op.flip ? 'PostDecrementOp' : 'PreDecrementOp'; | ||
break; | ||
case '--': | ||
nodeType = op.flip ? 'PostDecrementOp' : 'PreDecrementOp'; | ||
break; | ||
case '++': | ||
nodeType = op.flip ? 'PostIncrementOp' : 'PreIncrementOp'; | ||
break; | ||
case '++': | ||
nodeType = op.flip ? 'PostIncrementOp' : 'PreIncrementOp'; | ||
break; | ||
case 'delete': | ||
nodeType = 'DeleteOp'; | ||
break; | ||
case 'delete': | ||
nodeType = 'DeleteOp'; | ||
break; | ||
case 'new': | ||
// Parentheses-less "new". | ||
return makeNode('NewOp', op.locationData, { | ||
ctor: convertChild(op.first), | ||
arguments: [] | ||
}); | ||
case 'new': | ||
// Parentheses-less "new". | ||
return makeNode('NewOp', op.locationData, { | ||
ctor: convertChild(op.first), | ||
arguments: [] | ||
}); | ||
case 'yield': | ||
return makeNode('Yield', op.locationData, { | ||
expression: convertChild(op.first) | ||
}); | ||
case 'yield': | ||
return makeNode('Yield', op.locationData, { | ||
expression: convertChild(op.first) | ||
}); | ||
default: | ||
throw new Error(`unknown unary operator: ${op.operator}`); | ||
default: | ||
throw new Error(`unknown unary operator: ${op.operator}`); | ||
} | ||
return makeNode(nodeType, op.locationData, { | ||
expression: convertNode(op.first, [...ancestors, op]) | ||
}); | ||
} | ||
return makeNode(nodeType, op.locationData, { | ||
expression: convert(op.first, source, mapper, [...ancestors, op]) | ||
}); | ||
} | ||
} | ||
function expandLocationRightThrough(loc, string) { | ||
let offset = mapper(loc.last_line, loc.last_column) + 1; | ||
offset = source.indexOf(string, offset); | ||
function expandLocationRightThrough(loc, string) { | ||
let offset = mapper(loc.last_line, loc.last_column) + 1; | ||
offset = source.indexOf(string, offset); | ||
if (offset < 0) { | ||
throw new Error( | ||
`unable to expand location ending at ${loc.last_line + 1}:${loc.last_column + 1} ` + | ||
`because it is not followed by ${JSON.stringify(string)}` | ||
); | ||
if (offset < 0) { | ||
throw new Error( | ||
`unable to expand location ending at ${loc.last_line + 1}:${loc.last_column + 1} ` + | ||
`because it is not followed by ${JSON.stringify(string)}` | ||
); | ||
} | ||
const newLoc = mapper.invert(offset + string.length - 1); | ||
return { | ||
first_line: loc.first_line, | ||
first_column: loc.first_column, | ||
last_line: newLoc.line, | ||
last_column: newLoc.column | ||
}; | ||
} | ||
const newLoc = mapper.invert(offset + string.length - 1); | ||
function expandLocationLeftThrough(loc, string) { | ||
let offset = mapper(loc.first_line, loc.first_column); | ||
offset = source.lastIndexOf(string, offset); | ||
return { | ||
first_line: loc.first_line, | ||
first_column: loc.first_column, | ||
last_line: newLoc.line, | ||
last_column: newLoc.column | ||
}; | ||
} | ||
if (offset < 0) { | ||
throw new Error( | ||
`unable to expand location starting at ${loc.first_line + 1}:${loc.first_column + 1} ` + | ||
`because it is not preceded by ${JSON.stringify(string)}` | ||
); | ||
} | ||
function expandLocationLeftThrough(loc, string) { | ||
let offset = mapper(loc.first_line, loc.first_column); | ||
offset = source.lastIndexOf(string, offset); | ||
const newLoc = mapper.invert(offset); | ||
if (offset < 0) { | ||
throw new Error( | ||
`unable to expand location starting at ${loc.first_line + 1}:${loc.first_column + 1} ` + | ||
`because it is not preceded by ${JSON.stringify(string)}` | ||
); | ||
return { | ||
first_line: newLoc.line, | ||
first_column: newLoc.column, | ||
last_line: loc.last_line, | ||
last_column: loc.last_column | ||
}; | ||
} | ||
const newLoc = mapper.invert(offset); | ||
return { | ||
first_line: newLoc.line, | ||
first_column: newLoc.column, | ||
last_line: loc.last_line, | ||
last_column: loc.last_column | ||
}; | ||
} | ||
@@ -729,0 +988,0 @@ } |
/** | ||
* @param {string} string | ||
* @returns {string|number} | ||
* @param {number=} offset | ||
* @returns {*} | ||
*/ | ||
export default function parseLiteral(string) { | ||
if (string.slice(0, 3) === '"""') { | ||
return parseQuotedString(string, '"""'); | ||
} else if (string.slice(0, 3) === "'''") { | ||
return parseQuotedString(string, "'''"); | ||
} else if (string[0] === "'") { | ||
export default function parseLiteral(string, offset=0) { | ||
if (string.slice(0, 3) === '"""' || string.slice(-3) === '"""') { | ||
return parseHerestring(string, '"""', offset); | ||
} else if (string.slice(0, 3) === "'''" || string.slice(-3) === "'''") { | ||
return parseHerestring(string, "'''", offset); | ||
} else if (string[0] === "'" || string[string.length - 1] === "'") { | ||
return parseQuotedString(string, "'"); | ||
} else if (string[0] === '"') { | ||
} else if (string[0] === '"' || string[string.length - 1] === '"') { | ||
return parseQuotedString(string, '"'); | ||
} else if (/^\d+$/.test(string)) { | ||
return parseInt(string); | ||
return parseInteger(string); | ||
} else if (/^\d*\.\d+$/.test(string)) { | ||
return parseFloat(string); | ||
return parseFloatingPoint(string); | ||
} else if (/^0x[\da-f]+$/i.test(string)) { | ||
return parseHexidecimal(string); | ||
} else if (/^0o[0-7]+$/i.test(string)) { | ||
return parseOctal(string); | ||
} | ||
} | ||
function parseHerestring(string, quote, offset=0) { | ||
let { error, data } = parseQuotedString(string, quote); | ||
if (error) { | ||
return { type: 'error', error }; | ||
} | ||
let { leadingMargin, trailingMargin, ranges } = getIndentInfo(string, 3, string.length - 3); | ||
let indentSize = sharedIndentSize(ranges); | ||
let padding = []; | ||
let contentStart = offset + 3; | ||
let contentEnd = offset + string.length - 3; | ||
if (leadingMargin) { | ||
padding.push([contentStart, contentStart + leadingMargin]); | ||
} | ||
if (indentSize) { | ||
ranges.forEach(([start, end]) => { | ||
if (end - start >= indentSize) { | ||
padding.push([offset + start, offset + start + indentSize]); | ||
} | ||
}); | ||
} | ||
if (trailingMargin) { | ||
padding.push([contentEnd - trailingMargin, contentEnd]); | ||
} | ||
for (let i = padding.length - 1; i >= 0; i--) { | ||
let [ start, end ] = padding[i]; | ||
data = data.slice(0, start - contentStart) + data.slice(end - contentStart); | ||
} | ||
return { type: 'Herestring', data, padding }; | ||
} | ||
/** | ||
* @param {string} string | ||
* @param {string} quote | ||
* @returns {string} | ||
* @returns {*} | ||
*/ | ||
function parseQuotedString(string, quote) { | ||
if (string.slice(0, quote.length) !== quote || string.slice(-quote.length) !== quote) { | ||
throw new Error(`tried to parse quoted string not wrapped in quotes: ${string}`); | ||
return { | ||
type: 'error', | ||
error: { | ||
type: 'unbalanced-quotes', | ||
message: `tried to parse quoted string not wrapped in quotes: ${string}` | ||
} | ||
}; | ||
} | ||
@@ -43,3 +88,9 @@ | ||
} else { | ||
throw new Error(`found ${chr} when looking for a hex character`); | ||
return { | ||
type: 'error', | ||
error: { | ||
type: 'invalid-hex-character', | ||
message: `found ${chr} when looking for a hex character` | ||
} | ||
}; | ||
} | ||
@@ -84,7 +135,15 @@ } | ||
case 'x': | ||
result += String.fromCharCode(hex(2)); | ||
let x = hex(2); | ||
if (x.type === 'error') { | ||
return x; | ||
} | ||
result += String.fromCharCode(x); | ||
break; | ||
case 'u': | ||
result += String.fromCharCode(hex(4)); | ||
let u = hex(4); | ||
if (u.type === 'error') { | ||
return u; | ||
} | ||
result += String.fromCharCode(u); | ||
break; | ||
@@ -104,3 +163,9 @@ | ||
if (string.slice(p - 1, p - 1 + quote.length) === quote) { | ||
throw new Error('unexpected closing quote before the end of the string'); | ||
return { | ||
type: 'error', | ||
error: { | ||
type: 'unexpected-closing-quote', | ||
message: 'unexpected closing quote before the end of the string' | ||
} | ||
}; | ||
} else { | ||
@@ -117,3 +182,3 @@ result += chr; | ||
return result; | ||
return { type: 'string', data: result }; | ||
} | ||
@@ -123,6 +188,92 @@ | ||
* @param {string} string | ||
* @returns {number} | ||
* @returns {{type: string, data: number}} | ||
*/ | ||
function parseInteger(string) { | ||
return { type: 'int', data: parseInt(string, 10) }; | ||
} | ||
/** | ||
* @param {string} string | ||
* @returns {{type: string, data: number}} | ||
*/ | ||
function parseFloatingPoint(string) { | ||
return { type: 'float', data: parseFloat(string) }; | ||
} | ||
/** | ||
* @param {string} string | ||
* @returns {{type: string, data: number}} | ||
*/ | ||
function parseHexidecimal(string) { | ||
return parseInt(string.slice(2), 16); | ||
return { type: 'int', data: parseInt(string.slice(2), 16) }; | ||
} | ||
/** | ||
* @param {string} string | ||
* @returns {{type: string, data: number}} | ||
*/ | ||
function parseOctal(string) { | ||
return { type: 'int', data: parseInt(string.slice(2), 8) }; | ||
} | ||
/** | ||
* @param {string} source | ||
* @param {number=} start | ||
* @param {number=} end | ||
* @returns {{leadingMargin: number, trailingMargin: number, ranges: Array<Array<number>>}} | ||
*/ | ||
function getIndentInfo(source, start=0, end=source.length) { | ||
const ranges = []; | ||
let leadingMargin = 0; | ||
while (source[start + leadingMargin] === ' ') { | ||
leadingMargin += ' '.length; | ||
} | ||
if (source[start + leadingMargin] === '\n') { | ||
leadingMargin += '\n'.length; | ||
start += leadingMargin; | ||
} | ||
let trailingMargin = 0; | ||
while (source[end - trailingMargin - ' '.length] === ' ') { | ||
trailingMargin += ' '.length; | ||
} | ||
if (source[end - trailingMargin - '\n'.length] === '\n') { | ||
trailingMargin += '\n'.length; | ||
end -= trailingMargin; | ||
} | ||
for (let index = start; index < end; index++) { | ||
if (index === start || source[index - 1] === '\n') { | ||
if (source[index] !== '\n') { | ||
let start = index; | ||
while (source[index] === ' ') { | ||
index++; | ||
} | ||
ranges.push([start, index]); | ||
} | ||
} | ||
} | ||
return { | ||
leadingMargin, | ||
trailingMargin, | ||
ranges | ||
}; | ||
} | ||
/** | ||
* @param {Array<Array<number>>} ranges | ||
* @returns {number} | ||
*/ | ||
function sharedIndentSize(ranges) { | ||
let size = null; | ||
ranges.forEach(([start, end]) => { | ||
if (size === null || (start !== end && end - start < size)) { | ||
size = end - start; | ||
} | ||
}); | ||
return size === null ? 0 : size; | ||
} |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
45
51354
10
1448
1