add member expression
This commit is contained in:
@ -1350,4 +1350,174 @@ describe('testing pipe operator special', () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
test('object memberExpression simple', () => {
|
||||
const code = `const prop = yo.one.two`
|
||||
const tokens = lexer(code)
|
||||
const { body } = abstractSyntaxTree(tokens)
|
||||
expect(body).toEqual([
|
||||
{
|
||||
type: 'VariableDeclaration',
|
||||
start: 0,
|
||||
end: 23,
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
start: 6,
|
||||
end: 23,
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
start: 6,
|
||||
end: 10,
|
||||
name: 'prop',
|
||||
},
|
||||
init: {
|
||||
type: 'MemberExpression',
|
||||
start: 13,
|
||||
end: 23,
|
||||
computed: false,
|
||||
object: {
|
||||
type: 'MemberExpression',
|
||||
start: 13,
|
||||
end: 19,
|
||||
computed: false,
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
start: 13,
|
||||
end: 15,
|
||||
name: 'yo',
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
start: 16,
|
||||
end: 19,
|
||||
name: 'one',
|
||||
},
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
start: 20,
|
||||
end: 23,
|
||||
name: 'two',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
test('object memberExpression with square braces', () => {
|
||||
const code = `const prop = yo.one["two"]`
|
||||
const tokens = lexer(code)
|
||||
const { body } = abstractSyntaxTree(tokens)
|
||||
expect(body).toEqual([
|
||||
{
|
||||
type: 'VariableDeclaration',
|
||||
start: 0,
|
||||
end: 26,
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
start: 6,
|
||||
end: 26,
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
start: 6,
|
||||
end: 10,
|
||||
name: 'prop',
|
||||
},
|
||||
init: {
|
||||
type: 'MemberExpression',
|
||||
start: 13,
|
||||
end: 26,
|
||||
computed: false,
|
||||
object: {
|
||||
type: 'MemberExpression',
|
||||
start: 13,
|
||||
end: 19,
|
||||
computed: false,
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
start: 13,
|
||||
end: 15,
|
||||
name: 'yo',
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
start: 16,
|
||||
end: 19,
|
||||
name: 'one',
|
||||
},
|
||||
},
|
||||
property: {
|
||||
type: 'Literal',
|
||||
start: 20,
|
||||
end: 25,
|
||||
value: 'two',
|
||||
raw: '"two"',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
test('object memberExpression with two square braces literal and identifier', () => {
|
||||
const code = `const prop = yo["one"][two]`
|
||||
const tokens = lexer(code)
|
||||
const { body } = abstractSyntaxTree(tokens)
|
||||
expect(body).toEqual([
|
||||
{
|
||||
type: 'VariableDeclaration',
|
||||
start: 0,
|
||||
end: 27,
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
start: 6,
|
||||
end: 27,
|
||||
id: {
|
||||
type: 'Identifier',
|
||||
start: 6,
|
||||
end: 10,
|
||||
name: 'prop',
|
||||
},
|
||||
init: {
|
||||
type: 'MemberExpression',
|
||||
start: 13,
|
||||
end: 27,
|
||||
computed: true,
|
||||
object: {
|
||||
type: 'MemberExpression',
|
||||
start: 13,
|
||||
end: 22,
|
||||
computed: false,
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
start: 13,
|
||||
end: 15,
|
||||
name: 'yo',
|
||||
},
|
||||
property: {
|
||||
type: 'Literal',
|
||||
start: 16,
|
||||
end: 21,
|
||||
value: 'one',
|
||||
raw: '"one"',
|
||||
},
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
start: 23,
|
||||
end: 26,
|
||||
name: 'two',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
@ -290,6 +290,7 @@ export type Value =
|
||||
| PipeSubstitution
|
||||
| ArrayExpression
|
||||
| ObjectExpression
|
||||
| MemberExpression
|
||||
|
||||
function makeValue(
|
||||
tokens: Token[],
|
||||
@ -331,8 +332,16 @@ function makeValue(
|
||||
lastIndex: arrExp.lastIndex,
|
||||
}
|
||||
}
|
||||
if (currentToken.type === 'word' && nextToken.type === 'period') {
|
||||
// TODO object access
|
||||
if (
|
||||
currentToken.type === 'word' &&
|
||||
(nextToken.type === 'period' ||
|
||||
(nextToken.type === 'brace' && nextToken.value === '['))
|
||||
) {
|
||||
const memberExpression = makeMemberExpression(tokens, index)
|
||||
return {
|
||||
value: memberExpression.expression,
|
||||
lastIndex: memberExpression.lastIndex,
|
||||
}
|
||||
}
|
||||
if (currentToken.type === 'word') {
|
||||
const identifier = makeIdentifier(tokens, index)
|
||||
@ -615,6 +624,100 @@ function makeObjectProperties(
|
||||
])
|
||||
}
|
||||
|
||||
export interface MemberExpression extends GeneralStatement {
|
||||
type: 'MemberExpression'
|
||||
object: MemberExpression | Identifier
|
||||
property: Identifier | Literal
|
||||
computed: boolean
|
||||
}
|
||||
|
||||
function makeMemberExpression(
|
||||
tokens: Token[],
|
||||
index: number
|
||||
): { expression: MemberExpression; lastIndex: number } {
|
||||
const currentToken = tokens[index]
|
||||
const keysInfo = collectObjectKeys(tokens, index)
|
||||
const lastKey = keysInfo[keysInfo.length - 1]
|
||||
const firstKey = keysInfo.shift()
|
||||
if (!firstKey) throw new Error('Expected a key')
|
||||
const root = makeIdentifier(tokens, index)
|
||||
let memberExpression: MemberExpression = {
|
||||
type: 'MemberExpression',
|
||||
start: currentToken.start,
|
||||
end: tokens[firstKey.index].end,
|
||||
object: root,
|
||||
property: firstKey.key,
|
||||
computed: firstKey.computed,
|
||||
}
|
||||
keysInfo.forEach(({ key, computed, index }, i) => {
|
||||
const endToken = tokens[index]
|
||||
memberExpression = {
|
||||
type: 'MemberExpression',
|
||||
start: currentToken.start,
|
||||
end: endToken.end,
|
||||
object: memberExpression,
|
||||
property: key,
|
||||
computed,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
expression: memberExpression,
|
||||
lastIndex: lastKey.index,
|
||||
}
|
||||
}
|
||||
|
||||
interface ObjectKeyInfo {
|
||||
key: Identifier | Literal
|
||||
index: number
|
||||
computed: boolean
|
||||
}
|
||||
|
||||
function collectObjectKeys(
|
||||
tokens: Token[],
|
||||
index: number,
|
||||
previousKeys: ObjectKeyInfo[] = []
|
||||
): ObjectKeyInfo[] {
|
||||
const nextToken = nextMeaningfulToken(tokens, index)
|
||||
const periodOrOpeningBracketToken =
|
||||
nextToken?.token?.type === 'brace' && nextToken.token.value === ']'
|
||||
? nextMeaningfulToken(tokens, nextToken.index)
|
||||
: nextToken
|
||||
if (
|
||||
periodOrOpeningBracketToken?.token?.type !== 'period' &&
|
||||
periodOrOpeningBracketToken?.token?.type !== 'brace'
|
||||
) {
|
||||
return previousKeys
|
||||
}
|
||||
const keyToken = nextMeaningfulToken(
|
||||
tokens,
|
||||
periodOrOpeningBracketToken.index
|
||||
)
|
||||
const nextPeriodOrOpeningBracketToken = nextMeaningfulToken(
|
||||
tokens,
|
||||
keyToken.index
|
||||
)
|
||||
const isBraced =
|
||||
nextPeriodOrOpeningBracketToken?.token?.type === 'brace' &&
|
||||
nextPeriodOrOpeningBracketToken?.token?.value === ']'
|
||||
const endIndex = isBraced
|
||||
? nextPeriodOrOpeningBracketToken.index
|
||||
: keyToken.index
|
||||
const key =
|
||||
keyToken.token.type === 'word'
|
||||
? makeIdentifier(tokens, keyToken.index)
|
||||
: makeLiteral(tokens, keyToken.index)
|
||||
const computed = isBraced && keyToken.token.type === 'word' ? true : false
|
||||
return collectObjectKeys(tokens, keyToken.index, [
|
||||
...previousKeys,
|
||||
{
|
||||
key,
|
||||
index: endIndex,
|
||||
computed,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
export interface BinaryExpression extends GeneralStatement {
|
||||
type: 'BinaryExpression'
|
||||
operator: string
|
||||
|
@ -206,6 +206,18 @@ show(mySketch)
|
||||
},
|
||||
})
|
||||
})
|
||||
it('execute memberExpression', () => {
|
||||
const code = ["const yo = {a: {b: '123'}}", "const myVar = yo.a['b']"].join(
|
||||
'\n'
|
||||
)
|
||||
const { root } = exe(code)
|
||||
expect(root).toEqual({
|
||||
yo: {
|
||||
a: { b: '123' },
|
||||
},
|
||||
myVar: '123',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// helpers
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
BinaryExpression,
|
||||
PipeExpression,
|
||||
ObjectExpression,
|
||||
MemberExpression,
|
||||
} from './abstractSyntaxTree'
|
||||
import { Path, Transform, SketchGeo, sketchFns, ExtrudeGeo } from './sketch'
|
||||
import { BufferGeometry, Quaternion, Vector3 } from 'three'
|
||||
@ -116,6 +117,11 @@ export const executor = (
|
||||
})
|
||||
return executor(fnInit.body, fnMemory, { bodyType: 'block' }).return
|
||||
}
|
||||
} else if (declaration.init.type === 'MemberExpression') {
|
||||
_programMemory.root[variableName] = getMemberExpressionResult(
|
||||
declaration.init,
|
||||
_programMemory
|
||||
)
|
||||
} else if (declaration.init.type === 'CallExpression') {
|
||||
const functionName = declaration.init.callee.name
|
||||
const fnArgs = declaration.init.arguments.map((arg) => {
|
||||
@ -189,6 +195,10 @@ export const executor = (
|
||||
functionName
|
||||
](...fnArgs)
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unsupported declaration type: ' + declaration.init.type
|
||||
)
|
||||
}
|
||||
})
|
||||
} else if (statement.type === 'ExpressionStatement') {
|
||||
@ -240,6 +250,22 @@ export const executor = (
|
||||
return _programMemory
|
||||
}
|
||||
|
||||
function getMemberExpressionResult(
|
||||
expression: MemberExpression,
|
||||
programMemory: ProgramMemory
|
||||
) {
|
||||
const propertyName = (
|
||||
expression.property.type === 'Identifier'
|
||||
? expression.property.name
|
||||
: expression.property.value
|
||||
) as any
|
||||
const object: any =
|
||||
expression.object.type === 'MemberExpression'
|
||||
? getMemberExpressionResult(expression.object, programMemory)
|
||||
: programMemory.root[expression.object.name]
|
||||
return object[propertyName]
|
||||
}
|
||||
|
||||
function getBinaryExpressionResult(
|
||||
expression: BinaryExpression,
|
||||
programMemory: ProgramMemory
|
||||
@ -407,6 +433,11 @@ function executeObjectExpression(
|
||||
)
|
||||
} else if (property.value.type === 'Identifier') {
|
||||
obj[property.key.name] = _programMemory.root[property.value.name]
|
||||
} else if (property.value.type === 'ObjectExpression') {
|
||||
obj[property.key.name] = executeObjectExpression(
|
||||
_programMemory,
|
||||
property.value
|
||||
)
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unexpected property type ${property.value.type} in object expression`
|
||||
|
@ -140,6 +140,16 @@ const yo = {
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
})
|
||||
it('recast object execution with member expression', () => {
|
||||
const code = `const yo = { a: { b: { c: '123' } } }
|
||||
const key = 'c'
|
||||
const myVar = yo.a['b'][key]
|
||||
const key2 = 'b'
|
||||
const myVar2 = yo['a'][key2].c`
|
||||
const { ast } = code2ast(code)
|
||||
const recasted = recast(ast)
|
||||
expect(recasted).toBe(code.trim())
|
||||
})
|
||||
})
|
||||
|
||||
// helpers
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
SketchExpression,
|
||||
ArrayExpression,
|
||||
ObjectExpression,
|
||||
MemberExpression,
|
||||
} from './abstractSyntaxTree'
|
||||
|
||||
export function recast(
|
||||
@ -155,6 +156,23 @@ ${recast(expression.body, '', indentation + ' ')}
|
||||
}`
|
||||
}
|
||||
|
||||
function recastMemberExpression(
|
||||
expression: MemberExpression,
|
||||
indentation: string
|
||||
): string {
|
||||
// TODO handle breaking into multiple lines if too long
|
||||
let keyString =
|
||||
expression.computed && expression.property.type === 'Identifier'
|
||||
? `[${expression.property.name}]`
|
||||
: expression.property.type !== 'Identifier'
|
||||
? `[${expression.property.raw}]`
|
||||
: `.${expression.property.name}`
|
||||
if (expression.object.type === 'MemberExpression') {
|
||||
return recastMemberExpression(expression.object, indentation) + keyString
|
||||
}
|
||||
return expression.object.name + keyString
|
||||
}
|
||||
|
||||
function recastValue(node: Value, indentation = ''): string {
|
||||
if (node.type === 'BinaryExpression') {
|
||||
return recastBinaryExpression(node)
|
||||
@ -162,6 +180,8 @@ function recastValue(node: Value, indentation = ''): string {
|
||||
return recastArrayExpression(node, indentation)
|
||||
} else if (node.type === 'ObjectExpression') {
|
||||
return recastObjectExpression(node, indentation)
|
||||
} else if (node.type === 'MemberExpression') {
|
||||
return recastMemberExpression(node, indentation)
|
||||
} else if (node.type === 'Literal') {
|
||||
return recastLiteral(node)
|
||||
} else if (node.type === 'FunctionExpression') {
|
||||
|
@ -366,6 +366,67 @@ describe('testing lexer', () => {
|
||||
"brace '}' from 24 to 25",
|
||||
])
|
||||
})
|
||||
it('testing object property access', () => {
|
||||
const result = stringSummaryLexer(`const yo = {key: 'value'}
|
||||
const prop = yo.key
|
||||
const prop2 = yo['key']
|
||||
const key = 'key'
|
||||
const prop3 = yo[key]`)
|
||||
expect(result).toEqual([
|
||||
"word 'const' from 0 to 5",
|
||||
"whitespace ' ' from 5 to 6",
|
||||
"word 'yo' from 6 to 8",
|
||||
"whitespace ' ' from 8 to 9",
|
||||
"operator '=' from 9 to 10",
|
||||
"whitespace ' ' from 10 to 11",
|
||||
"brace '{' from 11 to 12",
|
||||
"word 'key' from 12 to 15",
|
||||
"colon ':' from 15 to 16",
|
||||
"whitespace ' ' from 16 to 17",
|
||||
"string ''value'' from 17 to 24",
|
||||
"brace '}' from 24 to 25",
|
||||
"whitespace '\n' from 25 to 26",
|
||||
"word 'const' from 26 to 31",
|
||||
"whitespace ' ' from 31 to 32",
|
||||
"word 'prop' from 32 to 36",
|
||||
"whitespace ' ' from 36 to 37",
|
||||
"operator '=' from 37 to 38",
|
||||
"whitespace ' ' from 38 to 39",
|
||||
"word 'yo' from 39 to 41",
|
||||
"period '.' from 41 to 42",
|
||||
"word 'key' from 42 to 45",
|
||||
"whitespace '\n' from 45 to 46",
|
||||
"word 'const' from 46 to 51",
|
||||
"whitespace ' ' from 51 to 52",
|
||||
"word 'prop2' from 52 to 57",
|
||||
"whitespace ' ' from 57 to 58",
|
||||
"operator '=' from 58 to 59",
|
||||
"whitespace ' ' from 59 to 60",
|
||||
"word 'yo' from 60 to 62",
|
||||
"brace '[' from 62 to 63",
|
||||
"string ''key'' from 63 to 68",
|
||||
"brace ']' from 68 to 69",
|
||||
"whitespace '\n' from 69 to 70",
|
||||
"word 'const' from 70 to 75",
|
||||
"whitespace ' ' from 75 to 76",
|
||||
"word 'key' from 76 to 79",
|
||||
"whitespace ' ' from 79 to 80",
|
||||
"operator '=' from 80 to 81",
|
||||
"whitespace ' ' from 81 to 82",
|
||||
"string ''key'' from 82 to 87",
|
||||
"whitespace '\n' from 87 to 88",
|
||||
"word 'const' from 88 to 93",
|
||||
"whitespace ' ' from 93 to 94",
|
||||
"word 'prop3' from 94 to 99",
|
||||
"whitespace ' ' from 99 to 100",
|
||||
"operator '=' from 100 to 101",
|
||||
"whitespace ' ' from 101 to 102",
|
||||
"word 'yo' from 102 to 104",
|
||||
"brace '[' from 104 to 105",
|
||||
"word 'key' from 105 to 108",
|
||||
"brace ']' from 108 to 109",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// helpers
|
||||
|
@ -16,6 +16,7 @@ const ARRAY_START = /^\[/
|
||||
const ARRAY_END = /^\]/
|
||||
const COMMA = /^,/
|
||||
const COLON = /^:/
|
||||
const PERIOD = /^\./
|
||||
|
||||
export const isNumber = (character: string) => NUMBER.test(character)
|
||||
export const isWhitespace = (character: string) => WHITESPACE.test(character)
|
||||
@ -30,6 +31,7 @@ export const isArrayStart = (character: string) => ARRAY_START.test(character)
|
||||
export const isArrayEnd = (character: string) => ARRAY_END.test(character)
|
||||
export const isComma = (character: string) => COMMA.test(character)
|
||||
export const isColon = (character: string) => COLON.test(character)
|
||||
export const isPeriod = (character: string) => PERIOD.test(character)
|
||||
|
||||
function matchFirst(str: string, regex: RegExp) {
|
||||
const theMatch = str.match(regex)
|
||||
@ -49,6 +51,7 @@ export interface Token {
|
||||
| 'whitespace'
|
||||
| 'comma'
|
||||
| 'colon'
|
||||
| 'period'
|
||||
value: string
|
||||
start: number
|
||||
end: number
|
||||
@ -100,9 +103,10 @@ const returnTokenAtIndex = (str: string, startIndex: number): Token | null => {
|
||||
if (isWord(strFromIndex)) {
|
||||
return makeToken('word', matchFirst(strFromIndex, WORD), startIndex)
|
||||
}
|
||||
if (isColon(strFromIndex)) {
|
||||
if (isColon(strFromIndex))
|
||||
return makeToken('colon', matchFirst(strFromIndex, COLON), startIndex)
|
||||
}
|
||||
if (isPeriod(strFromIndex))
|
||||
return makeToken('period', matchFirst(strFromIndex, PERIOD), startIndex)
|
||||
if (isWhitespace(strFromIndex)) {
|
||||
return makeToken(
|
||||
'whitespace',
|
||||
|
Reference in New Issue
Block a user