From b1cc6f4a3c601540b970ebd8ce3a44dc9787d47a Mon Sep 17 00:00:00 2001 From: olivier Dufour Date: Tue, 2 Apr 2024 16:10:07 +0200 Subject: [PATCH 1/2] apex: basic parser to propose field, properties and method on object --- addon/apex-runner.js | 160 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 156 insertions(+), 4 deletions(-) diff --git a/addon/apex-runner.js b/addon/apex-runner.js index 051d218b..04be9406 100644 --- a/addon/apex-runner.js +++ b/addon/apex-runner.js @@ -71,6 +71,7 @@ class Model { this.initialScript = ""; this.describeInfo = new DescribeInfo(this.spinFor.bind(this), () => { this.scriptAutocompleteHandler({newDescribe: true}); + //TODO refresh list of field this.didUpdate(); }); @@ -103,6 +104,8 @@ class Model { "Id batchId= Database.executeBatch(new BatchExample(), 200);", "ID jobID = System.enqueueJob(new AsyncExecutionExample());" ]; + this.propertyTypes = new Map(); + this.typeProperties = new Map(); this.spinFor(sfConn.soap(sfConn.wsdl(apiVersion, "Partner"), "getUserInfo", {}).then(res => { this.userInfo = res.userFullName + " / " + res.userName + " / " + res.organizationName; @@ -283,6 +286,11 @@ class Model { } return; } + let contextPath; + if (searchTerm && searchTerm.includes(".")) { + [contextPath, searchTerm] = searchTerm.split(".", 2); + + } let keywords = [ {value: "Blob", title: "Blob", suffix: " ", rank: 3, autocompleteType: "fieldName", dataType: "double"}, {value: "Boolean", title: "Boolean", suffix: " ", rank: 3, autocompleteType: "fieldName", dataType: "boolean"}, @@ -319,10 +327,9 @@ class Model { title: "Class suggestions:", results: new Enumerable(vm.apexClasses.records) // custom class .flatMap(function* (c) { - if (searchTerm && searchTerm.includes(".")) { - let [namespace, cls] = searchTerm.split(".", 2); - if (c.NamespacePrefix && c.NamespacePrefix.toLowerCase() == namespace.toLowerCase() - && c.Name.toLowerCase().includes(cls.toLowerCase())) { + if (contextPath) { + if (c.NamespacePrefix && c.NamespacePrefix.toLowerCase() == contextPath.toLowerCase() + && c.Name.toLowerCase().includes(searchTerm.toLowerCase())) { yield {"value": c.NamespacePrefix + "." + c.Name, "title": c.NamespacePrefix + "." + c.Name, "suffix": " ", "rank": 4, "autocompleteType": "class"}; } } else if (!c.NamespacePrefix && c.Name.toLowerCase().includes(searchTerm.toLowerCase())) { @@ -346,12 +353,156 @@ class Model { new Enumerable(keywords) //keywords .filter(keyword => keyword.title.toLowerCase().includes(searchTerm.toLowerCase())) ) + .concat( + new Enumerable(this.propertyTypes.keys()) + .filter(prop => contextPath && prop.toLowerCase().includes(contextPath.toLowerCase())) + .map(k => this.propertyTypes.get(k)) + .filter(k => k) + .flatMap(typ => this.typeProperties.get(typ)) + .filter(f => f && f.toLowerCase().startsWith(searchTerm.toLowerCase())) + .map(n => ({"value": n, "title": n, "suffix": " ", "rank": 0, "autocompleteType": "variable"})) + ) .toArray() .sort(vm.resultsSort(searchTerm)) .slice(0, 20) //only 10 first result }; } + //basic parser + parseAnonApex(source) { + if (!source) { + return; + } + source.replaceAll(/\/\/.*\n/g, "\n").replaceAll(/\/\*(.|\r|\n)*\*\//g, "\n").split(";").forEach(statement => { + let line = statement.trim() + ";"; + let forMatch = line.match(/^for\s*\(/); + if (forMatch) { + line = line.substring(forMatch[0].length); + } + let whileMatch = line.match(/^while\s*\(/); + if (whileMatch) { + line = line.substring(whileMatch[0].length); + } + line = line.trim(); + //[public | private | protected | global] + if (line.startsWith("public ")){ + line = line.substring(7); + line = line.trim(); + } else if (line.startsWith("private ")){ + line = line.substring(8); + line = line.trim(); + } else if (line.startsWith("protected ")){ + line = line.substring(10); + line = line.trim(); + } else if (line.startsWith("global ")){ + line = line.substring(7); + line = line.trim(); + } + //[final | override] + if (line.startsWith("final ")){ + line = line.substring(6); + line = line.trim(); + } else if (line.startsWith("override ")){ + line = line.substring(9); + line = line.trim(); + } + + if (line.startsWith("static ")){ + line = line.substring(7); + line = line.trim(); + } + + // type name + let fieldRE = /^([a-zA-Z][a-zA-Z0-9_]+)\s+([a-zA-Z][a-zA-Z0-9_]+)(\s*[=(;{]?)/; + let fieldMatch = fieldRE.exec(line); + if (fieldMatch) { + this.propertyTypes.set(fieldMatch[2], fieldMatch[1]); + } + }); + //TODO Set and remove primitive + let {globalDescribe, globalStatus} = this.describeInfo.describeGlobal(false); + let classes = new Set(); + for (let dataType of this.propertyTypes.values()) { + //SObject field + //TODO describeInfo.DidUpdate must do the same when ready so move it to external method + if (globalStatus == "ready") { + let sobj = globalDescribe.sobjects.find(sobjectDescribe => (sobjectDescribe.name == dataType)); + if (sobj) { + let {sobjectStatus, sobjectDescribe} = this.describeInfo.describeSobject(false, dataType); + if (sobjectStatus == "ready") { + let fields = sobjectDescribe.fields.map(field => field.Name); + fields.push("addError("); + fields.push("clear("); + fields.push("clone("); + fields.push("get("); + fields.push("getCloneSourceId("); + fields.push("getErrors("); + fields.push("getOptions("); + fields.push("getPopulatedFieldsAsMap("); + fields.push("getSObject("); + fields.push("getSObjects("); + fields.push("getSObjectType("); + fields.push("getQuickActionName("); + fields.push("hasErrors("); + fields.push("isClone("); + fields.push("isSet("); + fields.push("put("); + fields.push("putSObject("); + fields.push("setOptions("); + this.typeProperties.set(dataType, fields); + } + continue; + } + } + //potential class + if (this.apexClasses.records.some(cls => cls.Name == dataType)){ + classes.add(dataType); + } + } + if (!classes || classes.size == 0) { + return; + } + let queryApexClass = "SELECT Id, Name, NamespacePrefix, Body FROM ApexClass WHERE Name in (" + Array.from(classes).map(c => "'" + c + "'").join(",") + ")"; + let apexClassesSource = new RecordTable(); + this.batchHandler(sfConn.rest("/services/data/v" + apiVersion + "/query/?q=" + encodeURIComponent(queryApexClass), {}), this, apexClassesSource, (isFinished) => { + if (!isFinished){ + return; + } + apexClassesSource.records.forEach(cls => { + this.parseClass(cls.Body, cls.Name); + }); + }) + .catch(error => { + console.error(error); + }); + } + parseClass(source, clsName){ + //todo build hierarchy of block List with startPosition, endPosition and context + //for moment simple list + if (!source) { + return; + } + let cleanedSource = source.replaceAll(/\/\/.*\n/g, "\n").replaceAll(/\/\*(.|\r|\n)*?\*\//g, ""); + // type name + //let fieldRE = /(public|global)\s+(static\s*)?([a-zA-Z][a-zA-Z0-9_<>]+)\s+([a-zA-Z][a-zA-Z0-9_]+)\s*(;|=|\(|\{)/g; + let fieldRE = /(public|global)\s+(static\s*)?([a-zA-Z][a-zA-Z0-9_<>]+)\s+([a-zA-Z][a-zA-Z0-9_]+)/g; + //let methodRE = /(public|public static|global|global static)\s*([a-zA-Z][a-zA-Z0-9_<>]+)\s+([a-zA-Z][a-zA-Z0-9_]+)\s*(\([^\{]*\))\{/g; + let fieldMatch = null; + let fields = []; + while ((fieldMatch = fieldRE.exec(cleanedSource)) !== null) { + if (fieldMatch[3] == "class") { + continue; + } + //if (fieldMatch[5] == "(") { + // fields.push(fieldMatch[4] + "("); + //} else { + fields.push(fieldMatch[4]); + //} + } + //TODO inner class + this.typeProperties.set(clsName, fields); + } + /** * APEX script autocomplete handling. */ @@ -362,6 +513,7 @@ class Model { let selEnd = vm.scriptInput.selectionEnd; let ctrlSpace = e.ctrlSpace; let numberOfLines = script.split("\n").length; + this.parseAnonApex(script); if (vm.numberOfLines != numberOfLines) { vm.numberOfLines = numberOfLines; vm.didUpdate(); From 4e82048145e94bb8496883a9884aaa0222b6b149 Mon Sep 17 00:00:00 2001 From: olivier Dufour Date: Tue, 2 Apr 2024 18:03:02 +0200 Subject: [PATCH 2/2] fix few bugs on apex parsing --- addon/apex-runner.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/addon/apex-runner.js b/addon/apex-runner.js index 04be9406..08268b4a 100644 --- a/addon/apex-runner.js +++ b/addon/apex-runner.js @@ -106,6 +106,10 @@ class Model { ]; this.propertyTypes = new Map(); this.typeProperties = new Map(); + this.typeProperties.set("List", ["add(", "addAll(", "clear(", "clone(", "contains(", "deepClone(", "equals(", "get(", "getSObjectType(", "hashCode(", "indexOf(", "isEmpty(", "iterator(", "remove(", "set(", "size(", "sort(", "toString("]); + this.typeProperties.set("Map", ["clear(", "clone(", "containsKey(", "deepClone(", "equals(", "get(", "getSObjectType(", "hashCode(", "isEmpty(", "keySet(", "put(", "putAll(", "putAll(", "remove(", "size(", "toString(", "values("]); + this.typeProperties.set("Set", ["add(", "addAll(", "addAll(", "clear(", "clone(", "contains(", "containsAll(", "containsAll(", "equals(", "hashCode(", "isEmpty(", "remove(", "removeAll(", "removeAll(", "retainAll(", "retainAll(", "size("]); + this.typeProperties.set("Database", ["convertLead(", "countQuery(", "countQueryWithBinds(", "delete(", "deleteAsync(", "deleteImmediate(", "emptyRecycleBin(", "executeBatch(", "getAsyncDeleteResult(", "getAsyncLocator(", "getAsyncSaveResult(", "getDeleted(", "getQueryLocator(", "getQueryLocatorWithBinds(", "getUpdated(", "insert(", "insertAsync(", "insertImmediate(", "merge(", "query(", "queryWithBinds(", "releaseSavepoint(", "rollback(", "setSavepoint(", "undelete(", "update(", "upsert(", "updateAsync(", "updateImmediate("]); this.spinFor(sfConn.soap(sfConn.wsdl(apiVersion, "Partner"), "getUserInfo", {}).then(res => { this.userInfo = res.userFullName + " / " + res.userName + " / " + res.organizationName; @@ -373,6 +377,9 @@ class Model { if (!source) { return; } + this.propertyTypes.clear(); + //TODO ugly hack for static class + this.propertyTypes.set("Database", "Database"); source.replaceAll(/\/\/.*\n/g, "\n").replaceAll(/\/\*(.|\r|\n)*\*\//g, "\n").split(";").forEach(statement => { let line = statement.trim() + ";"; let forMatch = line.match(/^for\s*\(/); @@ -485,7 +492,7 @@ class Model { let cleanedSource = source.replaceAll(/\/\/.*\n/g, "\n").replaceAll(/\/\*(.|\r|\n)*?\*\//g, ""); // type name //let fieldRE = /(public|global)\s+(static\s*)?([a-zA-Z][a-zA-Z0-9_<>]+)\s+([a-zA-Z][a-zA-Z0-9_]+)\s*(;|=|\(|\{)/g; - let fieldRE = /(public|global)\s+(static\s*)?([a-zA-Z][a-zA-Z0-9_<>]+)\s+([a-zA-Z][a-zA-Z0-9_]+)/g; + let fieldRE = /(public|global)\s+(static\s*)?([a-zA-Z0-9_<>.]+)\s+([a-zA-Z][a-zA-Z0-9_]+)/g; //let methodRE = /(public|public static|global|global static)\s*([a-zA-Z][a-zA-Z0-9_<>]+)\s+([a-zA-Z][a-zA-Z0-9_]+)\s*(\([^\{]*\))\{/g; let fieldMatch = null; let fields = [];