Skip to content

Commit 62e483e

Browse files
author
Tom German
committed
Amendment of FormulaEvaluation and CustomFormatting classes to reduce use of 'any'
Amendment to CustomFormatting class type annotations
1 parent 08ac2e8 commit 62e483e

File tree

2 files changed

+37
-26
lines changed

2 files changed

+37
-26
lines changed

src/common/utilities/CustomFormatting.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { FormulaEvaluation } from "./FormulaEvaluation";
44
import { ASTNode, Context } from "./FormulaEvaluation.types";
55
import { ICustomFormattingExpressionNode, ICustomFormattingNode } from "./ICustomFormatting";
66

7+
type CustomFormatResult = string | number | boolean | JSX.Element | ICustomFormattingNode;
8+
79
/**
810
* A class that provides helper methods for custom formatting
911
* See: https://learn.microsoft.com/en-us/sharepoint/dev/declarative-customization/formatting-syntax-reference
@@ -47,15 +49,15 @@ export default class CustomFormattingHelper {
4749
* @param context A context object containing values / variables to be used in the evaluation
4850
* @returns
4951
*/
50-
private evaluateCustomFormatContent = (content: ICustomFormattingExpressionNode | ICustomFormattingNode | string | number | boolean, context: Context): JSX.Element | string | number | boolean => {
52+
private evaluateCustomFormatContent = (content: ICustomFormattingExpressionNode | ICustomFormattingNode | string | number | boolean, context: Context): CustomFormatResult => {
5153

5254
// If content is a string or number, it is a literal value and should be returned as-is
5355
if ((typeof content === "string" && content.charAt(0) !== "=") || typeof content === "number") return content;
5456

5557
// If content is a string beginning with '=' it is a formula/expression, and should be evaluated
5658
if (typeof content === "string" && content.charAt(0) === "=") {
5759
const result = this._formulaEvaluator.evaluate(content.substring(1), context);
58-
return result;
60+
return result as CustomFormatResult;
5961
}
6062

6163
// If content is an object, it is either further custom formatting described by an ICustomFormattingNode,
@@ -74,9 +76,9 @@ export default class CustomFormattingHelper {
7476
const astNode = this.convertCustomFormatExpressionNodes(content as ICustomFormattingExpressionNode);
7577
const result = this._formulaEvaluator.evaluateASTNode(astNode, context);
7678
if (typeof result === "object" && Object.prototype.hasOwnProperty.call(result, "elmType")) {
77-
return this.renderCustomFormatContent(result, context);
79+
return this.renderCustomFormatContent(result as ICustomFormattingNode, context);
7880
}
79-
return result;
81+
return result as CustomFormatResult;
8082

8183
}
8284
}
@@ -94,7 +96,7 @@ export default class CustomFormattingHelper {
9496

9597
// Custom formatting nodes / elements may have a txtContent property, which represents the inner
9698
// content of a HTML element. This can be a string literal, or another expression to be evaluated:
97-
let textContent: JSX.Element | string | number | boolean | undefined;
99+
let textContent: CustomFormatResult | undefined;
98100
if (node.txtContent) {
99101
textContent = this.evaluateCustomFormatContent(node.txtContent, context);
100102
}
@@ -135,7 +137,7 @@ export default class CustomFormattingHelper {
135137
}
136138

137139
// Custom formatting nodes / elements may have children. These are likely to be further custom formatting
138-
let children: (JSX.Element | string | number | boolean | undefined)[] = [];
140+
let children: (CustomFormatResult)[] = [];
139141

140142
// If the node has an iconName property, we'll render an Icon component as the first child.
141143
// SharePoint uses CSS to apply the icon in a ::before rule, but we can't count on the global selector for iconName

src/common/utilities/FormulaEvaluation.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import { IContext } from "../Interfaces";
32
import { ASTNode, ArrayLiteralNode, ArrayNodeValue, Context, Token, TokenType, ValidFuncNames } from "./FormulaEvaluation.types";
43

@@ -31,7 +30,7 @@ export class FormulaEvaluation {
3130
}
3231

3332
/** Evaluates a formula expression and returns the result, with optional context object for variables */
34-
public evaluate(expression: string, context: Context = {}): any {
33+
public evaluate(expression: string, context: Context = {}): boolean | string | number | ArrayNodeValue | object {
3534
context.me = this._meEmail;
3635
context.today = new Date();
3736
const tokens: Token[] = this.tokenize(expression, context);
@@ -240,7 +239,8 @@ export class FormulaEvaluation {
240239
return stack[0] as ASTNode;
241240
}
242241

243-
public evaluateASTNode(node: ASTNode | ArrayLiteralNode | ArrayNodeValue | string | number, context: Context = {}): any {
242+
public evaluateASTNode(node: ASTNode | ArrayLiteralNode | ArrayNodeValue | string | number, context: Context = {}):
243+
boolean | number | string | ArrayNodeValue | object {
244244

245245
if (!node) return 0;
246246

@@ -293,34 +293,40 @@ export class FormulaEvaluation {
293293
// OPERATOR nodes have their OPERANDS evaluated recursively, with the operator applied to the results
294294
if (node.type === "OPERATOR" && operatorTypes.includes(node.value as string) && node.operands) {
295295

296-
const leftValue = this.evaluateASTNode(node.operands[0], context);
297-
const rightValue = this.evaluateASTNode(node.operands[1], context);
296+
const leftValue = this.evaluateASTNode(node.operands[0], context) as string | number;
297+
const rightValue = this.evaluateASTNode(node.operands[1], context) as string | number;
298+
299+
// These operators are valid for both string and number operands
300+
switch (node.value) {
301+
case "==": return leftValue === rightValue ? 1 : 0;
302+
case "!=": return leftValue !== rightValue ? 1 : 0;
303+
case "<>": return leftValue !== rightValue ? 1 : 0;
304+
case ">": return leftValue > rightValue ? 1 : 0;
305+
case "<": return leftValue < rightValue ? 1 : 0;
306+
case ">=": return leftValue >= rightValue ? 1 : 0;
307+
case "<=": return leftValue <= rightValue ? 1 : 0;
308+
case "&&": return (leftValue !== 0 && rightValue !== 0) ? 1 : 0;
309+
case "||": return (leftValue !== 0 || rightValue !== 0) ? 1 : 0;
310+
}
298311

299312
if (typeof leftValue === "string" || typeof rightValue === "string") {
300-
// Throw an error if the operator is not valid for strings
301-
if (["-", "*", "/"].includes(node.value as string)) {
302-
throw new Error(`Invalid operation ${node.value} with string operand.`);
303-
}
304313
// Concatenate strings if either operand is a string
305314
if (node.value === "+") {
306-
return (leftValue || "").toString() + (rightValue || "").toString();
315+
const concatString: string = (leftValue || "").toString() + (rightValue || "").toString();
316+
return concatString;
317+
} else {
318+
// Throw an error if the operator is not valid for strings
319+
throw new Error(`Invalid operation ${node.value} with string operand.`);
307320
}
308321
}
309322

323+
// Both operands will be numbers at this point
310324
switch (node.value) {
311325
case "+": return leftValue + rightValue;
312326
case "-": return leftValue - rightValue;
313327
case "*": return leftValue * rightValue;
314328
case "/": return leftValue / rightValue;
315-
case "==": return leftValue === rightValue ? 1 : 0;
316-
case "!=": return leftValue !== rightValue ? 1 : 0;
317-
case "<>": return leftValue !== rightValue ? 1 : 0;
318-
case ">": return leftValue > rightValue ? 1 : 0;
319-
case "<": return leftValue < rightValue ? 1 : 0;
320-
case ">=": return leftValue >= rightValue ? 1 : 0;
321-
case "<=": return leftValue <= rightValue ? 1 : 0;
322-
case "&&": return (leftValue !== 0 && rightValue !== 0) ? 1 : 0;
323-
case "||": return (leftValue !== 0 || rightValue !== 0) ? 1 : 0;
329+
324330
case "%": return leftValue % rightValue;
325331
case "&": return leftValue & rightValue;
326332
case "|": return leftValue | rightValue;
@@ -330,7 +336,10 @@ export class FormulaEvaluation {
330336
// Evaluation of function nodes is handled here:
331337

332338
if (node.type === "FUNCTION" && node.operands) {
333-
const funcArgs = node.operands.map(arg => this.evaluateASTNode(arg, context));
339+
340+
// Evaluate operands recursively - casting to any here to allow for any type of operand
341+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
342+
const funcArgs = node.operands.map(arg => this.evaluateASTNode(arg, context)) as any[];
334343

335344
switch (node.value) {
336345

0 commit comments

Comments
 (0)