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 dynamicInputLocators = require("../../../../locators/DynamicInput.json");
|
||||
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
|
||||
|
||||
describe("Dynamic input autocomplete", () => {
|
||||
before(() => {
|
||||
|
|
@ -72,7 +71,7 @@ describe("Dynamic input autocomplete", () => {
|
|||
cy.wait(1000);
|
||||
|
||||
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}}",
|
||||
"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";
|
||||
_modalWrapper = "[data-cy='modal-wrapper']";
|
||||
_editorBackButton = ".t--close-editor";
|
||||
_evaluateMsg = ".t--evaluatedPopup-error";
|
||||
_canvas = "[data-testid=widgets-editor]";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,9 +64,7 @@
|
|||
"@uppy/url": "^1.5.16",
|
||||
"@uppy/webcam": "^1.8.4",
|
||||
"@welldone-software/why-did-you-render": "^4.2.5",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"algoliasearch": "^4.2.0",
|
||||
"astring": "^1.7.5",
|
||||
"axios": "^0.27.2",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.59.2",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||
import type { ReduxAction } 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 = (
|
||||
errors: LintErrors,
|
||||
): ReduxAction<{ errors: LintErrors }> => {
|
||||
errors: LintErrorsStore,
|
||||
): ReduxAction<{ errors: LintErrorsStore }> => {
|
||||
return {
|
||||
type: ReduxActionTypes.SET_LINT_ERRORS,
|
||||
payload: { errors },
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ import type { EditorContextState } from "reducers/uiReducers/editorContextReduce
|
|||
import type { LibraryState } from "reducers/uiReducers/libraryReducer";
|
||||
import type { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer";
|
||||
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 type { AutoHeightUIState } from "reducers/uiReducers/autoHeightReducer";
|
||||
import type { AnalyticsReduxState } from "reducers/uiReducers/analyticsReducer";
|
||||
|
|
@ -159,7 +159,7 @@ export interface AppState {
|
|||
triggers: TriggerValuesEvaluationState;
|
||||
};
|
||||
linting: {
|
||||
errors: LintErrors;
|
||||
errors: LintErrorsStore;
|
||||
};
|
||||
form: {
|
||||
[key: string]: any;
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
entityFns,
|
||||
getPlatformFunctions,
|
||||
} from "@appsmith/workers/Evaluation/fns";
|
||||
import { klona } from "klona/full";
|
||||
declare global {
|
||||
/** All identifiers added to the worker global scope should also
|
||||
* be included in the DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS in
|
||||
|
|
@ -33,21 +34,21 @@ export enum ExecutionType {
|
|||
export const addDataTreeToContext = (args: {
|
||||
EVAL_CONTEXT: EvalContext;
|
||||
dataTree: Readonly<DataTree>;
|
||||
skipEntityFunctions?: boolean;
|
||||
removeEntityFunctions?: boolean;
|
||||
isTriggerBased: boolean;
|
||||
}) => {
|
||||
const {
|
||||
dataTree,
|
||||
EVAL_CONTEXT,
|
||||
isTriggerBased,
|
||||
skipEntityFunctions = false,
|
||||
removeEntityFunctions = false,
|
||||
} = args;
|
||||
const dataTreeEntries = Object.entries(dataTree);
|
||||
const entityFunctionCollection: Record<string, Record<string, Function>> = {};
|
||||
|
||||
for (const [entityName, entity] of dataTreeEntries) {
|
||||
EVAL_CONTEXT[entityName] = entity;
|
||||
if (skipEntityFunctions || !isTriggerBased) continue;
|
||||
if (!removeEntityFunctions && !isTriggerBased) continue;
|
||||
for (const entityFn of entityFns) {
|
||||
if (!entityFn.qualifier(entity)) continue;
|
||||
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 (!isTriggerBased) return;
|
||||
|
||||
|
|
@ -87,3 +94,18 @@ export const getAllAsyncFunctions = (dataTree: DataTree) => {
|
|||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
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 {
|
||||
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
|
||||
// Check issue https://github.com/appsmithorg/appsmith/issues/719
|
||||
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 type { EvaluationError } from "utils/DynamicBindingUtils";
|
||||
import { PropertyEvaluationErrorCategory } from "utils/DynamicBindingUtils";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { Severity } from "@sentry/react";
|
||||
import type { CodeEditorExpected } from "components/editorComponents/CodeEditor/index";
|
||||
|
|
@ -33,6 +34,11 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import { getEvaluatedPopupState } from "selectors/editorContextSelectors";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
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"] = {
|
||||
offset: {
|
||||
|
|
@ -183,6 +189,24 @@ const StyledTitleName = styled.p`
|
|||
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 }) {
|
||||
const { isOpen } = props;
|
||||
return (
|
||||
|
|
@ -462,16 +486,38 @@ function PopoverContent(props: PopoverContentProps) {
|
|||
? popupContext.value
|
||||
: 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 = () =>
|
||||
setOpenExpectedDataType(!openExpectedDataType);
|
||||
const toggleExpectedExample = () =>
|
||||
setOpenExpectedExample(!openExpectedExample);
|
||||
const { errors, expected, hasError, onMouseEnter, onMouseLeave, theme } =
|
||||
props;
|
||||
|
||||
let error: EvaluationError | undefined;
|
||||
if (hasError) {
|
||||
error = errors[0];
|
||||
}
|
||||
const openDebugger = (
|
||||
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
dispatch(showDebugger());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
|
|
@ -508,13 +554,25 @@ function PopoverContent(props: PopoverContentProps) {
|
|||
{/* errorMessage could be an empty string */}
|
||||
{getErrorMessage(error.errorMessage)}
|
||||
</span>
|
||||
<EvaluatedValueDebugButton
|
||||
entity={props.entity}
|
||||
error={{
|
||||
type: error.errorType,
|
||||
message: error.errorMessage,
|
||||
}}
|
||||
/>
|
||||
|
||||
{errorNavigationUrl ? (
|
||||
<AsyncFunctionErrorView>
|
||||
<AsyncFunctionErrorLink onClick={(e) => openDebugger(e)} to="">
|
||||
See Error ({modText()} D)
|
||||
</AsyncFunctionErrorLink>
|
||||
<AsyncFunctionErrorLink to={errorNavigationUrl}>
|
||||
View Source
|
||||
</AsyncFunctionErrorLink>
|
||||
</AsyncFunctionErrorView>
|
||||
) : (
|
||||
<EvaluatedValueDebugButton
|
||||
entity={props.entity}
|
||||
error={{
|
||||
type: error.errorType,
|
||||
message: error.errorMessage,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ErrorText>
|
||||
)}
|
||||
{props.expected && props.expected.type !== UNDEFINED_VALIDATION && (
|
||||
|
|
|
|||
|
|
@ -1263,6 +1263,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
text="/"
|
||||
/>
|
||||
)}
|
||||
|
||||
<EvaluatedValuePopup
|
||||
dataTreePath={this.props.dataTreePath}
|
||||
editorRef={this.codeEditorTarget}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { Severity } from "entities/AppsmithConsole";
|
||||
import type { LintError } 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 {
|
||||
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 });
|
||||
expect(res).toEqual([
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
CUSTOM_LINT_ERRORS,
|
||||
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
|
||||
INVALID_JSOBJECT_START_STATEMENT,
|
||||
JS_OBJECT_START_STATEMENT,
|
||||
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
|
||||
} from "workers/Linting/constants";
|
||||
export const getIndexOfRegex = (
|
||||
str: string,
|
||||
|
|
@ -123,33 +123,32 @@ export const getLintAnnotations = (
|
|||
const lintErrors = filterInvalidLintErrors(errors, contextData);
|
||||
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) => {
|
||||
const { ch, errorMessage, line, originalBinding, severity, variables } =
|
||||
error;
|
||||
const {
|
||||
ch,
|
||||
code,
|
||||
errorMessage,
|
||||
line,
|
||||
originalBinding,
|
||||
severity,
|
||||
variables,
|
||||
} = error;
|
||||
|
||||
if (!originalBinding) {
|
||||
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;
|
||||
// Find the variable with minimal length
|
||||
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 { isEqual } from "lodash";
|
||||
|
||||
export interface LintErrors {
|
||||
[entityName: string]: LintError[];
|
||||
}
|
||||
export type LintErrorsStore = Record<string, LintError[]>;
|
||||
|
||||
const initialState: LintErrors = {};
|
||||
const initialState: LintErrorsStore = {};
|
||||
|
||||
export const lintErrorReducer = createImmerReducer(initialState, {
|
||||
[ReduxActionTypes.FETCH_PAGE_INIT]: () => initialState,
|
||||
[ReduxActionTypes.SET_LINT_ERRORS]: (
|
||||
state: LintErrors,
|
||||
state: LintErrorsStore,
|
||||
action: SetLintErrorsAction,
|
||||
) => {
|
||||
const { errors } = action.payload;
|
||||
for (const entityName of Object.keys(errors)) {
|
||||
if (isEqual(state[entityName], errors[entityName])) continue;
|
||||
state[entityName] = errors[entityName];
|
||||
for (const entityPath of Object.keys(errors)) {
|
||||
const entityPathLintErrors = errors[entityPath];
|
||||
if (isEqual(entityPathLintErrors, state[entityPath])) continue;
|
||||
state[entityPath] = entityPathLintErrors;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -404,7 +404,7 @@ function* logDebuggerErrorAnalyticsSaga(
|
|||
);
|
||||
const pluginId = action?.pluginId || payload?.analytics?.pluginId || "";
|
||||
const plugin: Plugin = yield select(getPlugin, pluginId);
|
||||
const pluginName = plugin.name.replace(/ /g, "");
|
||||
const pluginName = plugin?.name.replace(/ /g, "");
|
||||
let propertyPath = `${pluginName}`;
|
||||
|
||||
if (payload.propertyPath) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { logJSFunctionExecution } from "@appsmith/sagas/JSFunctionExecutionSaga"
|
|||
import { handleStoreOperations } from "./ActionExecution/StoreActionSaga";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
|
||||
import type { LintTreeSagaRequestData } from "workers/Linting/types";
|
||||
|
||||
export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
|
||||
while (true) {
|
||||
|
|
@ -31,12 +32,21 @@ export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
|
|||
export function* lintTreeActionHandler(message: any) {
|
||||
const { body } = message;
|
||||
const { data } = body;
|
||||
const {
|
||||
asyncJSFunctionsInDataFields,
|
||||
configTree,
|
||||
jsPropertiesState,
|
||||
pathsToLint: lintOrder,
|
||||
unevalTree,
|
||||
} = data as LintTreeSagaRequestData;
|
||||
yield put({
|
||||
type: ReduxActionTypes.LINT_TREE,
|
||||
payload: {
|
||||
pathsToLint: data.lintOrder,
|
||||
unevalTree: data.unevalTree,
|
||||
configTree: data.configTree,
|
||||
pathsToLint: lintOrder,
|
||||
unevalTree,
|
||||
jsPropertiesState,
|
||||
asyncJSFunctionsInDataFields,
|
||||
configTree,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ export function* evaluateTreeSaga(
|
|||
requiresLinting: isEditMode && requiresLinting,
|
||||
forceEvaluation,
|
||||
metaWidgets,
|
||||
appMode,
|
||||
};
|
||||
|
||||
const workerResponse: EvalTreeResponseData = yield call(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ import type {
|
|||
import { LINT_WORKER_ACTIONS } from "workers/Linting/types";
|
||||
import { logLatestLintPropertyErrors } from "./PostLintingSagas";
|
||||
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();
|
||||
|
||||
|
|
@ -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>) {
|
||||
const { configTree, pathsToLint, unevalTree } = action.payload;
|
||||
const {
|
||||
asyncJSFunctionsInDataFields,
|
||||
configTree,
|
||||
jsPropertiesState,
|
||||
pathsToLint,
|
||||
unevalTree,
|
||||
} = action.payload;
|
||||
// only perform lint operations in edit mode
|
||||
const appMode: APP_MODE = yield select(getAppMode);
|
||||
if (appMode !== APP_MODE.EDIT) return;
|
||||
|
|
@ -44,18 +99,32 @@ export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) {
|
|||
const lintTreeRequestData: LintTreeRequest = {
|
||||
pathsToLint,
|
||||
unevalTree,
|
||||
jsPropertiesState,
|
||||
configTree,
|
||||
cloudHosting: !!APPSMITH_CONFIGS.cloudHosting,
|
||||
asyncJSFunctionsInDataFields,
|
||||
};
|
||||
|
||||
const { errors }: LintTreeResponse = yield call(
|
||||
const { errors, updatedJSEntities }: LintTreeResponse = yield call(
|
||||
lintWorker.request,
|
||||
LINT_WORKER_ACTIONS.LINT_TREE,
|
||||
lintTreeRequestData,
|
||||
);
|
||||
|
||||
yield put(setLintingErrors(errors));
|
||||
yield call(logLatestLintPropertyErrors, { errors, dataTree: unevalTree });
|
||||
const oldJSCollectionLintErrors: LintErrorsStore =
|
||||
yield getValidOldJSCollectionLintErrors(
|
||||
updatedJSEntities,
|
||||
errors,
|
||||
jsPropertiesState,
|
||||
);
|
||||
|
||||
const updatedErrors = { ...errors, ...oldJSCollectionLintErrors };
|
||||
|
||||
yield put(setLintingErrors(updatedErrors));
|
||||
yield call(logLatestLintPropertyErrors, {
|
||||
errors,
|
||||
dataTree: unevalTree,
|
||||
});
|
||||
}
|
||||
|
||||
export default function* lintTreeSagaWatcher() {
|
||||
|
|
|
|||
|
|
@ -2,19 +2,19 @@ import { ENTITY_TYPE, Severity } from "entities/AppsmithConsole";
|
|||
import LOG_TYPE from "entities/AppsmithConsole/logtype";
|
||||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import { isEmpty } from "lodash";
|
||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
||||
import AppsmithConsole from "utils/AppsmithConsole";
|
||||
import {
|
||||
getEntityNameAndPropertyPath,
|
||||
isJSAction,
|
||||
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||
|
||||
// We currently only log lint errors in JSObjects
|
||||
export function* logLatestLintPropertyErrors({
|
||||
dataTree,
|
||||
errors,
|
||||
}: {
|
||||
errors: LintErrors;
|
||||
errors: LintErrorsStore;
|
||||
dataTree: DataTree;
|
||||
}) {
|
||||
const errorsToAdd = [];
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import type { AppState } from "@appsmith/reducers";
|
||||
import { get } from "lodash";
|
||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
||||
import type { LintError } from "utils/DynamicBindingUtils";
|
||||
|
||||
export const getAllLintErrors = (state: AppState): LintErrors =>
|
||||
state.linting.errors;
|
||||
|
||||
const emptyLint: LintError[] = [];
|
||||
|
||||
export const getEntityLintErrors = (state: AppState, path?: string) => {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ import { createNavData } from "utils/NavigationSelector/common";
|
|||
import { getWidgetChildrenNavData } from "utils/NavigationSelector/WidgetChildren";
|
||||
import { getJsChildrenNavData } from "utils/NavigationSelector/JsChildren";
|
||||
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 = {
|
||||
name: string;
|
||||
|
|
@ -124,3 +128,20 @@ export const getEntitiesForNavigation = createSelector(
|
|||
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",
|
||||
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 {
|
||||
raw: string;
|
||||
errorMessage: Error;
|
||||
|
|
@ -364,6 +373,7 @@ export interface EvaluationError extends DataTreeError {
|
|||
| PropertyEvaluationErrorType.PARSE
|
||||
| PropertyEvaluationErrorType.VALIDATION;
|
||||
originalBinding?: string;
|
||||
kind?: PropertyEvaluationErrorKind;
|
||||
}
|
||||
|
||||
export interface LintError extends DataTreeError {
|
||||
|
|
@ -374,6 +384,7 @@ export interface LintError extends DataTreeError {
|
|||
code: string;
|
||||
line: number;
|
||||
ch: number;
|
||||
originalPath?: string;
|
||||
}
|
||||
|
||||
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 { EvalErrorTypes } from "utils/DynamicBindingUtils";
|
||||
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 evaluateSync from "workers/Evaluation/evaluate";
|
||||
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||
|
|
@ -16,7 +16,9 @@ import {
|
|||
updateJSCollectionInUnEvalTree,
|
||||
} from "workers/Evaluation/JSObject/utils";
|
||||
import { functionDeterminer } from "../functionDeterminer";
|
||||
import { jsPropertiesState } from "./jsPropertiesState";
|
||||
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
|
||||
|
|
@ -81,94 +83,100 @@ export function saveResolvedFunctionsAndJSUpdates(
|
|||
unEvalDataTree: DataTree,
|
||||
entityName: string,
|
||||
) {
|
||||
jsPropertiesState.delete(entityName);
|
||||
const correctFormat = regex.test(entity.body);
|
||||
if (correctFormat) {
|
||||
const body = entity.body.replace(/export default/g, "");
|
||||
try {
|
||||
delete dataTreeEvalRef.resolvedFunctions[`${entityName}`];
|
||||
delete dataTreeEvalRef.currentJSCollectionState[`${entityName}`];
|
||||
const parseStartTime = performance.now();
|
||||
const parsedObject = parseJSObjectWithAST(body);
|
||||
const { parsedObject, success } = parseJSObject(entity.body);
|
||||
const parseEndTime = performance.now();
|
||||
const JSObjectASTParseTime = parseEndTime - parseStartTime;
|
||||
const JSObjectASTParseTime = getFixedTimeDifference(
|
||||
parseEndTime,
|
||||
parseStartTime,
|
||||
);
|
||||
dataTreeEvalRef.logs.push({
|
||||
JSObjectName: entityName,
|
||||
JSObjectASTParseTime,
|
||||
});
|
||||
const actions: any = [];
|
||||
const variables: any = [];
|
||||
if (!!parsedObject) {
|
||||
parsedObject.forEach((parsedElement) => {
|
||||
if (isTypeOfFunction(parsedElement.type)) {
|
||||
try {
|
||||
const { result } = evaluateSync(
|
||||
parsedElement.value,
|
||||
unEvalDataTree,
|
||||
{},
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
if (!!result) {
|
||||
let params: Array<{ key: string; value: unknown }> = [];
|
||||
if (success) {
|
||||
if (!!parsedObject) {
|
||||
jsPropertiesState.update(entityName, parsedObject);
|
||||
parsedObject.forEach((parsedElement) => {
|
||||
if (isJSFunctionProperty(parsedElement)) {
|
||||
try {
|
||||
const { result } = evaluateSync(
|
||||
parsedElement.value,
|
||||
unEvalDataTree,
|
||||
{},
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
if (!!result) {
|
||||
let params: Array<{ key: string; value: unknown }> = [];
|
||||
|
||||
if (parsedElement.arguments) {
|
||||
params = parsedElement.arguments.map(
|
||||
({ defaultValue, paramName }) => ({
|
||||
key: paramName,
|
||||
value: defaultValue,
|
||||
}),
|
||||
if (parsedElement.arguments) {
|
||||
params = parsedElement.arguments.map(
|
||||
({ defaultValue, paramName }) => ({
|
||||
key: paramName,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
// in case we need to handle error state
|
||||
}
|
||||
} catch {
|
||||
// in case we need to handle error state
|
||||
} else if (parsedElement.type !== "literal") {
|
||||
variables.push({
|
||||
name: parsedElement.key,
|
||||
value: parsedElement.value,
|
||||
});
|
||||
set(
|
||||
dataTreeEvalRef.currentJSCollectionState,
|
||||
`${entityName}.${parsedElement.key}`,
|
||||
parsedElement.value,
|
||||
);
|
||||
}
|
||||
} else if (parsedElement.type !== "literal") {
|
||||
variables.push({
|
||||
name: parsedElement.key,
|
||||
value: parsedElement.value,
|
||||
});
|
||||
set(
|
||||
dataTreeEvalRef.currentJSCollectionState,
|
||||
`${entityName}.${parsedElement.key}`,
|
||||
parsedElement.value,
|
||||
);
|
||||
}
|
||||
});
|
||||
const parsedBody = {
|
||||
body: entity.body,
|
||||
actions: actions,
|
||||
variables,
|
||||
};
|
||||
set(jsUpdates, `${entityName}`, {
|
||||
parsedBody,
|
||||
id: entity.actionId,
|
||||
});
|
||||
} else {
|
||||
set(jsUpdates, `${entityName}`, {
|
||||
parsedBody: undefined,
|
||||
id: entity.actionId,
|
||||
});
|
||||
});
|
||||
const parsedBody = {
|
||||
body: entity.body,
|
||||
actions: actions,
|
||||
variables,
|
||||
};
|
||||
set(jsUpdates, `${entityName}`, {
|
||||
parsedBody,
|
||||
id: entity.actionId,
|
||||
});
|
||||
} else {
|
||||
set(jsUpdates, `${entityName}`, {
|
||||
parsedBody: undefined,
|
||||
id: entity.actionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//if we need to push error as popup in case
|
||||
|
|
@ -194,6 +202,7 @@ export function parseJSActions(
|
|||
differences?: DataTreeDiff[],
|
||||
) {
|
||||
let jsUpdates: Record<string, JSUpdate> = {};
|
||||
jsPropertiesState.startUpdate();
|
||||
if (!!differences && !!oldUnEvalTree) {
|
||||
differences.forEach((diff) => {
|
||||
const { entityName, propertyPath } = getEntityNameAndPropertyPath(
|
||||
|
|
@ -249,7 +258,7 @@ export function parseJSActions(
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
jsPropertiesState.stopUpdate();
|
||||
functionDeterminer.setupEval(
|
||||
unEvalDataTree,
|
||||
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);
|
||||
}
|
||||
//remove functions
|
||||
|
||||
const reactivePaths = entity.reactivePaths;
|
||||
const meta = entity.meta;
|
||||
|
||||
|
|
|
|||
|
|
@ -124,46 +124,46 @@ describe("Test error modifier", () => {
|
|||
errorModifier.updateAsyncFunctions(dataTree);
|
||||
});
|
||||
|
||||
it("TypeError for defined Api in sync field ", () => {
|
||||
it("TypeError for defined Api in data field ", () => {
|
||||
const error = new Error();
|
||||
error.name = "TypeError";
|
||||
error.message = "Api2.run is not a function";
|
||||
const result = errorModifier.run(error);
|
||||
const { errorMessage: result } = errorModifier.run(error);
|
||||
expect(result).toEqual({
|
||||
name: "ValidationError",
|
||||
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();
|
||||
error.name = "TypeError";
|
||||
error.message = "Api1.run is not a function";
|
||||
const result = errorModifier.run(error);
|
||||
const { errorMessage: result } = errorModifier.run(error);
|
||||
expect(result).toEqual({
|
||||
name: "TypeError",
|
||||
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();
|
||||
error.name = "ReferenceError";
|
||||
error.message = "storeValue is not defined";
|
||||
const result = errorModifier.run(error);
|
||||
const { errorMessage: result } = errorModifier.run(error);
|
||||
expect(result).toEqual({
|
||||
name: "ValidationError",
|
||||
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();
|
||||
error.name = "ReferenceError";
|
||||
error.message = "storeValue2 is not defined";
|
||||
const result = errorModifier.run(error);
|
||||
const { errorMessage: result } = errorModifier.run(error);
|
||||
expect(result).toEqual({
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ describe("evaluateSync", () => {
|
|||
message: "wrongJS is not defined",
|
||||
},
|
||||
errorType: "PARSE",
|
||||
kind: undefined,
|
||||
raw: `
|
||||
function $$closedFn () {
|
||||
const $$result = wrongJS
|
||||
|
|
@ -98,6 +99,7 @@ describe("evaluateSync", () => {
|
|||
message: "{}.map is not a function",
|
||||
},
|
||||
errorType: "PARSE",
|
||||
kind: undefined,
|
||||
raw: `
|
||||
function $$closedFn () {
|
||||
const $$result = {}.map()
|
||||
|
|
@ -128,6 +130,7 @@ describe("evaluateSync", () => {
|
|||
message: "setImmediate is not defined",
|
||||
},
|
||||
errorType: "PARSE",
|
||||
kind: undefined,
|
||||
raw: `
|
||||
function $$closedFn () {
|
||||
const $$result = setImmediate(() => {}, 100)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import type { DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
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 =
|
||||
"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 {
|
||||
private errorNamesToScan = ["ReferenceError", "TypeError"];
|
||||
// Note all regex below groups the async function name
|
||||
|
|
@ -14,10 +17,23 @@ class ErrorModifier {
|
|||
this.asyncFunctionsNameMap = getAllAsyncFunctions(dataTree);
|
||||
}
|
||||
|
||||
run(error: Error) {
|
||||
run(error: Error): {
|
||||
errorMessage: ReturnType<typeof getErrorMessage>;
|
||||
errorCategory?: PropertyEvaluationErrorCategory;
|
||||
} {
|
||||
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(
|
||||
this.asyncFunctionsNameMap,
|
||||
|
|
@ -25,27 +41,49 @@ class ErrorModifier {
|
|||
const functionNameWithWhiteSpace = " " + asyncFunctionFullPath + " ";
|
||||
if (getErrorMessageWithType(error).match(functionNameWithWhiteSpace)) {
|
||||
return {
|
||||
name: "ValidationError",
|
||||
message: UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR.replaceAll(
|
||||
"{{actionName}}",
|
||||
asyncFunctionFullPath + "()",
|
||||
),
|
||||
errorMessage: {
|
||||
name: "ValidationError",
|
||||
message: UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR.replaceAll(
|
||||
"{{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();
|
||||
|
||||
const FOUND_PROMISE_IN_SYNC_EVAL_MESSAGE =
|
||||
"Found a Promise() during evaluation. Data fields cannot execute asynchronous code.";
|
||||
|
||||
export class FoundPromiseInSyncEvalError extends Error {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "";
|
||||
this.message =
|
||||
"Found a Promise() during evaluation. Sync fields cannot execute asynchronous code.";
|
||||
this.message = FOUND_PROMISE_IN_SYNC_EVAL_MESSAGE;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +92,7 @@ export class ActionCalledInSyncFieldError extends Error {
|
|||
super(actionName);
|
||||
|
||||
if (!actionName) {
|
||||
this.message = "Async function called in a sync field";
|
||||
this.message = "Async function called in a data field";
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -81,3 +119,10 @@ export const getErrorMessage = (error: Error) => {
|
|||
export const getErrorMessageWithType = (error: Error) => {
|
||||
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",
|
||||
ASYNC_ANONYMOUS_FUNCTION = "ASYNC_ANONYMOUS_FUNCTION",
|
||||
TRIGGERS = "TRIGGERS",
|
||||
OBJECT_PROPERTY = "OBJECT_PROPERTY",
|
||||
}
|
||||
|
||||
export const ScriptTemplate = "<<string>>";
|
||||
|
|
@ -58,6 +59,13 @@ export const EvaluationScripts: Record<EvaluationScriptType, string> = {
|
|||
}
|
||||
$$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) => {
|
||||
|
|
@ -120,8 +128,11 @@ export interface createEvaluationContextArgs {
|
|||
context?: EvaluateContext;
|
||||
isTriggerBased: boolean;
|
||||
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.
|
||||
|
|
@ -135,8 +146,8 @@ export const createEvaluationContext = (args: createEvaluationContextArgs) => {
|
|||
dataTree,
|
||||
evalArguments,
|
||||
isTriggerBased,
|
||||
removeEntityFunctions,
|
||||
resolvedFunctions,
|
||||
skipEntityFunctions,
|
||||
} = args;
|
||||
|
||||
const EVAL_CONTEXT: EvalContext = {};
|
||||
|
|
@ -152,7 +163,7 @@ export const createEvaluationContext = (args: createEvaluationContextArgs) => {
|
|||
addDataTreeToContext({
|
||||
EVAL_CONTEXT,
|
||||
dataTree,
|
||||
skipEntityFunctions: !!skipEntityFunctions,
|
||||
removeEntityFunctions: !!removeEntityFunctions,
|
||||
isTriggerBased,
|
||||
});
|
||||
|
||||
|
|
@ -279,18 +290,23 @@ export default function evaluateSync(
|
|||
result = indirectEval(script);
|
||||
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.
|
||||
* NOTE: Awaiting for promise will make sync field evaluation slower.
|
||||
* 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 data field evaluation slower.
|
||||
*/
|
||||
throw new FoundPromiseInSyncEvalError();
|
||||
}
|
||||
} catch (error) {
|
||||
const { errorCategory, errorMessage } = errorModifier.run(error as Error);
|
||||
errors.push({
|
||||
errorMessage: errorModifier.run(error as Error),
|
||||
errorMessage,
|
||||
severity: Severity.ERROR,
|
||||
raw: script,
|
||||
errorType: PropertyEvaluationErrorType.PARSE,
|
||||
originalBinding: userScript,
|
||||
kind: errorCategory && {
|
||||
category: errorCategory,
|
||||
rootcause: "",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
for (const entityName in evalContext) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import type ReplayEntity from "entities/Replay";
|
||||
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
|
||||
import { isEmpty } from "lodash";
|
||||
import { isEmpty, union } from "lodash";
|
||||
import type { DependencyMap, EvalError } from "utils/DynamicBindingUtils";
|
||||
import { EvalErrorTypes } from "utils/DynamicBindingUtils";
|
||||
import type { JSUpdate } from "utils/JSPaneUtils";
|
||||
|
|
@ -20,6 +20,8 @@ import type {
|
|||
EvalWorkerSyncRequest,
|
||||
} from "../types";
|
||||
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 dataTreeEvaluator: DataTreeEvaluator | undefined;
|
||||
export const CANVAS = "canvas";
|
||||
|
|
@ -44,6 +46,7 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
|
||||
const {
|
||||
allActionValidationConfig,
|
||||
appMode,
|
||||
forceEvaluation,
|
||||
metaWidgets,
|
||||
requiresLinting,
|
||||
|
|
@ -59,6 +62,7 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
try {
|
||||
if (!dataTreeEvaluator) {
|
||||
isCreateFirstTree = true;
|
||||
asyncJsFunctionInDataFields.initialize(appMode);
|
||||
replayMap = replayMap || {};
|
||||
replayMap[CANVAS] = new ReplayCanvas({ widgets, theme });
|
||||
dataTreeEvaluator = new DataTreeEvaluator(
|
||||
|
|
@ -71,17 +75,25 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
configTree,
|
||||
);
|
||||
evalOrder = setupFirstTreeResponse.evalOrder;
|
||||
lintOrder = setupFirstTreeResponse.lintOrder;
|
||||
lintOrder = union(
|
||||
setupFirstTreeResponse.lintOrder,
|
||||
jsPropertiesState.getUpdatedJSProperties(),
|
||||
);
|
||||
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
||||
|
||||
initiateLinting(
|
||||
initiateLinting({
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
unevalTree: makeEntityConfigsAsObjProperties(
|
||||
dataTreeEvaluator.oldUnEvalTree,
|
||||
{
|
||||
sanitizeDataTree: false,
|
||||
},
|
||||
),
|
||||
requiresLinting,
|
||||
dataTreeEvaluator.oldConfigTree,
|
||||
);
|
||||
jsPropertiesState: jsPropertiesState.getMap(),
|
||||
asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
|
||||
configTree: dataTreeEvaluator.oldConfigTree,
|
||||
});
|
||||
|
||||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
||||
|
|
@ -113,17 +125,25 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
);
|
||||
isCreateFirstTree = true;
|
||||
evalOrder = setupFirstTreeResponse.evalOrder;
|
||||
lintOrder = setupFirstTreeResponse.lintOrder;
|
||||
lintOrder = union(
|
||||
setupFirstTreeResponse.lintOrder,
|
||||
jsPropertiesState.getUpdatedJSProperties(),
|
||||
);
|
||||
jsUpdates = setupFirstTreeResponse.jsUpdates;
|
||||
|
||||
initiateLinting(
|
||||
initiateLinting({
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
unevalTree: makeEntityConfigsAsObjProperties(
|
||||
dataTreeEvaluator.oldUnEvalTree,
|
||||
{
|
||||
sanitizeDataTree: false,
|
||||
},
|
||||
),
|
||||
requiresLinting,
|
||||
dataTreeEvaluator.oldConfigTree,
|
||||
);
|
||||
jsPropertiesState: jsPropertiesState.getMap(),
|
||||
asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
|
||||
configTree: dataTreeEvaluator.oldConfigTree,
|
||||
});
|
||||
|
||||
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
|
||||
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
|
||||
|
|
@ -146,20 +166,28 @@ export default function (request: EvalWorkerSyncRequest) {
|
|||
);
|
||||
|
||||
evalOrder = setupUpdateTreeResponse.evalOrder;
|
||||
lintOrder = setupUpdateTreeResponse.lintOrder;
|
||||
lintOrder = union(
|
||||
setupUpdateTreeResponse.lintOrder,
|
||||
jsPropertiesState.getUpdatedJSProperties(),
|
||||
);
|
||||
jsUpdates = setupUpdateTreeResponse.jsUpdates;
|
||||
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
|
||||
pathsToClearErrorsFor = setupUpdateTreeResponse.pathsToClearErrorsFor;
|
||||
isNewWidgetAdded = setupUpdateTreeResponse.isNewWidgetAdded;
|
||||
|
||||
initiateLinting(
|
||||
initiateLinting({
|
||||
lintOrder,
|
||||
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, {
|
||||
sanitizeDataTree: false,
|
||||
}),
|
||||
unevalTree: makeEntityConfigsAsObjProperties(
|
||||
dataTreeEvaluator.oldUnEvalTree,
|
||||
{
|
||||
sanitizeDataTree: false,
|
||||
},
|
||||
),
|
||||
requiresLinting,
|
||||
dataTreeEvaluator.oldConfigTree,
|
||||
);
|
||||
jsPropertiesState: jsPropertiesState.getMap(),
|
||||
asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
|
||||
configTree: dataTreeEvaluator.oldConfigTree,
|
||||
});
|
||||
nonDynamicFieldValidationOrder =
|
||||
setupUpdateTreeResponse.nonDynamicFieldValidationOrder;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type { WidgetTypeConfigMap } from "utils/WidgetFactory";
|
|||
import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator/types";
|
||||
import type { WorkerRequest } from "@appsmith/workers/common/types";
|
||||
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 EvalWorkerASyncRequest = WorkerRequest<
|
||||
|
|
@ -38,6 +39,7 @@ export interface EvalTreeRequestData {
|
|||
requiresLinting: boolean;
|
||||
forceEvaluation: boolean;
|
||||
metaWidgets: MetaWidgetsReduxState;
|
||||
appMode: APP_MODE | undefined;
|
||||
}
|
||||
|
||||
export interface EvalTreeResponseData {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ECMA_VERSION } from "@shared/ast";
|
||||
import type { LintOptions } from "jshint";
|
||||
import { isEntityFunction } from "./utils";
|
||||
|
||||
export const lintOptions = (globalData: Record<string, boolean>) =>
|
||||
({
|
||||
|
|
@ -29,6 +30,8 @@ export const lintOptions = (globalData: Record<string, boolean>) =>
|
|||
} as LintOptions);
|
||||
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_ERROR_CODE =
|
||||
"INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE";
|
||||
// https://github.com/jshint/jshint/blob/d3d84ae1695359aef077ddb143f4be98001343b4/src/messages.js#L204
|
||||
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 = {
|
||||
W098: "'{a}' is defined but never used.",
|
||||
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) {
|
||||
return `Async framework action "${actionName}" cannot be executed in a function that is bound to a sync field.`;
|
||||
export function asyncActionInSyncFieldLintMessage(isJsObject = false) {
|
||||
return isJsObject
|
||||
? `Cannot execute async code on functions bound to data fields`
|
||||
: `Data fields cannot execute async code`;
|
||||
}
|
||||
|
||||
/** These errors should be overlooked
|
||||
|
|
@ -54,7 +61,9 @@ export const SUPPORTED_WEB_APIS = {
|
|||
};
|
||||
export enum CustomLintErrorCode {
|
||||
INVALID_ENTITY_PROPERTY = "INVALID_ENTITY_PROPERTY",
|
||||
ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD = "ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD",
|
||||
}
|
||||
|
||||
export const CUSTOM_LINT_ERRORS: Record<
|
||||
CustomLintErrorCode,
|
||||
(...args: any[]) => string
|
||||
|
|
@ -62,5 +71,26 @@ export const CUSTOM_LINT_ERRORS: Record<
|
|||
[CustomLintErrorCode.INVALID_ENTITY_PROPERTY]: (
|
||||
entityName: 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 {
|
||||
getEntityNameAndPropertyPath,
|
||||
isATriggerPath,
|
||||
isJSAction,
|
||||
} from "ce/workers/Evaluation/evaluationUtils";
|
||||
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import { get, set } from "lodash";
|
||||
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";
|
||||
lintBindingPath,
|
||||
lintJSObjectBody,
|
||||
lintJSObjectProperty,
|
||||
lintTriggerPath,
|
||||
sortLintingPathsByType,
|
||||
} from "./utils";
|
||||
|
||||
export function getlintErrorsFromTree(
|
||||
pathsToLint: string[],
|
||||
unEvalTree: DataTree,
|
||||
configTree: ConfigTree,
|
||||
cloudHosting: boolean,
|
||||
): LintErrors {
|
||||
const lintTreeErrors: LintErrors = {};
|
||||
|
||||
const evalContext = createEvaluationContext({
|
||||
dataTree: unEvalTree,
|
||||
resolvedFunctions: {},
|
||||
isTriggerBased: false,
|
||||
skipEntityFunctions: true,
|
||||
});
|
||||
|
||||
const platformFnNamesMap = Object.values(
|
||||
getActionTriggerFunctionNames(cloudHosting),
|
||||
).reduce(
|
||||
(acc, name) => ({ ...acc, [name]: true }),
|
||||
{} as { [x: string]: boolean },
|
||||
export function getlintErrorsFromTree({
|
||||
asyncJSFunctionsInDataFields,
|
||||
cloudHosting,
|
||||
configTree,
|
||||
jsPropertiesState,
|
||||
pathsToLint,
|
||||
unEvalTree,
|
||||
}: getlintErrorsFromTreeProps): getlintErrorsFromTreeResponse {
|
||||
const lintTreeErrors: LintErrorsStore = {};
|
||||
const updatedJSEntities = new Set<string>();
|
||||
globalData.initialize(unEvalTree, cloudHosting);
|
||||
const { bindingPaths, jsObjectPaths, triggerPaths } = sortLintingPathsByType(
|
||||
pathsToLint,
|
||||
unEvalTree,
|
||||
configTree,
|
||||
);
|
||||
Object.assign(evalContext, platformFnNamesMap);
|
||||
|
||||
const evalContextWithoutFunctions = createEvaluationContext({
|
||||
dataTree: unEvalTree,
|
||||
resolvedFunctions: {},
|
||||
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);
|
||||
// Lint binding paths
|
||||
bindingPaths.forEach((bindingPath) => {
|
||||
const { entityName } = getEntityNameAndPropertyPath(bindingPath);
|
||||
const entity = unEvalTree[entityName];
|
||||
const entityConfig = configTree[entityName];
|
||||
const unEvalPropertyValue = get(
|
||||
unEvalTree,
|
||||
fullPropertyPath,
|
||||
bindingPath,
|
||||
) 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({
|
||||
entity,
|
||||
fullPropertyPath,
|
||||
globalData: evalContextWithoutFunctions,
|
||||
dynamicBinding: unEvalPropertyValue,
|
||||
entity,
|
||||
fullPropertyPath: bindingPath,
|
||||
globalData: globalData.getGlobalData(false),
|
||||
});
|
||||
set(lintTreeErrors, `["${fullPropertyPath}"]`, lintErrors);
|
||||
set(lintTreeErrors, `["${bindingPath}"]`, lintErrors);
|
||||
});
|
||||
|
||||
if (triggerPaths.size || bindingPathsRequiringFunctions.size) {
|
||||
// we only create GLOBAL_DATA_WITH_FUNCTIONS if there are paths requiring it
|
||||
// In trigger based fields, functions such as showAlert, storeValue, etc need to be added to the global data
|
||||
// Lint TriggerPaths
|
||||
triggerPaths.forEach((triggerPath) => {
|
||||
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
|
||||
if (bindingPathsRequiringFunctions.size) {
|
||||
bindingPathsRequiringFunctions.forEach((fullPropertyPath) => {
|
||||
const { entityName } = getEntityNameAndPropertyPath(fullPropertyPath);
|
||||
const entity = unEvalTree[entityName];
|
||||
const unEvalPropertyValue = get(
|
||||
unEvalTree,
|
||||
fullPropertyPath,
|
||||
) as unknown as string;
|
||||
// remove all lint errors from path
|
||||
set(lintTreeErrors, `["${fullPropertyPath}"]`, []);
|
||||
const lintErrors = lintBindingPath({
|
||||
dynamicBinding: unEvalPropertyValue,
|
||||
entity,
|
||||
fullPropertyPath,
|
||||
globalData: evalContext,
|
||||
});
|
||||
set(lintTreeErrors, `["${fullPropertyPath}"]`, lintErrors);
|
||||
});
|
||||
}
|
||||
|
||||
// Lint triggerPaths
|
||||
if (triggerPaths.size) {
|
||||
triggerPaths.forEach((triggerPath) => {
|
||||
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({
|
||||
globalData: evalContext,
|
||||
userScript: unEvalPropertyValue,
|
||||
entity,
|
||||
fullPropertyPath: triggerPath,
|
||||
});
|
||||
set(lintTreeErrors, `["${triggerPath}"]`, lintErrors);
|
||||
});
|
||||
}
|
||||
// Lint jsobject paths
|
||||
if (jsObjectPaths.size) {
|
||||
jsObjectPaths.forEach((jsObjectPath) => {
|
||||
const { entityName: jsObjectName } =
|
||||
getEntityNameAndPropertyPath(jsObjectPath);
|
||||
const jsObjectState = get(jsPropertiesState, jsObjectName);
|
||||
const jsObjectBodyPath = `["${jsObjectName}.body"]`;
|
||||
updatedJSEntities.add(jsObjectName);
|
||||
// An empty state shows that there is a parse error in the jsObject or the object is empty, so we lint the entire body
|
||||
// instead of an individual properties
|
||||
if (isEmpty(jsObjectState)) {
|
||||
const jsObjectBodyLintErrors = lintJSObjectBody(
|
||||
jsObjectName,
|
||||
globalData.getGlobalData(true),
|
||||
);
|
||||
set(lintTreeErrors, jsObjectBodyPath, jsObjectBodyLintErrors);
|
||||
} else if (jsObjectPath !== "body") {
|
||||
const propertyLintErrors = lintJSObjectProperty(
|
||||
jsObjectPath,
|
||||
jsObjectState,
|
||||
asyncJSFunctionsInDataFields,
|
||||
);
|
||||
const currentLintErrorsInBody = get(
|
||||
lintTreeErrors,
|
||||
jsObjectBodyPath,
|
||||
[] as LintError[],
|
||||
);
|
||||
const updatedLintErrors = [
|
||||
...currentLintErrorsInBody,
|
||||
...propertyLintErrors,
|
||||
];
|
||||
set(lintTreeErrors, jsObjectBodyPath, updatedLintErrors);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return lintTreeErrors;
|
||||
return {
|
||||
errors: lintTreeErrors,
|
||||
updatedJSEntities: Array.from(updatedJSEntities),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,18 +64,32 @@ function eventRequestHandler({
|
|||
}): LintTreeResponse | unknown {
|
||||
switch (method) {
|
||||
case LINT_WORKER_ACTIONS.LINT_TREE: {
|
||||
const lintTreeResponse: LintTreeResponse = { errors: {} };
|
||||
const lintTreeResponse: LintTreeResponse = {
|
||||
errors: {},
|
||||
updatedJSEntities: [],
|
||||
};
|
||||
try {
|
||||
const { cloudHosting, configTree, pathsToLint, unevalTree } =
|
||||
requestData as LintTreeRequest;
|
||||
const lintErrors = getlintErrorsFromTree(
|
||||
pathsToLint,
|
||||
unevalTree,
|
||||
configTree,
|
||||
const {
|
||||
asyncJSFunctionsInDataFields,
|
||||
cloudHosting,
|
||||
configTree,
|
||||
jsPropertiesState,
|
||||
pathsToLint,
|
||||
unevalTree: unEvalTree,
|
||||
} = requestData as LintTreeRequest;
|
||||
const { errors: lintErrors, updatedJSEntities } = getlintErrorsFromTree(
|
||||
{
|
||||
pathsToLint,
|
||||
unEvalTree,
|
||||
jsPropertiesState,
|
||||
cloudHosting,
|
||||
asyncJSFunctionsInDataFields,
|
||||
configTree,
|
||||
},
|
||||
);
|
||||
|
||||
lintTreeResponse.errors = lintErrors;
|
||||
lintTreeResponse.updatedJSEntities = updatedJSEntities;
|
||||
} catch (e) {}
|
||||
return lintTreeResponse;
|
||||
}
|
||||
|
|
@ -97,6 +111,7 @@ function eventRequestHandler({
|
|||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
default: {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Action not registered on lintWorker ", method);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
|
||||
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
|
||||
import type {
|
||||
ConfigTree,
|
||||
DataTree,
|
||||
DataTreeEntity,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
|
||||
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 {
|
||||
LINT_TREE = "LINT_TREE",
|
||||
|
|
@ -8,14 +18,17 @@ export enum LINT_WORKER_ACTIONS {
|
|||
}
|
||||
|
||||
export interface LintTreeResponse {
|
||||
errors: LintErrors;
|
||||
errors: LintErrorsStore;
|
||||
updatedJSEntities: string[];
|
||||
}
|
||||
|
||||
export interface LintTreeRequest {
|
||||
pathsToLint: string[];
|
||||
unevalTree: DataTree;
|
||||
jsPropertiesState: TJSPropertiesState;
|
||||
configTree: ConfigTree;
|
||||
cloudHosting: boolean;
|
||||
asyncJSFunctionsInDataFields: DependencyMap;
|
||||
}
|
||||
|
||||
export type LintWorkerRequest = WorkerRequest<
|
||||
|
|
@ -26,5 +39,54 @@ export type LintWorkerRequest = WorkerRequest<
|
|||
export type LintTreeSagaRequestData = {
|
||||
pathsToLint: string[];
|
||||
unevalTree: DataTree;
|
||||
jsPropertiesState: TJSPropertiesState;
|
||||
asyncJSFunctionsInDataFields: DependencyMap;
|
||||
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 {
|
||||
ConfigTree,
|
||||
DataTree,
|
||||
DataTreeEntity,
|
||||
DataTreeEntityConfig,
|
||||
ConfigTree,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
|
||||
import type { Position } from "codemirror";
|
||||
import type { LintError } from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
isDynamicValue,
|
||||
isPathADynamicBinding,
|
||||
PropertyEvaluationErrorType,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import type { DependencyMap } from "utils/DynamicBindingUtils";
|
||||
import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
|
||||
import type { LintError as JSHintError } 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 {
|
||||
extractInvalidTopLevelMemberExpressionsFromCode,
|
||||
isLiteralNode,
|
||||
} from "@shared/ast";
|
||||
import { getDynamicBindings } from "utils/DynamicBindingUtils";
|
||||
|
||||
import type { createEvaluationContext } from "workers/Evaluation/evaluate";
|
||||
import {
|
||||
getDynamicBindings,
|
||||
PropertyEvaluationErrorType,
|
||||
} from "utils/DynamicBindingUtils";
|
||||
import {
|
||||
createEvaluationContext,
|
||||
EvaluationScripts,
|
||||
EvaluationScriptType,
|
||||
getScriptToEval,
|
||||
|
|
@ -33,13 +30,11 @@ import {
|
|||
} from "workers/Evaluation/evaluate";
|
||||
import {
|
||||
getEntityNameAndPropertyPath,
|
||||
isAction,
|
||||
isATriggerPath,
|
||||
isDataTreeEntity,
|
||||
isDynamicLeaf,
|
||||
isJSAction,
|
||||
isWidget,
|
||||
} 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 {
|
||||
asyncActionInSyncFieldLintMessage,
|
||||
|
|
@ -48,19 +43,33 @@ import {
|
|||
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
|
||||
IGNORED_LINT_ERRORS,
|
||||
INVALID_JSOBJECT_START_STATEMENT,
|
||||
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
|
||||
JS_OBJECT_START_STATEMENT,
|
||||
lintOptions,
|
||||
SUPPORTED_WEB_APIS,
|
||||
WARNING_LINT_ERRORS,
|
||||
} from "./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({
|
||||
dynamicBinding,
|
||||
entity,
|
||||
|
|
@ -68,30 +77,6 @@ export function lintBindingPath({
|
|||
globalData,
|
||||
}: lintBindingPathProps) {
|
||||
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);
|
||||
// Get the {{binding}} bound values
|
||||
const { jsSnippets, stringSegments } = getDynamicBindings(
|
||||
|
|
@ -116,8 +101,6 @@ export function lintBindingPath({
|
|||
data: globalData,
|
||||
originalBinding,
|
||||
scriptType,
|
||||
entity,
|
||||
fullPropertyPath,
|
||||
});
|
||||
lintErrors = lintErrors.concat(lintErrorsFromSnippet);
|
||||
}
|
||||
|
|
@ -125,15 +108,9 @@ export function lintBindingPath({
|
|||
}
|
||||
return lintErrors;
|
||||
}
|
||||
interface lintTriggerPathProps {
|
||||
userScript: string;
|
||||
entity: DataTreeEntity;
|
||||
globalData: ReturnType<typeof createEvaluationContext>;
|
||||
fullPropertyPath: string;
|
||||
}
|
||||
|
||||
export function lintTriggerPath({
|
||||
entity,
|
||||
fullPropertyPath,
|
||||
globalData,
|
||||
userScript,
|
||||
}: lintTriggerPathProps) {
|
||||
|
|
@ -145,35 +122,9 @@ export function lintTriggerPath({
|
|||
data: globalData,
|
||||
originalBinding: jsSnippets[0],
|
||||
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
|
||||
export function getJSToLint(
|
||||
entity: DataTreeEntity,
|
||||
|
|
@ -278,19 +229,26 @@ function sanitizeJSHintErrors(
|
|||
return result;
|
||||
}, []);
|
||||
}
|
||||
const getLintSeverity = (code: string): Severity.WARNING | Severity.ERROR => {
|
||||
const getLintSeverity = (
|
||||
code: string,
|
||||
errorMessage: string,
|
||||
): Severity.WARNING | Severity.ERROR => {
|
||||
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;
|
||||
};
|
||||
const getLintErrorMessage = (
|
||||
reason: string,
|
||||
code: string,
|
||||
variables: string[],
|
||||
isJSObject = false,
|
||||
): string => {
|
||||
switch (code) {
|
||||
case IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE: {
|
||||
return getRefinedW117Error(variables[0], reason);
|
||||
return getRefinedW117Error(variables[0], reason, isJSObject);
|
||||
}
|
||||
default: {
|
||||
return reason;
|
||||
|
|
@ -302,6 +260,7 @@ function convertJsHintErrorToAppsmithLintError(
|
|||
script: string,
|
||||
originalBinding: string,
|
||||
scriptPos: Position,
|
||||
isJSObject = false,
|
||||
): LintError {
|
||||
const { a, b, c, code, d, evidence, reason } = jsHintError;
|
||||
|
||||
|
|
@ -311,14 +270,20 @@ function convertJsHintErrorToAppsmithLintError(
|
|||
jsHintError.line === scriptPos.line
|
||||
? jsHintError.character - scriptPos.ch
|
||||
: jsHintError.character;
|
||||
const lintErrorMessage = getLintErrorMessage(
|
||||
reason,
|
||||
code,
|
||||
[a, b, c, d],
|
||||
isJSObject,
|
||||
);
|
||||
|
||||
return {
|
||||
errorType: PropertyEvaluationErrorType.LINT,
|
||||
raw: script,
|
||||
severity: getLintSeverity(code),
|
||||
severity: getLintSeverity(code, lintErrorMessage),
|
||||
errorMessage: {
|
||||
name: "LintingError",
|
||||
message: getLintErrorMessage(reason, code, [a, b, c, d]),
|
||||
message: lintErrorMessage,
|
||||
},
|
||||
errorSegment: evidence,
|
||||
originalBinding,
|
||||
|
|
@ -329,17 +294,10 @@ function convertJsHintErrorToAppsmithLintError(
|
|||
ch: actualErrorCh,
|
||||
};
|
||||
}
|
||||
interface getLintingErrorsProps {
|
||||
script: string;
|
||||
data: Record<string, unknown>;
|
||||
// {{user's code}}
|
||||
originalBinding: string;
|
||||
scriptType: EvaluationScriptType;
|
||||
entity: DataTreeEntity;
|
||||
fullPropertyPath: string;
|
||||
}
|
||||
|
||||
export function getLintingErrors({
|
||||
data,
|
||||
options,
|
||||
originalBinding,
|
||||
script,
|
||||
scriptType,
|
||||
|
|
@ -356,6 +314,7 @@ export function getLintingErrors({
|
|||
script,
|
||||
originalBinding,
|
||||
scriptPos,
|
||||
options?.isJsObject,
|
||||
),
|
||||
);
|
||||
const invalidPropertyErrors = getInvalidPropertyErrorsFromScript(
|
||||
|
|
@ -363,6 +322,7 @@ export function getLintingErrors({
|
|||
data,
|
||||
scriptPos,
|
||||
originalBinding,
|
||||
options?.isJsObject,
|
||||
);
|
||||
return jshintErrors.concat(invalidPropertyErrors);
|
||||
}
|
||||
|
|
@ -373,6 +333,7 @@ function getInvalidPropertyErrorsFromScript(
|
|||
data: Record<string, unknown>,
|
||||
scriptPos: Position,
|
||||
originalBinding: string,
|
||||
isJSObject = false,
|
||||
): LintError[] {
|
||||
let invalidTopLevelMemberExpressions: MemberExpressionData[] = [];
|
||||
try {
|
||||
|
|
@ -394,15 +355,19 @@ function getInvalidPropertyErrorsFromScript(
|
|||
const propertyStartColumn = !isLiteralNode(property)
|
||||
? property.loc.start.column + 1
|
||||
: property.loc.start.column + 2;
|
||||
const lintErrorMessage = CUSTOM_LINT_ERRORS[
|
||||
CustomLintErrorCode.INVALID_ENTITY_PROPERTY
|
||||
](object.name, propertyName, data[object.name], isJSObject);
|
||||
return {
|
||||
errorType: PropertyEvaluationErrorType.LINT,
|
||||
raw: script,
|
||||
severity: getLintSeverity(CustomLintErrorCode.INVALID_ENTITY_PROPERTY),
|
||||
severity: getLintSeverity(
|
||||
CustomLintErrorCode.INVALID_ENTITY_PROPERTY,
|
||||
lintErrorMessage,
|
||||
),
|
||||
errorMessage: {
|
||||
name: "LintingError",
|
||||
message: CUSTOM_LINT_ERRORS[
|
||||
CustomLintErrorCode.INVALID_ENTITY_PROPERTY
|
||||
](object.name, propertyName),
|
||||
message: lintErrorMessage,
|
||||
},
|
||||
errorSegment: `${object.name}.${propertyName}`,
|
||||
originalBinding,
|
||||
|
|
@ -419,19 +384,24 @@ function getInvalidPropertyErrorsFromScript(
|
|||
return invalidPropertyErrors;
|
||||
}
|
||||
|
||||
export function initiateLinting(
|
||||
lintOrder: string[],
|
||||
unevalTree: DataTree,
|
||||
requiresLinting: boolean,
|
||||
configTree: ConfigTree,
|
||||
) {
|
||||
export function initiateLinting({
|
||||
asyncJSFunctionsInDataFields,
|
||||
configTree,
|
||||
jsPropertiesState,
|
||||
lintOrder,
|
||||
requiresLinting,
|
||||
unevalTree,
|
||||
}: initiateLintingProps) {
|
||||
const data = {
|
||||
pathsToLint: lintOrder,
|
||||
unevalTree,
|
||||
jsPropertiesState,
|
||||
asyncJSFunctionsInDataFields,
|
||||
configTree,
|
||||
} as LintTreeSagaRequestData;
|
||||
if (!requiresLinting) return;
|
||||
WorkerMessenger.ping({
|
||||
data: {
|
||||
lintOrder,
|
||||
unevalTree,
|
||||
configTree,
|
||||
},
|
||||
data,
|
||||
method: MAIN_THREAD_ACTION.LINT_TREE,
|
||||
});
|
||||
}
|
||||
|
|
@ -439,14 +409,227 @@ export function initiateLinting(
|
|||
export function getRefinedW117Error(
|
||||
undefinedVar: string,
|
||||
originalReason: string,
|
||||
isJsObject = false,
|
||||
) {
|
||||
// Refine error message for await using in field not marked as async
|
||||
if (undefinedVar === "await") {
|
||||
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)) {
|
||||
return asyncActionInSyncFieldLintMessage(undefinedVar);
|
||||
return asyncActionInSyncFieldLintMessage(isJsObject);
|
||||
}
|
||||
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,
|
||||
parseJSActions,
|
||||
} from "workers/Evaluation/JSObject";
|
||||
import { getFixedTimeDifference } from "./utils";
|
||||
import {
|
||||
addRootcauseToAsyncInvocationErrors,
|
||||
getFixedTimeDifference,
|
||||
} from "./utils";
|
||||
import { isJSObjectFunction } from "workers/Evaluation/JSObject/utils";
|
||||
import {
|
||||
getValidatedTree,
|
||||
|
|
@ -1063,7 +1066,7 @@ export default class DataTreeEvaluator {
|
|||
});
|
||||
}
|
||||
|
||||
const result = this.evaluateDynamicBoundValue(
|
||||
const { errors: evalErrors, result } = this.evaluateDynamicBoundValue(
|
||||
toBeSentForEval,
|
||||
data,
|
||||
resolvedFunctions,
|
||||
|
|
@ -1071,16 +1074,20 @@ export default class DataTreeEvaluator {
|
|||
contextData,
|
||||
callBackData,
|
||||
);
|
||||
if (fullPropertyPath && result.errors.length) {
|
||||
if (fullPropertyPath && evalErrors.length) {
|
||||
addErrorToEntityProperty({
|
||||
errors: result.errors,
|
||||
errors: addRootcauseToAsyncInvocationErrors(
|
||||
fullPropertyPath,
|
||||
configTree,
|
||||
evalErrors,
|
||||
),
|
||||
evalProps: this.evalProps,
|
||||
fullPropertyPath,
|
||||
dataTree: data,
|
||||
configTree,
|
||||
});
|
||||
}
|
||||
return result.result;
|
||||
return result;
|
||||
} else {
|
||||
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) {
|
||||
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,
|
||||
} from "./utils";
|
||||
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 {
|
||||
dependencyMap: DependencyMap;
|
||||
|
|
@ -100,11 +102,18 @@ export function createDependencyMap(
|
|||
const { errors, invalidReferences, validReferences } =
|
||||
extractInfoFromBindings(dependencyMap[key], dataTreeEvalRef.allKeys);
|
||||
dependencyMap[key] = validReferences;
|
||||
// To keep invalidReferencesMap as minimal as possible, only paths with invalid references
|
||||
// are stored.
|
||||
if (invalidReferences.length) {
|
||||
invalidReferencesMap[key] = invalidReferences;
|
||||
}
|
||||
|
||||
updateMap(invalidReferencesMap, key, invalidReferences, {
|
||||
deleteOnEmpty: true,
|
||||
replaceValue: true,
|
||||
});
|
||||
|
||||
asyncJsFunctionInDataFields.update(
|
||||
key,
|
||||
validReferences,
|
||||
unEvalTree,
|
||||
configTree,
|
||||
);
|
||||
errors.forEach((error) => {
|
||||
dataTreeEvalRef.errors.push(error);
|
||||
});
|
||||
|
|
@ -169,11 +178,12 @@ export const updateDependencyMap = ({
|
|||
let didUpdateValidationDependencyMap = false;
|
||||
const dependenciesOfRemovedPaths: Array<string> = [];
|
||||
const removedPaths: Array<string> = [];
|
||||
const extraPathsToLint = new Set<string>();
|
||||
let extraPathsToLint: string[] = [];
|
||||
const pathsToClearErrorsFor: any[] = [];
|
||||
const {
|
||||
dependencyMap,
|
||||
invalidReferencesMap,
|
||||
inverseDependencyMap,
|
||||
oldConfigTree,
|
||||
oldUnEvalTree,
|
||||
triggerFieldDependencyMap,
|
||||
|
|
@ -207,7 +217,7 @@ export const updateDependencyMap = ({
|
|||
if (entityType !== "noop") {
|
||||
switch (event) {
|
||||
case DataTreeDiffEvent.NEW: {
|
||||
if (isWidget(entity) || isAction(entity) || isJSAction(entity)) {
|
||||
if (isWidgetActionOrJsObject(entity)) {
|
||||
if (!isDynamicLeaf(unEvalDataTree, fullPropertyPath, configTree)) {
|
||||
const entityDependencyMap: DependencyMap = listEntityDependencies(
|
||||
entity,
|
||||
|
|
@ -236,6 +246,19 @@ export const updateDependencyMap = ({
|
|||
invalidReferences,
|
||||
{ deleteOnEmpty: true, replaceValue: true },
|
||||
);
|
||||
// Update asyncJSFunctionsInDatafieldsMap
|
||||
const updatedAsyncJSFunctions =
|
||||
asyncJsFunctionInDataFields.update(
|
||||
entityDependent,
|
||||
validReferences,
|
||||
unEvalDataTree,
|
||||
configTree,
|
||||
);
|
||||
|
||||
extraPathsToLint = extraPathsToLint.concat(
|
||||
updatedAsyncJSFunctions,
|
||||
);
|
||||
|
||||
dataTreeEvalErrors = dataTreeEvalErrors.concat(
|
||||
extractDependencyErrors,
|
||||
);
|
||||
|
|
@ -367,7 +390,7 @@ export const updateDependencyMap = ({
|
|||
if (isChildPropertyPath(fullPropertyPath, invalidReference)) {
|
||||
updateMap(newlyValidReferencesMap, invalidReference, [path]);
|
||||
if (!dependencyMap[invalidReference]) {
|
||||
extraPathsToLint.add(path);
|
||||
extraPathsToLint.push(path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -403,6 +426,17 @@ export const updateDependencyMap = ({
|
|||
fullPath,
|
||||
validReferences,
|
||||
);
|
||||
// Update asyncJSMap
|
||||
const updatedAsyncJSFunctions =
|
||||
asyncJsFunctionInDataFields.update(
|
||||
fullPath,
|
||||
validReferences,
|
||||
unEvalDataTree,
|
||||
configTree,
|
||||
);
|
||||
extraPathsToLint = extraPathsToLint.concat(
|
||||
updatedAsyncJSFunctions,
|
||||
);
|
||||
|
||||
// Since the previously invalid reference has become valid,
|
||||
// remove it from the invalidReferencesMap
|
||||
|
|
@ -434,7 +468,7 @@ export const updateDependencyMap = ({
|
|||
if (
|
||||
isChildPropertyPath(fullPropertyPath, triggerPathDependency)
|
||||
) {
|
||||
extraPathsToLint.add(triggerPath);
|
||||
extraPathsToLint.push(triggerPath);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -463,7 +497,7 @@ export const updateDependencyMap = ({
|
|||
}
|
||||
|
||||
if (
|
||||
(isWidget(entity) || isAction(entity) || isJSAction(entity)) &&
|
||||
isWidgetActionOrJsObject(entity) &&
|
||||
fullPropertyPath === entityName
|
||||
) {
|
||||
const entityDependencies = listEntityDependencies(
|
||||
|
|
@ -502,6 +536,7 @@ export const updateDependencyMap = ({
|
|||
didUpdateValidationDependencyMap = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Either an existing entity or an existing property path has been deleted. Update the global dependency map
|
||||
// by removing the bindings from the same.
|
||||
Object.keys(dependencyMap).forEach((dependencyPath) => {
|
||||
|
|
@ -532,7 +567,7 @@ export const updateDependencyMap = ({
|
|||
if (
|
||||
isChildPropertyPath(fullPropertyPath, invalidReference)
|
||||
) {
|
||||
extraPathsToLint.add(dependencyPath);
|
||||
extraPathsToLint.push(dependencyPath);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -571,7 +606,7 @@ export const updateDependencyMap = ({
|
|||
if (
|
||||
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;
|
||||
}
|
||||
case DataTreeDiffEvent.EDIT: {
|
||||
// We only care if the difference is in dynamic bindings since static values do not need
|
||||
// an evaluation.
|
||||
if (
|
||||
(isWidget(entity) || isAction(entity) || isJSAction(entity)) &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
if (isWidgetActionOrJsObject(entity) && typeof value === "string") {
|
||||
const entity: ActionEntity | WidgetEntity | JSActionEntity =
|
||||
unEvalDataTree[entityName] as
|
||||
| ActionEntity
|
||||
|
|
@ -623,7 +664,18 @@ export const updateDependencyMap = ({
|
|||
dataTreeEvalErrors = dataTreeEvalErrors.concat(
|
||||
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
|
||||
// dependencies for this property path with the newly found dependencies
|
||||
|
||||
|
|
@ -673,6 +725,18 @@ export const updateDependencyMap = ({
|
|||
didUpdateDependencyMap = true;
|
||||
delete dependencyMap[fullPropertyPath];
|
||||
delete invalidReferencesMap[fullPropertyPath];
|
||||
// update asyncFunctionInSyncfieldsMap
|
||||
const updatedAsyncJSFunctions =
|
||||
asyncJsFunctionInDataFields.handlePathEdit(
|
||||
fullPropertyPath,
|
||||
[],
|
||||
unEvalDataTree,
|
||||
inverseDependencyMap,
|
||||
configTree,
|
||||
);
|
||||
extraPathsToLint = extraPathsToLint.concat(
|
||||
updatedAsyncJSFunctions,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
|
|
@ -783,6 +847,6 @@ export const updateDependencyMap = ({
|
|||
pathsToClearErrorsFor,
|
||||
dependenciesOfRemovedPaths,
|
||||
removedPaths,
|
||||
extraPathsToLint: Array.from(extraPathsToLint),
|
||||
extraPathsToLint: uniq(extraPathsToLint),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@ import {
|
|||
getEntityNameAndPropertyPath,
|
||||
isAction,
|
||||
isJSAction,
|
||||
isJSActionConfig,
|
||||
isWidget,
|
||||
} from "@appsmith/workers/Evaluation/evaluationUtils";
|
||||
|
||||
import type {
|
||||
ConfigTree,
|
||||
DataTree,
|
||||
ConfigTree,
|
||||
DataTreeEntity,
|
||||
DataTreeEntityConfig,
|
||||
WidgetEntity,
|
||||
|
|
@ -427,3 +428,24 @@ export function updateMap(
|
|||
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"
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz"
|
||||
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
||||
|
|
@ -7363,11 +7363,6 @@ astral-regex@^2.0.0:
|
|||
version "2.0.0"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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";
|
||||
|
||||
// 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
|
||||
export type {
|
||||
|
|
@ -29,6 +34,8 @@ export type {
|
|||
PropertyNode,
|
||||
MemberExpressionData,
|
||||
IdentifierInfo,
|
||||
TParsedJSProperty,
|
||||
JSPropertyPosition,
|
||||
};
|
||||
|
||||
export {
|
||||
|
|
@ -44,8 +51,9 @@ export {
|
|||
extractInvalidTopLevelMemberExpressionsFromCode,
|
||||
getFunctionalParamsFromNode,
|
||||
isTypeOfFunction,
|
||||
parseJSObjectWithAST,
|
||||
parseJSObject,
|
||||
ECMA_VERSION,
|
||||
SourceType,
|
||||
NodeTypes,
|
||||
isJSFunctionProperty,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@
|
|||
"link-package": "yarn install && rollup -c && cd build && cp -R ../node_modules ./node_modules && yarn link"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0",
|
||||
"acorn": "^8.8.0",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"astring": "^1.7.5",
|
||||
"escodegen": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"rollup": "^2.77.0",
|
||||
"typescript": "4.5.5",
|
||||
|
|
@ -31,6 +33,7 @@
|
|||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@rollup/plugin-commonjs": "^22.0.0",
|
||||
"@types/escodegen": "^0.0.7",
|
||||
"@types/jest": "29.0.3",
|
||||
"@types/lodash": "^4.14.120",
|
||||
"@typescript-eslint/eslint-plugin": "^5.25.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { parseJSObject } from "../index";
|
||||
import { extractIdentifierInfoFromCode } from "../src/index";
|
||||
import { parseJSObjectWithAST } from "../src/jsObject";
|
||||
|
||||
describe("getAllIdentifiers", () => {
|
||||
it("works properly", () => {
|
||||
|
|
@ -306,7 +306,7 @@ describe("getAllIdentifiers", () => {
|
|||
const { references } = extractIdentifierInfoFromCode(
|
||||
perCase.script,
|
||||
2,
|
||||
perCase.invalidIdentifiers
|
||||
perCase.invalidIdentifiers,
|
||||
);
|
||||
expect(references).toStrictEqual(perCase.expectedResults);
|
||||
});
|
||||
|
|
@ -315,7 +315,7 @@ describe("getAllIdentifiers", () => {
|
|||
|
||||
describe("parseJSObjectWithAST", () => {
|
||||
it("parse js object", () => {
|
||||
const body = `{
|
||||
const body = `export default{
|
||||
myVar1: [],
|
||||
myVar2: {},
|
||||
myFun1: () => {
|
||||
|
|
@ -325,36 +325,84 @@ describe("parseJSObjectWithAST", () => {
|
|||
//use async-await or promises
|
||||
}
|
||||
}`;
|
||||
const parsedObject = [
|
||||
|
||||
const expectedParsedObject = [
|
||||
{
|
||||
key: "myVar1",
|
||||
value: "[]",
|
||||
rawContent: "myVar1: []",
|
||||
type: "ArrayExpression",
|
||||
position: {
|
||||
startLine: 2,
|
||||
startColumn: 1,
|
||||
endLine: 2,
|
||||
endColumn: 11,
|
||||
keyStartLine: 2,
|
||||
keyEndLine: 2,
|
||||
keyStartColumn: 1,
|
||||
keyEndColumn: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "myVar2",
|
||||
value: "{}",
|
||||
rawContent: "myVar2: {}",
|
||||
type: "ObjectExpression",
|
||||
position: {
|
||||
startLine: 3,
|
||||
startColumn: 1,
|
||||
endLine: 3,
|
||||
endColumn: 11,
|
||||
keyStartLine: 3,
|
||||
keyEndLine: 3,
|
||||
keyStartColumn: 1,
|
||||
keyEndColumn: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "myFun1",
|
||||
value: "() => {}",
|
||||
rawContent: "myFun1: () => {\n\t\t//write code here\n\t}",
|
||||
type: "ArrowFunctionExpression",
|
||||
position: {
|
||||
startLine: 4,
|
||||
startColumn: 1,
|
||||
endLine: 6,
|
||||
endColumn: 2,
|
||||
keyStartLine: 4,
|
||||
keyEndLine: 4,
|
||||
keyStartColumn: 1,
|
||||
keyEndColumn: 7,
|
||||
},
|
||||
arguments: [],
|
||||
isMarkedAsync: false
|
||||
},
|
||||
{
|
||||
key: "myFun2",
|
||||
value: "async () => {}",
|
||||
rawContent:
|
||||
"myFun2: async () => {\n\t\t//use async-await or promises\n\t}",
|
||||
type: "ArrowFunctionExpression",
|
||||
position: {
|
||||
startLine: 7,
|
||||
startColumn: 1,
|
||||
endLine: 9,
|
||||
endColumn: 2,
|
||||
keyStartLine: 7,
|
||||
keyEndLine: 7,
|
||||
keyStartColumn: 1,
|
||||
keyEndColumn: 7,
|
||||
},
|
||||
arguments: [],
|
||||
isMarkedAsync: true,
|
||||
},
|
||||
];
|
||||
const resultParsedObject = parseJSObjectWithAST(body);
|
||||
expect(resultParsedObject).toStrictEqual(parsedObject);
|
||||
const { parsedObject } = parseJSObject(body);
|
||||
expect(parsedObject).toStrictEqual(expectedParsedObject);
|
||||
});
|
||||
|
||||
it("parse js object with literal", () => {
|
||||
const body = `{
|
||||
const body = `export default{
|
||||
myVar1: [],
|
||||
myVar2: {
|
||||
"a": "app",
|
||||
|
|
@ -366,36 +414,83 @@ describe("parseJSObjectWithAST", () => {
|
|||
//use async-await or promises
|
||||
}
|
||||
}`;
|
||||
const parsedObject = [
|
||||
const expectedParsedObject = [
|
||||
{
|
||||
key: "myVar1",
|
||||
value: "[]",
|
||||
rawContent: "myVar1: []",
|
||||
type: "ArrayExpression",
|
||||
position: {
|
||||
startLine: 2,
|
||||
startColumn: 1,
|
||||
endLine: 2,
|
||||
endColumn: 11,
|
||||
keyStartLine: 2,
|
||||
keyEndLine: 2,
|
||||
keyStartColumn: 1,
|
||||
keyEndColumn: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "myVar2",
|
||||
value: '{\n "a": "app"\n}',
|
||||
rawContent: 'myVar2: {\n\t\t"a": "app",\n\t}',
|
||||
type: "ObjectExpression",
|
||||
position: {
|
||||
startLine: 3,
|
||||
startColumn: 1,
|
||||
endLine: 5,
|
||||
endColumn: 2,
|
||||
keyStartLine: 3,
|
||||
keyEndLine: 3,
|
||||
keyStartColumn: 1,
|
||||
keyEndColumn: 7,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "myFun1",
|
||||
value: "() => {}",
|
||||
rawContent: "myFun1: () => {\n\t\t//write code here\n\t}",
|
||||
type: "ArrowFunctionExpression",
|
||||
position: {
|
||||
startLine: 6,
|
||||
startColumn: 1,
|
||||
endLine: 8,
|
||||
endColumn: 2,
|
||||
keyStartLine: 6,
|
||||
keyEndLine: 6,
|
||||
keyStartColumn: 1,
|
||||
keyEndColumn: 7,
|
||||
},
|
||||
arguments: [],
|
||||
isMarkedAsync: false
|
||||
},
|
||||
{
|
||||
key: "myFun2",
|
||||
value: "async () => {}",
|
||||
rawContent:
|
||||
"myFun2: async () => {\n\t\t//use async-await or promises\n\t}",
|
||||
type: "ArrowFunctionExpression",
|
||||
position: {
|
||||
startLine: 9,
|
||||
startColumn: 1,
|
||||
endLine: 11,
|
||||
endColumn: 2,
|
||||
keyStartLine: 9,
|
||||
keyEndLine: 9,
|
||||
keyStartColumn: 1,
|
||||
keyEndColumn: 7,
|
||||
},
|
||||
arguments: [],
|
||||
isMarkedAsync: true,
|
||||
},
|
||||
];
|
||||
const resultParsedObject = parseJSObjectWithAST(body);
|
||||
expect(resultParsedObject).toStrictEqual(parsedObject);
|
||||
const { parsedObject } = parseJSObject(body);
|
||||
expect(parsedObject).toStrictEqual(expectedParsedObject);
|
||||
});
|
||||
|
||||
it("parse js object with variable declaration inside function", () => {
|
||||
const body = `{
|
||||
const body = `export default{
|
||||
myFun1: () => {
|
||||
const a = {
|
||||
conditions: [],
|
||||
|
|
@ -408,89 +503,108 @@ describe("parseJSObjectWithAST", () => {
|
|||
//use async-await or promises
|
||||
}
|
||||
}`;
|
||||
const parsedObject = [
|
||||
const expectedParsedObject = [
|
||||
{
|
||||
key: "myFun1",
|
||||
value: `() => {
|
||||
const a = {
|
||||
conditions: [],
|
||||
requires: 1,
|
||||
testFunc: () => {},
|
||||
testFunc2: function () {}
|
||||
};
|
||||
}`,
|
||||
value:
|
||||
"() => {\n" +
|
||||
" const a = {\n" +
|
||||
" conditions: [],\n" +
|
||||
" requires: 1,\n" +
|
||||
" 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",
|
||||
position: {
|
||||
startLine: 2,
|
||||
startColumn: 6,
|
||||
endLine: 9,
|
||||
endColumn: 7,
|
||||
keyStartLine: 2,
|
||||
keyEndLine: 2,
|
||||
keyStartColumn: 6,
|
||||
keyEndColumn: 12,
|
||||
},
|
||||
arguments: [],
|
||||
isMarkedAsync: false,
|
||||
},
|
||||
{
|
||||
key: "myFun2",
|
||||
value: "async () => {}",
|
||||
rawContent:
|
||||
"myFun2: async () => {\n //use async-await or promises\n }",
|
||||
type: "ArrowFunctionExpression",
|
||||
position: {
|
||||
startLine: 10,
|
||||
startColumn: 6,
|
||||
endLine: 12,
|
||||
endColumn: 7,
|
||||
keyStartLine: 10,
|
||||
keyEndLine: 10,
|
||||
keyStartColumn: 6,
|
||||
keyEndColumn: 12,
|
||||
},
|
||||
arguments: [],
|
||||
isMarkedAsync: true,
|
||||
},
|
||||
];
|
||||
const resultParsedObject = parseJSObjectWithAST(body);
|
||||
expect(resultParsedObject).toStrictEqual(parsedObject);
|
||||
const { parsedObject } = parseJSObject(body);
|
||||
expect(parsedObject).toStrictEqual(expectedParsedObject);
|
||||
});
|
||||
|
||||
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()) => {
|
||||
//use async-await or promises
|
||||
},
|
||||
}`;
|
||||
|
||||
const parsedObject = [
|
||||
const expectedParsedObject = [
|
||||
{
|
||||
key: "myFun2",
|
||||
value:
|
||||
'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",
|
||||
position: {
|
||||
startLine: 2,
|
||||
startColumn: 6,
|
||||
endLine: 4,
|
||||
endColumn: 7,
|
||||
keyStartLine: 2,
|
||||
keyEndLine: 2,
|
||||
keyStartColumn: 6,
|
||||
keyEndColumn: 12,
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
paramName: "a",
|
||||
defaultValue: undefined,
|
||||
},
|
||||
{
|
||||
paramName: "b",
|
||||
defaultValue: undefined,
|
||||
},
|
||||
{
|
||||
paramName: "c",
|
||||
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,
|
||||
},
|
||||
{ paramName: "a", defaultValue: undefined },
|
||||
{ paramName: "b", defaultValue: undefined },
|
||||
{ paramName: "c", 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);
|
||||
expect(resultParsedObject).toEqual(parsedObject);
|
||||
const { parsedObject } = parseJSObject(body);
|
||||
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 { ECMA_VERSION, NodeTypes } from "./constants/ast";
|
||||
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
|
||||
interface IdentifierNode extends Node {
|
||||
export interface IdentifierNode extends Node {
|
||||
type: NodeTypes.Identifier;
|
||||
name: string;
|
||||
}
|
||||
|
|
@ -69,10 +69,12 @@ interface FunctionDeclarationNode extends Node, Function {
|
|||
// doc: https://github.com/estree/estree/blob/master/es5.md#functionexpression
|
||||
interface FunctionExpressionNode extends Expression, Function {
|
||||
type: NodeTypes.FunctionExpression;
|
||||
async: boolean;
|
||||
}
|
||||
|
||||
interface ArrowFunctionExpressionNode extends Expression, Function {
|
||||
type: NodeTypes.ArrowFunctionExpression;
|
||||
async: boolean;
|
||||
}
|
||||
|
||||
export interface ObjectExpression extends Expression {
|
||||
|
|
@ -87,7 +89,7 @@ interface AssignmentPatternNode extends Node {
|
|||
}
|
||||
|
||||
// doc: https://github.com/estree/estree/blob/master/es5.md#literal
|
||||
interface LiteralNode extends Node {
|
||||
export interface LiteralNode extends Node {
|
||||
type: NodeTypes.Literal;
|
||||
value: string | boolean | null | number | RegExp;
|
||||
}
|
||||
|
|
@ -107,8 +109,12 @@ export interface PropertyNode extends Node {
|
|||
kind: "init" | "get" | "set";
|
||||
}
|
||||
|
||||
export interface ExportDefaultDeclarationNode extends Node {
|
||||
declaration: Node;
|
||||
}
|
||||
|
||||
// Node with location details
|
||||
type NodeWithLocation<NodeType> = NodeType & {
|
||||
export type NodeWithLocation<NodeType> = NodeType & {
|
||||
loc: SourceLocation;
|
||||
};
|
||||
|
||||
|
|
@ -163,6 +169,12 @@ export const isPropertyNode = (node: Node): node is PropertyNode => {
|
|||
return node.type === NodeTypes.Property;
|
||||
};
|
||||
|
||||
export const isExportDefaultDeclarationNode = (
|
||||
node: Node,
|
||||
): node is ExportDefaultDeclarationNode => {
|
||||
return node.type === NodeTypes.ExportDefaultDeclaration;
|
||||
};
|
||||
|
||||
export const isPropertyAFunctionNode = (
|
||||
node: Node,
|
||||
): 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
|
||||
// for the the same code snippet
|
||||
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 { getAST } from "../index";
|
||||
import { generate } from "astring";
|
||||
import { simple } from "acorn-walk";
|
||||
import {
|
||||
getAST,
|
||||
IdentifierNode,
|
||||
isExportDefaultDeclarationNode,
|
||||
isObjectExpression,
|
||||
isPropertyNode,
|
||||
isTypeOfFunction,
|
||||
LiteralNode,
|
||||
NodeWithLocation,
|
||||
PropertyNode,
|
||||
} from "../index";
|
||||
import { generate } from "astring";
|
||||
import {
|
||||
getFunctionalParamsFromNode,
|
||||
isPropertyAFunctionNode,
|
||||
isVariableDeclarator,
|
||||
isObjectExpression,
|
||||
PropertyNode,
|
||||
functionParam,
|
||||
} from "../index";
|
||||
|
||||
type JsObjectProperty = {
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
arguments?: Array<functionParam>;
|
||||
};
|
||||
import { SourceType, NodeTypes } from "../../index";
|
||||
import { attachComments } from "escodegen";
|
||||
import { extractContentByPosition } from "../utils";
|
||||
|
||||
const jsObjectVariableName =
|
||||
"____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____";
|
||||
|
||||
export const jsObjectDeclaration = `var ${jsObjectVariableName} =`;
|
||||
|
||||
export const parseJSObjectWithAST = (
|
||||
jsObjectBody: string
|
||||
): Array<JsObjectProperty> => {
|
||||
/*
|
||||
jsObjectVariableName value is added such actual js code would never name same variable name.
|
||||
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.
|
||||
Keeping this just for sanity check if any caveat was missed.
|
||||
*/
|
||||
const jsCode = `${jsObjectDeclaration} ${jsObjectBody}`;
|
||||
export interface JSPropertyPosition {
|
||||
startLine: number;
|
||||
startColumn: number;
|
||||
endLine: number;
|
||||
endColumn: number;
|
||||
keyStartLine: number;
|
||||
keyEndLine: number;
|
||||
keyStartColumn: number;
|
||||
keyEndColumn: number;
|
||||
}
|
||||
|
||||
const ast = getAST(jsCode);
|
||||
interface baseJSProperty {
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
position: Partial<JSPropertyPosition>;
|
||||
rawContent: string;
|
||||
}
|
||||
|
||||
const parsedObjectProperties = new Set<JsObjectProperty>();
|
||||
let JSObjectProperties: Array<PropertyNode> = [];
|
||||
type JSFunctionProperty = baseJSProperty & {
|
||||
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, {
|
||||
VariableDeclarator(node: Node) {
|
||||
ExportDefaultDeclaration(node, ancestors: Node[]) {
|
||||
if (
|
||||
isVariableDeclarator(node) &&
|
||||
node.id.name === jsObjectVariableName &&
|
||||
node.init &&
|
||||
isObjectExpression(node.init)
|
||||
) {
|
||||
JSObjectProperties = node.init.properties;
|
||||
}
|
||||
!isExportDefaultDeclarationNode(node) ||
|
||||
!isObjectExpression(node.declaration)
|
||||
)
|
||||
return;
|
||||
JSObjectProperties = node.declaration
|
||||
.properties as NodeWithLocation<PropertyNode>[];
|
||||
},
|
||||
});
|
||||
|
||||
JSObjectProperties.forEach((node) => {
|
||||
let params = new Set<functionParam>();
|
||||
const propertyNode = node;
|
||||
let property: JsObjectProperty = {
|
||||
key: generate(propertyNode.key),
|
||||
value: generate(propertyNode.value),
|
||||
type: propertyNode.value.type,
|
||||
const propertyKey = node.key as NodeWithLocation<
|
||||
LiteralNode | IdentifierNode
|
||||
>;
|
||||
let property: TParsedJSProperty = {
|
||||
key: generate(node.key),
|
||||
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
|
||||
// currently we don't consume it anywhere hence avoiding to calculate that.
|
||||
params = getFunctionalParamsFromNode(propertyNode.value);
|
||||
const params = getFunctionalParamsFromNode(node.value);
|
||||
property = {
|
||||
...property,
|
||||
arguments: [...params],
|
||||
isMarkedAsync: node.value.async,
|
||||
};
|
||||
}
|
||||
|
||||
// here we use `generate` function to convert our AST Node to JSCode
|
||||
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+$/;
|
||||
|
||||
|
|
@ -8,14 +9,51 @@ export function sanitizeScript(js: string, evaluationVersion: number) {
|
|||
// so that eval can happen
|
||||
//default value of evalutaion version is 2
|
||||
evaluationVersion = evaluationVersion ? evaluationVersion : 2;
|
||||
const trimmedJS = js.replace(beginsWithLineBreakRegex, '');
|
||||
const trimmedJS = js.replace(beginsWithLineBreakRegex, "");
|
||||
return evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS);
|
||||
}
|
||||
|
||||
// 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
|
||||
export const isTrueObject = (
|
||||
item: unknown
|
||||
item: 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