fix: Added lint error for appsmith store mutations (#33484)

Co-authored-by: “sneha122” <“sneha@appsmith.com”>
This commit is contained in:
sneha122 2024-05-17 16:34:01 +05:30 committed by GitHub
parent 6179729e9a
commit bbfe4ffe70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 127 additions and 63 deletions

View File

@ -1,54 +0,0 @@
import {
PROPERTY_SELECTOR,
WIDGET,
getWidgetSelector,
} from "../../../../locators/WidgetLocators";
import {
entityExplorer,
jsEditor,
agHelper,
locators,
propPane,
} from "../../../../support/Objects/ObjectsCore";
describe(
"Linting warning for imperative store update",
{ tags: ["@tag.JS"] },
function () {
it("Shows lint error for imperative store update", function () {
entityExplorer.DragDropWidgetNVerify(WIDGET.BUTTON, 200, 200);
entityExplorer.DragDropWidgetNVerify(WIDGET.TEXT, 400, 400);
agHelper.GetNClick(getWidgetSelector(WIDGET.BUTTON));
propPane.TypeTextIntoField("Label", "{{appsmith.store.name = 6}}");
//Mouse hover to exact warning message
agHelper.AssertElementVisibility(locators._lintErrorElement);
agHelper.HoverElement(locators._lintErrorElement);
agHelper.AssertContains("Use storeValue() method to modify the store");
agHelper.Sleep();
//Create a JS object
jsEditor.CreateJSObject(
`export default {
myFun1: () => {
appsmith.store.name.text = 6
},
}`,
{
paste: true,
completeReplace: true,
toRun: false,
shouldCreateNewJSObj: true,
prettify: false,
},
);
agHelper.AssertElementVisibility(locators._lintErrorElement);
agHelper.HoverElement(locators._lintErrorElement);
agHelper.AssertContains("Use storeValue() method to modify the store");
agHelper.Sleep();
});
},
);

View File

@ -5,6 +5,7 @@ import type {
IdentifierInfo,
AssignmentExpressionData,
CallExpressionData,
MemberCallExpressionData,
} from "./src";
import {
isIdentifierNode,
@ -85,6 +86,7 @@ export type {
JSVarProperty,
JSFunctionProperty,
CallExpressionData,
MemberCallExpressionData,
};
export {

View File

@ -597,6 +597,16 @@ export interface CallExpressionData {
params: NodeWithLocation<MemberExpressionNode | LiteralNode>[];
}
// This interface is used for storing call expression nodes with callee as member node
// example of such case is when a function is called on object like obj.func()
// This is required to understand whether appsmith.store.test.func() is present in script
// in order to display mutation error on such statements.
export interface MemberCallExpressionData {
property: NodeWithLocation<MemberExpressionNode | LiteralNode>;
object: NodeWithLocation<MemberExpressionNode>;
parentNode: NodeWithLocation<CallExpressionNode>;
}
export interface AssignmentExpressionNode extends Node {
operator: string;
left: Expression;
@ -628,9 +638,11 @@ export const extractExpressionsFromCode = (
invalidTopLevelMemberExpressionsArray: MemberExpressionData[];
assignmentExpressionsData: AssignmentExpressionData[];
callExpressionsData: CallExpressionData[];
memberCallExpressionData: MemberCallExpressionData[];
} => {
const assignmentExpressionsData = new Set<AssignmentExpressionData>();
const callExpressionsData = new Set<CallExpressionData>();
const memberCallExpressionData = new Set<MemberCallExpressionData>();
const invalidTopLevelMemberExpressions = new Set<MemberExpressionData>();
const variableDeclarations = new Set<string>();
let functionalParams = new Set<string>();
@ -646,6 +658,7 @@ export const extractExpressionsFromCode = (
invalidTopLevelMemberExpressionsArray: [],
assignmentExpressionsData: [],
callExpressionsData: [],
memberCallExpressionData: [],
};
}
throw e;
@ -726,12 +739,24 @@ export const extractExpressionsFromCode = (
} as AssignmentExpressionData);
},
CallExpression(node: Node) {
if (isCallExpressionNode(node) && isIdentifierNode(node.callee)) {
if (isCallExpressionNode(node)) {
if (isIdentifierNode(node.callee)) {
callExpressionsData.add({
property: node.callee,
params: node.arguments,
} as CallExpressionData);
}
if (isMemberExpressionNode(node.callee)) {
const { object, property } = node.callee;
memberCallExpressionData.add({
object,
property,
parentNode: node,
} as MemberCallExpressionData);
}
}
},
});
@ -748,6 +773,7 @@ export const extractExpressionsFromCode = (
invalidTopLevelMemberExpressionsArray,
assignmentExpressionsData: [...assignmentExpressionsData],
callExpressionsData: [...callExpressionsData],
memberCallExpressionData: [...memberCallExpressionData],
};
};

View File

