All files / src nodeSpec.js

72.86% Statements 51/70
57.14% Branches 16/28
66.67% Functions 20/30
73.13% Lines 49/67

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 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 1761x 1x                           14x         13x                   6x         8x         14x     14x 27x       14x       18x 36x         18x 18x       30x       36x           30x 30x       30x 60x 14x 14x 7x 7x 7x 14x                 18x 18x       18x 36x 36x 28x 8x 4x 8x     4x                   13x 13x       4x                                           6x 6x       4x 4x 4x 4x 8x         4x               8x 8x              
import { ASTNode } from './ast';
import { hashObject } from './utils';
 
// A NodeSpec declares the types of the fields of an ASTNode.
// It is used to compute hashes, to validate nodes (to prevent the construction
// of an ill-formed AST), and to deal with some edits.
// Its constructor expects a list of:
//
// - required: an ASTNode.
// - optional: either an ASTNode, or `null`.
// - list: an array of ASTNodes.
// - value: an ordinary value that does not contain an ASTNode.
 
// nodeSpec :: Array<ChildSpec> -> NodeSpec
export function nodeSpec(childSpecs) {
  return new NodeSpec(childSpecs);
}
 
// required :: String -> ChildSpec
export function required(fieldName) {
  return new Required(fieldName);
}
 
// optional :: String -> ChildSpec
export function optional(fieldName) {
  return new Optional(fieldName);
}
 
// list :: String -> ChildSpec
export function list(fieldName) {
  return new List(fieldName);
}
 
// value :: any -> ChildSpec
export function value(fieldName) {
  return new Value(fieldName);
}
 
class NodeSpec {
  constructor(childSpecs) {
    Iif (!(childSpecs instanceof Array)) {
      throw new Error("NodeSpec: expected to receive an array of required/optional/list specs.");
    }
    for (const childSpec of childSpecs) {
      Iif (!(childSpec instanceof ChildSpec)) {
        throw new Error("NodeSpec: all child specs must be created by one of the functions: required/optional/list.");
      }
    }
    this.childSpecs = childSpecs;
  }
 
  validate(node) {
    for (const childSpec of this.childSpecs) {
      childSpec.validate(node);
    }
  }
 
  hash(node) {
    let hashes = new HashIterator(node, this);
    return hashObject([node.type, [...hashes]]);
  }
 
  children(node) {
    return new ChildrenIterator(node, this);
  }
 
  fieldNames() {
    return this.childSpecs.map((spec) => spec.fieldName);
  }
}
 
class ChildrenIterator {
  constructor(parent, nodeSpec) {
    this.parent = parent;
    this.nodeSpec = nodeSpec;
  }
 
  *[Symbol.iterator]() {
    for (let spec of this.nodeSpec.childSpecs) {
      if (spec instanceof Value) continue;
      let field = this.parent[spec.fieldName];
      if (field instanceof ASTNode) {
        yield field;
      } else Eif (field instanceof Array) {
        for (let elem of field) {
          yield elem;
        }
      }
    }
  }
}
 
class HashIterator {
  constructor(parent, nodeSpec) {
    this.parent = parent;
    this.nodeSpec = nodeSpec;
  }
 
  *[Symbol.iterator]() {
    for (let spec of this.nodeSpec.childSpecs) {
      let field = this.parent[spec.fieldName];
      if (spec instanceof Value) {
        yield hashObject(field);
      } else if (spec instanceof List) {
        for (let elem of field) {
          yield elem.hash;
        }
      } else {
        yield (field == null)? hashObject(null) : field.hash;
      }
    }
  }
}
 
class ChildSpec {}
 
export class Required extends ChildSpec {
  constructor(fieldName) {
    super();
    this.fieldName = fieldName;
  }
 
  validate(parent) {
    Iif (!(parent[this.fieldName] instanceof ASTNode)) {
      throw new Error(`Expected the required field '${this.fieldName}' of '${parent.type}' to contain an ASTNode.`);
    }
  }
}
 
export class Optional extends ChildSpec {
  constructor(fieldName) {
    super();
    this.fieldName = fieldName;
  }
 
  validate(parent) {
    let child = parent[this.fieldName];
    if (child !== null && !(child instanceof ASTNode)) {
      throw new Error(`Expected the optional field '${this.fieldName}' of '${parent.type}' to contain an ASTNode or null.`);
    }
  }
}
 
export class List extends ChildSpec {
  constructor(fieldName) {
    super();
    this.fieldName = fieldName;
  }
 
  validate(parent) {
    let array = parent[this.fieldName];
    let valid = true;
    Eif (array instanceof Array) {
      for (const elem of array) {
        Iif (!(elem instanceof ASTNode)) valid = false;
      }
    } else {
      valid = false;
    }
    Iif (!valid) {
      throw new Error(`Expected the listy field '${this.fieldName}' of '${parent.type}' to contain an array of ASTNodes.`);
    }
  }
}
 
export class Value extends ChildSpec {
  constructor(fieldName) {
    super();
    this.fieldName = fieldName;
  }
 
  validate(_parent) {
    // Any value is valid, even `undefined`, so there's nothing to check.
  }
}