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, IdentifierInfo,
AssignmentExpressionData, AssignmentExpressionData,
CallExpressionData, CallExpressionData,
MemberCallExpressionData,
} from "./src"; } from "./src";
import { import {
isIdentifierNode, isIdentifierNode,
@ -85,6 +86,7 @@ export type {
JSVarProperty, JSVarProperty,
JSFunctionProperty, JSFunctionProperty,
CallExpressionData, CallExpressionData,
MemberCallExpressionData,
}; };
export { export {

View File

@ -597,6 +597,16 @@ export interface CallExpressionData {
params: NodeWithLocation<MemberExpressionNode | LiteralNode>[]; 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 { export interface AssignmentExpressionNode extends Node {
operator: string; operator: string;
left: Expression; left: Expression;
@ -628,9 +638,11 @@ export const extractExpressionsFromCode = (
invalidTopLevelMemberExpressionsArray: MemberExpressionData[]; invalidTopLevelMemberExpressionsArray: MemberExpressionData[];
assignmentExpressionsData: AssignmentExpressionData[]; assignmentExpressionsData: AssignmentExpressionData[];
callExpressionsData: CallExpressionData[]; callExpressionsData: CallExpressionData[];
memberCallExpressionData: MemberCallExpressionData[];
} => { } => {
const assignmentExpressionsData = new Set<AssignmentExpressionData>(); const assignmentExpressionsData = new Set<AssignmentExpressionData>();
const callExpressionsData = new Set<CallExpressionData>(); const callExpressionsData = new Set<CallExpressionData>();
const memberCallExpressionData = new Set<MemberCallExpressionData>();
const invalidTopLevelMemberExpressions = new Set<MemberExpressionData>(); const invalidTopLevelMemberExpressions = new Set<MemberExpressionData>();
const variableDeclarations = new Set<string>(); const variableDeclarations = new Set<string>();
let functionalParams = new Set<string>(); let functionalParams = new Set<string>();
@ -646,6 +658,7 @@ export const extractExpressionsFromCode = (
invalidTopLevelMemberExpressionsArray: [], invalidTopLevelMemberExpressionsArray: [],
assignmentExpressionsData: [], assignmentExpressionsData: [],
callExpressionsData: [], callExpressionsData: [],
memberCallExpressionData: [],
}; };
} }
throw e; throw e;
@ -726,11 +739,23 @@ export const extractExpressionsFromCode = (
} as AssignmentExpressionData); } as AssignmentExpressionData);
}, },
CallExpression(node: Node) { CallExpression(node: Node) {
if (isCallExpressionNode(node) && isIdentifierNode(node.callee)) { if (isCallExpressionNode(node)) {
callExpressionsData.add({ if (isIdentifierNode(node.callee)) {
property: node.callee, callExpressionsData.add({
params: node.arguments, property: node.callee,
} as CallExpressionData); 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, invalidTopLevelMemberExpressionsArray,
assignmentExpressionsData: [...assignmentExpressionsData], assignmentExpressionsData: [...assignmentExpressionsData],
callExpressionsData: [...callExpressionsData], callExpressionsData: [...callExpressionsData],
memberCallExpressionData: [...memberCallExpressionData],
}; };
}; };

View File

@ -1,4 +1,5 @@
import { getScriptType } from "workers/Evaluation/evaluate"; import { getScriptType } from "workers/Evaluation/evaluate";
import { CustomLintErrorCode } from "../constants";
import getLintingErrors from "../utils/getLintingErrors"; import getLintingErrors from "../utils/getLintingErrors";
describe("getLintingErrors", () => { describe("getLintingErrors", () => {
@ -102,4 +103,81 @@ describe("getLintingErrors", () => {
expect(lintErrors.length).toEqual(1); 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, MemberExpressionData,
AssignmentExpressionData, AssignmentExpressionData,
CallExpressionData, CallExpressionData,
MemberCallExpressionData,
} from "@shared/ast"; } from "@shared/ast";
import { import {
extractExpressionsFromCode, extractExpressionsFromCode,
@ -271,20 +272,22 @@ function getInvalidWidgetPropertySetterErrors({
} }
function getInvalidAppsmithStoreSetterErrors({ function getInvalidAppsmithStoreSetterErrors({
assignmentExpressions, appsmithStoreMutationExpressions,
originalBinding, originalBinding,
script, script,
scriptPos, scriptPos,
}: { }: {
data: Record<string, unknown>; data: Record<string, unknown>;
assignmentExpressions: AssignmentExpressionData[]; appsmithStoreMutationExpressions: Array<
AssignmentExpressionData | MemberCallExpressionData
>;
scriptPos: Position; scriptPos: Position;
originalBinding: string; originalBinding: string;
script: string; script: string;
}) { }) {
const assignmentExpressionErrors: LintError[] = []; const assignmentExpressionErrors: LintError[] = [];
for (const { object, parentNode } of assignmentExpressions) { for (const { object, parentNode } of appsmithStoreMutationExpressions) {
if (!isMemberExpressionNode(object)) continue; if (!isMemberExpressionNode(object)) continue;
const assignmentExpressionString = generate(parentNode); const assignmentExpressionString = generate(parentNode);
if (!assignmentExpressionString.startsWith("appsmith.store")) continue; if (!assignmentExpressionString.startsWith("appsmith.store")) continue;
@ -386,6 +389,7 @@ function getCustomErrorsFromScript(
let invalidTopLevelMemberExpressions: MemberExpressionData[] = []; let invalidTopLevelMemberExpressions: MemberExpressionData[] = [];
let assignmentExpressions: AssignmentExpressionData[] = []; let assignmentExpressions: AssignmentExpressionData[] = [];
let callExpressions: CallExpressionData[] = []; let callExpressions: CallExpressionData[] = [];
let memberCallExpressions: MemberCallExpressionData[] = [];
try { try {
const value = extractExpressionsFromCode( const value = extractExpressionsFromCode(
script, script,
@ -396,6 +400,7 @@ function getCustomErrorsFromScript(
value.invalidTopLevelMemberExpressionsArray; value.invalidTopLevelMemberExpressionsArray;
assignmentExpressions = value.assignmentExpressionsData; assignmentExpressions = value.assignmentExpressionsData;
callExpressions = value.callExpressionsData; callExpressions = value.callExpressionsData;
memberCallExpressions = value.memberCallExpressionData;
} catch (e) {} } catch (e) {}
const invalidWidgetPropertySetterErrors = const invalidWidgetPropertySetterErrors =
@ -416,9 +421,16 @@ function getCustomErrorsFromScript(
isJSObject, 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 = const invalidAppsmithStorePropertyErrors =
getInvalidAppsmithStoreSetterErrors({ getInvalidAppsmithStoreSetterErrors({
assignmentExpressions, appsmithStoreMutationExpressions,
script, script,
scriptPos, scriptPos,
originalBinding, originalBinding,