in packages/sqrl/src/compile/SqrlCompiledOutput.ts [121:213]
private calculateSlotNames() {
const used: Set<Slot> = new Set();
const current: Set<Slot> = new Set();
const throwLoopError = (slot: Slot, slotNames: string[]) => {
// @TODO: This invariant protects us from allowing loops inside sqrl,
// but it's implemented at too late a stage to provide nice error
// messages. Ideally we'd have it higher up as well.
slotNames = slotNames.filter(isValidFeatureName).sort();
let errorMessage: string;
if (isValidFeatureName(slot.name)) {
errorMessage = `Feature '${slot.name}' depends on itself`;
slotNames = slotNames.filter((name) => name !== slot.name);
if (slotNames.length) {
errorMessage += ", see " + slotNames.join(", ");
}
} else {
this.warn(
{},
"Feature loop detected on non-feature slot:: %s",
slot.name
);
errorMessage =
"Feature loop detected with features: " + slotNames.join(", ");
}
// @NOTE: You get much better error messages with this, but it's not user safe
// const loops = this.printFeatureLoop(slot);
// errorMessage += '\nDetected loops:\n ' + loops.join('\n ');
throw buildSqrlError(slot.finalizedAst(), errorMessage);
};
const recurseUsedSlot = (slot: Slot) => {
if (used.has(slot)) {
return;
} else {
if (current.has(slot)) {
throwLoopError(
slot,
Array.from(current).map((slot) => slot.name)
);
}
}
current.add(slot);
let slotExpr: Expr;
try {
slotExpr = this.exprForSlot(slot.name);
} catch (err) {
if (err instanceof ExprLoopError) {
const names = Object.keys(this.slotExprMap).filter((name) => {
return this.slotExprMap[name] === null;
});
throwLoopError(slot, names);
} else {
throw err;
}
}
walkExpr(slotExpr, (expr) => {
(expr.load || []).forEach((slot) => {
if (!used.has(slot)) {
recurseUsedSlot(slot);
}
});
});
current.delete(slot);
used.add(slot);
};
let slotNames: string[];
if (this.slotFilter) {
Object.entries(this.slots).forEach(([name, slot]) => {
if (this.slotFilter(name)) {
recurseUsedSlot(slot);
}
});
slotNames = Object.keys(this.slots).filter((name) => {
return used.has(this.slots[name]);
});
} else {
slotNames = Object.keys(this.slots);
// This recurse is required for picking up cycles, might be able to remove one day
Object.values(this.slots).forEach((slot) => recurseUsedSlot(slot));
}
this._slotNames = slotNames;
this._usedSlotNames = slotNames.map((name) =>
this.slots.hasOwnProperty(name) ? name : null
);
}