All files / transpiler/output/codegen/helpers MemberAccessValidator.ts

100% Statements 25/25
100% Branches 29/29
100% Functions 3/3
100% Lines 25/25

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                                                  10x 1x   9x 9x 2x                                   28x 2x                                                                         337x 41x   296x 196x     100x           337x 30x 30x 30x 30x 2x             98x 98x 51x       47x 337x 337x 4x                
/**
 * MemberAccessValidator - Shared validation for member access patterns
 *
 * Extracted from CodeGenerator.generateMemberAccess() and PostfixExpressionGenerator
 * to eliminate cross-file duplication of:
 * - Write-only register member checks (ADR-013)
 * - Self-referential scope access checks (ADR-016/057)
 */
 
class MemberAccessValidator {
  /**
   * ADR-013: Validate that a register member is not write-only when reading.
   * @param registerKey - underscore-joined key (e.g., "GPIO7_DR")
   * @param memberName - member being accessed (e.g., "DR")
   * @param displayName - dot-notation for error (e.g., "GPIO7.DR")
   * @param registerMemberAccess - map of register member access modifiers
   * @param isAssignmentTarget - true to skip check (write context)
   */
  static validateRegisterReadAccess(
    registerKey: string,
    memberName: string,
    displayName: string,
    registerMemberAccess: ReadonlyMap<string, string>,
    isAssignmentTarget: boolean,
  ): void {
    if (isAssignmentTarget) {
      return;
    }
    const accessMod = registerMemberAccess.get(registerKey);
    if (accessMod === "wo") {
      throw new Error(
        `cannot read from write-only register member '${memberName}' ` +
          `(${displayName} has 'wo' access modifier)`,
      );
    }
  }
 
  /**
   * ADR-016/057: Validate not referencing own scope by name.
   * @param scopeName - scope being accessed
   * @param memberName - member after scope (for error message)
   * @param currentScope - active scope context (null = not in a scope)
   */
  static validateNotSelfScopeReference(
    scopeName: string,
    memberName: string,
    currentScope: string | null,
  ): void {
    if (currentScope && scopeName === currentScope) {
      throw new Error(
        `Error: Cannot reference own scope '${scopeName}' by name. Use 'this.${memberName}' instead of '${scopeName}.${memberName}'`,
      );
    }
  }
 
  /**
   * ADR-016/057: Validate that a global entity (enum or register) is accessed
   * with 'global.' prefix when inside a scope that has a naming conflict.
   *
   * Detects two types of conflicts:
   * 1. Scope member with same name as entity (via scopeMembers lookup)
   * 2. Identifier was resolved to scope member, shadowing a global enum
   *    (via rootIdentifier != resolvedName comparison)
   *
   * @param entityName - The resolved entity name (e.g., "Color" or "Motor_Color")
   * @param memberName - The member after the entity (e.g., "Red", "PIN0")
   * @param entityType - "enum" or "register" (for error message)
   * @param currentScope - Active scope context (null = not in a scope)
   * @param isGlobalAccess - Whether the access used 'global.' prefix
   * @param options - Optional conflict detection parameters
   * @param options.scopeMembers - Map of scope names to their member names
   * @param options.rootIdentifier - Original identifier before resolution (for shadowing detection)
   * @param options.knownEnums - Set of known enum names (for shadowing detection)
   */
  static validateGlobalEntityAccess(
    entityName: string,
    memberName: string,
    entityType: string,
    currentScope: string | null,
    isGlobalAccess: boolean,
    options?: {
      scopeMembers?: ReadonlyMap<string, ReadonlySet<string>>;
      rootIdentifier?: string;
      knownEnums?: ReadonlySet<string>;
    },
  ): void {
    if (isGlobalAccess) {
      return;
    }
    if (!currentScope) {
      return;
    }
 
    const { scopeMembers, rootIdentifier, knownEnums } = options ?? {};
 
    // Check 1: Shadowing detection - identifier was resolved to a scope member
    // that shadows a global enum. This produces invalid C (e.g., Motor_Color.RED).
    // This check must run BEFORE the belongsToCurrentScope check because
    // the resolved name (e.g., Motor_Color) will start with the scope prefix.
    if (rootIdentifier && knownEnums) {
      const wasResolved = entityName !== rootIdentifier;
      const rootIsEnum = knownEnums.has(rootIdentifier);
      const resolvedIsNotEnum = !knownEnums.has(entityName);
      if (wasResolved && rootIsEnum && resolvedIsNotEnum) {
        throw new Error(
          `Error: Use 'global.${rootIdentifier}.${memberName}' to access enum '${rootIdentifier}' from inside scope '${currentScope}' (scope member '${rootIdentifier}' shadows the global enum)`,
        );
      }
    }
 
    // Skip check if entity belongs to current scope (e.g., Motor_State in scope Motor)
    const belongsToCurrentScope = entityName.startsWith(currentScope + "_");
    if (belongsToCurrentScope) {
      return;
    }
 
    // Check 2: Direct conflict - scope has a member with the same name as the entity
    const scopeMemberNames = scopeMembers?.get(currentScope);
    const hasConflict = scopeMemberNames?.has(entityName) ?? false;
    if (hasConflict) {
      throw new Error(
        `Error: Use 'global.${entityName}.${memberName}' to access ${entityType} '${entityName}' from inside scope '${currentScope}'`,
      );
    }
  }
}
 
export default MemberAccessValidator;