add member expression

This commit is contained in:
Kurt Hutten IrevDev
2023-01-03 19:41:27 +11:00
parent d2a4bb7851
commit a1f844b0b1
8 changed files with 415 additions and 4 deletions

View File

@ -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',
},
},
},
],
},
])
})
}) })

View File

@ -290,6 +290,7 @@ export type Value =
| PipeSubstitution | PipeSubstitution
| ArrayExpression | ArrayExpression
| ObjectExpression | ObjectExpression
| MemberExpression
function makeValue( function makeValue(
tokens: Token[], tokens: Token[],
@ -331,8 +332,16 @@ function makeValue(
lastIndex: arrExp.lastIndex, lastIndex: arrExp.lastIndex,
} }
} }
if (currentToken.type === 'word' && nextToken.type === 'period') { if (
// TODO object access 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') { if (currentToken.type === 'word') {
const identifier = makeIdentifier(tokens, index) 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 { export interface BinaryExpression extends GeneralStatement {
type: 'BinaryExpression' type: 'BinaryExpression'
operator: string operator: string

View File

@ -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 // helpers

View File

@ -4,6 +4,7 @@ import {
BinaryExpression, BinaryExpression,
PipeExpression, PipeExpression,
ObjectExpression, ObjectExpression,
MemberExpression,
} from './abstractSyntaxTree' } from './abstractSyntaxTree'
import { Path, Transform, SketchGeo, sketchFns, ExtrudeGeo } from './sketch' import { Path, Transform, SketchGeo, sketchFns, ExtrudeGeo } from './sketch'
import { BufferGeometry, Quaternion, Vector3 } from 'three' import { BufferGeometry, Quaternion, Vector3 } from 'three'
@ -116,6 +117,11 @@ export const executor = (
}) })
return executor(fnInit.body, fnMemory, { bodyType: 'block' }).return 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') { } else if (declaration.init.type === 'CallExpression') {
const functionName = declaration.init.callee.name const functionName = declaration.init.callee.name
const fnArgs = declaration.init.arguments.map((arg) => { const fnArgs = declaration.init.arguments.map((arg) => {
@ -189,6 +195,10 @@ export const executor = (
functionName functionName
](...fnArgs) ](...fnArgs)
} }
} else {
throw new Error(
'Unsupported declaration type: ' + declaration.init.type
)
} }
}) })
} else if (statement.type === 'ExpressionStatement') { } else if (statement.type === 'ExpressionStatement') {
@ -240,6 +250,22 @@ export const executor = (
return _programMemory 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( function getBinaryExpressionResult(
expression: BinaryExpression, expression: BinaryExpression,
programMemory: ProgramMemory programMemory: ProgramMemory
@ -407,6 +433,11 @@ function executeObjectExpression(
) )
} else if (property.value.type === 'Identifier') { } else if (property.value.type === 'Identifier') {
obj[property.key.name] = _programMemory.root[property.value.name] obj[property.key.name] = _programMemory.root[property.value.name]
} else if (property.value.type === 'ObjectExpression') {
obj[property.key.name] = executeObjectExpression(
_programMemory,
property.value
)
} else { } else {
throw new Error( throw new Error(
`Unexpected property type ${property.value.type} in object expression` `Unexpected property type ${property.value.type} in object expression`

View File

@ -140,6 +140,16 @@ const yo = {
const recasted = recast(ast) const recasted = recast(ast)
expect(recasted).toBe(code.trim()) 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 // helpers

View File

@ -9,6 +9,7 @@ import {
SketchExpression, SketchExpression,
ArrayExpression, ArrayExpression,
ObjectExpression, ObjectExpression,
MemberExpression,
} from './abstractSyntaxTree' } from './abstractSyntaxTree'
export function recast( 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 { function recastValue(node: Value, indentation = ''): string {
if (node.type === 'BinaryExpression') { if (node.type === 'BinaryExpression') {
return recastBinaryExpression(node) return recastBinaryExpression(node)
@ -162,6 +180,8 @@ function recastValue(node: Value, indentation = ''): string {
return recastArrayExpression(node, indentation) return recastArrayExpression(node, indentation)
} else if (node.type === 'ObjectExpression') { } else if (node.type === 'ObjectExpression') {
return recastObjectExpression(node, indentation) return recastObjectExpression(node, indentation)
} else if (node.type === 'MemberExpression') {
return recastMemberExpression(node, indentation)
} else if (node.type === 'Literal') { } else if (node.type === 'Literal') {
return recastLiteral(node) return recastLiteral(node)
} else if (node.type === 'FunctionExpression') { } else if (node.type === 'FunctionExpression') {

View File

@ -366,6 +366,67 @@ describe('testing lexer', () => {
"brace '}' from 24 to 25", "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 // helpers

View File

@ -16,6 +16,7 @@ const ARRAY_START = /^\[/
const ARRAY_END = /^\]/ const ARRAY_END = /^\]/
const COMMA = /^,/ const COMMA = /^,/
const COLON = /^:/ const COLON = /^:/
const PERIOD = /^\./
export const isNumber = (character: string) => NUMBER.test(character) export const isNumber = (character: string) => NUMBER.test(character)
export const isWhitespace = (character: string) => WHITESPACE.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 isArrayEnd = (character: string) => ARRAY_END.test(character)
export const isComma = (character: string) => COMMA.test(character) export const isComma = (character: string) => COMMA.test(character)
export const isColon = (character: string) => COLON.test(character) export const isColon = (character: string) => COLON.test(character)
export const isPeriod = (character: string) => PERIOD.test(character)
function matchFirst(str: string, regex: RegExp) { function matchFirst(str: string, regex: RegExp) {
const theMatch = str.match(regex) const theMatch = str.match(regex)
@ -49,6 +51,7 @@ export interface Token {
| 'whitespace' | 'whitespace'
| 'comma' | 'comma'
| 'colon' | 'colon'
| 'period'
value: string value: string
start: number start: number
end: number end: number
@ -100,9 +103,10 @@ const returnTokenAtIndex = (str: string, startIndex: number): Token | null => {
if (isWord(strFromIndex)) { if (isWord(strFromIndex)) {
return makeToken('word', matchFirst(strFromIndex, WORD), startIndex) return makeToken('word', matchFirst(strFromIndex, WORD), startIndex)
} }
if (isColon(strFromIndex)) { if (isColon(strFromIndex))
return makeToken('colon', matchFirst(strFromIndex, COLON), startIndex) return makeToken('colon', matchFirst(strFromIndex, COLON), startIndex)
} if (isPeriod(strFromIndex))
return makeToken('period', matchFirst(strFromIndex, PERIOD), startIndex)
if (isWhitespace(strFromIndex)) { if (isWhitespace(strFromIndex)) {
return makeToken( return makeToken(
'whitespace', 'whitespace',