PromucFlow_constructor/app/client/packages/ast/src/index.test.ts
Ashit Rath 70f8777afd
chore: Compute default value for jsaction params (#34708)
## Description
This PR adds the values to jsArguments. The logic for this is
- If the value is string then it is kept as is
- For non-strings they are wrapped with `{{ }}` do maintain the data
type integrity when evaluated.

This property is currently not used anywhere in the platform and this is
intended to be used by js modules to identify the default values of
parameters and provide support to alter then in a UI in the app.

This PR also splits `workers/Evaluation/getJSActionForEvalContext.ts` to
override in the EE for modules

PR for https://github.com/appsmithorg/appsmith-ee/pull/4612

## Automation

/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/9919551354>
> Commit: c6ab372477fb3fd2f1ce171729af4fa64ac2a487
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9919551354&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Sat, 13 Jul 2024 12:16:08 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added support for additional node types (`RestElement`,
`ObjectPattern`, `ArrayPattern`) in our AST processing.
- Introduced `addPropertiesToJSObjectCode` function to enhance
JavaScript object property management.

- **Updates**
- Enhanced `myFun2` function with new parameters and default values to
improve flexibility and usage.
- Improved `parseJSObject` function with additional parameters for
better functionality.

- **Tests**
- Added a new test suite for `addPropertiesToJSObjectCode` function to
ensure robust property management in JavaScript objects.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-07-15 21:01:42 +05:30

790 lines
22 KiB
TypeScript

import { parseJSObject } from "../index";
import {
extractIdentifierInfoFromCode,
getMemberExpressionObjectFromProperty,
isFunctionPresent,
} from "../src/index";
describe("getAllIdentifiers", () => {
it("works properly", () => {
const cases: Array<{
script: string;
expectedResults: string[];
invalidIdentifiers?: Record<string, unknown>;
}> = [
{
// Entity reference
script: "DirectTableReference",
expectedResults: ["DirectTableReference"],
},
{
// One level nesting
script: "TableDataReference.data",
expectedResults: ["TableDataReference.data"],
},
{
// Deep nesting
script: "TableDataDetailsReference.data.details",
expectedResults: ["TableDataDetailsReference.data.details"],
},
{
// Deep nesting
script: "TableDataDetailsMoreReference.data.details.more",
expectedResults: ["TableDataDetailsMoreReference.data.details.more"],
},
{
// Deep optional chaining
script: "TableDataOptionalReference.data?.details.more",
expectedResults: ["TableDataOptionalReference.data"],
},
{
// Deep optional chaining with logical operator
script:
"TableDataOptionalWithLogical.data?.details.more || FallbackTableData.data",
expectedResults: [
"TableDataOptionalWithLogical.data",
"FallbackTableData.data",
],
},
{
// null coalescing
script: "TableDataOptionalWithLogical.data ?? FallbackTableData.data",
expectedResults: [
"TableDataOptionalWithLogical.data",
"FallbackTableData.data",
],
},
{
// Basic map function
script: "Table5.data.map(c => ({ name: c.name }))",
expectedResults: ["Table5.data.map"],
},
{
// Literal property search
script: "Table6['data']",
expectedResults: ["Table6"],
},
{
// Deep literal property search
script: "TableDataOptionalReference['data'].details",
expectedResults: ["TableDataOptionalReference"],
},
{
// Array index search
script: "array[8]",
expectedResults: ["array[8]"],
},
{
// Deep array index search
script: "Table7.data[4]",
expectedResults: ["Table7.data[4]"],
},
{
// Deep array index search
script: "Table7.data[4].value",
expectedResults: ["Table7.data[4].value"],
},
{
// string literal and array index search
script: "Table['data'][9]",
expectedResults: ["Table"],
},
{
// array index and string literal search
script: "Array[9]['data']",
expectedResults: [],
invalidIdentifiers: {
Array: true,
},
},
{
// Index identifier search
script: "Table8.data[row][name]",
expectedResults: ["Table8.data", "row"],
// name is a global scoped variable
invalidIdentifiers: {
name: true,
},
},
{
// Index identifier search with global
script: "Table9.data[appsmith.store.row]",
expectedResults: ["Table9.data", "appsmith.store.row"],
},
{
// Index literal with further nested lookups
script: "Table10.data[row].name",
expectedResults: ["Table10.data", "row"],
},
{
// IIFE and if conditions
script:
"(function(){ if(Table11.isVisible) { return Api1.data } else { return Api2.data } })()",
expectedResults: ["Table11.isVisible", "Api1.data", "Api2.data"],
},
{
// Functions and arguments
script: "JSObject1.run(Api1.data, Api2.data)",
expectedResults: ["JSObject1.run", "Api1.data", "Api2.data"],
},
{
// IIFE - without braces
script: `function() {
const index = Input1.text
const obj = {
"a": 123
}
return obj[index]
}()`,
expectedResults: ["Input1.text"],
},
{
// IIFE
script: `(function() {
const index = Input2.text
const obj = {
"a": 123
}
return obj[index]
})()`,
expectedResults: ["Input2.text"],
},
{
// arrow IIFE - without braces - will fail
script: `() => {
const index = Input3.text
const obj = {
"a": 123
}
return obj[index]
}()`,
expectedResults: [],
},
{
// arrow IIFE
script: `(() => {
const index = Input4.text
const obj = {
"a": 123
}
return obj[index]
})()`,
expectedResults: ["Input4.text"],
},
{
// Direct object access
script: `{ "a": 123 }[Input5.text]`,
expectedResults: ["Input5.text"],
},
{
// Function declaration and default arguments
script: `function run(apiData = Api1.data) {
return apiData;
}`,
expectedResults: ["Api1.data"],
},
{
// Function declaration with arguments
script: `function run(data) {
return data;
}`,
expectedResults: [],
},
{
// anonymous function with variables
script: `() => {
let row = 0;
const data = {};
while(row < 10) {
data["test__" + row] = Table12.data[row];
row = row += 1;
}
}`,
expectedResults: ["Table12.data"],
},
{
// function with variables
script: `function myFunction() {
let row = 0;
const data = {};
while(row < 10) {
data["test__" + row] = Table13.data[row];
row = row += 1;
}
}`,
expectedResults: ["Table13.data"],
},
{
// expression with arithmetic operations
script: `Table14.data + 15`,
expectedResults: ["Table14.data"],
},
{
// expression with logical operations
script: `Table15.data || [{}]`,
expectedResults: ["Table15.data"],
},
// JavaScript built in classes should not be valid identifiers
{
script: `function(){
const firstApiRun = Api1.run();
const secondApiRun = Api2.run();
const randomNumber = Math.random();
return Promise.all([firstApiRun, secondApiRun])
}()`,
expectedResults: ["Api1.run", "Api2.run"],
invalidIdentifiers: {
Math: true,
Promise: true,
},
},
// Global dependencies should not be valid identifiers
{
script: `function(){
const names = [["john","doe"],["Jane","dane"]];
const flattenedNames = _.flatten(names);
return {flattenedNames, time: moment()}
}()`,
expectedResults: [],
invalidIdentifiers: {
_: true,
moment: true,
},
},
// browser Apis should not be valid identifiers
{
script: `function(){
const names = {
firstName: "John",
lastName:"Doe"
};
const joinedName = Object.values(names).join(" ");
console.log(joinedName)
return Api2.name
}()`,
expectedResults: ["Api2.name"],
invalidIdentifiers: {
Object: true,
console: true,
},
},
// identifiers and member expressions derived from params should not be valid identifiers
{
script: `function(a, b){
return a.name + b.name
}()`,
expectedResults: [],
},
// identifiers and member expressions derived from local variables should not be valid identifiers
{
script: `function(){
const a = "variableA";
const b = "variableB";
return a.length + b.length
}()`,
expectedResults: [],
},
// "appsmith" is an internal identifier and should be a valid reference
{
script: `function(){
return appsmith.user
}()`,
expectedResults: ["appsmith.user"],
},
];
// commenting to trigger test shared workflow action
cases.forEach((perCase) => {
const { references } = extractIdentifierInfoFromCode(
perCase.script,
2,
perCase.invalidIdentifiers,
);
expect(references).toStrictEqual(perCase.expectedResults);
});
});
});
describe("parseJSObjectWithAST", () => {
it("parse js object", () => {
const body = `export default{
myVar1: [],
myVar2: {},
myFun1: () => {
//write code here
},
myFun2: async () => {
//use async-await or promises
}
}`;
const expectedParsedObject = [
{
key: "myVar1",
value: "[]",
rawContent: "myVar1: []",
type: "ArrayExpression",
position: {
startLine: 2,
startColumn: 1,
endLine: 2,
endColumn: 11,
keyStartLine: 2,
keyEndLine: 2,
keyStartColumn: 1,
keyEndColumn: 7,
},
},
{
key: "myVar2",
value: "{}",
rawContent: "myVar2: {}",
type: "ObjectExpression",
position: {
startLine: 3,
startColumn: 1,
endLine: 3,
endColumn: 11,
keyStartLine: 3,
keyEndLine: 3,
keyStartColumn: 1,
keyEndColumn: 7,
},
},
{
key: "myFun1",
value: "() => {}",
rawContent: "myFun1: () => {\n\t\t//write code here\n\t}",
type: "ArrowFunctionExpression",
position: {
startLine: 4,
startColumn: 1,
endLine: 6,
endColumn: 2,
keyStartLine: 4,
keyEndLine: 4,
keyStartColumn: 1,
keyEndColumn: 7,
},
arguments: [],
isMarkedAsync: false,
},
{
key: "myFun2",
value: "async () => {}",
rawContent:
"myFun2: async () => {\n\t\t//use async-await or promises\n\t}",
type: "ArrowFunctionExpression",
position: {
startLine: 7,
startColumn: 1,
endLine: 9,
endColumn: 2,
keyStartLine: 7,
keyEndLine: 7,
keyStartColumn: 1,
keyEndColumn: 7,
},
arguments: [],
isMarkedAsync: true,
},
];
const { parsedObject } = parseJSObject(body);
expect(parsedObject).toStrictEqual(expectedParsedObject);
});
it("parse js object with literal", () => {
const body = `export default{
myVar1: [],
myVar2: {
"a": "app",
},
myFun1: () => {
//write code here
},
myFun2: async () => {
//use async-await or promises
}
}`;
const expectedParsedObject = [
{
key: "myVar1",
value: "[]",
rawContent: "myVar1: []",
type: "ArrayExpression",
position: {
startLine: 2,
startColumn: 1,
endLine: 2,
endColumn: 11,
keyStartLine: 2,
keyEndLine: 2,
keyStartColumn: 1,
keyEndColumn: 7,
},
},
{
key: "myVar2",
value: '{\n "a": "app"\n}',
rawContent: 'myVar2: {\n\t\t"a": "app",\n\t}',
type: "ObjectExpression",
position: {
startLine: 3,
startColumn: 1,
endLine: 5,
endColumn: 2,
keyStartLine: 3,
keyEndLine: 3,
keyStartColumn: 1,
keyEndColumn: 7,
},
},
{
key: "myFun1",
value: "() => {}",
rawContent: "myFun1: () => {\n\t\t//write code here\n\t}",
type: "ArrowFunctionExpression",
position: {
startLine: 6,
startColumn: 1,
endLine: 8,
endColumn: 2,
keyStartLine: 6,
keyEndLine: 6,
keyStartColumn: 1,
keyEndColumn: 7,
},
arguments: [],
isMarkedAsync: false,
},
{
key: "myFun2",
value: "async () => {}",
rawContent:
"myFun2: async () => {\n\t\t//use async-await or promises\n\t}",
type: "ArrowFunctionExpression",
position: {
startLine: 9,
startColumn: 1,
endLine: 11,
endColumn: 2,
keyStartLine: 9,
keyEndLine: 9,
keyStartColumn: 1,
keyEndColumn: 7,
},
arguments: [],
isMarkedAsync: true,
},
];
const { parsedObject } = parseJSObject(body);
expect(parsedObject).toStrictEqual(expectedParsedObject);
});
it("parse js object with variable declaration inside function", () => {
const body = `export default{
myFun1: () => {
const a = {
conditions: [],
requires: 1,
testFunc: () => {},
testFunc2: function(){}
};
},
myFun2: async () => {
//use async-await or promises
}
}`;
const expectedParsedObject = [
{
key: "myFun1",
value:
"() => {\n" +
" const a = {\n" +
" conditions: [],\n" +
" requires: 1,\n" +
" testFunc: () => {},\n" +
" testFunc2: function () {}\n" +
" };\n" +
"}",
rawContent:
"myFun1: () => {\n" +
" const a = {\n" +
" conditions: [],\n" +
" requires: 1,\n" +
" testFunc: () => {},\n" +
" testFunc2: function(){}\n" +
" };\n" +
" }",
type: "ArrowFunctionExpression",
position: {
startLine: 2,
startColumn: 6,
endLine: 9,
endColumn: 7,
keyStartLine: 2,
keyEndLine: 2,
keyStartColumn: 6,
keyEndColumn: 12,
},
arguments: [],
isMarkedAsync: false,
},
{
key: "myFun2",
value: "async () => {}",
rawContent:
"myFun2: async () => {\n //use async-await or promises\n }",
type: "ArrowFunctionExpression",
position: {
startLine: 10,
startColumn: 6,
endLine: 12,
endColumn: 7,
keyStartLine: 10,
keyEndLine: 10,
keyStartColumn: 6,
keyEndColumn: 12,
},
arguments: [],
isMarkedAsync: true,
},
];
const { parsedObject } = parseJSObject(body);
expect(parsedObject).toStrictEqual(expectedParsedObject);
});
it("parse js object with params of all types", () => {
const body = `export default{
myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue(), k = "Hello", l = 10, m = null, n = "hello" + 500, o = true, p = () => "arrow function", { o1 = 20, o2 }, [ a1, a2 = 30 ], { k1 = 20, k2 = 40 } = { k1: 500, k2: 600 }, [ g1 = 5, g2 ] = [], ...rest) => {
//use async-await or promises
},
}`;
const expectedParsedObject = [
{
key: "myFun2",
value: `async (a, b = Array(1, 2, 3), c = \"\", d = [], e = this.myVar1, f = {}, g = function () {}, h = Object.assign({}), i = String(), j = storeValue(), k = \"Hello\", l = 10, m = null, n = \"hello\" + 500, o = true, p = () => \"arrow function\", {o1 = 20, o2}, [a1, a2 = 30], {k1 = 20, k2 = 40} = {
k1: 500,
k2: 600
}, [g1 = 5, g2] = [], ...rest) => {}`,
rawContent:
'myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue(), k = "Hello", l = 10, m = null, n = "hello" + 500, o = true, p = () => "arrow function", { o1 = 20, o2 }, [ a1, a2 = 30 ], { k1 = 20, k2 = 40 } = { k1: 500, k2: 600 }, [ g1 = 5, g2 ] = [], ...rest) => {\n' +
" //use async-await or promises\n" +
" }",
type: "ArrowFunctionExpression",
position: {
startLine: 2,
startColumn: 6,
endLine: 4,
endColumn: 7,
keyStartLine: 2,
keyEndLine: 2,
keyStartColumn: 6,
keyEndColumn: 12,
},
arguments: [
{ paramName: "a", defaultValue: undefined },
{ paramName: "b", defaultValue: "{{Array(1,2,3)}}" },
{ paramName: "c", defaultValue: "" },
{ paramName: "d", defaultValue: "{{[]}}" },
{ paramName: "e", defaultValue: "{{this.myVar1}}" },
{ paramName: "f", defaultValue: "{{{}}}" },
{ paramName: "g", defaultValue: "{{function(){}}}" },
{ paramName: "h", defaultValue: "{{Object.assign({})}}" },
{ paramName: "i", defaultValue: "{{String()}}" },
{ paramName: "j", defaultValue: "{{storeValue()}}" },
{ paramName: "k", defaultValue: "Hello" },
{ paramName: "l", defaultValue: "{{10}}" },
{ paramName: "m", defaultValue: "{{null}}" },
{ paramName: "n", defaultValue: '{{"hello" + 500}}' },
{ paramName: "o", defaultValue: "{{true}}" },
{ paramName: "p", defaultValue: '{{() => "arrow function"}}' },
{ paramName: "", defaultValue: "{{{}}}" },
{ paramName: "", defaultValue: "{{[]}}" },
{ paramName: "", defaultValue: undefined },
{ paramName: "", defaultValue: undefined },
{ paramName: "rest", defaultValue: undefined },
],
isMarkedAsync: true,
},
];
const { parsedObject } = parseJSObject(body);
expect(parsedObject).toStrictEqual(expectedParsedObject);
});
});
describe("isFunctionPresent", () => {
it("should return true if function is present", () => {
const code = "function myFun(){}";
const result = isFunctionPresent(code, 2);
expect(result).toBe(true);
});
it("should return true if arrow function is present", () => {
const code = "const myFun = () => {}";
const result = isFunctionPresent(code, 2);
expect(result).toBe(true);
});
it("should return false if function is absent", () => {
const code = "const a = { key: 'value' }";
const result = isFunctionPresent(code, 2);
expect(result).toBe(false);
});
it("should return false for a string", () => {
const code = "Hello world {{appsmith.store.name}}!!";
const result = isFunctionPresent(code, 2);
expect(result).toBe(false);
});
it("should return true for shorthand arrow function", () => {
const code = "const myFun = () => 'value'";
const result = isFunctionPresent(code, 2);
expect(result).toBe(true);
});
it("should return true for IFFE function", () => {
const code = "(function myFun(){ console.log('hello') })()";
const result = isFunctionPresent(code, 2);
expect(result).toBe(true);
});
it("should return true for functions with parameters", () => {
const code = "function myFun(arg1, arg2){ console.log(arg1, arg2); }";
const result = isFunctionPresent(code, 2);
expect(result).toBe(true);
});
it("should return true for functions with parameters", () => {
const code = "function myFun(arg1, arg2){ console.log(arg1, arg2); }";
const result = isFunctionPresent(code, 2);
expect(result).toBe(true);
});
it("should return true for higher order functions", () => {
const code = "function myFun(cb){ const val = cb(); }";
const result = isFunctionPresent(code, 2);
expect(result).toBe(true);
});
it("should return true for functions with promises", () => {
const code = "async function myFun(promise){ const val = await promise; }";
const result = isFunctionPresent(code, 2);
expect(result).toBe(true);
});
});
describe("getMemberExpressionObjectFromProperty", () => {
it("returns an empty array for empty property name", () => {
const code = `function(){
const test = Api1.data.test
return "Favour"
}`;
const propertyName = "";
const actualResponse = getMemberExpressionObjectFromProperty(
propertyName,
code,
);
expect(actualResponse).toStrictEqual([]);
});
it("returns an empty array for invalid js", () => {
const code = `function(){
const test = Api1.data.test
return "Favour"
`;
const propertyName = "test";
const actualResponse = getMemberExpressionObjectFromProperty(
propertyName,
code,
);
expect(actualResponse).toStrictEqual([]);
});
it("returns correct member expression object(s)", () => {
const testData = [
{
code: `function(){
const test = Api1.data.test
return "Favour"
}`,
propertyName: "test",
expectedResponse: ["Api1.data"],
},
{
code: `function(){
const test = Api1.data.test;
Button1.test;
return "Favour"
}`,
propertyName: "test",
expectedResponse: ["Api1.data", "Button1"],
},
{
code: `function(){
// const test = Api1.data.test
return "Favour"
}`,
propertyName: "test",
expectedResponse: [],
},
{
code: `function(){
const test = Api1.data.test.now
return "Favour"
}`,
propertyName: "test",
expectedResponse: ["Api1.data"],
},
{
code: `function(){
const test = Api1.data["test"]
return "Favour"
}`,
propertyName: "test",
expectedResponse: ["Api1.data"],
},
];
for (const testDatum of testData) {
const actualResponse = getMemberExpressionObjectFromProperty(
testDatum.propertyName,
testDatum.code,
);
expect(actualResponse).toStrictEqual(testDatum.expectedResponse);
}
});
});