UNPKG

@typespec/compiler

Version:

TypeSpec compiler and standard library

583 lines 23.2 kB
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