Replace values with variable helper (#84)

* Refactor getNodePathFromSourceRange

getNodePathFromSourceRange wouldn't go as deep as it should have,
stopping at pipe expressions, when it should have followed as deep
into the ast as possible.

The fact that it stopped early then had other part of the code base that
expected this behaviour and it effected a lot, so a rather large refactor

* overhaul of getNodePathFromSourceRange

* quick fix for moreNodePathFromSourceRange

* minor bugs in moreNodePathFromSourceRange

* couple more tests

* add moveValueIntoNewVariable

* add UI for replacing valuse with variable

* update button text
This commit is contained in:
Kurt Hutten
2023-04-01 16:47:00 +11:00
committed by GitHub
parent 42eb3506bb
commit 0593afc4ff
16 changed files with 936 additions and 168 deletions

View File

@ -1,16 +1,28 @@
import { PathToNode, ProgramMemory } from './executor'
import { Range } from '../useStore'
import { Program } from './abstractSyntaxTree'
import { splitPathAtLastIndex } from './modifyAst'
import {
BinaryExpression,
Program,
SyntaxType,
Value,
CallExpression,
ExpressionStatement,
VariableDeclaration,
ReturnStatement,
ArrayExpression,
Identifier,
} from './abstractSyntaxTree'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
export function getNodeFromPath<T>(
node: Program,
path: (string | number)[],
stopAt: string = '',
path: PathToNode,
stopAt: string | string[] = '',
returnEarly = false
): {
node: T
path: PathToNode
shallowPath: PathToNode
deepPath: PathToNode
} {
let currentNode = node as any
let stopAtNode = null
@ -18,21 +30,26 @@ export function getNodeFromPath<T>(
let pathsExplored: PathToNode = []
for (const pathItem of path) {
try {
if (typeof currentNode[pathItem] !== 'object')
if (typeof currentNode[pathItem[0]] !== 'object')
throw new Error('not an object')
currentNode = currentNode[pathItem]
currentNode = currentNode?.[pathItem[0]]
successfulPaths.push(pathItem)
if (!stopAtNode) {
pathsExplored.push(pathItem)
}
if (currentNode.type === stopAt) {
if (
Array.isArray(stopAt)
? stopAt.includes(currentNode.type)
: currentNode.type === stopAt
) {
// it will match the deepest node of the type
// instead of returning at the first match
stopAtNode = currentNode
if (returnEarly) {
return {
node: stopAtNode,
path: pathsExplored,
shallowPath: pathsExplored,
deepPath: successfulPaths,
}
}
}
@ -48,13 +65,14 @@ export function getNodeFromPath<T>(
}
return {
node: stopAtNode || currentNode,
path: pathsExplored,
shallowPath: pathsExplored,
deepPath: successfulPaths,
}
}
export function getNodeFromPathCurry(
node: Program,
path: (string | number)[]
path: PathToNode
): <T>(
stopAt: string,
returnEarly?: boolean
@ -63,18 +81,171 @@ export function getNodeFromPathCurry(
path: PathToNode
} {
return <T>(stopAt: string = '', returnEarly = false) => {
return getNodeFromPath<T>(node, path, stopAt, returnEarly)
const { node: _node, shallowPath } = getNodeFromPath<T>(
node,
path,
stopAt,
returnEarly
)
return {
node: _node,
path: shallowPath,
}
}
}
function moreNodePathFromSourceRange(
node: Value | ExpressionStatement | VariableDeclaration | ReturnStatement,
sourceRange: Range,
previousPath: PathToNode = [['body', '']]
): PathToNode {
const [start, end] = sourceRange
let path: PathToNode = [...previousPath]
const _node = { ...node }
if (start < _node.start || end > _node.end) return path
const isInRange = _node.start <= start && _node.end >= end
if ((_node.type === 'Identifier' || _node.type === 'Literal') && isInRange)
return path
if (_node.type === 'CallExpression' && isInRange) {
const { callee, arguments: args } = _node
if (
callee.type === 'Identifier' &&
callee.start <= start &&
callee.end >= end
) {
path.push(['callee', 'CallExpression'])
return path
}
if (args.length > 0) {
for (let argIndex = 0; argIndex < args.length; argIndex++) {
const arg = args[argIndex]
if (arg.start <= start && arg.end >= end) {
path.push(['arguments', 'CallExpression'])
path.push([argIndex, 'index'])
return moreNodePathFromSourceRange(arg, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'BinaryExpression' && isInRange) {
const { left, right } = _node
if (left.start <= start && left.end >= end) {
path.push(['left', 'BinaryExpression'])
return moreNodePathFromSourceRange(left, sourceRange, path)
}
if (right.start <= start && right.end >= end) {
path.push(['right', 'BinaryExpression'])
return moreNodePathFromSourceRange(right, sourceRange, path)
}
return path
}
if (_node.type === 'PipeExpression' && isInRange) {
const { body } = _node
for (let i = 0; i < body.length; i++) {
const pipe = body[i]
if (pipe.start <= start && pipe.end >= end) {
path.push(['body', 'PipeExpression'])
path.push([i, 'index'])
return moreNodePathFromSourceRange(pipe, sourceRange, path)
}
}
return path
}
if (_node.type === 'ArrayExpression' && isInRange) {
const { elements } = _node
for (let elIndex = 0; elIndex < elements.length; elIndex++) {
const element = elements[elIndex]
if (element.start <= start && element.end >= end) {
path.push(['elements', 'ArrayExpression'])
path.push([elIndex, 'index'])
return moreNodePathFromSourceRange(element, sourceRange, path)
}
}
return path
}
if (_node.type === 'ObjectExpression' && isInRange) {
const { properties } = _node
for (let propIndex = 0; propIndex < properties.length; propIndex++) {
const property = properties[propIndex]
if (property.start <= start && property.end >= end) {
path.push(['properties', 'ObjectExpression'])
path.push([propIndex, 'index'])
if (property.key.start <= start && property.key.end >= end) {
path.push(['key', 'Property'])
return moreNodePathFromSourceRange(property.key, sourceRange, path)
}
if (property.value.start <= start && property.value.end >= end) {
path.push(['value', 'Property'])
return moreNodePathFromSourceRange(property.value, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'ExpressionStatement' && isInRange) {
const { expression } = _node
path.push(['expression', 'ExpressionStatement'])
return moreNodePathFromSourceRange(expression, sourceRange, path)
}
if (_node.type === 'VariableDeclaration' && isInRange) {
const declarations = _node.declarations
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
const declaration = declarations[decIndex]
if (declaration.start <= start && declaration.end >= end) {
path.push(['declarations', 'VariableDeclaration'])
path.push([decIndex, 'index'])
const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path)
}
}
}
}
if (_node.type === 'VariableDeclaration' && isInRange) {
const declarations = _node.declarations
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
const declaration = declarations[decIndex]
if (declaration.start <= start && declaration.end >= end) {
const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push(['declarations', 'VariableDeclaration'])
path.push([decIndex, 'index'])
path.push(['init', ''])
return moreNodePathFromSourceRange(init, sourceRange, path)
}
}
}
return path
}
if (_node.type === 'UnaryExpression' && isInRange) {
const { argument } = _node
if (argument.start <= start && argument.end >= end) {
path.push(['argument', 'UnaryExpression'])
return moreNodePathFromSourceRange(argument, sourceRange, path)
}
return path
}
console.error('not implemented')
return path
}
export function getNodePathFromSourceRange(
node: Program,
sourceRange: Range,
previousPath: PathToNode = []
previousPath: PathToNode = [['body', '']]
): PathToNode {
const [start, end] = sourceRange
let path: PathToNode = [...previousPath, 'body']
let path: PathToNode = [...previousPath]
const _node = { ...node }
// loop over each statement in body getting the index with a for loop
for (
let statementIndex = 0;
@ -83,53 +254,8 @@ export function getNodePathFromSourceRange(
) {
const statement = _node.body[statementIndex]
if (statement.start <= start && statement.end >= end) {
path.push(statementIndex)
if (statement.type === 'ExpressionStatement') {
const expression = statement.expression
if (expression.start <= start && expression.end >= end) {
path.push('expression')
if (expression.type === 'CallExpression') {
const callee = expression.callee
if (callee.start <= start && callee.end >= end) {
path.push('callee')
if (callee.type === 'Identifier') {
}
}
}
}
} else if (statement.type === 'VariableDeclaration') {
const declarations = statement.declarations
for (let decIndex = 0; decIndex < declarations.length; decIndex++) {
const declaration = declarations[decIndex]
if (declaration.start <= start && declaration.end >= end) {
path.push('declarations')
path.push(decIndex)
const init = declaration.init
if (init.start <= start && init.end >= end) {
path.push('init')
if (init.type === 'PipeExpression') {
const body = init.body
for (let pipeIndex = 0; pipeIndex < body.length; pipeIndex++) {
const pipe = body[pipeIndex]
if (pipe.start <= start && pipe.end >= end) {
path.push('body')
path.push(pipeIndex)
}
}
} else if (init.type === 'CallExpression') {
const callee = init.callee
if (callee.start <= start && callee.end >= end) {
path.push('callee')
if (callee.type === 'Identifier') {
}
}
}
}
}
}
}
path.push([statementIndex, 'index'])
return moreNodePathFromSourceRange(statement, sourceRange, path)
}
}
return path
@ -151,7 +277,11 @@ export function findAllPreviousVariables(
insertIndex: number
} {
const path = getNodePathFromSourceRange(ast, sourceRange)
const { path: pathToDec } = getNodeFromPath(ast, path, 'VariableDeclaration')
const { shallowPath: pathToDec } = getNodeFromPath(
ast,
path,
'VariableDeclaration'
)
const { index: insertIndex, path: bodyPath } = splitPathAtLastIndex(pathToDec)
const { node: bodyItems } = getNodeFromPath<Program['body']>(ast, bodyPath)
@ -174,3 +304,98 @@ export function findAllPreviousVariables(
variables,
}
}
type ReplacerFn = (_ast: Program, varName: string) => { modifiedAst: Program }
export function isNodeSafeToReplace(
ast: Program,
sourceRange: [number, number]
): {
isSafe: boolean
value: Value
replacer: ReplacerFn
} {
let path = getNodePathFromSourceRange(ast, sourceRange)
if (path[path.length - 1][0] === 'callee') {
path = path.slice(0, -1)
}
const acceptedNodeTypes = [
'BinaryExpression',
'Identifier',
'CallExpression',
'Literal',
]
const { node: value, deepPath: outPath } = getNodeFromPath(
ast,
path,
acceptedNodeTypes
)
const { node: binValue, shallowPath: outBinPath } = getNodeFromPath(
ast,
path,
'BinaryExpression'
)
// binaryExpression should take precedence
const [finVal, finPath] =
(binValue as Value)?.type === 'BinaryExpression'
? [binValue, outBinPath]
: [value, outPath]
const replaceNodeWithIdentifier: ReplacerFn = (_ast, varName) => {
const identifier = createIdentifier(varName)
const last = finPath[finPath.length - 1]
const startPath = finPath.slice(0, -1)
const nodeToReplace = getNodeFromPath(_ast, startPath).node as any
nodeToReplace[last[0]] = identifier
return { modifiedAst: _ast }
}
const hasPipeSub = isTypeInValue(finVal as Value, 'PipeSubstitution')
const isIdentifierCallee = path[path.length - 1][0] !== 'callee'
return {
isSafe:
!hasPipeSub &&
isIdentifierCallee &&
acceptedNodeTypes.includes((finVal as any)?.type) &&
finPath.map(([_, type]) => type).includes('VariableDeclaration'),
value: finVal as Value,
replacer: replaceNodeWithIdentifier,
}
}
export function isTypeInValue(node: Value, syntaxType: SyntaxType): boolean {
if (node.type === syntaxType) return true
if (node.type === 'BinaryExpression') return isTypeInBinExp(node, syntaxType)
if (node.type === 'CallExpression') return isTypeInCallExp(node, syntaxType)
if (node.type === 'ArrayExpression') return isTypeInArrayExp(node, syntaxType)
return false
}
function isTypeInBinExp(
node: BinaryExpression,
syntaxType: SyntaxType
): boolean {
if (node.type === syntaxType) return true
if (node.left.type === syntaxType) return true
if (node.right.type === syntaxType) return true
return (
isTypeInValue(node.left, syntaxType) ||
isTypeInValue(node.right, syntaxType)
)
}
function isTypeInCallExp(
node: CallExpression,
syntaxType: SyntaxType
): boolean {
if (node.callee.type === syntaxType) return true
return node.arguments.some((arg) => isTypeInValue(arg, syntaxType))
}
function isTypeInArrayExp(
node: ArrayExpression,
syntaxType: SyntaxType
): boolean {
return node.elements.some((el) => isTypeInValue(el, syntaxType))
}