All files / src/type standard-type-locator.ts

97.5% Statements 39/40
100% Branches 25/25
91.66% Functions 11/12
97.22% Lines 35/36

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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130              25x                                 5840x               5840x   5840x 58400x         236x 236x 56x         180x 179x 181x 181x 171x         9x               18x 13x   6x               3x 3x 2x                           42x           8x     8x     12x 12x 8x     4x 4x 1x   3x     8x   5x 5x 2x             42x      
import type { TypeDescriptor } from './type-descriptor.js';
import type { TypeLocator } from './type-locator.js';
import { JAVA_TYPE_DEFINITIONS, createTypeDescriptor } from './java-types.js';
import { SpelEvaluationException } from '../error/spel-evaluation-exception.js';
import { SpelMessage } from '../error/spel-message.js';
 
/** Prefixes tried for an unqualified type name, as Java's implicit imports do. */
const DEFAULT_PREFIXES: readonly string[] = ['java.lang.'];
 
/**
 * Parallels Spring StandardTypeLocator.
 *
 * Resolves the names used by `T(...)`, `instanceof` and `new ...`.
 *
 * Two behaviours matter and both mirror Spring:
 *
 *  - `java.lang` is imported implicitly, so `T(String)` and `T(java.lang.String)`
 *    are the same type. An unqualified name that is not found is retried with
 *    each registered prefix.
 *  - The catalogue of available types is explicit (see `java-types.ts`). Where
 *    Spring consults a class loader, this port has a registry, so a type that is
 *    not registered raises `TYPE_NOT_FOUND` rather than being loaded on demand.
 */
export class StandardTypeLocator implements TypeLocator {
  private readonly types = new Map<string, TypeDescriptor>();
  private readonly prefixes: string[];
 
  /**
   * @param prefixes Package prefixes tried for an unqualified name. Defaults to
   *   `java.lang`, matching Java's implicit import.
   */
  constructor(prefixes: readonly string[] = DEFAULT_PREFIXES) {
    this.prefixes = prefixes.map((prefix) => (prefix.endsWith('.') ? prefix : `${prefix}.`));
 
    for (const definition of JAVA_TYPE_DEFINITIONS) {
      this.types.set(definition.name, createTypeDescriptor(definition));
    }
  }
 
  public findType(typeName: string): TypeDescriptor {
    const exact = this.types.get(typeName);
    if (exact !== undefined) {
      return exact;
    }
 
    // An unqualified name is resolved against the implicit imports, so
    // `T(String)` finds `java.lang.String` just as it does in Java.
    if (!typeName.includes('.')) {
      for (const prefix of this.prefixes) {
        const imported = this.types.get(prefix + typeName);
        if (imported !== undefined) {
          return imported;
        }
      }
    }
 
    throw new SpelEvaluationException(-1, SpelMessage.TYPE_NOT_FOUND, typeName);
  }
 
  public registerType(name: string, descriptor: TypeDescriptor): void {
    this.types.set(name, descriptor);
  }
 
  public hasType(name: string): boolean {
    if (this.types.has(name)) {
      return true;
    }
    return this.prefixes.some((prefix) => this.types.has(prefix + name));
  }
 
  /**
   * Add a package prefix for unqualified names, mirroring Spring's
   * `registerImport`.
   */
  public registerImport(prefix: string): void {
    const normalised = prefix.endsWith('.') ? prefix : `${prefix}.`;
    if (!this.prefixes.includes(normalised)) {
      this.prefixes.push(normalised);
    }
  }
 
  /**
   * Convenience: register a JavaScript constructor as a type.
   * Automatically creates a TypeDescriptor wrapping the constructor.
   */
  public register(
    name: string,
    constructor: new (...args: unknown[]) => unknown,
    staticMethods: Record<string, (...args: unknown[]) => unknown> = {},
    staticFields: Record<string, unknown> = {},
  ): void {
    const descriptor: TypeDescriptor = {
      name,
      constructor,
      staticMethods,
      staticFields,
      isInstance(value: unknown): boolean {
        return value instanceof constructor;
      },
      newInstance(...args: unknown[]): unknown {
        return new constructor(...args);
      },
      callStaticMethod(methodName: string, ...args: unknown[]): unknown {
        const method = staticMethods[methodName];
        if (typeof method === 'function') {
          return method(...args);
        }
        // Try prototype method
        const protoMethod = (constructor.prototype as Record<string, unknown>)[methodName];
        if (typeof protoMethod === 'function') {
          return (protoMethod as (...a: unknown[]) => unknown)(...args);
        }
        throw new SpelEvaluationException(-1, SpelMessage.METHOD_NOT_FOUND, methodName, this.name);
      },
      getStaticField(fieldName: string): unknown {
        if (fieldName in staticFields) return staticFields[fieldName];
        // Try constructor property
        const ctor = constructor as unknown as Record<string, unknown>;
        if (fieldName in ctor) return ctor[fieldName];
        throw new SpelEvaluationException(
          -1,
          SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE,
          fieldName,
        );
      },
    };
    this.types.set(name, descriptor);
  }
}