feat: Linting in trigger fields (#7638)

This commit is contained in:
Hetu Nandu 2021-10-05 19:22:27 +05:30 committed by GitHub
parent 08e37756dc
commit 94e3ffef67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 186 additions and 124 deletions

View File

@ -480,9 +480,7 @@ class CodeEditor extends Component<Props, State> {
[], [],
) as EvaluationError[]; ) as EvaluationError[];
let annotations: Annotation[] = []; const annotations = getLintAnnotations(editor.getValue(), errors);
annotations = getLintAnnotations(editor.getValue(), errors);
this.updateLintingCallback(editor, annotations); this.updateLintingCallback(editor, annotations);
} }
@ -593,7 +591,8 @@ class CodeEditor extends Component<Props, State> {
this.state.isFocused && this.state.isFocused &&
!hideEvaluatedValue && !hideEvaluatedValue &&
("evaluatedValue" in this.props || ("evaluatedValue" in this.props ||
("dataTreePath" in this.props && !!this.props.dataTreePath)); ("dataTreePath" in this.props && !!dataTreePath));
return ( return (
<DynamicAutocompleteInputWrapper <DynamicAutocompleteInputWrapper
className="t--code-editor-wrapper" className="t--code-editor-wrapper"

View File

@ -352,7 +352,6 @@ const PropertyControl = memo((props: Props) => {
type: "Function", type: "Function",
autocompleteDataType: AutocompleteDataType.FUNCTION, autocompleteDataType: AutocompleteDataType.FUNCTION,
}; };
delete config.dataTreePath;
delete config.evaluatedValue; delete config.evaluatedValue;
} }

View File

@ -53,9 +53,15 @@ import {
EXECUTION_PARAM_REFERENCE_REGEX, EXECUTION_PARAM_REFERENCE_REGEX,
} from "constants/AppsmithActionConstants/ActionConstants"; } from "constants/AppsmithActionConstants/ActionConstants";
import { DATA_BIND_REGEX } from "constants/BindingsConstants"; import { DATA_BIND_REGEX } from "constants/BindingsConstants";
import evaluate, { EvalResult } from "workers/evaluate"; import evaluate, {
createGlobalData,
EvalResult,
EvaluationScriptType,
getScriptToEval,
} from "workers/evaluate";
import { substituteDynamicBindingWithValues } from "workers/evaluationSubstitution"; import { substituteDynamicBindingWithValues } from "workers/evaluationSubstitution";
import { Severity } from "entities/AppsmithConsole"; import { Severity } from "entities/AppsmithConsole";
import { getLintingErrors } from "workers/lint";
export default class DataTreeEvaluator { export default class DataTreeEvaluator {
dependencyMap: DependencyMap = {}; dependencyMap: DependencyMap = {};
@ -373,7 +379,8 @@ export default class DataTreeEvaluator {
} }
} }
if (isWidget(entity)) { if (isWidget(entity)) {
// Set default property dependency // Make property dependant on the default property as any time the default changes
// the property needs to change
const defaultProperties = this.widgetConfigMap[entity.type] const defaultProperties = this.widgetConfigMap[entity.type]
.defaultProperties; .defaultProperties;
Object.entries(defaultProperties).forEach( Object.entries(defaultProperties).forEach(
@ -383,6 +390,13 @@ export default class DataTreeEvaluator {
]; ];
}, },
); );
// Adding the dynamic triggers in the dependency list as they need linting whenever updated
// we dont make it dependant on anything else
if (entity.dynamicTriggerPathList) {
Object.values(entity.dynamicTriggerPathList).forEach(({ key }) => {
dependencies[`${entityName}.${key}`] = [];
});
}
} }
if (isAction(entity)) { if (isAction(entity)) {
Object.entries(entity.dependencyMap).forEach( Object.entries(entity.dependencyMap).forEach(
@ -442,9 +456,13 @@ export default class DataTreeEvaluator {
const isABindingPath = const isABindingPath =
(isAction(entity) || isWidget(entity)) && (isAction(entity) || isWidget(entity)) &&
isPathADynamicBinding(entity, propertyPath); isPathADynamicBinding(entity, propertyPath);
const isATriggerPath =
isWidget(entity) && isPathADynamicTrigger(entity, propertyPath);
let evalPropertyValue; let evalPropertyValue;
const requiresEval = const requiresEval =
isABindingPath && isDynamicValue(unEvalPropertyValue); isABindingPath &&
!isATriggerPath &&
isDynamicValue(unEvalPropertyValue);
if (propertyPath) { if (propertyPath) {
_.set(currentTree, getEvalErrorPath(fullPropertyPath), []); _.set(currentTree, getEvalErrorPath(fullPropertyPath), []);
} }
@ -475,8 +493,7 @@ export default class DataTreeEvaluator {
} else { } else {
evalPropertyValue = unEvalPropertyValue; evalPropertyValue = unEvalPropertyValue;
} }
if (isWidget(entity) && !isATriggerPath) {
if (isWidget(entity)) {
const widgetEntity = entity; const widgetEntity = entity;
const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type] const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type]
.defaultProperties; .defaultProperties;
@ -508,6 +525,10 @@ export default class DataTreeEvaluator {
return _.set(currentTree, fullPropertyPath, parsedValue); return _.set(currentTree, fullPropertyPath, parsedValue);
} }
return _.set(currentTree, fullPropertyPath, evalPropertyValue); return _.set(currentTree, fullPropertyPath, evalPropertyValue);
} else if (isATriggerPath) {
const errors = this.lintTriggerPath(evalPropertyValue, entity);
addErrorToEntityProperty(errors, currentTree, fullPropertyPath);
return currentTree;
} else if (isAction(entity)) { } else if (isAction(entity)) {
const safeEvaluatedValue = removeFunctions(evalPropertyValue); const safeEvaluatedValue = removeFunctions(evalPropertyValue);
_.set( _.set(
@ -1319,6 +1340,25 @@ export default class DataTreeEvaluator {
clearLogs() { clearLogs() {
this.logs = []; this.logs = [];
} }
private lintTriggerPath(userScript: string, entity: DataTreeEntity) {
const { jsSnippets } = getDynamicBindings(userScript, entity);
const script = getScriptToEval(
jsSnippets[0],
EvaluationScriptType.TRIGGERS,
);
const GLOBAL_DATA = createGlobalData(
this.evalTree,
this.resolvedFunctions,
true,
);
return getLintingErrors(
script,
GLOBAL_DATA,
jsSnippets[0],
EvaluationScriptType.TRIGGERS,
);
}
} }
const extractReferencesFromBinding = ( const extractReferencesFromBinding = (

View File

@ -6,12 +6,11 @@ import {
unsafeFunctionForEval, unsafeFunctionForEval,
} from "utils/DynamicBindingUtils"; } from "utils/DynamicBindingUtils";
import unescapeJS from "unescape-js"; import unescapeJS from "unescape-js";
import { JSHINT as jshint } from "jshint";
import { Severity } from "entities/AppsmithConsole"; import { Severity } from "entities/AppsmithConsole";
import { Position } from "codemirror";
import { AppsmithPromise, enhanceDataTreeWithFunctions } from "./Actions"; import { AppsmithPromise, enhanceDataTreeWithFunctions } from "./Actions";
import { ActionDescription } from "entities/DataTree/actionTriggers"; import { ActionDescription } from "entities/DataTree/actionTriggers";
import { isEmpty, last } from "lodash"; import { isEmpty } from "lodash";
import { getLintingErrors } from "workers/lint";
export type EvalResult = { export type EvalResult = {
result: any; result: any;
@ -25,7 +24,7 @@ export enum EvaluationScriptType {
TRIGGERS = "TRIGGERS", TRIGGERS = "TRIGGERS",
} }
const evaluationScriptsPos: Record<EvaluationScriptType, string> = { export const EvaluationScripts: Record<EvaluationScriptType, string> = {
[EvaluationScriptType.EXPRESSION]: ` [EvaluationScriptType.EXPRESSION]: `
function closedFunction () { function closedFunction () {
const result = <<script>> const result = <<script>>
@ -50,19 +49,6 @@ const evaluationScriptsPos: Record<EvaluationScriptType, string> = {
`, `,
}; };
const getPositionInEvaluationScript = (
type: EvaluationScriptType,
): Position => {
const script = evaluationScriptsPos[type];
const index = script.indexOf("<<script>>");
const substr = script.substr(0, index);
const lines = substr.split("\n");
const lastLine = last(lines) || "";
return { line: lines.length, ch: lastLine.length };
};
const getScriptType = ( const getScriptType = (
evalArguments?: Array<any>, evalArguments?: Array<any>,
isTriggerBased = false, isTriggerBased = false,
@ -76,73 +62,49 @@ const getScriptType = (
return scriptType; return scriptType;
}; };
const getScriptToEval = ( export const getScriptToEval = (
userScript: string, userScript: string,
type: EvaluationScriptType, type: EvaluationScriptType,
): string => { ): string => {
return evaluationScriptsPos[type].replace("<<script>>", userScript); return EvaluationScripts[type].replace("<<script>>", userScript);
};
const getLintingErrors = (
script: string,
data: Record<string, unknown>,
originalBinding: string,
scriptPos: Position,
): EvaluationError[] => {
const globalData: Record<string, boolean> = {};
Object.keys(data).forEach((datum) => (globalData[datum] = true));
// Jshint shouldn't throw errors for additional libraries
extraLibraries.forEach((lib) => (globalData[lib.accessor] = true));
globalData.console = true;
const options = {
indent: 2,
esversion: 8, // For async/await support
eqeqeq: false, // Not necessary to use ===
curly: false, // Blocks can be added without {}, eg if (x) return true
freeze: true, // Overriding inbuilt classes like Array is not allowed
undef: true, // Undefined variables should be reported as error
forin: false, // Doesn't require filtering for..in loops with obj.hasOwnProperty()
noempty: false, // Empty blocks are allowed
strict: false, // We won't force strict mode
unused: false, // Unused variables are allowed
asi: true, // Tolerate Automatic Semicolon Insertion (no semicolons)
boss: true, // Tolerate assignments where comparisons would be expected
evil: false, // Use of eval not allowed
sub: true, // Don't force dot notation
funcscope: true, // Tolerate variable definition inside control statements
// environments
browser: true,
worker: true,
mocha: false,
// global values
globals: globalData,
};
jshint(script, options);
return jshint.errors.map((lintError) => {
const ch = lintError.character;
return {
errorType: PropertyEvaluationErrorType.LINT,
raw: script,
// We are forcing warnings to errors and removing unwanted JSHint checks
severity: Severity.ERROR,
errorMessage: lintError.reason,
errorSegment: lintError.evidence,
originalBinding,
// By keeping track of these variables we can highlight the exact text that caused the error.
variables: [lintError.a, lintError.b, lintError.c, lintError.d],
code: lintError.code,
line: lintError.line - scriptPos.line,
ch: lintError.line === scriptPos.line ? ch - scriptPos.ch : ch,
};
});
}; };
const beginsWithLineBreakRegex = /^\s+|\s+$/; const beginsWithLineBreakRegex = /^\s+|\s+$/;
export const createGlobalData = (
dataTree: DataTree,
resolvedFunctions: Record<string, any>,
isTriggerBased: boolean,
evalArguments?: Array<any>,
) => {
const GLOBAL_DATA: Record<string, any> = {};
///// Adding callback data
GLOBAL_DATA.ARGUMENTS = evalArguments;
///// Mocking Promise class
GLOBAL_DATA.Promise = AppsmithPromise;
if (isTriggerBased) {
//// Add internal functions to dataTree;
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(dataTree);
///// Adding Data tree with functions
Object.keys(dataTreeWithFunctions).forEach((datum) => {
GLOBAL_DATA[datum] = dataTreeWithFunctions[datum];
});
} else {
Object.keys(dataTree).forEach((datum) => {
GLOBAL_DATA[datum] = dataTree[datum];
});
}
if (!isEmpty(resolvedFunctions)) {
Object.keys(resolvedFunctions).forEach((datum: any) => {
const resolvedObject = resolvedFunctions[datum];
Object.keys(resolvedObject).forEach((key: any) => {
GLOBAL_DATA[datum][key] = resolvedObject[key];
});
});
}
return GLOBAL_DATA;
};
export default function evaluate( export default function evaluate(
js: string, js: string,
data: DataTree, data: DataTree,
@ -157,55 +119,30 @@ export default function evaluate(
const scriptType = getScriptType(evalArguments, isTriggerBased); const scriptType = getScriptType(evalArguments, isTriggerBased);
const script = getScriptToEval(unescapedJS, scriptType); const script = getScriptToEval(unescapedJS, scriptType);
// We are linting original js binding, // We are linting original js binding,
// This will make sure that the characted count is not messed up when we do unescapejs // This will make sure that the character count is not messed up when we do unescapejs
const scriptToLint = getScriptToEval(js, scriptType); const scriptToLint = getScriptToEval(js, scriptType);
return (function() { return (function() {
let errors: EvaluationError[] = []; let errors: EvaluationError[] = [];
let result; let result;
let triggers: any[] = []; let triggers: any[] = [];
/**** Setting the eval context ****/ /**** Setting the eval context ****/
const GLOBAL_DATA: Record<string, any> = {}; const GLOBAL_DATA: Record<string, any> = createGlobalData(
///// Adding callback data data,
GLOBAL_DATA.ARGUMENTS = evalArguments; resolvedFunctions,
GLOBAL_DATA.Promise = AppsmithPromise; isTriggerBased,
if (isTriggerBased) { evalArguments,
//// Add internal functions to dataTree; );
const dataTreeWithFunctions = enhanceDataTreeWithFunctions(data);
///// Adding Data tree with functions
Object.keys(dataTreeWithFunctions).forEach((datum) => {
GLOBAL_DATA[datum] = dataTreeWithFunctions[datum];
});
} else {
///// Adding Data tree
Object.keys(data).forEach((datum) => {
GLOBAL_DATA[datum] = data[datum];
});
}
// Set it to self so that the eval function can have access to it // Set it to self so that the eval function can have access to it
// as global data. This is what enables access all appsmith // as global data. This is what enables access all appsmith
// entity properties from the global context // entity properties from the global context
for (const entity in GLOBAL_DATA) {
Object.keys(GLOBAL_DATA).forEach((key) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: No types available // @ts-ignore: No types available
self[key] = GLOBAL_DATA[key]; self[entity] = GLOBAL_DATA[entity];
});
if (!isEmpty(resolvedFunctions)) {
Object.keys(resolvedFunctions).forEach((datum: any) => {
const resolvedObject = resolvedFunctions[datum];
Object.keys(resolvedObject).forEach((key: any) => {
self[datum][key] = resolvedObject[key];
});
});
} }
errors = getLintingErrors(
scriptToLint, errors = getLintingErrors(scriptToLint, GLOBAL_DATA, js, scriptType);
GLOBAL_DATA,
js,
getPositionInEvaluationScript(scriptType),
);
///// Adding extra libraries separately ///// Adding extra libraries separately
extraLibraries.forEach((library) => { extraLibraries.forEach((library) => {

View File

@ -526,7 +526,10 @@ export const isDynamicLeaf = (unEvalTree: DataTree, propertyPath: string) => {
if (!isAction(entity) && !isWidget(entity) && !isJSAction(entity)) if (!isAction(entity) && !isWidget(entity) && !isJSAction(entity))
return false; return false;
const relativePropertyPath = convertPathToString(propPathEls); const relativePropertyPath = convertPathToString(propPathEls);
return relativePropertyPath in entity.bindingPaths; return (
relativePropertyPath in entity.bindingPaths ||
(isWidget(entity) && relativePropertyPath in entity.triggerPaths)
);
}; };
/* /*

View File

@ -0,0 +1,84 @@
import { Position } from "codemirror";
import {
EvaluationError,
extraLibraries,
PropertyEvaluationErrorType,
} from "utils/DynamicBindingUtils";
import { JSHINT as jshint } from "jshint";
import { Severity } from "entities/AppsmithConsole";
import { last } from "lodash";
import { EvaluationScripts, EvaluationScriptType } from "workers/evaluate";
export const getLintingErrors = (
script: string,
data: Record<string, unknown>,
originalBinding: string,
scriptType: EvaluationScriptType,
): EvaluationError[] => {
const scriptPos = getPositionInEvaluationScript(scriptType);
const globalData: Record<string, boolean> = {};
for (const dataKey in data) {
globalData[dataKey] = true;
}
// Jshint shouldn't throw errors for additional libraries
extraLibraries.forEach((lib) => (globalData[lib.accessor] = true));
globalData.console = true;
const options = {
indent: 2,
esversion: 8, // For async/await support
eqeqeq: false, // Not necessary to use ===
curly: false, // Blocks can be added without {}, eg if (x) return true
freeze: true, // Overriding inbuilt classes like Array is not allowed
undef: true, // Undefined variables should be reported as error
forin: false, // Doesn't require filtering for..in loops with obj.hasOwnProperty()
noempty: false, // Empty blocks are allowed
strict: false, // We won't force strict mode
unused: false, // Unused variables are allowed
asi: true, // Tolerate Automatic Semicolon Insertion (no semicolons)
boss: true, // Tolerate assignments where comparisons would be expected
evil: false, // Use of eval not allowed
funcscope: true, // Tolerate variable definition inside control statements
sub: true, // Don't force dot notation
// environments
browser: true,
worker: true,
mocha: false,
// global values
globals: globalData,
};
jshint(script, options);
return jshint.errors.map((lintError) => {
const ch = lintError.character;
return {
errorType: PropertyEvaluationErrorType.LINT,
raw: script,
// We are forcing warnings to errors and removing unwanted JSHint checks
severity: Severity.ERROR,
errorMessage: lintError.reason,
errorSegment: lintError.evidence,
originalBinding,
// By keeping track of these variables we can highlight the exact text that caused the error.
variables: [lintError.a, lintError.b, lintError.c, lintError.d],
code: lintError.code,
line: lintError.line - scriptPos.line,
ch: lintError.line === scriptPos.line ? ch - scriptPos.ch : ch,
};
});
};
export const getPositionInEvaluationScript = (
type: EvaluationScriptType,
): Position => {
const script = EvaluationScripts[type];
const index = script.indexOf("<<script>>");
const substr = script.substr(0, index);
const lines = substr.split("\n");
const lastLine = last(lines) || "";
return { line: lines.length, ch: lastLine.length };
};