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                           16x         17x                   6x         10x         16x     16x 33x       16x       176x 352x         176x 176x       280x       352x           280x 280x       280x 560x 116x 116x 58x 58x 58x 58x                 176x 176x       176x 352x 352x 280x 72x 36x 36x     36x                   17x 17x       36x                                           6x 6x       36x 36x 36x 36x 36x         36x               10x 10x              
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.
  }
}