powerplatform-review-tool
Version:
Evaluate Power Platform solution zip files based on best practice patterns
518 lines (517 loc) • 23.5 kB
JavaScript
;
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;
}