fix: parsing of nested parentheses in TableComputeValue expressions (#40326)

## Problem
The table compute value parser was unable to handle expressions with
nested parentheses, causing incorrect extraction of computation
expressions when functions with multiple parameters or nested function
calls were used. This caused issues when users tried to use more complex
expressions like `JSObject1.somefunction(currentRow["id"] || 0)`.

## Solution
Implemented a proper nested parentheses tracking algorithm that counts
opening and closing parentheses to find the correct end of the
computation expression, rather than simply looking for the first closing
parenthesis sequence.

## Why This Approach
This solution is robust because it:
1. Properly handles any level of nested parentheses in function calls
2. Maintains backward compatibility with existing simple expressions
3. Provides a more accurate way to extract the computation expression

## Testing
- Added a new test case with nested parentheses:
`JSObject1.somefunction(currentRow["id"] || 0)`
- Ensured existing test cases still pass
- Manually verified with complex expressions containing multiple nested
parentheses

This fix enables users to write more complex table computations
involving function calls with multiple parameters and conditions.


Fixes #40265 

## Automation

/ok-to-test tags="@tag.Table, @tag.Widget, @tag.Binding, @tag.Sanity,
@tag.PropertyPane"

### 🔍 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/14610729968>
> Commit: abbbaebfe5cf723109bee517e5f6f0cebf96a74a
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14610729968&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Table, @tag.Widget, @tag.Binding, @tag.Sanity,
@tag.PropertyPane`
> Spec:
> <hr>Wed, 23 Apr 2025 07:45:26 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

- **Bug Fixes**
- Improved handling of computed values in table controls to correctly
process nested parentheses and avoid errors from malformed expressions.

- **Tests**
- Added test cases for complex computed expressions with nested
parentheses, logical operators, and malformed inputs to ensure
robustness.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Vivekanand Ilango <vivek@Vivekanands-MacBook-Pro.local>
Co-authored-by: Vivekanand Ilango <vivek.ilango@appsmith.com>
This commit is contained in:
Jacques Ikot 2025-04-25 03:10:03 -07:00 committed by GitHub
parent 25979c8deb
commit f87f17e610
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 107 additions and 4 deletions

View File

@ -4,6 +4,7 @@ describe("ComputeTablePropertyControlV2.getInputComputedValue", () => {
const tableName = "Table1";
const inputVariations = [
"currentRow.price",
`JSObject1.somefunction(currentRow["id"] || 0)`,
`
[
{
@ -58,4 +59,45 @@ describe("ComputeTablePropertyControlV2.getInputComputedValue", () => {
ComputeTablePropertyControlV2.getInputComputedValue(computedValue),
).toBe(`{{currentRow.quantity}}{{5}}`);
});
it("3. should handle nested parentheses correctly", () => {
const input =
"JSObject1.complexFunction(currentRow.id, JSObject2.helperFunction(currentRow.name))";
const computedValue = ComputeTablePropertyControlV2.getTableComputeBinding(
tableName,
input,
);
expect(
ComputeTablePropertyControlV2.getInputComputedValue(computedValue),
).toBe(`{{${input}}}`);
});
it("4. should handle malformed expressions with unbalanced parentheses", () => {
// Create a malformed expression by manually crafting a bad computedValue string
// Removing the proper closing parenthesis structure
const malformedComputedValue =
"{{(() => { const tableData = Table1.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (currentRow.function(unbalanced)) : fallback })";
// The function should gracefully handle this and return the original string
// rather than throwing an error or returning a partial/incorrect result
expect(
ComputeTablePropertyControlV2.getInputComputedValue(
malformedComputedValue,
),
).toBe(malformedComputedValue);
});
it("5. should correctly parse complex but valid expressions with multiple nested parentheses", () => {
const complexInput =
"JSObject1.process(currentRow.value1, (currentRow.value2 || getDefault()), JSObject2.format(currentRow.value3))";
const computedValue = ComputeTablePropertyControlV2.getTableComputeBinding(
tableName,
complexInput,
);
expect(
ComputeTablePropertyControlV2.getInputComputedValue(computedValue),
).toBe(`{{${complexInput}}}`);
});
});

View File

@ -161,25 +161,86 @@ class ComputeTablePropertyControlV2 extends BaseControl<ComputeTablePropertyCont
if (!isComputedValue) return propertyValue;
// Check if the entire structure of the expression looks valid before attempting to parse
if (!this.isLikelyValidComputedValue(propertyValue)) {
return propertyValue;
}
// Extract the computation logic from the full binding string
// Input example: "{{(() => { const tableData = Table1.processedTableData || []; return tableData.length > 0 ? tableData.map((currentRow, currentIndex) => (currentRow.price * 2)) : currentRow.price * 2 })()}}"
const mapSignatureIndex = propertyValue.indexOf(MAP_FUNCTION_SIGNATURE);
// Find the actual computation expression between the map parentheses
const computationStart = mapSignatureIndex + MAP_FUNCTION_SIGNATURE.length;
const computationEnd = propertyValue.indexOf("))", computationStart);
const { endIndex, isValid } = this.findMatchingClosingParenthesis(
propertyValue,
computationStart,
);
// Handle case where no matching closing parenthesis is found
if (!isValid) {
// If we can't find the proper closing parenthesis, fall back to returning the original value
// This prevents errors when the expression is malformed
return propertyValue;
}
// Extract the computation expression between the map parentheses
// Note: At this point, we're just extracting the raw expression like "currentRow.price * 2"
// The actual removal of "currentRow." prefix happens later in JSToString()
const computationExpression = propertyValue.substring(
computationStart,
computationEnd,
endIndex,
);
return JSToString(computationExpression);
};
/**
* Check if the computed value string looks structurally valid
* This helps catch obviously malformed expressions early
*/
private static isLikelyValidComputedValue(value: string): boolean {
// Check for basic structural elements that should be present
const hasOpeningStructure = value.includes("(() => {");
const hasTableDataAssignment = value.includes("const tableData =");
const hasReturnStatement = value.includes("return tableData.length > 0 ?");
const hasClosingStructure = value.includes("})()}}");
return (
hasOpeningStructure &&
hasTableDataAssignment &&
hasReturnStatement &&
hasClosingStructure
);
}
/**
* Utility function to find the matching closing parenthesis
* @param text - The text to search in
* @param startIndex - The index after the opening parenthesis
* @returns Object containing the index of the matching closing parenthesis and whether it was found
*/
private static findMatchingClosingParenthesis(
text: string,
startIndex: number,
) {
let openParenCount = 1; // Start with 1 for the opening parenthesis
for (let i = startIndex; i < text.length; i++) {
if (text[i] === "(") {
openParenCount++;
} else if (text[i] === ")") {
openParenCount--;
if (openParenCount === 0) {
return { endIndex: i, isValid: true };
}
}
}
// No matching closing parenthesis found
return { endIndex: text.length, isValid: false };
}
getComputedValue = (value: string, tableName: string) => {
// Return raw value if:
// 1. The value is not a dynamic binding (not wrapped in {{...}})