UNPKG

powerplatform-review-tool

Version:

Evaluate Power Platform solution zip files based on best practice patterns

518 lines (517 loc) 23.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createMsApp = createMsApp; exports.removeComments = removeComments; exports.getExternalDataSources = getExternalDataSources; exports.iterateThroughScreens = iterateThroughScreens; exports.processControlProperties = processControlProperties; exports.processSubControls = processSubControls; exports.processFailures = processFailures; exports.containsNestedIf = containsNestedIf; exports.tokenizeCode = tokenizeCode; exports.findAPIFunctionOccurrences = findAPIFunctionOccurrences; exports.findMatchingParenthesis = findMatchingParenthesis; exports.getFirstParameter = getFirstParameter; exports.normalizeDataSourceName = normalizeDataSourceName; exports.findPatchOccurrences = findPatchOccurrences; exports.getNthArgument = getNthArgument; exports.isFunctionWrapped = isFunctionWrapped; exports.findFirstLastOccurrences = findFirstLastOccurrences; exports.findFilterSearchOccurrences = findFilterSearchOccurrences; exports.findFilterSearchLookUpOccurrences = findFilterSearchLookUpOccurrences; const ManifestConstant_1 = require("../ManifestConstant"); const logger_1 = __importDefault(require("./logger")); const powerappsPatterns_1 = require("../common/powerappsPatterns"); function createMsApp() { return { appSettings: { Name: "", AppPreviewFlagsMap: {}, AppDescription: "", DocumentType: "" }, totalCodeComponents: 0, dataSources: [], appCheckerIssues: [], assets: [], yaml: {}, totalCanvasComponents: 0, totalScreens: 0, }; } function removeComments(code) { // Remove single-line comments const noSingleLineComments = code.replace(/\/\/.*$/gm, ""); // Remove multi-line comments return noSingleLineComments.replace(/\/\*[\s\S]*?\*\//gm, ""); } function getExternalDataSources(msapp) { if (!msapp.dataSources || !Array.isArray(msapp.dataSources)) { console.error("DataSources not found or not an array in msapp."); return []; } const externalDataSources = msapp.dataSources.filter((dataSource) => { return dataSource.Type !== "CollectionDataSourceInfo" && dataSource.Type !== "StaticDataSourceInfo"; }); return externalDataSources.map((dataSource) => dataSource.Name); } function iterateThroughScreens(msapp) { const yamlScreens = msapp.yaml.Screens; const failures = []; if (!yamlScreens) { logger_1.default.warn("No screens found in the yaml structure."); return failures; } const externalDataSources = getExternalDataSources(msapp).map((ds) => ds.toLowerCase()); for (const screenName in yamlScreens) { if (Object.prototype.hasOwnProperty.call(yamlScreens, screenName)) { const screen = yamlScreens[screenName]; // Process the screen properties if (screen?.Properties) { processControlProperties(screenName, "", screen.Properties, failures, msapp, screen.Control || "", "", externalDataSources); } // Process the children of the screen if (screen?.Children) { processSubControls(screenName, "", screen.Children, failures, msapp, screen.Control || "", externalDataSources); (0, powerappsPatterns_1.patternCheckForUxLayout)(screenName, screen.Children[0], failures); } } } const rootApp = msapp.yaml.App; if (rootApp?.Properties) { processControlProperties("App", "", rootApp.Properties, failures, msapp, "", "", externalDataSources); } return failures; } function processControlProperties(screenName, controlName, properties, failures, msapp, currentControlType, parentControlType, externalDataSources) { currentControlType = currentControlType.split("@")[0]; parentControlType = parentControlType.split("@")[0]; for (const propertyName in properties) { const propertyValue = properties[propertyName]; // Check if the property is in the exclusion list if (ManifestConstant_1.PropertyExclusionList.includes(propertyName)) { // Skip pattern checks for this property continue; } if (parentControlType === "Gallery" && !propertyName.startsWith("On")) { (0, powerappsPatterns_1.patternCheckForNPlusOneQuery)(propertyName, propertyValue, failures, screenName, controlName, msapp, externalDataSources, currentControlType); } //Check for NamedFormula pattern (0, powerappsPatterns_1.patternCheckForNamedFormulas)(propertyValue, screenName, controlName, propertyName, currentControlType); // Check for Inefficient Data Retrieval pattern (0, powerappsPatterns_1.patternCheckForInefficientDataRetrieval)(propertyName, propertyValue, failures, screenName, controlName, msapp, externalDataSources, currentControlType); // Check for Nested Filters pattern (0, powerappsPatterns_1.patternCheckForNestedFilters)(propertyName, propertyValue, failures, screenName, controlName, msapp, externalDataSources, currentControlType); // Check for Patch Formula Optimization pattern (0, powerappsPatterns_1.patternCheckForPatchFormulaOptimization)(propertyName, propertyValue, failures, screenName, controlName, msapp, externalDataSources, currentControlType); // Check for Code Readability pattern (0, powerappsPatterns_1.patternCheckForCodeReadability)(propertyName, propertyValue, failures, screenName, controlName, currentControlType); // Check for Nested API Calls pattern (0, powerappsPatterns_1.patternCheckForNestedAPICalls)(propertyName, propertyValue, failures, screenName, controlName, msapp, externalDataSources, currentControlType); // Check for Error Handling pattern (0, powerappsPatterns_1.patternCheckForErrorHandling)(propertyName, propertyValue, failures, screenName, controlName, msapp, externalDataSources, currentControlType); } } function processSubControls(screenName, currentControlName, children, failures, msapp, controlType, externalDataSources) { for (const child of children) { const childName = Object.keys(child)[0]; const childControl = child[childName]; if (childControl.Properties) { processControlProperties(screenName, childName, childControl.Properties, failures, msapp, childControl.Control, controlType, externalDataSources); } if (childControl.Children) { processSubControls(screenName, childName, childControl.Children, failures, msapp, childControl.Control, externalDataSources); } } } function processFailures(failures, patternDetails, skipYamlBasedPatterns = false) { let patternResults = []; const groupedFailures = groupFailuresByPattern(failures); const allPatternNames = Object.keys(ManifestConstant_1.PowerAppsPatternDetails); for (const patternName of allPatternNames) { const patternFailures = groupedFailures[patternName] || []; const patternResult = createPatternResult(patternName, patternFailures, patternDetails, skipYamlBasedPatterns); patternResults.push(patternResult); } // Now finalize leftover single-usage variables and collections which means they are never updated => push them as new failures, and build a single PatternResult for NamedFormulas with instanceValue. // We'll gather them in an array of FailureInfo, then push them to `failures` // and also include them in the NamedFormulas pattern result. const neverUpdatedFailures = []; // A) leftover singleSetVariables for (const varName of Object.keys(powerappsPatterns_1.singleSetVariables)) { // fill in the "FailureReason" and push to new array const fi = powerappsPatterns_1.singleSetVariables[varName]; fi.FailureReason = `Variable '${varName}' is initialized once but never updated. Consider defining in Named Formula.`; neverUpdatedFailures.push(fi); } // B) leftover singleCollectCollections for (const colName of Object.keys(powerappsPatterns_1.singleCollectCollections)) { const fi = powerappsPatterns_1.singleCollectCollections[colName]; fi.FailureReason = `Collection '${colName}' is initialized once but never updated (no Patch). Consider defining in Named Formula.`; neverUpdatedFailures.push(fi); } // If we have leftover items => produce a NamedFormulas pattern result: if (neverUpdatedFailures.length > 0) { // 1) Merge them into 'failures' so they appear in the global list failures.push(...neverUpdatedFailures); patternResults = patternResults.filter((item) => item.patternID !== "app-NamedFormulas"); // 2) Build a PatternResult specifically for NamedFormulas const namedFormulasInfo = ManifestConstant_1.PowerAppsPatternDetails[ManifestConstant_1.PowerAppsPatternInfo.NamedFormulas]; const patternResult = { patternID: namedFormulasInfo.patternID, patternName: namedFormulasInfo.patternName, description: namedFormulasInfo.description, status: "Warning" /* ManifestPropertyStatusNames.Warning */, // or "Fail" if you prefer // Put them in instanceValue so they appear as part of the pattern's detail instanceValue: neverUpdatedFailures, // A combined string listing all leftover items failureReason: buildNamedFormulasFailureReason(neverUpdatedFailures), recommendation: namedFormulasInfo.recommendation, docLinks: namedFormulasInfo.docLinks, severity: namedFormulasInfo.severity, category: namedFormulasInfo.category, }; patternResults.push(patternResult); } // Clear out those single usage maps so they don't persist if we re-run Object.keys(powerappsPatterns_1.singleSetVariables).forEach((k) => delete powerappsPatterns_1.singleSetVariables[k]); Object.keys(powerappsPatterns_1.singleCollectCollections).forEach((k) => delete powerappsPatterns_1.singleCollectCollections[k]); return patternResults; } function groupFailuresByPattern(failures) { const grouped = {}; for (const failure of failures) { const patternName = failure.PatternName; if (!grouped[patternName]) { grouped[patternName] = []; } grouped[patternName].push(failure); } return grouped; } // Helper to build a short summary string naming leftover variables/collections function buildNamedFormulasFailureReason(failures) { const vars = []; const cols = []; failures.forEach((fi) => { const matchVar = fi.FailureReason?.match(/Variable '(.*?)'/); if (matchVar) { vars.push(matchVar[1]); } const matchCol = fi.FailureReason?.match(/Collection '(.*?)'/); if (matchCol) { cols.push(matchCol[1]); } }); const varList = vars.length > 0 ? `Variables that are initialized but never updated: ${vars.join(", ")}.` : ""; const colList = cols.length > 0 ? `\nCollections that are initialized but never updated: ${cols.join(", ")}.` : ""; return [varList, colList].filter(Boolean).join(" "); } // FOR YAML-SKIP: define which patterns rely heavily on YAML const YamlBasedPatterns = [ ManifestConstant_1.PowerAppsPatternInfo.ErrorHandling, ManifestConstant_1.PowerAppsPatternInfo.UxLayout, ManifestConstant_1.PowerAppsPatternInfo.NamedFormulas, ManifestConstant_1.PowerAppsPatternInfo.CodeReadability, ManifestConstant_1.PowerAppsPatternInfo.NestedAPICalls, ManifestConstant_1.PowerAppsPatternInfo.NPlusOne, ManifestConstant_1.PowerAppsPatternInfo.InefficientDataRetrieval, ManifestConstant_1.PowerAppsPatternInfo.NestedFilters, ManifestConstant_1.PowerAppsPatternInfo.PatchFormulaOptimization, ]; // eslint-disable-next-line sonarjs/cognitive-complexity function createPatternResult(patternName, failures, patternDetails, skipYamlBasedPatterns) { const patternInfo = ManifestConstant_1.PowerAppsPatternDetails[patternName]; const hasFailures = failures.length > 0; // Helper function to select property value based on priority const getValueOrDefault = (customValue, defaultValue) => customValue != null && customValue !== "" ? customValue : defaultValue; // Assign base values from patternInfo const patternID = patternInfo.patternID; let { patternName: patternNameStr, description, docLinks, recommendation, severity, category } = patternInfo; // Update values if patternDetails has a matching entry if (patternDetails) { const matchingPatternDetail = patternDetails.find((detail) => detail.key === patternID); if (matchingPatternDetail) { const { data } = matchingPatternDetail; patternNameStr = getValueOrDefault(data.patternName, patternNameStr); description = getValueOrDefault(data.description, description); docLinks = getValueOrDefault(data.docLinks, docLinks); // Only apply recommendation if we have failures (or we skip). recommendation = getValueOrDefault(data.recommendation, recommendation); severity = getValueOrDefault(data.severity, severity); category = getValueOrDefault(data.category, category); } } // Default status is Pass if no failures, else Fail let status = !hasFailures ? "Pass" /* ManifestPropertyStatusNames.Pass */ : "Fail" /* ManifestPropertyStatusNames.Fail */; // FOR YAML-SKIP: // If we have no failures, and skipYamlBasedPatterns is true, and pattern is in YamlBasedPatterns => Skipped if (!hasFailures && skipYamlBasedPatterns && YamlBasedPatterns.includes(patternName)) { status = "Skipped" /* ManifestPropertyStatusNames.Skipped */; } // For NamedFormulas, set status to Warning if there are failures if (patternName === ManifestConstant_1.PowerAppsPatternInfo.NamedFormulas && hasFailures && status !== "Skipped" /* ManifestPropertyStatusNames.Skipped */) { status = "Warning" /* ManifestPropertyStatusNames.Warning */; } return { patternID, patternName: patternNameStr, description, status, instanceValue: failures, failureReason: hasFailures ? `Found ${failures.length} occurrences.` : "", recommendation, docLinks, severity, category, }; } // Helper functions called within 'pattern' functions Returns the outer if function in failure function containsNestedIf(code) { const lowerCode = code.toLowerCase(); const regex = /\bif\s*\(/gi; const stack = []; while (true) { const match = regex.exec(lowerCode); if (!match) break; const openParenIndex = match.index + match[0].indexOf("("); const { endIndex } = findMatchingParenthesis(code, openParenIndex); if (endIndex !== -1) { const currentIfBlock = code.substring(match.index, endIndex + 1); // Check if the content inside contains another If const innerContent = code.substring(openParenIndex + 1, endIndex); if (/\bif\s*\(/i.test(innerContent)) { // This If contains a nested If => return the full outer If expression return code.substring(match.index, endIndex + 1); } } } return null; // no nested If found } // eslint-disable-next-line sonarjs/cognitive-complexity function tokenizeCode(code) { const tokens = []; let currentToken = ""; let inString = false; let stringChar = ""; for (let i = 0; i < code.length; i++) { const char = code[i]; if (inString) { currentToken += char; if (char === stringChar && code[i - 1] !== "\\") { inString = false; } } else if (char === '"' || char === "'") { if (currentToken.trim()) { tokens.push(currentToken.trim()); } currentToken = char; inString = true; stringChar = char; } else if (char === "(" || char === ")" || char === ",") { if (currentToken.trim()) { tokens.push(currentToken.trim()); } tokens.push(char); currentToken = ""; } else if (/\s/.test(char)) { if (currentToken.trim()) { tokens.push(currentToken.trim()); currentToken = ""; } } else { currentToken += char; } } if (currentToken.trim()) { tokens.push(currentToken.trim()); } return tokens; } function findAPIFunctionOccurrences(code) { const occurrences = []; const regex = /\b(Filter|Search|ForAll|LookUp|Patch)\s*\(/gi; let match; while ((match = regex.exec(code)) !== null) { const functionName = match[1]; const openParenIndex = match.index + match[0].indexOf("("); // Now find the matching closing parenthesis const { endIndex } = findMatchingParenthesis(code, openParenIndex); if (endIndex !== -1) { const content = code.substring(openParenIndex + 1, endIndex); occurrences.push({ functionName, startIndex: openParenIndex + 1, endIndex, content }); } } return occurrences; } function findMatchingParenthesis(code, openParenIndex) { let parenCount = 1; let currentIndex = openParenIndex + 1; while (currentIndex < code.length && parenCount > 0) { if (code[currentIndex] === "(") { parenCount++; } else if (code[currentIndex] === ")") { parenCount--; } currentIndex++; } if (parenCount === 0) { const endIndex = currentIndex - 1; // index of closing parenthesis return { endIndex }; } else { // Mismatched parentheses return { endIndex: -1 }; } } function getFirstParameter(content) { // Parse content up to first comma, considering nested parentheses let parenCount = 0; let index = 0; while (index < content.length) { if (content[index] === "(") { parenCount++; } else if (content[index] === ")") { parenCount--; } else if (content[index] === "," && parenCount === 0) { // Found first parameter break; } index++; } return content.substring(0, index).trim(); } function normalizeDataSourceName(dataSourceName) { // Remove any surrounding brackets [ ], @ symbol, and single quotes return dataSourceName.replace(/^[\[\']\s*@?\s*(.+?)\s*[\]\']$/i, "$1").trim(); } function findPatchOccurrences(code) { const occurrences = []; const regex = /\b(Patch)\s*\(/gi; let match; while ((match = regex.exec(code)) !== null) { const functionName = match[1]; const openParenIndex = match.index + match[0].indexOf("("); // Now find the matching closing parenthesis const { endIndex } = findMatchingParenthesis(code, openParenIndex); if (endIndex !== -1) { const content = code.substring(openParenIndex + 1, endIndex); occurrences.push({ functionName, startIndex: openParenIndex + 1, endIndex, content }); } } return occurrences; } function getNthArgument(content, n) { // Parse content to get the nth argument, considering nested parentheses let parenCount = 0; let index = 0; let argStartIndex = 0; let argEndIndex = 0; let currentArgNumber = 1; while (index < content.length) { const char = content[index]; if (char === "(") { parenCount++; } else if (char === ")") { parenCount--; } else if (char === "," && parenCount === 0) { if (currentArgNumber === n) { argEndIndex = index; break; } else { currentArgNumber++; argStartIndex = index + 1; } } index++; } if (currentArgNumber === n && argEndIndex === 0) { argEndIndex = content.length; } if (currentArgNumber === n) { return content.substring(argStartIndex, argEndIndex).trim(); } else { return null; } } function isFunctionWrapped(code, functionStartIndex, wrapperFunctions) { const regex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g; let match; // Parse the code from the beginning while ((match = regex.exec(code)) !== null) { const functionName = match[1]; const openParenIndex = match.index + match[0].length - 1; // Position of '(' const { endIndex } = findMatchingParenthesis(code, openParenIndex); if (endIndex === -1) { continue; // Mismatched parentheses, skip this function } if (functionStartIndex >= openParenIndex && functionStartIndex <= endIndex && wrapperFunctions.map((fn) => fn.toLowerCase()).includes(functionName.toLowerCase())) { return true; } // Move regex lastIndex to endIndex to avoid reprocessing inside content regex.lastIndex = endIndex + 1; } return false; } function findFirstLastOccurrences(code) { const occurrences = []; const regex = /\b(First|FirstN|Last|LastN)\s*\(/gi; let match; while ((match = regex.exec(code)) !== null) { const functionName = match[1]; const openParenIndex = match.index + match[0].indexOf("("); // Now find the matching closing parenthesis const { endIndex } = findMatchingParenthesis(code, openParenIndex); if (endIndex !== -1) { const content = code.substring(openParenIndex + 1, endIndex); occurrences.push({ functionName, startIndex: openParenIndex + 1, endIndex, content }); } } return occurrences; } function findFilterSearchOccurrences(code) { const occurrences = []; const regex = /\b(Filter|Search)\s*\(/gi; let match; while ((match = regex.exec(code)) !== null) { const functionName = match[1]; const openParenIndex = match.index + match[0].indexOf("("); // Now find the matching closing parenthesis const { endIndex } = findMatchingParenthesis(code, openParenIndex); if (endIndex !== -1) { const content = code.substring(openParenIndex + 1, endIndex); occurrences.push({ functionName, startIndex: openParenIndex + 1, endIndex, content }); } } return occurrences; } function findFilterSearchLookUpOccurrences(code) { const occurrences = []; const regex = /\b(Filter|Search|LookUp)\s*\(/gi; let match; while ((match = regex.exec(code)) !== null) { const functionName = match[1]; const openParenIndex = match.index + match[0].indexOf("("); // Now find the matching closing parenthesis const { endIndex } = findMatchingParenthesis(code, openParenIndex); if (endIndex !== -1) { const content = code.substring(openParenIndex + 1, endIndex); occurrences.push({ functionName, startIndex: openParenIndex + 1, endIndex, content }); } } return occurrences; }