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:
Favour Ohanekwu 2023-04-03 11:41:15 +01:00 committed by GitHub
parent 1b92f97d61
commit b80b0ca3fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 4383 additions and 1360 deletions

View File

@ -1,6 +1,5 @@
const dsl = require("../../../../fixtures/autocomp.json"); const dsl = require("../../../../fixtures/autocomp.json");
const dynamicInputLocators = require("../../../../locators/DynamicInput.json"); const dynamicInputLocators = require("../../../../locators/DynamicInput.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
describe("Dynamic input autocomplete", () => { describe("Dynamic input autocomplete", () => {
before(() => { before(() => {
@ -72,7 +71,7 @@ describe("Dynamic input autocomplete", () => {
cy.wait(1000); cy.wait(1000);
cy.evaluateErrorMessage( cy.evaluateErrorMessage(
"Found a reference to {{actionName}} during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.".replaceAll( "Found a reference to {{actionName}} during evaluation. Data fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.".replaceAll(
"{{actionName}}", "{{actionName}}",
"storeValue()", "storeValue()",
), ),

View File

@ -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?",
);
});
});

View File

@ -176,5 +176,6 @@ export class CommonLocators {
_commentString = ".cm-comment"; _commentString = ".cm-comment";
_modalWrapper = "[data-cy='modal-wrapper']"; _modalWrapper = "[data-cy='modal-wrapper']";
_editorBackButton = ".t--close-editor"; _editorBackButton = ".t--close-editor";
_evaluateMsg = ".t--evaluatedPopup-error";
_canvas = "[data-testid=widgets-editor]"; _canvas = "[data-testid=widgets-editor]";
} }

View File

@ -64,9 +64,7 @@
"@uppy/url": "^1.5.16", "@uppy/url": "^1.5.16",
"@uppy/webcam": "^1.8.4", "@uppy/webcam": "^1.8.4",
"@welldone-software/why-did-you-render": "^4.2.5", "@welldone-software/why-did-you-render": "^4.2.5",
"acorn-walk": "^8.2.0",
"algoliasearch": "^4.2.0", "algoliasearch": "^4.2.0",
"astring": "^1.7.5",
"axios": "^0.27.2", "axios": "^0.27.2",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"codemirror": "^5.59.2", "codemirror": "^5.59.2",

View File

@ -1,11 +1,11 @@
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants"; import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants"; import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
export type SetLintErrorsAction = ReduxAction<{ errors: LintErrors }>; export type SetLintErrorsAction = ReduxAction<{ errors: LintErrorsStore }>;
export const setLintingErrors = ( export const setLintingErrors = (
errors: LintErrors, errors: LintErrorsStore,
): ReduxAction<{ errors: LintErrors }> => { ): ReduxAction<{ errors: LintErrorsStore }> => {
return { return {
type: ReduxActionTypes.SET_LINT_ERRORS, type: ReduxActionTypes.SET_LINT_ERRORS,
payload: { errors }, payload: { errors },

View File

@ -68,7 +68,7 @@ import type { EditorContextState } from "reducers/uiReducers/editorContextReduce
import type { LibraryState } from "reducers/uiReducers/libraryReducer"; import type { LibraryState } from "reducers/uiReducers/libraryReducer";
import type { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer"; import type { AutoHeightLayoutTreeReduxState } from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer";
import type { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightReducers/canvasLevelsReducer"; import type { CanvasLevelsReduxState } from "reducers/entityReducers/autoHeightReducers/canvasLevelsReducer";
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers"; import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
import lintErrorReducer from "reducers/lintingReducers"; import lintErrorReducer from "reducers/lintingReducers";
import type { AutoHeightUIState } from "reducers/uiReducers/autoHeightReducer"; import type { AutoHeightUIState } from "reducers/uiReducers/autoHeightReducer";
import type { AnalyticsReduxState } from "reducers/uiReducers/analyticsReducer"; import type { AnalyticsReduxState } from "reducers/uiReducers/analyticsReducer";
@ -159,7 +159,7 @@ export interface AppState {
triggers: TriggerValuesEvaluationState; triggers: TriggerValuesEvaluationState;
}; };
linting: { linting: {
errors: LintErrors; errors: LintErrorsStore;
}; };
form: { form: {
[key: string]: any; [key: string]: any;

View File

@ -8,6 +8,7 @@ import {
entityFns, entityFns,
getPlatformFunctions, getPlatformFunctions,
} from "@appsmith/workers/Evaluation/fns"; } from "@appsmith/workers/Evaluation/fns";
import { klona } from "klona/full";
declare global { declare global {
/** All identifiers added to the worker global scope should also /** All identifiers added to the worker global scope should also
* be included in the DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS in * be included in the DEDICATED_WORKER_GLOBAL_SCOPE_IDENTIFIERS in
@ -33,21 +34,21 @@ export enum ExecutionType {
export const addDataTreeToContext = (args: { export const addDataTreeToContext = (args: {
EVAL_CONTEXT: EvalContext; EVAL_CONTEXT: EvalContext;
dataTree: Readonly<DataTree>; dataTree: Readonly<DataTree>;
skipEntityFunctions?: boolean; removeEntityFunctions?: boolean;
isTriggerBased: boolean; isTriggerBased: boolean;
}) => { }) => {
const { const {
dataTree, dataTree,
EVAL_CONTEXT, EVAL_CONTEXT,
isTriggerBased, isTriggerBased,
skipEntityFunctions = false, removeEntityFunctions = false,
} = args; } = args;
const dataTreeEntries = Object.entries(dataTree); const dataTreeEntries = Object.entries(dataTree);
const entityFunctionCollection: Record<string, Record<string, Function>> = {}; const entityFunctionCollection: Record<string, Record<string, Function>> = {};
for (const [entityName, entity] of dataTreeEntries) { for (const [entityName, entity] of dataTreeEntries) {
EVAL_CONTEXT[entityName] = entity; EVAL_CONTEXT[entityName] = entity;
if (skipEntityFunctions || !isTriggerBased) continue; if (!removeEntityFunctions && !isTriggerBased) continue;
for (const entityFn of entityFns) { for (const entityFn of entityFns) {
if (!entityFn.qualifier(entity)) continue; if (!entityFn.qualifier(entity)) continue;
const func = entityFn.fn(entity, entityName); const func = entityFn.fn(entity, entityName);
@ -56,6 +57,12 @@ export const addDataTreeToContext = (args: {
} }
} }
if (removeEntityFunctions)
return removeEntityFunctionsFromEvalContext(
entityFunctionCollection,
EVAL_CONTEXT,
);
// if eval is not trigger based i.e., sync eval then we skip adding entity and platform function to evalContext // if eval is not trigger based i.e., sync eval then we skip adding entity and platform function to evalContext
if (!isTriggerBased) return; if (!isTriggerBased) return;
@ -87,3 +94,18 @@ export const getAllAsyncFunctions = (dataTree: DataTree) => {
} }
return asyncFunctionNameMap; return asyncFunctionNameMap;
}; };
export const removeEntityFunctionsFromEvalContext = (
entityFunctionCollection: Record<string, Record<string, Function>>,
evalContext: EvalContext,
) => {
for (const [entityName, funcObj] of Object.entries(
entityFunctionCollection,
)) {
const entity = klona(evalContext[entityName]);
Object.keys(funcObj).forEach((entityFn) => {
delete entity[entityFn];
});
evalContext[entityName] = entity;
}
};

View File

@ -391,6 +391,15 @@ export function isJSAction(entity: DataTreeEntity): entity is JSActionEntity {
entity.ENTITY_TYPE === ENTITY_TYPE.JSACTION entity.ENTITY_TYPE === ENTITY_TYPE.JSACTION
); );
} }
export function isJSActionConfig(
entity: DataTreeEntityConfig,
): entity is JSActionEntityConfig {
return (
typeof entity === "object" &&
"ENTITY_TYPE" in entity &&
entity.ENTITY_TYPE === ENTITY_TYPE.JSACTION
);
}
export function isJSObject(entity: DataTreeEntity): entity is JSActionEntity { export function isJSObject(entity: DataTreeEntity): entity is JSActionEntity {
return ( return (
@ -402,6 +411,10 @@ export function isJSObject(entity: DataTreeEntity): entity is JSActionEntity {
); );
} }
export function isDataTreeEntity(entity: unknown) {
return !!entity && typeof entity === "object" && "ENTITY_TYPE" in entity;
}
// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing // We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing
// Check issue https://github.com/appsmithorg/appsmith/issues/719 // Check issue https://github.com/appsmithorg/appsmith/issues/719
export const removeFunctions = (value: any) => { export const removeFunctions = (value: any) => {

View File

@ -24,6 +24,7 @@ import { ReactComponent as CopyIcon } from "assets/icons/menu/copy-snippet.svg";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
import type { EvaluationError } from "utils/DynamicBindingUtils"; import type { EvaluationError } from "utils/DynamicBindingUtils";
import { PropertyEvaluationErrorCategory } from "utils/DynamicBindingUtils";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { Severity } from "@sentry/react"; import { Severity } from "@sentry/react";
import type { CodeEditorExpected } from "components/editorComponents/CodeEditor/index"; import type { CodeEditorExpected } from "components/editorComponents/CodeEditor/index";
@ -33,6 +34,11 @@ import { useDispatch, useSelector } from "react-redux";
import { getEvaluatedPopupState } from "selectors/editorContextSelectors"; import { getEvaluatedPopupState } from "selectors/editorContextSelectors";
import type { AppState } from "@appsmith/reducers"; import type { AppState } from "@appsmith/reducers";
import { setEvalPopupState } from "actions/editorContextActions"; import { setEvalPopupState } from "actions/editorContextActions";
import { Link } from "react-router-dom";
import { showDebugger } from "actions/debuggerActions";
import { modText } from "utils/helpers";
import { getEntityNameAndPropertyPath } from "@appsmith/workers/Evaluation/evaluationUtils";
import { getJSFunctionNavigationUrl } from "selectors/navigationSelectors";
const modifiers: IPopoverSharedProps["modifiers"] = { const modifiers: IPopoverSharedProps["modifiers"] = {
offset: { offset: {
@ -183,6 +189,24 @@ const StyledTitleName = styled.p`
cursor: pointer; cursor: pointer;
`; `;
const AsyncFunctionErrorLink = styled(Link)`
color: ${(props) => props.theme.colors.debugger.entityLink};
font-weight: 600;
font-size: 12px;
line-height: 14px;
cursor: pointer;
letter-spacing: 0.6px;
&:hover {
color: ${(props) => props.theme.colors.debugger.entityLink};
}
`;
const AsyncFunctionErrorView = styled.div`
display: flex;
margin-top: 12px;
justify-content: space-between;
`;
function CollapseToggle(props: { isOpen: boolean }) { function CollapseToggle(props: { isOpen: boolean }) {
const { isOpen } = props; const { isOpen } = props;
return ( return (
@ -462,16 +486,38 @@ function PopoverContent(props: PopoverContentProps) {
? popupContext.value ? popupContext.value
: true, : true,
); );
const { errors, expected, hasError, onMouseEnter, onMouseLeave, theme } =
props;
const { entityName } = getEntityNameAndPropertyPath(props.dataTreePath || "");
const JSFunctionInvocationError = errors.find(
({ kind }) =>
kind &&
kind.category ===
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD &&
kind.rootcause,
);
const errorNavigationUrl = useSelector((state: AppState) =>
getJSFunctionNavigationUrl(
state,
entityName,
JSFunctionInvocationError?.kind?.rootcause,
),
);
const toggleExpectedDataType = () => const toggleExpectedDataType = () =>
setOpenExpectedDataType(!openExpectedDataType); setOpenExpectedDataType(!openExpectedDataType);
const toggleExpectedExample = () => const toggleExpectedExample = () =>
setOpenExpectedExample(!openExpectedExample); setOpenExpectedExample(!openExpectedExample);
const { errors, expected, hasError, onMouseEnter, onMouseLeave, theme } =
props;
let error: EvaluationError | undefined; let error: EvaluationError | undefined;
if (hasError) { if (hasError) {
error = errors[0]; error = errors[0];
} }
const openDebugger = (
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>,
) => {
event.preventDefault();
dispatch(showDebugger());
};
useEffect(() => { useEffect(() => {
dispatch( dispatch(
@ -508,6 +554,17 @@ function PopoverContent(props: PopoverContentProps) {
{/* errorMessage could be an empty string */} {/* errorMessage could be an empty string */}
{getErrorMessage(error.errorMessage)} {getErrorMessage(error.errorMessage)}
</span> </span>
{errorNavigationUrl ? (
<AsyncFunctionErrorView>
<AsyncFunctionErrorLink onClick={(e) => openDebugger(e)} to="">
See Error ({modText()} D)
</AsyncFunctionErrorLink>
<AsyncFunctionErrorLink to={errorNavigationUrl}>
View Source
</AsyncFunctionErrorLink>
</AsyncFunctionErrorView>
) : (
<EvaluatedValueDebugButton <EvaluatedValueDebugButton
entity={props.entity} entity={props.entity}
error={{ error={{
@ -515,6 +572,7 @@ function PopoverContent(props: PopoverContentProps) {
message: error.errorMessage, message: error.errorMessage,
}} }}
/> />
)}
</ErrorText> </ErrorText>
)} )}
{props.expected && props.expected.type !== UNDEFINED_VALIDATION && ( {props.expected && props.expected.type !== UNDEFINED_VALIDATION && (

View File

@ -1263,6 +1263,7 @@ class CodeEditor extends Component<Props, State> {
text="/" text="/"
/> />
)} )}
<EvaluatedValuePopup <EvaluatedValuePopup
dataTreePath={this.props.dataTreePath} dataTreePath={this.props.dataTreePath}
editorRef={this.codeEditorTarget} editorRef={this.codeEditorTarget}

View File

@ -1,6 +1,10 @@
import { Severity } from "entities/AppsmithConsole"; import { Severity } from "entities/AppsmithConsole";
import type { LintError } from "utils/DynamicBindingUtils"; import type { LintError } from "utils/DynamicBindingUtils";
import { PropertyEvaluationErrorType } from "utils/DynamicBindingUtils"; import { PropertyEvaluationErrorType } from "utils/DynamicBindingUtils";
import {
INVALID_JSOBJECT_START_STATEMENT,
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
} from "workers/Linting/constants";
import { CODE_EDITOR_START_POSITION } from "./constants"; import { CODE_EDITOR_START_POSITION } from "./constants";
import { import {
getKeyPositionInString, getKeyPositionInString,
@ -213,7 +217,23 @@ describe("getLintAnnotations()", () => {
} }
`; `;
const errors: LintError[] = []; const errors: LintError[] = [
{
errorType: PropertyEvaluationErrorType.LINT,
errorSegment: "",
originalBinding: value,
line: 0,
ch: 0,
code: INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
variables: [],
raw: value,
errorMessage: {
name: "LintingError",
message: INVALID_JSOBJECT_START_STATEMENT,
},
severity: Severity.ERROR,
},
];
const res = getLintAnnotations(value, errors, { isJSObject: true }); const res = getLintAnnotations(value, errors, { isJSObject: true });
expect(res).toEqual([ expect(res).toEqual([

View File

@ -13,7 +13,7 @@ import {
CUSTOM_LINT_ERRORS, CUSTOM_LINT_ERRORS,
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE, IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
INVALID_JSOBJECT_START_STATEMENT, INVALID_JSOBJECT_START_STATEMENT,
JS_OBJECT_START_STATEMENT, INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
} from "workers/Linting/constants"; } from "workers/Linting/constants";
export const getIndexOfRegex = ( export const getIndexOfRegex = (
str: string, str: string,
@ -123,33 +123,32 @@ export const getLintAnnotations = (
const lintErrors = filterInvalidLintErrors(errors, contextData); const lintErrors = filterInvalidLintErrors(errors, contextData);
const lines = value.split("\n"); const lines = value.split("\n");
// The binding position of every valid JS Object is constant, so we need not
// waste time checking for position of binding.
// For JS Objects not starting with the expected "export default" statement, we return early
// with a "invalid start statement" lint error
if (
isJSObject &&
!isEmpty(lines) &&
!lines[0].startsWith(JS_OBJECT_START_STATEMENT)
) {
return [
{
from: CODE_EDITOR_START_POSITION,
to: getFirstNonEmptyPosition(lines),
message: INVALID_JSOBJECT_START_STATEMENT,
severity: Severity.ERROR,
},
];
}
lintErrors.forEach((error) => { lintErrors.forEach((error) => {
const { ch, errorMessage, line, originalBinding, severity, variables } = const {
error; ch,
code,
errorMessage,
line,
originalBinding,
severity,
variables,
} = error;
if (!originalBinding) { if (!originalBinding) {
return annotations; return annotations;
} }
if (code === INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE) {
// The binding position of every valid JS Object is constant, so we need not
// waste time checking for position of binding.
// For JS Objects not starting with the expected "export default" statement, we return early
// with a "invalid start statement" lint error
return annotations.push({
from: CODE_EDITOR_START_POSITION,
to: getFirstNonEmptyPosition(lines),
message: INVALID_JSOBJECT_START_STATEMENT,
severity: Severity.ERROR,
});
}
let variableLength = 1; let variableLength = 1;
// Find the variable with minimal length // Find the variable with minimal length
if (variables) { if (variables) {

View File

@ -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;
};

View File

@ -4,22 +4,21 @@ import { createImmerReducer } from "utils/ReducerUtils";
import type { SetLintErrorsAction } from "actions/lintingActions"; import type { SetLintErrorsAction } from "actions/lintingActions";
import { isEqual } from "lodash"; import { isEqual } from "lodash";
export interface LintErrors { export type LintErrorsStore = Record<string, LintError[]>;
[entityName: string]: LintError[];
}
const initialState: LintErrors = {}; const initialState: LintErrorsStore = {};
export const lintErrorReducer = createImmerReducer(initialState, { export const lintErrorReducer = createImmerReducer(initialState, {
[ReduxActionTypes.FETCH_PAGE_INIT]: () => initialState, [ReduxActionTypes.FETCH_PAGE_INIT]: () => initialState,
[ReduxActionTypes.SET_LINT_ERRORS]: ( [ReduxActionTypes.SET_LINT_ERRORS]: (
state: LintErrors, state: LintErrorsStore,
action: SetLintErrorsAction, action: SetLintErrorsAction,
) => { ) => {
const { errors } = action.payload; const { errors } = action.payload;
for (const entityName of Object.keys(errors)) { for (const entityPath of Object.keys(errors)) {
if (isEqual(state[entityName], errors[entityName])) continue; const entityPathLintErrors = errors[entityPath];
state[entityName] = errors[entityName]; if (isEqual(entityPathLintErrors, state[entityPath])) continue;
state[entityPath] = entityPathLintErrors;
} }
}, },
}); });

View File

@ -404,7 +404,7 @@ function* logDebuggerErrorAnalyticsSaga(
); );
const pluginId = action?.pluginId || payload?.analytics?.pluginId || ""; const pluginId = action?.pluginId || payload?.analytics?.pluginId || "";
const plugin: Plugin = yield select(getPlugin, pluginId); const plugin: Plugin = yield select(getPlugin, pluginId);
const pluginName = plugin.name.replace(/ /g, ""); const pluginName = plugin?.name.replace(/ /g, "");
let propertyPath = `${pluginName}`; let propertyPath = `${pluginName}`;
if (payload.propertyPath) { if (payload.propertyPath) {

View File

@ -20,6 +20,7 @@ import { logJSFunctionExecution } from "@appsmith/sagas/JSFunctionExecutionSaga"
import { handleStoreOperations } from "./ActionExecution/StoreActionSaga"; import { handleStoreOperations } from "./ActionExecution/StoreActionSaga";
import isEmpty from "lodash/isEmpty"; import isEmpty from "lodash/isEmpty";
import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils"; import { sortJSExecutionDataByCollectionId } from "workers/Evaluation/JSObject/utils";
import type { LintTreeSagaRequestData } from "workers/Linting/types";
export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) { export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
while (true) { while (true) {
@ -31,12 +32,21 @@ export function* handleEvalWorkerRequestSaga(listenerChannel: Channel<any>) {
export function* lintTreeActionHandler(message: any) { export function* lintTreeActionHandler(message: any) {
const { body } = message; const { body } = message;
const { data } = body; const { data } = body;
const {
asyncJSFunctionsInDataFields,
configTree,
jsPropertiesState,
pathsToLint: lintOrder,
unevalTree,
} = data as LintTreeSagaRequestData;
yield put({ yield put({
type: ReduxActionTypes.LINT_TREE, type: ReduxActionTypes.LINT_TREE,
payload: { payload: {
pathsToLint: data.lintOrder, pathsToLint: lintOrder,
unevalTree: data.unevalTree, unevalTree,
configTree: data.configTree, jsPropertiesState,
asyncJSFunctionsInDataFields,
configTree,
}, },
}); });
} }

View File

@ -165,6 +165,7 @@ export function* evaluateTreeSaga(
requiresLinting: isEditMode && requiresLinting, requiresLinting: isEditMode && requiresLinting,
forceEvaluation, forceEvaluation,
metaWidgets, metaWidgets,
appMode,
}; };
const workerResponse: EvalTreeResponseData = yield call( const workerResponse: EvalTreeResponseData = yield call(

View File

@ -14,6 +14,11 @@ import type {
import { LINT_WORKER_ACTIONS } from "workers/Linting/types"; import { LINT_WORKER_ACTIONS } from "workers/Linting/types";
import { logLatestLintPropertyErrors } from "./PostLintingSagas"; import { logLatestLintPropertyErrors } from "./PostLintingSagas";
import { getAppsmithConfigs } from "@appsmith/configs"; import { getAppsmithConfigs } from "@appsmith/configs";
import type { AppState } from "@appsmith/reducers";
import type { LintError } from "utils/DynamicBindingUtils";
import { get, set, union } from "lodash";
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
import type { TJSPropertiesState } from "workers/Evaluation/JSObject/jsPropertiesState";
const APPSMITH_CONFIGS = getAppsmithConfigs(); const APPSMITH_CONFIGS = getAppsmithConfigs();
@ -35,8 +40,58 @@ function* updateLintGlobals(action: ReduxAction<TJSLibrary>) {
); );
} }
function* getValidOldJSCollectionLintErrors(
jsEntities: string[],
errors: LintErrorsStore,
jsObjectsState: TJSPropertiesState,
) {
const updatedJSCollectionLintErrors: LintErrorsStore = {};
for (const jsObjectName of jsEntities) {
const jsObjectBodyPath = `["${jsObjectName}.body"]`;
const oldJsBodyLintErrors: LintError[] = yield select((state: AppState) =>
get(state.linting.errors, jsObjectBodyPath, []),
);
const newJSBodyLintErrors = get(
errors,
jsObjectBodyPath,
[] as LintError[],
);
const newJSBodyLintErrorsOriginalPaths = newJSBodyLintErrors.reduce(
(paths, currentError) => {
if (currentError.originalPath)
return union(paths, [currentError.originalPath]);
return paths;
},
[] as string[],
);
const jsObjectState = get(jsObjectsState, jsObjectName, {});
const jsObjectProperties = Object.keys(jsObjectState);
const filteredOldJsObjectBodyLintErrors = oldJsBodyLintErrors.filter(
(lintError) =>
lintError.originalPath &&
lintError.originalPath in jsObjectProperties &&
!(lintError.originalPath in newJSBodyLintErrorsOriginalPaths),
);
const updatedLintErrors = [
...filteredOldJsObjectBodyLintErrors,
...newJSBodyLintErrors,
];
set(updatedJSCollectionLintErrors, jsObjectBodyPath, updatedLintErrors);
}
return updatedJSCollectionLintErrors;
}
export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) { export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) {
const { configTree, pathsToLint, unevalTree } = action.payload; const {
asyncJSFunctionsInDataFields,
configTree,
jsPropertiesState,
pathsToLint,
unevalTree,
} = action.payload;
// only perform lint operations in edit mode // only perform lint operations in edit mode
const appMode: APP_MODE = yield select(getAppMode); const appMode: APP_MODE = yield select(getAppMode);
if (appMode !== APP_MODE.EDIT) return; if (appMode !== APP_MODE.EDIT) return;
@ -44,18 +99,32 @@ export function* lintTreeSaga(action: ReduxAction<LintTreeSagaRequestData>) {
const lintTreeRequestData: LintTreeRequest = { const lintTreeRequestData: LintTreeRequest = {
pathsToLint, pathsToLint,
unevalTree, unevalTree,
jsPropertiesState,
configTree, configTree,
cloudHosting: !!APPSMITH_CONFIGS.cloudHosting, cloudHosting: !!APPSMITH_CONFIGS.cloudHosting,
asyncJSFunctionsInDataFields,
}; };
const { errors }: LintTreeResponse = yield call( const { errors, updatedJSEntities }: LintTreeResponse = yield call(
lintWorker.request, lintWorker.request,
LINT_WORKER_ACTIONS.LINT_TREE, LINT_WORKER_ACTIONS.LINT_TREE,
lintTreeRequestData, lintTreeRequestData,
); );
yield put(setLintingErrors(errors)); const oldJSCollectionLintErrors: LintErrorsStore =
yield call(logLatestLintPropertyErrors, { errors, dataTree: unevalTree }); yield getValidOldJSCollectionLintErrors(
updatedJSEntities,
errors,
jsPropertiesState,
);
const updatedErrors = { ...errors, ...oldJSCollectionLintErrors };
yield put(setLintingErrors(updatedErrors));
yield call(logLatestLintPropertyErrors, {
errors,
dataTree: unevalTree,
});
} }
export default function* lintTreeSagaWatcher() { export default function* lintTreeSagaWatcher() {

View File

@ -2,19 +2,19 @@ import { ENTITY_TYPE, Severity } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype"; import LOG_TYPE from "entities/AppsmithConsole/logtype";
import type { DataTree } from "entities/DataTree/dataTreeFactory"; import type { DataTree } from "entities/DataTree/dataTreeFactory";
import { isEmpty } from "lodash"; import { isEmpty } from "lodash";
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
import AppsmithConsole from "utils/AppsmithConsole"; import AppsmithConsole from "utils/AppsmithConsole";
import { import {
getEntityNameAndPropertyPath, getEntityNameAndPropertyPath,
isJSAction, isJSAction,
} from "@appsmith/workers/Evaluation/evaluationUtils"; } from "@appsmith/workers/Evaluation/evaluationUtils";
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
// We currently only log lint errors in JSObjects // We currently only log lint errors in JSObjects
export function* logLatestLintPropertyErrors({ export function* logLatestLintPropertyErrors({
dataTree, dataTree,
errors, errors,
}: { }: {
errors: LintErrors; errors: LintErrorsStore;
dataTree: DataTree; dataTree: DataTree;
}) { }) {
const errorsToAdd = []; const errorsToAdd = [];

View File

@ -1,11 +1,7 @@
import type { AppState } from "@appsmith/reducers"; import type { AppState } from "@appsmith/reducers";
import { get } from "lodash"; import { get } from "lodash";
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
import type { LintError } from "utils/DynamicBindingUtils"; import type { LintError } from "utils/DynamicBindingUtils";
export const getAllLintErrors = (state: AppState): LintErrors =>
state.linting.errors;
const emptyLint: LintError[] = []; const emptyLint: LintError[] = [];
export const getEntityLintErrors = (state: AppState, path?: string) => { export const getEntityLintErrors = (state: AppState, path?: string) => {

View File

@ -19,7 +19,11 @@ import { createNavData } from "utils/NavigationSelector/common";
import { getWidgetChildrenNavData } from "utils/NavigationSelector/WidgetChildren"; import { getWidgetChildrenNavData } from "utils/NavigationSelector/WidgetChildren";
import { getJsChildrenNavData } from "utils/NavigationSelector/JsChildren"; import { getJsChildrenNavData } from "utils/NavigationSelector/JsChildren";
import { getAppsmithNavData } from "utils/NavigationSelector/AppsmithNavData"; import { getAppsmithNavData } from "utils/NavigationSelector/AppsmithNavData";
import { isJSAction } from "ce/workers/Evaluation/evaluationUtils"; import {
getEntityNameAndPropertyPath,
isJSAction,
} from "@appsmith/workers/Evaluation/evaluationUtils";
import type { AppState } from "@appsmith/reducers";
export type NavigationData = { export type NavigationData = {
name: string; name: string;
@ -124,3 +128,20 @@ export const getEntitiesForNavigation = createSelector(
return navigationData; return navigationData;
}, },
); );
export const getJSFunctionNavigationUrl = createSelector(
[
(state: AppState, entityName: string) =>
getEntitiesForNavigation(state, entityName),
(_, __, jsFunctionFullName: string | undefined) => jsFunctionFullName,
],
(entitiesForNavigation, jsFunctionFullName) => {
if (!jsFunctionFullName) return undefined;
const { entityName: jsObjectName, propertyPath: jsFunctionName } =
getEntityNameAndPropertyPath(jsFunctionFullName);
const jsObjectNavigationData = entitiesForNavigation[jsObjectName];
const jsFuncNavigationData =
jsObjectNavigationData && jsObjectNavigationData.children[jsFunctionName];
return jsFuncNavigationData?.url;
},
);

View File

@ -353,6 +353,15 @@ export enum PropertyEvaluationErrorType {
PARSE = "PARSE", PARSE = "PARSE",
LINT = "LINT", LINT = "LINT",
} }
export enum PropertyEvaluationErrorCategory {
INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD = "INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD",
}
export interface PropertyEvaluationErrorKind {
category: PropertyEvaluationErrorCategory;
rootcause: string;
}
export interface DataTreeError { export interface DataTreeError {
raw: string; raw: string;
errorMessage: Error; errorMessage: Error;
@ -364,6 +373,7 @@ export interface EvaluationError extends DataTreeError {
| PropertyEvaluationErrorType.PARSE | PropertyEvaluationErrorType.PARSE
| PropertyEvaluationErrorType.VALIDATION; | PropertyEvaluationErrorType.VALIDATION;
originalBinding?: string; originalBinding?: string;
kind?: PropertyEvaluationErrorKind;
} }
export interface LintError extends DataTreeError { export interface LintError extends DataTreeError {
@ -374,6 +384,7 @@ export interface LintError extends DataTreeError {
code: string; code: string;
line: number; line: number;
ch: number; ch: number;
originalPath?: string;
} }
export interface DataTreeEvaluationProps { export interface DataTreeEvaluationProps {

View File

@ -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();

View File

@ -2,7 +2,7 @@ import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
import { isEmpty, set } from "lodash"; import { isEmpty, set } from "lodash";
import { EvalErrorTypes } from "utils/DynamicBindingUtils"; import { EvalErrorTypes } from "utils/DynamicBindingUtils";
import type { JSUpdate, ParsedJSSubAction } from "utils/JSPaneUtils"; import type { JSUpdate, ParsedJSSubAction } from "utils/JSPaneUtils";
import { isTypeOfFunction, parseJSObjectWithAST } from "@shared/ast"; import { parseJSObject, isJSFunctionProperty } from "@shared/ast";
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator"; import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import evaluateSync from "workers/Evaluation/evaluate"; import evaluateSync from "workers/Evaluation/evaluate";
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils"; import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
@ -16,7 +16,9 @@ import {
updateJSCollectionInUnEvalTree, updateJSCollectionInUnEvalTree,
} from "workers/Evaluation/JSObject/utils"; } from "workers/Evaluation/JSObject/utils";
import { functionDeterminer } from "../functionDeterminer"; import { functionDeterminer } from "../functionDeterminer";
import { jsPropertiesState } from "./jsPropertiesState";
import type { JSActionEntity } from "entities/DataTree/types"; import type { JSActionEntity } from "entities/DataTree/types";
import { getFixedTimeDifference } from "workers/common/DataTreeEvaluator/utils";
/** /**
* Here we update our unEvalTree according to the change in JSObject's body * Here we update our unEvalTree according to the change in JSObject's body
@ -81,25 +83,30 @@ export function saveResolvedFunctionsAndJSUpdates(
unEvalDataTree: DataTree, unEvalDataTree: DataTree,
entityName: string, entityName: string,
) { ) {
jsPropertiesState.delete(entityName);
const correctFormat = regex.test(entity.body); const correctFormat = regex.test(entity.body);
if (correctFormat) { if (correctFormat) {
const body = entity.body.replace(/export default/g, "");
try { try {
delete dataTreeEvalRef.resolvedFunctions[`${entityName}`]; delete dataTreeEvalRef.resolvedFunctions[`${entityName}`];
delete dataTreeEvalRef.currentJSCollectionState[`${entityName}`]; delete dataTreeEvalRef.currentJSCollectionState[`${entityName}`];
const parseStartTime = performance.now(); const parseStartTime = performance.now();
const parsedObject = parseJSObjectWithAST(body); const { parsedObject, success } = parseJSObject(entity.body);
const parseEndTime = performance.now(); const parseEndTime = performance.now();
const JSObjectASTParseTime = parseEndTime - parseStartTime; const JSObjectASTParseTime = getFixedTimeDifference(
parseEndTime,
parseStartTime,
);
dataTreeEvalRef.logs.push({ dataTreeEvalRef.logs.push({
JSObjectName: entityName, JSObjectName: entityName,
JSObjectASTParseTime, JSObjectASTParseTime,
}); });
const actions: any = []; const actions: any = [];
const variables: any = []; const variables: any = [];
if (success) {
if (!!parsedObject) { if (!!parsedObject) {
jsPropertiesState.update(entityName, parsedObject);
parsedObject.forEach((parsedElement) => { parsedObject.forEach((parsedElement) => {
if (isTypeOfFunction(parsedElement.type)) { if (isJSFunctionProperty(parsedElement)) {
try { try {
const { result } = evaluateSync( const { result } = evaluateSync(
parsedElement.value, parsedElement.value,
@ -170,6 +177,7 @@ export function saveResolvedFunctionsAndJSUpdates(
id: entity.actionId, id: entity.actionId,
}); });
} }
}
} catch (e) { } catch (e) {
//if we need to push error as popup in case //if we need to push error as popup in case
} }
@ -194,6 +202,7 @@ export function parseJSActions(
differences?: DataTreeDiff[], differences?: DataTreeDiff[],
) { ) {
let jsUpdates: Record<string, JSUpdate> = {}; let jsUpdates: Record<string, JSUpdate> = {};
jsPropertiesState.startUpdate();
if (!!differences && !!oldUnEvalTree) { if (!!differences && !!oldUnEvalTree) {
differences.forEach((diff) => { differences.forEach((diff) => {
const { entityName, propertyPath } = getEntityNameAndPropertyPath( const { entityName, propertyPath } = getEntityNameAndPropertyPath(
@ -249,7 +258,7 @@ export function parseJSActions(
); );
}); });
} }
jsPropertiesState.stopUpdate();
functionDeterminer.setupEval( functionDeterminer.setupEval(
unEvalDataTree, unEvalDataTree,
dataTreeEvalRef.resolvedFunctions, dataTreeEvalRef.resolvedFunctions,

View File

@ -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

View File

@ -216,7 +216,6 @@ export const removeFunctionsAndVariableJSCollection = (
unset(modifiedDataTree[entityName], varName); unset(modifiedDataTree[entityName], varName);
} }
//remove functions //remove functions
const reactivePaths = entity.reactivePaths; const reactivePaths = entity.reactivePaths;
const meta = entity.meta; const meta = entity.meta;

View File

@ -124,46 +124,46 @@ describe("Test error modifier", () => {
errorModifier.updateAsyncFunctions(dataTree); errorModifier.updateAsyncFunctions(dataTree);
}); });
it("TypeError for defined Api in sync field ", () => { it("TypeError for defined Api in data field ", () => {
const error = new Error(); const error = new Error();
error.name = "TypeError"; error.name = "TypeError";
error.message = "Api2.run is not a function"; error.message = "Api2.run is not a function";
const result = errorModifier.run(error); const { errorMessage: result } = errorModifier.run(error);
expect(result).toEqual({ expect(result).toEqual({
name: "ValidationError", name: "ValidationError",
message: message:
"Found a reference to Api2.run() during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to Api2.run() and try again.", "Found a reference to Api2.run() during evaluation. Data fields cannot execute framework actions. Please remove any direct/indirect references to Api2.run() and try again.",
}); });
}); });
it("TypeError for undefined Api in sync field ", () => { it("TypeError for undefined Api in data field ", () => {
const error = new Error(); const error = new Error();
error.name = "TypeError"; error.name = "TypeError";
error.message = "Api1.run is not a function"; error.message = "Api1.run is not a function";
const result = errorModifier.run(error); const { errorMessage: result } = errorModifier.run(error);
expect(result).toEqual({ expect(result).toEqual({
name: "TypeError", name: "TypeError",
message: "Api1.run is not a function", message: "Api1.run is not a function",
}); });
}); });
it("ReferenceError for platform function in sync field", () => { it("ReferenceError for platform function in data field", () => {
const error = new Error(); const error = new Error();
error.name = "ReferenceError"; error.name = "ReferenceError";
error.message = "storeValue is not defined"; error.message = "storeValue is not defined";
const result = errorModifier.run(error); const { errorMessage: result } = errorModifier.run(error);
expect(result).toEqual({ expect(result).toEqual({
name: "ValidationError", name: "ValidationError",
message: message:
"Found a reference to storeValue() during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to storeValue() and try again.", "Found a reference to storeValue() during evaluation. Data fields cannot execute framework actions. Please remove any direct/indirect references to storeValue() and try again.",
}); });
}); });
it("ReferenceError for undefined function in sync field", () => { it("ReferenceError for undefined function in data field", () => {
const error = new Error(); const error = new Error();
error.name = "ReferenceError"; error.name = "ReferenceError";
error.message = "storeValue2 is not defined"; error.message = "storeValue2 is not defined";
const result = errorModifier.run(error); const { errorMessage: result } = errorModifier.run(error);
expect(result).toEqual({ expect(result).toEqual({
name: error.name, name: error.name,
message: error.message, message: error.message,

View File

@ -76,6 +76,7 @@ describe("evaluateSync", () => {
message: "wrongJS is not defined", message: "wrongJS is not defined",
}, },
errorType: "PARSE", errorType: "PARSE",
kind: undefined,
raw: ` raw: `
function $$closedFn () { function $$closedFn () {
const $$result = wrongJS const $$result = wrongJS
@ -98,6 +99,7 @@ describe("evaluateSync", () => {
message: "{}.map is not a function", message: "{}.map is not a function",
}, },
errorType: "PARSE", errorType: "PARSE",
kind: undefined,
raw: ` raw: `
function $$closedFn () { function $$closedFn () {
const $$result = {}.map() const $$result = {}.map()
@ -128,6 +130,7 @@ describe("evaluateSync", () => {
message: "setImmediate is not defined", message: "setImmediate is not defined",
}, },
errorType: "PARSE", errorType: "PARSE",
kind: undefined,
raw: ` raw: `
function $$closedFn () { function $$closedFn () {
const $$result = setImmediate(() => {}, 100) const $$result = setImmediate(() => {}, 100)

View File

@ -1,9 +1,12 @@
import type { DataTree } from "entities/DataTree/dataTreeFactory"; import type { DataTree } from "entities/DataTree/dataTreeFactory";
import { getAllAsyncFunctions } from "@appsmith/workers/Evaluation/Actions"; import { getAllAsyncFunctions } from "@appsmith/workers/Evaluation/Actions";
import type { EvaluationError } from "utils/DynamicBindingUtils";
import { PropertyEvaluationErrorCategory } from "utils/DynamicBindingUtils";
const FOUND_ASYNC_IN_SYNC_EVAL_MESSAGE =
"Found an action invocation during evaluation. Data fields cannot execute actions.";
const UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR = const UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR =
"Found a reference to {{actionName}} during evaluation. Sync fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again."; "Found a reference to {{actionName}} during evaluation. Data fields cannot execute framework actions. Please remove any direct/indirect references to {{actionName}} and try again.";
class ErrorModifier { class ErrorModifier {
private errorNamesToScan = ["ReferenceError", "TypeError"]; private errorNamesToScan = ["ReferenceError", "TypeError"];
// Note all regex below groups the async function name // Note all regex below groups the async function name
@ -14,10 +17,23 @@ class ErrorModifier {
this.asyncFunctionsNameMap = getAllAsyncFunctions(dataTree); this.asyncFunctionsNameMap = getAllAsyncFunctions(dataTree);
} }
run(error: Error) { run(error: Error): {
errorMessage: ReturnType<typeof getErrorMessage>;
errorCategory?: PropertyEvaluationErrorCategory;
} {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
if (
error instanceof FoundPromiseInSyncEvalError ||
error instanceof ActionCalledInSyncFieldError
) {
return {
errorMessage,
errorCategory:
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD,
};
}
if (!this.errorNamesToScan.includes(error.name)) return errorMessage; if (!this.errorNamesToScan.includes(error.name)) return { errorMessage };
for (const asyncFunctionFullPath of Object.keys( for (const asyncFunctionFullPath of Object.keys(
this.asyncFunctionsNameMap, this.asyncFunctionsNameMap,
@ -25,27 +41,49 @@ class ErrorModifier {
const functionNameWithWhiteSpace = " " + asyncFunctionFullPath + " "; const functionNameWithWhiteSpace = " " + asyncFunctionFullPath + " ";
if (getErrorMessageWithType(error).match(functionNameWithWhiteSpace)) { if (getErrorMessageWithType(error).match(functionNameWithWhiteSpace)) {
return { return {
errorMessage: {
name: "ValidationError", name: "ValidationError",
message: UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR.replaceAll( message: UNDEFINED_ACTION_IN_SYNC_EVAL_ERROR.replaceAll(
"{{actionName}}", "{{actionName}}",
asyncFunctionFullPath + "()", asyncFunctionFullPath + "()",
), ),
},
errorCategory:
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD,
}; };
} }
} }
return errorMessage; return { errorMessage };
}
setAsyncInvocationErrorsRootcause(
errors: EvaluationError[],
asyncFunc: string,
) {
return errors.map((error) => {
if (isAsyncFunctionCalledInSyncFieldError(error)) {
error.errorMessage.message = FOUND_ASYNC_IN_SYNC_EVAL_MESSAGE;
error.kind = {
category:
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD,
rootcause: asyncFunc,
};
}
return error;
});
} }
} }
export const errorModifier = new ErrorModifier(); export const errorModifier = new ErrorModifier();
const FOUND_PROMISE_IN_SYNC_EVAL_MESSAGE =
"Found a Promise() during evaluation. Data fields cannot execute asynchronous code.";
export class FoundPromiseInSyncEvalError extends Error { export class FoundPromiseInSyncEvalError extends Error {
constructor() { constructor() {
super(); super();
this.name = ""; this.name = "";
this.message = this.message = FOUND_PROMISE_IN_SYNC_EVAL_MESSAGE;
"Found a Promise() during evaluation. Sync fields cannot execute asynchronous code.";
} }
} }
@ -54,7 +92,7 @@ export class ActionCalledInSyncFieldError extends Error {
super(actionName); super(actionName);
if (!actionName) { if (!actionName) {
this.message = "Async function called in a sync field"; this.message = "Async function called in a data field";
return; return;
} }
@ -81,3 +119,10 @@ export const getErrorMessage = (error: Error) => {
export const getErrorMessageWithType = (error: Error) => { export const getErrorMessageWithType = (error: Error) => {
return error.name ? `${error.name}: ${error.message}` : error.message; return error.name ? `${error.name}: ${error.message}` : error.message;
}; };
function isAsyncFunctionCalledInSyncFieldError(error: EvaluationError) {
return (
error.kind?.category ===
PropertyEvaluationErrorCategory.INVALID_JS_FUNCTION_INVOCATION_IN_DATA_FIELD
);
}

View File

@ -23,6 +23,7 @@ export enum EvaluationScriptType {
ANONYMOUS_FUNCTION = "ANONYMOUS_FUNCTION", ANONYMOUS_FUNCTION = "ANONYMOUS_FUNCTION",
ASYNC_ANONYMOUS_FUNCTION = "ASYNC_ANONYMOUS_FUNCTION", ASYNC_ANONYMOUS_FUNCTION = "ASYNC_ANONYMOUS_FUNCTION",
TRIGGERS = "TRIGGERS", TRIGGERS = "TRIGGERS",
OBJECT_PROPERTY = "OBJECT_PROPERTY",
} }
export const ScriptTemplate = "<<string>>"; export const ScriptTemplate = "<<string>>";
@ -58,6 +59,13 @@ export const EvaluationScripts: Record<EvaluationScriptType, string> = {
} }
$$closedFn.call(THIS_CONTEXT) $$closedFn.call(THIS_CONTEXT)
`, `,
[EvaluationScriptType.OBJECT_PROPERTY]: `
function $$closedFn () {
const $$result = {${ScriptTemplate}}
return $$result
}
$$closedFn.call(THIS_CONTEXT)
`,
}; };
const topLevelWorkerAPIs = Object.keys(self).reduce((acc, key: string) => { const topLevelWorkerAPIs = Object.keys(self).reduce((acc, key: string) => {
@ -120,8 +128,11 @@ export interface createEvaluationContextArgs {
context?: EvaluateContext; context?: EvaluateContext;
isTriggerBased: boolean; isTriggerBased: boolean;
evalArguments?: Array<unknown>; evalArguments?: Array<unknown>;
// Whether not to add functions like "run", "clear" to entity in global data /*
skipEntityFunctions?: boolean; Whether to remove functions like "run", "clear" from entities in global context
use case => To show lint warning when Api.run is used in a function bound to a data field (Eg. Button.text)
*/
removeEntityFunctions?: boolean;
} }
/** /**
* This method created an object with dataTree and appsmith's framework actions that needs to be added to worker global scope for the JS code evaluation to then consume it. * This method created an object with dataTree and appsmith's framework actions that needs to be added to worker global scope for the JS code evaluation to then consume it.
@ -135,8 +146,8 @@ export const createEvaluationContext = (args: createEvaluationContextArgs) => {
dataTree, dataTree,
evalArguments, evalArguments,
isTriggerBased, isTriggerBased,
removeEntityFunctions,
resolvedFunctions, resolvedFunctions,
skipEntityFunctions,
} = args; } = args;
const EVAL_CONTEXT: EvalContext = {}; const EVAL_CONTEXT: EvalContext = {};
@ -152,7 +163,7 @@ export const createEvaluationContext = (args: createEvaluationContextArgs) => {
addDataTreeToContext({ addDataTreeToContext({
EVAL_CONTEXT, EVAL_CONTEXT,
dataTree, dataTree,
skipEntityFunctions: !!skipEntityFunctions, removeEntityFunctions: !!removeEntityFunctions,
isTriggerBased, isTriggerBased,
}); });
@ -279,18 +290,23 @@ export default function evaluateSync(
result = indirectEval(script); result = indirectEval(script);
if (result instanceof Promise) { if (result instanceof Promise) {
/** /**
* If a promise is returned in sync field then show the error to help understand sync field doesn't await to resolve promise. * If a promise is returned in data field then show the error to help understand data field doesn't await to resolve promise.
* NOTE: Awaiting for promise will make sync field evaluation slower. * NOTE: Awaiting for promise will make data field evaluation slower.
*/ */
throw new FoundPromiseInSyncEvalError(); throw new FoundPromiseInSyncEvalError();
} }
} catch (error) { } catch (error) {
const { errorCategory, errorMessage } = errorModifier.run(error as Error);
errors.push({ errors.push({
errorMessage: errorModifier.run(error as Error), errorMessage,
severity: Severity.ERROR, severity: Severity.ERROR,
raw: script, raw: script,
errorType: PropertyEvaluationErrorType.PARSE, errorType: PropertyEvaluationErrorType.PARSE,
originalBinding: userScript, originalBinding: userScript,
kind: errorCategory && {
category: errorCategory,
rootcause: "",
},
}); });
} finally { } finally {
for (const entityName in evalContext) { for (const entityName in evalContext) {

View File

@ -1,7 +1,7 @@
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory"; import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory";
import type ReplayEntity from "entities/Replay"; import type ReplayEntity from "entities/Replay";
import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas"; import ReplayCanvas from "entities/Replay/ReplayEntity/ReplayCanvas";
import { isEmpty } from "lodash"; import { isEmpty, union } from "lodash";
import type { DependencyMap, EvalError } from "utils/DynamicBindingUtils"; import type { DependencyMap, EvalError } from "utils/DynamicBindingUtils";
import { EvalErrorTypes } from "utils/DynamicBindingUtils"; import { EvalErrorTypes } from "utils/DynamicBindingUtils";
import type { JSUpdate } from "utils/JSPaneUtils"; import type { JSUpdate } from "utils/JSPaneUtils";
@ -20,6 +20,8 @@ import type {
EvalWorkerSyncRequest, EvalWorkerSyncRequest,
} from "../types"; } from "../types";
import { clearAllIntervals } from "../fns/overrides/interval"; import { clearAllIntervals } from "../fns/overrides/interval";
import { jsPropertiesState } from "../JSObject/jsPropertiesState";
import { asyncJsFunctionInDataFields } from "../JSObject/asyncJSFunctionBoundToDataField";
export let replayMap: Record<string, ReplayEntity<any>>; export let replayMap: Record<string, ReplayEntity<any>>;
export let dataTreeEvaluator: DataTreeEvaluator | undefined; export let dataTreeEvaluator: DataTreeEvaluator | undefined;
export const CANVAS = "canvas"; export const CANVAS = "canvas";
@ -44,6 +46,7 @@ export default function (request: EvalWorkerSyncRequest) {
const { const {
allActionValidationConfig, allActionValidationConfig,
appMode,
forceEvaluation, forceEvaluation,
metaWidgets, metaWidgets,
requiresLinting, requiresLinting,
@ -59,6 +62,7 @@ export default function (request: EvalWorkerSyncRequest) {
try { try {
if (!dataTreeEvaluator) { if (!dataTreeEvaluator) {
isCreateFirstTree = true; isCreateFirstTree = true;
asyncJsFunctionInDataFields.initialize(appMode);
replayMap = replayMap || {}; replayMap = replayMap || {};
replayMap[CANVAS] = new ReplayCanvas({ widgets, theme }); replayMap[CANVAS] = new ReplayCanvas({ widgets, theme });
dataTreeEvaluator = new DataTreeEvaluator( dataTreeEvaluator = new DataTreeEvaluator(
@ -71,17 +75,25 @@ export default function (request: EvalWorkerSyncRequest) {
configTree, configTree,
); );
evalOrder = setupFirstTreeResponse.evalOrder; evalOrder = setupFirstTreeResponse.evalOrder;
lintOrder = setupFirstTreeResponse.lintOrder; lintOrder = union(
setupFirstTreeResponse.lintOrder,
jsPropertiesState.getUpdatedJSProperties(),
);
jsUpdates = setupFirstTreeResponse.jsUpdates; jsUpdates = setupFirstTreeResponse.jsUpdates;
initiateLinting( initiateLinting({
lintOrder, lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, { unevalTree: makeEntityConfigsAsObjProperties(
dataTreeEvaluator.oldUnEvalTree,
{
sanitizeDataTree: false, sanitizeDataTree: false,
}), },
),
requiresLinting, requiresLinting,
dataTreeEvaluator.oldConfigTree, jsPropertiesState: jsPropertiesState.getMap(),
); asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
configTree: dataTreeEvaluator.oldConfigTree,
});
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree(); const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, { dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
@ -113,17 +125,25 @@ export default function (request: EvalWorkerSyncRequest) {
); );
isCreateFirstTree = true; isCreateFirstTree = true;
evalOrder = setupFirstTreeResponse.evalOrder; evalOrder = setupFirstTreeResponse.evalOrder;
lintOrder = setupFirstTreeResponse.lintOrder; lintOrder = union(
setupFirstTreeResponse.lintOrder,
jsPropertiesState.getUpdatedJSProperties(),
);
jsUpdates = setupFirstTreeResponse.jsUpdates; jsUpdates = setupFirstTreeResponse.jsUpdates;
initiateLinting( initiateLinting({
lintOrder, lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, { unevalTree: makeEntityConfigsAsObjProperties(
dataTreeEvaluator.oldUnEvalTree,
{
sanitizeDataTree: false, sanitizeDataTree: false,
}), },
),
requiresLinting, requiresLinting,
dataTreeEvaluator.oldConfigTree, jsPropertiesState: jsPropertiesState.getMap(),
); asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
configTree: dataTreeEvaluator.oldConfigTree,
});
const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree(); const dataTreeResponse = dataTreeEvaluator.evalAndValidateFirstTree();
dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, { dataTree = makeEntityConfigsAsObjProperties(dataTreeResponse.evalTree, {
@ -146,20 +166,28 @@ export default function (request: EvalWorkerSyncRequest) {
); );
evalOrder = setupUpdateTreeResponse.evalOrder; evalOrder = setupUpdateTreeResponse.evalOrder;
lintOrder = setupUpdateTreeResponse.lintOrder; lintOrder = union(
setupUpdateTreeResponse.lintOrder,
jsPropertiesState.getUpdatedJSProperties(),
);
jsUpdates = setupUpdateTreeResponse.jsUpdates; jsUpdates = setupUpdateTreeResponse.jsUpdates;
unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates; unEvalUpdates = setupUpdateTreeResponse.unEvalUpdates;
pathsToClearErrorsFor = setupUpdateTreeResponse.pathsToClearErrorsFor; pathsToClearErrorsFor = setupUpdateTreeResponse.pathsToClearErrorsFor;
isNewWidgetAdded = setupUpdateTreeResponse.isNewWidgetAdded; isNewWidgetAdded = setupUpdateTreeResponse.isNewWidgetAdded;
initiateLinting( initiateLinting({
lintOrder, lintOrder,
makeEntityConfigsAsObjProperties(dataTreeEvaluator.oldUnEvalTree, { unevalTree: makeEntityConfigsAsObjProperties(
dataTreeEvaluator.oldUnEvalTree,
{
sanitizeDataTree: false, sanitizeDataTree: false,
}), },
),
requiresLinting, requiresLinting,
dataTreeEvaluator.oldConfigTree, jsPropertiesState: jsPropertiesState.getMap(),
); asyncJSFunctionsInDataFields: asyncJsFunctionInDataFields.getMap(),
configTree: dataTreeEvaluator.oldConfigTree,
});
nonDynamicFieldValidationOrder = nonDynamicFieldValidationOrder =
setupUpdateTreeResponse.nonDynamicFieldValidationOrder; setupUpdateTreeResponse.nonDynamicFieldValidationOrder;

View File

@ -18,6 +18,7 @@ import type { WidgetTypeConfigMap } from "utils/WidgetFactory";
import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator/types"; import type { EvalMetaUpdates } from "@appsmith/workers/common/DataTreeEvaluator/types";
import type { WorkerRequest } from "@appsmith/workers/common/types"; import type { WorkerRequest } from "@appsmith/workers/common/types";
import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils"; import type { DataTreeDiff } from "@appsmith/workers/Evaluation/evaluationUtils";
import type { APP_MODE } from "entities/App";
export type EvalWorkerSyncRequest = WorkerRequest<any, EVAL_WORKER_SYNC_ACTION>; export type EvalWorkerSyncRequest = WorkerRequest<any, EVAL_WORKER_SYNC_ACTION>;
export type EvalWorkerASyncRequest = WorkerRequest< export type EvalWorkerASyncRequest = WorkerRequest<
@ -38,6 +39,7 @@ export interface EvalTreeRequestData {
requiresLinting: boolean; requiresLinting: boolean;
forceEvaluation: boolean; forceEvaluation: boolean;
metaWidgets: MetaWidgetsReduxState; metaWidgets: MetaWidgetsReduxState;
appMode: APP_MODE | undefined;
} }
export interface EvalTreeResponseData { export interface EvalTreeResponseData {

View File

@ -1,5 +1,6 @@
import { ECMA_VERSION } from "@shared/ast"; import { ECMA_VERSION } from "@shared/ast";
import type { LintOptions } from "jshint"; import type { LintOptions } from "jshint";
import { isEntityFunction } from "./utils";
export const lintOptions = (globalData: Record<string, boolean>) => export const lintOptions = (globalData: Record<string, boolean>) =>
({ ({
@ -29,6 +30,8 @@ export const lintOptions = (globalData: Record<string, boolean>) =>
} as LintOptions); } as LintOptions);
export const JS_OBJECT_START_STATEMENT = "export default"; export const JS_OBJECT_START_STATEMENT = "export default";
export const INVALID_JSOBJECT_START_STATEMENT = `JSObject must start with '${JS_OBJECT_START_STATEMENT}'`; export const INVALID_JSOBJECT_START_STATEMENT = `JSObject must start with '${JS_OBJECT_START_STATEMENT}'`;
export const INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE =
"INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE";
// https://github.com/jshint/jshint/blob/d3d84ae1695359aef077ddb143f4be98001343b4/src/messages.js#L204 // https://github.com/jshint/jshint/blob/d3d84ae1695359aef077ddb143f4be98001343b4/src/messages.js#L204
export const IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE = "W117"; export const IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE = "W117";
@ -37,10 +40,14 @@ export const IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE = "W117";
export const WARNING_LINT_ERRORS = { export const WARNING_LINT_ERRORS = {
W098: "'{a}' is defined but never used.", W098: "'{a}' is defined but never used.",
W014: "Misleading line break before '{a}'; readers may interpret this as an expression boundary.", W014: "Misleading line break before '{a}'; readers may interpret this as an expression boundary.",
ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD:
"Cannot execute async code on functions bound to data fields",
}; };
export function asyncActionInSyncFieldLintMessage(actionName: string) { export function asyncActionInSyncFieldLintMessage(isJsObject = false) {
return `Async framework action "${actionName}" cannot be executed in a function that is bound to a sync field.`; return isJsObject
? `Cannot execute async code on functions bound to data fields`
: `Data fields cannot execute async code`;
} }
/** These errors should be overlooked /** These errors should be overlooked
@ -54,7 +61,9 @@ export const SUPPORTED_WEB_APIS = {
}; };
export enum CustomLintErrorCode { export enum CustomLintErrorCode {
INVALID_ENTITY_PROPERTY = "INVALID_ENTITY_PROPERTY", INVALID_ENTITY_PROPERTY = "INVALID_ENTITY_PROPERTY",
ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD = "ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD",
} }
export const CUSTOM_LINT_ERRORS: Record< export const CUSTOM_LINT_ERRORS: Record<
CustomLintErrorCode, CustomLintErrorCode,
(...args: any[]) => string (...args: any[]) => string
@ -62,5 +71,26 @@ export const CUSTOM_LINT_ERRORS: Record<
[CustomLintErrorCode.INVALID_ENTITY_PROPERTY]: ( [CustomLintErrorCode.INVALID_ENTITY_PROPERTY]: (
entityName: string, entityName: string,
propertyName: string, propertyName: string,
) => `"${propertyName}" doesn't exist in ${entityName}`, entity: unknown,
isJsObject: boolean,
) =>
isEntityFunction(entity, propertyName)
? asyncActionInSyncFieldLintMessage(isJsObject)
: `"${propertyName}" doesn't exist in ${entityName}`,
[CustomLintErrorCode.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD]: (
dataFieldBindings: string[],
fullName: string,
isMarkedAsync: boolean,
) => {
const hasMultipleBindings = dataFieldBindings.length > 1;
const bindings = dataFieldBindings.join(" , ");
return isMarkedAsync
? `Cannot bind async functions to data fields. Convert this to a sync function or remove references to "${fullName}" on the following data ${
hasMultipleBindings ? "fields" : "field"
}: ${bindings}`
: `Functions bound to data fields cannot execute async code. Remove async statements highlighted below or remove references to "${fullName}" on the following data ${
hasMultipleBindings ? "fields" : "field"
}: ${bindings}`;
},
}; };

View 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();

View File

@ -1,107 +1,55 @@
import { getEntityNameAndPropertyPath } from "@appsmith/workers/Evaluation/evaluationUtils";
import { get, isEmpty, set } from "lodash";
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
import type { LintError } from "utils/DynamicBindingUtils";
import { globalData } from "./globalData";
import type {
getlintErrorsFromTreeProps,
getlintErrorsFromTreeResponse,
} from "./types";
import { import {
getEntityNameAndPropertyPath, lintBindingPath,
isATriggerPath, lintJSObjectBody,
isJSAction, lintJSObjectProperty,
} from "ce/workers/Evaluation/evaluationUtils"; lintTriggerPath,
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory"; sortLintingPathsByType,
import { get, set } from "lodash"; } from "./utils";
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers";
import { createEvaluationContext } from "workers/Evaluation/evaluate";
import { getActionTriggerFunctionNames } from "workers/Evaluation/fns";
import { lintBindingPath, lintTriggerPath, pathRequiresLinting } from "./utils";
export function getlintErrorsFromTree( export function getlintErrorsFromTree({
pathsToLint: string[], asyncJSFunctionsInDataFields,
unEvalTree: DataTree, cloudHosting,
configTree: ConfigTree, configTree,
cloudHosting: boolean, jsPropertiesState,
): LintErrors { pathsToLint,
const lintTreeErrors: LintErrors = {}; unEvalTree,
}: getlintErrorsFromTreeProps): getlintErrorsFromTreeResponse {
const evalContext = createEvaluationContext({ const lintTreeErrors: LintErrorsStore = {};
dataTree: unEvalTree, const updatedJSEntities = new Set<string>();
resolvedFunctions: {}, globalData.initialize(unEvalTree, cloudHosting);
isTriggerBased: false, const { bindingPaths, jsObjectPaths, triggerPaths } = sortLintingPathsByType(
skipEntityFunctions: true, pathsToLint,
}); unEvalTree,
configTree,
const platformFnNamesMap = Object.values(
getActionTriggerFunctionNames(cloudHosting),
).reduce(
(acc, name) => ({ ...acc, [name]: true }),
{} as { [x: string]: boolean },
); );
Object.assign(evalContext, platformFnNamesMap);
const evalContextWithoutFunctions = createEvaluationContext({ // Lint binding paths
dataTree: unEvalTree, bindingPaths.forEach((bindingPath) => {
resolvedFunctions: {}, const { entityName } = getEntityNameAndPropertyPath(bindingPath);
isTriggerBased: true,
skipEntityFunctions: true,
});
// trigger paths
const triggerPaths = new Set<string>();
// Certain paths, like JS Object's body are binding paths where appsmith functions are needed in the global data
const bindingPathsRequiringFunctions = new Set<string>();
pathsToLint.forEach((fullPropertyPath) => {
const { entityName, propertyPath } =
getEntityNameAndPropertyPath(fullPropertyPath);
const entity = unEvalTree[entityName];
const entityConfig = configTree[entityName];
const unEvalPropertyValue = get(
unEvalTree,
fullPropertyPath,
) 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,
});
set(lintTreeErrors, `["${fullPropertyPath}"]`, 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 binding paths that need GLOBAL_DATA_WITH_FUNCTIONS
if (bindingPathsRequiringFunctions.size) {
bindingPathsRequiringFunctions.forEach((fullPropertyPath) => {
const { entityName } = getEntityNameAndPropertyPath(fullPropertyPath);
const entity = unEvalTree[entityName]; const entity = unEvalTree[entityName];
const unEvalPropertyValue = get( const unEvalPropertyValue = get(
unEvalTree, unEvalTree,
fullPropertyPath, bindingPath,
) as unknown as string; ) as unknown as string;
// remove all lint errors from path
set(lintTreeErrors, `["${fullPropertyPath}"]`, []);
const lintErrors = lintBindingPath({ const lintErrors = lintBindingPath({
dynamicBinding: unEvalPropertyValue, dynamicBinding: unEvalPropertyValue,
entity, entity,
fullPropertyPath, fullPropertyPath: bindingPath,
globalData: evalContext, globalData: globalData.getGlobalData(false),
}); });
set(lintTreeErrors, `["${fullPropertyPath}"]`, lintErrors); set(lintTreeErrors, `["${bindingPath}"]`, lintErrors);
}); });
}
// Lint triggerPaths // Lint TriggerPaths
if (triggerPaths.size) {
triggerPaths.forEach((triggerPath) => { triggerPaths.forEach((triggerPath) => {
const { entityName } = getEntityNameAndPropertyPath(triggerPath); const { entityName } = getEntityNameAndPropertyPath(triggerPath);
const entity = unEvalTree[entityName]; const entity = unEvalTree[entityName];
@ -112,15 +60,51 @@ export function getlintErrorsFromTree(
// remove all lint errors from path // remove all lint errors from path
set(lintTreeErrors, `["${triggerPath}"]`, []); set(lintTreeErrors, `["${triggerPath}"]`, []);
const lintErrors = lintTriggerPath({ const lintErrors = lintTriggerPath({
globalData: evalContext,
userScript: unEvalPropertyValue, userScript: unEvalPropertyValue,
entity, entity,
fullPropertyPath: triggerPath, globalData: globalData.getGlobalData(true),
}); });
set(lintTreeErrors, `["${triggerPath}"]`, lintErrors); 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),
};
} }

View File

@ -64,18 +64,32 @@ function eventRequestHandler({
}): LintTreeResponse | unknown { }): LintTreeResponse | unknown {
switch (method) { switch (method) {
case LINT_WORKER_ACTIONS.LINT_TREE: { case LINT_WORKER_ACTIONS.LINT_TREE: {
const lintTreeResponse: LintTreeResponse = { errors: {} }; const lintTreeResponse: LintTreeResponse = {
errors: {},
updatedJSEntities: [],
};
try { try {
const { cloudHosting, configTree, pathsToLint, unevalTree } = const {
requestData as LintTreeRequest; asyncJSFunctionsInDataFields,
const lintErrors = getlintErrorsFromTree(
pathsToLint,
unevalTree,
configTree,
cloudHosting, cloudHosting,
configTree,
jsPropertiesState,
pathsToLint,
unevalTree: unEvalTree,
} = requestData as LintTreeRequest;
const { errors: lintErrors, updatedJSEntities } = getlintErrorsFromTree(
{
pathsToLint,
unEvalTree,
jsPropertiesState,
cloudHosting,
asyncJSFunctionsInDataFields,
configTree,
},
); );
lintTreeResponse.errors = lintErrors; lintTreeResponse.errors = lintErrors;
lintTreeResponse.updatedJSEntities = updatedJSEntities;
} catch (e) {} } catch (e) {}
return lintTreeResponse; return lintTreeResponse;
} }
@ -97,6 +111,7 @@ function eventRequestHandler({
} }
return true; return true;
} }
default: { default: {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error("Action not registered on lintWorker ", method); console.error("Action not registered on lintWorker ", method);

View File

@ -1,6 +1,16 @@
import type { ConfigTree, DataTree } from "entities/DataTree/dataTreeFactory"; import type {
import type { LintErrors } from "reducers/lintingReducers/lintErrorsReducers"; ConfigTree,
DataTree,
DataTreeEntity,
} from "entities/DataTree/dataTreeFactory";
import type { LintErrorsStore } from "reducers/lintingReducers/lintErrorsReducers";
import type { WorkerRequest } from "@appsmith/workers/common/types"; import type { WorkerRequest } from "@appsmith/workers/common/types";
import type {
createEvaluationContext,
EvaluationScriptType,
} from "workers/Evaluation/evaluate";
import type { DependencyMap } from "utils/DynamicBindingUtils";
import type { TJSPropertiesState } from "workers/Evaluation/JSObject/jsPropertiesState";
export enum LINT_WORKER_ACTIONS { export enum LINT_WORKER_ACTIONS {
LINT_TREE = "LINT_TREE", LINT_TREE = "LINT_TREE",
@ -8,14 +18,17 @@ export enum LINT_WORKER_ACTIONS {
} }
export interface LintTreeResponse { export interface LintTreeResponse {
errors: LintErrors; errors: LintErrorsStore;
updatedJSEntities: string[];
} }
export interface LintTreeRequest { export interface LintTreeRequest {
pathsToLint: string[]; pathsToLint: string[];
unevalTree: DataTree; unevalTree: DataTree;
jsPropertiesState: TJSPropertiesState;
configTree: ConfigTree; configTree: ConfigTree;
cloudHosting: boolean; cloudHosting: boolean;
asyncJSFunctionsInDataFields: DependencyMap;
} }
export type LintWorkerRequest = WorkerRequest< export type LintWorkerRequest = WorkerRequest<
@ -26,5 +39,54 @@ export type LintWorkerRequest = WorkerRequest<
export type LintTreeSagaRequestData = { export type LintTreeSagaRequestData = {
pathsToLint: string[]; pathsToLint: string[];
unevalTree: DataTree; unevalTree: DataTree;
jsPropertiesState: TJSPropertiesState;
asyncJSFunctionsInDataFields: DependencyMap;
configTree: ConfigTree; configTree: ConfigTree;
}; };
export interface lintTriggerPathProps {
userScript: string;
entity: DataTreeEntity;
globalData: ReturnType<typeof createEvaluationContext>;
}
export interface lintBindingPathProps {
dynamicBinding: string;
entity: DataTreeEntity;
fullPropertyPath: string;
globalData: ReturnType<typeof createEvaluationContext>;
}
export interface getLintingErrorsProps {
script: string;
data: Record<string, unknown>;
// {{user's code}}
originalBinding: string;
scriptType: EvaluationScriptType;
options?: {
isJsObject: boolean;
};
}
export interface getlintErrorsFromTreeProps {
pathsToLint: string[];
unEvalTree: DataTree;
jsPropertiesState: TJSPropertiesState;
cloudHosting: boolean;
asyncJSFunctionsInDataFields: DependencyMap;
configTree: ConfigTree;
}
export interface getlintErrorsFromTreeResponse {
errors: LintErrorsStore;
updatedJSEntities: string[];
}
export interface initiateLintingProps {
asyncJSFunctionsInDataFields: DependencyMap;
lintOrder: string[];
unevalTree: DataTree;
requiresLinting: boolean;
jsPropertiesState: TJSPropertiesState;
configTree: ConfigTree;
}

View File

@ -1,30 +1,27 @@
import type { import type {
ConfigTree,
DataTree, DataTree,
DataTreeEntity, DataTreeEntity,
DataTreeEntityConfig, ConfigTree,
} from "entities/DataTree/dataTreeFactory"; } from "entities/DataTree/dataTreeFactory";
import type { Position } from "codemirror"; import type { Position } from "codemirror";
import type { LintError } from "utils/DynamicBindingUtils"; import type { LintError } from "utils/DynamicBindingUtils";
import { import type { DependencyMap } from "utils/DynamicBindingUtils";
isDynamicValue,
isPathADynamicBinding,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions"; import { MAIN_THREAD_ACTION } from "@appsmith/workers/Evaluation/evalWorkerActions";
import type { LintError as JSHintError } from "jshint";
import { JSHINT as jshint } from "jshint"; import { JSHINT as jshint } from "jshint";
import { get, isEmpty, isNumber, keys, last } from "lodash"; import type { LintError as JSHintError } from "jshint";
import { isEmpty, isNil, isNumber, keys, last } from "lodash";
import type { MemberExpressionData } from "@shared/ast"; import type { MemberExpressionData } from "@shared/ast";
import { import {
extractInvalidTopLevelMemberExpressionsFromCode, extractInvalidTopLevelMemberExpressionsFromCode,
isLiteralNode, isLiteralNode,
} from "@shared/ast"; } from "@shared/ast";
import { getDynamicBindings } from "utils/DynamicBindingUtils";
import type { createEvaluationContext } from "workers/Evaluation/evaluate";
import { import {
getDynamicBindings,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import {
createEvaluationContext,
EvaluationScripts, EvaluationScripts,
EvaluationScriptType, EvaluationScriptType,
getScriptToEval, getScriptToEval,
@ -33,13 +30,11 @@ import {
} from "workers/Evaluation/evaluate"; } from "workers/Evaluation/evaluate";
import { import {
getEntityNameAndPropertyPath, getEntityNameAndPropertyPath,
isAction,
isATriggerPath, isATriggerPath,
isDataTreeEntity,
isDynamicLeaf,
isJSAction, isJSAction,
isWidget,
} from "@appsmith/workers/Evaluation/evaluationUtils"; } from "@appsmith/workers/Evaluation/evaluationUtils";
import { Severity } from "entities/AppsmithConsole";
import { JSLibraries } from "workers/common/JSLibrary";
import { WorkerMessenger } from "workers/Evaluation/fns/utils/Messenger"; import { WorkerMessenger } from "workers/Evaluation/fns/utils/Messenger";
import { import {
asyncActionInSyncFieldLintMessage, asyncActionInSyncFieldLintMessage,
@ -48,19 +43,33 @@ import {
IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE, IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE,
IGNORED_LINT_ERRORS, IGNORED_LINT_ERRORS,
INVALID_JSOBJECT_START_STATEMENT, INVALID_JSOBJECT_START_STATEMENT,
INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
JS_OBJECT_START_STATEMENT, JS_OBJECT_START_STATEMENT,
lintOptions, lintOptions,
SUPPORTED_WEB_APIS, SUPPORTED_WEB_APIS,
WARNING_LINT_ERRORS, WARNING_LINT_ERRORS,
} from "./constants"; } from "./constants";
import { APPSMITH_GLOBAL_FUNCTIONS } from "components/editorComponents/ActionCreator/constants"; import { APPSMITH_GLOBAL_FUNCTIONS } from "components/editorComponents/ActionCreator/constants";
import type {
getLintingErrorsProps,
initiateLintingProps,
lintBindingPathProps,
LintTreeSagaRequestData,
lintTriggerPathProps,
} from "./types";
import { JSLibraries } from "workers/common/JSLibrary";
import { Severity } from "entities/AppsmithConsole";
import {
entityFns,
getActionTriggerFunctionNames,
} from "workers/Evaluation/fns";
import type {
TJSFunctionPropertyState,
TJSpropertyState,
} from "workers/Evaluation/JSObject/jsPropertiesState";
import type { JSActionEntity } from "entities/DataTree/types";
import { globalData } from "./globalData";
interface lintBindingPathProps {
dynamicBinding: string;
entity: DataTreeEntity;
fullPropertyPath: string;
globalData: ReturnType<typeof createEvaluationContext>;
}
export function lintBindingPath({ export function lintBindingPath({
dynamicBinding, dynamicBinding,
entity, entity,
@ -68,30 +77,6 @@ export function lintBindingPath({
globalData, globalData,
}: lintBindingPathProps) { }: lintBindingPathProps) {
let lintErrors: LintError[] = []; let lintErrors: LintError[] = [];
if (isJSAction(entity)) {
if (!entity.body) return lintErrors;
if (!entity.body.startsWith(JS_OBJECT_START_STATEMENT)) {
return lintErrors.concat([
{
errorType: PropertyEvaluationErrorType.LINT,
errorSegment: "",
originalBinding: entity.body,
line: 0,
ch: 0,
code: entity.body,
variables: [],
raw: entity.body,
errorMessage: {
name: "LintingError",
message: INVALID_JSOBJECT_START_STATEMENT,
},
severity: Severity.ERROR,
},
]);
}
}
const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath); const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath);
// Get the {{binding}} bound values // Get the {{binding}} bound values
const { jsSnippets, stringSegments } = getDynamicBindings( const { jsSnippets, stringSegments } = getDynamicBindings(
@ -116,8 +101,6 @@ export function lintBindingPath({
data: globalData, data: globalData,
originalBinding, originalBinding,
scriptType, scriptType,
entity,
fullPropertyPath,
}); });
lintErrors = lintErrors.concat(lintErrorsFromSnippet); lintErrors = lintErrors.concat(lintErrorsFromSnippet);
} }
@ -125,15 +108,9 @@ export function lintBindingPath({
} }
return lintErrors; return lintErrors;
} }
interface lintTriggerPathProps {
userScript: string;
entity: DataTreeEntity;
globalData: ReturnType<typeof createEvaluationContext>;
fullPropertyPath: string;
}
export function lintTriggerPath({ export function lintTriggerPath({
entity, entity,
fullPropertyPath,
globalData, globalData,
userScript, userScript,
}: lintTriggerPathProps) { }: lintTriggerPathProps) {
@ -145,35 +122,9 @@ export function lintTriggerPath({
data: globalData, data: globalData,
originalBinding: jsSnippets[0], originalBinding: jsSnippets[0],
scriptType: EvaluationScriptType.TRIGGERS, scriptType: EvaluationScriptType.TRIGGERS,
entity,
fullPropertyPath,
}); });
} }
export function pathRequiresLinting(
dataTree: DataTree,
entity: DataTreeEntity,
fullPropertyPath: string,
entityConfig: DataTreeEntityConfig,
): boolean {
const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath);
const unEvalPropertyValue = get(
dataTree,
fullPropertyPath,
) as unknown as string;
if (isATriggerPath(entityConfig, propertyPath)) {
return isDynamicValue(unEvalPropertyValue);
}
const isADynamicBindingPath =
(isAction(entity) || isWidget(entity) || isJSAction(entity)) &&
isPathADynamicBinding(entityConfig, propertyPath);
const requiresLinting =
(isADynamicBindingPath && isDynamicValue(unEvalPropertyValue)) ||
isJSAction(entity);
return requiresLinting;
}
// Removes "export default" statement from js Object // Removes "export default" statement from js Object
export function getJSToLint( export function getJSToLint(
entity: DataTreeEntity, entity: DataTreeEntity,
@ -278,19 +229,26 @@ function sanitizeJSHintErrors(
return result; return result;
}, []); }, []);
} }
const getLintSeverity = (code: string): Severity.WARNING | Severity.ERROR => { const getLintSeverity = (
code: string,
errorMessage: string,
): Severity.WARNING | Severity.ERROR => {
const severity = const severity =
code in WARNING_LINT_ERRORS ? Severity.WARNING : Severity.ERROR; code in WARNING_LINT_ERRORS ||
errorMessage === asyncActionInSyncFieldLintMessage(true)
? Severity.WARNING
: Severity.ERROR;
return severity; return severity;
}; };
const getLintErrorMessage = ( const getLintErrorMessage = (
reason: string, reason: string,
code: string, code: string,
variables: string[], variables: string[],
isJSObject = false,
): string => { ): string => {
switch (code) { switch (code) {
case IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE: { case IDENTIFIER_NOT_DEFINED_LINT_ERROR_CODE: {
return getRefinedW117Error(variables[0], reason); return getRefinedW117Error(variables[0], reason, isJSObject);
} }
default: { default: {
return reason; return reason;
@ -302,6 +260,7 @@ function convertJsHintErrorToAppsmithLintError(
script: string, script: string,
originalBinding: string, originalBinding: string,
scriptPos: Position, scriptPos: Position,
isJSObject = false,
): LintError { ): LintError {
const { a, b, c, code, d, evidence, reason } = jsHintError; const { a, b, c, code, d, evidence, reason } = jsHintError;
@ -311,14 +270,20 @@ function convertJsHintErrorToAppsmithLintError(
jsHintError.line === scriptPos.line jsHintError.line === scriptPos.line
? jsHintError.character - scriptPos.ch ? jsHintError.character - scriptPos.ch
: jsHintError.character; : jsHintError.character;
const lintErrorMessage = getLintErrorMessage(
reason,
code,
[a, b, c, d],
isJSObject,
);
return { return {
errorType: PropertyEvaluationErrorType.LINT, errorType: PropertyEvaluationErrorType.LINT,
raw: script, raw: script,
severity: getLintSeverity(code), severity: getLintSeverity(code, lintErrorMessage),
errorMessage: { errorMessage: {
name: "LintingError", name: "LintingError",
message: getLintErrorMessage(reason, code, [a, b, c, d]), message: lintErrorMessage,
}, },
errorSegment: evidence, errorSegment: evidence,
originalBinding, originalBinding,
@ -329,17 +294,10 @@ function convertJsHintErrorToAppsmithLintError(
ch: actualErrorCh, ch: actualErrorCh,
}; };
} }
interface getLintingErrorsProps {
script: string;
data: Record<string, unknown>;
// {{user's code}}
originalBinding: string;
scriptType: EvaluationScriptType;
entity: DataTreeEntity;
fullPropertyPath: string;
}
export function getLintingErrors({ export function getLintingErrors({
data, data,
options,
originalBinding, originalBinding,
script, script,
scriptType, scriptType,
@ -356,6 +314,7 @@ export function getLintingErrors({
script, script,
originalBinding, originalBinding,
scriptPos, scriptPos,
options?.isJsObject,
), ),
); );
const invalidPropertyErrors = getInvalidPropertyErrorsFromScript( const invalidPropertyErrors = getInvalidPropertyErrorsFromScript(
@ -363,6 +322,7 @@ export function getLintingErrors({
data, data,
scriptPos, scriptPos,
originalBinding, originalBinding,
options?.isJsObject,
); );
return jshintErrors.concat(invalidPropertyErrors); return jshintErrors.concat(invalidPropertyErrors);
} }
@ -373,6 +333,7 @@ function getInvalidPropertyErrorsFromScript(
data: Record<string, unknown>, data: Record<string, unknown>,
scriptPos: Position, scriptPos: Position,
originalBinding: string, originalBinding: string,
isJSObject = false,
): LintError[] { ): LintError[] {
let invalidTopLevelMemberExpressions: MemberExpressionData[] = []; let invalidTopLevelMemberExpressions: MemberExpressionData[] = [];
try { try {
@ -394,15 +355,19 @@ function getInvalidPropertyErrorsFromScript(
const propertyStartColumn = !isLiteralNode(property) const propertyStartColumn = !isLiteralNode(property)
? property.loc.start.column + 1 ? property.loc.start.column + 1
: property.loc.start.column + 2; : property.loc.start.column + 2;
const lintErrorMessage = CUSTOM_LINT_ERRORS[
CustomLintErrorCode.INVALID_ENTITY_PROPERTY
](object.name, propertyName, data[object.name], isJSObject);
return { return {
errorType: PropertyEvaluationErrorType.LINT, errorType: PropertyEvaluationErrorType.LINT,
raw: script, raw: script,
severity: getLintSeverity(CustomLintErrorCode.INVALID_ENTITY_PROPERTY), severity: getLintSeverity(
CustomLintErrorCode.INVALID_ENTITY_PROPERTY,
lintErrorMessage,
),
errorMessage: { errorMessage: {
name: "LintingError", name: "LintingError",
message: CUSTOM_LINT_ERRORS[ message: lintErrorMessage,
CustomLintErrorCode.INVALID_ENTITY_PROPERTY
](object.name, propertyName),
}, },
errorSegment: `${object.name}.${propertyName}`, errorSegment: `${object.name}.${propertyName}`,
originalBinding, originalBinding,
@ -419,19 +384,24 @@ function getInvalidPropertyErrorsFromScript(
return invalidPropertyErrors; return invalidPropertyErrors;
} }
export function initiateLinting( export function initiateLinting({
lintOrder: string[], asyncJSFunctionsInDataFields,
unevalTree: DataTree, configTree,
requiresLinting: boolean, jsPropertiesState,
configTree: ConfigTree, lintOrder,
) { requiresLinting,
unevalTree,
}: initiateLintingProps) {
const data = {
pathsToLint: lintOrder,
unevalTree,
jsPropertiesState,
asyncJSFunctionsInDataFields,
configTree,
} as LintTreeSagaRequestData;
if (!requiresLinting) return; if (!requiresLinting) return;
WorkerMessenger.ping({ WorkerMessenger.ping({
data: { data,
lintOrder,
unevalTree,
configTree,
},
method: MAIN_THREAD_ACTION.LINT_TREE, method: MAIN_THREAD_ACTION.LINT_TREE,
}); });
} }
@ -439,14 +409,227 @@ export function initiateLinting(
export function getRefinedW117Error( export function getRefinedW117Error(
undefinedVar: string, undefinedVar: string,
originalReason: string, originalReason: string,
isJsObject = false,
) { ) {
// Refine error message for await using in field not marked as async // Refine error message for await using in field not marked as async
if (undefinedVar === "await") { if (undefinedVar === "await") {
return "'await' expressions are only allowed within async functions. Did you mean to mark this function as 'async'?"; return "'await' expressions are only allowed within async functions. Did you mean to mark this function as 'async'?";
} }
// Handle case where platform functions are used in sync fields // Handle case where platform functions are used in data fields
if (APPSMITH_GLOBAL_FUNCTIONS.hasOwnProperty(undefinedVar)) { if (APPSMITH_GLOBAL_FUNCTIONS.hasOwnProperty(undefinedVar)) {
return asyncActionInSyncFieldLintMessage(undefinedVar); return asyncActionInSyncFieldLintMessage(isJsObject);
} }
return originalReason; return originalReason;
} }
export function lintJSProperty(
jsPropertyFullName: string,
jsPropertyState: TJSpropertyState,
globalData: DataTree,
): LintError[] {
if (isNil(jsPropertyState)) {
return [];
}
const { propertyPath: jsPropertyPath } =
getEntityNameAndPropertyPath(jsPropertyFullName);
const scriptType = getScriptType(false, false);
const scriptToLint = getScriptToEval(
jsPropertyState.value,
EvaluationScriptType.OBJECT_PROPERTY,
);
const propLintErrors = getLintingErrors({
script: scriptToLint,
data: globalData,
originalBinding: jsPropertyState.value,
scriptType,
options: { isJsObject: true },
});
const refinedErrors = propLintErrors.map((lintError) => {
return {
...lintError,
line: lintError.line + jsPropertyState.position.startLine - 1,
ch:
lintError.line === 0
? lintError.ch + jsPropertyState.position.startColumn
: lintError.ch,
originalPath: jsPropertyPath,
};
});
return refinedErrors;
}
export function lintJSObjectProperty(
jsPropertyFullName: string,
jsObjectState: Record<string, TJSpropertyState>,
asyncJSFunctionsInDataFields: DependencyMap,
) {
let lintErrors: LintError[] = [];
const { propertyPath: jsPropertyName } =
getEntityNameAndPropertyPath(jsPropertyFullName);
const jsPropertyState = jsObjectState[jsPropertyName];
const isAsyncJSFunctionBoundToSyncField =
asyncJSFunctionsInDataFields.hasOwnProperty(jsPropertyFullName);
const jsPropertyLintErrors = lintJSProperty(
jsPropertyFullName,
jsPropertyState,
globalData.getGlobalData(!isAsyncJSFunctionBoundToSyncField),
);
lintErrors = lintErrors.concat(jsPropertyLintErrors);
// if function is async, and bound to a data field, then add custom lint error
if (isAsyncJSFunctionBoundToSyncField) {
lintErrors.push(
generateAsyncFunctionBoundToDataFieldCustomError(
asyncJSFunctionsInDataFields[jsPropertyFullName],
jsPropertyState,
jsPropertyFullName,
),
);
}
return lintErrors;
}
export function lintJSObjectBody(
jsObjectName: string,
globalData: DataTree,
): LintError[] {
const jsObject = globalData[jsObjectName];
const rawJSObjectbody = (jsObject as unknown as JSActionEntity).body;
if (!rawJSObjectbody) return [];
if (!rawJSObjectbody.startsWith(JS_OBJECT_START_STATEMENT)) {
return [
{
errorType: PropertyEvaluationErrorType.LINT,
errorSegment: "",
originalBinding: rawJSObjectbody,
line: 0,
ch: 0,
code: INVALID_JSOBJECT_START_STATEMENT_ERROR_CODE,
variables: [],
raw: rawJSObjectbody,
errorMessage: {
name: "LintingError",
message: INVALID_JSOBJECT_START_STATEMENT,
},
severity: Severity.ERROR,
},
];
}
const scriptType = getScriptType(false, false);
const jsbodyToLint = getJSToLint(jsObject, rawJSObjectbody, "body"); // remove "export default"
const scriptToLint = getScriptToEval(jsbodyToLint, scriptType);
return getLintingErrors({
script: scriptToLint,
data: globalData,
originalBinding: jsbodyToLint,
scriptType,
});
}
export function getEvaluationContext(
unevalTree: DataTree,
cloudHosting: boolean,
options: { withFunctions: boolean },
) {
if (!options.withFunctions)
return createEvaluationContext({
dataTree: unevalTree,
resolvedFunctions: {},
isTriggerBased: false,
removeEntityFunctions: true,
});
const evalContext = createEvaluationContext({
dataTree: unevalTree,
resolvedFunctions: {},
isTriggerBased: false,
removeEntityFunctions: false,
});
const platformFnNamesMap = Object.values(
getActionTriggerFunctionNames(cloudHosting),
).reduce(
(acc, name) => ({ ...acc, [name]: true }),
{} as { [x: string]: boolean },
);
Object.assign(evalContext, platformFnNamesMap);
return evalContext;
}
export function sortLintingPathsByType(
pathsToLint: string[],
unevalTree: DataTree,
configTree: ConfigTree,
) {
const triggerPaths = new Set<string>();
const bindingPaths = new Set<string>();
const jsObjectPaths = new Set<string>();
for (const fullPropertyPath of pathsToLint) {
const { entityName, propertyPath } =
getEntityNameAndPropertyPath(fullPropertyPath);
const entity = unevalTree[entityName];
const entityConfig = configTree[entityName];
// We are only interested in dynamic leaves
if (!isDynamicLeaf(unevalTree, fullPropertyPath, configTree)) continue;
if (isATriggerPath(entityConfig, propertyPath)) {
triggerPaths.add(fullPropertyPath);
continue;
}
if (isJSAction(entity)) {
jsObjectPaths.add(fullPropertyPath);
continue;
}
bindingPaths.add(fullPropertyPath);
}
return { triggerPaths, bindingPaths, jsObjectPaths };
}
function generateAsyncFunctionBoundToDataFieldCustomError(
dataFieldBindings: string[],
jsPropertyState: TJSpropertyState,
jsPropertyFullName: string,
): LintError {
const { propertyPath: jsPropertyName } =
getEntityNameAndPropertyPath(jsPropertyFullName);
const lintErrorMessage =
CUSTOM_LINT_ERRORS.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD(
dataFieldBindings,
jsPropertyFullName,
(jsPropertyState as TJSFunctionPropertyState).isMarkedAsync,
);
return {
errorType: PropertyEvaluationErrorType.LINT,
raw: jsPropertyState.value,
severity: getLintSeverity(
CustomLintErrorCode.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD,
lintErrorMessage,
),
errorMessage: {
name: "LintingError",
message: lintErrorMessage,
},
errorSegment: jsPropertyFullName,
originalBinding: jsPropertyState.value,
// By keeping track of these variables we can highlight the exact text that caused the error.
variables: [jsPropertyName, null, null, null],
code: CustomLintErrorCode.ASYNC_FUNCTION_BOUND_TO_SYNC_FIELD,
line: jsPropertyState.position.keyStartLine - 1,
ch: jsPropertyState.position.keyStartColumn + 1,
originalPath: jsPropertyName,
};
}
export function isEntityFunction(entity: unknown, propertyName: string) {
if (!isDataTreeEntity(entity)) return false;
return entityFns.find(
(entityFn) =>
entityFn.name === propertyName &&
entityFn.qualifier(entity as DataTreeEntity),
);
}

View File

@ -97,7 +97,10 @@ import {
getUpdatedLocalUnEvalTreeAfterJSUpdates, getUpdatedLocalUnEvalTreeAfterJSUpdates,
parseJSActions, parseJSActions,
} from "workers/Evaluation/JSObject"; } from "workers/Evaluation/JSObject";
import { getFixedTimeDifference } from "./utils"; import {
addRootcauseToAsyncInvocationErrors,
getFixedTimeDifference,
} from "./utils";
import { isJSObjectFunction } from "workers/Evaluation/JSObject/utils"; import { isJSObjectFunction } from "workers/Evaluation/JSObject/utils";
import { import {
getValidatedTree, getValidatedTree,
@ -1063,7 +1066,7 @@ export default class DataTreeEvaluator {
}); });
} }
const result = this.evaluateDynamicBoundValue( const { errors: evalErrors, result } = this.evaluateDynamicBoundValue(
toBeSentForEval, toBeSentForEval,
data, data,
resolvedFunctions, resolvedFunctions,
@ -1071,16 +1074,20 @@ export default class DataTreeEvaluator {
contextData, contextData,
callBackData, callBackData,
); );
if (fullPropertyPath && result.errors.length) { if (fullPropertyPath && evalErrors.length) {
addErrorToEntityProperty({ addErrorToEntityProperty({
errors: result.errors, errors: addRootcauseToAsyncInvocationErrors(
fullPropertyPath,
configTree,
evalErrors,
),
evalProps: this.evalProps, evalProps: this.evalProps,
fullPropertyPath, fullPropertyPath,
dataTree: data, dataTree: data,
configTree, configTree,
}); });
} }
return result.result; return result;
} else { } else {
return stringSegments[index]; return stringSegments[index];
} }

View File

@ -1,3 +1,55 @@
import {
getEntityNameAndPropertyPath,
isAction,
isJSAction,
isWidget,
} from "@appsmith/workers/Evaluation/evaluationUtils";
import type {
ConfigTree,
DataTreeEntity,
WidgetEntity,
} from "entities/DataTree/dataTreeFactory";
import type { ActionEntity, JSActionEntity } from "entities/DataTree/types";
import type { EvaluationError } from "utils/DynamicBindingUtils";
import { errorModifier } from "workers/Evaluation/errorModifier";
import { asyncJsFunctionInDataFields } from "workers/Evaluation/JSObject/asyncJSFunctionBoundToDataField";
export function getFixedTimeDifference(endTime: number, startTime: number) { export function getFixedTimeDifference(endTime: number, startTime: number) {
return (endTime - startTime).toFixed(2) + " ms"; return (endTime - startTime).toFixed(2) + " ms";
} }
export function isDataField(fullPath: string, configTree: ConfigTree) {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(fullPath);
const entityConfig = configTree[entityName];
if ("triggerPaths" in entityConfig) {
return !(propertyPath in entityConfig.triggerPaths);
}
return false;
}
export function isWidgetActionOrJsObject(
entity: DataTreeEntity,
): entity is ActionEntity | WidgetEntity | JSActionEntity {
return isWidget(entity) || isAction(entity) || isJSAction(entity);
}
export function addRootcauseToAsyncInvocationErrors(
fullPropertyPath: string,
configTree: ConfigTree,
errors: EvaluationError[],
) {
let updatedErrors = errors;
if (isDataField(fullPropertyPath, configTree)) {
const asyncFunctionBindingInPath =
asyncJsFunctionInDataFields.getAsyncFunctionBindingInDataField(
fullPropertyPath,
);
if (asyncFunctionBindingInPath) {
updatedErrors = errorModifier.setAsyncInvocationErrorsRootcause(
errors,
asyncFunctionBindingInPath,
);
}
}
return updatedErrors;
}

View File

@ -37,7 +37,9 @@ import {
updateMap, updateMap,
} from "./utils"; } from "./utils";
import type DataTreeEvaluator from "workers/common/DataTreeEvaluator"; import type DataTreeEvaluator from "workers/common/DataTreeEvaluator";
import { difference, isEmpty, set } from "lodash"; import { difference, isEmpty, set, uniq } from "lodash";
import { isWidgetActionOrJsObject } from "../DataTreeEvaluator/utils";
import { asyncJsFunctionInDataFields } from "workers/Evaluation/JSObject/asyncJSFunctionBoundToDataField";
interface CreateDependencyMap { interface CreateDependencyMap {
dependencyMap: DependencyMap; dependencyMap: DependencyMap;
@ -100,11 +102,18 @@ export function createDependencyMap(
const { errors, invalidReferences, validReferences } = const { errors, invalidReferences, validReferences } =
extractInfoFromBindings(dependencyMap[key], dataTreeEvalRef.allKeys); extractInfoFromBindings(dependencyMap[key], dataTreeEvalRef.allKeys);
dependencyMap[key] = validReferences; dependencyMap[key] = validReferences;
// To keep invalidReferencesMap as minimal as possible, only paths with invalid references
// are stored. updateMap(invalidReferencesMap, key, invalidReferences, {
if (invalidReferences.length) { deleteOnEmpty: true,
invalidReferencesMap[key] = invalidReferences; replaceValue: true,
} });
asyncJsFunctionInDataFields.update(
key,
validReferences,
unEvalTree,
configTree,
);
errors.forEach((error) => { errors.forEach((error) => {
dataTreeEvalRef.errors.push(error); dataTreeEvalRef.errors.push(error);
}); });
@ -169,11 +178,12 @@ export const updateDependencyMap = ({
let didUpdateValidationDependencyMap = false; let didUpdateValidationDependencyMap = false;
const dependenciesOfRemovedPaths: Array<string> = []; const dependenciesOfRemovedPaths: Array<string> = [];
const removedPaths: Array<string> = []; const removedPaths: Array<string> = [];
const extraPathsToLint = new Set<string>(); let extraPathsToLint: string[] = [];
const pathsToClearErrorsFor: any[] = []; const pathsToClearErrorsFor: any[] = [];
const { const {
dependencyMap, dependencyMap,
invalidReferencesMap, invalidReferencesMap,
inverseDependencyMap,
oldConfigTree, oldConfigTree,
oldUnEvalTree, oldUnEvalTree,
triggerFieldDependencyMap, triggerFieldDependencyMap,
@ -207,7 +217,7 @@ export const updateDependencyMap = ({
if (entityType !== "noop") { if (entityType !== "noop") {
switch (event) { switch (event) {
case DataTreeDiffEvent.NEW: { case DataTreeDiffEvent.NEW: {
if (isWidget(entity) || isAction(entity) || isJSAction(entity)) { if (isWidgetActionOrJsObject(entity)) {
if (!isDynamicLeaf(unEvalDataTree, fullPropertyPath, configTree)) { if (!isDynamicLeaf(unEvalDataTree, fullPropertyPath, configTree)) {
const entityDependencyMap: DependencyMap = listEntityDependencies( const entityDependencyMap: DependencyMap = listEntityDependencies(
entity, entity,
@ -236,6 +246,19 @@ export const updateDependencyMap = ({
invalidReferences, invalidReferences,
{ deleteOnEmpty: true, replaceValue: true }, { deleteOnEmpty: true, replaceValue: true },
); );
// Update asyncJSFunctionsInDatafieldsMap
const updatedAsyncJSFunctions =
asyncJsFunctionInDataFields.update(
entityDependent,
validReferences,
unEvalDataTree,
configTree,
);
extraPathsToLint = extraPathsToLint.concat(
updatedAsyncJSFunctions,
);
dataTreeEvalErrors = dataTreeEvalErrors.concat( dataTreeEvalErrors = dataTreeEvalErrors.concat(
extractDependencyErrors, extractDependencyErrors,
); );
@ -367,7 +390,7 @@ export const updateDependencyMap = ({
if (isChildPropertyPath(fullPropertyPath, invalidReference)) { if (isChildPropertyPath(fullPropertyPath, invalidReference)) {
updateMap(newlyValidReferencesMap, invalidReference, [path]); updateMap(newlyValidReferencesMap, invalidReference, [path]);
if (!dependencyMap[invalidReference]) { if (!dependencyMap[invalidReference]) {
extraPathsToLint.add(path); extraPathsToLint.push(path);
} }
} }
}); });
@ -403,6 +426,17 @@ export const updateDependencyMap = ({
fullPath, fullPath,
validReferences, validReferences,
); );
// Update asyncJSMap
const updatedAsyncJSFunctions =
asyncJsFunctionInDataFields.update(
fullPath,
validReferences,
unEvalDataTree,
configTree,
);
extraPathsToLint = extraPathsToLint.concat(
updatedAsyncJSFunctions,
);
// Since the previously invalid reference has become valid, // Since the previously invalid reference has become valid,
// remove it from the invalidReferencesMap // remove it from the invalidReferencesMap
@ -434,7 +468,7 @@ export const updateDependencyMap = ({
if ( if (
isChildPropertyPath(fullPropertyPath, triggerPathDependency) isChildPropertyPath(fullPropertyPath, triggerPathDependency)
) { ) {
extraPathsToLint.add(triggerPath); extraPathsToLint.push(triggerPath);
} }
}, },
); );
@ -463,7 +497,7 @@ export const updateDependencyMap = ({
} }
if ( if (
(isWidget(entity) || isAction(entity) || isJSAction(entity)) && isWidgetActionOrJsObject(entity) &&
fullPropertyPath === entityName fullPropertyPath === entityName
) { ) {
const entityDependencies = listEntityDependencies( const entityDependencies = listEntityDependencies(
@ -502,6 +536,7 @@ export const updateDependencyMap = ({
didUpdateValidationDependencyMap = true; didUpdateValidationDependencyMap = true;
} }
} }
// Either an existing entity or an existing property path has been deleted. Update the global dependency map // Either an existing entity or an existing property path has been deleted. Update the global dependency map
// by removing the bindings from the same. // by removing the bindings from the same.
Object.keys(dependencyMap).forEach((dependencyPath) => { Object.keys(dependencyMap).forEach((dependencyPath) => {
@ -532,7 +567,7 @@ export const updateDependencyMap = ({
if ( if (
isChildPropertyPath(fullPropertyPath, invalidReference) isChildPropertyPath(fullPropertyPath, invalidReference)
) { ) {
extraPathsToLint.add(dependencyPath); extraPathsToLint.push(dependencyPath);
} }
}, },
); );
@ -571,7 +606,7 @@ export const updateDependencyMap = ({
if ( if (
isChildPropertyPath(fullPropertyPath, invalidReference) isChildPropertyPath(fullPropertyPath, invalidReference)
) { ) {
extraPathsToLint.add(dependencyPath); extraPathsToLint.push(dependencyPath);
} }
}, },
); );
@ -579,15 +614,21 @@ export const updateDependencyMap = ({
} }
}); });
// update asyncJsFunctionInDataFields
const updatedAsyncJSFunctions =
asyncJsFunctionInDataFields.handlePathDeletion(
fullPropertyPath,
unEvalDataTree,
configTree,
);
extraPathsToLint = extraPathsToLint.concat(updatedAsyncJSFunctions);
break; break;
} }
case DataTreeDiffEvent.EDIT: { case DataTreeDiffEvent.EDIT: {
// We only care if the difference is in dynamic bindings since static values do not need // We only care if the difference is in dynamic bindings since static values do not need
// an evaluation. // an evaluation.
if ( if (isWidgetActionOrJsObject(entity) && typeof value === "string") {
(isWidget(entity) || isAction(entity) || isJSAction(entity)) &&
typeof value === "string"
) {
const entity: ActionEntity | WidgetEntity | JSActionEntity = const entity: ActionEntity | WidgetEntity | JSActionEntity =
unEvalDataTree[entityName] as unEvalDataTree[entityName] as
| ActionEntity | ActionEntity
@ -623,7 +664,18 @@ export const updateDependencyMap = ({
dataTreeEvalErrors = dataTreeEvalErrors.concat( dataTreeEvalErrors = dataTreeEvalErrors.concat(
extractDependencyErrors, extractDependencyErrors,
); );
// update asyncFunctionInSyncfieldsMap
const updatedAsyncJSFunctions =
asyncJsFunctionInDataFields.handlePathEdit(
fullPropertyPath,
validReferences,
unEvalDataTree,
inverseDependencyMap,
configTree,
);
extraPathsToLint = extraPathsToLint.concat(
updatedAsyncJSFunctions,
);
// We found a new dynamic binding for this property path. We update the dependency map by overwriting the // We found a new dynamic binding for this property path. We update the dependency map by overwriting the
// dependencies for this property path with the newly found dependencies // dependencies for this property path with the newly found dependencies
@ -673,6 +725,18 @@ export const updateDependencyMap = ({
didUpdateDependencyMap = true; didUpdateDependencyMap = true;
delete dependencyMap[fullPropertyPath]; delete dependencyMap[fullPropertyPath];
delete invalidReferencesMap[fullPropertyPath]; delete invalidReferencesMap[fullPropertyPath];
// update asyncFunctionInSyncfieldsMap
const updatedAsyncJSFunctions =
asyncJsFunctionInDataFields.handlePathEdit(
fullPropertyPath,
[],
unEvalDataTree,
inverseDependencyMap,
configTree,
);
extraPathsToLint = extraPathsToLint.concat(
updatedAsyncJSFunctions,
);
} }
} }
if ( if (
@ -783,6 +847,6 @@ export const updateDependencyMap = ({
pathsToClearErrorsFor, pathsToClearErrorsFor,
dependenciesOfRemovedPaths, dependenciesOfRemovedPaths,
removedPaths, removedPaths,
extraPathsToLint: Array.from(extraPathsToLint), extraPathsToLint: uniq(extraPathsToLint),
}; };
}; };

View File

@ -13,12 +13,13 @@ import {
getEntityNameAndPropertyPath, getEntityNameAndPropertyPath,
isAction, isAction,
isJSAction, isJSAction,
isJSActionConfig,
isWidget, isWidget,
} from "@appsmith/workers/Evaluation/evaluationUtils"; } from "@appsmith/workers/Evaluation/evaluationUtils";
import type { import type {
ConfigTree,
DataTree, DataTree,
ConfigTree,
DataTreeEntity, DataTreeEntity,
DataTreeEntityConfig, DataTreeEntityConfig,
WidgetEntity, WidgetEntity,
@ -427,3 +428,24 @@ export function updateMap(
map[path] = updatedEntries; map[path] = updatedEntries;
} }
} }
export function isAsyncJSFunction(configTree: ConfigTree, fullPath: string) {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(fullPath);
const configEntity = configTree[entityName];
return (
isJSActionConfig(configEntity) &&
propertyPath &&
propertyPath in configEntity.meta &&
configEntity.meta[propertyPath].isAsync
);
}
export function isJSFunction(configTree: ConfigTree, fullPath: string) {
const { entityName, propertyPath } = getEntityNameAndPropertyPath(fullPath);
const entityConfig = configTree[entityName];
return (
isJSActionConfig(entityConfig) &&
propertyPath &&
propertyPath in entityConfig.meta
);
}

View File

@ -6816,7 +6816,7 @@ acorn-walk@^7.0.0, acorn-walk@^7.1.1, acorn-walk@^7.2.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz"
acorn-walk@^8.1.1, acorn-walk@^8.2.0: acorn-walk@^8.1.1:
version "8.2.0" version "8.2.0"
resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
@ -7363,11 +7363,6 @@ astral-regex@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz"
astring@^1.7.5:
version "1.8.3"
resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.3.tgz#1a0ae738c7cc558f8e5ddc8e3120636f5cebcb85"
integrity sha512-sRpyiNrx2dEYIMmUXprS8nlpRg2Drs8m9ElX9vVEXaCB4XEAJhKfs7IcX0IwShjuOAjLR6wzIrgoptz1n19i1A==
async-limiter@~1.0.0: async-limiter@~1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz" resolved "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz"

View File

@ -21,7 +21,12 @@ import {
import { ECMA_VERSION, SourceType, NodeTypes } from "./src/constants"; import { ECMA_VERSION, SourceType, NodeTypes } from "./src/constants";
// JSObjects // JSObjects
import { parseJSObjectWithAST } from "./src/jsObject"; import {
parseJSObject,
isJSFunctionProperty,
TParsedJSProperty,
JSPropertyPosition,
} from "./src/jsObject";
// types or intefaces should be exported with type keyword, while enums can be exported like normal functions // types or intefaces should be exported with type keyword, while enums can be exported like normal functions
export type { export type {
@ -29,6 +34,8 @@ export type {
PropertyNode, PropertyNode,
MemberExpressionData, MemberExpressionData,
IdentifierInfo, IdentifierInfo,
TParsedJSProperty,
JSPropertyPosition,
}; };
export { export {
@ -44,8 +51,9 @@ export {
extractInvalidTopLevelMemberExpressionsFromCode, extractInvalidTopLevelMemberExpressionsFromCode,
getFunctionalParamsFromNode, getFunctionalParamsFromNode,
isTypeOfFunction, isTypeOfFunction,
parseJSObjectWithAST, parseJSObject,
ECMA_VERSION, ECMA_VERSION,
SourceType, SourceType,
NodeTypes, NodeTypes,
isJSFunctionProperty,
}; };

View File

@ -20,9 +20,11 @@
"link-package": "yarn install && rollup -c && cd build && cp -R ../node_modules ./node_modules && yarn link" "link-package": "yarn install && rollup -c && cd build && cp -R ../node_modules ./node_modules && yarn link"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.21.0",
"acorn": "^8.8.0", "acorn": "^8.8.0",
"acorn-walk": "^8.2.0", "acorn-walk": "^8.2.0",
"astring": "^1.7.5", "astring": "^1.7.5",
"escodegen": "^2.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"rollup": "^2.77.0", "rollup": "^2.77.0",
"typescript": "4.5.5", "typescript": "4.5.5",
@ -31,6 +33,7 @@
"devDependencies": { "devDependencies": {
"@babel/preset-typescript": "^7.17.12", "@babel/preset-typescript": "^7.17.12",
"@rollup/plugin-commonjs": "^22.0.0", "@rollup/plugin-commonjs": "^22.0.0",
"@types/escodegen": "^0.0.7",
"@types/jest": "29.0.3", "@types/jest": "29.0.3",
"@types/lodash": "^4.14.120", "@types/lodash": "^4.14.120",
"@typescript-eslint/eslint-plugin": "^5.25.0", "@typescript-eslint/eslint-plugin": "^5.25.0",

View File

@ -1,5 +1,5 @@
import { parseJSObject } from "../index";
import { extractIdentifierInfoFromCode } from "../src/index"; import { extractIdentifierInfoFromCode } from "../src/index";
import { parseJSObjectWithAST } from "../src/jsObject";
describe("getAllIdentifiers", () => { describe("getAllIdentifiers", () => {
it("works properly", () => { it("works properly", () => {
@ -306,7 +306,7 @@ describe("getAllIdentifiers", () => {
const { references } = extractIdentifierInfoFromCode( const { references } = extractIdentifierInfoFromCode(
perCase.script, perCase.script,
2, 2,
perCase.invalidIdentifiers perCase.invalidIdentifiers,
); );
expect(references).toStrictEqual(perCase.expectedResults); expect(references).toStrictEqual(perCase.expectedResults);
}); });
@ -315,7 +315,7 @@ describe("getAllIdentifiers", () => {
describe("parseJSObjectWithAST", () => { describe("parseJSObjectWithAST", () => {
it("parse js object", () => { it("parse js object", () => {
const body = `{ const body = `export default{
myVar1: [], myVar1: [],
myVar2: {}, myVar2: {},
myFun1: () => { myFun1: () => {
@ -325,36 +325,84 @@ describe("parseJSObjectWithAST", () => {
//use async-await or promises //use async-await or promises
} }
}`; }`;
const parsedObject = [
const expectedParsedObject = [
{ {
key: "myVar1", key: "myVar1",
value: "[]", value: "[]",
rawContent: "myVar1: []",
type: "ArrayExpression", type: "ArrayExpression",
position: {
startLine: 2,
startColumn: 1,
endLine: 2,
endColumn: 11,
keyStartLine: 2,
keyEndLine: 2,
keyStartColumn: 1,
keyEndColumn: 7,
},
}, },
{ {
key: "myVar2", key: "myVar2",
value: "{}", value: "{}",
rawContent: "myVar2: {}",
type: "ObjectExpression", type: "ObjectExpression",
position: {
startLine: 3,
startColumn: 1,
endLine: 3,
endColumn: 11,
keyStartLine: 3,
keyEndLine: 3,
keyStartColumn: 1,
keyEndColumn: 7,
},
}, },
{ {
key: "myFun1", key: "myFun1",
value: "() => {}", value: "() => {}",
rawContent: "myFun1: () => {\n\t\t//write code here\n\t}",
type: "ArrowFunctionExpression", type: "ArrowFunctionExpression",
position: {
startLine: 4,
startColumn: 1,
endLine: 6,
endColumn: 2,
keyStartLine: 4,
keyEndLine: 4,
keyStartColumn: 1,
keyEndColumn: 7,
},
arguments: [], arguments: [],
isMarkedAsync: false
}, },
{ {
key: "myFun2", key: "myFun2",
value: "async () => {}", value: "async () => {}",
rawContent:
"myFun2: async () => {\n\t\t//use async-await or promises\n\t}",
type: "ArrowFunctionExpression", type: "ArrowFunctionExpression",
position: {
startLine: 7,
startColumn: 1,
endLine: 9,
endColumn: 2,
keyStartLine: 7,
keyEndLine: 7,
keyStartColumn: 1,
keyEndColumn: 7,
},
arguments: [], arguments: [],
isMarkedAsync: true,
}, },
]; ];
const resultParsedObject = parseJSObjectWithAST(body); const { parsedObject } = parseJSObject(body);
expect(resultParsedObject).toStrictEqual(parsedObject); expect(parsedObject).toStrictEqual(expectedParsedObject);
}); });
it("parse js object with literal", () => { it("parse js object with literal", () => {
const body = `{ const body = `export default{
myVar1: [], myVar1: [],
myVar2: { myVar2: {
"a": "app", "a": "app",
@ -366,36 +414,83 @@ describe("parseJSObjectWithAST", () => {
//use async-await or promises //use async-await or promises
} }
}`; }`;
const parsedObject = [ const expectedParsedObject = [
{ {
key: "myVar1", key: "myVar1",
value: "[]", value: "[]",
rawContent: "myVar1: []",
type: "ArrayExpression", type: "ArrayExpression",
position: {
startLine: 2,
startColumn: 1,
endLine: 2,
endColumn: 11,
keyStartLine: 2,
keyEndLine: 2,
keyStartColumn: 1,
keyEndColumn: 7,
},
}, },
{ {
key: "myVar2", key: "myVar2",
value: '{\n "a": "app"\n}', value: '{\n "a": "app"\n}',
rawContent: 'myVar2: {\n\t\t"a": "app",\n\t}',
type: "ObjectExpression", type: "ObjectExpression",
position: {
startLine: 3,
startColumn: 1,
endLine: 5,
endColumn: 2,
keyStartLine: 3,
keyEndLine: 3,
keyStartColumn: 1,
keyEndColumn: 7,
},
}, },
{ {
key: "myFun1", key: "myFun1",
value: "() => {}", value: "() => {}",
rawContent: "myFun1: () => {\n\t\t//write code here\n\t}",
type: "ArrowFunctionExpression", type: "ArrowFunctionExpression",
position: {
startLine: 6,
startColumn: 1,
endLine: 8,
endColumn: 2,
keyStartLine: 6,
keyEndLine: 6,
keyStartColumn: 1,
keyEndColumn: 7,
},
arguments: [], arguments: [],
isMarkedAsync: false
}, },
{ {
key: "myFun2", key: "myFun2",
value: "async () => {}", value: "async () => {}",
rawContent:
"myFun2: async () => {\n\t\t//use async-await or promises\n\t}",
type: "ArrowFunctionExpression", type: "ArrowFunctionExpression",
position: {
startLine: 9,
startColumn: 1,
endLine: 11,
endColumn: 2,
keyStartLine: 9,
keyEndLine: 9,
keyStartColumn: 1,
keyEndColumn: 7,
},
arguments: [], arguments: [],
isMarkedAsync: true,
}, },
]; ];
const resultParsedObject = parseJSObjectWithAST(body); const { parsedObject } = parseJSObject(body);
expect(resultParsedObject).toStrictEqual(parsedObject); expect(parsedObject).toStrictEqual(expectedParsedObject);
}); });
it("parse js object with variable declaration inside function", () => { it("parse js object with variable declaration inside function", () => {
const body = `{ const body = `export default{
myFun1: () => { myFun1: () => {
const a = { const a = {
conditions: [], conditions: [],
@ -408,89 +503,108 @@ describe("parseJSObjectWithAST", () => {
//use async-await or promises //use async-await or promises
} }
}`; }`;
const parsedObject = [ const expectedParsedObject = [
{ {
key: "myFun1", key: "myFun1",
value: `() => { value:
const a = { "() => {\n" +
conditions: [], " const a = {\n" +
requires: 1, " conditions: [],\n" +
testFunc: () => {}, " requires: 1,\n" +
testFunc2: function () {} " testFunc: () => {},\n" +
}; " testFunc2: function () {}\n" +
}`, " };\n" +
"}",
rawContent:
"myFun1: () => {\n" +
" const a = {\n" +
" conditions: [],\n" +
" requires: 1,\n" +
" testFunc: () => {},\n" +
" testFunc2: function(){}\n" +
" };\n" +
" }",
type: "ArrowFunctionExpression", type: "ArrowFunctionExpression",
position: {
startLine: 2,
startColumn: 6,
endLine: 9,
endColumn: 7,
keyStartLine: 2,
keyEndLine: 2,
keyStartColumn: 6,
keyEndColumn: 12,
},
arguments: [], arguments: [],
isMarkedAsync: false,
}, },
{ {
key: "myFun2", key: "myFun2",
value: "async () => {}", value: "async () => {}",
rawContent:
"myFun2: async () => {\n //use async-await or promises\n }",
type: "ArrowFunctionExpression", type: "ArrowFunctionExpression",
position: {
startLine: 10,
startColumn: 6,
endLine: 12,
endColumn: 7,
keyStartLine: 10,
keyEndLine: 10,
keyStartColumn: 6,
keyEndColumn: 12,
},
arguments: [], arguments: [],
isMarkedAsync: true,
}, },
]; ];
const resultParsedObject = parseJSObjectWithAST(body); const { parsedObject } = parseJSObject(body);
expect(resultParsedObject).toStrictEqual(parsedObject); expect(parsedObject).toStrictEqual(expectedParsedObject);
}); });
it("parse js object with params of all types", () => { it("parse js object with params of all types", () => {
const body = `{ const body = `export default{
myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue()) => { myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue()) => {
//use async-await or promises //use async-await or promises
}, },
}`; }`;
const parsedObject = [ const expectedParsedObject = [
{ {
key: "myFun2", key: "myFun2",
value: value:
'async (a, b = Array(1, 2, 3), c = "", d = [], e = this.myVar1, f = {}, g = function () {}, h = Object.assign({}), i = String(), j = storeValue()) => {}', 'async (a, b = Array(1, 2, 3), c = "", d = [], e = this.myVar1, f = {}, g = function () {}, h = Object.assign({}), i = String(), j = storeValue()) => {}',
rawContent:
'myFun2: async (a,b = Array(1,2,3),c = "", d = [], e = this.myVar1, f = {}, g = function(){}, h = Object.assign({}), i = String(), j = storeValue()) => {\n' +
" //use async-await or promises\n" +
" }",
type: "ArrowFunctionExpression", type: "ArrowFunctionExpression",
position: {
startLine: 2,
startColumn: 6,
endLine: 4,
endColumn: 7,
keyStartLine: 2,
keyEndLine: 2,
keyStartColumn: 6,
keyEndColumn: 12,
},
arguments: [ arguments: [
{ { paramName: "a", defaultValue: undefined },
paramName: "a", { paramName: "b", defaultValue: undefined },
defaultValue: undefined, { paramName: "c", defaultValue: undefined },
}, { paramName: "d", defaultValue: undefined },
{ { paramName: "e", defaultValue: undefined },
paramName: "b", { paramName: "f", defaultValue: undefined },
defaultValue: undefined, { paramName: "g", defaultValue: undefined },
}, { paramName: "h", defaultValue: undefined },
{ { paramName: "i", defaultValue: undefined },
paramName: "c", { paramName: "j", defaultValue: undefined },
defaultValue: undefined,
},
{
paramName: "d",
defaultValue: undefined,
},
{
paramName: "e",
defaultValue: undefined,
},
{
paramName: "f",
defaultValue: undefined,
},
{
paramName: "g",
defaultValue: undefined,
},
{
paramName: "h",
defaultValue: undefined,
},
{
paramName: "i",
defaultValue: undefined,
},
{
paramName: "j",
defaultValue: undefined,
},
], ],
isMarkedAsync: true,
}, },
]; ];
const resultParsedObject = parseJSObjectWithAST(body); const { parsedObject } = parseJSObject(body);
expect(resultParsedObject).toEqual(parsedObject); expect(parsedObject).toStrictEqual(expectedParsedObject);
}); });
}); });

View File

@ -1,4 +1,4 @@
import { parse, Node, SourceLocation, Options, Comment } from "acorn"; import { parse, Node, SourceLocation, Options } from "acorn";
import { ancestor, simple } from "acorn-walk"; import { ancestor, simple } from "acorn-walk";
import { ECMA_VERSION, NodeTypes } from "./constants/ast"; import { ECMA_VERSION, NodeTypes } from "./constants/ast";
import { has, isFinite, isString, memoize, toPath } from "lodash"; import { has, isFinite, isString, memoize, toPath } from "lodash";
@ -36,7 +36,7 @@ interface MemberExpressionNode extends Node {
} }
// doc: https://github.com/estree/estree/blob/master/es5.md#identifier // doc: https://github.com/estree/estree/blob/master/es5.md#identifier
interface IdentifierNode extends Node { export interface IdentifierNode extends Node {
type: NodeTypes.Identifier; type: NodeTypes.Identifier;
name: string; name: string;
} }
@ -69,10 +69,12 @@ interface FunctionDeclarationNode extends Node, Function {
// doc: https://github.com/estree/estree/blob/master/es5.md#functionexpression // doc: https://github.com/estree/estree/blob/master/es5.md#functionexpression
interface FunctionExpressionNode extends Expression, Function { interface FunctionExpressionNode extends Expression, Function {
type: NodeTypes.FunctionExpression; type: NodeTypes.FunctionExpression;
async: boolean;
} }
interface ArrowFunctionExpressionNode extends Expression, Function { interface ArrowFunctionExpressionNode extends Expression, Function {
type: NodeTypes.ArrowFunctionExpression; type: NodeTypes.ArrowFunctionExpression;
async: boolean;
} }
export interface ObjectExpression extends Expression { export interface ObjectExpression extends Expression {
@ -87,7 +89,7 @@ interface AssignmentPatternNode extends Node {
} }
// doc: https://github.com/estree/estree/blob/master/es5.md#literal // doc: https://github.com/estree/estree/blob/master/es5.md#literal
interface LiteralNode extends Node { export interface LiteralNode extends Node {
type: NodeTypes.Literal; type: NodeTypes.Literal;
value: string | boolean | null | number | RegExp; value: string | boolean | null | number | RegExp;
} }
@ -107,8 +109,12 @@ export interface PropertyNode extends Node {
kind: "init" | "get" | "set"; kind: "init" | "get" | "set";
} }
export interface ExportDefaultDeclarationNode extends Node {
declaration: Node;
}
// Node with location details // Node with location details
type NodeWithLocation<NodeType> = NodeType & { export type NodeWithLocation<NodeType> = NodeType & {
loc: SourceLocation; loc: SourceLocation;
}; };
@ -163,6 +169,12 @@ export const isPropertyNode = (node: Node): node is PropertyNode => {
return node.type === NodeTypes.Property; return node.type === NodeTypes.Property;
}; };
export const isExportDefaultDeclarationNode = (
node: Node,
): node is ExportDefaultDeclarationNode => {
return node.type === NodeTypes.ExportDefaultDeclaration;
};
export const isPropertyAFunctionNode = ( export const isPropertyAFunctionNode = (
node: Node, node: Node,
): node is ArrowFunctionExpressionNode | FunctionExpressionNode => { ): node is ArrowFunctionExpressionNode | FunctionExpressionNode => {
@ -211,7 +223,11 @@ const getFunctionalParamNamesFromNode = (
// Since this will be used by both the server and the client, we want to prevent regeneration of ast // Since this will be used by both the server and the client, we want to prevent regeneration of ast
// for the the same code snippet // for the the same code snippet
export const getAST = memoize((code: string, options?: AstOptions) => export const getAST = memoize((code: string, options?: AstOptions) =>
parse(code, { ...options, ecmaVersion: ECMA_VERSION }), parse(code, {
...options,
ecmaVersion: ECMA_VERSION,
locations: true, // Adds location data to each node
}),
); );
/** /**

View File

@ -1,78 +1,140 @@
import { Node } from "acorn"; import { Node } from "acorn";
import { getAST } from "../index";
import { generate } from "astring";
import { simple } from "acorn-walk"; import { simple } from "acorn-walk";
import {
getAST,
IdentifierNode,
isExportDefaultDeclarationNode,
isObjectExpression,
isPropertyNode,
isTypeOfFunction,
LiteralNode,
NodeWithLocation,
PropertyNode,
} from "../index";
import { generate } from "astring";
import { import {
getFunctionalParamsFromNode, getFunctionalParamsFromNode,
isPropertyAFunctionNode, isPropertyAFunctionNode,
isVariableDeclarator,
isObjectExpression,
PropertyNode,
functionParam, functionParam,
} from "../index"; } from "../index";
import { SourceType, NodeTypes } from "../../index";
type JsObjectProperty = { import { attachComments } from "escodegen";
key: string; import { extractContentByPosition } from "../utils";
value: string;
type: string;
arguments?: Array<functionParam>;
};
const jsObjectVariableName = const jsObjectVariableName =
"____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____"; "____INTERNAL_JS_OBJECT_NAME_USED_FOR_PARSING_____";
export const jsObjectDeclaration = `var ${jsObjectVariableName} =`; export const jsObjectDeclaration = `var ${jsObjectVariableName} =`;
export const parseJSObjectWithAST = ( export interface JSPropertyPosition {
jsObjectBody: string startLine: number;
): Array<JsObjectProperty> => { startColumn: number;
/* endLine: number;
jsObjectVariableName value is added such actual js code would never name same variable name. endColumn: number;
if the variable name will be same then also we won't have problem here as jsObjectVariableName will be last node in VariableDeclarator hence overriding the previous JSObjectProperties. keyStartLine: number;
Keeping this just for sanity check if any caveat was missed. keyEndLine: number;
*/ keyStartColumn: number;
const jsCode = `${jsObjectDeclaration} ${jsObjectBody}`; keyEndColumn: number;
}
const ast = getAST(jsCode); interface baseJSProperty {
key: string;
value: string;
type: string;
position: Partial<JSPropertyPosition>;
rawContent: string;
}
const parsedObjectProperties = new Set<JsObjectProperty>(); type JSFunctionProperty = baseJSProperty & {
let JSObjectProperties: Array<PropertyNode> = []; arguments: functionParam[];
// If function uses the "async" keyword
isMarkedAsync: boolean;
};
type JSVarProperty = baseJSProperty;
export type TParsedJSProperty = JSVarProperty | JSFunctionProperty;
export const isJSFunctionProperty = (
t: TParsedJSProperty,
): t is JSFunctionProperty => {
return isTypeOfFunction(t.type);
};
export const parseJSObject = (code: string) => {
let ast: Node = { end: 0, start: 0, type: "" };
const result: TParsedJSProperty[] = [];
try {
const comments: any = [];
const token: any = [];
ast = getAST(code, {
sourceType: SourceType.module,
onComment: comments,
onToken: token,
ranges: true,
});
attachComments(ast, comments, token);
} catch (e) {
return { parsedObject: result, success: false };
}
const parsedObjectProperties = new Set<TParsedJSProperty>();
let JSObjectProperties: NodeWithLocation<PropertyNode>[] = [];
simple(ast, { simple(ast, {
VariableDeclarator(node: Node) { ExportDefaultDeclaration(node, ancestors: Node[]) {
if ( if (
isVariableDeclarator(node) && !isExportDefaultDeclarationNode(node) ||
node.id.name === jsObjectVariableName && !isObjectExpression(node.declaration)
node.init && )
isObjectExpression(node.init) return;
) { JSObjectProperties = node.declaration
JSObjectProperties = node.init.properties; .properties as NodeWithLocation<PropertyNode>[];
}
}, },
}); });
JSObjectProperties.forEach((node) => { JSObjectProperties.forEach((node) => {
let params = new Set<functionParam>(); const propertyKey = node.key as NodeWithLocation<
const propertyNode = node; LiteralNode | IdentifierNode
let property: JsObjectProperty = { >;
key: generate(propertyNode.key), let property: TParsedJSProperty = {
value: generate(propertyNode.value), key: generate(node.key),
type: propertyNode.value.type, value: generate(node.value),
rawContent: extractContentByPosition(code, {
from: {
line: node.loc.start.line - 1,
ch: node.loc.start.column,
},
to: {
line: node.loc.end.line - 1,
ch: node.loc.end.column - 1,
},
}),
type: node.value.type,
position: {
startLine: node.loc.start.line,
startColumn: node.loc.start.column,
endLine: node.loc.end.line,
endColumn: node.loc.end.column,
keyStartLine: propertyKey.loc.start.line,
keyEndLine: propertyKey.loc.end.line,
keyStartColumn: propertyKey.loc.start.column,
keyEndColumn: propertyKey.loc.end.column,
},
}; };
if (isPropertyAFunctionNode(propertyNode.value)) { if (isPropertyAFunctionNode(node.value)) {
// if in future we need default values of each param, we could implement that in getFunctionalParamsFromNode // if in future we need default values of each param, we could implement that in getFunctionalParamsFromNode
// currently we don't consume it anywhere hence avoiding to calculate that. // currently we don't consume it anywhere hence avoiding to calculate that.
params = getFunctionalParamsFromNode(propertyNode.value); const params = getFunctionalParamsFromNode(node.value);
property = { property = {
...property, ...property,
arguments: [...params], arguments: [...params],
isMarkedAsync: node.value.async,
}; };
} }
// here we use `generate` function to convert our AST Node to JSCode
parsedObjectProperties.add(property); parsedObjectProperties.add(property);
}); });
return [...parsedObjectProperties]; return { parsedObject: [...parsedObjectProperties], success: true };
}; };

View File

@ -1,4 +1,5 @@
import unescapeJS from 'unescape-js'; import unescapeJS from "unescape-js";
import { isLiteralNode, PropertyNode } from "../index";
const beginsWithLineBreakRegex = /^\s+|\s+$/; const beginsWithLineBreakRegex = /^\s+|\s+$/;
@ -8,14 +9,51 @@ export function sanitizeScript(js: string, evaluationVersion: number) {
// so that eval can happen // so that eval can happen
//default value of evalutaion version is 2 //default value of evalutaion version is 2
evaluationVersion = evaluationVersion ? evaluationVersion : 2; evaluationVersion = evaluationVersion ? evaluationVersion : 2;
const trimmedJS = js.replace(beginsWithLineBreakRegex, ''); const trimmedJS = js.replace(beginsWithLineBreakRegex, "");
return evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS); return evaluationVersion > 1 ? trimmedJS : unescapeJS(trimmedJS);
} }
// For the times when you need to know if something truly an object like { a: 1, b: 2} // For the times when you need to know if something truly an object like { a: 1, b: 2}
// typeof, lodash.isObject and others will return false positives for things like array, null, etc // typeof, lodash.isObject and others will return false positives for things like array, null, etc
export const isTrueObject = ( export const isTrueObject = (
item: unknown item: unknown,
): item is Record<string, unknown> => { ): item is Record<string, unknown> => {
return Object.prototype.toString.call(item) === '[object Object]'; return Object.prototype.toString.call(item) === "[object Object]";
};
export const getNameFromPropertyNode = (node: PropertyNode): string =>
isLiteralNode(node.key) ? String(node.key.value) : node.key.name;
type Position = {
line: number;
ch: number;
};
export const extractContentByPosition = (
content: string,
position: { from: Position; to: Position },
) => {
const eachLine = content.split("\n");
let returnedString = "";
for (let i = position.from.line; i <= position.to.line; i++) {
if (i === position.from.line) {
returnedString =
position.from.line !== position.to.line
? eachLine[position.from.line].slice(position.from.ch)
: eachLine[position.from.line].slice(
position.from.ch,
position.to.ch + 1,
);
} else if (i === position.to.line) {
returnedString += eachLine[position.to.line].slice(0, position.to.ch + 1);
} else {
returnedString += eachLine[i];
}
if (i !== position.to.line) {
returnedString += "\n";
}
}
return returnedString;
}; };

File diff suppressed because it is too large Load Diff