All files / src/ast/reference method-reference.ts

75% Statements 21/28
70% Branches 7/10
83.33% Functions 5/6
73.07% Lines 19/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83                      119x 119x               103x   103x 103x   103x 3x               100x 100x 100x 100x 85x         10x 40x                                     10x 10x 10x         10x                 7x      
import type { ExpressionState } from '../../expression-state.js';
import { TypedValue } from '../../typed-value.js';
import { SpelNodeImpl } from '../spel-node.js';
import { NodeType } from '../../language/node-type.js';
import { SpelEvaluationException } from '../../error/spel-evaluation-exception.js';
import { SpelMessage } from '../../error/spel-message.js';
 
export class MethodReference extends SpelNodeImpl {
  private readonly methodName: string;
 
  constructor(startPos: number, endPos: number, methodName: string, ...args: SpelNodeImpl[]) {
    super(NodeType.METHOD_REFERENCE, startPos, endPos, ...args);
    this.methodName = methodName;
  }
 
  public getMethodName(): string {
    return this.methodName;
  }
 
  public getValueInternal(state: ExpressionState): TypedValue {
    const target = state.getThis().getValue();
    // Arguments resolve at the expression's root scope, not at the receiver's.
    const argumentScope = state.getArgumentScope();
    const argValues = this.children.map((c) => c.getValue(argumentScope).getValue());
 
    if (target === null || target === undefined) {
      throw new SpelEvaluationException(
        this.startPos,
        SpelMessage.METHOD_NOT_FOUND,
        this.methodName,
        'null',
      );
    }
 
    const context = state.getEvaluationContext();
    for (const resolver of context.getMethodResolvers()) {
      const result = resolver.resolve(context, target, this.methodName, argValues);
      if (result !== null) {
        return result;
      }
    }
 
    // Fallback: try property accessor chain (e.g., TypeDescriptor.staticMethods)
    for (const accessor of context.getPropertyAccessors()) {
      Iif (accessor.canRead(context, target, this.methodName)) {
        const propValue = accessor.read(context, target, this.methodName).getValue();
        if (typeof propValue === 'function') {
          try {
            return new TypedValue((propValue as (...a: unknown[]) => unknown)(...argValues));
          } catch (e) {
            throw new SpelEvaluationException(
              this.startPos,
              SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION,
              this.methodName,
              (e as Error).message,
            );
          }
        }
        break;
      }
    }
 
    // Also try function registry
    try {
      const fn = state.lookupFunction(this.methodName);
      return new TypedValue(fn(...argValues));
    } catch {
      // not found in functions
    }
 
    throw new SpelEvaluationException(
      this.startPos,
      SpelMessage.METHOD_NOT_FOUND,
      this.methodName,
      typeof target,
    );
  }
 
  public toStringAST(): string {
    return `${this.methodName}(${this.children.map((c) => c.toStringAST()).join(', ')})`;
  }
}