fix: Added lint error for appsmith store mutations (#33484)
Co-authored-by: “sneha122” <“sneha@appsmith.com”>
This commit is contained in:
parent
6179729e9a
commit
bbfe4ffe70
|
|
@ -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();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user