import { DataTree, DataTreeEntity } from "entities/DataTree/dataTreeFactory"; import { Position } from "codemirror"; import { isDynamicValue, isPathADynamicBinding, LintError, PropertyEvaluationErrorType, } from "utils/DynamicBindingUtils"; import { MAIN_THREAD_ACTION } from "workers/Evaluation/evalWorkerActions"; import { JSHINT as jshint, LintError as JSHintError, LintOptions, } from "jshint"; import { get, isEmpty, isNumber, keys, last, set } from "lodash"; import { getLintErrorMessage, getLintSeverity, } from "components/editorComponents/CodeEditor/lintHelpers"; import { CustomLintErrorCode, CUSTOM_LINT_ERRORS, IGNORED_LINT_ERRORS, INVALID_JSOBJECT_START_STATEMENT, JS_OBJECT_START_STATEMENT, SUPPORTED_WEB_APIS, } from "components/editorComponents/CodeEditor/constants"; import { extractInvalidTopLevelMemberExpressionsFromCode, isLiteralNode, ECMA_VERSION, MemberExpressionData, } from "@shared/ast"; import { getDynamicBindings } from "utils/DynamicBindingUtils"; import { createGlobalData, EvaluationScripts, EvaluationScriptType, getScriptToEval, getScriptType, ScriptTemplate, } from "workers/Evaluation/evaluate"; import { getEntityNameAndPropertyPath, isAction, isATriggerPath, isJSAction, isWidget, } from "workers/Evaluation/evaluationUtils"; import { LintErrors } from "reducers/lintingReducers/lintErrorsReducers"; import { Severity } from "entities/AppsmithConsole"; import { JSLibraries } from "workers/common/JSLibrary"; import { MessageType, sendMessage } from "utils/MessageUtil"; export function getlintErrorsFromTree( pathsToLint: string[], unEvalTree: DataTree, ): LintErrors { const lintTreeErrors: LintErrors = {}; const GLOBAL_DATA_WITHOUT_FUNCTIONS = createGlobalData({ dataTree: unEvalTree, resolvedFunctions: {}, isTriggerBased: false, }); // trigger paths const triggerPaths = new Set(); // Certain paths, like JS Object's body are binding paths where appsmith functions are needed in the global data const bindingPathsRequiringFunctions = new Set(); pathsToLint.forEach((fullPropertyPath) => { const { entityName, propertyPath } = getEntityNameAndPropertyPath( fullPropertyPath, ); const entity = unEvalTree[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)) return; if (isATriggerPath(entity, propertyPath)) return triggerPaths.add(fullPropertyPath); if (isJSAction(entity)) return bindingPathsRequiringFunctions.add(`${entityName}.body`); const lintErrors = lintBindingPath( unEvalPropertyValue, entity, fullPropertyPath, GLOBAL_DATA_WITHOUT_FUNCTIONS, ); 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 const GLOBAL_DATA_WITH_FUNCTIONS = createGlobalData({ dataTree: unEvalTree, resolvedFunctions: {}, isTriggerBased: true, skipEntityFunctions: true, }); // lint binding paths that need GLOBAL_DATA_WITH_FUNCTIONS if (bindingPathsRequiringFunctions.size) { bindingPathsRequiringFunctions.forEach((fullPropertyPath) => { const { entityName } = getEntityNameAndPropertyPath(fullPropertyPath); const entity = unEvalTree[entityName]; const unEvalPropertyValue = (get( unEvalTree, fullPropertyPath, ) as unknown) as string; // remove all lint errors from path set(lintTreeErrors, `["${fullPropertyPath}"]`, []); const lintErrors = lintBindingPath( unEvalPropertyValue, entity, fullPropertyPath, GLOBAL_DATA_WITH_FUNCTIONS, ); set(lintTreeErrors, `["${fullPropertyPath}"]`, lintErrors); }); } // Lint triggerPaths if (triggerPaths.size) { triggerPaths.forEach((triggerPath) => { const { entityName } = getEntityNameAndPropertyPath(triggerPath); const entity = unEvalTree[entityName]; const unEvalPropertyValue = (get( unEvalTree, triggerPath, ) as unknown) as string; // remove all lint errors from path set(lintTreeErrors, `["${triggerPath}"]`, []); const lintErrors = lintTriggerPath( unEvalPropertyValue, entity, GLOBAL_DATA_WITH_FUNCTIONS, ); set(lintTreeErrors, `["${triggerPath}"]`, lintErrors); }); } } return lintTreeErrors; } function lintBindingPath( dynamicBinding: string, entity: DataTreeEntity, fullPropertyPath: string, globalData: ReturnType, ) { 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: INVALID_JSOBJECT_START_STATEMENT, severity: Severity.ERROR, }, ]); } } const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath); // Get the {{binding}} bound values const { jsSnippets, stringSegments } = getDynamicBindings( dynamicBinding, entity, ); if (stringSegments) { jsSnippets.forEach((jsSnippet, index) => { if (jsSnippet) { const jsSnippetToLint = getJSToLint(entity, jsSnippet, propertyPath); // {{user's code}} const originalBinding = getJSToLint( entity, stringSegments[index], propertyPath, ); const scriptType = getScriptType(false, false); const scriptToLint = getScriptToEval(jsSnippetToLint, scriptType); const lintErrorsFromSnippet = getLintingErrors( scriptToLint, globalData, originalBinding, scriptType, ); lintErrors = lintErrors.concat(lintErrorsFromSnippet); } }); } return lintErrors; } function lintTriggerPath( userScript: string, entity: DataTreeEntity, globalData: ReturnType, ) { const { jsSnippets } = getDynamicBindings(userScript, entity); const script = getScriptToEval(jsSnippets[0], EvaluationScriptType.TRIGGERS); return getLintingErrors( script, globalData, jsSnippets[0], EvaluationScriptType.TRIGGERS, ); } export function pathRequiresLinting( dataTree: DataTree, entity: DataTreeEntity, fullPropertyPath: string, ): boolean { const { propertyPath } = getEntityNameAndPropertyPath(fullPropertyPath); const unEvalPropertyValue = (get( dataTree, fullPropertyPath, ) as unknown) as string; if (isATriggerPath(entity, propertyPath)) { return isDynamicValue(unEvalPropertyValue); } const isADynamicBindingPath = (isAction(entity) || isWidget(entity) || isJSAction(entity)) && isPathADynamicBinding(entity, propertyPath); const requiresLinting = (isADynamicBindingPath && isDynamicValue(unEvalPropertyValue)) || isJSAction(entity); return requiresLinting; } // Removes "export default" statement from js Object export function getJSToLint( entity: DataTreeEntity, snippet: string, propertyPath: string, ): string { return entity && isJSAction(entity) && propertyPath === "body" ? snippet.replace(/export default/g, "") : snippet; } export function getPositionInEvaluationScript( type: EvaluationScriptType, ): Position { const script = EvaluationScripts[type]; const index = script.indexOf(ScriptTemplate); const substr = script.slice(0, index !== -1 ? index : 0); const lines = substr.split("\n"); const lastLine = last(lines) || ""; return { line: lines.length, ch: lastLine.length }; } const EvaluationScriptPositions: Record = {}; function getEvaluationScriptPosition(scriptType: EvaluationScriptType) { if (isEmpty(EvaluationScriptPositions)) { // We are computing position of <