PromucFlow_constructor/app/client/packages/ast/src/peekOverlay/utils.ts
Anand Srinivasan 9dd015a1e6
feat: peek overlay nested properties + perf improvements (#23414)
Fixes #23057
Fixes #23054

## Description
TL;DR Added support for peeking on nested properties. e.g.
`Api1.data[0].id`.

This won't work when:
-  local variables are involved in the expression. 
e.g. `Api1.data[x].id` won't support peeking at the variable `[x]` or
anything after that.
- library code is involved e.g. `moment`, `_` etc...
- when functions are called. e.g. Api1.data[0].id.toFixed()

Because these cases requires evaluation.

<img width="355" alt="image"
src="https://github.com/appsmithorg/appsmith/assets/66776129/d09d1f0d-1692-46f5-8ec1-592f4fe75f7a">

#### Media (old vs new)
https://www.loom.com/share/dedcf113439c4ee2a19028acca54045e




## Performance improvements:
- Use AST to identify expressions instead marking text manually.
- This reduces the number of markers we process (~ half).

- Before

![image](https://github.com/appsmithorg/appsmith/assets/66776129/bb16ac6b-46dd-4e39-8524-e4f4fa2c3243)

- After

![image](https://github.com/appsmithorg/appsmith/assets/66776129/28f0f209-5437-4718-a74a-f025c576afda)

- AST logs
https://www.loom.com/share/ddde93233cc8470ea04309d8a8332240

#### Type of change
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)

## Testing
>
#### How Has This Been Tested?
- [x] Manual
- [x] Jest
- [x] Cypress
>
>
#### Test Plan
https://github.com/appsmithorg/TestSmith/issues/2402

#### Issues raised during DP testing

https://github.com/appsmithorg/appsmith/pull/23414#issuecomment-1553164908

## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [x] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Test-plan-implementation#speedbreaker-features-to-consider-for-every-change)
have been covered
- [x] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans/_edit#areas-of-interest)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [x] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [x] Cypress test cases have been added and approved by SDET/manual QA
- [x] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
2023-05-26 17:12:10 +05:30

185 lines
5.8 KiB
TypeScript

import type { Node } from "acorn";
import type {
BinaryExpressionNode,
CallExpressionNode,
ConditionalExpressionNode,
ExpressionStatement,
IdentifierNode,
MemberExpressionNode,
} from "../index";
import { isAwaitExpressionNode } from "../index";
import { isBinaryExpressionNode } from "../index";
import { isConditionalExpressionNode } from "../index";
import {
isCallExpressionNode,
isExpressionStatementNode,
isIdentifierNode,
isMemberExpressionNode,
isThisExpressionNode,
} from "../index";
import * as escodegen from "escodegen";
import { NodeTypes } from "../constants/ast";
import type { PeekOverlayExpressionIdentifierOptions } from "./index";
export const isPositionWithinNode = (node: Node, pos: number) =>
pos >= node.start && pos <= node.end;
export const getExpressionStringAtPos = (
node: Node,
pos: number,
options?: PeekOverlayExpressionIdentifierOptions,
replaceThisExpression = true,
): string | undefined => {
if (!isPositionWithinNode(node, pos)) return;
if (isMemberExpressionNode(node)) {
return getExpressionAtPosFromMemberExpression(
node,
pos,
options,
replaceThisExpression,
);
} else if (isExpressionStatementNode(node)) {
return getExpressionAtPosFromExpressionStatement(node, pos, options);
} else if (isCallExpressionNode(node)) {
return getExpressionAtPosFromCallExpression(node, pos, options);
} else if (isBinaryExpressionNode(node)) {
return getExpressionAtPosFromBinaryExpression(node, pos, options);
} else if (isAwaitExpressionNode(node)) {
return getExpressionStringAtPos(node.argument, pos, options);
} else if (isConditionalExpressionNode(node)) {
return getExpressionAtPosFromConditionalExpression(node, pos, options);
} else if (isIdentifierNode(node)) {
return removeSemiColon(escodegen.generate(node));
}
};
const getExpressionAtPosFromMemberExpression = (
node: MemberExpressionNode,
pos: number,
options?: PeekOverlayExpressionIdentifierOptions,
replaceThisExpression = true,
): string | undefined => {
const objectNode = node.object;
if (isLocalVariableNode(node) || isLocalVariableNode(objectNode)) return;
if (replaceThisExpression && options?.thisExpressionReplacement) {
node = replaceThisinMemberExpression(node, options);
}
// stop if objectNode is a function call -> needs evaluation
if (isCallExpressionNode(objectNode)) return;
// position is within the object node
if (pos <= objectNode.end) {
return getExpressionStringAtPos(objectNode, pos, options, false);
}
// position is within the property node
else {
const propertyNode = node.property;
if (isMemberExpressionNode(propertyNode)) {
return getExpressionAtPosFromMemberExpression(
propertyNode,
pos,
options,
false,
);
}
// generate string for the whole path
return escodegen.generate(node);
}
};
const getExpressionAtPosFromExpressionStatement = (
node: ExpressionStatement,
pos: number,
options?: PeekOverlayExpressionIdentifierOptions,
): string | undefined => {
if (
isThisExpressionNode(node.expression) &&
options?.thisExpressionReplacement
) {
node.expression = thisReplacementNode(node.expression, options);
}
return getExpressionStringAtPos(node.expression, pos, options);
};
const getExpressionAtPosFromCallExpression = (
node: CallExpressionNode,
pos: number,
options?: PeekOverlayExpressionIdentifierOptions,
): string | undefined => {
let selectedNode: Node | undefined;
// function call -> needs evaluation
// if (isPositionWithinNode(node.callee, pos)) {
// selectedNode = node.callee;
// }
if (node.arguments.length > 0) {
const argumentNode = node.arguments.find((node) =>
isPositionWithinNode(node, pos),
);
if (argumentNode) {
selectedNode = argumentNode;
}
}
return selectedNode && getExpressionStringAtPos(selectedNode, pos, options);
};
const getExpressionAtPosFromConditionalExpression = (
node: ConditionalExpressionNode,
pos: number,
options?: PeekOverlayExpressionIdentifierOptions,
): string | undefined => {
let selectedNode: Node | undefined;
if (isPositionWithinNode(node.test, pos)) {
selectedNode = node.test;
} else if (isPositionWithinNode(node.consequent, pos)) {
selectedNode = node.consequent;
} else if (isPositionWithinNode(node.alternate, pos)) {
selectedNode = node.alternate;
}
return selectedNode && getExpressionStringAtPos(selectedNode, pos, options);
};
const getExpressionAtPosFromBinaryExpression = (
node: BinaryExpressionNode,
pos: number,
options?: PeekOverlayExpressionIdentifierOptions,
): string | undefined => {
let selectedNode: Node | undefined;
if (isPositionWithinNode(node.left, pos)) {
selectedNode = node.left;
} else if (isPositionWithinNode(node.right, pos)) {
selectedNode = node.right;
}
return selectedNode && getExpressionStringAtPos(selectedNode, pos, options);
};
export const replaceThisinMemberExpression = (
node: MemberExpressionNode,
options: PeekOverlayExpressionIdentifierOptions,
): MemberExpressionNode => {
if (isMemberExpressionNode(node.object)) {
node.object = replaceThisinMemberExpression(node.object, options);
} else if (isThisExpressionNode(node.object)) {
node.object = thisReplacementNode(node.object, options);
}
return node;
};
// replace "this" node with the provided replacement
const thisReplacementNode = (
node: Node,
options: PeekOverlayExpressionIdentifierOptions,
) => {
return {
...node,
type: NodeTypes.Identifier,
name: options.thisExpressionReplacement,
} as IdentifierNode;
};
const removeSemiColon = (value: string) =>
value.slice(-1) === ";" ? value.slice(0, value.length - 1) : value;
const isLocalVariableNode = (node: Node) =>
isMemberExpressionNode(node) &&
node.computed &&
isIdentifierNode(node.property);