@ -1,4 +1,5 @@
import { getScriptType } from "workers/Evaluation/evaluate";
import { CustomLintErrorCode } from "../constants";
import getLintingErrors from "../utils/getLintingErrors";
describe("getLintingErrors", () => {
@ -102,4 +103,81 @@ describe("getLintingErrors", () => {
expect(lintErrors.length).toEqual(1);
});
});
describe("3. Verify lint errors are shown when mutations are performed on unmutable objects", () => {
const data = {
THIS_CONTEXT: {},
Input1: {
text: "inputValue",
ENTITY_TYPE: "WIDGET",
},
appsmith: {
store: {
test: "testValue",
},
},
};
it("1. Assigning values to input widget's properties", () => {
const originalBinding = "'myFun1() {\n\t\tInput1.text = \"\";\n\t}'";
const script =
'\n function $$closedFn () {\n const $$result = {myFun1() {\n\t\tInput1.text = "";\n\t}}\n return $$result\n }\n $$closedFn.call(THIS_CONTEXT)\n ';
const options = { isJsObject: true };
const scriptType = getScriptType(false, false);
const lintErrors = getLintingErrors({
data,
options,
originalBinding,
script,
scriptType,
});
expect(lintErrors.length).toEqual(1);
expect(lintErrors[0].code).toEqual(
CustomLintErrorCode.INVALID_ENTITY_PROPERTY,
);
});
it("2. Assigning values to appsmith store variables", () => {
const originalBinding = 'myFun1() {\n\t\tappsmith.store.test = "";\n\t}';
const script =
'\n function $$closedFn () {\n const $$result = {myFun1() {\n\t\tappsmith.store.test = "";\n\t}}\n return $$result\n }\n $$closedFn.call(THIS_CONTEXT)\n';
const options = { isJsObject: true };
const scriptType = getScriptType(false, false);
const lintErrors = getLintingErrors({
data,
options,
originalBinding,
script,
scriptType,
});
expect(lintErrors.length).toEqual(1);
expect(lintErrors[0].code).toEqual(
CustomLintErrorCode.INVALID_APPSMITH_STORE_PROPERTY_SETTER,
);
});
it("3. Mutating appsmith store values by calling any functions on it", () => {
const originalBinding =
"myFun1() {\n\t\tappsmith.store.test.push(1);\n\t}";
const script =
"\n function $$closedFn () {\n const $$result = {myFun1() {\n\t\tappsmith.store.test.push(1);\n\t}}\n return $$result\n }\n $$closedFn.call(THIS_CONTEXT)\n";
const options = { isJsObject: true };
const scriptType = getScriptType(false, false);
const lintErrors = getLintingErrors({
data,
options,
originalBinding,
script,
scriptType,
});
expect(lintErrors.length).toEqual(1);
expect(lintErrors[0].code).toEqual(
CustomLintErrorCode.INVALID_APPSMITH_STORE_PROPERTY_SETTER,
);
});
});
});

View File

@ -7,6 +7,7 @@ import type {
MemberExpressionData,
AssignmentExpressionData,
CallExpressionData,
MemberCallExpressionData,
} from "@shared/ast";
import {
extractExpressionsFromCode,
@ -271,20 +272,22 @@ function getInvalidWidgetPropertySetterErrors({
}
function getInvalidAppsmithStoreSetterErrors({
assignmentExpressions,
appsmithStoreMutationExpressions,
originalBinding,
script,
scriptPos,
}: {
data: Record<string, unknown>;
assignmentExpressions: AssignmentExpressionData[];
appsmithStoreMutationExpressions: Array<
AssignmentExpressionData | MemberCallExpressionData
>;
scriptPos: Position;
originalBinding: string;
script: string;
}) {
const assignmentExpressionErrors: LintError[] = [];
for (const { object, parentNode } of assignmentExpressions) {
for (const { object, parentNode } of appsmithStoreMutationExpressions) {
if (!isMemberExpressionNode(object)) continue;
const assignmentExpressionString = generate(parentNode);
if (!assignmentExpressionString.startsWith("appsmith.store")) continue;
@ -386,6 +389,7 @@ function getCustomErrorsFromScript(
let invalidTopLevelMemberExpressions: MemberExpressionData[] = [];
let assignmentExpressions: AssignmentExpressionData[] = [];
let callExpressions: CallExpressionData[] = [];
let memberCallExpressions: MemberCallExpressionData[] = [];
try {
const value = extractExpressionsFromCode(
script,
@ -396,6 +400,7 @@ function getCustomErrorsFromScript(
value.invalidTopLevelMemberExpressionsArray;
assignmentExpressions = value.assignmentExpressionsData;
callExpressions = value.callExpressionsData;
memberCallExpressions = value.memberCallExpressionData;
} catch (e) {}
const invalidWidgetPropertySetterErrors =
@ -416,9 +421,16 @@ function getCustomErrorsFromScript(
isJSObject,
);
// This ensures that all cases where appsmith.store is getting modified
// either by assignment using `appsmith.store.test = ""`
// or by calling a function like `appsmith.store.test.push()` will result in lint error
const appsmithStoreMutationExpressions: Array<
AssignmentExpressionData | MemberCallExpressionData
> = [...assignmentExpressions, ...memberCallExpressions];
const invalidAppsmithStorePropertyErrors =
getInvalidAppsmithStoreSetterErrors({
assignmentExpressions,
appsmithStoreMutationExpressions,
script,
scriptPos,
originalBinding,