All files / src/ast/collection projection.ts

93.54% Statements 58/62
94.73% Branches 18/19
60% Functions 3/5
93.54% Lines 58/62

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  1x 1x 1x 1x 1x   1x 8x   8x 8x 8x 8x 8x 8x 8x 8x 8x 8x   8x       8x 8x   8x 2x 2x 2x 2x 2x 2x 2x   6x 6x 5x 4x 8x   3x 3x 3x 3x 3x 3x 3x 3x   3x 3x 1x 2x 8x 8x 7x 7x 7x 7x 7x 7x 7x 3x 8x   8x     8x  
import type { ExpressionState } from '../../expression-state.js';
import { TypedValue } from '../../typed-value.js';
import { SpelNodeImpl } from '../spel-node.js';
import { SpelEvaluationException } from '../../error/spel-evaluation-exception.js';
import { SpelMessage } from '../../error/spel-message.js';
import { NodeType } from '../../language/node-type.js';
 
export class Projection extends SpelNodeImpl {
  private readonly nullSafe: boolean;
 
  constructor(
    startPos: number,
    endPos: number,
    nullSafe: boolean,
    target: SpelNodeImpl,
    projection: SpelNodeImpl,
  ) {
    super(NodeType.PROJECTION, startPos, endPos, target, projection);
    this.nullSafe = nullSafe;
  }
 
  public isNullSafe(): boolean {
    return this.nullSafe;
  }
 
  public getValueInternal(state: ExpressionState): TypedValue {
    const targetValue = this.children[0]!.getValue(state).getValue();
 
    if (targetValue === null || targetValue === undefined) {
      if (this.nullSafe) return TypedValue.NULL;
      throw new SpelEvaluationException(
        this.startPos,
        SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE,
        'null',
      );
    }
 
    if (
      !Array.isArray(targetValue) &&
      !(targetValue instanceof Set) &&
      !(targetValue instanceof Map)
    ) {
      // Spring SpEL: non-collection is wrapped in single-element array for projection
      state.pushHeadIndex(new TypedValue(targetValue));
      try {
        const projected = this.children[1]!.getValue(state).getValue();
        return new TypedValue([projected]);
      } finally {
        state.popHeadIndex();
      }
    }
 
    const collection =
      targetValue instanceof Map
        ? [...targetValue.values()]
        : [...(targetValue as Iterable<unknown>)];
    const result: unknown[] = [];
    for (const item of collection) {
      state.pushHeadIndex(new TypedValue(item));
      try {
        result.push(this.children[1]!.getValue(state).getValue());
      } finally {
        state.popHeadIndex();
      }
    }
    return new TypedValue(result);
  }
 
  public toStringAST(): string {
    return `${this.children[0]!.toStringAST()}.![${this.children[1]!.toStringAST()}]`;
  }
}