@typespec/compiler
Version:
TypeSpec compiler and standard library
446 lines • 21.8 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { validateDecoratorTarget, validateDecoratorUniqueOnNode } from "../core/decorator-utils.js";
import { compilerAssert } from "../core/diagnostics.js";
import { reportDiagnostic } from "../core/messages.js";
import { addVisibilityModifiers, clearVisibilityModifiersForClass, getRawVisibilityStore, isVisible, removeVisibilityModifiers, resetVisibilityModifiersForClass, setDefaultModifierSetForVisibilityClass, VisibilityFilter, } from "../core/visibility/core.js";
import { getLifecycleVisibilityEnum } from "../core/visibility/lifecycle.js";
import { mutateSubgraph, MutatorFlow, setAlwaysMutate, } from "../experimental/mutators.js";
import { $ } from "../typekit/index.js";
import { useStateMap } from "../utils/index.js";
import { mutate } from "../utils/misc.js";
import { isKey } from "./key.js";
import { createStateSymbol, filterModelPropertiesInPlace, replaceTemplatedStringFromProperties, } from "./utils.js";
export const $withDefaultKeyVisibility = (context, entity, visibility) => {
const keyProperties = [...entity.properties].filter(([_, prop]) => {
// Keep track of any key property without a visibility
return isKey(context.program, prop) && !getRawVisibilityStore(context.program, prop);
});
// For each key property without a visibility, clone it and add the specified
// default visibility value
for (const [name, keyProp] of keyProperties) {
entity.properties.set(name, context.program.checker.cloneType(keyProp, {
decorators: [
...keyProp.decorators,
{
decorator: $visibility,
args: [
{
value: visibility,
jsValue: visibility,
},
],
},
],
}));
}
};
const [getOperationVisibilityConfigRaw, setOperationVisibilityConfigRaw] = useStateMap(createStateSymbol("operationVisibilityConfig"));
function getOperationVisibilityConfig(program, operation) {
let config = getOperationVisibilityConfigRaw(program, operation);
if (!config) {
config = {};
setOperationVisibilityConfigRaw(program, operation, config);
}
return config;
}
export const $parameterVisibility = (context, operation, ...modifiers) => {
validateDecoratorUniqueOnNode(context, operation, $parameterVisibility);
if (modifiers.length === 0) {
reportDiagnostic(context.program, {
code: "operation-visibility-constraint-empty",
messageId: "parameter",
target: context.decoratorTarget,
});
return;
}
getOperationVisibilityConfig(context.program, operation).parameters = modifiers.map((m) => m.value);
};
/**
* An empty visibility provider. This provider returns an empty filter that considers all properties visible. This filter
* is used when no context-specific visibility provider is available.
*
* When working with an HTTP specification, use the `HttpVisibilityProvider` from the `@typespec/http` library instead.
*/
export const EmptyVisibilityProvider = {
parameters: () => ({}),
returnType: () => ({}),
};
/**
* Get the visibility filter that should apply to the parameters of the given operation, or `undefined` if no parameter
* visibility is set.
*
* If you are not working in a protocol that has specific implicit visibility semantics, you can use the
* {@link EmptyVisibilityProvider} as a default provider. If you working in a protocol or context where parameters have
* implicit visibility transformations (like HTTP), you should use the visibility provider from that library (for HTTP,
* use the `HttpVisibilityProvider` from the `@typespec/http` library).
*
* @param program - the Program in which the operation is defined
* @param operation - the Operation to get the parameter visibility filter for
* @param defaultProvider - a provider for visibility filters that apply when no visibility constraints are explicitly
* set. Defaults to an empty provider that returns an empty filter if not provided.
* @returns a visibility filter for the parameters of the operation, or `undefined` if no parameter visibility is set
*/
export function getParameterVisibilityFilter(program, operation, defaultProvider) {
const operationVisibilityConfig = getOperationVisibilityConfig(program, operation);
if (!operationVisibilityConfig.parameters)
return defaultProvider.parameters(program, operation);
compilerAssert(operationVisibilityConfig.parameters.length !== 0, "Empty parameter visibility constraint.");
return {
// WARNING: the HTTP library depends on `any` being the only key in the filter object returned by this method.
// if you change this logic, you will need to update the HTTP library to account for differences in the
// returned object. HTTP does not currently have a way to express `all` or `none` constraints in the same
// way that the core visibility system does.
any: new Set(operationVisibilityConfig.parameters),
};
}
export const $returnTypeVisibility = (context, operation, ...modifiers) => {
validateDecoratorUniqueOnNode(context, operation, $parameterVisibility);
if (modifiers.length === 0) {
reportDiagnostic(context.program, {
code: "operation-visibility-constraint-empty",
messageId: "returnType",
target: context.decoratorTarget,
});
return;
}
getOperationVisibilityConfig(context.program, operation).returnType = modifiers.map((m) => m.value);
};
/**
* Get the visibility filter that should apply to the return type of the given operation, or `undefined` if no return
* type visibility is set.
*
* @param program - the Program in which the operation is defined
* @param operation - the Operation to get the return type visibility filter for
* @param defaultProvider - a provider for visibility filters that apply when no visibility constraints are explicitly
* set. Defaults to an empty provider that returns an empty filter if not provided.
* @returns a visibility filter for the return type of the operation, or `undefined` if no return type visibility is set
*/
export function getReturnTypeVisibilityFilter(program, operation, defaultProvider) {
const visibilityConfig = getOperationVisibilityConfig(program, operation);
if (!visibilityConfig.returnType)
return defaultProvider.returnType(program, operation);
compilerAssert(visibilityConfig.returnType.length !== 0, "Empty return type visibility constraint.");
return {
// WARNING: the HTTP library depends on `any` being the only key in the filter object returned by this method.
// if you change this logic, you will need to update the HTTP library to account for differences in the
// returned object. HTTP does not currently have a way to express `all` or `none` constraints in the same
// way that the core visibility system does.
any: new Set(visibilityConfig.returnType),
};
}
// #endregion
// #region Core Visibility Decorators
// -- @visibility decorator ---------------------
export const $visibility = (context, target, ...modifiers) => {
addVisibilityModifiers(context.program, target, modifiers.map((m) => m.value), context);
};
// -- @removeVisibility decorator ---------------------
export const $removeVisibility = (context, target, ...visibilities) => {
removeVisibilityModifiers(context.program, target, visibilities.map((v) => v.value));
};
// -- @invisible decorator ---------------------
export const $invisible = (context, target, visibilityClass) => {
clearVisibilityModifiersForClass(context.program, target, visibilityClass);
};
// -- @defaultVisibility decorator ------------------
export const $defaultVisibility = (context, target, ...visibilities) => {
validateDecoratorUniqueOnNode(context, target, $defaultVisibility);
const modifierSet = new Set();
for (const visibility of visibilities) {
if (visibility.value.enum !== target) {
reportDiagnostic(context.program, {
code: "default-visibility-not-member",
target: context.decoratorTarget,
});
}
else {
modifierSet.add(visibility.value);
}
}
setDefaultModifierSetForVisibilityClass(context.program, target, modifierSet);
};
// #endregion
// #region Legacy Visibility Transforms
// -- @withVisibility decorator ---------------------
export const $withVisibility = (context, target, ...modifiers) => {
const modifierMembers = modifiers.map((m) => m.value);
const filter = {
all: new Set(modifierMembers),
};
const visibilityClasses = new Set(modifierMembers.map((m) => m.enum));
filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, filter));
for (const p of target.properties.values()) {
for (const c of visibilityClasses) {
resetVisibilityModifiersForClass(context.program, p, c);
}
}
};
// -- @withUpdateableProperties decorator ----------------------
/**
* Filters a model for properties that are updateable.
*
* @param context - the program context
* @param target - Model to filter for updateable properties
*/
export const $withUpdateableProperties = (context, target) => {
if (!validateDecoratorTarget(context, target, "@withUpdateableProperties", "Model")) {
return;
}
const filter = {
all: new Set([getLifecycleVisibilityEnum(context.program).members.get("Update")]),
};
filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, filter));
};
// #endregion
// #region Mutator Driven Transforms
// -- @withVisibilityFilter decorator ----------------------
const VISIBILITY_FILTER_MUTATOR_CACHE = Symbol.for("TypeSpec.Core.visibilityFilterMutatorCache");
/** @internal */
export function applyVisibilityFilter(context, input, _filter, nameTemplate) {
const filter = VisibilityFilter.fromDecoratorArgument(_filter);
const mutatorCache = (context.program[VISIBILITY_FILTER_MUTATOR_CACHE] ??= {});
const byNameTemplate = (mutatorCache.byNameTemplate ??= {});
const mutatorCacheByNameTemplate = nameTemplate === undefined
? (mutatorCache.unnamed ??= {})
: (byNameTemplate[nameTemplate] ??= {});
const mutatorCacheByVisibilityFilter = (mutatorCacheByNameTemplate.byVisibilityFilter ??= new Map());
const vfKey = VisibilityFilter.toCacheKey(context.program, filter);
let mutator = mutatorCacheByVisibilityFilter.get(vfKey);
if (!mutator) {
mutator = createVisibilityFilterMutator(filter, {
decoratorFn: $withVisibilityFilter,
decoratorName: "@withVisibilityFilter",
nameTemplate,
});
mutatorCacheByVisibilityFilter.set(vfKey, mutator);
}
setAlwaysMutate(context.program, input);
const { type } = cachedMutateSubgraph(context.program, mutator, input);
setAlwaysMutate(context.program, input, false);
compilerAssert(type.kind === "Model", "Expected visibility filter mutator to return a Model type.");
return type;
}
export const $withVisibilityFilter = (context, target, _filter, nameTemplate) => {
const transformed = applyVisibilityFilter(context, target, _filter, nameTemplate);
target.properties = transformed.properties;
};
// -- @withLifecycleUpdate decorator ----------------------
/** @internal */
export function applyLifecycleUpdate(context, input, nameTemplate) {
const mutatorCache = (context.program[VISIBILITY_FILTER_MUTATOR_CACHE] ??= {});
const byNameTemplate = (mutatorCache.byNameTemplate ??= {});
const mutatorCacheByNameTemplate = nameTemplate === undefined
? (mutatorCache.unnamed ??= {})
: (byNameTemplate[nameTemplate] ??= {});
let mutator = mutatorCacheByNameTemplate.lifecycleUpdate;
if (!mutator) {
const lifecycle = getLifecycleVisibilityEnum(context.program);
const lifecycleUpdate = {
all: new Set([lifecycle.members.get("Update")]),
};
const lifecycleCreateOrUpdate = {
any: new Set([lifecycle.members.get("Create"), lifecycle.members.get("Update")]),
};
const createOrUpdateMutator = createVisibilityFilterMutator(lifecycleCreateOrUpdate, {
nameTemplate,
});
mutator = createVisibilityFilterMutator(lifecycleUpdate, {
recur: createOrUpdateMutator,
decoratorFn: $withLifecycleUpdate,
decoratorName: "@withLifecycleUpdate",
nameTemplate,
});
mutatorCacheByNameTemplate.lifecycleUpdate = mutator;
}
setAlwaysMutate(context.program, input);
const { type } = cachedMutateSubgraph(context.program, mutator, input);
setAlwaysMutate(context.program, input, false);
compilerAssert(type.kind === "Model", "Expected lifecycle update mutator to return a Model type.");
return type;
}
export const $withLifecycleUpdate = (context, target, nameTemplate) => {
const transformed = applyLifecycleUpdate(context, target, nameTemplate);
target.properties = transformed.properties;
};
const VISIBILITY_FILTER_MUTATOR_RESULT = Symbol.for("TypeSpec.Core.visibilityFilterMutatorResult");
function cachedMutateSubgraph(program, mutator, source) {
const mutatorCache = (mutator[VISIBILITY_FILTER_MUTATOR_RESULT] ??= new WeakMap());
const cached = mutatorCache.get(source);
if (cached) {
return cached;
}
const mutated = mutateSubgraph(program, [mutator], source);
mutatorCache.set(source, mutated);
return mutated;
}
/**
* Create a mutator that applies a visibility filter to a type.
*
* @param filter - The visibility filter to apply
* @param options - optional settings for the mutator
* @returns
*/
function createVisibilityFilterMutator(filter, options = {}) {
const visibilityClasses = VisibilityFilter.getVisibilityClasses(filter);
const isTypeSpecDecorator = (application, decoratorName) => application.definition?.name === decoratorName &&
application.definition.namespace.name === "TypeSpec";
const matchesDecorator = (application, decoratorName, decoratorFn) => isTypeSpecDecorator(application, decoratorName) || application.decorator === decoratorFn;
const mpMutator = {
name: "VisibilityFilterProperty",
ModelProperty: {
filter: () => MutatorFlow.DoNotRecur,
replace: (prop, clone, program) => {
let modified = false;
// We need to create a copy of the decorators array to avoid modifying the original.
// Decorators are _NOT_ cloned by the type kit, so we have to be careful not to modify the decorator arguments
// of the original type.
const decorators = [];
for (const decorator of prop.decorators) {
if (matchesDecorator(decorator, "@visibility", $visibility) ||
matchesDecorator(decorator, "@removeVisibility", $removeVisibility)) {
const nextArgs = decorator.args.filter((arg) => {
if (arg.value.entityKind !== "Value")
return false;
const isString = arg.value.valueKind === "StringValue";
const isOperativeVisibility = arg.value.valueKind === "EnumValue" && visibilityClasses.has(arg.value.value.enum);
return !(isString || isOperativeVisibility);
});
modified ||= nextArgs.length !== decorator.args.length;
if (nextArgs.length > 0) {
decorators.push({
...decorator,
args: nextArgs,
});
}
}
else if (!matchesDecorator(decorator, "@invisible", $invisible)) {
decorators.push(decorator);
}
}
clone.decorators = decorators;
modified ||= decorators.length !== prop.decorators.length;
for (const visibilityClass of visibilityClasses) {
resetVisibilityModifiersForClass(program, clone, visibilityClass);
}
if (isVisibilitySubject(prop.type)) {
clone.type = cachedMutateSubgraph(program, options.recur ?? self, prop.type).type;
modified ||= clone.type !== prop.type;
}
return modified ? clone : prop;
},
},
};
const self = {
name: "VisibilityFilter",
Union: {
filter: () => MutatorFlow.DoNotRecur,
replace: (union, clone, program) => {
let modified = false;
for (const [key, member] of union.variants) {
if (isVisibilitySubject(member.type)) {
const variant = {
...member,
type: cachedMutateSubgraph(program, self, member.type).type,
};
clone.variants.set(key, variant);
modified ||= variant.type !== member.type;
}
}
rename(clone, options.nameTemplate);
return modified ? clone : union;
},
},
Model: {
filter: () => MutatorFlow.DoNotRecur,
replace: (model, clone, program, realm) => {
let modified = false;
if (model.indexer && isVisibilitySubject(model.indexer.value)) {
clone.indexer = { ...model.indexer };
mutate(clone.indexer).value = cachedMutateSubgraph(program, options.recur ?? self, model.indexer.value).type;
if ($(realm).array.is(model) &&
clone.name === "Array" &&
clone.indexer.value !== model.indexer.value) {
return $(program).array.create(clone.indexer.value);
}
if ($(realm).record.is(model) &&
clone.name === "Record" &&
clone.indexer.value !== model.indexer.value) {
return $(program).record.create(clone.indexer.value);
}
modified ||= clone.indexer.value !== model.indexer.value;
}
for (const [key, prop] of model.properties) {
if (!isVisible(program, prop, filter)) {
// Property is not visible, remove it
clone.properties.delete(key);
realm.remove(clone);
modified = true;
}
else {
const mutated = cachedMutateSubgraph(program, mpMutator, prop);
clone.properties.set(key, mutated.type);
modified ||= mutated.type !== prop;
}
}
if (options.decoratorFn) {
clone.decorators = clone.decorators.filter((d) => !(d.decorator === options.decoratorFn ||
(options.decoratorName && isTypeSpecDecorator(d, options.decoratorName))));
modified ||= clone.decorators.length !== model.decorators.length;
}
rename(clone, options.nameTemplate);
return modified ? clone : model;
},
},
ModelProperty: {
filter: () => MutatorFlow.DoNotRecur,
replace: (prop, clone, program) => {
if (isVisibilitySubject(prop.type)) {
clone.type = cachedMutateSubgraph(program, self, prop.type).type;
}
return clone.type !== prop.type ? clone : prop;
},
},
UnionVariant: {
filter: () => MutatorFlow.DoNotRecur,
replace: (variant, clone, program) => {
if (isVisibilitySubject(variant.type)) {
clone.type = cachedMutateSubgraph(program, self, variant.type).type;
}
return clone.type !== variant.type ? clone : variant;
},
},
Tuple: {
filter: () => MutatorFlow.DoNotRecur,
replace: (tuple, clone, program) => {
let modified = false;
for (const [index, element] of tuple.values.entries()) {
if (isVisibilitySubject(element)) {
clone.values[index] = cachedMutateSubgraph(program, self, element).type;
modified ||= clone.values[index] !== element;
}
}
return modified ? clone : tuple;
},
},
};
return self;
}
/**
* Internal helper to rename a type in place.
*/
function rename(type, nameTemplate) {
if (!nameTemplate || !type.name)
return;
const renamed = replaceTemplatedStringFromProperties(nameTemplate, type);
type.name = renamed;
}
function isVisibilitySubject(t) {
return (t.kind === "Model" ||
t.kind === "Union" ||
t.kind === "ModelProperty" ||
t.kind === "UnionVariant" ||
t.kind === "Tuple");
}
// #endregion
//# sourceMappingURL=visibility.js.map