@typespec/compiler
Version:
TypeSpec compiler and standard library
583 lines • 23.2 kB
JavaScript
import { compilerAssert } from "../core/diagnostics.js";
import { getLocationContext } from "../core/helpers/location-context.js";
import { isNumeric } from "../core/numeric.js";
import { isTemplateInstance, isType, isValue } from "../core/type-utils.js";
import { $ } from "../typekit/index.js";
import { CustomKeyMap } from "../utils/custom-key-map.js";
import { mutate } from "../utils/misc.js";
import { useStateMap } from "../utils/state-accessor.js";
import { Realm } from "./realm.js";
/**
* Flow control for mutators.
*
* When filtering types in a mutator, the filter function may return MutatorFlow flags to control how mutation should
* proceed.
*
* @see {@link MutatorFilterFn}
*
* @experimental
*/
export var MutatorFlow;
(function (MutatorFlow) {
/**
* Mutate the type and recur, further mutating the type's children. This is the default behavior.
*/
MutatorFlow[MutatorFlow["MutateAndRecur"] = 0] = "MutateAndRecur";
/**
* If this flag is set, the type will not be mutated.
*/
MutatorFlow[MutatorFlow["DoNotMutate"] = 1] = "DoNotMutate";
/**
* If this flag is set, the mutator will not proceed recursively into the children of the type.
*/
MutatorFlow[MutatorFlow["DoNotRecur"] = 2] = "DoNotRecur";
})(MutatorFlow || (MutatorFlow = {}));
/**
* Determines if a type is mutable.
*
* @experimental
*/
function isMutableTypeWithNamespace(type) {
switch (type.kind) {
case "TemplateParameter":
case "Intrinsic":
case "Decorator":
case "FunctionParameter":
return false;
default:
void type;
return true;
}
}
/**
* Determines if a type is mutable.
*
* @experimental
*/
export function isMutableType(type) {
switch (type.kind) {
case "TemplateParameter":
case "Intrinsic":
case "Decorator":
case "FunctionParameter":
case "Namespace":
return false;
default:
void type;
return true;
}
}
// These keyers assign a stable identity to each type/mutator. They are backed by
// `WeakMap`s and never strongly retain their keys, so they are safe to keep at
// module scope and shared across programs.
const typeId = CustomKeyMap.objectKeyer();
const mutatorId = CustomKeyMap.objectKeyer();
// `seen` memoizes mutation results so cyclic and shared types are cloned exactly
// once. It must be shared across the nested `mutateSubgraph`/
// `mutateSubgraphWithNamespace` calls a mutator makes while running (each spins
// up its own engine); this is what lets recursive type graphs terminate instead
// of recursing forever, and it also lets repeated mutations reuse earlier clones.
//
// The cache is scoped per `Program` rather than to the module so that it — and
// every `Type` it references — becomes eligible for garbage collection once the
// program does. A module-level cache would instead pin the type graph of every
// mutated program in memory for the lifetime of the process.
const seenByProgram = new WeakMap();
function getSeenCache(program) {
let seen = seenByProgram.get(program);
if (seen === undefined) {
seen = new CustomKeyMap(([type, mutators]) => {
const key = `${typeId.getKey(type)}-${[...mutators.values()]
.map((v) => mutatorId.getKey(v))
.join("-")}`;
return key;
});
seenByProgram.set(program, seen);
}
return seen;
}
/**
* Mutate the type graph, allowing namespaces to be mutated.
*
* **Warning**: This function will likely mutate the entire type graph. Most TypeSpec types relate to namespaces
* in some way (e.g. through namespace parent links, or the `namespace` property of a Model).
*
* @param program - The program in which the `type` occurs.
* @param mutators - An array of mutators to apply to the type graph rooted at `type`.
* @param type - The type to mutate.
*
* @returns an object containing the mutated `type` and a nullable `Realm` in which the mutated type resides.
*
* @see {@link mutateSubgraph}
*
* @experimental
*/
export function mutateSubgraphWithNamespace(program, mutators, type) {
const engine = createMutatorEngine(program, mutators, {
mutateNamespaces: true,
});
const mutated = engine.mutate(type);
if (mutated === type) {
return { realm: null, type };
}
return { realm: engine.realm, type: mutated };
}
/**
* Mutate the type graph.
*
* Mutators clone the input `type`, creating a new type instance that is mutated in place.
*
* The mutator returns the mutated type and optionally a `realm` in which the mutated clone resides.
*
* @see {@link Mutator}
* @see {@link Realm}
*
* **Warning**: Mutators _SHOULD NOT_ modify the source type. Modifications to the source type
* will be visible to other emitters or libraries that view the original source type, and will
* be sensitive to the order in which the mutator was applied. Only edit the `clone` type.
* Furthermore, mutators must take care not to modify elements of the source and clone types
* that are shared between the two types, such as the properties of any parent references
* or the `decorators` of the type without taking care to clone them first.
*
* @param program - The program in which the `type` occurs.
* @param mutators - An array of mutators to apply to the type graph rooted at `type`.
* @param type - The type to mutate.
*
* @returns an object containing the mutated `type` and a nullable `Realm` in which the mutated type resides.
*
* @experimental
*/
export function mutateSubgraph(program, mutators, type) {
const engine = createMutatorEngine(program, mutators, {
mutateNamespaces: false,
});
const mutated = engine.mutate(type);
if (mutated === type) {
return { realm: null, type };
}
return { realm: engine.realm, type: mutated };
}
const [getAlwaysMutate, _setAlwaysMutate] = useStateMap(Symbol.for("TypeSpec.Mutators.alwaysMutate"));
/**
* Set to true to always mutate a type, even if filtering logic would exclude it.
*
* This is a hack to allow core types to be mutated when they are unfinished template instances.
*
* @internal
*/
export function setAlwaysMutate(program, type, alwaysMutate = true) {
_setAlwaysMutate(program, type, alwaysMutate);
}
function createMutatorEngine(program, mutators, options) {
const realm = new Realm(program, `Mutator realm ${mutators.map((m) => m.name).join(", ")}`);
const interstitialFunctions = [];
// Shared across every engine operating on this program (including the nested
// engines a mutator spins up via re-entrant `mutateSubgraph` calls), so
// recursive type graphs are cloned once and terminate. Scoped to the program
// so it is collected once the program is.
const seen = getSeenCache(program);
let preparingNamespace = false;
const muts = new Set(mutators);
const postVisits = [];
const namespacesVisitedContent = new Set();
// If we are mutating namespace we need to make sure we mutate everything first
if (options.mutateNamespaces) {
preparingNamespace = true;
// Prepare namespaces first
mutateSubgraphWorker(program.getGlobalNamespaceType(), muts);
preparingNamespace = false;
postVisits.forEach((visit) => visit());
}
return {
realm,
mutate: (type) => {
return mutateSubgraphWorker(type, muts);
},
};
/** Resolve the mutators to apply. */
function resolveMutators(activeMutators, type) {
const mutatorsWithOptions = [];
const newMutators = new Set(activeMutators.values());
// step 1: see what mutators to run
for (const mutator of activeMutators) {
const record = mutator[type.kind];
if (!record) {
continue;
}
let mutationFn;
let replaceFn = null;
let mutate;
let recurse;
if (typeof record === "function") {
mutationFn = record;
mutate = true;
recurse = true;
}
else {
mutationFn = "mutate" in record ? record.mutate : null;
replaceFn = "replace" in record ? record.replace : null;
if (record.filter) {
const filterResult = record.filter(type, program, realm);
if (filterResult === true) {
mutate = true;
recurse = true;
}
else if (filterResult === false) {
mutate = false;
recurse = true;
}
else {
mutate = (filterResult & MutatorFlow.DoNotMutate) === 0;
recurse = (filterResult & MutatorFlow.DoNotRecur) === 0;
}
}
else {
mutate = true;
recurse = true;
}
}
if (!recurse) {
newMutators.delete(mutator);
}
if (mutate) {
mutatorsWithOptions.push({ mutator, mutationFn, replaceFn });
}
}
return { mutatorsWithOptions, newMutators };
}
function applyMutations(type, clone, mutatorsWithOptions) {
let resolved = clone;
for (const { mutationFn, replaceFn } of mutatorsWithOptions) {
// todo: handle replace earlier in the mutation chain
const result = (mutationFn ?? replaceFn)(type, resolved, program, realm);
if (replaceFn && result !== undefined) {
resolved = result;
}
}
return resolved;
}
function mutateSubgraphWorker(type, activeMutators, mutateSubNamespace = true) {
let existing = seen.get([type, activeMutators]);
if (existing) {
if (mutateSubNamespace &&
existing.kind === "Namespace" &&
!namespacesVisitedContent.has(existing)) {
namespacesVisitedContent.add(existing);
mutateSubMap(existing, "namespaces", true, activeMutators);
}
clearInterstitialFunctions();
return existing;
}
// Mutating compiler types breaks a lot of things in the type checker. It is better to keep any of those as they are.
const alwaysMutate = getAlwaysMutate(program, type);
if (!alwaysMutate &&
getLocationContext(program, type).type === "compiler" &&
!isTemplateInstance(type)) {
return type;
}
let clone = null;
const { mutatorsWithOptions, newMutators } = resolveMutators(activeMutators, type);
const mutatorsToApply = mutatorsWithOptions.map((v) => v.mutator);
let mutating = false;
// if we have no mutators to apply, let's bail out.
if (mutatorsWithOptions.length === 0) {
if (newMutators.size > 0) {
// we might need to clone this type later if something in our subgraph needs mutated.
interstitialFunctions.push(initializeClone);
seen.set([type, activeMutators], type);
visitSubgraph();
visitParents();
interstitialFunctions.pop();
return clone ?? type;
}
else {
// we don't need to clone this type, so let's just return it.
return type;
}
}
clearInterstitialFunctions();
// step 2: see if we need to mutate based on the set of mutators we're actually going to run
existing = seen.get([type, mutatorsToApply]);
if (existing) {
return existing;
}
// step 3: run the mutators
initializeClone();
const result = applyMutations(type, clone, mutatorsWithOptions);
if (result !== clone) {
clone = result;
seen.set([type, activeMutators], clone);
seen.set([type, mutatorsToApply], clone);
}
if (newMutators.size > 0) {
if (preparingNamespace && type.kind === "Namespace") {
compilerAssert(mutating, "Cannot be preparing namespaces and not have cloned it.");
prepareNamespace(clone);
postVisits.push(() => visitNamespaceContents(clone));
}
else {
visitSubgraph();
}
}
if (type.isFinished) {
$(realm).type.finishType(clone);
}
if (type.kind === "Namespace" && mutateSubNamespace) {
compilerAssert(mutating, "Cannot be preparing namespaces and not have cloned it.");
visitSubNamespaces(clone);
}
if (type.kind !== "Namespace") {
visitParents();
}
return clone;
function initializeClone() {
clone = $(realm).type.clone(type);
mutating = true;
seen.set([type, activeMutators], clone);
seen.set([type, mutatorsToApply], clone);
}
function clearInterstitialFunctions() {
for (const interstitial of interstitialFunctions) {
interstitial();
}
interstitialFunctions.length = 0;
}
function mutateNamespaceProperty(root) {
if (!options.mutateNamespaces) {
return;
}
if ("namespace" in root && root.namespace) {
const newNs = mutateSubgraphWorker(root.namespace, newMutators, false);
compilerAssert(newNs.kind === "Namespace", `Expected to be mutated to a namespace`);
clone.namespace = newNs;
}
}
function prepareNamespace(root) {
visitDecorators(root, true, newMutators);
mutateNamespaceProperty(root);
}
function visitSubNamespaces(type) {
namespacesVisitedContent.add(type);
mutateSubMap(type, "namespaces", true, newMutators);
}
function visitNamespaceContents(root) {
mutateSubMap(root, "models", mutating, newMutators);
mutateSubMap(root, "operations", mutating, newMutators);
mutateSubMap(root, "interfaces", mutating, newMutators);
mutateSubMap(root, "enums", mutating, newMutators);
mutateSubMap(root, "unions", mutating, newMutators);
mutateSubMap(root, "scalars", mutating, newMutators);
}
function visitModel(root) {
mutateSubMap(root, "properties", mutating, newMutators);
if (root.indexer) {
const res = mutateSubgraphWorker(root.indexer.value, newMutators);
if (mutating) {
root.indexer.value = res;
}
}
for (const [index, prop] of root.sourceModels.entries()) {
const newModel = mutateSubgraphWorker(prop.model, newMutators);
if (mutating) {
mutate(root.sourceModels[index]).model = newModel;
}
}
mutateProperty(root, "sourceModel", mutating, newMutators);
mutateProperty(root, "baseModel", mutating, newMutators);
mutateSubArray(root, "derivedModels", mutating, newMutators);
}
function visitSubgraph() {
const root = clone ?? type;
switch (root.kind) {
case "Namespace":
visitNamespaceContents(root);
break;
case "Model":
visitModel(root);
break;
case "ModelProperty":
mutateProperty(root, "type", mutating, newMutators);
mutateProperty(root, "sourceProperty", mutating, newMutators);
break;
case "Tuple":
mutateSubArray(root, "values", mutating, newMutators);
break;
case "Operation":
mutateProperty(root, "parameters", mutating, newMutators);
mutateProperty(root, "returnType", mutating, newMutators);
mutateProperty(root, "sourceOperation", mutating, newMutators);
break;
case "Interface":
mutateSubMap(root, "operations", mutating, newMutators);
break;
case "Enum":
mutateSubMap(root, "members", mutating, newMutators);
break;
case "EnumMember":
break;
case "Union":
mutateSubMap(root, "variants", mutating, newMutators);
break;
case "UnionVariant":
mutateProperty(root, "type", mutating, newMutators);
break;
case "Scalar":
mutateSubMap(root, "constructors", mutating, newMutators);
mutateProperty(root, "baseScalar", mutating, newMutators);
mutateSubArray(root, "derivedScalars", mutating, newMutators);
break;
case "ScalarConstructor":
mutateProperty(root, "scalar", mutating, newMutators);
break;
}
if ("templateMapper" in root) {
mutateTemplateMapper(root, mutating, newMutators);
}
if ("decorators" in root) {
visitDecorators(root, mutating, newMutators);
}
mutateNamespaceProperty(root);
}
// Parents needs to be visited after the type is finished
function visitParents() {
const root = clone ?? type;
switch (root.kind) {
case "ModelProperty":
mutateProperty(root, "model", mutating, newMutators);
break;
case "Operation":
mutateProperty(root, "interface", mutating, newMutators);
break;
case "EnumMember":
mutateProperty(root, "enum", mutating, newMutators);
break;
case "UnionVariant":
mutateProperty(root, "union", mutating, newMutators);
break;
case "ScalarConstructor":
mutateProperty(root, "scalar", mutating, newMutators);
break;
}
}
}
function visitDecorators(type, mutating, newMutators) {
for (const [index, dec] of type.decorators.entries()) {
const args = [];
for (const arg of dec.args) {
args.push({
...arg,
value: mutateDecoratorArgumentValue(arg.value, newMutators),
jsValue: mutateDecoratorArgumentValue(arg.jsValue, newMutators),
});
}
if (mutating) {
type.decorators[index] = { ...dec, args };
}
}
}
function mutateDecoratorArgumentValue(value, newMutators) {
if (typeof value === "object" && value !== null) {
if (isType(value)) {
return isMutableTypeWithNamespace(value)
? mutateSubgraphWorker(value, newMutators)
: value;
}
if (isValue(value)) {
return mutateValue(value, newMutators);
}
if (isNumeric(value)) {
return value;
}
if (Array.isArray(value)) {
return value.map((item) => mutateDecoratorArgumentValue(item, newMutators));
}
return Object.entries(value).reduce((acc, [key, val]) => {
return {
...acc,
[key]: mutateDecoratorArgumentValue(val, newMutators),
};
}, {});
}
return value;
}
function mutateValue(value, newMutators) {
switch (value.valueKind) {
case "ObjectValue":
const newObjectValue = {
...value,
properties: new Map(),
type: mutateDecoratorArgumentValue(value.type, newMutators),
};
for (const [key, val] of value.properties.entries()) {
newObjectValue.properties.set(key, {
...val,
value: mutateValue(val.value, newMutators),
});
}
return newObjectValue;
case "ArrayValue":
return {
...value,
type: mutateDecoratorArgumentValue(value.type, newMutators),
values: value.values.map((val) => mutateValue(val, newMutators)),
};
case "EnumValue":
return {
...value,
type: mutateDecoratorArgumentValue(value.type, newMutators),
value: mutateDecoratorArgumentValue(value.value, newMutators),
};
default:
return value;
}
}
function mutateTemplateMapper(type, mutating, newMutators) {
if (type.templateMapper === undefined) {
return;
}
const mutatedMapper = {
...type.templateMapper,
args: [],
map: new Map(),
};
for (const arg of type.templateMapper.args) {
mutate(mutatedMapper.args).push(mutateSubgraphWorker(arg, newMutators));
}
for (const [param, paramType] of type.templateMapper.map) {
mutatedMapper.map.set(param, mutateSubgraphWorker(paramType, newMutators));
}
if (mutating) {
type.templateMapper = mutatedMapper;
}
}
function mutateSubMap(type, prop, mutate, newMutators) {
for (const [key, value] of type[prop].entries()) {
const newValue = mutateSubgraphWorker(value, newMutators);
if (mutate) {
type[prop].set(key, newValue);
if (newValue.name !== value.name) {
type[prop].rekey(key, newValue.name);
}
}
}
}
function mutateSubArray(type, prop, mutate, newMutators) {
for (const [index, value] of type[prop].entries()) {
const newValue = mutateSubgraphWorker(value, newMutators);
if (mutate) {
type[prop][index] = newValue;
}
}
}
function mutateProperty(type, prop, mutating, newMutators) {
if (type[prop] === undefined) {
return;
}
const newValue = mutateSubgraphWorker(type[prop], newMutators);
if (mutating) {
type[prop] = newValue;
}
}
}
// #endregion
//# sourceMappingURL=mutators.js.map