feat: show lint errors in async functions bound to sync fields (#21187)
## Description This PR improves the error resolution journey for users. Lint warnings are added to async JS functions which are bound to data fields (sync fields). - JSObjects are "linted" by individual properties (as opposed to being "linted" as a whole) - Only edited jsobject properties get "linted", improving jsObject linting by ~35%.(This largely depends on the size of the JSObject) <img width="500" alt="Screenshot 2023-04-03 at 11 17 45" src="https://user-images.githubusercontent.com/46670083/229482424-233f3950-ffec-46f5-8c42-680dff6a412f.png"> <img width="500" alt="Screenshot 2023-03-14 at 11 26 00" src="https://user-images.githubusercontent.com/46670083/224975572-b2d8d404-aac6-43fb-be14-20edf7c56117.png"> <img width="500" alt="Screenshot 2023-03-14 at 11 41 11" src="https://user-images.githubusercontent.com/46670083/224975952-c40848b1-69d8-489d-9b62-24127ea1a2f1.png"> Fixes #20289 Fixes #20008 ## Type of change - Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - CYPRESS - JEST ### Test Plan > Add Testsmith test cases links that relate to this PR ### Issues raised during DP testing > Link issues raised during DP testing for better visiblity and tracking (copy link from comments dropped on this PR) ## Checklist: ### Dev activity - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [x] Test plan has been peer reviewed by QA - [x] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test
This commit is contained in:
parent
1b92f97d61
commit
b80b0ca3fa
|
|
@ -1,6 +1,5 @@
|
||||||
const dsl = require("../../../../fixtures/autocomp.json");
|
const dsl = require("../../../../fixtures/autocomp.json");
|
||||||
const dynamicInputLocators = require("../../../../locators/DynamicInput.json");
|
const dynamicInputLocators = require("../../../../locators/DynamicInput.json");
|
||||||
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
|
|
||||||
|
|
||||||
describe("Dynamic input autocomplete", () => {
|
describe("Dynamic input autocomplete", () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
|
|
@ -72,7 +71,7 @@ describe("Dynamic input autocomplete", () => {
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
|
|
||||||
cy.evaluateErrorMessage(
|
cy.evaluateErrorMessage(
|
||||||
"Found a reference to {{actionName}} during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.".replaceAll(
|
"Found a reference to {{actionName}} during evaluation. Data fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.".replaceAll(
|
||||||
"{{actionName}}",
|
"{{actionName}}",
|
||||||
"storeValue()",
|
"storeValue()",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import * as _ from "../../../../support/Objects/ObjectsCore";
|
||||||
|
|
||||||
|
describe("Linting async JSFunctions bound to data fields", () => {
|
||||||
|
before(() => {
|
||||||
|
_.entityExplorer.DragDropWidgetNVerify("buttonwidget", 300, 300);
|
||||||
|
_.entityExplorer.NavigateToSwitcher("explorer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1. Doesn't show lint warnings in debugger but shows on Hover only", () => {
|
||||||
|
_.apiPage.CreateApi();
|
||||||
|
const JS_OBJECT_CONTENT = `export default {
|
||||||
|
myFun1: () => {
|
||||||
|
//write code here
|
||||||
|
Api1.run()
|
||||||
|
},
|
||||||
|
myFun2: async () => {
|
||||||
|
//use async-await or promises
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
_.jsEditor.CreateJSObject(JS_OBJECT_CONTENT, {
|
||||||
|
paste: true,
|
||||||
|
completeReplace: true,
|
||||||
|
toRun: false,
|
||||||
|
shouldCreateNewJSObj: true,
|
||||||
|
});
|
||||||
|
_.entityExplorer.SelectEntityByName("Button1", "Widgets");
|
||||||
|
_.propPane.UpdatePropertyFieldValue("Label", "{{JSObject1.myFun2()}}");
|
||||||
|
cy.get(_.locators._evaluateMsg).should("be.visible");
|
||||||
|
cy.contains("View Source").click(); // should route to jsobject page
|
||||||
|
cy.get(_.locators._lintWarningElement).should("have.length", 1);
|
||||||
|
MouseHoverNVerify(
|
||||||
|
"myFun2",
|
||||||
|
`Cannot bind async functions to data fields. Convert this to a sync function or remove references to "JSObject1.myFun2" on the following data field: Button1.text`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
// remove async tag from function
|
||||||
|
_.jsEditor.EditJSObj(`export default {
|
||||||
|
myFun1: () => {
|
||||||
|
//write code here
|
||||||
|
Api1.run()
|
||||||
|
},
|
||||||
|
myFun2: () => {
|
||||||
|
//use async-await or promises
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
cy.get(_.locators._lintWarningElement).should("not.exist");
|
||||||
|
|
||||||
|
// Add async tag from function
|
||||||
|
_.jsEditor.EditJSObj(`export default {
|
||||||
|
myFun1: () => {
|
||||||
|
//write code here
|
||||||
|
Api1.run()
|
||||||
|
},
|
||||||
|
myFun2: async () => {
|
||||||
|
//use async-await or promises
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
|
||||||
|
cy.get(_.locators._lintWarningElement).should("have.length", 1);
|
||||||
|
MouseHoverNVerify(
|
||||||
|
"myFun2",
|
||||||
|
`Cannot bind async functions to data fields. Convert this to a sync function or remove references to "JSObject1.myFun2" on the following data field: Button1.text`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
_.entityExplorer.SelectEntityByName("Button1", "Widgets");
|
||||||
|
_.propPane.UpdatePropertyFieldValue("Label", "{{JSObject1.myFun1()}}");
|
||||||
|
cy.get(_.locators._evaluateMsg).should("be.visible");
|
||||||
|
cy.contains("View Source").click(); // should route to jsobject page
|
||||||
|
cy.get(_.locators._lintWarningElement).should("have.length", 2);
|
||||||
|
MouseHoverNVerify(
|
||||||
|
"myFun1",
|
||||||
|
`Functions bound to data fields cannot execute async code. Remove async statements highlighted below or remove references to "JSObject1.myFun1" on the following data field: Button1.text`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
MouseHoverNVerify(
|
||||||
|
"run",
|
||||||
|
`Cannot execute async code on functions bound to data fields`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
_.jsEditor.EditJSObj(`export default {
|
||||||
|
myFun1: () => {
|
||||||
|
//write code here
|
||||||
|
Api1.run()
|
||||||
|
},
|
||||||
|
myFun2: async () => {
|
||||||
|
//use async-await or promises
|
||||||
|
}
|
||||||
|
}`);
|
||||||
|
// Remove binding from label, and add to onClick. Expect no error
|
||||||
|
_.entityExplorer.SelectEntityByName("Button1", "Widgets");
|
||||||
|
_.propPane.UpdatePropertyFieldValue("Label", "Click here");
|
||||||
|
_.propPane.EnterJSContext(
|
||||||
|
"onClick",
|
||||||
|
`{{
|
||||||
|
() => {
|
||||||
|
JSObject1.myFun1();
|
||||||
|
JSObject1.myFun2()
|
||||||
|
}}}`,
|
||||||
|
);
|
||||||
|
_.entityExplorer.ExpandCollapseEntity("Queries/JS");
|
||||||
|
_.entityExplorer.SelectEntityByName("JSObject1", "Queries/JS");
|
||||||
|
cy.get(_.locators._lintWarningElement).should("not.exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
function MouseHoverNVerify(lintOn: string, debugMsg: string, isError = true) {
|
||||||
|
_.agHelper.Sleep();
|
||||||
|
const element = isError
|
||||||
|
? cy.get(_.locators._lintErrorElement)
|
||||||
|
: cy.get(_.locators._lintWarningElement);
|
||||||
|
element.contains(lintOn).should("exist").first().trigger("mouseover");
|
||||||
|
_.agHelper.AssertContains(debugMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
//deleting all test data
|
||||||
|
_.entityExplorer.ActionContextMenuByEntityName(
|
||||||
|
"Api1",
|
||||||
|
"Delete",
|
||||||
|
"Are you sure?",
|
||||||
|
);
|
||||||
|
_.entityExplorer.ActionContextMenuByEntityName(
|
||||||
|
"JSObject1",
|
||||||
|
"Delete",
|
||||||
|
"Are you sure?",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -176,5 +176,6 @@ export class CommonLocators {
|
||||||
_commentString = ".cm-comment";
|
_commentString = ".cm-comment";
|
||||||
_modalWrapper = "[data-cy='modal-wrapper']";
|
_modalWrapper = "[data-cy='modal-wrapper']";
|
||||||
_editorBackButton = ".t--close-editor";
|
_editorBackButton = ".t--close-editor";
|
||||||
|
_evaluateMsg = ".t--evaluatedPopup-error";
|
||||||
_canvas = "[data-testid=widgets-editor]";
|
_canvas = "[data-testid=widgets-editor]";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,7 @@
|
||||||
"@uppy/url": "^1.5.16",
|
"@uppy/url": "^1.5.16",
|
||||||
"@uppy/webcam": "^1.8.4",
|
"@uppy/webcam": "^1.8.4",
|
||||||
"@welldone-software/why-did-you-render": "^4.2.5",
|
"@welldone-software/why-did-you-render": "^4.2.5",
|
||||||
"acorn-walk": "^8.2.0",
|
|
||||||
"algoliasearch": "^4.2.0",
|
"algoliasearch": "^4.2.0",
|
||||||
"astring": "^1.7.5",
|
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"codemirror": "^5.59.2",
|
"codemirror": "^5.59.2",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||||
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
|
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
|
||||||
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
|
||||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
|
||||||
|
|
||||||
export type SetLintErrorsAction = ReduxAction<{ errors: LintErrors }>;
|
export type SetLintErrorsAction = ReduxAction<{ errors: LintErrorsStore }>;
|
||||||
export const setLintingErrors = (
|
export const setLintingErrors = (
|
||||||
errors: LintErrors,
|
errors: LintErrorsStore,
|
||||||
): ReduxAction<{ errors: LintErrors }> => {
|
): ReduxAction<{ errors: LintErrorsStore }> => {
|
||||||
return {
|
return {
|
||||||
type: ReduxActionTypes.SET_LINT_ERRORS,
|
type: ReduxActionTypes.SET_LINT_ERRORS,
|
||||||
payload: { errors },
|
payload: { errors },
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ import type { EditorContextState } from "reducers/uiReducers/editorContextReduce
|
||||||
import type { LibraryState } from "reducers/uiReducers/libraryReducer";
|
import type { LibraryState } from "reducers/uiReducers/libraryReducer";
|
||||||
import type { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer";
|
import type { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer";
|
||||||
import type { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightReducers/canvasLevelsReducer";
|
import type { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightReducers/canvasLevelsReducer";
|
||||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||||
import lintErrorReducer from "reducers/lintingReducers";
|
import lintErrorReducer from "reducers/lintingReducers";
|
||||||
import type { AutoHeightUIState } from "reducers/uiReducers/autoHeightReducer";
|
import type { AutoHeightUIState } from "reducers/uiReducers/autoHeightReducer";
|
||||||
import type { AnalyticsReduxState } from "reducers/uiReducers/analyticsReducer";
|
import type { AnalyticsReduxState } from "reducers/uiReducers/analyticsReducer";
|
||||||
|
|
@ -159,7 +159,7 @@ export interface AppState {
|
||||||
triggers: TriggerValuesEvaluationState;
|
triggers: TriggerValuesEvaluationState;
|
||||||
};
|
};
|
||||||
linting: {
|
linting: {
|
||||||
errors: LintErrors;
|
errors: LintErrorsStore;
|
||||||
};
|
};
|
||||||
form: {
|
form: {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
entityFns,
|
entityFns,
|
||||||
getPlatformFunctions,
|
getPlatformFunctions,
|
||||||
} from "@appsmith/workers/Evaluation/fns";
|
} from "@appsmith/workers/Evaluation/fns";
|
||||||
|
import { klona } from "klona/full";
|
||||||
declare global {
|
declare global {
|
||||||
/** All identifiers added to the worker global scope should also
|
/** All identifiers added to the worker global scope should also
|
||||||
* be included in the DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS in
|
* be included in the DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS in
|
||||||
|
|
@ -33,21 +34,21 @@ export enum ExecutionType {
|
||||||
export const addDataTreeToContext = (args: {
|
export const addDataTreeToContext = (args: {
|
||||||
EVAL_CONTEXT: EvalContext;
|
EVAL_CONTEXT: EvalContext;
|
||||||
dataTree: Readonly<DataTree>;
|
dataTree: Readonly<DataTree>;
|
||||||
skipEntityFunctions?: boolean;
|
removeEntityFunctions?: boolean;
|
||||||
isTriggerBased: boolean;
|
isTriggerBased: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
dataTree,
|
dataTree,
|
||||||
EVAL_CONTEXT,
|
EVAL_CONTEXT,
|
||||||
isTriggerBased,
|
isTriggerBased,
|
||||||
skipEntityFunctions = false,
|
removeEntityFunctions = false,
|
||||||
} = args;
|
} = args;
|
||||||
const dataTreeEntries = Object.entries(dataTree);
|
const dataTreeEntries = Object.entries(dataTree);
|
||||||
const entityFunctionCollection: Record<string, Record<string, Function>> = {};
|
const entityFunctionCollection: Record<string, Record<string, Function>> = {};
|
||||||
|
|
||||||
for (const [entityName, entity] of dataTreeEntries) {
|
for (const [entityName, entity] of dataTreeEntries) {
|
||||||
EVAL_CONTEXT[entityName] = entity;
|
EVAL_CONTEXT[entityName] = entity;
|
||||||
if (skipEntityFunctions || !isTriggerBased) continue;
|
if (!removeEntityFunctions && !isTriggerBased) continue;
|
||||||
for (const entityFn of entityFns) {
|
for (const entityFn of entityFns) {
|
||||||
if (!entityFn.qualifier(entity)) continue;
|
if (!entityFn.qualifier(entity)) continue;
|
||||||
const func = entityFn.fn(entity, entityName);
|
const func = entityFn.fn(entity, entityName);
|
||||||
|
|
@ -56,6 +57,12 @@ export const addDataTreeToContext = (args: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (removeEntityFunctions)
|
||||||
|
return removeEntityFunctionsFromEvalContext(
|
||||||
|
entityFunctionCollection,
|
||||||
|
EVAL_CONTEXT,
|
||||||
|
);
|
||||||
|
|
||||||
// if eval is not trigger based i.e., sync eval then we skip adding entity and platform function to evalContext
|
// if eval is not trigger based i.e., sync eval then we skip adding entity and platform function to evalContext
|
||||||
if (!isTriggerBased) return;
|
if (!isTriggerBased) return;
|
||||||
|
|
||||||
|
|
@ -87,3 +94,18 @@ export const getAllAsyncFunctions = (dataTree: DataTree) => {
|
||||||
}
|
}
|
||||||
return asyncFunctionNameMap;
|
return asyncFunctionNameMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeEntityFunctionsFromEvalContext = (
|
||||||
|
entityFunctionCollection: Record<string, Record<string, Function>>,
|
||||||
|
evalContext: EvalContext,
|
||||||
|
) => {
|
||||||
|
for (const [entityName, funcObj] of Object.entries(
|
||||||
|
entityFunctionCollection,
|
||||||
|
)) {
|
||||||
|
const entity = klona(evalContext[entityName]);
|
||||||
|
Object.keys(funcObj).forEach((entityFn) => {
|
||||||
|
delete entity[entityFn];
|
||||||
|
});
|
||||||
|
evalContext[entityName] = entity;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,15 @@ export function isJSAction(entity: DataTreeEntity): entity is JSActionEntity {
|
||||||
entity.ENTITY_TYPE === ENTITY_TYPE.JSACTION
|
entity.ENTITY_TYPE === ENTITY_TYPE.JSACTION
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export function isJSActionConfig(
|
||||||
|
entity: DataTreeEntityConfig,
|
||||||
|
): entity is JSActionEntityConfig {
|
||||||
|
return (
|
||||||
|
typeof entity === "object" &&
|
||||||
|
"ENTITY_TYPE" in entity &&
|
||||||
|
entity.ENTITY_TYPE === ENTITY_TYPE.JSACTION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function isJSObject(entity: DataTreeEntity): entity is JSActionEntity {
|
export function isJSObject(entity: DataTreeEntity): entity is JSActionEntity {
|
||||||
return (
|
return (
|
||||||
|
|
@ -402,6 +411,10 @@ export function isJSObject(entity: DataTreeEntity): entity is JSActionEntity {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDataTreeEntity(entity: unknown) {
|
||||||
|
return !!entity && typeof entity === "object" && "ENTITY_TYPE" in entity;
|
||||||
|
}
|
||||||
|
|
||||||
// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing
|
// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing
|
||||||
// Check issue https://github.com/appsmithorg/appsmith/issues/719
|
// Check issue https://github.com/appsmithorg/appsmith/issues/719
|
||||||
export const removeFunctions = (value: any) => {
|
export const removeFunctions = (value: any) => {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { ReactComponent as CopyIcon } from "assets/icons/menu/copy-snippet.svg";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
|
|
||||||
import type { EvaluationError } from "utils/DynamicBindingUtils";
|
import type { EvaluationError } from "utils/DynamicBindingUtils";
|
||||||
|
import { PropertyEvaluationErrorCategory } from "utils/DynamicBindingUtils";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { Severity } from "@sentry/react";
|
import { Severity } from "@sentry/react";
|
||||||
import type { CodeEditorExpected } from "components/editorComponents/CodeEditor/index";
|
import type { CodeEditorExpected } from "components/editorComponents/CodeEditor/index";
|
||||||
|
|
@ -33,6 +34,11 @@ import { useDispatch, useSelector } from "react-redux";
|
||||||
import { getEvaluatedPopupState } from "selectors/editorContextSelectors";
|
import { getEvaluatedPopupState } from "selectors/editorContextSelectors";
|
||||||
import type { AppState } from "@appsmith/reducers";
|
import type { AppState } from "@appsmith/reducers";
|
||||||
import { setEvalPopupState } from "actions/editorContextActions";
|
import { setEvalPopupState } from "actions/editorContextActions";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { showDebugger } from "actions/debuggerActions";
|
||||||
|
import { modText } from "utils/helpers";
|
||||||
|
import { getEntityNameAndPropertyPath } from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
import { getJSFunctionNavigationUrl } from "selectors/navigationSelectors";
|
||||||
|
|
||||||
const modifiers: IPopoverSharedProps["modifiers"] = {
|
const modifiers: IPopoverSharedProps["modifiers"] = {
|
||||||
offset: {
|
offset: {
|
||||||
|
|
@ -183,6 +189,24 @@ const StyledTitleName = styled.p`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const AsyncFunctionErrorLink = styled(Link)`
|
||||||
|
color: ${(props) => props.theme.colors.debugger.entityLink};
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
&:hover {
|
||||||
|
color: ${(props) => props.theme.colors.debugger.entityLink};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AsyncFunctionErrorView = styled.div`
|
||||||
|
display: flex;
|
||||||
|
margin-top: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
function CollapseToggle(props: { isOpen: boolean }) {
|
function CollapseToggle(props: { isOpen: boolean }) {
|
||||||
const { isOpen } = props;
|
const { isOpen } = props;
|
||||||
return (
|
return (
|
||||||
|
|
@ -462,16 +486,38 @@ function PopoverContent(props: PopoverContentProps) {
|
||||||
? popupContext.value
|
? popupContext.value
|
||||||
: true,
|
: true,
|
||||||
);
|
);
|
||||||
|
const { errors, expected, hasError, onMouseEnter, onMouseLeave, theme } =
|
||||||
|
props;
|
||||||
|
const { entityName } = getEntityNameAndPropertyPath(props.dataTreePath || "");
|
||||||
|
const JSFunctionInvocationError = errors.find(
|
||||||
|
({ kind }) =>
|
||||||
|
kind &&
|
||||||
|
kind.category ===
|
||||||
|
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD &&
|
||||||
|
kind.rootcause,
|
||||||
|
);
|
||||||
|
const errorNavigationUrl = useSelector((state: AppState) =>
|
||||||
|
getJSFunctionNavigationUrl(
|
||||||
|
state,
|
||||||
|
entityName,
|
||||||
|
JSFunctionInvocationError?.kind?.rootcause,
|
||||||
|
),
|
||||||
|
);
|
||||||
const toggleExpectedDataType = () =>
|
const toggleExpectedDataType = () =>
|
||||||
setOpenExpectedDataType(!openExpectedDataType);
|
setOpenExpectedDataType(!openExpectedDataType);
|
||||||
const toggleExpectedExample = () =>
|
const toggleExpectedExample = () =>
|
||||||
setOpenExpectedExample(!openExpectedExample);
|
setOpenExpectedExample(!openExpectedExample);
|
||||||
const { errors, expected, hasError, onMouseEnter, onMouseLeave, theme } =
|
|
||||||
props;
|
|
||||||
let error: EvaluationError | undefined;
|
let error: EvaluationError | undefined;
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
error = errors[0];
|
error = errors[0];
|
||||||
}
|
}
|
||||||
|
const openDebugger = (
|
||||||
|
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||||
|
) => {
|
||||||
|
event.preventDefault();
|
||||||
|
dispatch(showDebugger());
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
|
|
@ -508,13 +554,25 @@ function PopoverContent(props: PopoverContentProps) {
|
||||||
{/* errorMessage could be an empty string */}
|
{/* errorMessage could be an empty string */}
|
||||||
{getErrorMessage(error.errorMessage)}
|
{getErrorMessage(error.errorMessage)}
|
||||||
</span>
|
</span>
|
||||||
<EvaluatedValueDebugButton
|
|
||||||
entity={props.entity}
|
{errorNavigationUrl ? (
|
||||||
error={{
|
<AsyncFunctionErrorView>
|
||||||
type: error.errorType,
|
<AsyncFunctionErrorLink onClick={(e) => openDebugger(e)} to="">
|
||||||
message: error.errorMessage,
|
See Error ({modText()} D)
|
||||||
}}
|
</AsyncFunctionErrorLink>
|
||||||
/>
|
<AsyncFunctionErrorLink to={errorNavigationUrl}>
|
||||||
|
View Source
|
||||||
|
</AsyncFunctionErrorLink>
|
||||||
|
</AsyncFunctionErrorView>
|
||||||
|
) : (
|
||||||
|
<EvaluatedValueDebugButton
|
||||||
|
entity={props.entity}
|
||||||
|
error={{
|
||||||
|
type: error.errorType,
|
||||||
|
message: error.errorMessage,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ErrorText>
|
</ErrorText>
|
||||||
)}
|
)}
|
||||||
{props.expected && props.expected.type !== UNDEFINED_VALIDATION && (
|
{props.expected && props.expected.type !== UNDEFINED_VALIDATION && (
|
||||||
|
|
|
||||||
|
|
@ -1263,6 +1263,7 @@ class CodeEditor extends Component<Props, State> {
|
||||||
text="/"
|
text="/"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<EvaluatedValuePopup
|
<EvaluatedValuePopup
|
||||||
dataTreePath={this.props.dataTreePath}
|
dataTreePath={this.props.dataTreePath}
|
||||||
editorRef={this.codeEditorTarget}
|
editorRef={this.codeEditorTarget}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { Severity } from "entities/AppsmithConsole";
|
import { Severity } from "entities/AppsmithConsole";
|
||||||
import type { LintError } from "utils/DynamicBindingUtils";
|
import type { LintError } from "utils/DynamicBindingUtils";
|
||||||
import { PropertyEvaluationErrorType } from "utils/DynamicBindingUtils";
|
import { PropertyEvaluationErrorType } from "utils/DynamicBindingUtils";
|
||||||
|
import {
|
||||||
|
INVALID_JSOBJECT_START_STATEMENT,
|
||||||
|
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
|
||||||
|
} from "workers/Linting/constants";
|
||||||
import { CODE_EDITOR_START_POSITION } from "./constants";
|
import { CODE_EDITOR_START_POSITION } from "./constants";
|
||||||
import {
|
import {
|
||||||
getKeyPositionInString,
|
getKeyPositionInString,
|
||||||
|
|
@ -213,7 +217,23 @@ describe("getLintAnnotations()", () => {
|
||||||
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
const errors: LintError[] = [];
|
const errors: LintError[] = [
|
||||||
|
{
|
||||||
|
errorType: PropertyEvaluationErrorType.LINT,
|
||||||
|
errorSegment: "",
|
||||||
|
originalBinding: value,
|
||||||
|
line: 0,
|
||||||
|
ch: 0,
|
||||||
|
code: INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
|
||||||
|
variables: [],
|
||||||
|
raw: value,
|
||||||
|
errorMessage: {
|
||||||
|
name: "LintingError",
|
||||||
|
message: INVALID_JSOBJECT_START_STATEMENT,
|
||||||
|
},
|
||||||
|
severity: Severity.ERROR,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const res = getLintAnnotations(value, errors, { isJSObject: true });
|
const res = getLintAnnotations(value, errors, { isJSObject: true });
|
||||||
expect(res).toEqual([
|
expect(res).toEqual([
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
CUSTOM_LINT_ERRORS,
|
CUSTOM_LINT_ERRORS,
|
||||||
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
|
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
|
||||||
INVALID_JSOBJECT_START_STATEMENT,
|
INVALID_JSOBJECT_START_STATEMENT,
|
||||||
JS_OBJECT_START_STATEMENT,
|
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
|
||||||
} from "workers/Linting/constants";
|
} from "workers/Linting/constants";
|
||||||
export const getIndexOfRegex = (
|
export const getIndexOfRegex = (
|
||||||
str: string,
|
str: string,
|
||||||
|
|
@ -123,33 +123,32 @@ export const getLintAnnotations = (
|
||||||
const lintErrors = filterInvalidLintErrors(errors, contextData);
|
const lintErrors = filterInvalidLintErrors(errors, contextData);
|
||||||
const lines = value.split("\n");
|
const lines = value.split("\n");
|
||||||
|
|
||||||
// The binding position of every valid JS Object is constant, so we need not
|
|
||||||
// waste time checking for position of binding.
|
|
||||||
// For JS Objects not starting with the expected "export default" statement, we return early
|
|
||||||
// with a "invalid start statement" lint error
|
|
||||||
if (
|
|
||||||
isJSObject &&
|
|
||||||
!isEmpty(lines) &&
|
|
||||||
!lines[0].startsWith(JS_OBJECT_START_STATEMENT)
|
|
||||||
) {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
from: CODE_EDITOR_START_POSITION,
|
|
||||||
to: getFirstNonEmptyPosition(lines),
|
|
||||||
message: INVALID_JSOBJECT_START_STATEMENT,
|
|
||||||
severity: Severity.ERROR,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
lintErrors.forEach((error) => {
|
lintErrors.forEach((error) => {
|
||||||
const { ch, errorMessage, line, originalBinding, severity, variables } =
|
const {
|
||||||
error;
|
ch,
|
||||||
|
code,
|
||||||
|
errorMessage,
|
||||||
|
line,
|
||||||
|
originalBinding,
|
||||||
|
severity,
|
||||||
|
variables,
|
||||||
|
} = error;
|
||||||
|
|
||||||
if (!originalBinding) {
|
if (!originalBinding) {
|
||||||
return annotations;
|
return annotations;
|
||||||
}
|
}
|
||||||
|
if (code === INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE) {
|
||||||
|
// The binding position of every valid JS Object is constant, so we need not
|
||||||
|
// waste time checking for position of binding.
|
||||||
|
// For JS Objects not starting with the expected "export default" statement, we return early
|
||||||
|
// with a "invalid start statement" lint error
|
||||||
|
return annotations.push({
|
||||||
|
from: CODE_EDITOR_START_POSITION,
|
||||||
|
to: getFirstNonEmptyPosition(lines),
|
||||||
|
message: INVALID_JSOBJECT_START_STATEMENT,
|
||||||
|
severity: Severity.ERROR,
|
||||||
|
});
|
||||||
|
}
|
||||||
let variableLength = 1;
|
let variableLength = 1;
|
||||||
// Find the variable with minimal length
|
// Find the variable with minimal length
|
||||||
if (variables) {
|
if (variables) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import type { EntityNavigationData } from "selectors/navigationSelectors";
|
||||||
|
|
||||||
|
export const addThisReference = (
|
||||||
|
navigationData: EntityNavigationData,
|
||||||
|
entityName?: string,
|
||||||
|
) => {
|
||||||
|
if (entityName && entityName in navigationData) {
|
||||||
|
return {
|
||||||
|
...navigationData,
|
||||||
|
this: navigationData[entityName],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return navigationData;
|
||||||
|
};
|
||||||
|
|
@ -4,22 +4,21 @@ import { createImmerReducer } from "utils/ReducerUtils";
|
||||||
import type { SetLintErrorsAction } from "actions/lintingActions";
|
import type { SetLintErrorsAction } from "actions/lintingActions";
|
||||||
import { isEqual } from "lodash";
|
import { isEqual } from "lodash";
|
||||||
|
|
||||||
export interface LintErrors {
|
export type LintErrorsStore = Record<string, LintError[]>;
|
||||||
[entityName: string]: LintError[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: LintErrors = {};
|
const initialState: LintErrorsStore = {};
|
||||||
|
|
||||||
export const lintErrorReducer = createImmerReducer(initialState, {
|
export const lintErrorReducer = createImmerReducer(initialState, {
|
||||||
[ReduxActionTypes.FETCH_PAGE_INIT]: () => initialState,
|
[ReduxActionTypes.FETCH_PAGE_INIT]: () => initialState,
|
||||||
[ReduxActionTypes.SET_LINT_ERRORS]: (
|
[ReduxActionTypes.SET_LINT_ERRORS]: (
|
||||||
state: LintErrors,
|
state: LintErrorsStore,
|
||||||
action: SetLintErrorsAction,
|
action: SetLintErrorsAction,
|
||||||
) => {
|
) => {
|
||||||
const { errors } = action.payload;
|
const { errors } = action.payload;
|
||||||
for (const entityName of Object.keys(errors)) {
|
for (const entityPath of Object.keys(errors)) {
|
||||||
if (isEqual(state[entityName], errors[entityName])) continue;
|
const entityPathLintErrors = errors[entityPath];
|
||||||
state[entityName] = errors[entityName];
|
if (isEqual(entityPathLintErrors, state[entityPath])) continue;
|
||||||
|
state[entityPath] = entityPathLintErrors;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -404,7 +404,7 @@ function* logDebuggerErrorAnalyticsSaga(
|
||||||
);
|
);
|
||||||
const pluginId = action?.pluginId || payload?.analytics?.pluginId || "";
|
const pluginId = action?.pluginId || payload?.analytics?.pluginId || "";
|
||||||
const plugin: Plugin = yield select(getPlugin, pluginId);
|
const plugin: Plugin = yield select(getPlugin, pluginId);
|
||||||
const pluginName = plugin.name.replace(/ /g, "");
|
const pluginName = plugin?.name.replace(/ /g, "");
|
||||||
let propertyPath = `${pluginName}`;
|
let propertyPath = `${pluginName}`;
|
||||||
|
|
||||||
if (payload.propertyPath) {
|
if (payload.propertyPath) {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { logJSFunctionExecution } from "@appsmith/sagas/JSFunctionExecutionSaga"
|
||||||
import { handleStoreOperations } from "./ActionExecution/StoreActionSaga";
|
import { handleStoreOperations } from "./ActionExecution/StoreActionSaga";
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
|
import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
|
||||||
|
import type { LintTreeSagaRequestData } from "workers/Linting/types";
|
||||||
|
|
||||||
export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
|
export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -31,12 +32,21 @@ export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
|
||||||
export function* lintTreeActionHandler(message: any) {
|
export function* lintTreeActionHandler(message: any) {
|
||||||
const { body } = message;
|
const { body } = message;
|
||||||
const { data } = body;
|
const { data } = body;
|
||||||
|
const {
|
||||||
|
asyncJSFunctionsInDataFields,
|
||||||
|
configTree,
|
||||||
|
jsPropertiesState,
|
||||||
|
pathsToLint: lintOrder,
|
||||||
|
unevalTree,
|
||||||
|
} = data as LintTreeSagaRequestData;
|
||||||
yield put({
|
yield put({
|
||||||
type: ReduxActionTypes.LINT_TREE,
|
type: ReduxActionTypes.LINT_TREE,
|
||||||
payload: {
|
payload: {
|
||||||
pathsToLint: data.lintOrder,
|
pathsToLint: lintOrder,
|
||||||
unevalTree: data.unevalTree,
|
unevalTree,
|
||||||
configTree: data.configTree,
|
jsPropertiesState,
|
||||||
|
asyncJSFunctionsInDataFields,
|
||||||
|
configTree,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,7 @@ export function* evaluateTreeSaga(
|
||||||
requiresLinting: isEditMode && requiresLinting,
|
requiresLinting: isEditMode && requiresLinting,
|
||||||
forceEvaluation,
|
forceEvaluation,
|
||||||
metaWidgets,
|
metaWidgets,
|
||||||
|
appMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
const workerResponse: EvalTreeResponseData = yield call(
|
const workerResponse: EvalTreeResponseData = yield call(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,11 @@ import type {
|
||||||
import { LINT_WORKER_ACTIONS } from "workers/Linting/types";
|
import { LINT_WORKER_ACTIONS } from "workers/Linting/types";
|
||||||
import { logLatestLintPropertyErrors } from "./PostLintingSagas";
|
import { logLatestLintPropertyErrors } from "./PostLintingSagas";
|
||||||
import { getAppsmithConfigs } from "@appsmith/configs";
|
import { getAppsmithConfigs } from "@appsmith/configs";
|
||||||
|
import type { AppState } from "@appsmith/reducers";
|
||||||
|
import type { LintError } from "utils/DynamicBindingUtils";
|
||||||
|
import { get, set, union } from "lodash";
|
||||||
|
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||||
|
import type { TJSPropertiesState } from "workers/Evaluation/JSObject/jsPropertiesState";
|
||||||
|
|
||||||
const APPSMITH_CONFIGS = getAppsmithConfigs();
|
const APPSMITH_CONFIGS = getAppsmithConfigs();
|
||||||
|
|
||||||
|
|
@ -35,8 +40,58 @@ function* updateLintGlobals(action: ReduxAction<TJSLibrary>) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function* getValidOldJSCollectionLintErrors(
|
||||||
|
jsEntities: string[],
|
||||||
|
errors: LintErrorsStore,
|
||||||
|
jsObjectsState: TJSPropertiesState,
|
||||||
|
) {
|
||||||
|
const updatedJSCollectionLintErrors: LintErrorsStore = {};
|
||||||
|
for (const jsObjectName of jsEntities) {
|
||||||
|
const jsObjectBodyPath = `["${jsObjectName}.body"]`;
|
||||||
|
const oldJsBodyLintErrors: LintError[] = yield select((state: AppState) =>
|
||||||
|
get(state.linting.errors, jsObjectBodyPath, []),
|
||||||
|
);
|
||||||
|
const newJSBodyLintErrors = get(
|
||||||
|
errors,
|
||||||
|
jsObjectBodyPath,
|
||||||
|
[] as LintError[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const newJSBodyLintErrorsOriginalPaths = newJSBodyLintErrors.reduce(
|
||||||
|
(paths, currentError) => {
|
||||||
|
if (currentError.originalPath)
|
||||||
|
return union(paths, [currentError.originalPath]);
|
||||||
|
return paths;
|
||||||
|
},
|
||||||
|
[] as string[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const jsObjectState = get(jsObjectsState, jsObjectName, {});
|
||||||
|
const jsObjectProperties = Object.keys(jsObjectState);
|
||||||
|
|
||||||
|
const filteredOldJsObjectBodyLintErrors = oldJsBodyLintErrors.filter(
|
||||||
|
(lintError) =>
|
||||||
|
lintError.originalPath &&
|
||||||
|
lintError.originalPath in jsObjectProperties &&
|
||||||
|
!(lintError.originalPath in newJSBodyLintErrorsOriginalPaths),
|
||||||
|
);
|
||||||
|
const updatedLintErrors = [
|
||||||
|
...filteredOldJsObjectBodyLintErrors,
|
||||||
|
...newJSBodyLintErrors,
|
||||||
|
];
|
||||||
|
set(updatedJSCollectionLintErrors, jsObjectBodyPath, updatedLintErrors);
|
||||||
|
}
|
||||||
|
return updatedJSCollectionLintErrors;
|
||||||
|
}
|
||||||
|
|
||||||
export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) {
|
export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) {
|
||||||
const { configTree, pathsToLint, unevalTree } = action.payload;
|
const {
|
||||||
|
asyncJSFunctionsInDataFields,
|
||||||
|
configTree,
|
||||||
|
jsPropertiesState,
|
||||||
|
pathsToLint,
|
||||||
|
unevalTree,
|
||||||
|
} = action.payload;
|
||||||
// only perform lint operations in edit mode
|
// only perform lint operations in edit mode
|
||||||
const appMode: APP_MODE = yield select(getAppMode);
|
const appMode: APP_MODE = yield select(getAppMode);
|
||||||
if (appMode !== APP_MODE.EDIT) return;
|
if (appMode !== APP_MODE.EDIT) return;
|
||||||
|
|
@ -44,18 +99,32 @@ export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) {
|
||||||
const lintTreeRequestData: LintTreeRequest = {
|
const lintTreeRequestData: LintTreeRequest = {
|
||||||
pathsToLint,
|
pathsToLint,
|
||||||
unevalTree,
|
unevalTree,
|
||||||
|
jsPropertiesState,
|
||||||
configTree,
|
configTree,
|
||||||
cloudHosting: !!APPSMITH_CONFIGS.cloudHosting,
|
cloudHosting: !!APPSMITH_CONFIGS.cloudHosting,
|
||||||
|
asyncJSFunctionsInDataFields,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { errors }: LintTreeResponse = yield call(
|
const { errors, updatedJSEntities }: LintTreeResponse = yield call(
|
||||||
lintWorker.request,
|
lintWorker.request,
|
||||||
LINT_WORKER_ACTIONS.LINT_TREE,
|
LINT_WORKER_ACTIONS.LINT_TREE,
|
||||||
lintTreeRequestData,
|
lintTreeRequestData,
|
||||||
);
|
);
|
||||||
|
|
||||||
yield put(setLintingErrors(errors));
|
const oldJSCollectionLintErrors: LintErrorsStore =
|
||||||
yield call(logLatestLintPropertyErrors, { errors, dataTree: unevalTree });
|
yield getValidOldJSCollectionLintErrors(
|
||||||
|
updatedJSEntities,
|
||||||
|
errors,
|
||||||
|
jsPropertiesState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedErrors = { ...errors, ...oldJSCollectionLintErrors };
|
||||||
|
|
||||||
|
yield put(setLintingErrors(updatedErrors));
|
||||||
|
yield call(logLatestLintPropertyErrors, {
|
||||||
|
errors,
|
||||||
|
dataTree: unevalTree,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function* lintTreeSagaWatcher() {
|
export default function* lintTreeSagaWatcher() {
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,19 @@ import { ENTITY_TYPE, Severity } from "entities/AppsmithConsole";
|
||||||
import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
||||||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty } from "lodash";
|
||||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
|
||||||
import AppsmithConsole from "utils/AppsmithConsole";
|
import AppsmithConsole from "utils/AppsmithConsole";
|
||||||
import {
|
import {
|
||||||
getEntityNameAndPropertyPath,
|
getEntityNameAndPropertyPath,
|
||||||
isJSAction,
|
isJSAction,
|
||||||
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||||
|
|
||||||
// We currently only log lint errors in JSObjects
|
// We currently only log lint errors in JSObjects
|
||||||
export function* logLatestLintPropertyErrors({
|
export function* logLatestLintPropertyErrors({
|
||||||
dataTree,
|
dataTree,
|
||||||
errors,
|
errors,
|
||||||
}: {
|
}: {
|
||||||
errors: LintErrors;
|
errors: LintErrorsStore;
|
||||||
dataTree: DataTree;
|
dataTree: DataTree;
|
||||||
}) {
|
}) {
|
||||||
const errorsToAdd = [];
|
const errorsToAdd = [];
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
import type { AppState } from "@appsmith/reducers";
|
import type { AppState } from "@appsmith/reducers";
|
||||||
import { get } from "lodash";
|
import { get } from "lodash";
|
||||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
|
||||||
import type { LintError } from "utils/DynamicBindingUtils";
|
import type { LintError } from "utils/DynamicBindingUtils";
|
||||||
|
|
||||||
export const getAllLintErrors = (state: AppState): LintErrors =>
|
|
||||||
state.linting.errors;
|
|
||||||
|
|
||||||
const emptyLint: LintError[] = [];
|
const emptyLint: LintError[] = [];
|
||||||
|
|
||||||
export const getEntityLintErrors = (state: AppState, path?: string) => {
|
export const getEntityLintErrors = (state: AppState, path?: string) => {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,11 @@ import { createNavData } from "utils/NavigationSelector/common";
|
||||||
import { getWidgetChildrenNavData } from "utils/NavigationSelector/WidgetChildren";
|
import { getWidgetChildrenNavData } from "utils/NavigationSelector/WidgetChildren";
|
||||||
import { getJsChildrenNavData } from "utils/NavigationSelector/JsChildren";
|
import { getJsChildrenNavData } from "utils/NavigationSelector/JsChildren";
|
||||||
import { getAppsmithNavData } from "utils/NavigationSelector/AppsmithNavData";
|
import { getAppsmithNavData } from "utils/NavigationSelector/AppsmithNavData";
|
||||||
import { isJSAction } from "ce/workers/Evaluation/evaluationUtils";
|
import {
|
||||||
|
getEntityNameAndPropertyPath,
|
||||||
|
isJSAction,
|
||||||
|
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
import type { AppState } from "@appsmith/reducers";
|
||||||
|
|
||||||
export type NavigationData = {
|
export type NavigationData = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -124,3 +128,20 @@ export const getEntitiesForNavigation = createSelector(
|
||||||
return navigationData;
|
return navigationData;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getJSFunctionNavigationUrl = createSelector(
|
||||||
|
[
|
||||||
|
(state: AppState, entityName: string) =>
|
||||||
|
getEntitiesForNavigation(state, entityName),
|
||||||
|
(_, __, jsFunctionFullName: string | undefined) => jsFunctionFullName,
|
||||||
|
],
|
||||||
|
(entitiesForNavigation, jsFunctionFullName) => {
|
||||||
|
if (!jsFunctionFullName) return undefined;
|
||||||
|
const { entityName: jsObjectName, propertyPath: jsFunctionName } =
|
||||||
|
getEntityNameAndPropertyPath(jsFunctionFullName);
|
||||||
|
const jsObjectNavigationData = entitiesForNavigation[jsObjectName];
|
||||||
|
const jsFuncNavigationData =
|
||||||
|
jsObjectNavigationData && jsObjectNavigationData.children[jsFunctionName];
|
||||||
|
return jsFuncNavigationData?.url;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,15 @@ export enum PropertyEvaluationErrorType {
|
||||||
PARSE = "PARSE",
|
PARSE = "PARSE",
|
||||||
LINT = "LINT",
|
LINT = "LINT",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PropertyEvaluationErrorCategory {
|
||||||
|
INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD = "INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD",
|
||||||
|
}
|
||||||
|
export interface PropertyEvaluationErrorKind {
|
||||||
|
category: PropertyEvaluationErrorCategory;
|
||||||
|
rootcause: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DataTreeError {
|
export interface DataTreeError {
|
||||||
raw: string;
|
raw: string;
|
||||||
errorMessage: Error;
|
errorMessage: Error;
|
||||||
|
|
@ -364,6 +373,7 @@ export interface EvaluationError extends DataTreeError {
|
||||||
| PropertyEvaluationErrorType.PARSE
|
| PropertyEvaluationErrorType.PARSE
|
||||||
| PropertyEvaluationErrorType.VALIDATION;
|
| PropertyEvaluationErrorType.VALIDATION;
|
||||||
originalBinding?: string;
|
originalBinding?: string;
|
||||||
|
kind?: PropertyEvaluationErrorKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LintError extends DataTreeError {
|
export interface LintError extends DataTreeError {
|
||||||
|
|
@ -374,6 +384,7 @@ export interface LintError extends DataTreeError {
|
||||||
code: string;
|
code: string;
|
||||||
line: number;
|
line: number;
|
||||||
ch: number;
|
ch: number;
|
||||||
|
originalPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTreeEvaluationProps {
|
export interface DataTreeEvaluationProps {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
import {
|
||||||
|
getEntityNameAndPropertyPath,
|
||||||
|
isATriggerPath,
|
||||||
|
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
import { APP_MODE } from "entities/App";
|
||||||
|
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
||||||
|
import { difference, get, isString } from "lodash";
|
||||||
|
import type { DependencyMap } from "utils/DynamicBindingUtils";
|
||||||
|
import { getDynamicBindings } from "utils/DynamicBindingUtils";
|
||||||
|
import { isChildPropertyPath } from "utils/DynamicBindingUtils";
|
||||||
|
import {
|
||||||
|
isDataField,
|
||||||
|
isWidgetActionOrJsObject,
|
||||||
|
} from "workers/common/DataTreeEvaluator/utils";
|
||||||
|
import {
|
||||||
|
isAsyncJSFunction,
|
||||||
|
isJSFunction,
|
||||||
|
updateMap,
|
||||||
|
} from "workers/common/DependencyMap/utils";
|
||||||
|
|
||||||
|
export class AsyncJsFunctionInDataField {
|
||||||
|
private asyncFunctionsInDataFieldsMap: DependencyMap = {};
|
||||||
|
private isDisabled = true;
|
||||||
|
initialize(appMode: APP_MODE | undefined) {
|
||||||
|
this.isDisabled = !(appMode === APP_MODE.EDIT);
|
||||||
|
this.asyncFunctionsInDataFieldsMap = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
update(
|
||||||
|
fullPath: string,
|
||||||
|
referencesInPath: string[],
|
||||||
|
unEvalDataTree: DataTree,
|
||||||
|
configTree: ConfigTree,
|
||||||
|
) {
|
||||||
|
if (this.isDisabled) return [];
|
||||||
|
const updatedAsyncJSFunctionsInMap = new Set<string>();
|
||||||
|
// Only datafields can cause updates
|
||||||
|
if (!isDataField(fullPath, configTree)) return [];
|
||||||
|
|
||||||
|
const asyncJSFunctionsInvokedInPath = getAsyncJSFunctionInvocationsInPath(
|
||||||
|
referencesInPath,
|
||||||
|
unEvalDataTree,
|
||||||
|
configTree,
|
||||||
|
fullPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const asyncJSFunc of asyncJSFunctionsInvokedInPath) {
|
||||||
|
updatedAsyncJSFunctionsInMap.add(asyncJSFunc);
|
||||||
|
updateMap(this.asyncFunctionsInDataFieldsMap, asyncJSFunc, [fullPath], {
|
||||||
|
deleteOnEmpty: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Array.from(updatedAsyncJSFunctionsInMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePathDeletion(
|
||||||
|
deletedPath: string,
|
||||||
|
unevalTree: DataTree,
|
||||||
|
configTree: ConfigTree,
|
||||||
|
) {
|
||||||
|
if (this.isDisabled) return [];
|
||||||
|
const updatedAsyncJSFunctionsInMap = new Set<string>();
|
||||||
|
const { entityName, propertyPath } =
|
||||||
|
getEntityNameAndPropertyPath(deletedPath);
|
||||||
|
const entity = unevalTree[entityName];
|
||||||
|
const entityConfig = configTree[entityName];
|
||||||
|
if (
|
||||||
|
isWidgetActionOrJsObject(entity) ||
|
||||||
|
isATriggerPath(entityConfig, propertyPath)
|
||||||
|
)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
Object.keys(this.asyncFunctionsInDataFieldsMap).forEach((asyncFuncName) => {
|
||||||
|
if (isChildPropertyPath(deletedPath, asyncFuncName)) {
|
||||||
|
this.deleteFunctionFromMap(asyncFuncName);
|
||||||
|
} else {
|
||||||
|
const toRemove: string[] = [];
|
||||||
|
this.asyncFunctionsInDataFieldsMap[asyncFuncName].forEach(
|
||||||
|
(dependantPath) => {
|
||||||
|
if (isChildPropertyPath(deletedPath, dependantPath)) {
|
||||||
|
updatedAsyncJSFunctionsInMap.add(asyncFuncName);
|
||||||
|
toRemove.push(dependantPath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const newAsyncFunctiondependents = difference(
|
||||||
|
this.asyncFunctionsInDataFieldsMap[asyncFuncName],
|
||||||
|
toRemove,
|
||||||
|
);
|
||||||
|
updateMap(
|
||||||
|
this.asyncFunctionsInDataFieldsMap,
|
||||||
|
asyncFuncName,
|
||||||
|
newAsyncFunctiondependents,
|
||||||
|
{ replaceValue: true, deleteOnEmpty: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(updatedAsyncJSFunctionsInMap);
|
||||||
|
}
|
||||||
|
handlePathEdit(
|
||||||
|
editedPath: string,
|
||||||
|
dependenciesInPath: string[],
|
||||||
|
unevalTree: DataTree,
|
||||||
|
inverseDependencyMap: DependencyMap,
|
||||||
|
configTree: ConfigTree,
|
||||||
|
) {
|
||||||
|
if (this.isDisabled) return [];
|
||||||
|
const updatedAsyncJSFunctionsInMap = new Set<string>();
|
||||||
|
if (isDataField(editedPath, configTree)) {
|
||||||
|
const asyncJSFunctionInvocationsInPath =
|
||||||
|
getAsyncJSFunctionInvocationsInPath(
|
||||||
|
dependenciesInPath,
|
||||||
|
unevalTree,
|
||||||
|
configTree,
|
||||||
|
editedPath,
|
||||||
|
);
|
||||||
|
asyncJSFunctionInvocationsInPath.forEach((funcName) => {
|
||||||
|
updatedAsyncJSFunctionsInMap.add(funcName);
|
||||||
|
updateMap(this.asyncFunctionsInDataFieldsMap, funcName, [editedPath], {
|
||||||
|
deleteOnEmpty: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(this.asyncFunctionsInDataFieldsMap).forEach(
|
||||||
|
(asyncFuncName) => {
|
||||||
|
const toRemove: string[] = [];
|
||||||
|
this.asyncFunctionsInDataFieldsMap[asyncFuncName].forEach(
|
||||||
|
(dependantPath) => {
|
||||||
|
if (
|
||||||
|
editedPath === dependantPath &&
|
||||||
|
!asyncJSFunctionInvocationsInPath.includes(asyncFuncName)
|
||||||
|
) {
|
||||||
|
updatedAsyncJSFunctionsInMap.add(asyncFuncName);
|
||||||
|
toRemove.push(dependantPath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const newAsyncFunctiondependents = difference(
|
||||||
|
this.asyncFunctionsInDataFieldsMap[asyncFuncName],
|
||||||
|
toRemove,
|
||||||
|
);
|
||||||
|
updateMap(
|
||||||
|
this.asyncFunctionsInDataFieldsMap,
|
||||||
|
asyncFuncName,
|
||||||
|
newAsyncFunctiondependents,
|
||||||
|
{ replaceValue: true, deleteOnEmpty: true },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (isJSFunction(configTree, editedPath)) {
|
||||||
|
if (
|
||||||
|
!isAsyncJSFunction(configTree, editedPath) &&
|
||||||
|
Object.keys(this.asyncFunctionsInDataFieldsMap).includes(editedPath)
|
||||||
|
) {
|
||||||
|
updatedAsyncJSFunctionsInMap.add(editedPath);
|
||||||
|
delete this.asyncFunctionsInDataFieldsMap[editedPath];
|
||||||
|
} else if (isAsyncJSFunction(configTree, editedPath)) {
|
||||||
|
const boundFields = inverseDependencyMap[editedPath];
|
||||||
|
let boundDataFields: string[] = [];
|
||||||
|
if (boundFields) {
|
||||||
|
boundDataFields = boundFields.filter((path) =>
|
||||||
|
isDataField(path, configTree),
|
||||||
|
);
|
||||||
|
for (const dataFieldPath of boundDataFields) {
|
||||||
|
const asyncJSFunctionInvocationsInPath =
|
||||||
|
getAsyncJSFunctionInvocationsInPath(
|
||||||
|
[editedPath],
|
||||||
|
unevalTree,
|
||||||
|
configTree,
|
||||||
|
dataFieldPath,
|
||||||
|
);
|
||||||
|
if (asyncJSFunctionInvocationsInPath) {
|
||||||
|
updatedAsyncJSFunctionsInMap.add(editedPath);
|
||||||
|
updateMap(
|
||||||
|
this.asyncFunctionsInDataFieldsMap,
|
||||||
|
editedPath,
|
||||||
|
[dataFieldPath],
|
||||||
|
{ deleteOnEmpty: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(updatedAsyncJSFunctionsInMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMap() {
|
||||||
|
return this.asyncFunctionsInDataFieldsMap;
|
||||||
|
}
|
||||||
|
deleteFunctionFromMap(funcName: string) {
|
||||||
|
this.asyncFunctionsInDataFieldsMap[funcName] &&
|
||||||
|
delete this.asyncFunctionsInDataFieldsMap[funcName];
|
||||||
|
}
|
||||||
|
getAsyncFunctionBindingInDataField(fullPath: string): string | undefined {
|
||||||
|
let hasAsyncFunctionInvocation: string | undefined = undefined;
|
||||||
|
Object.keys(this.asyncFunctionsInDataFieldsMap).forEach((path) => {
|
||||||
|
if (this.asyncFunctionsInDataFieldsMap[path].includes(fullPath)) {
|
||||||
|
return (hasAsyncFunctionInvocation = path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hasAsyncFunctionInvocation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAsyncJSFunctionInvocationsInPath(
|
||||||
|
dependencies: string[],
|
||||||
|
unEvalTree: DataTree,
|
||||||
|
configTree: ConfigTree,
|
||||||
|
fullPath: string,
|
||||||
|
) {
|
||||||
|
const invokedAsyncJSFunctions = new Set<string>();
|
||||||
|
const { entityName, propertyPath } = getEntityNameAndPropertyPath(fullPath);
|
||||||
|
const entity = unEvalTree[entityName];
|
||||||
|
const unevalPropValue = get(entity, propertyPath);
|
||||||
|
|
||||||
|
dependencies.forEach((dependant) => {
|
||||||
|
if (
|
||||||
|
isAsyncJSFunction(configTree, dependant) &&
|
||||||
|
isFunctionInvoked(dependant, unevalPropValue)
|
||||||
|
) {
|
||||||
|
invokedAsyncJSFunctions.add(dependant);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(invokedAsyncJSFunctions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFunctionInvocationRegex(funcName: string) {
|
||||||
|
return new RegExp(`${funcName}[.call | .apply]*\s*\\(.*?\\)`, "g");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFunctionInvoked(
|
||||||
|
functionName: string,
|
||||||
|
unevalPropValue: unknown,
|
||||||
|
) {
|
||||||
|
if (!isString(unevalPropValue)) return false;
|
||||||
|
const { jsSnippets } = getDynamicBindings(unevalPropValue);
|
||||||
|
for (const jsSnippet of jsSnippets) {
|
||||||
|
if (!jsSnippet.includes(functionName)) continue;
|
||||||
|
const isInvoked = getFunctionInvocationRegex(functionName).test(jsSnippet);
|
||||||
|
if (isInvoked) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const asyncJsFunctionInDataFields = new AsyncJsFunctionInDataField();
|
||||||
|
|
@ -2,7 +2,7 @@ import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
||||||
import { isEmpty, set } from "lodash";
|
import { isEmpty, set } from "lodash";
|
||||||
import { EvalErrorTypes } from "utils/DynamicBindingUtils";
|
import { EvalErrorTypes } from "utils/DynamicBindingUtils";
|
||||||
import type { JSUpdate, ParsedJSSubAction } from "utils/JSPaneUtils";
|
import type { JSUpdate, ParsedJSSubAction } from "utils/JSPaneUtils";
|
||||||
import { isTypeOfFunction, parseJSObjectWithAST } from "@shared/ast";
|
import { parseJSObject, isJSFunctionProperty } from "@shared/ast";
|
||||||
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
||||||
import evaluateSync from "workers/Evaluation/evaluate";
|
import evaluateSync from "workers/Evaluation/evaluate";
|
||||||
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
|
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
|
@ -16,7 +16,9 @@ import {
|
||||||
updateJSCollectionInUnEvalTree,
|
updateJSCollectionInUnEvalTree,
|
||||||
} from "workers/Evaluation/JSObject/utils";
|
} from "workers/Evaluation/JSObject/utils";
|
||||||
import { functionDeterminer } from "../functionDeterminer";
|
import { functionDeterminer } from "../functionDeterminer";
|
||||||
|
import { jsPropertiesState } from "./jsPropertiesState";
|
||||||
import type { JSActionEntity } from "entities/DataTree/types";
|
import type { JSActionEntity } from "entities/DataTree/types";
|
||||||
|
import { getFixedTimeDifference } from "workers/common/DataTreeEvaluator/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Here we update our unEvalTree according to the change in JSObject's body
|
* Here we update our unEvalTree according to the change in JSObject's body
|
||||||
|
|
@ -81,94 +83,100 @@ export function saveResolvedFunctionsAndJSUpdates(
|
||||||
unEvalDataTree: DataTree,
|
unEvalDataTree: DataTree,
|
||||||
entityName: string,
|
entityName: string,
|
||||||
) {
|
) {
|
||||||
|
jsPropertiesState.delete(entityName);
|
||||||
const correctFormat = regex.test(entity.body);
|
const correctFormat = regex.test(entity.body);
|
||||||
if (correctFormat) {
|
if (correctFormat) {
|
||||||
const body = entity.body.replace(/export default/g, "");
|
|
||||||
try {
|
try {
|
||||||
delete dataTreeEvalRef.resolvedFunctions[`${entityName}`];
|
delete dataTreeEvalRef.resolvedFunctions[`${entityName}`];
|
||||||
delete dataTreeEvalRef.currentJSCollectionState[`${entityName}`];
|
delete dataTreeEvalRef.currentJSCollectionState[`${entityName}`];
|
||||||
const parseStartTime = performance.now();
|
const parseStartTime = performance.now();
|
||||||
const parsedObject = parseJSObjectWithAST(body);
|
const { parsedObject, success } = parseJSObject(entity.body);
|
||||||
const parseEndTime = performance.now();
|
const parseEndTime = performance.now();
|
||||||
const JSObjectASTParseTime = parseEndTime - parseStartTime;
|
const JSObjectASTParseTime = getFixedTimeDifference(
|
||||||
|
parseEndTime,
|
||||||
|
parseStartTime,
|
||||||
|
);
|
||||||
dataTreeEvalRef.logs.push({
|
dataTreeEvalRef.logs.push({
|
||||||
JSObjectName: entityName,
|
JSObjectName: entityName,
|
||||||
JSObjectASTParseTime,
|
JSObjectASTParseTime,
|
||||||
});
|
});
|
||||||
const actions: any = [];
|
const actions: any = [];
|
||||||
const variables: any = [];
|
const variables: any = [];
|
||||||
if (!!parsedObject) {
|
if (success) {
|
||||||
parsedObject.forEach((parsedElement) => {
|
if (!!parsedObject) {
|
||||||
if (isTypeOfFunction(parsedElement.type)) {
|
jsPropertiesState.update(entityName, parsedObject);
|
||||||
try {
|
parsedObject.forEach((parsedElement) => {
|
||||||
const { result } = evaluateSync(
|
if (isJSFunctionProperty(parsedElement)) {
|
||||||
parsedElement.value,
|
try {
|
||||||
unEvalDataTree,
|
const { result } = evaluateSync(
|
||||||
{},
|
parsedElement.value,
|
||||||
false,
|
unEvalDataTree,
|
||||||
undefined,
|
{},
|
||||||
undefined,
|
false,
|
||||||
);
|
undefined,
|
||||||
if (!!result) {
|
undefined,
|
||||||
let params: Array<{ key: string; value: unknown }> = [];
|
);
|
||||||
|
if (!!result) {
|
||||||
|
let params: Array<{ key: string; value: unknown }> = [];
|
||||||
|
|
||||||
if (parsedElement.arguments) {
|
if (parsedElement.arguments) {
|
||||||
params = parsedElement.arguments.map(
|
params = parsedElement.arguments.map(
|
||||||
({ defaultValue, paramName }) => ({
|
({ defaultValue, paramName }) => ({
|
||||||
key: paramName,
|
key: paramName,
|
||||||
value: defaultValue,
|
value: defaultValue,
|
||||||
}),
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionString = parsedElement.value;
|
||||||
|
set(
|
||||||
|
dataTreeEvalRef.resolvedFunctions,
|
||||||
|
`${entityName}.${parsedElement.key}`,
|
||||||
|
result,
|
||||||
);
|
);
|
||||||
|
set(
|
||||||
|
dataTreeEvalRef.currentJSCollectionState,
|
||||||
|
`${entityName}.${parsedElement.key}`,
|
||||||
|
functionString,
|
||||||
|
);
|
||||||
|
actions.push({
|
||||||
|
name: parsedElement.key,
|
||||||
|
body: functionString,
|
||||||
|
arguments: params,
|
||||||
|
parsedFunction: result,
|
||||||
|
isAsync: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
const functionString = parsedElement.value;
|
// in case we need to handle error state
|
||||||
set(
|
|
||||||
dataTreeEvalRef.resolvedFunctions,
|
|
||||||
`${entityName}.${parsedElement.key}`,
|
|
||||||
result,
|
|
||||||
);
|
|
||||||
set(
|
|
||||||
dataTreeEvalRef.currentJSCollectionState,
|
|
||||||
`${entityName}.${parsedElement.key}`,
|
|
||||||
functionString,
|
|
||||||
);
|
|
||||||
actions.push({
|
|
||||||
name: parsedElement.key,
|
|
||||||
body: functionString,
|
|
||||||
arguments: params,
|
|
||||||
parsedFunction: result,
|
|
||||||
isAsync: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch {
|
} else if (parsedElement.type !== "literal") {
|
||||||
// in case we need to handle error state
|
variables.push({
|
||||||
|
name: parsedElement.key,
|
||||||
|
value: parsedElement.value,
|
||||||
|
});
|
||||||
|
set(
|
||||||
|
dataTreeEvalRef.currentJSCollectionState,
|
||||||
|
`${entityName}.${parsedElement.key}`,
|
||||||
|
parsedElement.value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (parsedElement.type !== "literal") {
|
});
|
||||||
variables.push({
|
const parsedBody = {
|
||||||
name: parsedElement.key,
|
body: entity.body,
|
||||||
value: parsedElement.value,
|
actions: actions,
|
||||||
});
|
variables,
|
||||||
set(
|
};
|
||||||
dataTreeEvalRef.currentJSCollectionState,
|
set(jsUpdates, `${entityName}`, {
|
||||||
`${entityName}.${parsedElement.key}`,
|
parsedBody,
|
||||||
parsedElement.value,
|
id: entity.actionId,
|
||||||
);
|
});
|
||||||
}
|
} else {
|
||||||
});
|
set(jsUpdates, `${entityName}`, {
|
||||||
const parsedBody = {
|
parsedBody: undefined,
|
||||||
body: entity.body,
|
id: entity.actionId,
|
||||||
actions: actions,
|
});
|
||||||
variables,
|
}
|
||||||
};
|
|
||||||
set(jsUpdates, `${entityName}`, {
|
|
||||||
parsedBody,
|
|
||||||
id: entity.actionId,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
set(jsUpdates, `${entityName}`, {
|
|
||||||
parsedBody: undefined,
|
|
||||||
id: entity.actionId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
//if we need to push error as popup in case
|
//if we need to push error as popup in case
|
||||||
|
|
@ -194,6 +202,7 @@ export function parseJSActions(
|
||||||
differences?: DataTreeDiff[],
|
differences?: DataTreeDiff[],
|
||||||
) {
|
) {
|
||||||
let jsUpdates: Record<string, JSUpdate> = {};
|
let jsUpdates: Record<string, JSUpdate> = {};
|
||||||
|
jsPropertiesState.startUpdate();
|
||||||
if (!!differences && !!oldUnEvalTree) {
|
if (!!differences && !!oldUnEvalTree) {
|
||||||
differences.forEach((diff) => {
|
differences.forEach((diff) => {
|
||||||
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
|
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
|
||||||
|
|
@ -249,7 +258,7 @@ export function parseJSActions(
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
jsPropertiesState.stopUpdate();
|
||||||
functionDeterminer.setupEval(
|
functionDeterminer.setupEval(
|
||||||
unEvalDataTree,
|
unEvalDataTree,
|
||||||
dataTreeEvalRef.resolvedFunctions,
|
dataTreeEvalRef.resolvedFunctions,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import type { JSPropertyPosition, TParsedJSProperty } from "@shared/ast";
|
||||||
|
import { isJSFunctionProperty } from "@shared/ast";
|
||||||
|
import { diff } from "deep-diff";
|
||||||
|
import { klona } from "klona/full";
|
||||||
|
import { set, union } from "lodash";
|
||||||
|
|
||||||
|
class JsPropertiesState {
|
||||||
|
private jsPropertiesState: TJSPropertiesState = {};
|
||||||
|
private oldJsPropertiesState: TJSPropertiesState = {};
|
||||||
|
private updatedProperties: string[] = [];
|
||||||
|
|
||||||
|
startUpdate() {
|
||||||
|
this.oldJsPropertiesState = klona(this.jsPropertiesState);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(jsObjectName: string) {
|
||||||
|
delete this.jsPropertiesState[`${jsObjectName}`];
|
||||||
|
}
|
||||||
|
|
||||||
|
update(jsObjectName: string, properties: TParsedJSProperty[]) {
|
||||||
|
for (const jsObjectProperty of properties) {
|
||||||
|
const { key, position, rawContent, type } = jsObjectProperty;
|
||||||
|
if (isJSFunctionProperty(jsObjectProperty)) {
|
||||||
|
set(
|
||||||
|
this.jsPropertiesState,
|
||||||
|
`[${jsObjectName}.${jsObjectProperty.key}]`,
|
||||||
|
{
|
||||||
|
position: position,
|
||||||
|
value: rawContent,
|
||||||
|
isMarkedAsync: jsObjectProperty.isMarkedAsync,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (type !== "literal") {
|
||||||
|
set(this.jsPropertiesState, `[${jsObjectName}.${key}]`, {
|
||||||
|
position: position,
|
||||||
|
value: rawContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopUpdate() {
|
||||||
|
const difference = diff(this.oldJsPropertiesState, this.jsPropertiesState);
|
||||||
|
let updatedJSProperties: string[] = [];
|
||||||
|
if (difference) {
|
||||||
|
updatedJSProperties = difference.reduce(
|
||||||
|
(updatedProperties, currentDiff) => {
|
||||||
|
if (!currentDiff.path) return updatedProperties;
|
||||||
|
const updatedProperty = currentDiff.path.slice(0, 2).join(".");
|
||||||
|
return union(updatedProperties, [updatedProperty]);
|
||||||
|
},
|
||||||
|
[] as string[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.updatedProperties = updatedJSProperties;
|
||||||
|
}
|
||||||
|
getMap() {
|
||||||
|
return this.jsPropertiesState;
|
||||||
|
}
|
||||||
|
getUpdatedJSProperties() {
|
||||||
|
return this.updatedProperties;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const jsPropertiesState = new JsPropertiesState();
|
||||||
|
|
||||||
|
export interface TBasePropertyState {
|
||||||
|
value: string;
|
||||||
|
position: JSPropertyPosition;
|
||||||
|
}
|
||||||
|
export interface TJSFunctionPropertyState extends TBasePropertyState {
|
||||||
|
isMarkedAsync: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TJSpropertyState = TBasePropertyState | TJSFunctionPropertyState;
|
||||||
|
|
||||||
|
export type TJSPropertiesState = Record<
|
||||||
|
string,
|
||||||
|
Record<string, TJSpropertyState>
|
||||||
|
>;
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -216,7 +216,6 @@ export const removeFunctionsAndVariableJSCollection = (
|
||||||
unset(modifiedDataTree[entityName], varName);
|
unset(modifiedDataTree[entityName], varName);
|
||||||
}
|
}
|
||||||
//remove functions
|
//remove functions
|
||||||
|
|
||||||
const reactivePaths = entity.reactivePaths;
|
const reactivePaths = entity.reactivePaths;
|
||||||
const meta = entity.meta;
|
const meta = entity.meta;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,46 +124,46 @@ describe("Test error modifier", () => {
|
||||||
errorModifier.updateAsyncFunctions(dataTree);
|
errorModifier.updateAsyncFunctions(dataTree);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("TypeError for defined Api in sync field ", () => {
|
it("TypeError for defined Api in data field ", () => {
|
||||||
const error = new Error();
|
const error = new Error();
|
||||||
error.name = "TypeError";
|
error.name = "TypeError";
|
||||||
error.message = "Api2.run is not a function";
|
error.message = "Api2.run is not a function";
|
||||||
const result = errorModifier.run(error);
|
const { errorMessage: result } = errorModifier.run(error);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
name: "ValidationError",
|
name: "ValidationError",
|
||||||
message:
|
message:
|
||||||
"Found a reference to Api2.run() during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to Api2.run() and try again.",
|
"Found a reference to Api2.run() during evaluation. Data fields cannot execute framework actions. Please remove any direct/indirect references to Api2.run() and try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("TypeError for undefined Api in sync field ", () => {
|
it("TypeError for undefined Api in data field ", () => {
|
||||||
const error = new Error();
|
const error = new Error();
|
||||||
error.name = "TypeError";
|
error.name = "TypeError";
|
||||||
error.message = "Api1.run is not a function";
|
error.message = "Api1.run is not a function";
|
||||||
const result = errorModifier.run(error);
|
const { errorMessage: result } = errorModifier.run(error);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
name: "TypeError",
|
name: "TypeError",
|
||||||
message: "Api1.run is not a function",
|
message: "Api1.run is not a function",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ReferenceError for platform function in sync field", () => {
|
it("ReferenceError for platform function in data field", () => {
|
||||||
const error = new Error();
|
const error = new Error();
|
||||||
error.name = "ReferenceError";
|
error.name = "ReferenceError";
|
||||||
error.message = "storeValue is not defined";
|
error.message = "storeValue is not defined";
|
||||||
const result = errorModifier.run(error);
|
const { errorMessage: result } = errorModifier.run(error);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
name: "ValidationError",
|
name: "ValidationError",
|
||||||
message:
|
message:
|
||||||
"Found a reference to storeValue() during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to storeValue() and try again.",
|
"Found a reference to storeValue() during evaluation. Data fields cannot execute framework actions. Please remove any direct/indirect references to storeValue() and try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ReferenceError for undefined function in sync field", () => {
|
it("ReferenceError for undefined function in data field", () => {
|
||||||
const error = new Error();
|
const error = new Error();
|
||||||
error.name = "ReferenceError";
|
error.name = "ReferenceError";
|
||||||
error.message = "storeValue2 is not defined";
|
error.message = "storeValue2 is not defined";
|
||||||
const result = errorModifier.run(error);
|
const { errorMessage: result } = errorModifier.run(error);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
name: error.name,
|
name: error.name,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ describe("evaluateSync", () => {
|
||||||
message: "wrongJS is not defined",
|
message: "wrongJS is not defined",
|
||||||
},
|
},
|
||||||
errorType: "PARSE",
|
errorType: "PARSE",
|
||||||
|
kind: undefined,
|
||||||
raw: `
|
raw: `
|
||||||
function $$closedFn () {
|
function $$closedFn () {
|
||||||
const $$result = wrongJS
|
const $$result = wrongJS
|
||||||
|
|
@ -98,6 +99,7 @@ describe("evaluateSync", () => {
|
||||||
message: "{}.map is not a function",
|
message: "{}.map is not a function",
|
||||||
},
|
},
|
||||||
errorType: "PARSE",
|
errorType: "PARSE",
|
||||||
|
kind: undefined,
|
||||||
raw: `
|
raw: `
|
||||||
function $$closedFn () {
|
function $$closedFn () {
|
||||||
const $$result = {}.map()
|
const $$result = {}.map()
|
||||||
|
|
@ -128,6 +130,7 @@ describe("evaluateSync", () => {
|
||||||
message: "setImmediate is not defined",
|
message: "setImmediate is not defined",
|
||||||
},
|
},
|
||||||
errorType: "PARSE",
|
errorType: "PARSE",
|
||||||
|
kind: undefined,
|
||||||
raw: `
|
raw: `
|
||||||
function $$closedFn () {
|
function $$closedFn () {
|
||||||
const $$result = setImmediate(() => {}, 100)
|
const $$result = setImmediate(() => {}, 100)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||||
import { getAllAsyncFunctions } from "@appsmith/workers/Evaluation/Actions";
|
import { getAllAsyncFunctions } from "@appsmith/workers/Evaluation/Actions";
|
||||||
|
import type { EvaluationError } from "utils/DynamicBindingUtils";
|
||||||
|
import { PropertyEvaluationErrorCategory } from "utils/DynamicBindingUtils";
|
||||||
|
|
||||||
|
const FOUND_ASYNC_IN_SYNC_EVAL_MESSAGE =
|
||||||
|
"Found an action invocation during evaluation. Data fields cannot execute actions.";
|
||||||
const UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR =
|
const UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR =
|
||||||
"Found a reference to {{actionName}} during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.";
|
"Found a reference to {{actionName}} during evaluation. Data fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.";
|
||||||
|
|
||||||
class ErrorModifier {
|
class ErrorModifier {
|
||||||
private errorNamesToScan = ["ReferenceError", "TypeError"];
|
private errorNamesToScan = ["ReferenceError", "TypeError"];
|
||||||
// Note all regex below groups the async function name
|
// Note all regex below groups the async function name
|
||||||
|
|
@ -14,10 +17,23 @@ class ErrorModifier {
|
||||||
this.asyncFunctionsNameMap = getAllAsyncFunctions(dataTree);
|
this.asyncFunctionsNameMap = getAllAsyncFunctions(dataTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
run(error: Error) {
|
run(error: Error): {
|
||||||
|
errorMessage: ReturnType<typeof getErrorMessage>;
|
||||||
|
errorCategory?: PropertyEvaluationErrorCategory;
|
||||||
|
} {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
if (
|
||||||
|
error instanceof FoundPromiseInSyncEvalError ||
|
||||||
|
error instanceof ActionCalledInSyncFieldError
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
errorMessage,
|
||||||
|
errorCategory:
|
||||||
|
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.errorNamesToScan.includes(error.name)) return errorMessage;
|
if (!this.errorNamesToScan.includes(error.name)) return { errorMessage };
|
||||||
|
|
||||||
for (const asyncFunctionFullPath of Object.keys(
|
for (const asyncFunctionFullPath of Object.keys(
|
||||||
this.asyncFunctionsNameMap,
|
this.asyncFunctionsNameMap,
|
||||||
|
|
@ -25,27 +41,49 @@ class ErrorModifier {
|
||||||
const functionNameWithWhiteSpace = " " + asyncFunctionFullPath + " ";
|
const functionNameWithWhiteSpace = " " + asyncFunctionFullPath + " ";
|
||||||
if (getErrorMessageWithType(error).match(functionNameWithWhiteSpace)) {
|
if (getErrorMessageWithType(error).match(functionNameWithWhiteSpace)) {
|
||||||
return {
|
return {
|
||||||
name: "ValidationError",
|
errorMessage: {
|
||||||
message: UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR.replaceAll(
|
name: "ValidationError",
|
||||||
"{{actionName}}",
|
message: UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR.replaceAll(
|
||||||
asyncFunctionFullPath + "()",
|
"{{actionName}}",
|
||||||
),
|
asyncFunctionFullPath + "()",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
errorCategory:
|
||||||
|
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errorMessage;
|
return { errorMessage };
|
||||||
|
}
|
||||||
|
setAsyncInvocationErrorsRootcause(
|
||||||
|
errors: EvaluationError[],
|
||||||
|
asyncFunc: string,
|
||||||
|
) {
|
||||||
|
return errors.map((error) => {
|
||||||
|
if (isAsyncFunctionCalledInSyncFieldError(error)) {
|
||||||
|
error.errorMessage.message = FOUND_ASYNC_IN_SYNC_EVAL_MESSAGE;
|
||||||
|
error.kind = {
|
||||||
|
category:
|
||||||
|
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD,
|
||||||
|
rootcause: asyncFunc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const errorModifier = new ErrorModifier();
|
export const errorModifier = new ErrorModifier();
|
||||||
|
|
||||||
|
const FOUND_PROMISE_IN_SYNC_EVAL_MESSAGE =
|
||||||
|
"Found a Promise() during evaluation. Data fields cannot execute asynchronous code.";
|
||||||
|
|
||||||
export class FoundPromiseInSyncEvalError extends Error {
|
export class FoundPromiseInSyncEvalError extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.name = "";
|
this.name = "";
|
||||||
this.message =
|
this.message = FOUND_PROMISE_IN_SYNC_EVAL_MESSAGE;
|
||||||
"Found a Promise() during evaluation. Sync fields cannot execute asynchronous code.";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,7 +92,7 @@ export class ActionCalledInSyncFieldError extends Error {
|
||||||
super(actionName);
|
super(actionName);
|
||||||
|
|
||||||
if (!actionName) {
|
if (!actionName) {
|
||||||
this.message = "Async function called in a sync field";
|
this.message = "Async function called in a data field";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,3 +119,10 @@ export const getErrorMessage = (error: Error) => {
|
||||||
export const getErrorMessageWithType = (error: Error) => {
|
export const getErrorMessageWithType = (error: Error) => {
|
||||||
return error.name ? `${error.name}: ${error.message}` : error.message;
|
return error.name ? `${error.name}: ${error.message}` : error.message;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isAsyncFunctionCalledInSyncFieldError(error: EvaluationError) {
|
||||||
|
return (
|
||||||
|
error.kind?.category ===
|
||||||
|
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ export enum EvaluationScriptType {
|
||||||
ANONYMOUS_FUNCTION = "ANONYMOUS_FUNCTION",
|
ANONYMOUS_FUNCTION = "ANONYMOUS_FUNCTION",
|
||||||
ASYNC_ANONYMOUS_FUNCTION = "ASYNC_ANONYMOUS_FUNCTION",
|
ASYNC_ANONYMOUS_FUNCTION = "ASYNC_ANONYMOUS_FUNCTION",
|
||||||
TRIGGERS = "TRIGGERS",
|
TRIGGERS = "TRIGGERS",
|
||||||
|
OBJECT_PROPERTY = "OBJECT_PROPERTY",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScriptTemplate = "<<string>>";
|
export const ScriptTemplate = "<<string>>";
|
||||||
|
|
@ -58,6 +59,13 @@ export const EvaluationScripts: Record<EvaluationScriptType, string> = {
|
||||||
}
|
}
|
||||||
$$closedFn.call(THIS_CONTEXT)
|
$$closedFn.call(THIS_CONTEXT)
|
||||||
`,
|
`,
|
||||||
|
[EvaluationScriptType.OBJECT_PROPERTY]: `
|
||||||
|
function $$closedFn () {
|
||||||
|
const $$result = {${ScriptTemplate}}
|
||||||
|
return $$result
|
||||||
|
}
|
||||||
|
$$closedFn.call(THIS_CONTEXT)
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const topLevelWorkerAPIs = Object.keys(self).reduce((acc, key: string) => {
|
const topLevelWorkerAPIs = Object.keys(self).reduce((acc, key: string) => {
|
||||||
|
|
@ -120,8 +128,11 @@ export interface createEvaluationContextArgs {
|
||||||
context?: EvaluateContext;
|
context?: EvaluateContext;
|
||||||
isTriggerBased: boolean;
|
isTriggerBased: boolean;
|
||||||
evalArguments?: Array<unknown>;
|
evalArguments?: Array<unknown>;
|
||||||
// Whether not to add functions like "run", "clear" to entity in global data
|
/*
|
||||||
skipEntityFunctions?: boolean;
|
Whether to remove functions like "run", "clear" from entities in global context
|
||||||
|
use case => To show lint warning when Api.run is used in a function bound to a data field (Eg. Button.text)
|
||||||
|
*/
|
||||||
|
removeEntityFunctions?: boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This method created an object with dataTree and appsmith's framework actions that needs to be added to worker global scope for the JS code evaluation to then consume it.
|
* This method created an object with dataTree and appsmith's framework actions that needs to be added to worker global scope for the JS code evaluation to then consume it.
|
||||||
|
|
@ -135,8 +146,8 @@ export const createEvaluationContext = (args: createEvaluationContextArgs) => {
|
||||||
dataTree,
|
dataTree,
|
||||||
evalArguments,
|
evalArguments,
|
||||||
isTriggerBased,
|
isTriggerBased,
|
||||||
|
removeEntityFunctions,
|
||||||
resolvedFunctions,
|
resolvedFunctions,
|
||||||
skipEntityFunctions,
|
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
const EVAL_CONTEXT: EvalContext = {};
|
const EVAL_CONTEXT: EvalContext = {};
|
||||||
|
|
@ -152,7 +163,7 @@ export const createEvaluationContext = (args: createEvaluationContextArgs) => {
|
||||||
addDataTreeToContext({
|
addDataTreeToContext({
|
||||||
EVAL_CONTEXT,
|
EVAL_CONTEXT,
|
||||||
dataTree,
|
dataTree,
|
||||||
skipEntityFunctions: !!skipEntityFunctions,
|
removeEntityFunctions: !!removeEntityFunctions,
|
||||||
isTriggerBased,
|
isTriggerBased,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -279,18 +290,23 @@ export default function evaluateSync(
|
||||||
result = indirectEval(script);
|
result = indirectEval(script);
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
/**
|
/**
|
||||||
* If a promise is returned in sync field then show the error to help understand sync field doesn't await to resolve promise.
|
* If a promise is returned in data field then show the error to help understand data field doesn't await to resolve promise.
|
||||||
* NOTE: Awaiting for promise will make sync field evaluation slower.
|
* NOTE: Awaiting for promise will make data field evaluation slower.
|
||||||
*/
|
*/
|
||||||
throw new FoundPromiseInSyncEvalError();
|
throw new FoundPromiseInSyncEvalError();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const { errorCategory, errorMessage } = errorModifier.run(error as Error);
|
||||||
errors.push({
|
errors.push({
|
||||||
errorMessage: errorModifier.run(error as Error),
|
errorMessage,
|
||||||
severity: Severity.ERROR,
|
severity: Severity.ERROR,
|
||||||
raw: script,
|
raw: script,
|
||||||
errorType: PropertyEvaluationErrorType.PARSE,
|
errorType: PropertyEvaluationErrorType.PARSE,
|
||||||
originalBinding: userScript,
|
originalBinding: userScript,
|
||||||
|
kind: errorCategory && {
|
||||||
|
category: errorCategory,
|
||||||
|
rootcause: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
for (const entityName in evalContext) {
|
for (const entityName in evalContext) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
||||||
import type ReplayEntity from "entities/Replay";
|
import type ReplayEntity from "entities/Replay";
|
||||||
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
|
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
|
||||||
import { isEmpty } from "lodash";
|
import { isEmpty, union } from "lodash";
|
||||||
import type { DependencyMap, EvalError } from "utils/DynamicBindingUtils";
|
import type { DependencyMap, EvalError } from "utils/DynamicBindingUtils";
|
||||||
import { EvalErrorTypes } from "utils/DynamicBindingUtils";
|
import { EvalErrorTypes } from "utils/DynamicBindingUtils";
|
||||||
import type { JSUpdate } from "utils/JSPaneUtils";
|
import type { JSUpdate } from "utils/JSPaneUtils";
|
||||||
|
|
@ -20,6 +20,8 @@ import type {
|
||||||
EvalWorkerSyncRequest,
|
EvalWorkerSyncRequest,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { clearAllIntervals } from "../fns/overrides/interval";
|
import { clearAllIntervals } from "../fns/overrides/interval";
|
||||||
|
import { jsPropertiesState } from "../JSObject/jsPropertiesState";
|
||||||
|
import { asyncJsFunctionInDataFields } from "../JSObject/asyncJSFunctionBoundToDataField";
|
||||||
export let replayMap: Record<string, ReplayEntity<any>>;
|
export let replayMap: Record<string, ReplayEntity<any>>;
|
||||||
export let dataTreeEvaluator: DataTreeEvaluator | undefined;
|
export let dataTreeEvaluator: DataTreeEvaluator | undefined;
|
||||||
export const CANVAS = "canvas";
|
export const CANVAS = "canvas";
|
||||||
|
|
@ -44,6 +46,7 @@ export default function (request: EvalWorkerSyncRequest) {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
allActionValidationConfig,
|
allActionValidationConfig,
|
||||||
|
appMode,
|
||||||
forceEvaluation,
|
forceEvaluation,
|
||||||
metaWidgets,
|
metaWidgets,
|
||||||
requiresLinting,
|
requiresLinting,
|
||||||
|
|
@ -59,6 +62,7 @@ export default function (request: EvalWorkerSyncRequest) {
|
||||||
try {
|
try {
|
||||||
if (!dataTreeEvaluator) {
|
if (!dataTreeEvaluator) {
|
||||||
isCreateFirstTree = true;
|
isCreateFirstTree = true;
|
||||||
|
asyncJsFunctionInDataFields.initialize(appMode);
|
||||||
replayMap = replayMap || {};
|
replayMap = replayMap || {};
|
||||||
replayMap[CANVAS] = new ReplayCanvas({ widgets, theme });
|
replayMap[CANVAS] = new ReplayCanvas({ widgets, theme });
|
||||||
dataTreeEvaluator = new DataTreeEvaluator(
|
dataTreeEvaluator = new DataTreeEvaluator(
|
||||||
|
|
@ -71,17 +75,25 @@ export default function (request: EvalWorkerSyncRequest) {
|
||||||
configTree,
|
configTree,
|
||||||
);
|
);
|
||||||
evalOrder = setupFirstTreeResponse.evalOrder;
|
evalOrder = setupFirstTreeResponse.evalOrder;
|
||||||
lintOrder = setupFirstTreeResponse.lintOrder;
|
lintOrder = union(
|
||||||
|
setupFirstTreeResponse.lintOrder,
|
||||||
|
jsPropertiesState.getUpdatedJSProperties(),
|
||||||
|
);
|
||||||
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
||||||
|
|
||||||
initiateLinting(
|
initiateLinting({
|
||||||
lintOrder,
|
lintOrder,
|
||||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
unevalTree: makeEntityConfigsAsObjProperties(
|
||||||
sanitizeDataTree: false,
|
dataTreeEvaluator.oldUnEvalTree,
|
||||||
}),
|
{
|
||||||
|
sanitizeDataTree: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
requiresLinting,
|
requiresLinting,
|
||||||
dataTreeEvaluator.oldConfigTree,
|
jsPropertiesState: jsPropertiesState.getMap(),
|
||||||
);
|
asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
|
||||||
|
configTree: dataTreeEvaluator.oldConfigTree,
|
||||||
|
});
|
||||||
|
|
||||||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
||||||
|
|
@ -113,17 +125,25 @@ export default function (request: EvalWorkerSyncRequest) {
|
||||||
);
|
);
|
||||||
isCreateFirstTree = true;
|
isCreateFirstTree = true;
|
||||||
evalOrder = setupFirstTreeResponse.evalOrder;
|
evalOrder = setupFirstTreeResponse.evalOrder;
|
||||||
lintOrder = setupFirstTreeResponse.lintOrder;
|
lintOrder = union(
|
||||||
|
setupFirstTreeResponse.lintOrder,
|
||||||
|
jsPropertiesState.getUpdatedJSProperties(),
|
||||||
|
);
|
||||||
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
||||||
|
|
||||||
initiateLinting(
|
initiateLinting({
|
||||||
lintOrder,
|
lintOrder,
|
||||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
unevalTree: makeEntityConfigsAsObjProperties(
|
||||||
sanitizeDataTree: false,
|
dataTreeEvaluator.oldUnEvalTree,
|
||||||
}),
|
{
|
||||||
|
sanitizeDataTree: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
requiresLinting,
|
requiresLinting,
|
||||||
dataTreeEvaluator.oldConfigTree,
|
jsPropertiesState: jsPropertiesState.getMap(),
|
||||||
);
|
asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
|
||||||
|
configTree: dataTreeEvaluator.oldConfigTree,
|
||||||
|
});
|
||||||
|
|
||||||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
||||||
|
|
@ -146,20 +166,28 @@ export default function (request: EvalWorkerSyncRequest) {
|
||||||
);
|
);
|
||||||
|
|
||||||
evalOrder = setupUpdateTreeResponse.evalOrder;
|
evalOrder = setupUpdateTreeResponse.evalOrder;
|
||||||
lintOrder = setupUpdateTreeResponse.lintOrder;
|
lintOrder = union(
|
||||||
|
setupUpdateTreeResponse.lintOrder,
|
||||||
|
jsPropertiesState.getUpdatedJSProperties(),
|
||||||
|
);
|
||||||
jsUpdates = setupUpdateTreeResponse.jsUpdates;
|
jsUpdates = setupUpdateTreeResponse.jsUpdates;
|
||||||
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
|
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
|
||||||
pathsToClearErrorsFor = setupUpdateTreeResponse.pathsToClearErrorsFor;
|
pathsToClearErrorsFor = setupUpdateTreeResponse.pathsToClearErrorsFor;
|
||||||
isNewWidgetAdded = setupUpdateTreeResponse.isNewWidgetAdded;
|
isNewWidgetAdded = setupUpdateTreeResponse.isNewWidgetAdded;
|
||||||
|
|
||||||
initiateLinting(
|
initiateLinting({
|
||||||
lintOrder,
|
lintOrder,
|
||||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
unevalTree: makeEntityConfigsAsObjProperties(
|
||||||
sanitizeDataTree: false,
|
dataTreeEvaluator.oldUnEvalTree,
|
||||||
}),
|
{
|
||||||
|
sanitizeDataTree: false,
|
||||||
|
},
|
||||||
|
),
|
||||||
requiresLinting,
|
requiresLinting,
|
||||||
dataTreeEvaluator.oldConfigTree,
|
jsPropertiesState: jsPropertiesState.getMap(),
|
||||||
);
|
asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
|
||||||
|
configTree: dataTreeEvaluator.oldConfigTree,
|
||||||
|
});
|
||||||
nonDynamicFieldValidationOrder =
|
nonDynamicFieldValidationOrder =
|
||||||
setupUpdateTreeResponse.nonDynamicFieldValidationOrder;
|
setupUpdateTreeResponse.nonDynamicFieldValidationOrder;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import type { WidgetTypeConfigMap } from "utils/WidgetFactory";
|
||||||
import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator/types";
|
import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator/types";
|
||||||
import type { WorkerRequest } from "@appsmith/workers/common/types";
|
import type { WorkerRequest } from "@appsmith/workers/common/types";
|
||||||
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
|
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
import type { APP_MODE } from "entities/App";
|
||||||
|
|
||||||
export type EvalWorkerSyncRequest = WorkerRequest<any, EVAL_WORKER_SYNC_ACTION>;
|
export type EvalWorkerSyncRequest = WorkerRequest<any, EVAL_WORKER_SYNC_ACTION>;
|
||||||
export type EvalWorkerASyncRequest = WorkerRequest<
|
export type EvalWorkerASyncRequest = WorkerRequest<
|
||||||
|
|
@ -38,6 +39,7 @@ export interface EvalTreeRequestData {
|
||||||
requiresLinting: boolean;
|
requiresLinting: boolean;
|
||||||
forceEvaluation: boolean;
|
forceEvaluation: boolean;
|
||||||
metaWidgets: MetaWidgetsReduxState;
|
metaWidgets: MetaWidgetsReduxState;
|
||||||
|
appMode: APP_MODE | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EvalTreeResponseData {
|
export interface EvalTreeResponseData {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ECMA_VERSION } from "@shared/ast";
|
import { ECMA_VERSION } from "@shared/ast";
|
||||||
import type { LintOptions } from "jshint";
|
import type { LintOptions } from "jshint";
|
||||||
|
import { isEntityFunction } from "./utils";
|
||||||
|
|
||||||
export const lintOptions = (globalData: Record<string, boolean>) =>
|
export const lintOptions = (globalData: Record<string, boolean>) =>
|
||||||
({
|
({
|
||||||
|
|
@ -29,6 +30,8 @@ export const lintOptions = (globalData: Record<string, boolean>) =>
|
||||||
} as LintOptions);
|
} as LintOptions);
|
||||||
export const JS_OBJECT_START_STATEMENT = "export default";
|
export const JS_OBJECT_START_STATEMENT = "export default";
|
||||||
export const INVALID_JSOBJECT_START_STATEMENT = `JSObject must start with '${JS_OBJECT_START_STATEMENT}'`;
|
export const INVALID_JSOBJECT_START_STATEMENT = `JSObject must start with '${JS_OBJECT_START_STATEMENT}'`;
|
||||||
|
export const INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE =
|
||||||
|
"INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE";
|
||||||
// https://github.com/jshint/jshint/blob/d3d84ae1695359aef077ddb143f4be98001343b4/src/messages.js#L204
|
// https://github.com/jshint/jshint/blob/d3d84ae1695359aef077ddb143f4be98001343b4/src/messages.js#L204
|
||||||
export const IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE = "W117";
|
export const IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE = "W117";
|
||||||
|
|
||||||
|
|
@ -37,10 +40,14 @@ export const IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE = "W117";
|
||||||
export const WARNING_LINT_ERRORS = {
|
export const WARNING_LINT_ERRORS = {
|
||||||
W098: "'{a}' is defined but never used.",
|
W098: "'{a}' is defined but never used.",
|
||||||
W014: "Misleading line break before '{a}'; readers may interpret this as an expression boundary.",
|
W014: "Misleading line break before '{a}'; readers may interpret this as an expression boundary.",
|
||||||
|
ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD:
|
||||||
|
"Cannot execute async code on functions bound to data fields",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function asyncActionInSyncFieldLintMessage(actionName: string) {
|
export function asyncActionInSyncFieldLintMessage(isJsObject = false) {
|
||||||
return `Async framework action "${actionName}" cannot be executed in a function that is bound to a sync field.`;
|
return isJsObject
|
||||||
|
? `Cannot execute async code on functions bound to data fields`
|
||||||
|
: `Data fields cannot execute async code`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** These errors should be overlooked
|
/** These errors should be overlooked
|
||||||
|
|
@ -54,7 +61,9 @@ export const SUPPORTED_WEB_APIS = {
|
||||||
};
|
};
|
||||||
export enum CustomLintErrorCode {
|
export enum CustomLintErrorCode {
|
||||||
INVALID_ENTITY_PROPERTY = "INVALID_ENTITY_PROPERTY",
|
INVALID_ENTITY_PROPERTY = "INVALID_ENTITY_PROPERTY",
|
||||||
|
ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD = "ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CUSTOM_LINT_ERRORS: Record<
|
export const CUSTOM_LINT_ERRORS: Record<
|
||||||
CustomLintErrorCode,
|
CustomLintErrorCode,
|
||||||
(...args: any[]) => string
|
(...args: any[]) => string
|
||||||
|
|
@ -62,5 +71,26 @@ export const CUSTOM_LINT_ERRORS: Record<
|
||||||
[CustomLintErrorCode.INVALID_ENTITY_PROPERTY]: (
|
[CustomLintErrorCode.INVALID_ENTITY_PROPERTY]: (
|
||||||
entityName: string,
|
entityName: string,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
) => `"${propertyName}" doesn't exist in ${entityName}`,
|
entity: unknown,
|
||||||
|
isJsObject: boolean,
|
||||||
|
) =>
|
||||||
|
isEntityFunction(entity, propertyName)
|
||||||
|
? asyncActionInSyncFieldLintMessage(isJsObject)
|
||||||
|
: `"${propertyName}" doesn't exist in ${entityName}`,
|
||||||
|
|
||||||
|
[CustomLintErrorCode.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD]: (
|
||||||
|
dataFieldBindings: string[],
|
||||||
|
fullName: string,
|
||||||
|
isMarkedAsync: boolean,
|
||||||
|
) => {
|
||||||
|
const hasMultipleBindings = dataFieldBindings.length > 1;
|
||||||
|
const bindings = dataFieldBindings.join(" , ");
|
||||||
|
return isMarkedAsync
|
||||||
|
? `Cannot bind async functions to data fields. Convert this to a sync function or remove references to "${fullName}" on the following data ${
|
||||||
|
hasMultipleBindings ? "fields" : "field"
|
||||||
|
}: ${bindings}`
|
||||||
|
: `Functions bound to data fields cannot execute async code. Remove async statements highlighted below or remove references to "${fullName}" on the following data ${
|
||||||
|
hasMultipleBindings ? "fields" : "field"
|
||||||
|
}: ${bindings}`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
47
app/client/src/workers/Linting/globalData.ts
Normal file
47
app/client/src/workers/Linting/globalData.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||||
|
import { isEmpty } from "lodash";
|
||||||
|
import type { EvalContext } from "workers/Evaluation/evaluate";
|
||||||
|
import { getEvaluationContext } from "./utils";
|
||||||
|
|
||||||
|
class GlobalData {
|
||||||
|
globalDataWithFunctions: EvalContext = {};
|
||||||
|
globalDataWithoutFunctions: EvalContext = {};
|
||||||
|
unevalTree: DataTree = {};
|
||||||
|
cloudHosting = false;
|
||||||
|
|
||||||
|
initialize(unevalTree: DataTree, cloudHosting: boolean) {
|
||||||
|
this.globalDataWithFunctions = {};
|
||||||
|
this.globalDataWithoutFunctions = {};
|
||||||
|
this.unevalTree = unevalTree;
|
||||||
|
this.cloudHosting = cloudHosting;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGlobalData(withFunctions: boolean) {
|
||||||
|
// Our goal is to create global data (with or without functions) only once during a linting cycle
|
||||||
|
if (withFunctions) {
|
||||||
|
if (isEmpty(this.globalDataWithFunctions)) {
|
||||||
|
this.globalDataWithFunctions = getEvaluationContext(
|
||||||
|
this.unevalTree,
|
||||||
|
this.cloudHosting,
|
||||||
|
{
|
||||||
|
withFunctions: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.globalDataWithFunctions;
|
||||||
|
} else {
|
||||||
|
if (isEmpty(this.globalDataWithoutFunctions)) {
|
||||||
|
this.globalDataWithoutFunctions = getEvaluationContext(
|
||||||
|
this.unevalTree,
|
||||||
|
this.cloudHosting,
|
||||||
|
{
|
||||||
|
withFunctions: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.globalDataWithoutFunctions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const globalData = new GlobalData();
|
||||||
|
|
@ -1,126 +1,110 @@
|
||||||
|
import { getEntityNameAndPropertyPath } from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
import { get, isEmpty, set } from "lodash";
|
||||||
|
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||||
|
import type { LintError } from "utils/DynamicBindingUtils";
|
||||||
|
import { globalData } from "./globalData";
|
||||||
|
import type {
|
||||||
|
getlintErrorsFromTreeProps,
|
||||||
|
getlintErrorsFromTreeResponse,
|
||||||
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
getEntityNameAndPropertyPath,
|
lintBindingPath,
|
||||||
isATriggerPath,
|
lintJSObjectBody,
|
||||||
isJSAction,
|
lintJSObjectProperty,
|
||||||
} from "ce/workers/Evaluation/evaluationUtils";
|
lintTriggerPath,
|
||||||
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
sortLintingPathsByType,
|
||||||
import { get, set } from "lodash";
|
} from "./utils";
|
||||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
|
||||||
import { createEvaluationContext } from "workers/Evaluation/evaluate";
|
|
||||||
import { getActionTriggerFunctionNames } from "workers/Evaluation/fns";
|
|
||||||
import { lintBindingPath, lintTriggerPath, pathRequiresLinting } from "./utils";
|
|
||||||
|
|
||||||
export function getlintErrorsFromTree(
|
export function getlintErrorsFromTree({
|
||||||
pathsToLint: string[],
|
asyncJSFunctionsInDataFields,
|
||||||
unEvalTree: DataTree,
|
cloudHosting,
|
||||||
configTree: ConfigTree,
|
configTree,
|
||||||
cloudHosting: boolean,
|
jsPropertiesState,
|
||||||
): LintErrors {
|
pathsToLint,
|
||||||
const lintTreeErrors: LintErrors = {};
|
unEvalTree,
|
||||||
|
}: getlintErrorsFromTreeProps): getlintErrorsFromTreeResponse {
|
||||||
const evalContext = createEvaluationContext({
|
const lintTreeErrors: LintErrorsStore = {};
|
||||||
dataTree: unEvalTree,
|
const updatedJSEntities = new Set<string>();
|
||||||
resolvedFunctions: {},
|
globalData.initialize(unEvalTree, cloudHosting);
|
||||||
isTriggerBased: false,
|
const { bindingPaths, jsObjectPaths, triggerPaths } = sortLintingPathsByType(
|
||||||
skipEntityFunctions: true,
|
pathsToLint,
|
||||||
});
|
unEvalTree,
|
||||||
|
configTree,
|
||||||
const platformFnNamesMap = Object.values(
|
|
||||||
getActionTriggerFunctionNames(cloudHosting),
|
|
||||||
).reduce(
|
|
||||||
(acc, name) => ({ ...acc, [name]: true }),
|
|
||||||
{} as { [x: string]: boolean },
|
|
||||||
);
|
);
|
||||||
Object.assign(evalContext, platformFnNamesMap);
|
|
||||||
|
|
||||||
const evalContextWithoutFunctions = createEvaluationContext({
|
// Lint binding paths
|
||||||
dataTree: unEvalTree,
|
bindingPaths.forEach((bindingPath) => {
|
||||||
resolvedFunctions: {},
|
const { entityName } = getEntityNameAndPropertyPath(bindingPath);
|
||||||
isTriggerBased: true,
|
|
||||||
skipEntityFunctions: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// trigger paths
|
|
||||||
const triggerPaths = new Set<string>();
|
|
||||||
// Certain paths, like JS Object's body are binding paths where appsmith functions are needed in the global data
|
|
||||||
const bindingPathsRequiringFunctions = new Set<string>();
|
|
||||||
|
|
||||||
pathsToLint.forEach((fullPropertyPath) => {
|
|
||||||
const { entityName, propertyPath } =
|
|
||||||
getEntityNameAndPropertyPath(fullPropertyPath);
|
|
||||||
const entity = unEvalTree[entityName];
|
const entity = unEvalTree[entityName];
|
||||||
const entityConfig = configTree[entityName];
|
|
||||||
const unEvalPropertyValue = get(
|
const unEvalPropertyValue = get(
|
||||||
unEvalTree,
|
unEvalTree,
|
||||||
fullPropertyPath,
|
bindingPath,
|
||||||
) as unknown as string;
|
) as unknown as string;
|
||||||
// remove all lint errors from path
|
|
||||||
set(lintTreeErrors, `["${fullPropertyPath}"]`, []);
|
|
||||||
|
|
||||||
// We are only interested in paths that require linting
|
|
||||||
if (
|
|
||||||
!pathRequiresLinting(unEvalTree, entity, fullPropertyPath, entityConfig)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
if (isATriggerPath(entityConfig, propertyPath))
|
|
||||||
return triggerPaths.add(fullPropertyPath);
|
|
||||||
if (isJSAction(entity))
|
|
||||||
return bindingPathsRequiringFunctions.add(`${entityName}.body`);
|
|
||||||
const lintErrors = lintBindingPath({
|
const lintErrors = lintBindingPath({
|
||||||
entity,
|
|
||||||
fullPropertyPath,
|
|
||||||
globalData: evalContextWithoutFunctions,
|
|
||||||
dynamicBinding: unEvalPropertyValue,
|
dynamicBinding: unEvalPropertyValue,
|
||||||
|
entity,
|
||||||
|
fullPropertyPath: bindingPath,
|
||||||
|
globalData: globalData.getGlobalData(false),
|
||||||
});
|
});
|
||||||
set(lintTreeErrors, `["${fullPropertyPath}"]`, lintErrors);
|
set(lintTreeErrors, `["${bindingPath}"]`, lintErrors);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (triggerPaths.size || bindingPathsRequiringFunctions.size) {
|
// Lint TriggerPaths
|
||||||
// we only create GLOBAL_DATA_WITH_FUNCTIONS if there are paths requiring it
|
triggerPaths.forEach((triggerPath) => {
|
||||||
// In trigger based fields, functions such as showAlert, storeValue, etc need to be added to the global data
|
const { entityName } = getEntityNameAndPropertyPath(triggerPath);
|
||||||
|
const entity = unEvalTree[entityName];
|
||||||
|
const unEvalPropertyValue = get(
|
||||||
|
unEvalTree,
|
||||||
|
triggerPath,
|
||||||
|
) as unknown as string;
|
||||||
|
// remove all lint errors from path
|
||||||
|
set(lintTreeErrors, `["${triggerPath}"]`, []);
|
||||||
|
const lintErrors = lintTriggerPath({
|
||||||
|
userScript: unEvalPropertyValue,
|
||||||
|
entity,
|
||||||
|
globalData: globalData.getGlobalData(true),
|
||||||
|
});
|
||||||
|
set(lintTreeErrors, `["${triggerPath}"]`, lintErrors);
|
||||||
|
});
|
||||||
|
|
||||||
// lint binding paths that need GLOBAL_DATA_WITH_FUNCTIONS
|
// Lint jsobject paths
|
||||||
if (bindingPathsRequiringFunctions.size) {
|
if (jsObjectPaths.size) {
|
||||||
bindingPathsRequiringFunctions.forEach((fullPropertyPath) => {
|
jsObjectPaths.forEach((jsObjectPath) => {
|
||||||
const { entityName } = getEntityNameAndPropertyPath(fullPropertyPath);
|
const { entityName: jsObjectName } =
|
||||||
const entity = unEvalTree[entityName];
|
getEntityNameAndPropertyPath(jsObjectPath);
|
||||||
const unEvalPropertyValue = get(
|
const jsObjectState = get(jsPropertiesState, jsObjectName);
|
||||||
unEvalTree,
|
const jsObjectBodyPath = `["${jsObjectName}.body"]`;
|
||||||
fullPropertyPath,
|
updatedJSEntities.add(jsObjectName);
|
||||||
) as unknown as string;
|
// An empty state shows that there is a parse error in the jsObject or the object is empty, so we lint the entire body
|
||||||
// remove all lint errors from path
|
// instead of an individual properties
|
||||||
set(lintTreeErrors, `["${fullPropertyPath}"]`, []);
|
if (isEmpty(jsObjectState)) {
|
||||||
const lintErrors = lintBindingPath({
|
const jsObjectBodyLintErrors = lintJSObjectBody(
|
||||||
dynamicBinding: unEvalPropertyValue,
|
jsObjectName,
|
||||||
entity,
|
globalData.getGlobalData(true),
|
||||||
fullPropertyPath,
|
);
|
||||||
globalData: evalContext,
|
set(lintTreeErrors, jsObjectBodyPath, jsObjectBodyLintErrors);
|
||||||
});
|
} else if (jsObjectPath !== "body") {
|
||||||
set(lintTreeErrors, `["${fullPropertyPath}"]`, lintErrors);
|
const propertyLintErrors = lintJSObjectProperty(
|
||||||
});
|
jsObjectPath,
|
||||||
}
|
jsObjectState,
|
||||||
|
asyncJSFunctionsInDataFields,
|
||||||
// Lint triggerPaths
|
);
|
||||||
if (triggerPaths.size) {
|
const currentLintErrorsInBody = get(
|
||||||
triggerPaths.forEach((triggerPath) => {
|
lintTreeErrors,
|
||||||
const { entityName } = getEntityNameAndPropertyPath(triggerPath);
|
jsObjectBodyPath,
|
||||||
const entity = unEvalTree[entityName];
|
[] as LintError[],
|
||||||
const unEvalPropertyValue = get(
|
);
|
||||||
unEvalTree,
|
const updatedLintErrors = [
|
||||||
triggerPath,
|
...currentLintErrorsInBody,
|
||||||
) as unknown as string;
|
...propertyLintErrors,
|
||||||
// remove all lint errors from path
|
];
|
||||||
set(lintTreeErrors, `["${triggerPath}"]`, []);
|
set(lintTreeErrors, jsObjectBodyPath, updatedLintErrors);
|
||||||
const lintErrors = lintTriggerPath({
|
}
|
||||||
globalData: evalContext,
|
});
|
||||||
userScript: unEvalPropertyValue,
|
|
||||||
entity,
|
|
||||||
fullPropertyPath: triggerPath,
|
|
||||||
});
|
|
||||||
set(lintTreeErrors, `["${triggerPath}"]`, lintErrors);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return lintTreeErrors;
|
return {
|
||||||
|
errors: lintTreeErrors,
|
||||||
|
updatedJSEntities: Array.from(updatedJSEntities),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,18 +64,32 @@ function eventRequestHandler({
|
||||||
}): LintTreeResponse | unknown {
|
}): LintTreeResponse | unknown {
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case LINT_WORKER_ACTIONS.LINT_TREE: {
|
case LINT_WORKER_ACTIONS.LINT_TREE: {
|
||||||
const lintTreeResponse: LintTreeResponse = { errors: {} };
|
const lintTreeResponse: LintTreeResponse = {
|
||||||
|
errors: {},
|
||||||
|
updatedJSEntities: [],
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const { cloudHosting, configTree, pathsToLint, unevalTree } =
|
const {
|
||||||
requestData as LintTreeRequest;
|
asyncJSFunctionsInDataFields,
|
||||||
const lintErrors = getlintErrorsFromTree(
|
|
||||||
pathsToLint,
|
|
||||||
unevalTree,
|
|
||||||
configTree,
|
|
||||||
cloudHosting,
|
cloudHosting,
|
||||||
|
configTree,
|
||||||
|
jsPropertiesState,
|
||||||
|
pathsToLint,
|
||||||
|
unevalTree: unEvalTree,
|
||||||
|
} = requestData as LintTreeRequest;
|
||||||
|
const { errors: lintErrors, updatedJSEntities } = getlintErrorsFromTree(
|
||||||
|
{
|
||||||
|
pathsToLint,
|
||||||
|
unEvalTree,
|
||||||
|
jsPropertiesState,
|
||||||
|
cloudHosting,
|
||||||
|
asyncJSFunctionsInDataFields,
|
||||||
|
configTree,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
lintTreeResponse.errors = lintErrors;
|
lintTreeResponse.errors = lintErrors;
|
||||||
|
lintTreeResponse.updatedJSEntities = updatedJSEntities;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return lintTreeResponse;
|
return lintTreeResponse;
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +111,7 @@ function eventRequestHandler({
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error("Action not registered on lintWorker ", method);
|
console.error("Action not registered on lintWorker ", method);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,16 @@
|
||||||
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
import type {
|
||||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
ConfigTree,
|
||||||
|
DataTree,
|
||||||
|
DataTreeEntity,
|
||||||
|
} from "entities/DataTree/dataTreeFactory";
|
||||||
|
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||||
import type { WorkerRequest } from "@appsmith/workers/common/types";
|
import type { WorkerRequest } from "@appsmith/workers/common/types";
|
||||||
|
import type {
|
||||||
|
createEvaluationContext,
|
||||||
|
EvaluationScriptType,
|
||||||
|
} from "workers/Evaluation/evaluate";
|
||||||
|
import type { DependencyMap } from "utils/DynamicBindingUtils";
|
||||||
|
import type { TJSPropertiesState } from "workers/Evaluation/JSObject/jsPropertiesState";
|
||||||
|
|
||||||
export enum LINT_WORKER_ACTIONS {
|
export enum LINT_WORKER_ACTIONS {
|
||||||
LINT_TREE = "LINT_TREE",
|
LINT_TREE = "LINT_TREE",
|
||||||
|
|
@ -8,14 +18,17 @@ export enum LINT_WORKER_ACTIONS {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LintTreeResponse {
|
export interface LintTreeResponse {
|
||||||
errors: LintErrors;
|
errors: LintErrorsStore;
|
||||||
|
updatedJSEntities: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LintTreeRequest {
|
export interface LintTreeRequest {
|
||||||
pathsToLint: string[];
|
pathsToLint: string[];
|
||||||
unevalTree: DataTree;
|
unevalTree: DataTree;
|
||||||
|
jsPropertiesState: TJSPropertiesState;
|
||||||
configTree: ConfigTree;
|
configTree: ConfigTree;
|
||||||
cloudHosting: boolean;
|
cloudHosting: boolean;
|
||||||
|
asyncJSFunctionsInDataFields: DependencyMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LintWorkerRequest = WorkerRequest<
|
export type LintWorkerRequest = WorkerRequest<
|
||||||
|
|
@ -26,5 +39,54 @@ export type LintWorkerRequest = WorkerRequest<
|
||||||
export type LintTreeSagaRequestData = {
|
export type LintTreeSagaRequestData = {
|
||||||
pathsToLint: string[];
|
pathsToLint: string[];
|
||||||
unevalTree: DataTree;
|
unevalTree: DataTree;
|
||||||
|
jsPropertiesState: TJSPropertiesState;
|
||||||
|
asyncJSFunctionsInDataFields: DependencyMap;
|
||||||
configTree: ConfigTree;
|
configTree: ConfigTree;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface lintTriggerPathProps {
|
||||||
|
userScript: string;
|
||||||
|
entity: DataTreeEntity;
|
||||||
|
globalData: ReturnType<typeof createEvaluationContext>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface lintBindingPathProps {
|
||||||
|
dynamicBinding: string;
|
||||||
|
entity: DataTreeEntity;
|
||||||
|
fullPropertyPath: string;
|
||||||
|
globalData: ReturnType<typeof createEvaluationContext>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getLintingErrorsProps {
|
||||||
|
script: string;
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
// {{user's code}}
|
||||||
|
originalBinding: string;
|
||||||
|
scriptType: EvaluationScriptType;
|
||||||
|
options?: {
|
||||||
|
isJsObject: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getlintErrorsFromTreeProps {
|
||||||
|
pathsToLint: string[];
|
||||||
|
unEvalTree: DataTree;
|
||||||
|
jsPropertiesState: TJSPropertiesState;
|
||||||
|
cloudHosting: boolean;
|
||||||
|
asyncJSFunctionsInDataFields: DependencyMap;
|
||||||
|
configTree: ConfigTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getlintErrorsFromTreeResponse {
|
||||||
|
errors: LintErrorsStore;
|
||||||
|
updatedJSEntities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface initiateLintingProps {
|
||||||
|
asyncJSFunctionsInDataFields: DependencyMap;
|
||||||
|
lintOrder: string[];
|
||||||
|
unevalTree: DataTree;
|
||||||
|
requiresLinting: boolean;
|
||||||
|
jsPropertiesState: TJSPropertiesState;
|
||||||
|
configTree: ConfigTree;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,27 @@
|
||||||
import type {
|
import type {
|
||||||
ConfigTree,
|
|
||||||
DataTree,
|
DataTree,
|
||||||
DataTreeEntity,
|
DataTreeEntity,
|
||||||
DataTreeEntityConfig,
|
ConfigTree,
|
||||||
} from "entities/DataTree/dataTreeFactory";
|
} from "entities/DataTree/dataTreeFactory";
|
||||||
|
|
||||||
import type { Position } from "codemirror";
|
import type { Position } from "codemirror";
|
||||||
import type { LintError } from "utils/DynamicBindingUtils";
|
import type { LintError } from "utils/DynamicBindingUtils";
|
||||||
import {
|
import type { DependencyMap } from "utils/DynamicBindingUtils";
|
||||||
isDynamicValue,
|
|
||||||
isPathADynamicBinding,
|
|
||||||
PropertyEvaluationErrorType,
|
|
||||||
} from "utils/DynamicBindingUtils";
|
|
||||||
import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
|
import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
|
||||||
import type { LintError as JSHintError } from "jshint";
|
|
||||||
import { JSHINT as jshint } from "jshint";
|
import { JSHINT as jshint } from "jshint";
|
||||||
import { get, isEmpty, isNumber, keys, last } from "lodash";
|
import type { LintError as JSHintError } from "jshint";
|
||||||
|
import { isEmpty, isNil, isNumber, keys, last } from "lodash";
|
||||||
import type { MemberExpressionData } from "@shared/ast";
|
import type { MemberExpressionData } from "@shared/ast";
|
||||||
import {
|
import {
|
||||||
extractInvalidTopLevelMemberExpressionsFromCode,
|
extractInvalidTopLevelMemberExpressionsFromCode,
|
||||||
isLiteralNode,
|
isLiteralNode,
|
||||||
} from "@shared/ast";
|
} from "@shared/ast";
|
||||||
import { getDynamicBindings } from "utils/DynamicBindingUtils";
|
|
||||||
|
|
||||||
import type { createEvaluationContext } from "workers/Evaluation/evaluate";
|
|
||||||
import {
|
import {
|
||||||
|
getDynamicBindings,
|
||||||
|
PropertyEvaluationErrorType,
|
||||||
|
} from "utils/DynamicBindingUtils";
|
||||||
|
import {
|
||||||
|
createEvaluationContext,
|
||||||
EvaluationScripts,
|
EvaluationScripts,
|
||||||
EvaluationScriptType,
|
EvaluationScriptType,
|
||||||
getScriptToEval,
|
getScriptToEval,
|
||||||
|
|
@ -33,13 +30,11 @@ import {
|
||||||
} from "workers/Evaluation/evaluate";
|
} from "workers/Evaluation/evaluate";
|
||||||
import {
|
import {
|
||||||
getEntityNameAndPropertyPath,
|
getEntityNameAndPropertyPath,
|
||||||
isAction,
|
|
||||||
isATriggerPath,
|
isATriggerPath,
|
||||||
|
isDataTreeEntity,
|
||||||
|
isDynamicLeaf,
|
||||||
isJSAction,
|
isJSAction,
|
||||||
isWidget,
|
|
||||||
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
import { Severity } from "entities/AppsmithConsole";
|
|
||||||
import { JSLibraries } from "workers/common/JSLibrary";
|
|
||||||
import { WorkerMessenger } from "workers/Evaluation/fns/utils/Messenger";
|
import { WorkerMessenger } from "workers/Evaluation/fns/utils/Messenger";
|
||||||
import {
|
import {
|
||||||
asyncActionInSyncFieldLintMessage,
|
asyncActionInSyncFieldLintMessage,
|
||||||
|
|
@ -48,19 +43,33 @@ import {
|
||||||
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
|
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
|
||||||
IGNORED_LINT_ERRORS,
|
IGNORED_LINT_ERRORS,
|
||||||
INVALID_JSOBJECT_START_STATEMENT,
|
INVALID_JSOBJECT_START_STATEMENT,
|
||||||
|
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
|
||||||
JS_OBJECT_START_STATEMENT,
|
JS_OBJECT_START_STATEMENT,
|
||||||
lintOptions,
|
lintOptions,
|
||||||
SUPPORTED_WEB_APIS,
|
SUPPORTED_WEB_APIS,
|
||||||
WARNING_LINT_ERRORS,
|
WARNING_LINT_ERRORS,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { APPSMITH_GLOBAL_FUNCTIONS } from "components/editorComponents/ActionCreator/constants";
|
import { APPSMITH_GLOBAL_FUNCTIONS } from "components/editorComponents/ActionCreator/constants";
|
||||||
|
import type {
|
||||||
|
getLintingErrorsProps,
|
||||||
|
initiateLintingProps,
|
||||||
|
lintBindingPathProps,
|
||||||
|
LintTreeSagaRequestData,
|
||||||
|
lintTriggerPathProps,
|
||||||
|
} from "./types";
|
||||||
|
import { JSLibraries } from "workers/common/JSLibrary";
|
||||||
|
import { Severity } from "entities/AppsmithConsole";
|
||||||
|
import {
|
||||||
|
entityFns,
|
||||||
|
getActionTriggerFunctionNames,
|
||||||
|
} from "workers/Evaluation/fns";
|
||||||
|
import type {
|
||||||
|
TJSFunctionPropertyState,
|
||||||
|
TJSpropertyState,
|
||||||
|
} from "workers/Evaluation/JSObject/jsPropertiesState";
|
||||||
|
import type { JSActionEntity } from "entities/DataTree/types";
|
||||||
|
import { globalData } from "./globalData";
|
||||||
|
|
||||||
interface lintBindingPathProps {
|
|
||||||
dynamicBinding: string;
|
|
||||||
entity: DataTreeEntity;
|
|
||||||
fullPropertyPath: string;
|
|
||||||
globalData: ReturnType<typeof createEvaluationContext>;
|
|
||||||
}
|
|
||||||
export function lintBindingPath({
|
export function lintBindingPath({
|
||||||
dynamicBinding,
|
dynamicBinding,
|
||||||
entity,
|
entity,
|
||||||
|
|
@ -68,30 +77,6 @@ export function lintBindingPath({
|
||||||
globalData,
|
globalData,
|
||||||
}: lintBindingPathProps) {
|
}: lintBindingPathProps) {
|
||||||
let lintErrors: LintError[] = [];
|
let lintErrors: LintError[] = [];
|
||||||
|
|
||||||
if (isJSAction(entity)) {
|
|
||||||
if (!entity.body) return lintErrors;
|
|
||||||
if (!entity.body.startsWith(JS_OBJECT_START_STATEMENT)) {
|
|
||||||
return lintErrors.concat([
|
|
||||||
{
|
|
||||||
errorType: PropertyEvaluationErrorType.LINT,
|
|
||||||
errorSegment: "",
|
|
||||||
originalBinding: entity.body,
|
|
||||||
line: 0,
|
|
||||||
ch: 0,
|
|
||||||
code: entity.body,
|
|
||||||
variables: [],
|
|
||||||
raw: entity.body,
|
|
||||||
errorMessage: {
|
|
||||||
name: "LintingError",
|
|
||||||
message: INVALID_JSOBJECT_START_STATEMENT,
|
|
||||||
},
|
|
||||||
severity: Severity.ERROR,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath);
|
const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath);
|
||||||
// Get the {{binding}} bound values
|
// Get the {{binding}} bound values
|
||||||
const { jsSnippets, stringSegments } = getDynamicBindings(
|
const { jsSnippets, stringSegments } = getDynamicBindings(
|
||||||
|
|
@ -116,8 +101,6 @@ export function lintBindingPath({
|
||||||
data: globalData,
|
data: globalData,
|
||||||
originalBinding,
|
originalBinding,
|
||||||
scriptType,
|
scriptType,
|
||||||
entity,
|
|
||||||
fullPropertyPath,
|
|
||||||
});
|
});
|
||||||
lintErrors = lintErrors.concat(lintErrorsFromSnippet);
|
lintErrors = lintErrors.concat(lintErrorsFromSnippet);
|
||||||
}
|
}
|
||||||
|
|
@ -125,15 +108,9 @@ export function lintBindingPath({
|
||||||
}
|
}
|
||||||
return lintErrors;
|
return lintErrors;
|
||||||
}
|
}
|
||||||
interface lintTriggerPathProps {
|
|
||||||
userScript: string;
|
|
||||||
entity: DataTreeEntity;
|
|
||||||
globalData: ReturnType<typeof createEvaluationContext>;
|
|
||||||
fullPropertyPath: string;
|
|
||||||
}
|
|
||||||
export function lintTriggerPath({
|
export function lintTriggerPath({
|
||||||
entity,
|
entity,
|
||||||
fullPropertyPath,
|
|
||||||
globalData,
|
globalData,
|
||||||
userScript,
|
userScript,
|
||||||
}: lintTriggerPathProps) {
|
}: lintTriggerPathProps) {
|
||||||
|
|
@ -145,35 +122,9 @@ export function lintTriggerPath({
|
||||||
data: globalData,
|
data: globalData,
|
||||||
originalBinding: jsSnippets[0],
|
originalBinding: jsSnippets[0],
|
||||||
scriptType: EvaluationScriptType.TRIGGERS,
|
scriptType: EvaluationScriptType.TRIGGERS,
|
||||||
entity,
|
|
||||||
fullPropertyPath,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pathRequiresLinting(
|
|
||||||
dataTree: DataTree,
|
|
||||||
entity: DataTreeEntity,
|
|
||||||
fullPropertyPath: string,
|
|
||||||
entityConfig: DataTreeEntityConfig,
|
|
||||||
): boolean {
|
|
||||||
const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath);
|
|
||||||
const unEvalPropertyValue = get(
|
|
||||||
dataTree,
|
|
||||||
fullPropertyPath,
|
|
||||||
) as unknown as string;
|
|
||||||
|
|
||||||
if (isATriggerPath(entityConfig, propertyPath)) {
|
|
||||||
return isDynamicValue(unEvalPropertyValue);
|
|
||||||
}
|
|
||||||
const isADynamicBindingPath =
|
|
||||||
(isAction(entity) || isWidget(entity) || isJSAction(entity)) &&
|
|
||||||
isPathADynamicBinding(entityConfig, propertyPath);
|
|
||||||
const requiresLinting =
|
|
||||||
(isADynamicBindingPath && isDynamicValue(unEvalPropertyValue)) ||
|
|
||||||
isJSAction(entity);
|
|
||||||
return requiresLinting;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes "export default" statement from js Object
|
// Removes "export default" statement from js Object
|
||||||
export function getJSToLint(
|
export function getJSToLint(
|
||||||
entity: DataTreeEntity,
|
entity: DataTreeEntity,
|
||||||
|
|
@ -278,19 +229,26 @@ function sanitizeJSHintErrors(
|
||||||
return result;
|
return result;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
const getLintSeverity = (code: string): Severity.WARNING | Severity.ERROR => {
|
const getLintSeverity = (
|
||||||
|
code: string,
|
||||||
|
errorMessage: string,
|
||||||
|
): Severity.WARNING | Severity.ERROR => {
|
||||||
const severity =
|
const severity =
|
||||||
code in WARNING_LINT_ERRORS ? Severity.WARNING : Severity.ERROR;
|
code in WARNING_LINT_ERRORS ||
|
||||||
|
errorMessage === asyncActionInSyncFieldLintMessage(true)
|
||||||
|
? Severity.WARNING
|
||||||
|
: Severity.ERROR;
|
||||||
return severity;
|
return severity;
|
||||||
};
|
};
|
||||||
const getLintErrorMessage = (
|
const getLintErrorMessage = (
|
||||||
reason: string,
|
reason: string,
|
||||||
code: string,
|
code: string,
|
||||||
variables: string[],
|
variables: string[],
|
||||||
|
isJSObject = false,
|
||||||
): string => {
|
): string => {
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE: {
|
case IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE: {
|
||||||
return getRefinedW117Error(variables[0], reason);
|
return getRefinedW117Error(variables[0], reason, isJSObject);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return reason;
|
return reason;
|
||||||
|
|
@ -302,6 +260,7 @@ function convertJsHintErrorToAppsmithLintError(
|
||||||
script: string,
|
script: string,
|
||||||
originalBinding: string,
|
originalBinding: string,
|
||||||
scriptPos: Position,
|
scriptPos: Position,
|
||||||
|
isJSObject = false,
|
||||||
): LintError {
|
): LintError {
|
||||||
const { a, b, c, code, d, evidence, reason } = jsHintError;
|
const { a, b, c, code, d, evidence, reason } = jsHintError;
|
||||||
|
|
||||||
|
|
@ -311,14 +270,20 @@ function convertJsHintErrorToAppsmithLintError(
|
||||||
jsHintError.line === scriptPos.line
|
jsHintError.line === scriptPos.line
|
||||||
? jsHintError.character - scriptPos.ch
|
? jsHintError.character - scriptPos.ch
|
||||||
: jsHintError.character;
|
: jsHintError.character;
|
||||||
|
const lintErrorMessage = getLintErrorMessage(
|
||||||
|
reason,
|
||||||
|
code,
|
||||||
|
[a, b, c, d],
|
||||||
|
isJSObject,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
errorType: PropertyEvaluationErrorType.LINT,
|
errorType: PropertyEvaluationErrorType.LINT,
|
||||||
raw: script,
|
raw: script,
|
||||||
severity: getLintSeverity(code),
|
severity: getLintSeverity(code, lintErrorMessage),
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
name: "LintingError",
|
name: "LintingError",
|
||||||
message: getLintErrorMessage(reason, code, [a, b, c, d]),
|
message: lintErrorMessage,
|
||||||
},
|
},
|
||||||
errorSegment: evidence,
|
errorSegment: evidence,
|
||||||
originalBinding,
|
originalBinding,
|
||||||
|
|
@ -329,17 +294,10 @@ function convertJsHintErrorToAppsmithLintError(
|
||||||
ch: actualErrorCh,
|
ch: actualErrorCh,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
interface getLintingErrorsProps {
|
|
||||||
script: string;
|
|
||||||
data: Record<string, unknown>;
|
|
||||||
// {{user's code}}
|
|
||||||
originalBinding: string;
|
|
||||||
scriptType: EvaluationScriptType;
|
|
||||||
entity: DataTreeEntity;
|
|
||||||
fullPropertyPath: string;
|
|
||||||
}
|
|
||||||
export function getLintingErrors({
|
export function getLintingErrors({
|
||||||
data,
|
data,
|
||||||
|
options,
|
||||||
originalBinding,
|
originalBinding,
|
||||||
script,
|
script,
|
||||||
scriptType,
|
scriptType,
|
||||||
|
|
@ -356,6 +314,7 @@ export function getLintingErrors({
|
||||||
script,
|
script,
|
||||||
originalBinding,
|
originalBinding,
|
||||||
scriptPos,
|
scriptPos,
|
||||||
|
options?.isJsObject,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const invalidPropertyErrors = getInvalidPropertyErrorsFromScript(
|
const invalidPropertyErrors = getInvalidPropertyErrorsFromScript(
|
||||||
|
|
@ -363,6 +322,7 @@ export function getLintingErrors({
|
||||||
data,
|
data,
|
||||||
scriptPos,
|
scriptPos,
|
||||||
originalBinding,
|
originalBinding,
|
||||||
|
options?.isJsObject,
|
||||||
);
|
);
|
||||||
return jshintErrors.concat(invalidPropertyErrors);
|
return jshintErrors.concat(invalidPropertyErrors);
|
||||||
}
|
}
|
||||||
|
|
@ -373,6 +333,7 @@ function getInvalidPropertyErrorsFromScript(
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown>,
|
||||||
scriptPos: Position,
|
scriptPos: Position,
|
||||||
originalBinding: string,
|
originalBinding: string,
|
||||||
|
isJSObject = false,
|
||||||
): LintError[] {
|
): LintError[] {
|
||||||
let invalidTopLevelMemberExpressions: MemberExpressionData[] = [];
|
let invalidTopLevelMemberExpressions: MemberExpressionData[] = [];
|
||||||
try {
|
try {
|
||||||
|
|
@ -394,15 +355,19 @@ function getInvalidPropertyErrorsFromScript(
|
||||||
const propertyStartColumn = !isLiteralNode(property)
|
const propertyStartColumn = !isLiteralNode(property)
|
||||||
? property.loc.start.column + 1
|
? property.loc.start.column + 1
|
||||||
: property.loc.start.column + 2;
|
: property.loc.start.column + 2;
|
||||||
|
const lintErrorMessage = CUSTOM_LINT_ERRORS[
|
||||||
|
CustomLintErrorCode.INVALID_ENTITY_PROPERTY
|
||||||
|
](object.name, propertyName, data[object.name], isJSObject);
|
||||||
return {
|
return {
|
||||||
errorType: PropertyEvaluationErrorType.LINT,
|
errorType: PropertyEvaluationErrorType.LINT,
|
||||||
raw: script,
|
raw: script,
|
||||||
severity: getLintSeverity(CustomLintErrorCode.INVALID_ENTITY_PROPERTY),
|
severity: getLintSeverity(
|
||||||
|
CustomLintErrorCode.INVALID_ENTITY_PROPERTY,
|
||||||
|
lintErrorMessage,
|
||||||
|
),
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
name: "LintingError",
|
name: "LintingError",
|
||||||
message: CUSTOM_LINT_ERRORS[
|
message: lintErrorMessage,
|
||||||
CustomLintErrorCode.INVALID_ENTITY_PROPERTY
|
|
||||||
](object.name, propertyName),
|
|
||||||
},
|
},
|
||||||
errorSegment: `${object.name}.${propertyName}`,
|
errorSegment: `${object.name}.${propertyName}`,
|
||||||
originalBinding,
|
originalBinding,
|
||||||
|
|
@ -419,19 +384,24 @@ function getInvalidPropertyErrorsFromScript(
|
||||||
return invalidPropertyErrors;
|
return invalidPropertyErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initiateLinting(
|
export function initiateLinting({
|
||||||
lintOrder: string[],
|
asyncJSFunctionsInDataFields,
|
||||||
unevalTree: DataTree,
|
configTree,
|
||||||
requiresLinting: boolean,
|
jsPropertiesState,
|
||||||
configTree: ConfigTree,
|
lintOrder,
|
||||||
) {
|
requiresLinting,
|
||||||
|
unevalTree,
|
||||||
|
}: initiateLintingProps) {
|
||||||
|
const data = {
|
||||||
|
pathsToLint: lintOrder,
|
||||||
|
unevalTree,
|
||||||
|
jsPropertiesState,
|
||||||
|
asyncJSFunctionsInDataFields,
|
||||||
|
configTree,
|
||||||
|
} as LintTreeSagaRequestData;
|
||||||
if (!requiresLinting) return;
|
if (!requiresLinting) return;
|
||||||
WorkerMessenger.ping({
|
WorkerMessenger.ping({
|
||||||
data: {
|
data,
|
||||||
lintOrder,
|
|
||||||
unevalTree,
|
|
||||||
configTree,
|
|
||||||
},
|
|
||||||
method: MAIN_THREAD_ACTION.LINT_TREE,
|
method: MAIN_THREAD_ACTION.LINT_TREE,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -439,14 +409,227 @@ export function initiateLinting(
|
||||||
export function getRefinedW117Error(
|
export function getRefinedW117Error(
|
||||||
undefinedVar: string,
|
undefinedVar: string,
|
||||||
originalReason: string,
|
originalReason: string,
|
||||||
|
isJsObject = false,
|
||||||
) {
|
) {
|
||||||
// Refine error message for await using in field not marked as async
|
// Refine error message for await using in field not marked as async
|
||||||
if (undefinedVar === "await") {
|
if (undefinedVar === "await") {
|
||||||
return "'await' expressions are only allowed within async functions. Did you mean to mark this function as 'async'?";
|
return "'await' expressions are only allowed within async functions. Did you mean to mark this function as 'async'?";
|
||||||
}
|
}
|
||||||
// Handle case where platform functions are used in sync fields
|
// Handle case where platform functions are used in data fields
|
||||||
if (APPSMITH_GLOBAL_FUNCTIONS.hasOwnProperty(undefinedVar)) {
|
if (APPSMITH_GLOBAL_FUNCTIONS.hasOwnProperty(undefinedVar)) {
|
||||||
return asyncActionInSyncFieldLintMessage(undefinedVar);
|
return asyncActionInSyncFieldLintMessage(isJsObject);
|
||||||
}
|
}
|
||||||
return originalReason;
|
return originalReason;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function lintJSProperty(
|
||||||
|
jsPropertyFullName: string,
|
||||||
|
jsPropertyState: TJSpropertyState,
|
||||||
|
globalData: DataTree,
|
||||||
|
): LintError[] {
|
||||||
|
if (isNil(jsPropertyState)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const { propertyPath: jsPropertyPath } =
|
||||||
|
getEntityNameAndPropertyPath(jsPropertyFullName);
|
||||||
|
const scriptType = getScriptType(false, false);
|
||||||
|
const scriptToLint = getScriptToEval(
|
||||||
|
jsPropertyState.value,
|
||||||
|
EvaluationScriptType.OBJECT_PROPERTY,
|
||||||
|
);
|
||||||
|
const propLintErrors = getLintingErrors({
|
||||||
|
script: scriptToLint,
|
||||||
|
data: globalData,
|
||||||
|
originalBinding: jsPropertyState.value,
|
||||||
|
scriptType,
|
||||||
|
options: { isJsObject: true },
|
||||||
|
});
|
||||||
|
const refinedErrors = propLintErrors.map((lintError) => {
|
||||||
|
return {
|
||||||
|
...lintError,
|
||||||
|
line: lintError.line + jsPropertyState.position.startLine - 1,
|
||||||
|
ch:
|
||||||
|
lintError.line === 0
|
||||||
|
? lintError.ch + jsPropertyState.position.startColumn
|
||||||
|
: lintError.ch,
|
||||||
|
originalPath: jsPropertyPath,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return refinedErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lintJSObjectProperty(
|
||||||
|
jsPropertyFullName: string,
|
||||||
|
jsObjectState: Record<string, TJSpropertyState>,
|
||||||
|
asyncJSFunctionsInDataFields: DependencyMap,
|
||||||
|
) {
|
||||||
|
let lintErrors: LintError[] = [];
|
||||||
|
const { propertyPath: jsPropertyName } =
|
||||||
|
getEntityNameAndPropertyPath(jsPropertyFullName);
|
||||||
|
const jsPropertyState = jsObjectState[jsPropertyName];
|
||||||
|
const isAsyncJSFunctionBoundToSyncField =
|
||||||
|
asyncJSFunctionsInDataFields.hasOwnProperty(jsPropertyFullName);
|
||||||
|
|
||||||
|
const jsPropertyLintErrors = lintJSProperty(
|
||||||
|
jsPropertyFullName,
|
||||||
|
jsPropertyState,
|
||||||
|
globalData.getGlobalData(!isAsyncJSFunctionBoundToSyncField),
|
||||||
|
);
|
||||||
|
lintErrors = lintErrors.concat(jsPropertyLintErrors);
|
||||||
|
|
||||||
|
// if function is async, and bound to a data field, then add custom lint error
|
||||||
|
if (isAsyncJSFunctionBoundToSyncField) {
|
||||||
|
lintErrors.push(
|
||||||
|
generateAsyncFunctionBoundToDataFieldCustomError(
|
||||||
|
asyncJSFunctionsInDataFields[jsPropertyFullName],
|
||||||
|
jsPropertyState,
|
||||||
|
jsPropertyFullName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lintErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lintJSObjectBody(
|
||||||
|
jsObjectName: string,
|
||||||
|
globalData: DataTree,
|
||||||
|
): LintError[] {
|
||||||
|
const jsObject = globalData[jsObjectName];
|
||||||
|
const rawJSObjectbody = (jsObject as unknown as JSActionEntity).body;
|
||||||
|
if (!rawJSObjectbody) return [];
|
||||||
|
if (!rawJSObjectbody.startsWith(JS_OBJECT_START_STATEMENT)) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
errorType: PropertyEvaluationErrorType.LINT,
|
||||||
|
errorSegment: "",
|
||||||
|
originalBinding: rawJSObjectbody,
|
||||||
|
line: 0,
|
||||||
|
ch: 0,
|
||||||
|
code: INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
|
||||||
|
variables: [],
|
||||||
|
raw: rawJSObjectbody,
|
||||||
|
errorMessage: {
|
||||||
|
name: "LintingError",
|
||||||
|
message: INVALID_JSOBJECT_START_STATEMENT,
|
||||||
|
},
|
||||||
|
severity: Severity.ERROR,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const scriptType = getScriptType(false, false);
|
||||||
|
const jsbodyToLint = getJSToLint(jsObject, rawJSObjectbody, "body"); // remove "export default"
|
||||||
|
const scriptToLint = getScriptToEval(jsbodyToLint, scriptType);
|
||||||
|
return getLintingErrors({
|
||||||
|
script: scriptToLint,
|
||||||
|
data: globalData,
|
||||||
|
originalBinding: jsbodyToLint,
|
||||||
|
scriptType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEvaluationContext(
|
||||||
|
unevalTree: DataTree,
|
||||||
|
cloudHosting: boolean,
|
||||||
|
options: { withFunctions: boolean },
|
||||||
|
) {
|
||||||
|
if (!options.withFunctions)
|
||||||
|
return createEvaluationContext({
|
||||||
|
dataTree: unevalTree,
|
||||||
|
resolvedFunctions: {},
|
||||||
|
isTriggerBased: false,
|
||||||
|
removeEntityFunctions: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const evalContext = createEvaluationContext({
|
||||||
|
dataTree: unevalTree,
|
||||||
|
resolvedFunctions: {},
|
||||||
|
isTriggerBased: false,
|
||||||
|
removeEntityFunctions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const platformFnNamesMap = Object.values(
|
||||||
|
getActionTriggerFunctionNames(cloudHosting),
|
||||||
|
).reduce(
|
||||||
|
(acc, name) => ({ ...acc, [name]: true }),
|
||||||
|
{} as { [x: string]: boolean },
|
||||||
|
);
|
||||||
|
Object.assign(evalContext, platformFnNamesMap);
|
||||||
|
|
||||||
|
return evalContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortLintingPathsByType(
|
||||||
|
pathsToLint: string[],
|
||||||
|
unevalTree: DataTree,
|
||||||
|
configTree: ConfigTree,
|
||||||
|
) {
|
||||||
|
const triggerPaths = new Set<string>();
|
||||||
|
const bindingPaths = new Set<string>();
|
||||||
|
const jsObjectPaths = new Set<string>();
|
||||||
|
|
||||||
|
for (const fullPropertyPath of pathsToLint) {
|
||||||
|
const { entityName, propertyPath } =
|
||||||
|
getEntityNameAndPropertyPath(fullPropertyPath);
|
||||||
|
const entity = unevalTree[entityName];
|
||||||
|
const entityConfig = configTree[entityName];
|
||||||
|
|
||||||
|
// We are only interested in dynamic leaves
|
||||||
|
if (!isDynamicLeaf(unevalTree, fullPropertyPath, configTree)) continue;
|
||||||
|
if (isATriggerPath(entityConfig, propertyPath)) {
|
||||||
|
triggerPaths.add(fullPropertyPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isJSAction(entity)) {
|
||||||
|
jsObjectPaths.add(fullPropertyPath);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bindingPaths.add(fullPropertyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { triggerPaths, bindingPaths, jsObjectPaths };
|
||||||
|
}
|
||||||
|
function generateAsyncFunctionBoundToDataFieldCustomError(
|
||||||
|
dataFieldBindings: string[],
|
||||||
|
jsPropertyState: TJSpropertyState,
|
||||||
|
jsPropertyFullName: string,
|
||||||
|
): LintError {
|
||||||
|
const { propertyPath: jsPropertyName } =
|
||||||
|
getEntityNameAndPropertyPath(jsPropertyFullName);
|
||||||
|
const lintErrorMessage =
|
||||||
|
CUSTOM_LINT_ERRORS.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD(
|
||||||
|
dataFieldBindings,
|
||||||
|
jsPropertyFullName,
|
||||||
|
(jsPropertyState as TJSFunctionPropertyState).isMarkedAsync,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
errorType: PropertyEvaluationErrorType.LINT,
|
||||||
|
raw: jsPropertyState.value,
|
||||||
|
severity: getLintSeverity(
|
||||||
|
CustomLintErrorCode.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD,
|
||||||
|
lintErrorMessage,
|
||||||
|
),
|
||||||
|
errorMessage: {
|
||||||
|
name: "LintingError",
|
||||||
|
message: lintErrorMessage,
|
||||||
|
},
|
||||||
|
errorSegment: jsPropertyFullName,
|
||||||
|
originalBinding: jsPropertyState.value,
|
||||||
|
// By keeping track of these variables we can highlight the exact text that caused the error.
|
||||||
|
variables: [jsPropertyName, null, null, null],
|
||||||
|
code: CustomLintErrorCode.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD,
|
||||||
|
line: jsPropertyState.position.keyStartLine - 1,
|
||||||
|
ch: jsPropertyState.position.keyStartColumn + 1,
|
||||||
|
originalPath: jsPropertyName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEntityFunction(entity: unknown, propertyName: string) {
|
||||||
|
if (!isDataTreeEntity(entity)) return false;
|
||||||
|
return entityFns.find(
|
||||||
|
(entityFn) =>
|
||||||
|
entityFn.name === propertyName &&
|
||||||
|
entityFn.qualifier(entity as DataTreeEntity),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,10 @@ import {
|
||||||
getUpdatedLocalUnEvalTreeAfterJSUpdates,
|
getUpdatedLocalUnEvalTreeAfterJSUpdates,
|
||||||
parseJSActions,
|
parseJSActions,
|
||||||
} from "workers/Evaluation/JSObject";
|
} from "workers/Evaluation/JSObject";
|
||||||
import { getFixedTimeDifference } from "./utils";
|
import {
|
||||||
|
addRootcauseToAsyncInvocationErrors,
|
||||||
|
getFixedTimeDifference,
|
||||||
|
} from "./utils";
|
||||||
import { isJSObjectFunction } from "workers/Evaluation/JSObject/utils";
|
import { isJSObjectFunction } from "workers/Evaluation/JSObject/utils";
|
||||||
import {
|
import {
|
||||||
getValidatedTree,
|
getValidatedTree,
|
||||||
|
|
@ -1063,7 +1066,7 @@ export default class DataTreeEvaluator {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = this.evaluateDynamicBoundValue(
|
const { errors: evalErrors, result } = this.evaluateDynamicBoundValue(
|
||||||
toBeSentForEval,
|
toBeSentForEval,
|
||||||
data,
|
data,
|
||||||
resolvedFunctions,
|
resolvedFunctions,
|
||||||
|
|
@ -1071,16 +1074,20 @@ export default class DataTreeEvaluator {
|
||||||
contextData,
|
contextData,
|
||||||
callBackData,
|
callBackData,
|
||||||
);
|
);
|
||||||
if (fullPropertyPath && result.errors.length) {
|
if (fullPropertyPath && evalErrors.length) {
|
||||||
addErrorToEntityProperty({
|
addErrorToEntityProperty({
|
||||||
errors: result.errors,
|
errors: addRootcauseToAsyncInvocationErrors(
|
||||||
|
fullPropertyPath,
|
||||||
|
configTree,
|
||||||
|
evalErrors,
|
||||||
|
),
|
||||||
evalProps: this.evalProps,
|
evalProps: this.evalProps,
|
||||||
fullPropertyPath,
|
fullPropertyPath,
|
||||||
dataTree: data,
|
dataTree: data,
|
||||||
configTree,
|
configTree,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result.result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
return stringSegments[index];
|
return stringSegments[index];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,55 @@
|
||||||
|
import {
|
||||||
|
getEntityNameAndPropertyPath,
|
||||||
|
isAction,
|
||||||
|
isJSAction,
|
||||||
|
isWidget,
|
||||||
|
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
import type {
|
||||||
|
ConfigTree,
|
||||||
|
DataTreeEntity,
|
||||||
|
WidgetEntity,
|
||||||
|
} from "entities/DataTree/dataTreeFactory";
|
||||||
|
import type { ActionEntity, JSActionEntity } from "entities/DataTree/types";
|
||||||
|
import type { EvaluationError } from "utils/DynamicBindingUtils";
|
||||||
|
import { errorModifier } from "workers/Evaluation/errorModifier";
|
||||||
|
import { asyncJsFunctionInDataFields } from "workers/Evaluation/JSObject/asyncJSFunctionBoundToDataField";
|
||||||
|
|
||||||
export function getFixedTimeDifference(endTime: number, startTime: number) {
|
export function getFixedTimeDifference(endTime: number, startTime: number) {
|
||||||
return (endTime - startTime).toFixed(2) + " ms";
|
return (endTime - startTime).toFixed(2) + " ms";
|
||||||
}
|
}
|
||||||
|
export function isDataField(fullPath: string, configTree: ConfigTree) {
|
||||||
|
const { entityName, propertyPath } = getEntityNameAndPropertyPath(fullPath);
|
||||||
|
const entityConfig = configTree[entityName];
|
||||||
|
if ("triggerPaths" in entityConfig) {
|
||||||
|
return !(propertyPath in entityConfig.triggerPaths);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWidgetActionOrJsObject(
|
||||||
|
entity: DataTreeEntity,
|
||||||
|
): entity is ActionEntity | WidgetEntity | JSActionEntity {
|
||||||
|
return isWidget(entity) || isAction(entity) || isJSAction(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addRootcauseToAsyncInvocationErrors(
|
||||||
|
fullPropertyPath: string,
|
||||||
|
configTree: ConfigTree,
|
||||||
|
errors: EvaluationError[],
|
||||||
|
) {
|
||||||
|
let updatedErrors = errors;
|
||||||
|
|
||||||
|
if (isDataField(fullPropertyPath, configTree)) {
|
||||||
|
const asyncFunctionBindingInPath =
|
||||||
|
asyncJsFunctionInDataFields.getAsyncFunctionBindingInDataField(
|
||||||
|
fullPropertyPath,
|
||||||
|
);
|
||||||
|
if (asyncFunctionBindingInPath) {
|
||||||
|
updatedErrors = errorModifier.setAsyncInvocationErrorsRootcause(
|
||||||
|
errors,
|
||||||
|
asyncFunctionBindingInPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedErrors;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ import {
|
||||||
updateMap,
|
updateMap,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
|
||||||
import { difference, isEmpty, set } from "lodash";
|
import { difference, isEmpty, set, uniq } from "lodash";
|
||||||
|
import { isWidgetActionOrJsObject } from "../DataTreeEvaluator/utils";
|
||||||
|
import { asyncJsFunctionInDataFields } from "workers/Evaluation/JSObject/asyncJSFunctionBoundToDataField";
|
||||||
|
|
||||||
interface CreateDependencyMap {
|
interface CreateDependencyMap {
|
||||||
dependencyMap: DependencyMap;
|
dependencyMap: DependencyMap;
|
||||||
|
|
@ -100,11 +102,18 @@ export function createDependencyMap(
|
||||||
const { errors, invalidReferences, validReferences } =
|
const { errors, invalidReferences, validReferences } =
|
||||||
extractInfoFromBindings(dependencyMap[key], dataTreeEvalRef.allKeys);
|
extractInfoFromBindings(dependencyMap[key], dataTreeEvalRef.allKeys);
|
||||||
dependencyMap[key] = validReferences;
|
dependencyMap[key] = validReferences;
|
||||||
// To keep invalidReferencesMap as minimal as possible, only paths with invalid references
|
|
||||||
// are stored.
|
updateMap(invalidReferencesMap, key, invalidReferences, {
|
||||||
if (invalidReferences.length) {
|
deleteOnEmpty: true,
|
||||||
invalidReferencesMap[key] = invalidReferences;
|
replaceValue: true,
|
||||||
}
|
});
|
||||||
|
|
||||||
|
asyncJsFunctionInDataFields.update(
|
||||||
|
key,
|
||||||
|
validReferences,
|
||||||
|
unEvalTree,
|
||||||
|
configTree,
|
||||||
|
);
|
||||||
errors.forEach((error) => {
|
errors.forEach((error) => {
|
||||||
dataTreeEvalRef.errors.push(error);
|
dataTreeEvalRef.errors.push(error);
|
||||||
});
|
});
|
||||||
|
|
@ -169,11 +178,12 @@ export const updateDependencyMap = ({
|
||||||
let didUpdateValidationDependencyMap = false;
|
let didUpdateValidationDependencyMap = false;
|
||||||
const dependenciesOfRemovedPaths: Array<string> = [];
|
const dependenciesOfRemovedPaths: Array<string> = [];
|
||||||
const removedPaths: Array<string> = [];
|
const removedPaths: Array<string> = [];
|
||||||
const extraPathsToLint = new Set<string>();
|
let extraPathsToLint: string[] = [];
|
||||||
const pathsToClearErrorsFor: any[] = [];
|
const pathsToClearErrorsFor: any[] = [];
|
||||||
const {
|
const {
|
||||||
dependencyMap,
|
dependencyMap,
|
||||||
invalidReferencesMap,
|
invalidReferencesMap,
|
||||||
|
inverseDependencyMap,
|
||||||
oldConfigTree,
|
oldConfigTree,
|
||||||
oldUnEvalTree,
|
oldUnEvalTree,
|
||||||
triggerFieldDependencyMap,
|
triggerFieldDependencyMap,
|
||||||
|
|
@ -207,7 +217,7 @@ export const updateDependencyMap = ({
|
||||||
if (entityType !== "noop") {
|
if (entityType !== "noop") {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case DataTreeDiffEvent.NEW: {
|
case DataTreeDiffEvent.NEW: {
|
||||||
if (isWidget(entity) || isAction(entity) || isJSAction(entity)) {
|
if (isWidgetActionOrJsObject(entity)) {
|
||||||
if (!isDynamicLeaf(unEvalDataTree, fullPropertyPath, configTree)) {
|
if (!isDynamicLeaf(unEvalDataTree, fullPropertyPath, configTree)) {
|
||||||
const entityDependencyMap: DependencyMap = listEntityDependencies(
|
const entityDependencyMap: DependencyMap = listEntityDependencies(
|
||||||
entity,
|
entity,
|
||||||
|
|
@ -236,6 +246,19 @@ export const updateDependencyMap = ({
|
||||||
invalidReferences,
|
invalidReferences,
|
||||||
{ deleteOnEmpty: true, replaceValue: true },
|
{ deleteOnEmpty: true, replaceValue: true },
|
||||||
);
|
);
|
||||||
|
// Update asyncJSFunctionsInDatafieldsMap
|
||||||
|
const updatedAsyncJSFunctions =
|
||||||
|
asyncJsFunctionInDataFields.update(
|
||||||
|
entityDependent,
|
||||||
|
validReferences,
|
||||||
|
unEvalDataTree,
|
||||||
|
configTree,
|
||||||
|
);
|
||||||
|
|
||||||
|
extraPathsToLint = extraPathsToLint.concat(
|
||||||
|
updatedAsyncJSFunctions,
|
||||||
|
);
|
||||||
|
|
||||||
dataTreeEvalErrors = dataTreeEvalErrors.concat(
|
dataTreeEvalErrors = dataTreeEvalErrors.concat(
|
||||||
extractDependencyErrors,
|
extractDependencyErrors,
|
||||||
);
|
);
|
||||||
|
|
@ -367,7 +390,7 @@ export const updateDependencyMap = ({
|
||||||
if (isChildPropertyPath(fullPropertyPath, invalidReference)) {
|
if (isChildPropertyPath(fullPropertyPath, invalidReference)) {
|
||||||
updateMap(newlyValidReferencesMap, invalidReference, [path]);
|
updateMap(newlyValidReferencesMap, invalidReference, [path]);
|
||||||
if (!dependencyMap[invalidReference]) {
|
if (!dependencyMap[invalidReference]) {
|
||||||
extraPathsToLint.add(path);
|
extraPathsToLint.push(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -403,6 +426,17 @@ export const updateDependencyMap = ({
|
||||||
fullPath,
|
fullPath,
|
||||||
validReferences,
|
validReferences,
|
||||||
);
|
);
|
||||||
|
// Update asyncJSMap
|
||||||
|
const updatedAsyncJSFunctions =
|
||||||
|
asyncJsFunctionInDataFields.update(
|
||||||
|
fullPath,
|
||||||
|
validReferences,
|
||||||
|
unEvalDataTree,
|
||||||
|
configTree,
|
||||||
|
);
|
||||||
|
extraPathsToLint = extraPathsToLint.concat(
|
||||||
|
updatedAsyncJSFunctions,
|
||||||
|
);
|
||||||
|
|
||||||
// Since the previously invalid reference has become valid,
|
// Since the previously invalid reference has become valid,
|
||||||
// remove it from the invalidReferencesMap
|
// remove it from the invalidReferencesMap
|
||||||
|
|
@ -434,7 +468,7 @@ export const updateDependencyMap = ({
|
||||||
if (
|
if (
|
||||||
isChildPropertyPath(fullPropertyPath, triggerPathDependency)
|
isChildPropertyPath(fullPropertyPath, triggerPathDependency)
|
||||||
) {
|
) {
|
||||||
extraPathsToLint.add(triggerPath);
|
extraPathsToLint.push(triggerPath);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -463,7 +497,7 @@ export const updateDependencyMap = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(isWidget(entity) || isAction(entity) || isJSAction(entity)) &&
|
isWidgetActionOrJsObject(entity) &&
|
||||||
fullPropertyPath === entityName
|
fullPropertyPath === entityName
|
||||||
) {
|
) {
|
||||||
const entityDependencies = listEntityDependencies(
|
const entityDependencies = listEntityDependencies(
|
||||||
|
|
@ -502,6 +536,7 @@ export const updateDependencyMap = ({
|
||||||
didUpdateValidationDependencyMap = true;
|
didUpdateValidationDependencyMap = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Either an existing entity or an existing property path has been deleted. Update the global dependency map
|
// Either an existing entity or an existing property path has been deleted. Update the global dependency map
|
||||||
// by removing the bindings from the same.
|
// by removing the bindings from the same.
|
||||||
Object.keys(dependencyMap).forEach((dependencyPath) => {
|
Object.keys(dependencyMap).forEach((dependencyPath) => {
|
||||||
|
|
@ -532,7 +567,7 @@ export const updateDependencyMap = ({
|
||||||
if (
|
if (
|
||||||
isChildPropertyPath(fullPropertyPath, invalidReference)
|
isChildPropertyPath(fullPropertyPath, invalidReference)
|
||||||
) {
|
) {
|
||||||
extraPathsToLint.add(dependencyPath);
|
extraPathsToLint.push(dependencyPath);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -571,7 +606,7 @@ export const updateDependencyMap = ({
|
||||||
if (
|
if (
|
||||||
isChildPropertyPath(fullPropertyPath, invalidReference)
|
isChildPropertyPath(fullPropertyPath, invalidReference)
|
||||||
) {
|
) {
|
||||||
extraPathsToLint.add(dependencyPath);
|
extraPathsToLint.push(dependencyPath);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -579,15 +614,21 @@ export const updateDependencyMap = ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update asyncJsFunctionInDataFields
|
||||||
|
const updatedAsyncJSFunctions =
|
||||||
|
asyncJsFunctionInDataFields.handlePathDeletion(
|
||||||
|
fullPropertyPath,
|
||||||
|
unEvalDataTree,
|
||||||
|
configTree,
|
||||||
|
);
|
||||||
|
extraPathsToLint = extraPathsToLint.concat(updatedAsyncJSFunctions);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DataTreeDiffEvent.EDIT: {
|
case DataTreeDiffEvent.EDIT: {
|
||||||
// We only care if the difference is in dynamic bindings since static values do not need
|
// We only care if the difference is in dynamic bindings since static values do not need
|
||||||
// an evaluation.
|
// an evaluation.
|
||||||
if (
|
if (isWidgetActionOrJsObject(entity) && typeof value === "string") {
|
||||||
(isWidget(entity) || isAction(entity) || isJSAction(entity)) &&
|
|
||||||
typeof value === "string"
|
|
||||||
) {
|
|
||||||
const entity: ActionEntity | WidgetEntity | JSActionEntity =
|
const entity: ActionEntity | WidgetEntity | JSActionEntity =
|
||||||
unEvalDataTree[entityName] as
|
unEvalDataTree[entityName] as
|
||||||
| ActionEntity
|
| ActionEntity
|
||||||
|
|
@ -623,7 +664,18 @@ export const updateDependencyMap = ({
|
||||||
dataTreeEvalErrors = dataTreeEvalErrors.concat(
|
dataTreeEvalErrors = dataTreeEvalErrors.concat(
|
||||||
extractDependencyErrors,
|
extractDependencyErrors,
|
||||||
);
|
);
|
||||||
|
// update asyncFunctionInSyncfieldsMap
|
||||||
|
const updatedAsyncJSFunctions =
|
||||||
|
asyncJsFunctionInDataFields.handlePathEdit(
|
||||||
|
fullPropertyPath,
|
||||||
|
validReferences,
|
||||||
|
unEvalDataTree,
|
||||||
|
inverseDependencyMap,
|
||||||
|
configTree,
|
||||||
|
);
|
||||||
|
extraPathsToLint = extraPathsToLint.concat(
|
||||||
|
updatedAsyncJSFunctions,
|
||||||
|
);
|
||||||
// We found a new dynamic binding for this property path. We update the dependency map by overwriting the
|
// We found a new dynamic binding for this property path. We update the dependency map by overwriting the
|
||||||
// dependencies for this property path with the newly found dependencies
|
// dependencies for this property path with the newly found dependencies
|
||||||
|
|
||||||
|
|
@ -673,6 +725,18 @@ export const updateDependencyMap = ({
|
||||||
didUpdateDependencyMap = true;
|
didUpdateDependencyMap = true;
|
||||||
delete dependencyMap[fullPropertyPath];
|
delete dependencyMap[fullPropertyPath];
|
||||||
delete invalidReferencesMap[fullPropertyPath];
|
delete invalidReferencesMap[fullPropertyPath];
|
||||||
|
// update asyncFunctionInSyncfieldsMap
|
||||||
|
const updatedAsyncJSFunctions =
|
||||||
|
asyncJsFunctionInDataFields.handlePathEdit(
|
||||||
|
fullPropertyPath,
|
||||||
|
[],
|
||||||
|
unEvalDataTree,
|
||||||
|
inverseDependencyMap,
|
||||||
|
configTree,
|
||||||
|
);
|
||||||
|
extraPathsToLint = extraPathsToLint.concat(
|
||||||
|
updatedAsyncJSFunctions,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|
@ -783,6 +847,6 @@ export const updateDependencyMap = ({
|
||||||
pathsToClearErrorsFor,
|
pathsToClearErrorsFor,
|
||||||
dependenciesOfRemovedPaths,
|
dependenciesOfRemovedPaths,
|
||||||
removedPaths,
|
removedPaths,
|
||||||
extraPathsToLint: Array.from(extraPathsToLint),
|
extraPathsToLint: uniq(extraPathsToLint),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,13 @@ import {
|
||||||
getEntityNameAndPropertyPath,
|
getEntityNameAndPropertyPath,
|
||||||
isAction,
|
isAction,
|
||||||
isJSAction,
|
isJSAction,
|
||||||
|
isJSActionConfig,
|
||||||
isWidget,
|
isWidget,
|
||||||
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ConfigTree,
|
|
||||||
DataTree,
|
DataTree,
|
||||||
|
ConfigTree,
|
||||||
DataTreeEntity,
|
DataTreeEntity,
|
||||||
DataTreeEntityConfig,
|
DataTreeEntityConfig,
|
||||||
WidgetEntity,
|
WidgetEntity,
|
||||||
|
|
@ -427,3 +428,24 @@ export function updateMap(
|
||||||
map[path] = updatedEntries;
|
map[path] = updatedEntries;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAsyncJSFunction(configTree: ConfigTree, fullPath: string) {
|
||||||
|
const { entityName, propertyPath } = getEntityNameAndPropertyPath(fullPath);
|
||||||
|
const configEntity = configTree[entityName];
|
||||||
|
return (
|
||||||
|
isJSActionConfig(configEntity) &&
|
||||||
|
propertyPath &&
|
||||||
|
propertyPath in configEntity.meta &&
|
||||||
|
configEntity.meta[propertyPath].isAsync
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJSFunction(configTree: ConfigTree, fullPath: string) {
|
||||||
|
const { entityName, propertyPath } = getEntityNameAndPropertyPath(fullPath);
|
||||||
|
const entityConfig = configTree[entityName];
|
||||||
|
return (
|
||||||
|
isJSActionConfig(entityConfig) &&
|
||||||
|
propertyPath &&
|
||||||
|
propertyPath in entityConfig.meta
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6816,7 +6816,7 @@ acorn-walk@^7.0.0, acorn-walk@^7.1.1, acorn-walk@^7.2.0:
|
||||||
version "7.2.0"
|
version "7.2.0"
|
||||||
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz"
|
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz"
|
||||||
|
|
||||||
acorn-walk@^8.1.1, acorn-walk@^8.2.0:
|
acorn-walk@^8.1.1:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz"
|
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz"
|
||||||
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
||||||
|
|
@ -7363,11 +7363,6 @@ astral-regex@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz"
|
||||||
|
|
||||||
astring@^1.7.5:
|
|
||||||
version "1.8.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.3.tgz#1a0ae738c7cc558f8e5ddc8e3120636f5cebcb85"
|
|
||||||
integrity sha512-sRpyiNrx2dEYIMmUXprS8nlpRg2Drs8m9ElX9vVEXaCB4XEAJhKfs7IcX0IwShjuOAjLR6wzIrgoptz1n19i1A==
|
|
||||||
|
|
||||||
async-limiter@~1.0.0:
|
async-limiter@~1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz"
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,12 @@ import {
|
||||||
import { ECMA_VERSION, SourceType, NodeTypes } from "./src/constants";
|
import { ECMA_VERSION, SourceType, NodeTypes } from "./src/constants";
|
||||||
|
|
||||||
// JSObjects
|
// JSObjects
|
||||||
import { parseJSObjectWithAST } from "./src/jsObject";
|
import {
|
||||||
|
parseJSObject,
|
||||||
|
isJSFunctionProperty,
|
||||||
|
TParsedJSProperty,
|
||||||
|
JSPropertyPosition,
|
||||||
|
} from "./src/jsObject";
|
||||||
|
|
||||||
// types or intefaces should be exported with type keyword, while enums can be exported like normal functions
|
// types or intefaces should be exported with type keyword, while enums can be exported like normal functions
|
||||||
export type {
|
export type {
|
||||||
|
|
@ -29,6 +34,8 @@ export type {
|
||||||
PropertyNode,
|
PropertyNode,
|
||||||
MemberExpressionData,
|
MemberExpressionData,
|
||||||
IdentifierInfo,
|
IdentifierInfo,
|
||||||
|
TParsedJSProperty,
|
||||||
|
JSPropertyPosition,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -44,8 +51,9 @@ export {
|
||||||
extractInvalidTopLevelMemberExpressionsFromCode,
|
extractInvalidTopLevelMemberExpressionsFromCode,
|
||||||
getFunctionalParamsFromNode,
|
getFunctionalParamsFromNode,
|
||||||
isTypeOfFunction,
|
isTypeOfFunction,
|
||||||
parseJSObjectWithAST,
|
parseJSObject,
|
||||||
ECMA_VERSION,
|
ECMA_VERSION,
|
||||||
SourceType,
|
SourceType,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
|
isJSFunctionProperty,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@
|
||||||
"link-package": "yarn install && rollup -c && cd build && cp -R ../node_modules ./node_modules && yarn link"
|
"link-package": "yarn install && rollup -c && cd build && cp -R ../node_modules ./node_modules && yarn link"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.21.0",
|
||||||
"acorn": "^8.8.0",
|
"acorn": "^8.8.0",
|
||||||
"acorn-walk": "^8.2.0",
|
"acorn-walk": "^8.2.0",
|
||||||
"astring": "^1.7.5",
|
"astring": "^1.7.5",
|
||||||
|
"escodegen": "^2.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"rollup": "^2.77.0",
|
"rollup": "^2.77.0",
|
||||||
"typescript": "4.5.5",
|
"typescript": "4.5.5",
|
||||||
|
|
@ -31,6 +33,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-typescript": "^7.17.12",
|
"@babel/preset-typescript": "^7.17.12",
|
||||||
"@rollup/plugin-commonjs": "^22.0.0",
|
"@rollup/plugin-commonjs": "^22.0.0",
|
||||||
|
"@types/escodegen": "^0.0.7",
|
||||||
"@types/jest": "29.0.3",
|
"@types/jest": "29.0.3",
|
||||||
"@types/lodash": "^4.14.120",
|
"@types/lodash": "^4.14.120",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.25.0",
|
"@typescript-eslint/eslint-plugin": "^5.25.0",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { parseJSObject } from "../index";
|
||||||
import { extractIdentifierInfoFromCode } from "../src/index";
|
import { extractIdentifierInfoFromCode } from "../src/index";
|
||||||
import { parseJSObjectWithAST } from "../src/jsObject";
|
|
||||||
|
|
||||||
describe("getAllIdentifiers", () => {
|
describe("getAllIdentifiers", () => {
|
||||||
it("works properly", () => {
|
it("works properly", () => {
|
||||||
|
|
@ -306,7 +306,7 @@ describe("getAllIdentifiers", () => {
|
||||||
const { references } = extractIdentifierInfoFromCode(
|
const { references } = extractIdentifierInfoFromCode(
|
||||||
perCase.script,
|
perCase.script,
|
||||||
2,
|
2,
|
||||||
perCase.invalidIdentifiers
|
perCase.invalidIdentifiers,
|
||||||
);
|
);
|
||||||
expect(references).toStrictEqual(perCase.expectedResults);
|
expect(references).toStrictEqual(perCase.expectedResults);
|
||||||
});
|
});
|
||||||
|
|
@ -315,7 +315,7 @@ describe("getAllIdentifiers", () => {
|
||||||
|
|
||||||
describe("parseJSObjectWithAST", () => {
|
describe("parseJSObjectWithAST", () => {
|
||||||
it("parse js object", () => {
|
it("parse js object", () => {
|
||||||
const body = `{
|
const body = `export default{
|
||||||
myVar1: [],
|
myVar1: [],
|
||||||
myVar2: {},
|
myVar2: {},
|
||||||
myFun1: () => {
|
myFun1: () => {
|
||||||
|
|
@ -325,36 +325,84 @@ describe("parseJSObjectWithAST", () => {
|
||||||
//use async-await or promises
|
//use async-await or promises
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
const parsedObject = [
|
|
||||||
|
const expectedParsedObject = [
|
||||||
{
|
{
|
||||||
key: "myVar1",
|
key: "myVar1",
|
||||||
value: "[]",
|
value: "[]",
|
||||||
|
rawContent: "myVar1: []",
|
||||||
type: "ArrayExpression",
|
type: "ArrayExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 2,
|
||||||
|
startColumn: 1,
|
||||||
|
endLine: 2,
|
||||||
|
endColumn: 11,
|
||||||
|
keyStartLine: 2,
|
||||||
|
keyEndLine: 2,
|
||||||
|
keyStartColumn: 1,
|
||||||
|
keyEndColumn: 7,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "myVar2",
|
key: "myVar2",
|
||||||
value: "{}",
|
value: "{}",
|
||||||
|
rawContent: "myVar2: {}",
|
||||||
type: "ObjectExpression",
|
type: "ObjectExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 3,
|
||||||
|
startColumn: 1,
|
||||||
|
endLine: 3,
|
||||||
|
endColumn: 11,
|
||||||
|
keyStartLine: 3,
|
||||||
|
keyEndLine: 3,
|
||||||
|
keyStartColumn: 1,
|
||||||
|
keyEndColumn: 7,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "myFun1",
|
key: "myFun1",
|
||||||
value: "() => {}",
|
value: "() => {}",
|
||||||
|
rawContent: "myFun1: () => {\n\t\t//write code here\n\t}",
|
||||||
type: "ArrowFunctionExpression",
|
type: "ArrowFunctionExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 4,
|
||||||
|
startColumn: 1,
|
||||||
|
endLine: 6,
|
||||||
|
endColumn: 2,
|
||||||
|
keyStartLine: 4,
|
||||||
|
keyEndLine: 4,
|
||||||
|
keyStartColumn: 1,
|
||||||
|
keyEndColumn: 7,
|
||||||
|
},
|
||||||
arguments: [],
|
arguments: [],
|
||||||
|
isMarkedAsync: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "myFun2",
|
key: "myFun2",
|
||||||
value: "async () => {}",
|
value: "async () => {}",
|
||||||
|
rawContent:
|
||||||
|
"myFun2: async () => {\n\t\t//use async-await or promises\n\t}",
|
||||||
type: "ArrowFunctionExpression",
|
type: "ArrowFunctionExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 7,
|
||||||
|
startColumn: 1,
|
||||||
|
endLine: 9,
|
||||||
|
endColumn: 2,
|
||||||
|
keyStartLine: 7,
|
||||||
|
keyEndLine: 7,
|
||||||
|
keyStartColumn: 1,
|
||||||
|
keyEndColumn: 7,
|
||||||
|
},
|
||||||
arguments: [],
|
arguments: [],
|
||||||
|
isMarkedAsync: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const resultParsedObject = parseJSObjectWithAST(body);
|
const { parsedObject } = parseJSObject(body);
|
||||||
expect(resultParsedObject).toStrictEqual(parsedObject);
|
expect(parsedObject).toStrictEqual(expectedParsedObject);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parse js object with literal", () => {
|
it("parse js object with literal", () => {
|
||||||
const body = `{
|
const body = `export default{
|
||||||
myVar1: [],
|
myVar1: [],
|
||||||
myVar2: {
|
myVar2: {
|
||||||
"a": "app",
|
"a": "app",
|
||||||
|
|
@ -366,36 +414,83 @@ describe("parseJSObjectWithAST", () => {
|
||||||
//use async-await or promises
|
//use async-await or promises
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
const parsedObject = [
|
const expectedParsedObject = [
|
||||||
{
|
{
|
||||||
key: "myVar1",
|
key: "myVar1",
|
||||||
value: "[]",
|
value: "[]",
|
||||||
|
rawContent: "myVar1: []",
|
||||||
type: "ArrayExpression",
|
type: "ArrayExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 2,
|
||||||
|
startColumn: 1,
|
||||||
|
endLine: 2,
|
||||||
|
endColumn: 11,
|
||||||
|
keyStartLine: 2,
|
||||||
|
keyEndLine: 2,
|
||||||
|
keyStartColumn: 1,
|
||||||
|
keyEndColumn: 7,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "myVar2",
|
key: "myVar2",
|
||||||
value: '{\n "a": "app"\n}',
|
value: '{\n "a": "app"\n}',
|
||||||
|
rawContent: 'myVar2: {\n\t\t"a": "app",\n\t}',
|
||||||
type: "ObjectExpression",
|
type: "ObjectExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 3,
|
||||||
|
startColumn: 1,
|
||||||
|
endLine: 5,
|
||||||
|
endColumn: 2,
|
||||||
|
keyStartLine: 3,
|
||||||
|
keyEndLine: 3,
|
||||||
|
keyStartColumn: 1,
|
||||||
|
keyEndColumn: 7,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "myFun1",
|
key: "myFun1",
|
||||||
value: "() => {}",
|
value: "() => {}",
|
||||||
|
rawContent: "myFun1: () => {\n\t\t//write code here\n\t}",
|
||||||
type: "ArrowFunctionExpression",
|
type: "ArrowFunctionExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 6,
|
||||||
|
startColumn: 1,
|
||||||
|
endLine: 8,
|
||||||
|
endColumn: 2,
|
||||||
|
keyStartLine: 6,
|
||||||
|
keyEndLine: 6,
|
||||||
|
keyStartColumn: 1,
|
||||||
|
keyEndColumn: 7,
|
||||||
|
},
|
||||||
arguments: [],
|
arguments: [],
|
||||||
|
isMarkedAsync: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "myFun2",
|
key: "myFun2",
|
||||||
value: "async () => {}",
|
value: "async () => {}",
|
||||||
|
rawContent:
|
||||||
|
"myFun2: async () => {\n\t\t//use async-await or promises\n\t}",
|
||||||
type: "ArrowFunctionExpression",
|
type: "ArrowFunctionExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 9,
|
||||||
|
startColumn: 1,
|
||||||
|
endLine: 11,
|
||||||
|
endColumn: 2,
|
||||||
|
keyStartLine: 9,
|
||||||
|
keyEndLine: 9,
|
||||||
|
keyStartColumn: 1,
|
||||||
|
keyEndColumn: 7,
|
||||||
|
},
|
||||||
arguments: [],
|
arguments: [],
|
||||||
|
isMarkedAsync: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const resultParsedObject = parseJSObjectWithAST(body);
|
const { parsedObject } = parseJSObject(body);
|
||||||
expect(resultParsedObject).toStrictEqual(parsedObject);
|
expect(parsedObject).toStrictEqual(expectedParsedObject);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parse js object with variable declaration inside function", () => {
|
it("parse js object with variable declaration inside function", () => {
|
||||||
const body = `{
|
const body = `export default{
|
||||||
myFun1: () => {
|
myFun1: () => {
|
||||||
const a = {
|
const a = {
|
||||||
conditions: [],
|
conditions: [],
|
||||||
|
|
@ -408,89 +503,108 @@ describe("parseJSObjectWithAST", () => {
|
||||||
//use async-await or promises
|
//use async-await or promises
|
||||||
}
|
}
|
||||||
}`;
|
}`;
|
||||||
const parsedObject = [
|
const expectedParsedObject = [
|
||||||
{
|
{
|
||||||
key: "myFun1",
|
key: "myFun1",
|
||||||
value: `() => {
|
value:
|
||||||
const a = {
|
"() => {\n" +
|
||||||
conditions: [],
|
" const a = {\n" +
|
||||||
requires: 1,
|
" conditions: [],\n" +
|
||||||
testFunc: () => {},
|
" requires: 1,\n" +
|
||||||
testFunc2: function () {}
|
" 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",
|
type: "ArrowFunctionExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 2,
|
||||||
|
startColumn: 6,
|
||||||
|
endLine: 9,
|
||||||
|
endColumn: 7,
|
||||||
|
keyStartLine: 2,
|
||||||
|
keyEndLine: 2,
|
||||||
|
keyStartColumn: 6,
|
||||||
|
keyEndColumn: 12,
|
||||||
|
},
|
||||||
arguments: [],
|
arguments: [],
|
||||||
|
isMarkedAsync: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "myFun2",
|
key: "myFun2",
|
||||||
value: "async () => {}",
|
value: "async () => {}",
|
||||||
|
rawContent:
|
||||||
|
"myFun2: async () => {\n //use async-await or promises\n }",
|
||||||
type: "ArrowFunctionExpression",
|
type: "ArrowFunctionExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 10,
|
||||||
|
startColumn: 6,
|
||||||
|
endLine: 12,
|
||||||
|
endColumn: 7,
|
||||||
|
keyStartLine: 10,
|
||||||
|
keyEndLine: 10,
|
||||||
|
keyStartColumn: 6,
|
||||||
|
keyEndColumn: 12,
|
||||||
|
},
|
||||||
arguments: [],
|
arguments: [],
|
||||||
|
isMarkedAsync: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const resultParsedObject = parseJSObjectWithAST(body);
|
const { parsedObject } = parseJSObject(body);
|
||||||
expect(resultParsedObject).toStrictEqual(parsedObject);
|
expect(parsedObject).toStrictEqual(expectedParsedObject);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parse js object with params of all types", () => {
|
it("parse js object with params of all types", () => {
|
||||||
const body = `{
|
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()) => {
|
myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue()) => {
|
||||||
//use async-await or promises
|
//use async-await or promises
|
||||||
},
|
},
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const parsedObject = [
|
const expectedParsedObject = [
|
||||||
{
|
{
|
||||||
key: "myFun2",
|
key: "myFun2",
|
||||||
value:
|
value:
|
||||||
'async (a, b = Array(1, 2, 3), c = "", d = [], e = this.myVar1, f = {}, g = function () {}, h = Object.assign({}), i = String(), j = storeValue()) => {}',
|
'async (a, b = Array(1, 2, 3), c = "", d = [], e = this.myVar1, f = {}, g = function () {}, h = Object.assign({}), i = String(), j = storeValue()) => {}',
|
||||||
|
rawContent:
|
||||||
|
'myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue()) => {\n' +
|
||||||
|
" //use async-await or promises\n" +
|
||||||
|
" }",
|
||||||
type: "ArrowFunctionExpression",
|
type: "ArrowFunctionExpression",
|
||||||
|
position: {
|
||||||
|
startLine: 2,
|
||||||
|
startColumn: 6,
|
||||||
|
endLine: 4,
|
||||||
|
endColumn: 7,
|
||||||
|
keyStartLine: 2,
|
||||||
|
keyEndLine: 2,
|
||||||
|
keyStartColumn: 6,
|
||||||
|
keyEndColumn: 12,
|
||||||
|
},
|
||||||
arguments: [
|
arguments: [
|
||||||
{
|
{ paramName: "a", defaultValue: undefined },
|
||||||
paramName: "a",
|
{ paramName: "b", defaultValue: undefined },
|
||||||
defaultValue: undefined,
|
{ paramName: "c", defaultValue: undefined },
|
||||||
},
|
{ paramName: "d", defaultValue: undefined },
|
||||||
{
|
{ paramName: "e", defaultValue: undefined },
|
||||||
paramName: "b",
|
{ paramName: "f", defaultValue: undefined },
|
||||||
defaultValue: undefined,
|
{ paramName: "g", defaultValue: undefined },
|
||||||
},
|
{ paramName: "h", defaultValue: undefined },
|
||||||
{
|
{ paramName: "i", defaultValue: undefined },
|
||||||
paramName: "c",
|
{ paramName: "j", defaultValue: undefined },
|
||||||
defaultValue: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
paramName: "d",
|
|
||||||
defaultValue: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
paramName: "e",
|
|
||||||
defaultValue: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
paramName: "f",
|
|
||||||
defaultValue: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
paramName: "g",
|
|
||||||
defaultValue: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
paramName: "h",
|
|
||||||
defaultValue: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
paramName: "i",
|
|
||||||
defaultValue: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
paramName: "j",
|
|
||||||
defaultValue: undefined,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
|
isMarkedAsync: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const resultParsedObject = parseJSObjectWithAST(body);
|
const { parsedObject } = parseJSObject(body);
|
||||||
expect(resultParsedObject).toEqual(parsedObject);
|
expect(parsedObject).toStrictEqual(expectedParsedObject);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { parse, Node, SourceLocation, Options, Comment } from "acorn";
|
import { parse, Node, SourceLocation, Options } from "acorn";
|
||||||
import { ancestor, simple } from "acorn-walk";
|
import { ancestor, simple } from "acorn-walk";
|
||||||
import { ECMA_VERSION, NodeTypes } from "./constants/ast";
|
import { ECMA_VERSION, NodeTypes } from "./constants/ast";
|
||||||
import { has, isFinite, isString, memoize, toPath } from "lodash";
|
import { has, isFinite, isString, memoize, toPath } from "lodash";
|
||||||
|
|
@ -36,7 +36,7 @@ interface MemberExpressionNode extends Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
// doc: https://github.com/estree/estree/blob/master/es5.md#identifier
|
// doc: https://github.com/estree/estree/blob/master/es5.md#identifier
|
||||||
interface IdentifierNode extends Node {
|
export interface IdentifierNode extends Node {
|
||||||
type: NodeTypes.Identifier;
|
type: NodeTypes.Identifier;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
@ -69,10 +69,12 @@ interface FunctionDeclarationNode extends Node, Function {
|
||||||
// doc: https://github.com/estree/estree/blob/master/es5.md#functionexpression
|
// doc: https://github.com/estree/estree/blob/master/es5.md#functionexpression
|
||||||
interface FunctionExpressionNode extends Expression, Function {
|
interface FunctionExpressionNode extends Expression, Function {
|
||||||
type: NodeTypes.FunctionExpression;
|
type: NodeTypes.FunctionExpression;
|
||||||
|
async: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ArrowFunctionExpressionNode extends Expression, Function {
|
interface ArrowFunctionExpressionNode extends Expression, Function {
|
||||||
type: NodeTypes.ArrowFunctionExpression;
|
type: NodeTypes.ArrowFunctionExpression;
|
||||||
|
async: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObjectExpression extends Expression {
|
export interface ObjectExpression extends Expression {
|
||||||
|
|
@ -87,7 +89,7 @@ interface AssignmentPatternNode extends Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
// doc: https://github.com/estree/estree/blob/master/es5.md#literal
|
// doc: https://github.com/estree/estree/blob/master/es5.md#literal
|
||||||
interface LiteralNode extends Node {
|
export interface LiteralNode extends Node {
|
||||||
type: NodeTypes.Literal;
|
type: NodeTypes.Literal;
|
||||||
value: string | boolean | null | number | RegExp;
|
value: string | boolean | null | number | RegExp;
|
||||||
}
|
}
|
||||||
|
|
@ -107,8 +109,12 @@ export interface PropertyNode extends Node {
|
||||||
kind: "init" | "get" | "set";
|
kind: "init" | "get" | "set";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExportDefaultDeclarationNode extends Node {
|
||||||
|
declaration: Node;
|
||||||
|
}
|
||||||
|
|
||||||
// Node with location details
|
// Node with location details
|
||||||
type NodeWithLocation<NodeType> = NodeType & {
|
export type NodeWithLocation<NodeType> = NodeType & {
|
||||||
loc: SourceLocation;
|
loc: SourceLocation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -163,6 +169,12 @@ export const isPropertyNode = (node: Node): node is PropertyNode => {
|
||||||
return node.type === NodeTypes.Property;
|
return node.type === NodeTypes.Property;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isExportDefaultDeclarationNode = (
|
||||||
|
node: Node,
|
||||||
|
): node is ExportDefaultDeclarationNode => {
|
||||||
|
return node.type === NodeTypes.ExportDefaultDeclaration;
|
||||||
|
};
|
||||||
|
|
||||||
export const isPropertyAFunctionNode = (
|
export const isPropertyAFunctionNode = (
|
||||||
node: Node,
|
node: Node,
|
||||||
): node is ArrowFunctionExpressionNode | FunctionExpressionNode => {
|
): node is ArrowFunctionExpressionNode | FunctionExpressionNode => {
|
||||||
|
|
@ -211,7 +223,11 @@ const getFunctionalParamNamesFromNode = (
|
||||||
// Since this will be used by both the server and the client, we want to prevent regeneration of ast
|
// Since this will be used by both the server and the client, we want to prevent regeneration of ast
|
||||||
// for the the same code snippet
|
// for the the same code snippet
|
||||||
export const getAST = memoize((code: string, options?: AstOptions) =>
|
export const getAST = memoize((code: string, options?: AstOptions) =>
|
||||||
parse(code, { ...options, ecmaVersion: ECMA_VERSION }),
|
parse(code, {
|
||||||
|
...options,
|
||||||
|
ecmaVersion: ECMA_VERSION,
|
||||||
|
locations: true, // Adds location data to each node
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,140 @@
|
||||||
import { Node } from "acorn";
|
import { Node } from "acorn";
|
||||||
import { getAST } from "../index";
|
|
||||||
import { generate } from "astring";
|
|
||||||
import { simple } from "acorn-walk";
|
import { simple } from "acorn-walk";
|
||||||
|
import {
|
||||||
|
getAST,
|
||||||
|
IdentifierNode,
|
||||||
|
isExportDefaultDeclarationNode,
|
||||||
|
isObjectExpression,
|
||||||
|
isPropertyNode,
|
||||||
|
isTypeOfFunction,
|
||||||
|
LiteralNode,
|
||||||
|
NodeWithLocation,
|
||||||
|
PropertyNode,
|
||||||
|
} from "../index";
|
||||||
|
import { generate } from "astring";
|
||||||
import {
|
import {
|
||||||
getFunctionalParamsFromNode,
|
getFunctionalParamsFromNode,
|
||||||
isPropertyAFunctionNode,
|
isPropertyAFunctionNode,
|
||||||
isVariableDeclarator,
|
|
||||||
isObjectExpression,
|
|
||||||
PropertyNode,
|
|
||||||
functionParam,
|
functionParam,
|
||||||
} from "../index";
|
} from "../index";
|
||||||
|
import { SourceType, NodeTypes } from "../../index";
|
||||||
type JsObjectProperty = {
|
import { attachComments } from "escodegen";
|
||||||
key: string;
|
import { extractContentByPosition } from "../utils";
|
||||||
value: string;
|
|
||||||
type: string;
|
|
||||||
arguments?: Array<functionParam>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const jsObjectVariableName =
|
const jsObjectVariableName =
|
||||||
"____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____";
|
"____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____";
|
||||||
|
|
||||||
export const jsObjectDeclaration = `var ${jsObjectVariableName} =`;
|
export const jsObjectDeclaration = `var ${jsObjectVariableName} =`;
|
||||||
|
|
||||||
export const parseJSObjectWithAST = (
|
export interface JSPropertyPosition {
|
||||||
jsObjectBody: string
|
startLine: number;
|
||||||
): Array<JsObjectProperty> => {
|
startColumn: number;
|
||||||
/*
|
endLine: number;
|
||||||
jsObjectVariableName value is added such actual js code would never name same variable name.
|
endColumn: number;
|
||||||
if the variable name will be same then also we won't have problem here as jsObjectVariableName will be last node in VariableDeclarator hence overriding the previous JSObjectProperties.
|
keyStartLine: number;
|
||||||
Keeping this just for sanity check if any caveat was missed.
|
keyEndLine: number;
|
||||||
*/
|
keyStartColumn: number;
|
||||||
const jsCode = `${jsObjectDeclaration} ${jsObjectBody}`;
|
keyEndColumn: number;
|
||||||
|
}
|
||||||
|
|
||||||
const ast = getAST(jsCode);
|
interface baseJSProperty {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
type: string;
|
||||||
|
position: Partial<JSPropertyPosition>;
|
||||||
|
rawContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
const parsedObjectProperties = new Set<JsObjectProperty>();
|
type JSFunctionProperty = baseJSProperty & {
|
||||||
let JSObjectProperties: Array<PropertyNode> = [];
|
arguments: functionParam[];
|
||||||
|
// If function uses the "async" keyword
|
||||||
|
isMarkedAsync: boolean;
|
||||||
|
};
|
||||||
|
type JSVarProperty = baseJSProperty;
|
||||||
|
|
||||||
|
export type TParsedJSProperty = JSVarProperty | JSFunctionProperty;
|
||||||
|
|
||||||
|
export const isJSFunctionProperty = (
|
||||||
|
t: TParsedJSProperty,
|
||||||
|
): t is JSFunctionProperty => {
|
||||||
|
return isTypeOfFunction(t.type);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseJSObject = (code: string) => {
|
||||||
|
let ast: Node = { end: 0, start: 0, type: "" };
|
||||||
|
const result: TParsedJSProperty[] = [];
|
||||||
|
try {
|
||||||
|
const comments: any = [];
|
||||||
|
const token: any = [];
|
||||||
|
ast = getAST(code, {
|
||||||
|
sourceType: SourceType.module,
|
||||||
|
onComment: comments,
|
||||||
|
onToken: token,
|
||||||
|
ranges: true,
|
||||||
|
});
|
||||||
|
attachComments(ast, comments, token);
|
||||||
|
} catch (e) {
|
||||||
|
return { parsedObject: result, success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedObjectProperties = new Set<TParsedJSProperty>();
|
||||||
|
let JSObjectProperties: NodeWithLocation<PropertyNode>[] = [];
|
||||||
|
|
||||||
simple(ast, {
|
simple(ast, {
|
||||||
VariableDeclarator(node: Node) {
|
ExportDefaultDeclaration(node, ancestors: Node[]) {
|
||||||
if (
|
if (
|
||||||
isVariableDeclarator(node) &&
|
!isExportDefaultDeclarationNode(node) ||
|
||||||
node.id.name === jsObjectVariableName &&
|
!isObjectExpression(node.declaration)
|
||||||
node.init &&
|
)
|
||||||
isObjectExpression(node.init)
|
return;
|
||||||
) {
|
JSObjectProperties = node.declaration
|
||||||
JSObjectProperties = node.init.properties;
|
.properties as NodeWithLocation<PropertyNode>[];
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
JSObjectProperties.forEach((node) => {
|
JSObjectProperties.forEach((node) => {
|
||||||
let params = new Set<functionParam>();
|
const propertyKey = node.key as NodeWithLocation<
|
||||||
const propertyNode = node;
|
LiteralNode | IdentifierNode
|
||||||
let property: JsObjectProperty = {
|
>;
|
||||||
key: generate(propertyNode.key),
|
let property: TParsedJSProperty = {
|
||||||
value: generate(propertyNode.value),
|
key: generate(node.key),
|
||||||
type: propertyNode.value.type,
|
value: generate(node.value),
|
||||||
|
rawContent: extractContentByPosition(code, {
|
||||||
|
from: {
|
||||||
|
line: node.loc.start.line - 1,
|
||||||
|
ch: node.loc.start.column,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
line: node.loc.end.line - 1,
|
||||||
|
ch: node.loc.end.column - 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
type: node.value.type,
|
||||||
|
position: {
|
||||||
|
startLine: node.loc.start.line,
|
||||||
|
startColumn: node.loc.start.column,
|
||||||
|
endLine: node.loc.end.line,
|
||||||
|
endColumn: node.loc.end.column,
|
||||||
|
keyStartLine: propertyKey.loc.start.line,
|
||||||
|
keyEndLine: propertyKey.loc.end.line,
|
||||||
|
keyStartColumn: propertyKey.loc.start.column,
|
||||||
|
keyEndColumn: propertyKey.loc.end.column,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isPropertyAFunctionNode(propertyNode.value)) {
|
if (isPropertyAFunctionNode(node.value)) {
|
||||||
// if in future we need default values of each param, we could implement that in getFunctionalParamsFromNode
|
// if in future we need default values of each param, we could implement that in getFunctionalParamsFromNode
|
||||||
// currently we don't consume it anywhere hence avoiding to calculate that.
|
// currently we don't consume it anywhere hence avoiding to calculate that.
|
||||||
params = getFunctionalParamsFromNode(propertyNode.value);
|
const params = getFunctionalParamsFromNode(node.value);
|
||||||
property = {
|
property = {
|
||||||
...property,
|
...property,
|
||||||
arguments: [...params],
|
arguments: [...params],
|
||||||
|
isMarkedAsync: node.value.async,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// here we use `generate` function to convert our AST Node to JSCode
|
|
||||||
parsedObjectProperties.add(property);
|
parsedObjectProperties.add(property);
|
||||||
});
|
});
|
||||||
|
|
||||||
return [...parsedObjectProperties];
|
return { parsedObject: [...parsedObjectProperties], success: true };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import unescapeJS from 'unescape-js';
|
import unescapeJS from "unescape-js";
|
||||||
|
import { isLiteralNode, PropertyNode } from "../index";
|
||||||
|
|
||||||
const beginsWithLineBreakRegex = /^\s+|\s+$/;
|
const beginsWithLineBreakRegex = /^\s+|\s+$/;
|
||||||
|
|
||||||
|
|
@ -8,14 +9,51 @@ export function sanitizeScript(js: string, evaluationVersion: number) {
|
||||||
// so that eval can happen
|
// so that eval can happen
|
||||||
//default value of evalutaion version is 2
|
//default value of evalutaion version is 2
|
||||||
evaluationVersion = evaluationVersion ? evaluationVersion : 2;
|
evaluationVersion = evaluationVersion ? evaluationVersion : 2;
|
||||||
const trimmedJS = js.replace(beginsWithLineBreakRegex, '');
|
const trimmedJS = js.replace(beginsWithLineBreakRegex, "");
|
||||||
return evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS);
|
return evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For the times when you need to know if something truly an object like { a: 1, b: 2}
|
// For the times when you need to know if something truly an object like { a: 1, b: 2}
|
||||||
// typeof, lodash.isObject and others will return false positives for things like array, null, etc
|
// typeof, lodash.isObject and others will return false positives for things like array, null, etc
|
||||||
export const isTrueObject = (
|
export const isTrueObject = (
|
||||||
item: unknown
|
item: unknown,
|
||||||
): item is Record<string, unknown> => {
|
): item is Record<string, unknown> => {
|
||||||
return Object.prototype.toString.call(item) === '[object Object]';
|
return Object.prototype.toString.call(item) === "[object Object]";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNameFromPropertyNode = (node: PropertyNode): string =>
|
||||||
|
isLiteralNode(node.key) ? String(node.key.value) : node.key.name;
|
||||||
|
|
||||||
|
type Position = {
|
||||||
|
line: number;
|
||||||
|
ch: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractContentByPosition = (
|
||||||
|
content: string,
|
||||||
|
position: { from: Position; to: Position },
|
||||||
|
) => {
|
||||||
|
const eachLine = content.split("\n");
|
||||||
|
|
||||||
|
let returnedString = "";
|
||||||
|
|
||||||
|
for (let i = position.from.line; i <= position.to.line; i++) {
|
||||||
|
if (i === position.from.line) {
|
||||||
|
returnedString =
|
||||||
|
position.from.line !== position.to.line
|
||||||
|
? eachLine[position.from.line].slice(position.from.ch)
|
||||||
|
: eachLine[position.from.line].slice(
|
||||||
|
position.from.ch,
|
||||||
|
position.to.ch + 1,
|
||||||
|
);
|
||||||
|
} else if (i === position.to.line) {
|
||||||
|
returnedString += eachLine[position.to.line].slice(0, position.to.ch + 1);
|
||||||
|
} else {
|
||||||
|
returnedString += eachLine[i];
|
||||||
|
}
|
||||||
|
if (i !== position.to.line) {
|
||||||
|
returnedString += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnedString;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user