From c1667c2bca0c8ebb16ce3990255062e7532cfe9e Mon Sep 17 00:00:00 2001 From: Will Temple Date: Sun, 22 Sep 2024 16:03:21 -0400 Subject: [PATCH 01/28] WIP --- packages/compiler/generated-defs/TypeSpec.ts | 57 +- packages/compiler/lib/std/decorators.tsp | 94 --- packages/compiler/lib/std/main.tsp | 1 + packages/compiler/lib/std/visibility.tsp | 140 +++++ packages/compiler/src/core/index.ts | 1 + packages/compiler/src/core/messages.ts | 18 + packages/compiler/src/core/visibility/core.ts | 539 ++++++++++++++++++ .../compiler/src/core/visibility/index.ts | 5 + .../compiler/src/core/visibility/lifecycle.ts | 53 ++ packages/compiler/src/lib/decorators.ts | 138 +---- packages/compiler/src/lib/tsp-index.ts | 22 +- packages/compiler/src/lib/visibility.ts | 182 ++++++ .../test/decorators/decorators.test.ts | 48 +- .../test/decorators/visibility.test.ts | 54 ++ 14 files changed, 1043 insertions(+), 309 deletions(-) create mode 100644 packages/compiler/lib/std/visibility.tsp create mode 100644 packages/compiler/src/core/visibility/core.ts create mode 100644 packages/compiler/src/core/visibility/index.ts create mode 100644 packages/compiler/src/core/visibility/lifecycle.ts create mode 100644 packages/compiler/src/lib/visibility.ts create mode 100644 packages/compiler/test/decorators/visibility.test.ts diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 6c2e2d64d1..85f50d6b97 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -119,7 +119,7 @@ export type WithoutDefaultValuesDecorator = (context: DecoratorContext, target: export type WithDefaultKeyVisibilityDecorator = ( context: DecoratorContext, target: Model, - visibility: string + visibility: string | EnumValue ) => void; /** @@ -629,6 +629,24 @@ export type OpExampleDecorator = ( options?: ExampleOptions ) => void; +/** + * A debugging decorator used to inspect a type. + * + * @param text Custom text to log + */ +export type InspectTypeDecorator = (context: DecoratorContext, target: Type, text: string) => void; + +/** + * A debugging decorator used to inspect a type name. + * + * @param text Custom text to log + */ +export type InspectTypeNameDecorator = ( + context: DecoratorContext, + target: Type, + text: string +) => void; + /** * Indicates that a property is only considered to be present or applicable ("visible") with * the in the given named contexts ("visibilities"). When a property has no visibilities applied @@ -662,7 +680,13 @@ export type OpExampleDecorator = ( export type VisibilityDecorator = ( context: DecoratorContext, target: ModelProperty, - ...visibilities: string[] + ...visibilities: (string | EnumValue)[] +) => void; + +export type InvisibleDecorator = ( + context: DecoratorContext, + target: ModelProperty, + visibilityClass: Enum ) => void; /** @@ -707,25 +731,7 @@ export type VisibilityDecorator = ( export type WithVisibilityDecorator = ( context: DecoratorContext, target: Model, - ...visibilities: string[] -) => void; - -/** - * A debugging decorator used to inspect a type. - * - * @param text Custom text to log - */ -export type InspectTypeDecorator = (context: DecoratorContext, target: Type, text: string) => void; - -/** - * A debugging decorator used to inspect a type name. - * - * @param text Custom text to log - */ -export type InspectTypeNameDecorator = ( - context: DecoratorContext, - target: Type, - text: string + ...visibilities: (string | EnumValue)[] ) => void; /** @@ -736,7 +742,7 @@ export type InspectTypeNameDecorator = ( export type ParameterVisibilityDecorator = ( context: DecoratorContext, target: Operation, - ...visibilities: string[] + ...visibilities: (string | EnumValue)[] ) => void; /** @@ -747,7 +753,7 @@ export type ParameterVisibilityDecorator = ( export type ReturnTypeVisibilityDecorator = ( context: DecoratorContext, target: Operation, - ...visibilities: string[] + ...visibilities: (string | EnumValue)[] ) => void; export type TypeSpecDecorators = { @@ -787,10 +793,11 @@ export type TypeSpecDecorators = { discriminator: DiscriminatorDecorator; example: ExampleDecorator; opExample: OpExampleDecorator; - visibility: VisibilityDecorator; - withVisibility: WithVisibilityDecorator; inspectType: InspectTypeDecorator; inspectTypeName: InspectTypeNameDecorator; + visibility: VisibilityDecorator; + invisible: InvisibleDecorator; + withVisibility: WithVisibilityDecorator; parameterVisibility: ParameterVisibilityDecorator; returnTypeVisibility: ReturnTypeVisibilityDecorator; }; diff --git a/packages/compiler/lib/std/decorators.tsp b/packages/compiler/lib/std/decorators.tsp index 0b98125874..5dd065655e 100644 --- a/packages/compiler/lib/std/decorators.tsp +++ b/packages/compiler/lib/std/decorators.tsp @@ -579,88 +579,6 @@ extern dec opExample( example: valueof OperationExample, options?: valueof ExampleOptions ); -/** - * Indicates that a property is only considered to be present or applicable ("visible") with - * the in the given named contexts ("visibilities"). When a property has no visibilities applied - * to it, it is implicitly visible always. - * - * As far as the TypeSpec core library is concerned, visibilities are open-ended and can be arbitrary - * strings, but the following visibilities are well-known to standard libraries and should be used - * with standard emitters that interpret them as follows: - * - * - "read": output of any operation. - * - "create": input to operations that create an entity.. - * - "query": input to operations that read data. - * - "update": input to operations that update data. - * - "delete": input to operations that delete data. - * - * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) - * - * @param visibilities List of visibilities which apply to this property. - * - * @example - * - * ```typespec - * model Dog { - * // the service will generate an ID, so you don't need to send it. - * @visibility("read") id: int32; - * // the service will store this secret name, but won't ever return it - * @visibility("create", "update") secretName: string; - * // the regular name is always present - * name: string; - * } - * ``` - */ -extern dec visibility(target: ModelProperty, ...visibilities: valueof string[]); - -/** - * Removes properties that are not considered to be present or applicable - * ("visible") in the given named contexts ("visibilities"). Can be used - * together with spread to effectively spread only visible properties into - * a new model. - * - * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) - * - * When using an emitter that applies visibility automatically, it is generally - * not necessary to use this decorator. - * - * @param visibilities List of visibilities which apply to this property. - * - * @example - * ```typespec - * model Dog { - * @visibility("read") id: int32; - * @visibility("create", "update") secretName: string; - * name: string; - * } - * - * // The spread operator will copy all the properties of Dog into DogRead, - * // and @withVisibility will then remove those that are not visible with - * // create or update visibility. - * // - * // In this case, the id property is removed, and the name and secretName - * // properties are kept. - * @withVisibility("create", "update") - * model DogCreateOrUpdate { - * ...Dog; - * } - * - * // In this case the id and name properties are kept and the secretName property - * // is removed. - * @withVisibility("read") - * model DogRead { - * ...Dog; - * } - * ``` - */ -extern dec withVisibility(target: Model, ...visibilities: valueof string[]); - -/** - * Set the visibility of key properties in a model if not already set. - * - * @param visibility The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. - */ -extern dec withDefaultKeyVisibility(target: Model, visibility: valueof string); /** * Returns the model with non-updateable properties removed. @@ -704,15 +622,3 @@ extern dec inspectType(target: unknown, text: valueof string); * @param text Custom text to log */ extern dec inspectTypeName(target: unknown, text: valueof string); - -/** - * Sets which visibilities apply to parameters for the given operation. - * @param visibilities List of visibility strings which apply to this operation. - */ -extern dec parameterVisibility(target: Operation, ...visibilities: valueof string[]); - -/** - * Sets which visibilities apply to the return type for the given operation. - * @param visibilities List of visibility strings which apply to this operation. - */ -extern dec returnTypeVisibility(target: Operation, ...visibilities: valueof string[]); diff --git a/packages/compiler/lib/std/main.tsp b/packages/compiler/lib/std/main.tsp index e0abf3ef19..4bc071fa50 100644 --- a/packages/compiler/lib/std/main.tsp +++ b/packages/compiler/lib/std/main.tsp @@ -3,3 +3,4 @@ import "./types.tsp"; import "./decorators.tsp"; import "./reflection.tsp"; import "./projected-names.tsp"; +import "./visibility.tsp"; diff --git a/packages/compiler/lib/std/visibility.tsp b/packages/compiler/lib/std/visibility.tsp new file mode 100644 index 0000000000..99fead6b2e --- /dev/null +++ b/packages/compiler/lib/std/visibility.tsp @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +import "../../dist/src/lib/tsp-index.js"; + +using TypeSpec.Reflection; + +namespace TypeSpec; + +/** + * Indicates that a property is only considered to be present or applicable ("visible") with + * the in the given named contexts ("visibilities"). When a property has no visibilities applied + * to it, it is implicitly visible always. + * + * As far as the TypeSpec core library is concerned, visibilities are open-ended and can be arbitrary + * strings, but the following visibilities are well-known to standard libraries and should be used + * with standard emitters that interpret them as follows: + * + * - "read": output of any operation. + * - "create": input to operations that create an entity.. + * - "query": input to operations that read data. + * - "update": input to operations that update data. + * - "delete": input to operations that delete data. + * + * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) + * + * @param visibilities List of visibilities which apply to this property. + * + * @example + * + * ```typespec + * model Dog { + * // the service will generate an ID, so you don't need to send it. + * @visibility("read") id: int32; + * // the service will store this secret name, but won't ever return it + * @visibility("create", "update") secretName: string; + * // the regular name is always present + * name: string; + * } + * ``` + */ +extern dec visibility(target: ModelProperty, ...visibilities: valueof (string | EnumMember)[]); + +extern dec invisible(target: ModelProperty, visibilityClass: Enum); + +/** + * Removes properties that are not considered to be present or applicable + * ("visible") in the given named contexts ("visibilities"). Can be used + * together with spread to effectively spread only visible properties into + * a new model. + * + * See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) + * + * When using an emitter that applies visibility automatically, it is generally + * not necessary to use this decorator. + * + * @param visibilities List of visibilities which apply to this property. + * + * @example + * ```typespec + * model Dog { + * @visibility("read") id: int32; + * @visibility("create", "update") secretName: string; + * name: string; + * } + * + * // The spread operator will copy all the properties of Dog into DogRead, + * // and @withVisibility will then remove those that are not visible with + * // create or update visibility. + * // + * // In this case, the id property is removed, and the name and secretName + * // properties are kept. + * @withVisibility("create", "update") + * model DogCreateOrUpdate { + * ...Dog; + * } + * + * // In this case the id and name properties are kept and the secretName property + * // is removed. + * @withVisibility("read") + * model DogRead { + * ...Dog; + * } + * ``` + */ +extern dec withVisibility(target: Model, ...visibilities: valueof (string | EnumMember)[]); + +/** + * Set the visibility of key properties in a model if not already set. + * + * @param visibility The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. + */ +extern dec withDefaultKeyVisibility(target: Model, visibility: valueof string | EnumMember); + +/** + * Sets which visibilities apply to parameters for the given operation. + * @param visibilities List of visibility strings which apply to this operation. + */ +extern dec parameterVisibility(target: Operation, ...visibilities: valueof (string | EnumMember)[]); + +/** + * Sets which visibilities apply to the return type for the given operation. + * @param visibilities List of visibility strings which apply to this operation. + */ +extern dec returnTypeVisibility( + target: Operation, + ...visibilities: valueof (string | EnumMember)[] +); + +/** + * Returns the model with non-updateable properties removed. + */ +extern dec withUpdateableProperties(target: Model); + +/** + * A visibility class for resource lifecycle phases. + * + * These visibilities control whether a property is visible during the create, read, and update phases of a resource's + * lifecycle. + * + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) id: int32; + * @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; + * name: string; + * } + * ``` + * + * In this example, the `id` property is only visible during the read phase, and the `secretName` property is only visible + * during the create and update phases. This means that the server will return the `id` property when returning a `Dog`, + * but the client will not be able to set or update it. In contrast, the `secretName` property can be set when creating + * or updating a `Dog`, but the server will never return it. The `name` property has no visibility modifiers and is + * therefore visible in all phases. + */ +enum Lifecycle { + Create, + Read, + Update, +} diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index 1166028cef..3534c248cc 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -55,3 +55,4 @@ export * from "./semantic-walker.js"; export { createSourceFile, getSourceFileKindFromExt } from "./source-file.js"; export * from "./type-utils.js"; export * from "./types.js"; +export * from "./visibility/core.js"; diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index 8f8b7c5c34..ff898ca716 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -982,6 +982,24 @@ const diagnostics = { }, }, + // #region Visibility + "visibility-sealed": { + severity: "error", + messages: { + default: paramMessage`Visibility of property '${"propName"}' is sealed and cannot be changed.`, + }, + }, + "visibility-mixed-legacy": { + severity: "error", + messages: { + "same-invocation": + "Cannot apply both string (legacy) visibility modifiers and enum-based visibility modifiers at the same time.", + "mixed-invocation": + "Cannot apply both string (legacy) visibility modifiers and enum-based visibility modifiers to a property.", + }, + }, + // #endregion + // #region CLI "no-compatible-vs-installed": { severity: "error", diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts new file mode 100644 index 0000000000..53496866bb --- /dev/null +++ b/packages/compiler/src/core/visibility/core.ts @@ -0,0 +1,539 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +// TypeSpec Visibility System +// -------------------------- + +// This module defines the core visibility system of the TypeSpec language. The visibility system is used to decide when +// properties of a _conceptual resource_ are present. The system is based on the concept of _visibility classes_, +// represented by TypeSpec enums. Each visibility class has a set of _visibility modifiers_ that can be applied to a +// model property, each modifier represented by a member of the visibility class enum. +// +// Each visibility class has a _default modifier set_ that is used when no modifiers are specified for a property, and +// each property has an _active modifier set_ that is used when analyzing the visibility of the property. +// +// Visibility can be _sealed_ for a program, property, or visibility class within a property. Once visibility is sealed, +// it cannot be unsealed, and any attempts to modify a sealed visibility will fail. + +import { compilerAssert } from "../diagnostics.js"; +import { reportDiagnostic } from "../messages.js"; +import { Program } from "../program.js"; +import { DecoratorContext, Enum, EnumMember, ModelProperty } from "../types.js"; +import { + getLifecycleVisibilityEnum, + normalizeLegacyLifecycleVisibilityString, +} from "./lifecycle.js"; + +/** + * A set of active visibility modifiers per visibility class. + */ +type VisibilityModifiers = Map>; + +/** + * The global visibility store. + * + * This store is used to track the visibility modifiers + */ +const VISIBILITY_STORE = new WeakMap(); + +/** + * Returns the visibility modifiers for a given `property` within a `program`. + */ +function getOrInitializeVisibilityModifiers(property: ModelProperty): VisibilityModifiers { + let visibilityModifiers = VISIBILITY_STORE.get(property); + + if (!visibilityModifiers) { + visibilityModifiers = new Map(); + VISIBILITY_STORE.set(property, visibilityModifiers); + } + + return visibilityModifiers; +} + +/** + * Returns the active visibility modifier set for a given `property` and `visibilityClass`. + * + * If no visibility modifiers have been set for the given `property` and `visibilityClass`, the function will use the + * provided `defaultSet` to initialize the visibility modifiers. + * + * @param program + * @param property + * @param visibilityClass + * @param defaultSet - the default set to use if no set has been initialized + * @returns + */ +function getOrInitializeActiveModifierSetForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, + defaultSet: Set +): Set { + const visibilityModifiers = getOrInitializeVisibilityModifiers(property); + let visibilityModifierSet = visibilityModifiers.get(visibilityClass); + + if (!visibilityModifierSet) { + visibilityModifierSet = defaultSet; + visibilityModifiers.set(visibilityClass, visibilityModifierSet); + } + + return visibilityModifierSet; +} + +/** + * If a Program is in this set, visibility is sealed for all properties in that Program. + */ +const VISIBILITY_PROGRAM_SEALS = new WeakSet(); + +/** + * If a property is in this set, visibility is sealed for that property. + */ +const VISIBILITY_SEALS = new WeakSet(); + +/** + * If a property is a key in this map, visibility is sealed for that property within all the visibility classes in the + * corresponding set. + */ +const VISIBILITY_SEALS_FOR_CLASS = new WeakMap>(); + +function sealVisibilityModifiersForClass(property: ModelProperty, visibilityClass: Enum) { + let sealedClasses = VISIBILITY_SEALS_FOR_CLASS.get(property); + + if (!sealedClasses) { + sealedClasses = new Set(); + VISIBILITY_SEALS_FOR_CLASS.set(property, sealedClasses); + } + + sealedClasses.add(visibilityClass); +} + +/** + * Stores the default modifier set for a given visibility class. + */ +const DEFAULT_MODIFIER_SET_CACHE = new WeakMap>(); + +function getDefaultModifierSetForClass(visibilityClass: Enum): Set { + const cached = DEFAULT_MODIFIER_SET_CACHE.get(visibilityClass); + + if (cached) return cached; + + const defaultModifierSet = new Set(visibilityClass.members.values()); + + DEFAULT_MODIFIER_SET_CACHE.set(visibilityClass, defaultModifierSet); + + return defaultModifierSet; +} + +/** + * Convert a sequence of visibility modifiers into a map of visibility classes to their respective modifiers in the + * sequence. + */ +function groupModifiersByVisibilityClass(modifiers: EnumMember[]) { + const enumMap = new Map>(); + + // Prepare new modifier sets for each visibility class + for (const modifier of modifiers) { + const visibilityClass = modifier.enum; + + let modifierSet = enumMap.get(visibilityClass); + + if (!modifierSet) { + modifierSet = new Set(); + enumMap.set(visibilityClass, modifierSet); + } + + modifierSet.add(modifier); + } + + return enumMap; +} + +// #region Legacy Visibility API + +const LEGACY_VISIBILITY_MODIFIERS = new WeakMap(); + +/** + * Sets the legacy visibility modifiers for a property. + * + * This function will also set the visibility modifiers for the property in the Lifecycle visibility class for any + * strings in the visibility array that are recognized as lifecycle visibility strings. + * + * Calling this function twice on the same property will result in a failed compiler assertion. + * + * @param program - the program in which the property occurs + * @param property - the property to set visibility modifiers for + * @param visibilities - the legacy visibility strings to set + */ +export function setLegacyVisibility( + context: DecoratorContext, + property: ModelProperty, + visibilities: string[] +) { + compilerAssert( + LEGACY_VISIBILITY_MODIFIERS.get(property) === undefined, + "Legacy visibility modifiers have already been set for this property." + ); + + LEGACY_VISIBILITY_MODIFIERS.set(property, visibilities); + + const lifecycleClass = getLifecycleVisibilityEnum(context.program); + + if (visibilities.length === 1 && visibilities[0] === "none") { + clearVisibilityModifiersForClass(context.program, property, lifecycleClass, context); + } else { + const lifecycleVisibilities = visibilities + .map((v) => normalizeLegacyLifecycleVisibilityString(context.program, v)) + .filter((v) => !!v); + + addVisibilityModifiers(context.program, property, lifecycleVisibilities); + } + + sealVisibilityModifiers(property, lifecycleClass); +} + +/** + * Returns the legacy visibility modifiers for a property. + * + * @deprecated Use `getVisibilityForClass` or `getLifecycleVisibility` instead. + * @param program - the program in which the property occurs + * @param property - the property to get legacy visibility modifiers for + */ +export function getVisibility(program: Program, property: ModelProperty): string[] | undefined { + void program; + return LEGACY_VISIBILITY_MODIFIERS.get(property); +} + +// #endregion + +// #region Visibility Management API + +/** + * Initializes the default modifier set for a visibility class. + * + * This function may be called once per visibility class to set the modifier set that should be used when no modifiers + * are specified on a property for the given visibility class. + * + * If no default set is provided for a visibility class using this function, the default set will be the set of ALL + * members/modifiers in the visibility class enum. + * + * This function may only be called ONCE per visibility class. + * + * @param visibilityClass + * @param defaultSet + */ +export function initializeDefaultModifierSetForClass( + visibilityClass: Enum, + defaultSet: Set +) { + compilerAssert( + !DEFAULT_MODIFIER_SET_CACHE.has(visibilityClass), + "The default modifier set for a visibility class may only be initialized once." + ); + + DEFAULT_MODIFIER_SET_CACHE.set(visibilityClass, defaultSet); +} + +/** + * Check if a property has had its visibility modifiers sealed. + * + * If the property has been sealed globally, this function will return true. If the property has been sealed for the + * given visibility class, this function will return true. + * + * Otherwise, this function returns false. + * + * @param property - the property to check + * @param visibilityClass - the optional visibility class to check + * @returns true if the property is sealed for the given visibility class, false otherwise + */ +export function isSealed( + program: Program, + property: ModelProperty, + visibilityClass?: Enum +): boolean { + if (VISIBILITY_PROGRAM_SEALS.has(program)) return true; + + const classSealed = visibilityClass + ? VISIBILITY_SEALS_FOR_CLASS.get(property)?.has(visibilityClass) + : false; + + return classSealed || VISIBILITY_SEALS.has(property); +} + +/** + * Seals a property's visibility modifiers. + * + * If the `visibilityClass` is provided, the property's visibility modifiers will be sealed for that visibility class + * only. Otherwise, the property's visibility modifiers will be sealed for all visibility classes (globally). + * + * @param property - the property to seal + * @param visibilityClass - the optional visibility class to seal the property for + */ +export function sealVisibilityModifiers(property: ModelProperty, visibilityClass?: Enum) { + if (visibilityClass) { + sealVisibilityModifiersForClass(property, visibilityClass); + } else { + VISIBILITY_SEALS.add(property); + } +} + +/** + * Seals a program's visibility modifiers. + * + * This affects all properties in the program and prevents any further modifications to visibility modifiers within the + * program. + * + * Once the modifiers for a program are sealed, they cannot be unsealed. + * + * @param program - the program to seal + */ +export function sealVisibilityModifiersForProgram(program: Program) { + VISIBILITY_PROGRAM_SEALS.add(program); +} + +/** + * Add visibility modifiers to a property. + * + * This function will add all the `modifiers` to the active set of visibility modifiers for the given `property`. + * + * If no set of active modifiers exists for the given `property`, an empty set will be created for the property. + * + * If the visibility modifiers for `property` in the given modifier's visibility class have been sealed, this function + * will issue a diagnostic and ignore that modifier, but it will still add the rest of the modifiers whose classes have + * not been sealed. + * + * @param program - the program in which the ModelProperty occurs + * @param property - the property to add visibility modifiers to + * @param modifiers - the visibility modifiers to add + * @param context - the optional decorator context to use for displaying diagnostics + */ +export function addVisibilityModifiers( + program: Program, + property: ModelProperty, + modifiers: EnumMember[], + context?: DecoratorContext +) { + const modifiersByClass = groupModifiersByVisibilityClass(modifiers); + + for (const [visibilityClass, newModifiers] of modifiersByClass.entries()) { + const target = context?.decoratorTarget ?? property; + if (isSealed(program, property, visibilityClass)) { + reportDiagnostic(program, { + code: "visibility-sealed", + format: { + propName: property.name, + }, + target, + }); + continue; + } + + const modifierSet = getOrInitializeActiveModifierSetForClass( + program, + property, + visibilityClass, + /* defaultSet: */ new Set() + ); + + for (const modifier of newModifiers) { + modifierSet.add(modifier); + } + } +} + +/** + * Remove visibility modifiers from a property. + * @param program + * @param property + * @param modifiers + * @param context + */ +export function removeVisibilityModifiers( + program: Program, + property: ModelProperty, + modifiers: EnumMember[], + context?: DecoratorContext +) { + const modifiersByClass = groupModifiersByVisibilityClass(modifiers); + + for (const [visibilityClass, newModifiers] of modifiersByClass.entries()) { + const target = context?.decoratorTarget ?? property; + if (isSealed(program, property, visibilityClass)) { + reportDiagnostic(program, { + code: "visibility-sealed", + format: { + propName: property.name, + }, + target, + }); + continue; + } + + const modifierSet = getOrInitializeActiveModifierSetForClass( + program, + property, + visibilityClass, + /* defaultSet: */ getDefaultModifierSetForClass(visibilityClass) + ); + + for (const modifier of newModifiers) { + modifierSet.delete(modifier); + } + } +} + +export function clearVisibilityModifiersForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, + context?: DecoratorContext +) { + const target = context?.decoratorTarget ?? property; + if (isSealed(program, property, visibilityClass)) { + reportDiagnostic(program, { + code: "visibility-sealed", + format: { + propName: property.name, + }, + target, + }); + return; + } + + const modifierSet = getOrInitializeActiveModifierSetForClass( + program, + property, + visibilityClass, + /* defaultSet: */ new Set() + ); + + modifierSet.clear(); +} + +// #endregion + +// #region Visibility Analysis API + +export function getVisibilityForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum +): Set { + return getOrInitializeActiveModifierSetForClass( + program, + property, + visibilityClass, + /* defaultSet: */ getDefaultModifierSetForClass(visibilityClass) + ); +} + +/** + * Determines if a property has a specified visibility modifier. + * + * If no visibility modifiers have been set for the visibility class of the modifier, the visibility class's default + * modifier set is used. + * + * @param program - the program in which the property occurs + * @param property - the property to check + * @param modifier - the visibility modifier to check for + * @returns true if the property has the visibility modifier, false otherwise + */ +export function hasVisibility( + program: Program, + property: ModelProperty, + modifier: EnumMember +): boolean { + const activeSet = getOrInitializeActiveModifierSetForClass( + program, + property, + modifier.enum, + /* defaultSet: */ getDefaultModifierSetForClass(modifier.enum) + ); + + return activeSet?.has(modifier) ?? false; +} + +/** + * A visibility filter that can be used to determine if a property is visible. + * + * The filter is defined by three sets of visibility modifiers. The filter is satisfied if the property has: + * + * - ALL of the visibilities in the `all` set. + * + * AND + * + * - ANY of the visibilities in the `any` set. + * + * AND + * + * - NONE of the visibilities in the `none` set. + */ +export interface VisibilityFilter { + all?: Set; + any?: Set; + none?: Set; +} + +/** + * Determines if a property is visible according to the given visibility filter. + * + * @see VisibilityFilter + * + * @param program - the program in which the property occurs + * @param property - the property to check + * @param filter - the visibility filter to use + * @returns true if the property is visible according to the filter, false otherwise + */ +export function isVisible( + program: Program, + property: ModelProperty, + filter: VisibilityFilter +): boolean; +/** + * Determines if a property has any of the specified (legacy) visibility strings. + * + * @deprecated This call signature is deprecated. Use the `VisibilityFilter` version instead. + */ +export function isVisible( + program: Program, + property: ModelProperty, + visibilities: readonly string[] +): boolean; +export function isVisible( + program: Program, + property: ModelProperty, + _filterOrLegacyVisibilities: VisibilityFilter | readonly string[] +): boolean { + if (Array.isArray(_filterOrLegacyVisibilities)) { + return isVisibleLegacy(_filterOrLegacyVisibilities); + } + + const filter = { ...(_filterOrLegacyVisibilities as VisibilityFilter) }; + filter.all ??= new Set(); + filter.any ??= new Set(); + filter.none ??= new Set(); + + for (const modifier of filter.all) { + if (!hasVisibility(program, property, modifier)) return false; + } + + outer: while (filter.any.size > 0) { + for (const modifier of filter.any) { + if (hasVisibility(program, property, modifier)) break outer; + } + + return false; + } + + for (const modifier of filter.none) { + if (hasVisibility(program, property, modifier)) return false; + } + + return true; + + function isVisibleLegacy(visibilities: readonly string[]) { + // eslint-disable-next-line deprecation/deprecation + const propertyVisibilities = getVisibility(program, property); + return !propertyVisibilities || propertyVisibilities.some((v) => visibilities.includes(v)); + } +} + +// #endregion diff --git a/packages/compiler/src/core/visibility/index.ts b/packages/compiler/src/core/visibility/index.ts new file mode 100644 index 0000000000..d02d988532 --- /dev/null +++ b/packages/compiler/src/core/visibility/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +export * from "./core.js"; +export * from "./lifecycle.js"; diff --git a/packages/compiler/src/core/visibility/lifecycle.ts b/packages/compiler/src/core/visibility/lifecycle.ts new file mode 100644 index 0000000000..8be376f12e --- /dev/null +++ b/packages/compiler/src/core/visibility/lifecycle.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +import { compilerAssert } from "../diagnostics.js"; +import { Program } from "../program.js"; +import { Enum, EnumMember } from "../types.js"; + +/** + * A cache for the `TypeSpec.Visibility.Lifecycle` enum per Program instance. + */ +const LIFECYCLE_ENUM_CACHE = new WeakMap(); + +/** + * Returns the instance of `TypeSpec.Visibility.Lifecycle` for the given `program`. + * + * @param program - the program to get the lifecycle visibility enum for + * @returns a reference to the lifecycle visibility enum + */ +export function getLifecycleVisibilityEnum(program: Program): Enum { + const cached = LIFECYCLE_ENUM_CACHE.get(program); + + if (cached) return cached; + + const [type, diagnostics] = program.resolveTypeReference("TypeSpec.Lifecycle"); + + compilerAssert( + diagnostics.length === 0, + "Encountered diagnostics when resolving the `TypeSpec.Lifecycle` visibility class enum" + ); + + compilerAssert(type!.kind === "Enum", "Expected `TypeSpec.Visibility.Lifecycle` to be an enum"); + + LIFECYCLE_ENUM_CACHE.set(program, type); + + return type; +} + +export function normalizeLegacyLifecycleVisibilityString( + program: Program, + visibility: string +): EnumMember | undefined { + const lifecycle = getLifecycleVisibilityEnum(program); + switch (visibility) { + case "create": + return lifecycle.members.get("Create")!; + case "read": + return lifecycle.members.get("Read")!; + case "update": + return lifecycle.members.get("Update")!; + default: + return undefined; + } +} diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index 90961eca1c..1b073d2e1b 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -23,20 +23,14 @@ import type { MinValueExclusiveDecorator, OpExampleDecorator, OverloadDecorator, - ParameterVisibilityDecorator, PatternDecorator, ProjectedNameDecorator, - ReturnTypeVisibilityDecorator, ReturnsDocDecorator, SecretDecorator, SummaryDecorator, TagDecorator, - VisibilityDecorator, - WithDefaultKeyVisibilityDecorator, WithOptionalPropertiesDecorator, WithPickedPropertiesDecorator, - WithUpdateablePropertiesDecorator, - WithVisibilityDecorator, WithoutDefaultValuesDecorator, WithoutOmittedPropertiesDecorator, } from "../../generated-defs/TypeSpec.js"; @@ -44,7 +38,6 @@ import { getPropertyType, isIntrinsicType, validateDecoratorNotOnType, - validateDecoratorTarget, } from "../core/decorator-utils.js"; import { getDeprecationDetails, markDeprecated } from "../core/deprecation.js"; import { @@ -125,7 +118,7 @@ function replaceTemplatedStringFromProperties(formatString: string, sourceObject }); } -function createStateSymbol(name: string) { +export function createStateSymbol(name: string) { return Symbol.for(`TypeSpec.${name}`); } @@ -819,47 +812,10 @@ export function getEncode( return program.stateMap(encodeKey).get(target); } -// -- @visibility decorator --------------------- - -const visibilitySettingsKey = createStateSymbol("visibilitySettings"); - -export const $visibility: VisibilityDecorator = ( - context: DecoratorContext, - target: ModelProperty, - ...visibilities: string[] -) => { - validateDecoratorUniqueOnNode(context, target, $visibility); - - context.program.stateMap(visibilitySettingsKey).set(target, visibilities); -}; - -export function getVisibility(program: Program, target: Type): string[] | undefined { - return program.stateMap(visibilitySettingsKey).get(target); -} - -function clearVisibilities(program: Program, target: Type) { - program.stateMap(visibilitySettingsKey).delete(target); -} - -export const $withVisibility: WithVisibilityDecorator = ( - context: DecoratorContext, - target: Model, - ...visibilities: string[] -) => { - filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, visibilities)); - [...target.properties.values()].forEach((p) => clearVisibilities(context.program, p)); -}; - -export function isVisible( - program: Program, - property: ModelProperty, - visibilities: readonly string[] +export function filterModelPropertiesInPlace( + model: Model, + filter: (prop: ModelProperty) => boolean ) { - const propertyVisibilities = getVisibility(program, property); - return !propertyVisibilities || propertyVisibilities.some((v) => visibilities.includes(v)); -} - -function filterModelPropertiesInPlace(model: Model, filter: (prop: ModelProperty) => boolean) { for (const [key, prop] of model.properties) { if (!filter(prop)) { model.properties.delete(key); @@ -877,19 +833,6 @@ export const $withOptionalProperties: WithOptionalPropertiesDecorator = ( target.properties.forEach((p) => (p.optional = true)); }; -// -- @withUpdateableProperties decorator ---------------------- - -export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( - context: DecoratorContext, - target: Type -) => { - if (!validateDecoratorTarget(context, target, "@withUpdateableProperties", "Model")) { - return; - } - - filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, ["update"])); -}; - // -- @withoutOmittedProperties decorator ---------------------- export const $withoutOmittedProperties: WithoutOmittedPropertiesDecorator = ( @@ -1185,39 +1128,6 @@ export function getKeyName(program: Program, property: ModelProperty): string { return program.stateMap(keyKey).get(property); } -export const $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator = ( - context: DecoratorContext, - entity: Model, - visibility: string -) => { - const keyProperties: ModelProperty[] = []; - entity.properties.forEach((prop: ModelProperty) => { - // Keep track of any key property without a visibility - if (isKey(context.program, prop) && !getVisibility(context.program, prop)) { - keyProperties.push(prop); - } - }); - - // For each key property without a visibility, clone it and add the specified - // default visibility value - keyProperties.forEach((keyProp) => { - entity.properties.set( - keyProp.name, - context.program.checker.cloneType(keyProp, { - decorators: [ - ...keyProp.decorators, - { - decorator: $visibility, - args: [ - { value: context.program.checker.createLiteralType(visibility), jsValue: visibility }, - ], - }, - ], - }) - ); - }); -}; - /** * Mark a type as deprecated * @param context DecoratorContext @@ -1431,46 +1341,6 @@ export const $discriminator: DiscriminatorDecorator = ( setDiscriminator(context.program, entity, discriminator); }; -const parameterVisibilityKey = createStateSymbol("parameterVisibility"); - -export const $parameterVisibility: ParameterVisibilityDecorator = ( - context: DecoratorContext, - entity: Operation, - ...visibilities: string[] -) => { - validateDecoratorUniqueOnNode(context, entity, $parameterVisibility); - context.program.stateMap(parameterVisibilityKey).set(entity, visibilities); -}; - -/** - * Returns the visibilities of the parameters of the given operation, if provided with `@parameterVisibility`. - * - * @see {@link $parameterVisibility} - */ -export function getParameterVisibility(program: Program, entity: Operation): string[] | undefined { - return program.stateMap(parameterVisibilityKey).get(entity); -} - -const returnTypeVisibilityKey = createStateSymbol("returnTypeVisibility"); - -export const $returnTypeVisibility: ReturnTypeVisibilityDecorator = ( - context: DecoratorContext, - entity: Operation, - ...visibilities: string[] -) => { - validateDecoratorUniqueOnNode(context, entity, $returnTypeVisibility); - context.program.stateMap(returnTypeVisibilityKey).set(entity, visibilities); -}; - -/** - * Returns the visibilities of the return type of the given operation, if provided with `@returnTypeVisibility`. - * - * @see {@link $returnTypeVisibility} - */ -export function getReturnTypeVisibility(program: Program, entity: Operation): string[] | undefined { - return program.stateMap(returnTypeVisibilityKey).get(entity); -} - export interface Example extends ExampleOptions { readonly value: Value; } diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index 2b82bdee65..fbd5ea8309 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -25,24 +25,27 @@ import { $minValueExclusive, $opExample, $overload, - $parameterVisibility, $pattern, $projectedName, - $returnTypeVisibility, $returnsDoc, $secret, $service, $summary, $tag, - $visibility, - $withDefaultKeyVisibility, $withOptionalProperties, $withPickedProperties, - $withUpdateableProperties, - $withVisibility, $withoutDefaultValues, $withoutOmittedProperties, } from "./decorators.js"; +import { + $invisible, + $parameterVisibility, + $returnTypeVisibility, + $visibility, + $withDefaultKeyVisibility, + $withUpdateableProperties, + $withVisibility, +} from "./visibility.js"; /** @internal */ export const $decorators = { @@ -54,7 +57,6 @@ export const $decorators = { withoutOmittedProperties: $withoutOmittedProperties, withPickedProperties: $withPickedProperties, withoutDefaultValues: $withoutDefaultValues, - withDefaultKeyVisibility: $withDefaultKeyVisibility, summary: $summary, returnsDoc: $returnsDoc, errorsDoc: $errorsDoc, @@ -84,10 +86,12 @@ export const $decorators = { discriminator: $discriminator, example: $example, opExample: $opExample, - visibility: $visibility, - withVisibility: $withVisibility, inspectType: $inspectType, inspectTypeName: $inspectTypeName, + visibility: $visibility, + invisible: $invisible, + withVisibility: $withVisibility, + withDefaultKeyVisibility: $withDefaultKeyVisibility, parameterVisibility: $parameterVisibility, returnTypeVisibility: $returnTypeVisibility, } satisfies TypeSpecDecorators, diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts new file mode 100644 index 0000000000..42b9b62b88 --- /dev/null +++ b/packages/compiler/src/lib/visibility.ts @@ -0,0 +1,182 @@ +import type { + InvisibleDecorator, + ParameterVisibilityDecorator, + ReturnTypeVisibilityDecorator, + VisibilityDecorator, + WithDefaultKeyVisibilityDecorator, + WithUpdateablePropertiesDecorator, + WithVisibilityDecorator, +} from "../../generated-defs/TypeSpec.js"; +import { validateDecoratorTarget, validateDecoratorUniqueOnNode } from "../core/decorator-utils.js"; +import { + addVisibilityModifiers, + clearVisibilityModifiersForClass, + getVisibility, + isVisible, + Program, + setLegacyVisibility, +} from "../core/index.js"; +import { reportDiagnostic } from "../core/messages.js"; +import { + DecoratorContext, + Enum, + EnumMember, + EnumValue, + Model, + ModelProperty, + Operation, + Type, +} from "../core/types.js"; +import { createStateSymbol, filterModelPropertiesInPlace, isKey } from "./decorators.js"; + +// #region Legacy Visibility Utilities + +export const $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator = ( + context: DecoratorContext, + entity: Model, + visibility: string | EnumValue +) => { + const keyProperties: ModelProperty[] = []; + entity.properties.forEach((prop: ModelProperty) => { + // Keep track of any key property without a visibility + if (isKey(context.program, prop) && !getVisibility(context.program, prop)) { + keyProperties.push(prop); + } + }); + + // For each key property without a visibility, clone it and add the specified + // default visibility value + keyProperties.forEach((keyProp) => { + entity.properties.set( + keyProp.name, + context.program.checker.cloneType(keyProp, { + decorators: [ + ...keyProp.decorators, + { + decorator: $visibility, + args: [ + { + value: + typeof visibility === "string" + ? context.program.checker.createLiteralType(visibility) + : visibility, + jsValue: visibility, + }, + ], + }, + ], + }) + ); + }); +}; + +export const parameterVisibilityKey = createStateSymbol("parameterVisibility"); + +export const $parameterVisibility: ParameterVisibilityDecorator = ( + context: DecoratorContext, + entity: Operation, + ...visibilities: (string | EnumValue)[] +) => { + validateDecoratorUniqueOnNode(context, entity, $parameterVisibility); + context.program.stateMap(parameterVisibilityKey).set(entity, visibilities); +}; + +/** + * Returns the visibilities of the parameters of the given operation, if provided with `@parameterVisibility`. + * + * @see {@link $parameterVisibility} + */ +export function getParameterVisibility(program: Program, entity: Operation): string[] | undefined { + return program.stateMap(parameterVisibilityKey).get(entity); +} + +export const returnTypeVisibilityKey = createStateSymbol("returnTypeVisibility"); + +export const $returnTypeVisibility: ReturnTypeVisibilityDecorator = ( + context: DecoratorContext, + entity: Operation, + ...visibilities: (string | EnumValue)[] +) => { + validateDecoratorUniqueOnNode(context, entity, $returnTypeVisibility); + context.program.stateMap(returnTypeVisibilityKey).set(entity, visibilities); +}; + +/** + * Returns the visibilities of the return type of the given operation, if provided with `@returnTypeVisibility`. + * + * @see {@link $returnTypeVisibility} + */ +export function getReturnTypeVisibility(program: Program, entity: Operation): string[] | undefined { + return program.stateMap(returnTypeVisibilityKey).get(entity); +} + +// -- @visibility decorator --------------------- + +export const $visibility: VisibilityDecorator = ( + context: DecoratorContext, + target: ModelProperty, + ...visibilities: (string | EnumValue)[] +) => { + const legacyVisibilities = [] as string[]; + const modifiers = [] as EnumMember[]; + + for (const visibility of visibilities) { + if (typeof visibility === "string") { + legacyVisibilities.push(visibility); + } else { + modifiers.push(visibility.value); + } + } + + if (legacyVisibilities.length > 0) { + const isUnique = validateDecoratorUniqueOnNode(context, target, $visibility); + + if (modifiers.length > 0) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + messageId: "same-invocation", + target: context.decoratorTarget, + }); + + return; + } + + // Only attempt to set the legacy visibility modifiers if the visibility invocation is unique. Otherwise, a compiler + // assertion will fail inside the legacy visibility management API. + if (isUnique) setLegacyVisibility(context, target, legacyVisibilities); + } else { + addVisibilityModifiers(context.program, target, modifiers, context); + } +}; + +export const $invisible: InvisibleDecorator = ( + context: DecoratorContext, + target: ModelProperty, + visibilityClass: Enum +) => { + clearVisibilityModifiersForClass(context.program, target, visibilityClass); +}; + +export const $withVisibility: WithVisibilityDecorator = ( + context: DecoratorContext, + target: Model, + ...visibilities: (string | EnumValue)[] +) => { + filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, visibilities)); + [...target.properties.values()].forEach((p) => clearVisibilities(context.program, p)); +}; + +// -- @withUpdateableProperties decorator ---------------------- + +export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( + context: DecoratorContext, + target: Type +) => { + if (!validateDecoratorTarget(context, target, "@withUpdateableProperties", "Model")) { + return; + } + + filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, ["update"])); +}; + +// #endregion diff --git a/packages/compiler/test/decorators/decorators.test.ts b/packages/compiler/test/decorators/decorators.test.ts index ebe846addd..573f66c613 100644 --- a/packages/compiler/test/decorators/decorators.test.ts +++ b/packages/compiler/test/decorators/decorators.test.ts @@ -1,14 +1,6 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { - Model, - ModelProperty, - Namespace, - Operation, - Scalar, - getVisibility, - isSecret, -} from "../../src/index.js"; +import { Model, ModelProperty, Namespace, Operation, Scalar, isSecret } from "../../src/index.js"; import { getDoc, getEncode, @@ -855,44 +847,6 @@ describe("compiler: built-in decorators", () => { }); }); - describe("@withDefaultKeyVisibility", () => { - it("sets the default visibility on a key property when not already present", async () => { - const { TestModel } = (await runner.compile( - ` - model OriginalModel { - @key - name: string; - } - - @test - model TestModel is DefaultKeyVisibility { - } ` - )) as { TestModel: Model }; - - deepStrictEqual(getVisibility(runner.program, TestModel.properties.get("name")!), ["read"]); - }); - - it("allows visibility applied to a key property to override the default", async () => { - const { TestModel } = (await runner.compile( - ` - model OriginalModel { - @key - @visibility("read", "update") - name: string; - } - - @test - model TestModel is DefaultKeyVisibility { - } ` - )) as { TestModel: Model }; - - deepStrictEqual(getVisibility(runner.program, TestModel.properties.get("name")!), [ - "read", - "update", - ]); - }); - }); - describe("@overload", () => { it("emits an error when @overload is given something other than an operation", async () => { const diagnostics = await runner.diagnose(` diff --git a/packages/compiler/test/decorators/visibility.test.ts b/packages/compiler/test/decorators/visibility.test.ts new file mode 100644 index 0000000000..3974e404c3 --- /dev/null +++ b/packages/compiler/test/decorators/visibility.test.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. + +import { deepStrictEqual } from "assert"; +import { beforeEach, describe, it } from "vitest"; +import { Model } from "../../src/core/types.js"; +import { getVisibility } from "../../src/lib/visibility.js"; +import { BasicTestRunner, createTestRunner } from "../../src/testing/index.js"; + +describe("visibility (legacy)", function () { + let runner: BasicTestRunner; + + beforeEach(async () => { + runner = await createTestRunner(); + }); + + describe("@withDefaultKeyVisibility", () => { + it("sets the default visibility on a key property when not already present", async () => { + const { TestModel } = (await runner.compile( + ` + model OriginalModel { + @key + name: string; + } + + @test + model TestModel is DefaultKeyVisibility { + } ` + )) as { TestModel: Model }; + + deepStrictEqual(getVisibility(runner.program, TestModel.properties.get("name")!), ["read"]); + }); + + it("allows visibility applied to a key property to override the default", async () => { + const { TestModel } = (await runner.compile( + ` + model OriginalModel { + @key + @visibility("read", "update") + name: string; + } + + @test + model TestModel is DefaultKeyVisibility { + } ` + )) as { TestModel: Model }; + + deepStrictEqual(getVisibility(runner.program, TestModel.properties.get("name")!), [ + "read", + "update", + ]); + }); + }); +}); From 7ac53e5c2a481c3170b9518dac07402a6829ebb7 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 10 Oct 2024 16:09:25 -0400 Subject: [PATCH 02/28] WIP --- packages/compiler/generated-defs/TypeSpec.ts | 82 +++++++++++++++---- packages/compiler/lib/std/visibility.tsp | 73 +++++++++++++++++ packages/compiler/src/core/messages.ts | 6 ++ packages/compiler/src/core/visibility/core.ts | 35 +++++++- .../compiler/src/core/visibility/lifecycle.ts | 30 ++++++- packages/compiler/src/lib/tsp-index.ts | 4 + packages/compiler/src/lib/visibility.ts | 68 ++++++++++++++- 7 files changed, 276 insertions(+), 22 deletions(-) diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 91c22840ce..bb29165165 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -24,6 +24,12 @@ export interface OperationExample { readonly returnType?: unknown; } +export interface VisibilityFilter { + readonly any?: readonly EnumValue[]; + readonly all?: readonly EnumValue[]; + readonly none?: readonly EnumValue[]; +} + /** * Specify how to encode the target type. * @@ -629,6 +635,24 @@ export type OpExampleDecorator = ( options?: ExampleOptions, ) => void; +/** + * A debugging decorator used to inspect a type. + * + * @param text Custom text to log + */ +export type InspectTypeDecorator = (context: DecoratorContext, target: Type, text: string) => void; + +/** + * A debugging decorator used to inspect a type name. + * + * @param text Custom text to log + */ +export type InspectTypeNameDecorator = ( + context: DecoratorContext, + target: Type, + text: string, +) => void; + /** * Indicates that a property is only considered to be present or applicable ("visible") with * the in the given named contexts ("visibilities"). When a property has no visibilities applied @@ -665,6 +689,21 @@ export type VisibilityDecorator = ( ...visibilities: (string | EnumValue)[] ) => void; +/** + * Indicates that a property is not visible in the given visibility class. + * + * This decorator removes all active visibility modifiers from the property within + * the given visibility class. + * + * @param visibilityClass The visibility class to make the property invisible within. + * @example + * ```typespec + * model Example { + * @invisible(Lifecycle) + * hidden_property: string; + * } + * ``` + */ export type InvisibleDecorator = ( context: DecoratorContext, target: ModelProperty, @@ -716,24 +755,6 @@ export type WithVisibilityDecorator = ( ...visibilities: (string | EnumValue)[] ) => void; -/** - * A debugging decorator used to inspect a type. - * - * @param text Custom text to log - */ -export type InspectTypeDecorator = (context: DecoratorContext, target: Type, text: string) => void; - -/** - * A debugging decorator used to inspect a type name. - * - * @param text Custom text to log - */ -export type InspectTypeNameDecorator = ( - context: DecoratorContext, - target: Type, - text: string, -) => void; - /** * Sets which visibilities apply to parameters for the given operation. * @@ -756,6 +777,29 @@ export type ReturnTypeVisibilityDecorator = ( ...visibilities: (string | EnumValue)[] ) => void; +/** + * Declares the default visibility modifiers for a visibility class. + * + * The default modifiers are used when a property does not have any visibility decorators + * applied to it. + */ +export type DefaultVisibilityDecorator = ( + context: DecoratorContext, + target: Enum, + ...visibilities: EnumValue[] +) => void; + +/** + * Applies the given visibility filter to the properties of the target model. + * + * The transformation is recursive + */ +export type WithVisibilityFilterDecorator = ( + context: DecoratorContext, + target: Model, + filter: VisibilityFilter, +) => void; + export type TypeSpecDecorators = { encode: EncodeDecorator; doc: DocDecorator; @@ -800,4 +844,6 @@ export type TypeSpecDecorators = { withVisibility: WithVisibilityDecorator; parameterVisibility: ParameterVisibilityDecorator; returnTypeVisibility: ReturnTypeVisibilityDecorator; + defaultVisibility: DefaultVisibilityDecorator; + withVisibilityFilter: WithVisibilityFilterDecorator; }; diff --git a/packages/compiler/lib/std/visibility.tsp b/packages/compiler/lib/std/visibility.tsp index 99fead6b2e..f3c5aafcd9 100644 --- a/packages/compiler/lib/std/visibility.tsp +++ b/packages/compiler/lib/std/visibility.tsp @@ -41,6 +41,22 @@ namespace TypeSpec; */ extern dec visibility(target: ModelProperty, ...visibilities: valueof (string | EnumMember)[]); +/** + * Indicates that a property is not visible in the given visibility class. + * + * This decorator removes all active visibility modifiers from the property within + * the given visibility class. + * + * @param visibilityClass The visibility class to make the property invisible within. + * + * @example + * ```typespec + * model Example { + * @invisible(Lifecycle) + * hidden_property: string; + * } + * ``` + */ extern dec invisible(target: ModelProperty, visibilityClass: Enum); /** @@ -112,6 +128,14 @@ extern dec returnTypeVisibility( */ extern dec withUpdateableProperties(target: Model); +/** + * Declares the default visibility modifiers for a visibility class. + * + * The default modifiers are used when a property does not have any visibility decorators + * applied to it. + */ +extern dec defaultVisibility(target: Enum, ...visibilities: valueof EnumMember[]); + /** * A visibility class for resource lifecycle phases. * @@ -138,3 +162,52 @@ enum Lifecycle { Read, Update, } + +/** + * A visibility filter, used to specify which properties should be included when + * using the `withVisibilityFilter` decorator. + * + * The filter matches any property with ALL of the following: + * - If the `any` key is present, the property must have at least one of the specified visibilities. + * - If the `all` key is present, the property must have all of the specified visibilities. + * - If the `none` key is present, the property must have none of the specified visibilities. + */ +model VisibilityFilter { + any?: EnumMember[]; + all?: EnumMember[]; + none?: EnumMember[]; +} + +/** + * Applies the given visibility filter to the properties of the target model. + * + * The transformation is recursive + */ +extern dec withVisibilityFilter(target: Model, filter: valueof VisibilityFilter); + +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ all: #[Lifecycle.Create] }) +model Create { + ...T; +} + +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ all: #[Lifecycle.Read] }) +model Read { + ...T; +} + +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ all: #[Lifecycle.Update] }) +model Update { + ...T; +} + +@friendlyName(NameTemplate, T) +@withVisibilityFilter(#{ any: #[Lifecycle.Create, Lifecycle.Update] }) +model CreateOrUpdate< + T extends Reflection.Model, + NameTemplate extends valueof string = "CreateOrUpdate{name}" +> { + ...T; +} diff --git a/packages/compiler/src/core/messages.ts b/packages/compiler/src/core/messages.ts index c0bd64cd33..f6bad480a8 100644 --- a/packages/compiler/src/core/messages.ts +++ b/packages/compiler/src/core/messages.ts @@ -1001,6 +1001,12 @@ const diagnostics = { "Cannot apply both string (legacy) visibility modifiers and enum-based visibility modifiers to a property.", }, }, + "default-visibility-not-member": { + severity: "error", + messages: { + default: "The default visibility modifiers of a class must be members of the class enum.", + }, + }, // #endregion // #region CLI diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index 209283d458..b7399a8cea 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -24,6 +24,8 @@ import { normalizeLegacyLifecycleVisibilityString, } from "./lifecycle.js"; +import { VisibilityFilter as GeneratedVisibilityFilter } from "../../../generated-defs/TypeSpec.js"; + /** * A set of active visibility modifiers per visibility class. */ @@ -123,6 +125,24 @@ function getDefaultModifierSetForClass(visibilityClass: Enum): Set { return defaultModifierSet; } +/** + * Set the default visibility modifier set for a visibility class. + * + * This function may only be called ONCE per visibility class and must be called + * before the default modifier set is used by any operation. + */ +export function setDefaultModifierSetForVisibilityClass( + visibilityClass: Enum, + defaultSet: Set, +) { + compilerAssert( + !DEFAULT_MODIFIER_SET_CACHE.has(visibilityClass), + "The default modifier set for a visibility class may only be set once.", + ); + + DEFAULT_MODIFIER_SET_CACHE.set(visibilityClass, defaultSet); +} + /** * Convert a sequence of visibility modifiers into a map of visibility classes to their respective modifiers in the * sequence. @@ -489,6 +509,16 @@ export interface VisibilityFilter { none?: Set; } +export const VisibilityFilter = { + fromDecoratorArgument(filter: GeneratedVisibilityFilter): VisibilityFilter { + return { + all: filter.all && new Set(filter.all.map((v) => v.value)), + any: filter.any && new Set(filter.any.map((v) => v.value)), + none: filter.none && new Set(filter.none.map((v) => v.value)), + }; + }, +}; + /** * Determines if a property is visible according to the given visibility filter. * @@ -528,10 +558,12 @@ export function isVisible( filter.any ??= new Set(); filter.none ??= new Set(); + // Validate that property has ALL of the required visibilities of filter.all for (const modifier of filter.all) { if (!hasVisibility(program, property, modifier)) return false; } + // Validate that property has ANY of the required visibilities of filter.any outer: while (filter.any.size > 0) { for (const modifier of filter.any) { if (hasVisibility(program, property, modifier)) break outer; @@ -540,6 +572,7 @@ export function isVisible( return false; } + // Validate that property has NONE of the excluded visibilities of filter.none for (const modifier of filter.none) { if (hasVisibility(program, property, modifier)) return false; } @@ -547,7 +580,7 @@ export function isVisible( return true; function isVisibleLegacy(visibilities: readonly string[]) { - // eslint-disable-next-line deprecation/deprecation + // eslint-disable-next-line @typescript-eslint/no-deprecated const propertyVisibilities = getVisibility(program, property); return !propertyVisibilities || propertyVisibilities.some((v) => visibilities.includes(v)); } diff --git a/packages/compiler/src/core/visibility/lifecycle.ts b/packages/compiler/src/core/visibility/lifecycle.ts index 8be376f12e..11c137d829 100644 --- a/packages/compiler/src/core/visibility/lifecycle.ts +++ b/packages/compiler/src/core/visibility/lifecycle.ts @@ -25,7 +25,7 @@ export function getLifecycleVisibilityEnum(program: Program): Enum { compilerAssert( diagnostics.length === 0, - "Encountered diagnostics when resolving the `TypeSpec.Lifecycle` visibility class enum" + "Encountered diagnostics when resolving the `TypeSpec.Lifecycle` visibility class enum", ); compilerAssert(type!.kind === "Enum", "Expected `TypeSpec.Visibility.Lifecycle` to be an enum"); @@ -35,9 +35,35 @@ export function getLifecycleVisibilityEnum(program: Program): Enum { return type; } +// interface LifecycleVisibilityEnumMembers { +// Create: EnumMember; +// Read: EnumMember; +// Update: EnumMember; +// } + +// const LIFECYCLE_ENUM_MEMBERS_CACHE = new WeakMap(); + +// function getLifecycleVisibilityEnumMembers(program: Program): LifecycleVisibilityEnumMembers { +// const cached = LIFECYCLE_ENUM_MEMBERS_CACHE.get(program); + +// if (cached) return cached; + +// const lifecycle = getLifecycleVisibilityEnum(program); + +// const members: LifecycleVisibilityEnumMembers = { +// Create: lifecycle.members.get("Create")!, +// Read: lifecycle.members.get("Read")!, +// Update: lifecycle.members.get("Update")!, +// }; + +// LIFECYCLE_ENUM_MEMBERS_CACHE.set(program, members); + +// return members; +// } + export function normalizeLegacyLifecycleVisibilityString( program: Program, - visibility: string + visibility: string, ): EnumMember | undefined { const lifecycle = getLifecycleVisibilityEnum(program); switch (visibility) { diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index cdb4bcb00c..89d1808244 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -38,6 +38,7 @@ import { $withoutOmittedProperties, } from "./decorators.js"; import { + $defaultVisibility, $invisible, $parameterVisibility, $returnTypeVisibility, @@ -45,6 +46,7 @@ import { $withDefaultKeyVisibility, $withUpdateableProperties, $withVisibility, + $withVisibilityFilter, } from "./visibility.js"; /** @internal */ @@ -89,7 +91,9 @@ export const $decorators = { inspectTypeName: $inspectTypeName, visibility: $visibility, invisible: $invisible, + defaultVisibility: $defaultVisibility, withVisibility: $withVisibility, + withVisibilityFilter: $withVisibilityFilter, withDefaultKeyVisibility: $withDefaultKeyVisibility, parameterVisibility: $parameterVisibility, returnTypeVisibility: $returnTypeVisibility, diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 5118a5e384..59f7d5886e 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -1,4 +1,6 @@ import type { + DefaultVisibilityDecorator, + VisibilityFilter as GeneratedVisibilityFilter, InvisibleDecorator, ParameterVisibilityDecorator, ReturnTypeVisibilityDecorator, @@ -6,6 +8,7 @@ import type { WithDefaultKeyVisibilityDecorator, WithUpdateablePropertiesDecorator, WithVisibilityDecorator, + WithVisibilityFilterDecorator, } from "../../generated-defs/TypeSpec.js"; import { validateDecoratorTarget, validateDecoratorUniqueOnNode } from "../core/decorator-utils.js"; import { @@ -14,6 +17,7 @@ import { getVisibility, isVisible, Program, + setDefaultModifierSetForVisibilityClass, setLegacyVisibility, splitLegacyVisibility, VisibilityFilter, @@ -22,6 +26,7 @@ import { reportDiagnostic } from "../core/messages.js"; import { DecoratorContext, Enum, + EnumMember, EnumValue, Model, ModelProperty, @@ -44,6 +49,7 @@ export const $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator = ( const keyProperties: ModelProperty[] = []; entity.properties.forEach((prop: ModelProperty) => { // Keep track of any key property without a visibility + // eslint-disable-next-line @typescript-eslint/no-deprecated if (isKey(context.program, prop) && !getVisibility(context.program, prop)) { keyProperties.push(prop); } @@ -144,6 +150,8 @@ export const $visibility: VisibilityDecorator = ( } }; +// -- @invisible decorator --------------------- + export const $invisible: InvisibleDecorator = ( context: DecoratorContext, target: ModelProperty, @@ -152,6 +160,33 @@ export const $invisible: InvisibleDecorator = ( clearVisibilityModifiersForClass(context.program, target, visibilityClass); }; +// -- @defaultVisibility decorator ------------------ + +export const $defaultVisibility: DefaultVisibilityDecorator = ( + context: DecoratorContext, + target: Enum, + ...visibilities: EnumValue[] +) => { + 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(target, modifierSet); +}; + +// -- @withVisibility decorator --------------------- + export const $withVisibility: WithVisibilityDecorator = ( context: DecoratorContext, target: Model, @@ -202,6 +237,20 @@ export const $withVisibility: WithVisibilityDecorator = ( // -- @withUpdateableProperties decorator ---------------------- +const UPDATE_VISIBILITY_MODIFIERS = new WeakMap(); + +function getUpdateVisibilityModifier(program: Program): EnumMember { + let modifier = UPDATE_VISIBILITY_MODIFIERS.get(program); + + if (!modifier) { + const lifecycle = getLifecycleVisibilityEnum(program); + modifier = lifecycle.members.get("Update")!; + UPDATE_VISIBILITY_MODIFIERS.set(program, modifier); + } + + return modifier; +} + export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( context: DecoratorContext, target: Type, @@ -210,7 +259,24 @@ export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( return; } - filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, ["update"])); + const filter: VisibilityFilter = { + all: new Set([getUpdateVisibilityModifier(context.program)]), + }; + + filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, filter)); +}; + +// -- @withVisibilityFilter decorator ---------------------- + +export const $withVisibilityFilter: WithVisibilityFilterDecorator = ( + context: DecoratorContext, + target: Model, + _filter: GeneratedVisibilityFilter, +) => { + const filter = VisibilityFilter.fromDecoratorArgument(_filter); + + // TODO + throw new Error("Not implemented."); }; // #endregion From fc849f7ca038882b332e3df946039acabcff2bad Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 22 Oct 2024 13:51:20 -0400 Subject: [PATCH 03/28] Nearly code complete --- docs/standard-library/built-in-data-types.md | 198 ++++++++++++++++++ docs/standard-library/built-in-decorators.md | 159 ++++++++++++-- packages/compiler/generated-defs/TypeSpec.ts | 50 ++++- packages/compiler/lib/std/decorators.tsp | 5 - packages/compiler/lib/std/visibility.tsp | 171 ++++++++++++++- packages/compiler/src/core/visibility/core.ts | 196 +++++++++-------- .../compiler/src/core/visibility/lifecycle.ts | 62 +++--- packages/compiler/src/lib/decorators.ts | 12 +- packages/compiler/src/lib/key.ts | 11 + packages/compiler/src/lib/tsp-index.ts | 2 + packages/compiler/src/lib/utils.ts | 20 +- packages/compiler/src/lib/visibility.ts | 189 ++++++++++++----- packages/http/src/metadata.ts | 7 +- 13 files changed, 882 insertions(+), 200 deletions(-) create mode 100644 packages/compiler/src/lib/key.ts diff --git a/docs/standard-library/built-in-data-types.md b/docs/standard-library/built-in-data-types.md index ae1639782f..b473e9b77a 100644 --- a/docs/standard-library/built-in-data-types.md +++ b/docs/standard-library/built-in-data-types.md @@ -19,6 +19,78 @@ model Array | Element | The type of the array elements | +#### Properties +None + +### `Create` {#Create} + +Makes a copy of the model `T` with only the properties that are visible during the +"Create" resource lifecycle phase. + +This transformation is recursive, and will include only properties that have the +`Lifecycle.Create` visibility modifier. + +If a `NameTemplate` is provided, the new model will be named according to the template. +The template uses the same syntax as the `@friendlyName` decorator. +```typespec +model Create +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| T | The model to transform. | +| NameTemplate | The name template to use for the new model.

* | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +model CreateDog is Create; +``` + +#### Properties +None + +### `CreateOrUpdate` {#CreateOrUpdate} + +Makes a copy of the model `T` with only the properties that are visible during the +"Create" or "Update" resource lifecycle phases. + +This transformation is recursive, and will include only properties that have the +`Lifecycle.Create` or `Lifecycle.Update` visibility modifier. + +If a `NameTemplate` is provided, the new model will be named according to the template. +The template uses the same syntax as the `@friendlyName` decorator. +```typespec +model CreateOrUpdate +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| T | The model to transform. | +| NameTemplate | The name template to use for the new model.

* | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +model CreateOrUpdateDog is CreateOrUpdate; +``` + #### Properties None @@ -144,6 +216,42 @@ model PickProperties | Keys | The property keys to include. | +#### Properties +None + +### `Read` {#Read} + +Makes a copy of the model `T` with only the properties that are visible during the +"Read" resource lifecycle phase. + +This transformation is recursive, and will include only properties that have the +`Lifecycle.Read` visibility modifier. + +If a `NameTemplate` is provided, the new model will be named according to the template. +The template uses the same syntax as the `@friendlyName` decorator. +```typespec +model Read +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| T | The model to transform. | +| NameTemplate | The name template to use for the new model.

* | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +model ReadDog is Read; +``` + #### Properties None @@ -178,6 +286,43 @@ model ServiceOptions | title? | [`string`](#string) | Title of the service. | | version? | [`string`](#string) | Version of the service. | +### `Update` {#Update} + +Makes a copy of the model `T` with only the properties that are visible during the +"Update" resource lifecycle phase. + +This transformation will include only the properties that have the `Lifecycle.Update` +visibility modifier, and the types of all properties will be replaced with the +equivalent `CreateOrUpdate` transformation. + +If a `NameTemplate` is provided, the new model will be named according to the template. +The template uses the same syntax as the `@friendlyName` decorator. +```typespec +model Update +``` + +#### Template Parameters +| Name | Description | +|------|-------------| +| T | The model to transform. | +| NameTemplate | The name template to use for the new model.

* | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +model UpdateDog is Update; +``` + +#### Properties +None + ### `UpdateableProperties` {#UpdateableProperties} Represents a collection of updateable properties. @@ -194,6 +339,27 @@ model UpdateableProperties #### Properties None +### `VisibilityFilter` {#VisibilityFilter} + +A visibility filter, used to specify which properties should be included when +using the `withVisibilityFilter` decorator. + +The filter matches any property with ALL of the following: +- If the `any` key is present, the property must have at least one of the specified visibilities. +- If the `all` key is present, the property must have all of the specified visibilities. +- If the `none` key is present, the property must have none of the specified visibilities. +```typespec +model VisibilityFilter +``` + + +#### Properties +| Name | Type | Description | +|------|------|-------------| +| any? | `EnumMember[]` | | +| all? | `EnumMember[]` | | +| none? | `EnumMember[]` | | + ### `ArrayEncoding` {#ArrayEncoding} Encoding for serializing arrays @@ -247,6 +413,38 @@ enum DurationKnownEncoding | seconds | `"seconds"` | Encode to integer or float | +### `Lifecycle` {#Lifecycle} + +A visibility class for resource lifecycle phases. + +These visibilities control whether a property is visible during the create, read, and update phases of a resource's +lifecycle. +```typespec +enum Lifecycle +``` + +| Name | Value | Description | +|------|-------|-------------| +| Create | | | +| Read | | | +| Update | | | +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) id: int32; + @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; + name: string; +} +``` + +In this example, the `id` property is only visible during the read phase, and the `secretName` property is only visible +during the create and update phases. This means that the server will return the `id` property when returning a `Dog`, +but the client will not be able to set or update it. In contrast, the `secretName` property can be set when creating +or updating a `Dog`, but the server will never return it. The `name` property has no visibility modifiers and is +therefore visible in all phases. + + ### `boolean` {#boolean} Boolean with `true` and `false` values. diff --git a/docs/standard-library/built-in-decorators.md b/docs/standard-library/built-in-decorators.md index d30994aa58..a288875019 100644 --- a/docs/standard-library/built-in-decorators.md +++ b/docs/standard-library/built-in-decorators.md @@ -5,6 +5,27 @@ toc_max_heading_level: 3 --- # Built-in Decorators ## TypeSpec +### `@defaultVisibility` {#@defaultVisibility} + +Declares the default visibility modifiers for a visibility class. + +The default modifiers are used when a property does not have any visibility decorators +applied to it. +```typespec +@defaultVisibility(...visibilities: valueof EnumMember[]) +``` + +#### Target + +`Enum` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| visibilities | `valueof EnumMember[]` | | + + + ### `@deprecated` {#@deprecated} :::warning **Deprecated**: @deprecated decorator is deprecated. Use the `#deprecated` directive instead. @@ -344,6 +365,35 @@ A debugging decorator used to inspect a type name. +### `@invisible` {#@invisible} + +Indicates that a property is not visible in the given visibility class. + +This decorator removes all active visibility modifiers from the property within +the given visibility class. +```typespec +@invisible(visibilityClass: Enum) +``` + +#### Target + +`ModelProperty` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| visibilityClass | `Enum` | The visibility class to make the property invisible within. | + +#### Examples + +```typespec +model Example { + @invisible(Lifecycle) + hidden_property: string; +} +``` + + ### `@key` {#@key} Mark a model property as the key to identify instances of that type @@ -669,7 +719,7 @@ op uploadBytes(data: bytes, @header contentType: "application/octet-stream"): vo Sets which visibilities apply to parameters for the given operation. ```typespec -@parameterVisibility(...visibilities: valueof string[]) +@parameterVisibility(...visibilities: valueof string | EnumMember[]) ``` #### Target @@ -679,7 +729,7 @@ Sets which visibilities apply to parameters for the given operation. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibilities | `valueof string[]` | List of visibility strings which apply to this operation. | +| visibilities | `valueof string \| EnumMember[]` | List of visibility strings which apply to this operation. | @@ -776,7 +826,7 @@ op get(): Pet | NotFound; Sets which visibilities apply to the return type for the given operation. ```typespec -@returnTypeVisibility(...visibilities: valueof string[]) +@returnTypeVisibility(...visibilities: valueof string | EnumMember[]) ``` #### Target @@ -786,7 +836,7 @@ Sets which visibilities apply to the return type for the given operation. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibilities | `valueof string[]` | List of visibility strings which apply to this operation. | +| visibilities | `valueof string \| EnumMember[]` | List of visibility strings which apply to this operation. | @@ -910,7 +960,7 @@ with standard emitters that interpret them as follows: See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operations#automatic-visibility) ```typespec -@visibility(...visibilities: valueof string[]) +@visibility(...visibilities: valueof string | EnumMember[]) ``` #### Target @@ -920,16 +970,16 @@ See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operati #### Parameters | Name | Type | Description | |------|------|-------------| -| visibilities | `valueof string[]` | List of visibilities which apply to this property. | +| visibilities | `valueof string \| EnumMember[]` | List of visibilities which apply to this property. | #### Examples ```typespec model Dog { // the service will generate an ID, so you don't need to send it. - @visibility("read") id: int32; + @visibility(Lifecycle.Read) id: int32; // the service will store this secret name, but won't ever return it - @visibility("create", "update") secretName: string; + @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; // the regular name is always present name: string; } @@ -939,8 +989,18 @@ model Dog { ### `@withDefaultKeyVisibility` {#@withDefaultKeyVisibility} Set the visibility of key properties in a model if not already set. + +This will set the visibility modifiers of all key properties in the model if the visibility is not already _explicitly_ set, +but will not change the visibility of any properties that have visibility set _explicitly_, even if the visibility +is the same as the default visibility. + +Visibility may be explicitly set using any of the following decorators: + +- `@visibility` +- `@removeVisibility` +- `@invisible` ```typespec -@withDefaultKeyVisibility(visibility: valueof string) +@withDefaultKeyVisibility(visibility: valueof string | EnumMember) ``` #### Target @@ -950,8 +1010,47 @@ Set the visibility of key properties in a model if not already set. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibility | [valueof `string`](#string) | The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. | +| visibility | `valueof string \| EnumMember` | The desired default visibility value. If a key property already has visibility set, it will not be changed. | + + + +### `@withLifecycleUpdate` {#@withLifecycleUpdate} + +Transforms the `target` model to include only properties that are visible during the +"Update" lifecycle phase. + +Any nested models of optional properties will be transformed into the "CreateOrUpdate" +lifecycle phase instead of the "Update" lifecycle phase, so that nested models may be +fully updated. +```typespec +@withLifecycleUpdate +``` + +#### Target +The model to apply the transformation to. +`Model` + +#### Parameters +None + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + @visibility(Lifecycle.Create, Lifecycle.Update) + secretName: string; + + name: string; +} + +@withLifecycleUpdate +model DogUpdate { + ...Dog +} +``` ### `@withOptionalProperties` {#@withOptionalProperties} @@ -1050,7 +1149,7 @@ See also: [Automatic visibility](https://typespec.io/docs/libraries/http/operati When using an emitter that applies visibility automatically, it is generally not necessary to use this decorator. ```typespec -@withVisibility(...visibilities: valueof string[]) +@withVisibility(...visibilities: valueof string | EnumMember[]) ``` #### Target @@ -1060,7 +1159,7 @@ not necessary to use this decorator. #### Parameters | Name | Type | Description | |------|------|-------------| -| visibilities | `valueof string[]` | List of visibilities which apply to this property. | +| visibilities | `valueof string \| EnumMember[]` | List of visibilities which apply to this property. | #### Examples @@ -1090,3 +1189,39 @@ model DogRead { } ``` + +### `@withVisibilityFilter` {#@withVisibilityFilter} + +Applies the given visibility filter to the properties of the target model. + +This transformation is recursive, so it will also apply the filter to any nested +or referenced models that are the types of any properties in the `target`. +```typespec +@withVisibilityFilter(filter: valueof VisibilityFilter) +``` + +#### Target +The model to apply the visibility filter to. +`Model` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| filter | [valueof `VisibilityFilter`](./built-in-data-types.md#VisibilityFilter) | The visibility filter to apply to the properties of the target model. | + +#### Examples + +```typespec +model Dog { + @visibility(Lifecycle.Read) + id: int32; + + name: string; +} + +@withVisibilityFilter(#{ all: #[Lifecycle.Read] }) +model DogRead { + ...Dog +} +``` + diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index bb29165165..242beb01a2 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -792,7 +792,25 @@ export type DefaultVisibilityDecorator = ( /** * Applies the given visibility filter to the properties of the target model. * - * The transformation is recursive + * This transformation is recursive, so it will also apply the filter to any nested + * or referenced models that are the types of any properties in the `target`. + * + * @param target The model to apply the visibility filter to. + * @param filter The visibility filter to apply to the properties of the target model. + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * @withVisibilityFilter(#{ all: #[Lifecycle.Read] }) + * model DogRead { + * ...Dog + * } + * ``` */ export type WithVisibilityFilterDecorator = ( context: DecoratorContext, @@ -800,6 +818,35 @@ export type WithVisibilityFilterDecorator = ( filter: VisibilityFilter, ) => void; +/** + * Transforms the `target` model to include only properties that are visible during the + * "Update" lifecycle phase. + * + * Any nested models of optional properties will be transformed into the "CreateOrUpdate" + * lifecycle phase instead of the "Update" lifecycle phase, so that nested models may be + * fully updated. + * + * @param target The model to apply the transformation to. + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * @visibility(Lifecycle.Create, Lifecycle.Update) + * secretName: string; + * + * name: string; + * } + * + * @withLifecycleUpdate + * model DogUpdate { + * ...Dog + * } + * ``` + */ +export type WithLifecycleUpdateDecorator = (context: DecoratorContext, target: Model) => void; + export type TypeSpecDecorators = { encode: EncodeDecorator; doc: DocDecorator; @@ -846,4 +893,5 @@ export type TypeSpecDecorators = { returnTypeVisibility: ReturnTypeVisibilityDecorator; defaultVisibility: DefaultVisibilityDecorator; withVisibilityFilter: WithVisibilityFilterDecorator; + withLifecycleUpdate: WithLifecycleUpdateDecorator; }; diff --git a/packages/compiler/lib/std/decorators.tsp b/packages/compiler/lib/std/decorators.tsp index 5dd065655e..6b549231cb 100644 --- a/packages/compiler/lib/std/decorators.tsp +++ b/packages/compiler/lib/std/decorators.tsp @@ -580,11 +580,6 @@ extern dec opExample( options?: valueof ExampleOptions ); -/** - * Returns the model with non-updateable properties removed. - */ -extern dec withUpdateableProperties(target: Model); - /** * Returns the model with required properties removed. */ diff --git a/packages/compiler/lib/std/visibility.tsp b/packages/compiler/lib/std/visibility.tsp index f3c5aafcd9..250d445e42 100644 --- a/packages/compiler/lib/std/visibility.tsp +++ b/packages/compiler/lib/std/visibility.tsp @@ -31,9 +31,9 @@ namespace TypeSpec; * ```typespec * model Dog { * // the service will generate an ID, so you don't need to send it. - * @visibility("read") id: int32; + * @visibility(Lifecycle.Read) id: int32; * // the service will store this secret name, but won't ever return it - * @visibility("create", "update") secretName: string; + * @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; * // the regular name is always present * name: string; * } @@ -104,12 +104,23 @@ extern dec withVisibility(target: Model, ...visibilities: valueof (string | Enum /** * Set the visibility of key properties in a model if not already set. * - * @param visibility The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. + * This will set the visibility modifiers of all key properties in the model if the visibility is not already _explicitly_ set, + * but will not change the visibility of any properties that have visibility set _explicitly_, even if the visibility + * is the same as the default visibility. + * + * Visibility may be explicitly set using any of the following decorators: + * + * - `@visibility` + * - `@removeVisibility` + * - `@invisible` + * + * @param visibility The desired default visibility value. If a key property already has visibility set, it will not be changed. */ extern dec withDefaultKeyVisibility(target: Model, visibility: valueof string | EnumMember); /** * Sets which visibilities apply to parameters for the given operation. + * * @param visibilities List of visibility strings which apply to this operation. */ extern dec parameterVisibility(target: Operation, ...visibilities: valueof (string | EnumMember)[]); @@ -181,28 +192,178 @@ model VisibilityFilter { /** * Applies the given visibility filter to the properties of the target model. * - * The transformation is recursive + * This transformation is recursive, so it will also apply the filter to any nested + * or referenced models that are the types of any properties in the `target`. + * + * @param target The model to apply the visibility filter to. + * @param filter The visibility filter to apply to the properties of the target model. + * + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * @withVisibilityFilter(#{ all: #[Lifecycle.Read] }) + * model DogRead { + * ...Dog + * } + * ``` */ extern dec withVisibilityFilter(target: Model, filter: valueof VisibilityFilter); +/** + * Transforms the `target` model to include only properties that are visible during the + * "Update" lifecycle phase. + * + * Any nested models of optional properties will be transformed into the "CreateOrUpdate" + * lifecycle phase instead of the "Update" lifecycle phase, so that nested models may be + * fully updated. + * + * @param target The model to apply the transformation to. + * + * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * @visibility(Lifecycle.Create, Lifecycle.Update) + * secretName: string; + * + * name: string; + * } + * + * @withLifecycleUpdate + * model DogUpdate { + * ...Dog + * } + * ``` + */ +extern dec withLifecycleUpdate(target: Model); + +/** + * Makes a copy of the model `T` with only the properties that are visible during the + * "Create" resource lifecycle phase. + * + * This transformation is recursive, and will include only properties that have the + * `Lifecycle.Create` visibility modifier. + * + * If a `NameTemplate` is provided, the new model will be named according to the template. + * The template uses the same syntax as the `@friendlyName` decorator. + * + * @template T The model to transform. + * @template NameTemplate The name template to use for the new model. + * + * * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * model CreateDog is Create; + * ``` + */ @friendlyName(NameTemplate, T) @withVisibilityFilter(#{ all: #[Lifecycle.Create] }) model Create { ...T; } +/** + * Makes a copy of the model `T` with only the properties that are visible during the + * "Read" resource lifecycle phase. + * + * This transformation is recursive, and will include only properties that have the + * `Lifecycle.Read` visibility modifier. + * + * If a `NameTemplate` is provided, the new model will be named according to the template. + * The template uses the same syntax as the `@friendlyName` decorator. + * + * @template T The model to transform. + * @template NameTemplate The name template to use for the new model. + * + * * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * model ReadDog is Read; + * ``` + */ @friendlyName(NameTemplate, T) @withVisibilityFilter(#{ all: #[Lifecycle.Read] }) model Read { ...T; } +/** + * Makes a copy of the model `T` with only the properties that are visible during the + * "Update" resource lifecycle phase. + * + * This transformation will include only the properties that have the `Lifecycle.Update` + * visibility modifier, and the types of all properties will be replaced with the + * equivalent `CreateOrUpdate` transformation. + * + * If a `NameTemplate` is provided, the new model will be named according to the template. + * The template uses the same syntax as the `@friendlyName` decorator. + * + * @template T The model to transform. + * @template NameTemplate The name template to use for the new model. + * + * * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * model UpdateDog is Update; + * ``` + */ @friendlyName(NameTemplate, T) -@withVisibilityFilter(#{ all: #[Lifecycle.Update] }) +@withLifecycleUpdate model Update { ...T; } +/** + * Makes a copy of the model `T` with only the properties that are visible during the + * "Create" or "Update" resource lifecycle phases. + * + * This transformation is recursive, and will include only properties that have the + * `Lifecycle.Create` or `Lifecycle.Update` visibility modifier. + * + * If a `NameTemplate` is provided, the new model will be named according to the template. + * The template uses the same syntax as the `@friendlyName` decorator. + * + * @template T The model to transform. + * @template NameTemplate The name template to use for the new model. + * + * * @example + * ```typespec + * model Dog { + * @visibility(Lifecycle.Read) + * id: int32; + * + * name: string; + * } + * + * model CreateOrUpdateDog is CreateOrUpdate; + * ``` + */ @friendlyName(NameTemplate, T) @withVisibilityFilter(#{ any: #[Lifecycle.Create, Lifecycle.Update] }) model CreateOrUpdate< diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index b7399a8cea..0020246db5 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -4,27 +4,32 @@ // TypeSpec Visibility System // -------------------------- -// This module defines the core visibility system of the TypeSpec language. The visibility system is used to decide when -// properties of a _conceptual resource_ are present. The system is based on the concept of _visibility classes_, -// represented by TypeSpec enums. Each visibility class has a set of _visibility modifiers_ that can be applied to a -// model property, each modifier represented by a member of the visibility class enum. +// This module defines the core visibility system of the TypeSpec language. The +// visibility system is used to decide when properties of a _conceptual resource_ +// are present. The system is based on the concept of _visibility classes_, +// represented by TypeSpec enums. Each visibility class has a set of _visibility +// modifiers_ that can be applied to a model property, each modifier represented +// by a member of the visibility class enum. // -// Each visibility class has a _default modifier set_ that is used when no modifiers are specified for a property, and -// each property has an _active modifier set_ that is used when analyzing the visibility of the property. +// Each visibility class has a _default modifier set_ that is used when no +// modifiers are specified for a property, and each property has an _active +// modifier set_ that is used when analyzing the visibility of the property. // -// Visibility can be _sealed_ for a program, property, or visibility class within a property. Once visibility is sealed, -// it cannot be unsealed, and any attempts to modify a sealed visibility will fail. +// Visibility can be _sealed_ for a program, property, or visibility class +// within a property. Once visibility is sealed, it cannot be unsealed, and any +// attempts to modify a sealed visibility will fail. import { compilerAssert } from "../diagnostics.js"; import { reportDiagnostic } from "../messages.js"; import { Program } from "../program.js"; -import { DecoratorContext, Enum, EnumMember, EnumValue, ModelProperty } from "../types.js"; +import { DecoratorContext, Enum, EnumMember, ModelProperty } from "../types.js"; import { getLifecycleVisibilityEnum, normalizeLegacyLifecycleVisibilityString, } from "./lifecycle.js"; import { VisibilityFilter as GeneratedVisibilityFilter } from "../../../generated-defs/TypeSpec.js"; +import { useStateMap, useStateSet } from "../../lib/utils.js"; /** * A set of active visibility modifiers per visibility class. @@ -36,17 +41,22 @@ type VisibilityModifiers = Map>; * * This store is used to track the visibility modifiers */ -const VISIBILITY_STORE = new WeakMap(); +const [getVisibilityStore, setVisibilityStore] = useStateMap( + "visibilityStore", +); /** * Returns the visibility modifiers for a given `property` within a `program`. */ -function getOrInitializeVisibilityModifiers(property: ModelProperty): VisibilityModifiers { - let visibilityModifiers = VISIBILITY_STORE.get(property); +function getOrInitializeVisibilityModifiers( + program: Program, + property: ModelProperty, +): VisibilityModifiers { + let visibilityModifiers = getVisibilityStore(program, property); if (!visibilityModifiers) { visibilityModifiers = new Map(); - VISIBILITY_STORE.set(property, visibilityModifiers); + setVisibilityStore(program, property, visibilityModifiers); } return visibilityModifiers; @@ -70,7 +80,7 @@ function getOrInitializeActiveModifierSetForClass( visibilityClass: Enum, defaultSet: Set, ): Set { - const visibilityModifiers = getOrInitializeVisibilityModifiers(property); + const visibilityModifiers = getOrInitializeVisibilityModifiers(program, property); let visibilityModifierSet = visibilityModifiers.get(visibilityClass); if (!visibilityModifierSet) { @@ -86,23 +96,25 @@ function getOrInitializeActiveModifierSetForClass( */ const VISIBILITY_PROGRAM_SEALS = new WeakSet(); -/** - * If a property is in this set, visibility is sealed for that property. - */ -const VISIBILITY_SEALS = new WeakSet(); +const [isVisibilitySealedForProperty, sealVisibilityForProperty] = useStateSet( + "propertyVisibilitySealed", +); -/** - * If a property is a key in this map, visibility is sealed for that property within all the visibility classes in the - * corresponding set. - */ -const VISIBILITY_SEALS_FOR_CLASS = new WeakMap>(); +const [getSealedVisibilityClasses, setSealedVisibilityClasses] = useStateMap< + ModelProperty, + Set +>("sealedVisibilityClasses"); -function sealVisibilityModifiersForClass(property: ModelProperty, visibilityClass: Enum) { - let sealedClasses = VISIBILITY_SEALS_FOR_CLASS.get(property); +function sealVisibilityModifiersForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, +) { + let sealedClasses = getSealedVisibilityClasses(program, property); if (!sealedClasses) { sealedClasses = new Set(); - VISIBILITY_SEALS_FOR_CLASS.set(property, sealedClasses); + setSealedVisibilityClasses(program, property, sealedClasses); } sealedClasses.add(visibilityClass); @@ -111,16 +123,18 @@ function sealVisibilityModifiersForClass(property: ModelProperty, visibilityClas /** * Stores the default modifier set for a given visibility class. */ -const DEFAULT_MODIFIER_SET_CACHE = new WeakMap>(); +const [getDefaultModifiers, setDefaultModifiers] = useStateMap>( + "defaultVisibilityModifiers", +); -function getDefaultModifierSetForClass(visibilityClass: Enum): Set { - const cached = DEFAULT_MODIFIER_SET_CACHE.get(visibilityClass); +function getDefaultModifierSetForClass(program: Program, visibilityClass: Enum): Set { + const cached = getDefaultModifiers(program, visibilityClass); if (cached) return cached; const defaultModifierSet = new Set(visibilityClass.members.values()); - DEFAULT_MODIFIER_SET_CACHE.set(visibilityClass, defaultModifierSet); + setDefaultModifiers(program, visibilityClass, defaultModifierSet); return defaultModifierSet; } @@ -132,22 +146,23 @@ function getDefaultModifierSetForClass(visibilityClass: Enum): Set { * before the default modifier set is used by any operation. */ export function setDefaultModifierSetForVisibilityClass( + program: Program, visibilityClass: Enum, defaultSet: Set, ) { compilerAssert( - !DEFAULT_MODIFIER_SET_CACHE.has(visibilityClass), + !getDefaultModifiers(program, visibilityClass), "The default modifier set for a visibility class may only be set once.", ); - DEFAULT_MODIFIER_SET_CACHE.set(visibilityClass, defaultSet); + setDefaultModifiers(program, visibilityClass, defaultSet); } /** * Convert a sequence of visibility modifiers into a map of visibility classes to their respective modifiers in the * sequence. */ -function groupModifiersByVisibilityClass(modifiers: EnumMember[]) { +function groupModifiersByVisibilityClass(modifiers: EnumMember[]): Map> { const enumMap = new Map>(); // Prepare new modifier sets for each visibility class @@ -169,7 +184,8 @@ function groupModifiersByVisibilityClass(modifiers: EnumMember[]) { // #region Legacy Visibility API -const LEGACY_VISIBILITY_MODIFIERS = new WeakMap(); +const [getLegacyVisibility, setLegacyVisibilityModifiers, getLegacyVisibilityStateMap] = + useStateMap("legacyVisibility"); /** * Sets the legacy visibility modifiers for a property. @@ -188,87 +204,79 @@ export function setLegacyVisibility( property: ModelProperty, visibilities: string[], ) { + const { program } = context; compilerAssert( - LEGACY_VISIBILITY_MODIFIERS.get(property) === undefined, + getLegacyVisibility(program, property) === undefined, "Legacy visibility modifiers have already been set for this property.", ); - LEGACY_VISIBILITY_MODIFIERS.set(property, visibilities); + setLegacyVisibilityModifiers(program, property, visibilities); - const lifecycleClass = getLifecycleVisibilityEnum(context.program); + const lifecycleClass = getLifecycleVisibilityEnum(program); if (visibilities.length === 1 && visibilities[0] === "none") { - clearVisibilityModifiersForClass(context.program, property, lifecycleClass, context); + clearVisibilityModifiersForClass(program, property, lifecycleClass, context); } else { const lifecycleVisibilities = visibilities - .map((v) => normalizeLegacyLifecycleVisibilityString(context.program, v)) + .map((v) => normalizeLegacyLifecycleVisibilityString(program, v)) .filter((v) => !!v); - addVisibilityModifiers(context.program, property, lifecycleVisibilities); + addVisibilityModifiers(program, property, lifecycleVisibilities); } - sealVisibilityModifiers(property, lifecycleClass); + sealVisibilityModifiers(program, property, lifecycleClass); +} + +/** + * Removes legacy visibility modifiers from a property. + * + * @param program - the program in which the property occurs + * @param property - the property to remove visibility modifiers from + */ +export function clearLegacyVisibility(program: Program, property: ModelProperty) { + getLegacyVisibilityStateMap(program).delete(property); } /** * Returns the legacy visibility modifiers for a property. * + * For a property using the enum-driven visibility system, the active Lifecycle visibility modifiers will be converted + * to strings for backwards compatibility as follows: + * + * - If Lifecycle visibility is not explicitly set, and no legacy visibility is set, this function will return `undefined`. + * - If the property has no active Lifecycle visibility modifiers, this function will return `["none"]`. + * - Otherwise, this function will return an array of lowercase strings representing the active Lifecycle visibility + * modifiers ("create", "read", "update"). + * * @deprecated Use `getVisibilityForClass` or `getLifecycleVisibility` instead. * @param program - the program in which the property occurs * @param property - the property to get legacy visibility modifiers for */ export function getVisibility(program: Program, property: ModelProperty): string[] | undefined { - void program; - return LEGACY_VISIBILITY_MODIFIERS.get(property); -} + const legacyModifiers = getLegacyVisibility(program, property); -export function splitLegacyVisibility( - visibilities: (string | EnumValue)[], -): [EnumMember[], string[]] { - const legacyVisibilities = [] as string[]; - const modifiers = [] as EnumMember[]; - - for (const visibility of visibilities) { - if (typeof visibility === "string") { - legacyVisibilities.push(visibility); - } else { - modifiers.push(visibility.value); - } - } + if (legacyModifiers) return legacyModifiers; - return [modifiers, legacyVisibilities] as const; -} + // Now check for applied lifecycle visibility modifiers and coerce them if necessary. -// #endregion + const lifecycleModifiers = getVisibilityStore(program, property)?.get( + getLifecycleVisibilityEnum(program), + ); -// #region Visibility Management API + // Visibility is completely uninitialized, so return undefined to mimic legacy behavior. + if (!lifecycleModifiers) return undefined; -/** - * Initializes the default modifier set for a visibility class. - * - * This function may be called once per visibility class to set the modifier set that should be used when no modifiers - * are specified on a property for the given visibility class. - * - * If no default set is provided for a visibility class using this function, the default set will be the set of ALL - * members/modifiers in the visibility class enum. - * - * This function may only be called ONCE per visibility class. - * - * @param visibilityClass - * @param defaultSet - */ -export function initializeDefaultModifierSetForClass( - visibilityClass: Enum, - defaultSet: Set, -) { - compilerAssert( - !DEFAULT_MODIFIER_SET_CACHE.has(visibilityClass), - "The default modifier set for a visibility class may only be initialized once.", - ); + // Visibility has been cleared explicitly: return ["none"] to mimic legacy application of visibility "none". + if (lifecycleModifiers.size === 0) return ["none"]; - DEFAULT_MODIFIER_SET_CACHE.set(visibilityClass, defaultSet); + // Otherwise we just convert the modifiers to strings. + return Array.from(lifecycleModifiers).map((v) => v.name.toLowerCase()); } +// #endregion + +// #region Visibility Management API + /** * Check if a property has had its visibility modifiers sealed. * @@ -289,10 +297,10 @@ export function isSealed( if (VISIBILITY_PROGRAM_SEALS.has(program)) return true; const classSealed = visibilityClass - ? VISIBILITY_SEALS_FOR_CLASS.get(property)?.has(visibilityClass) + ? getSealedVisibilityClasses(program, property)?.has(visibilityClass) : false; - return classSealed || VISIBILITY_SEALS.has(property); + return classSealed || isVisibilitySealedForProperty(program, property); } /** @@ -304,11 +312,15 @@ export function isSealed( * @param property - the property to seal * @param visibilityClass - the optional visibility class to seal the property for */ -export function sealVisibilityModifiers(property: ModelProperty, visibilityClass?: Enum) { +export function sealVisibilityModifiers( + program: Program, + property: ModelProperty, + visibilityClass?: Enum, +) { if (visibilityClass) { - sealVisibilityModifiersForClass(property, visibilityClass); + sealVisibilityModifiersForClass(program, property, visibilityClass); } else { - VISIBILITY_SEALS.add(property); + sealVisibilityForProperty(program, property); } } @@ -408,7 +420,7 @@ export function removeVisibilityModifiers( program, property, visibilityClass, - /* defaultSet: */ getDefaultModifierSetForClass(visibilityClass), + /* defaultSet: */ getDefaultModifierSetForClass(program, visibilityClass), ); for (const modifier of newModifiers) { @@ -458,7 +470,7 @@ export function getVisibilityForClass( program, property, visibilityClass, - /* defaultSet: */ getDefaultModifierSetForClass(visibilityClass), + /* defaultSet: */ getDefaultModifierSetForClass(program, visibilityClass), ); } @@ -482,7 +494,7 @@ export function hasVisibility( program, property, modifier.enum, - /* defaultSet: */ getDefaultModifierSetForClass(modifier.enum), + /* defaultSet: */ getDefaultModifierSetForClass(program, modifier.enum), ); return activeSet?.has(modifier) ?? false; diff --git a/packages/compiler/src/core/visibility/lifecycle.ts b/packages/compiler/src/core/visibility/lifecycle.ts index 11c137d829..a12468c04a 100644 --- a/packages/compiler/src/core/visibility/lifecycle.ts +++ b/packages/compiler/src/core/visibility/lifecycle.ts @@ -35,32 +35,13 @@ export function getLifecycleVisibilityEnum(program: Program): Enum { return type; } -// interface LifecycleVisibilityEnumMembers { -// Create: EnumMember; -// Read: EnumMember; -// Update: EnumMember; -// } - -// const LIFECYCLE_ENUM_MEMBERS_CACHE = new WeakMap(); - -// function getLifecycleVisibilityEnumMembers(program: Program): LifecycleVisibilityEnumMembers { -// const cached = LIFECYCLE_ENUM_MEMBERS_CACHE.get(program); - -// if (cached) return cached; - -// const lifecycle = getLifecycleVisibilityEnum(program); - -// const members: LifecycleVisibilityEnumMembers = { -// Create: lifecycle.members.get("Create")!, -// Read: lifecycle.members.get("Read")!, -// Update: lifecycle.members.get("Update")!, -// }; - -// LIFECYCLE_ENUM_MEMBERS_CACHE.set(program, members); - -// return members; -// } - +/** + * Returns the member of `Lifecycle` that corresponds to the given legacy `visibility` string. + * + * @param program - the program to get the lifecycle visibility enum for + * @param visibility - the visibility string to normalize + * @returns the corresponding member of `Lifecycle` or `undefined` if the visibility string is not recognized + */ export function normalizeLegacyLifecycleVisibilityString( program: Program, visibility: string, @@ -77,3 +58,32 @@ export function normalizeLegacyLifecycleVisibilityString( return undefined; } } + +/** + * Returns the legacy visibility string that corresponds to the given `visibility` member of `Lifecycle`. + * + * If the given `visibility` member is not a member of `Lifecycle`, the function will return `undefined`. + * + * @param program - the program to get the lifecycle visibility enum for + * @param visibility - the visibility modifier to normalize + * @returns the corresponding legacy visibility string or `undefined` if the visibility member is not recognized + */ +export function normalizeVisibilityToLegacyLifecycleString( + program: Program, + visibility: EnumMember, +): string | undefined { + const lifecycle = getLifecycleVisibilityEnum(program); + + if (visibility.enum !== lifecycle) return undefined; + + switch (visibility.name) { + case "Create": + return "create"; + case "Read": + return "read"; + case "Update": + return "update"; + default: + return undefined; + } +} diff --git a/packages/compiler/src/lib/decorators.ts b/packages/compiler/src/lib/decorators.ts index c6ebe40bed..024ca1ed25 100644 --- a/packages/compiler/src/lib/decorators.ts +++ b/packages/compiler/src/lib/decorators.ts @@ -99,11 +99,13 @@ import { UnionVariant, Value, } from "../core/types.js"; +import { setKey } from "./key.js"; import { useStateMap, useStateSet } from "./utils.js"; export { $encodedName, resolveEncodedName } from "./encoded-names.js"; export { serializeValueAsJson } from "./examples.js"; export * from "./service.js"; +export * from "./visibility.js"; export { ExampleOptions }; export const namespace = "TypeSpec"; @@ -1074,8 +1076,6 @@ function isEnumMemberAssignableToType(program: Program, typeName: Type, member: } export { getKnownValues }; -const [getKey, setKey] = useStateMap("key"); - /** * `@key` - mark a model property as the key to identify instances of that type * @@ -1104,13 +1104,7 @@ export const $key: KeyDecorator = ( setKey(context.program, entity, altName || entity.name); }; -export function isKey(program: Program, property: ModelProperty) { - return getKey(program, property) !== undefined; -} - -export function getKeyName(program: Program, property: ModelProperty): string | undefined { - return getKey(program, property); -} +export { getKeyName, isKey } from "./key.js"; /** * Mark a type as deprecated diff --git a/packages/compiler/src/lib/key.ts b/packages/compiler/src/lib/key.ts new file mode 100644 index 0000000000..2e7ba1a96a --- /dev/null +++ b/packages/compiler/src/lib/key.ts @@ -0,0 +1,11 @@ +import { Program } from "../core/index.js"; +import { ModelProperty, Type } from "../core/types.js"; +import { useStateMap } from "./utils.js"; + +const [getKey, setKey] = useStateMap("key"); + +export function isKey(program: Program, property: ModelProperty) { + return getKey(program, property) !== undefined; +} + +export { getKey as getKeyName, setKey }; diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index 89d1808244..dbc1d21061 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -44,6 +44,7 @@ import { $returnTypeVisibility, $visibility, $withDefaultKeyVisibility, + $withLifecycleUpdate, $withUpdateableProperties, $withVisibility, $withVisibilityFilter, @@ -94,6 +95,7 @@ export const $decorators = { defaultVisibility: $defaultVisibility, withVisibility: $withVisibility, withVisibilityFilter: $withVisibilityFilter, + withLifecycleUpdate: $withLifecycleUpdate, withDefaultKeyVisibility: $withDefaultKeyVisibility, parameterVisibility: $parameterVisibility, returnTypeVisibility: $returnTypeVisibility, diff --git a/packages/compiler/src/lib/utils.ts b/packages/compiler/src/lib/utils.ts index 910745ba1d..215a456f5f 100644 --- a/packages/compiler/src/lib/utils.ts +++ b/packages/compiler/src/lib/utils.ts @@ -1,4 +1,4 @@ -import type { Type } from "../core/types.js"; +import type { Model, ModelProperty, Type } from "../core/types.js"; import { unsafe_useStateMap, unsafe_useStateSet } from "../experimental/state-accessor.js"; function createStateSymbol(name: string) { @@ -12,3 +12,21 @@ export function useStateMap(key: string | symbol) { export function useStateSet(key: string | symbol) { return unsafe_useStateSet(typeof key === "string" ? createStateSymbol(key) : key); } + +/** + * Filters the properties of a model by removing them from the model instance if + * a given `filter` predicate is not satisfied. + * + * @param model - the model to filter properties on + * @param filter - the predicate to filter properties with + */ +export function filterModelPropertiesInPlace( + model: Model, + filter: (prop: ModelProperty) => boolean, +) { + for (const [key, prop] of model.properties) { + if (!filter(prop)) { + model.properties.delete(key); + } + } +} diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 59f7d5886e..17a56c52cf 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + import type { DefaultVisibilityDecorator, VisibilityFilter as GeneratedVisibilityFilter, @@ -6,6 +9,7 @@ import type { ReturnTypeVisibilityDecorator, VisibilityDecorator, WithDefaultKeyVisibilityDecorator, + WithLifecycleUpdateDecorator, WithUpdateablePropertiesDecorator, WithVisibilityDecorator, WithVisibilityFilterDecorator, @@ -13,13 +17,13 @@ import type { import { validateDecoratorTarget, validateDecoratorUniqueOnNode } from "../core/decorator-utils.js"; import { addVisibilityModifiers, + clearLegacyVisibility, clearVisibilityModifiersForClass, getVisibility, isVisible, Program, setDefaultModifierSetForVisibilityClass, setLegacyVisibility, - splitLegacyVisibility, VisibilityFilter, } from "../core/index.js"; import { reportDiagnostic } from "../core/messages.js"; @@ -35,12 +39,37 @@ import { } from "../core/types.js"; import { getLifecycleVisibilityEnum, - normalizeLegacyLifecycleVisibilityString, + normalizeVisibilityToLegacyLifecycleString, } from "../core/visibility/lifecycle.js"; -import { createStateSymbol, filterModelPropertiesInPlace, isKey } from "./decorators.js"; +import { isKey } from "./key.js"; +import { filterModelPropertiesInPlace, useStateMap } from "./utils.js"; // #region Legacy Visibility Utilities +/** + * Takes a list of visibilities that possibly include both legacy visibility + * strings and visibility class members, and returns two lists containing only + * each type. + * + * @param visibilities - The list of visibilities to split + * @returns a tuple containing visibility enum members in the first position and + * legacy visibility strings in the second position + */ +function splitLegacyVisibility(visibilities: (string | EnumValue)[]): [EnumMember[], string[]] { + const legacyVisibilities = [] as string[]; + const modifiers = [] as EnumMember[]; + + for (const visibility of visibilities) { + if (typeof visibility === "string") { + legacyVisibilities.push(visibility); + } else { + modifiers.push(visibility.value); + } + } + + return [modifiers, legacyVisibilities] as const; +} + export const $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator = ( context: DecoratorContext, entity: Model, @@ -81,44 +110,109 @@ export const $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator = ( }); }; -export const parameterVisibilityKey = createStateSymbol("parameterVisibility"); +interface OperationVisibilityConfig { + parameters?: string[] | EnumMember[]; + returnType?: string[] | EnumMember[]; +} + +const [getOperationVisibilityConfigRaw, setOperationVisibilityConfigRaw] = useStateMap< + Operation, + OperationVisibilityConfig +>("operationVisibilityConfig"); + +function getOperationVisibilityConfig( + program: Program, + operation: Operation, +): OperationVisibilityConfig { + let config = getOperationVisibilityConfigRaw(program, operation); + + if (!config) { + config = {}; + + setOperationVisibilityConfigRaw(program, operation, config); + } + + return config; +} export const $parameterVisibility: ParameterVisibilityDecorator = ( context: DecoratorContext, - entity: Operation, + operation: Operation, ...visibilities: (string | EnumValue)[] ) => { - validateDecoratorUniqueOnNode(context, entity, $parameterVisibility); - context.program.stateMap(parameterVisibilityKey).set(entity, visibilities); + validateDecoratorUniqueOnNode(context, operation, $parameterVisibility); + + const [modifiers, legacyVisibilities] = splitLegacyVisibility(visibilities); + + if (modifiers.length > 0 && legacyVisibilities.length > 0) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + target: context.decoratorTarget, + }); + + return; + } + + if (modifiers.length > 0) { + getOperationVisibilityConfig(context.program, operation).parameters = modifiers; + } else { + getOperationVisibilityConfig(context.program, operation).parameters = legacyVisibilities; + } }; /** * Returns the visibilities of the parameters of the given operation, if provided with `@parameterVisibility`. * + * @deprecated Use {@link getParameterVisibilityFilter} instead. + * * @see {@link $parameterVisibility} */ export function getParameterVisibility(program: Program, entity: Operation): string[] | undefined { - return program.stateMap(parameterVisibilityKey).get(entity); + return getOperationVisibilityConfig(program, entity) + .parameters?.map((p) => + typeof p === "string" ? p : normalizeVisibilityToLegacyLifecycleString(program, p), + ) + .filter((p) => !!p) as string[]; } -export const returnTypeVisibilityKey = createStateSymbol("returnTypeVisibility"); - export const $returnTypeVisibility: ReturnTypeVisibilityDecorator = ( context: DecoratorContext, - entity: Operation, + operation: Operation, ...visibilities: (string | EnumValue)[] ) => { - validateDecoratorUniqueOnNode(context, entity, $returnTypeVisibility); - context.program.stateMap(returnTypeVisibilityKey).set(entity, visibilities); + validateDecoratorUniqueOnNode(context, operation, $parameterVisibility); + + const [modifiers, legacyVisibilities] = splitLegacyVisibility(visibilities); + + if (modifiers.length > 0 && legacyVisibilities.length > 0) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + target: context.decoratorTarget, + }); + + return; + } + + if (modifiers.length > 0) { + getOperationVisibilityConfig(context.program, operation).returnType = modifiers; + } else { + getOperationVisibilityConfig(context.program, operation).returnType = legacyVisibilities; + } }; /** * Returns the visibilities of the return type of the given operation, if provided with `@returnTypeVisibility`. * + * @deprecated Use {@link getReturnTypeVisibilityFilter} instead. + * * @see {@link $returnTypeVisibility} */ export function getReturnTypeVisibility(program: Program, entity: Operation): string[] | undefined { - return program.stateMap(returnTypeVisibilityKey).get(entity); + return getOperationVisibilityConfig(program, entity) + .returnType?.map((p) => + typeof p === "string" ? p : normalizeVisibilityToLegacyLifecycleString(program, p), + ) + .filter((p) => !!p) as string[]; } // -- @visibility decorator --------------------- @@ -182,7 +276,7 @@ export const $defaultVisibility: DefaultVisibilityDecorator = ( } } - setDefaultModifierSetForVisibilityClass(target, modifierSet); + setDefaultModifierSetForVisibilityClass(context.program, target, modifierSet); }; // -- @withVisibility decorator --------------------- @@ -204,21 +298,22 @@ export const $withVisibility: WithVisibilityDecorator = ( return; } - const filter: VisibilityFilter = { - all: new Set( - legacyVisibilities - .map((v) => normalizeLegacyLifecycleVisibilityString(context.program, v)) - .filter((v) => !!v), - ), - }; + // eslint-disable-next-line @typescript-eslint/no-deprecated + filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, legacyVisibilities)); - filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, filter)); for (const p of target.properties.values()) { - clearVisibilityModifiersForClass( - context.program, - p, - getLifecycleVisibilityEnum(context.program), - ); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const legacyModifiers = getVisibility(context.program, p); + + if (legacyModifiers && legacyModifiers.length > 0) { + clearLegacyVisibility(context.program, p); + } else { + clearVisibilityModifiersForClass( + context.program, + p, + getLifecycleVisibilityEnum(context.program), + ); + } } } else { const filter: VisibilityFilter = { @@ -237,20 +332,14 @@ export const $withVisibility: WithVisibilityDecorator = ( // -- @withUpdateableProperties decorator ---------------------- -const UPDATE_VISIBILITY_MODIFIERS = new WeakMap(); - -function getUpdateVisibilityModifier(program: Program): EnumMember { - let modifier = UPDATE_VISIBILITY_MODIFIERS.get(program); - - if (!modifier) { - const lifecycle = getLifecycleVisibilityEnum(program); - modifier = lifecycle.members.get("Update")!; - UPDATE_VISIBILITY_MODIFIERS.set(program, modifier); - } - - return modifier; -} - +/** + * Filters a model for properties that are updateable. + * + * @deprecated Use `@withVisibilityFilter` or `@withLifecycleVisibility` instead. + * + * @param context - the program context + * @param target - Model to filter for updateable properties + */ export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( context: DecoratorContext, target: Type, @@ -259,11 +348,7 @@ export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( return; } - const filter: VisibilityFilter = { - all: new Set([getUpdateVisibilityModifier(context.program)]), - }; - - filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, filter)); + filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, ["update"])); }; // -- @withVisibilityFilter decorator ---------------------- @@ -279,4 +364,14 @@ export const $withVisibilityFilter: WithVisibilityFilterDecorator = ( throw new Error("Not implemented."); }; +// -- @withLifecycleUpdate decorator ---------------------- + +export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( + context: DecoratorContext, + target: Model, +) => { + // TODO + throw new Error("Not implemented."); +}; + // #endregion diff --git a/packages/http/src/metadata.ts b/packages/http/src/metadata.ts index 1c0de783cb..2385a830d4 100644 --- a/packages/http/src/metadata.ts +++ b/packages/http/src/metadata.ts @@ -206,9 +206,11 @@ export function resolveRequestVisibility( operation: Operation, verb: HttpVerb, ): Visibility { - const parameterVisibility = arrayToVisibility(getParameterVisibility(program, operation)); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const parameterVisibility = getParameterVisibility(program, operation); + const parameterVisibilityArray = arrayToVisibility(parameterVisibility); const defaultVisibility = getDefaultVisibilityForVerb(verb); - let visibility = parameterVisibility ?? defaultVisibility; + let visibility = parameterVisibilityArray ?? defaultVisibility; // If the verb is PATCH, then we need to add the patch flag to the visibility in order for // later processes to properly apply it if (verb === "patch") { @@ -234,6 +236,7 @@ export function isMetadata(program: Program, property: ModelProperty) { * Determines if the given property is visible with the given visibility. */ export function isVisible(program: Program, property: ModelProperty, visibility: Visibility) { + // eslint-disable-next-line @typescript-eslint/no-deprecated return isVisibleCore(program, property, visibilityToArray(visibility)); } From b6f893cfe076294bb7617bbf32dedc45ab891015 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Mon, 28 Oct 2024 14:19:42 -0400 Subject: [PATCH 04/28] Fix issue with resource create operations --- packages/compiler/src/core/visibility/core.ts | 22 +++++++++++++++++++ packages/compiler/src/lib/visibility.ts | 9 +++----- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index 0020246db5..db496ad41e 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -457,6 +457,28 @@ export function clearVisibilityModifiersForClass( modifierSet.clear(); } +export function resetVisibilityModifiersForClass( + program: Program, + property: ModelProperty, + visibilityClass: Enum, + context?: DecoratorContext, +) { + const target = context?.decoratorTarget ?? property; + + if (isSealed(program, property, visibilityClass)) { + reportDiagnostic(program, { + code: "visibility-sealed", + format: { + propName: property.name, + }, + target, + }); + return; + } + + getOrInitializeVisibilityModifiers(program, property).delete(visibilityClass); +} + // #endregion // #region Visibility Analysis API diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 17a56c52cf..902e608328 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -22,6 +22,7 @@ import { getVisibility, isVisible, Program, + resetVisibilityModifiersForClass, setDefaultModifierSetForVisibilityClass, setLegacyVisibility, VisibilityFilter, @@ -308,11 +309,7 @@ export const $withVisibility: WithVisibilityDecorator = ( if (legacyModifiers && legacyModifiers.length > 0) { clearLegacyVisibility(context.program, p); } else { - clearVisibilityModifiersForClass( - context.program, - p, - getLifecycleVisibilityEnum(context.program), - ); + resetVisibilityModifiersForClass(context.program, p, getLifecycleVisibilityEnum(context.program)); } } } else { @@ -324,7 +321,7 @@ export const $withVisibility: WithVisibilityDecorator = ( filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, filter)); for (const p of target.properties.values()) { for (const c of visibilityClasses) { - clearVisibilityModifiersForClass(context.program, p, c); + resetVisibilityModifiersForClass(context.program, p, c); } } } From c68c2c92a2abe3474bad19db684bb50a9030c673 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Mon, 28 Oct 2024 13:11:56 -0700 Subject: [PATCH 05/28] Fix circular reference issue --- .../compiler/.scripts/gen-extern-signature.ts | 5 +++- packages/compiler/generated-defs/TypeSpec.ts | 2 +- packages/compiler/src/core/visibility/core.ts | 6 ++-- .../compiler/src/core/visibility/lifecycle.ts | 4 +-- .../typekit/kits/model-property.ts | 2 +- packages/compiler/src/lib/visibility.ts | 30 +++++++++++-------- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/compiler/.scripts/gen-extern-signature.ts b/packages/compiler/.scripts/gen-extern-signature.ts index f69d1d8314..f54907f75f 100644 --- a/packages/compiler/.scripts/gen-extern-signature.ts +++ b/packages/compiler/.scripts/gen-extern-signature.ts @@ -15,7 +15,10 @@ const program = await compile(NodeHost, root, {}); const files = await generateExternDecorators(program, "@typespec/compiler"); for (const [name, content] of Object.entries(files)) { - const updatedContent = content.replace(/from "\@typespec\/compiler"/g, `from "../src/index.js"`); + const updatedContent = content.replace( + /from "\@typespec\/compiler"/g, + `from "../src/core/index.js"`, + ); const prettierConfig = await resolveConfig(root); await NodeHost.writeFile( diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 242beb01a2..9312995226 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -12,7 +12,7 @@ import type { Type, Union, UnionVariant, -} from "../src/index.js"; +} from "../src/core/index.js"; export interface ExampleOptions { readonly title?: string; diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index db496ad41e..771e9770ab 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -21,14 +21,14 @@ import { compilerAssert } from "../diagnostics.js"; import { reportDiagnostic } from "../messages.js"; -import { Program } from "../program.js"; -import { DecoratorContext, Enum, EnumMember, ModelProperty } from "../types.js"; +import type { Program } from "../program.js"; +import type { DecoratorContext, Enum, EnumMember, ModelProperty } from "../types.js"; import { getLifecycleVisibilityEnum, normalizeLegacyLifecycleVisibilityString, } from "./lifecycle.js"; -import { VisibilityFilter as GeneratedVisibilityFilter } from "../../../generated-defs/TypeSpec.js"; +import type { VisibilityFilter as GeneratedVisibilityFilter } from "../../../generated-defs/TypeSpec.js"; import { useStateMap, useStateSet } from "../../lib/utils.js"; /** diff --git a/packages/compiler/src/core/visibility/lifecycle.ts b/packages/compiler/src/core/visibility/lifecycle.ts index a12468c04a..0d6addf627 100644 --- a/packages/compiler/src/core/visibility/lifecycle.ts +++ b/packages/compiler/src/core/visibility/lifecycle.ts @@ -2,8 +2,8 @@ // Licensed under the MIT license. import { compilerAssert } from "../diagnostics.js"; -import { Program } from "../program.js"; -import { Enum, EnumMember } from "../types.js"; +import type { Program } from "../program.js"; +import type { Enum, EnumMember } from "../types.js"; /** * A cache for the `TypeSpec.Visibility.Lifecycle` enum per Program instance. diff --git a/packages/compiler/src/experimental/typekit/kits/model-property.ts b/packages/compiler/src/experimental/typekit/kits/model-property.ts index e7e9acfb05..a8b456f40d 100644 --- a/packages/compiler/src/experimental/typekit/kits/model-property.ts +++ b/packages/compiler/src/experimental/typekit/kits/model-property.ts @@ -1,5 +1,5 @@ -import { getVisibilityForClass } from "../../../core/index.js"; import type { Enum, EnumMember, ModelProperty, Scalar, Type } from "../../../core/types.js"; +import { getVisibilityForClass } from "../../../core/visibility/core.js"; import { EncodeData, getEncode, getFormat } from "../../../lib/decorators.js"; import { defineKit } from "../define-kit.js"; diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 902e608328..578e3e9e5a 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -15,19 +15,8 @@ import type { WithVisibilityFilterDecorator, } from "../../generated-defs/TypeSpec.js"; import { validateDecoratorTarget, validateDecoratorUniqueOnNode } from "../core/decorator-utils.js"; -import { - addVisibilityModifiers, - clearLegacyVisibility, - clearVisibilityModifiersForClass, - getVisibility, - isVisible, - Program, - resetVisibilityModifiersForClass, - setDefaultModifierSetForVisibilityClass, - setLegacyVisibility, - VisibilityFilter, -} from "../core/index.js"; import { reportDiagnostic } from "../core/messages.js"; +import type { Program } from "../core/program.js"; import { DecoratorContext, Enum, @@ -38,6 +27,17 @@ import { Operation, Type, } from "../core/types.js"; +import { + addVisibilityModifiers, + clearLegacyVisibility, + clearVisibilityModifiersForClass, + getVisibility, + isVisible, + resetVisibilityModifiersForClass, + setDefaultModifierSetForVisibilityClass, + setLegacyVisibility, + VisibilityFilter, +} from "../core/visibility/core.js"; import { getLifecycleVisibilityEnum, normalizeVisibilityToLegacyLifecycleString, @@ -309,7 +309,11 @@ export const $withVisibility: WithVisibilityDecorator = ( if (legacyModifiers && legacyModifiers.length > 0) { clearLegacyVisibility(context.program, p); } else { - resetVisibilityModifiersForClass(context.program, p, getLifecycleVisibilityEnum(context.program)); + resetVisibilityModifiersForClass( + context.program, + p, + getLifecycleVisibilityEnum(context.program), + ); } } } else { From b06b4da5f8ea14c6d0485e20b4a9d92bb5370063 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Tue, 29 Oct 2024 14:01:50 -0400 Subject: [PATCH 06/28] Implement visibility transform decorators --- packages/compiler/generated-defs/TypeSpec.ts | 16 +++- packages/compiler/src/core/visibility/core.ts | 13 ++- .../compiler/src/core/visibility/index.ts | 13 ++- packages/compiler/src/lib/visibility.ts | 80 +++++++++++++++++-- 4 files changed, 109 insertions(+), 13 deletions(-) diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index 242beb01a2..8e0a26c1aa 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -120,7 +120,17 @@ export type WithoutDefaultValuesDecorator = (context: DecoratorContext, target: /** * Set the visibility of key properties in a model if not already set. * - * @param visibility The desired default visibility value. If a key property already has a `visibility` decorator then the default visibility is not applied. + * This will set the visibility modifiers of all key properties in the model if the visibility is not already _explicitly_ set, + * but will not change the visibility of any properties that have visibility set _explicitly_, even if the visibility + * is the same as the default visibility. + * + * Visibility may be explicitly set using any of the following decorators: + * + * - `@visibility` + * - `@removeVisibility` + * - `@invisible` + * + * @param visibility The desired default visibility value. If a key property already has visibility set, it will not be changed. */ export type WithDefaultKeyVisibilityDecorator = ( context: DecoratorContext, @@ -675,9 +685,9 @@ export type InspectTypeNameDecorator = ( * ```typespec * model Dog { * // the service will generate an ID, so you don't need to send it. - * @visibility("read") id: int32; + * @visibility(Lifecycle.Read) id: int32; * // the service will store this secret name, but won't ever return it - * @visibility("create", "update") secretName: string; + * @visibility(Lifecycle.Create, Lifecycle.Update) secretName: string; * // the regular name is always present * name: string; * } diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index db496ad41e..acb5ac9d3d 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -28,9 +28,11 @@ import { normalizeLegacyLifecycleVisibilityString, } from "./lifecycle.js"; -import { VisibilityFilter as GeneratedVisibilityFilter } from "../../../generated-defs/TypeSpec.js"; +import { VisibilityFilter as TypespecVisibilityFilter } from "../../../generated-defs/TypeSpec.js"; import { useStateMap, useStateSet } from "../../lib/utils.js"; +export { TypespecVisibilityFilter } + /** * A set of active visibility modifiers per visibility class. */ @@ -544,13 +546,20 @@ export interface VisibilityFilter { } export const VisibilityFilter = { - fromDecoratorArgument(filter: GeneratedVisibilityFilter): VisibilityFilter { + fromDecoratorArgument(filter: TypespecVisibilityFilter): VisibilityFilter { return { all: filter.all && new Set(filter.all.map((v) => v.value)), any: filter.any && new Set(filter.any.map((v) => v.value)), none: filter.none && new Set(filter.none.map((v) => v.value)), }; }, + getVisibilityClasses(filter: VisibilityFilter): Set { + const classes = new Set(); + if (filter.all) filter.all.forEach((v) => classes.add(v.enum)); + if (filter.any) filter.any.forEach((v) => classes.add(v.enum)); + if (filter.none) filter.none.forEach((v) => classes.add(v.enum)); + return classes; + } }; /** diff --git a/packages/compiler/src/core/visibility/index.ts b/packages/compiler/src/core/visibility/index.ts index d02d988532..9819bf2ffe 100644 --- a/packages/compiler/src/core/visibility/index.ts +++ b/packages/compiler/src/core/visibility/index.ts @@ -1,5 +1,14 @@ // Copyright (c) Microsoft Corporation // Licensed under the MIT license. -export * from "./core.js"; -export * from "./lifecycle.js"; +export { + getLifecycleVisibilityEnum +} from "./lifecycle.js"; + +export { + getVisibilityForClass, + getVisibility, + addVisibilityModifiers, + hasVisibility, + isVisible, +} from "./core.js"; diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 902e608328..dcf3cd4fe2 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -3,7 +3,6 @@ import type { DefaultVisibilityDecorator, - VisibilityFilter as GeneratedVisibilityFilter, InvisibleDecorator, ParameterVisibilityDecorator, ReturnTypeVisibilityDecorator, @@ -25,6 +24,7 @@ import { resetVisibilityModifiersForClass, setDefaultModifierSetForVisibilityClass, setLegacyVisibility, + TypespecVisibilityFilter, VisibilityFilter, } from "../core/index.js"; import { reportDiagnostic } from "../core/messages.js"; @@ -42,6 +42,7 @@ import { getLifecycleVisibilityEnum, normalizeVisibilityToLegacyLifecycleString, } from "../core/visibility/lifecycle.js"; +import { mutateSubgraph, Mutator, MutatorFlow } from "../experimental/mutators.js"; import { isKey } from "./key.js"; import { filterModelPropertiesInPlace, useStateMap } from "./utils.js"; @@ -353,12 +354,15 @@ export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( export const $withVisibilityFilter: WithVisibilityFilterDecorator = ( context: DecoratorContext, target: Model, - _filter: GeneratedVisibilityFilter, + _filter: TypespecVisibilityFilter, ) => { const filter = VisibilityFilter.fromDecoratorArgument(_filter); - // TODO - throw new Error("Not implemented."); + const vfMutator: Mutator = createVisibilityFilterMutator(filter); + + const { type } = mutateSubgraph(context.program, [vfMutator], target); + + target.properties = (type as Model).properties; }; // -- @withLifecycleUpdate decorator ---------------------- @@ -367,8 +371,72 @@ export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( context: DecoratorContext, target: Model, ) => { - // TODO - throw new Error("Not implemented."); + const lifecycle = getLifecycleVisibilityEnum(context.program); + const lifecycleUpdate: VisibilityFilter = { + all: new Set([lifecycle.members.get("Update")!]) + }; + + const createOrUpdateMutator = createVisibilityFilterMutator({ + any: new Set([lifecycle.members.get("Create")!, lifecycle.members.get("Update")!]), + }); + + const updateMutator: Mutator = { + name: "LifecycleUpdate", + Model: { + mutate: (model, clone, program, realm) => { + for (const [key, prop] of model.properties) { + if (!isVisible(program, prop, lifecycleUpdate)) { + clone.properties.delete(key); + realm.remove(prop); + } else if (prop.type.kind === "Model") { + const { type } = mutateSubgraph(program, [createOrUpdateMutator], prop.type); + + prop.type = type; + } + + resetVisibilityModifiersForClass(program, prop, lifecycle); + } + + clone.decorators = clone.decorators.filter((d) => d.decorator !== $withLifecycleUpdate); + + return MutatorFlow.DoNotRecurse; + } + } + }; + + const { type } = mutateSubgraph(context.program, [updateMutator], target); + + target.properties = (type as Model).properties; }; + +function createVisibilityFilterMutator(filter: VisibilityFilter): Mutator { + const self: Mutator = { + name: "VisibilityFilter", + Model: { + mutate: (model, clone, program, realm) => { + for (const [key, prop] of model.properties) { + if (!isVisible(program, prop, filter)) { + clone.properties.delete(key); + realm.remove(prop); + } else if (prop.type.kind === "Model") { + const { type } = mutateSubgraph(program, [self], prop.type); + + prop.type = type; + } + + for (const visibilityClass of VisibilityFilter.getVisibilityClasses(filter)) { + resetVisibilityModifiersForClass(program, prop, visibilityClass); + } + } + + clone.decorators = clone.decorators.filter((d) => d.decorator !== $withVisibilityFilter); + + return MutatorFlow.DoNotRecurse; + } + } + }; + + return self; +} // #endregion From 50743ca2f50cca9f3110b40ebfd243c0f4e0de30 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 30 Oct 2024 12:40:23 -0400 Subject: [PATCH 07/28] Lint/format --- packages/compiler/src/core/visibility/core.ts | 2 +- packages/compiler/src/core/visibility/index.ts | 8 +++----- packages/compiler/src/lib/visibility.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index 7e82ce0da1..a943e239a8 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -559,7 +559,7 @@ export const VisibilityFilter = { if (filter.any) filter.any.forEach((v) => classes.add(v.enum)); if (filter.none) filter.none.forEach((v) => classes.add(v.enum)); return classes; - } + }, }; /** diff --git a/packages/compiler/src/core/visibility/index.ts b/packages/compiler/src/core/visibility/index.ts index 9819bf2ffe..01d52526ee 100644 --- a/packages/compiler/src/core/visibility/index.ts +++ b/packages/compiler/src/core/visibility/index.ts @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation // Licensed under the MIT license. -export { - getLifecycleVisibilityEnum -} from "./lifecycle.js"; +export { getLifecycleVisibilityEnum } from "./lifecycle.js"; export { - getVisibilityForClass, - getVisibility, addVisibilityModifiers, + getVisibility, + getVisibilityForClass, hasVisibility, isVisible, } from "./core.js"; diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 9f4ad99947..e536efa348 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -350,6 +350,7 @@ export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( return; } + // eslint-disable-next-line @typescript-eslint/no-deprecated filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, ["update"])); }; @@ -377,7 +378,7 @@ export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( ) => { const lifecycle = getLifecycleVisibilityEnum(context.program); const lifecycleUpdate: VisibilityFilter = { - all: new Set([lifecycle.members.get("Update")!]) + all: new Set([lifecycle.members.get("Update")!]), }; const createOrUpdateMutator = createVisibilityFilterMutator({ @@ -404,8 +405,8 @@ export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( clone.decorators = clone.decorators.filter((d) => d.decorator !== $withLifecycleUpdate); return MutatorFlow.DoNotRecurse; - } - } + }, + }, }; const { type } = mutateSubgraph(context.program, [updateMutator], target); @@ -413,7 +414,6 @@ export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( target.properties = (type as Model).properties; }; - function createVisibilityFilterMutator(filter: VisibilityFilter): Mutator { const self: Mutator = { name: "VisibilityFilter", @@ -437,8 +437,8 @@ function createVisibilityFilterMutator(filter: VisibilityFilter): Mutator { clone.decorators = clone.decorators.filter((d) => d.decorator !== $withVisibilityFilter); return MutatorFlow.DoNotRecurse; - } - } + }, + }, }; return self; From 62dfafb70f58050e55b1fb2e8be16b4d73a92177 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 30 Oct 2024 12:52:07 -0400 Subject: [PATCH 08/28] Clean API surface --- packages/compiler/src/core/index.ts | 2 +- packages/compiler/src/core/visibility/index.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/core/index.ts b/packages/compiler/src/core/index.ts index 934fd53b18..7aae3c2b42 100644 --- a/packages/compiler/src/core/index.ts +++ b/packages/compiler/src/core/index.ts @@ -53,4 +53,4 @@ export * from "./semantic-walker.js"; export { createSourceFile, getSourceFileKindFromExt } from "./source-file.js"; export * from "./type-utils.js"; export * from "./types.js"; -export * from "./visibility/core.js"; +export * from "./visibility/index.js"; diff --git a/packages/compiler/src/core/visibility/index.ts b/packages/compiler/src/core/visibility/index.ts index 01d52526ee..10111cbc5f 100644 --- a/packages/compiler/src/core/visibility/index.ts +++ b/packages/compiler/src/core/visibility/index.ts @@ -5,6 +5,12 @@ export { getLifecycleVisibilityEnum } from "./lifecycle.js"; export { addVisibilityModifiers, + removeVisibilityModifiers, + clearVisibilityModifiersForClass, + resetVisibilityModifiersForClass, + sealVisibilityModifiers, + sealVisibilityModifiersForProgram, + isSealed, getVisibility, getVisibilityForClass, hasVisibility, From 35d8400ad47c19f6893861fa6428450185aef4db Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 30 Oct 2024 14:47:18 -0400 Subject: [PATCH 09/28] Test new core visibility functionality. --- .../generated-defs/TypeSpec.ts-test.ts | 2 +- packages/compiler/lib/std/visibility.tsp | 8 +- packages/compiler/test/visibility.test.ts | 528 ++++++++++++++++++ 3 files changed, 533 insertions(+), 5 deletions(-) create mode 100644 packages/compiler/test/visibility.test.ts diff --git a/packages/compiler/generated-defs/TypeSpec.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.ts-test.ts index a5eb47202e..f7b58a3be3 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts-test.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts-test.ts @@ -1,5 +1,5 @@ /** An error here would mean that the decorator is not exported or doesn't have the right name. */ -import { $decorators } from "../src/index.js"; +import { $decorators } from "../src/core/index.js"; import type { TypeSpecDecorators } from "./TypeSpec.js"; /** An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ const _: TypeSpecDecorators = $decorators["TypeSpec"]; diff --git a/packages/compiler/lib/std/visibility.tsp b/packages/compiler/lib/std/visibility.tsp index 250d445e42..861d896337 100644 --- a/packages/compiler/lib/std/visibility.tsp +++ b/packages/compiler/lib/std/visibility.tsp @@ -246,7 +246,7 @@ extern dec withVisibilityFilter(target: Model, filter: valueof VisibilityFilter) extern dec withLifecycleUpdate(target: Model); /** - * Makes a copy of the model `T` with only the properties that are visible during the + * A copy of the input model `T` with only the properties that are visible during the * "Create" resource lifecycle phase. * * This transformation is recursive, and will include only properties that have the @@ -277,7 +277,7 @@ model Create { + let runner: BasicTestRunner; + + beforeEach(async () => { + runner = await createTestRunner(); + }); + + it("produces correct lifecycle visibility enum reference", async () => { + const { lifecycle } = await runner.compile(` + model X { + @test lifecycle: TypeSpec.Lifecycle; + } + `) as { lifecycle: ModelProperty }; + + const lifecycleEnum = getLifecycleVisibilityEnum(runner.program); + + strictEqual(lifecycleEnum, lifecycle.type); + strictEqual(lifecycleEnum, runner.program.resolveTypeReference("TypeSpec.Lifecycle")[0]) + }); + + describe("visibility seals", () => { + it("seals visibility modifiers for a program", async () => { + const { Example, Dummy } = await runner.compile(` + @test model Example { + x: string; + } + + @test enum Dummy {} + `) as { Example: Model, Dummy: Enum }; + + const x = Example.properties.get("x")!; + + const lifecycle = getLifecycleVisibilityEnum(runner.program); + + ok(!isSealed(runner.program, x)); + ok(!isSealed(runner.program, x, lifecycle)); + ok(!isSealed(runner.program, x, Dummy)); + + sealVisibilityModifiersForProgram(runner.program); + + ok(isSealed(runner.program, x)); + ok(isSealed(runner.program, x, lifecycle)); + ok(isSealed(runner.program, x, Dummy)); + }); + + it("seals visibility modifiers for a visibility class", async () => { + const { Example, Dummy } = await runner.compile(` + @test model Example { + x: string; + } + + @test enum Dummy {} + `) as { Example: Model, Dummy: Enum }; + + const x = Example.properties.get("x")!; + + const lifecycle = getLifecycleVisibilityEnum(runner.program); + + ok(!isSealed(runner.program, x)); + ok(!isSealed(runner.program, x, lifecycle)); + ok(!isSealed(runner.program, x, Dummy)); + + sealVisibilityModifiers(runner.program, x, lifecycle); + + ok(!isSealed(runner.program, x)); + ok(isSealed(runner.program, x, lifecycle)); + ok(!isSealed(runner.program, x, Dummy)); + }); + + it("seals visibility modifiers for a property", async () => { + const { Example, Dummy } = await runner.compile(` + @test model Example { + x: string; + y: string; + } + + @test enum Dummy {} + `) as { Example: Model, Dummy: Enum }; + + const x = Example.properties.get("x")!; + const y = Example.properties.get("y")!; + + const lifecycle = getLifecycleVisibilityEnum(runner.program); + + ok(!isSealed(runner.program, x)); + ok(!isSealed(runner.program, x, lifecycle)); + ok(!isSealed(runner.program, x, Dummy)); + + ok(!isSealed(runner.program, y)); + ok(!isSealed(runner.program, y, lifecycle)); + ok(!isSealed(runner.program, y, Dummy)); + + sealVisibilityModifiers(runner.program, x); + + ok(isSealed(runner.program, x)); + ok(isSealed(runner.program, x, lifecycle)); + ok(isSealed(runner.program, x, Dummy)); + + ok(!isSealed(runner.program, y)); + ok(!isSealed(runner.program, y, lifecycle)); + ok(!isSealed(runner.program, y, Dummy)); + }); + + it("correctly diagnoses modifying sealed visibility", async () => { + const { Example } = await runner.compile(` + @test model Example { + x: string; + } + `) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + const Create = Lifecycle.members.get("Create")!; + + sealVisibilityModifiersForProgram(runner.program); + + addVisibilityModifiers(runner.program, x, [Create]); + removeVisibilityModifiers(runner.program, x, [Create]); + clearVisibilityModifiersForClass(runner.program, x, Lifecycle); + + ok(runner.program.diagnostics.length === 3); + + expectDiagnostics(runner.program.diagnostics, [ + { code: "visibility-sealed", message: "Visibility of property 'x' is sealed and cannot be changed."}, + { code: "visibility-sealed", message: "Visibility of property 'x' is sealed and cannot be changed."}, + { code: "visibility-sealed", message: "Visibility of property 'x' is sealed and cannot be changed."}, + ]); + }); + }); + + describe("visibility modifiers", () => { + it("default visibility modifiers are all modifiers", async () => { + const { Example, Dummy } = await runner.compile(` + @test model Example { + x: string; + } + + @test + @defaultVisibility(Dummy.A) + enum Dummy { + A, + B, + } + `) as { Example: Model, Dummy: Enum }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, Lifecycle.members.size); + for (const member of Lifecycle.members.values()) { + ok(visibility.has(member)); + ok(hasVisibility(runner.program, x, member)) + } + + const dummyVisibility = getVisibilityForClass(runner.program, x, Dummy); + + strictEqual(dummyVisibility.size, 1); + ok(dummyVisibility.has(Dummy.members.get("A")!)); + ok(hasVisibility(runner.program, x, Dummy.members.get("A")!)); + ok(!dummyVisibility.has(Dummy.members.get("B")!)); + ok(!hasVisibility(runner.program, x, Dummy.members.get("B")!)); + }); + + it("adds a visibility modifier", async () => { + const { Example } = await runner.compile(` + @test model Example { + x: string; + } + `) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + const Create = Lifecycle.members.get("Create")!; + + addVisibilityModifiers(runner.program, x, [Create]); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 1); + + for (const member of Lifecycle.members.values()) { + if (member !== Create) { + ok(!visibility.has(member)); + ok(!hasVisibility(runner.program, x, member)); + } else { + ok(visibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } + } + }); + + it("removes a visibility modifier", async () => { + const { Example } = await runner.compile(` + @test model Example { + x: string; + } + `) as { Example: Model }; + + const x = Example.properties.get("x")!; + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + const Create = Lifecycle.members.get("Create")!; + + removeVisibilityModifiers(runner.program, x, [Create]); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, Lifecycle.members.size - 1); + + for (const member of Lifecycle.members.values()) { + if (member !== Create) { + ok(visibility.has(member)); + } else { + ok(!visibility.has(member)); + } + } + }); + + it("clears visibility modifiers for a class", async () => { + const { Example } = await runner.compile(` + @test model Example { + x: string; + } + `) as { Example: Model }; + + const x = Example.properties.get("x")!; + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + clearVisibilityModifiersForClass(runner.program, x, Lifecycle); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 0); + + for (const member of Lifecycle.members.values()) { + ok(!visibility.has(member)); + ok(!hasVisibility(runner.program, x, member)); + } + }); + + it("resets visibility modifiers for a class", async () => { + const { Example } = await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create) + x: string; + } + `) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 1); + ok(visibility.has(Lifecycle.members.get("Create")!)); + ok(hasVisibility(runner.program, x, Lifecycle.members.get("Create")!)); + + resetVisibilityModifiersForClass(runner.program, x, Lifecycle); + + const resetVisibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(resetVisibility.size, 3); + + for (const member of Lifecycle.members.values()) { + ok(resetVisibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } + }); + + it("preserves visibility for other classes", async () => { + const { Example, Dummy } = await runner.compile(` + @test model Example { + x: string; + } + + @test enum Dummy { + A, + B, + } + `) as { Example: Model, Dummy: Enum }; + + const x = Example.properties.get("x")!; + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + clearVisibilityModifiersForClass(runner.program, x, Dummy); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, Lifecycle.members.size); + + for (const member of Lifecycle.members.values()) { + ok(visibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } + }); + }); + + describe("visibility filters", () => { + type LifecycleVisibilityName = "Create" | "Read" | "Update"; + interface VisibilityFilterScenario { + name: string; + expect: boolean; + visibility: Array; + filter: StringVisibilityFilter; + } + + interface StringVisibilityFilter { + all?: LifecycleVisibilityName[]; + any?: LifecycleVisibilityName[]; + none?: LifecycleVisibilityName[]; + } + + const SCENARIOS: VisibilityFilterScenario[] = [ + { + name: "simple property - all - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { all: ["Read"] } + }, + { + name: "simple property - all - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Update"] } + }, + { + name: "simple property - partial all - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Create", "Update"] } + }, + { + name: "unmodified visibility - all - visible", + expect: true, + visibility: [], + filter: { all: ["Create", "Read", "Update"] } + }, + { + name: "simple property - any - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { any: ["Read"] } + }, + { + name: "simple property - partial any - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { any: ["Create", "Update"] } + }, + { + name: "simple property - any - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { any: ["Update"] } + }, + { + name: "simple property - none - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { none: ["Update"] } + }, + { + name: "simple property - none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { none: ["Create"] } + }, + { + name: "simple property - partial none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { none: ["Create", "Update"] } + }, + { + name: "unmodified visibility - none - not visible", + expect: false, + visibility: [], + filter: { none: ["Create"] } + }, + { + name: "simple property - all/any - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { all: ["Read"], any: ["Create"] } + }, + { + name: "simple property - all/any - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Read"], any: ["Update"] } + }, + { + name: "simple property - all/none - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { all: ["Read"], none: ["Update"] } + }, + { + name: "simple property - all/none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Read"], none: ["Create"] } + }, + { + name: "simple property - any/none - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { any: ["Read"], none: ["Update"] } + }, + { + name: "simple property - any/none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { any: ["Read"], none: ["Create"] } + }, + { + name: "simple property - all/any/none - visible", + expect: true, + visibility: ["Create", "Read"], + filter: { all: ["Read"], any: ["Create"], none: ["Update"] } + }, + { + name: "simple property - all/any/none - not visible", + expect: false, + visibility: ["Create", "Read"], + filter: { all: ["Read"], any: ["Create"], none: ["Create"] } + } + ]; + + for (const scenario of SCENARIOS) { + it(scenario.name, async () => { + + const visibilityDecorator = scenario.visibility.length > 0 + ? `@visibility(${scenario.visibility.map(v => `Lifecycle.${v}`).join(", ")})` + : ""; + const { Example } = await runner.compile(` + @test model Example { + ${visibilityDecorator} + x: string; + } + `) as { Example: Model }; + + const x = Example.properties.get("x")!; + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const filter = Object.fromEntries( + Object.entries(scenario.filter).map(([k, vis]) => + [k, new Set((vis as LifecycleVisibilityName[]).map(v => Lifecycle.members.get(v)!))] + ) + ) as VisibilityFilter; + + strictEqual(isVisible(runner.program, x, filter), scenario.expect); + }); + } + + it("mixed visibility classes in filter", async () => { + const { Example, Dummy: DummyEnum } = await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create, Dummy.B) + x: string; + } + + @test + @defaultVisibility(Dummy.A) + enum Dummy { + A, + B, + } + `) as { Example: Model, Dummy: Enum }; + + const x = Example.properties.get("x")!; + const LifecycleEnum = getLifecycleVisibilityEnum(runner.program); + + const Lifecycle = { + Create: LifecycleEnum.members.get("Create")!, + Read: LifecycleEnum.members.get("Read")!, + Update: LifecycleEnum.members.get("Update")! + } + + const Dummy = { + A: DummyEnum.members.get("A")!, + B: DummyEnum.members.get("B")! + } + + strictEqual(isVisible(runner.program, x, { + all: new Set([Lifecycle.Create, Dummy.B]) + }), true); + + strictEqual(isVisible(runner.program, x, { + any: new Set([Dummy.A]) + }), false); + + strictEqual(isVisible(runner.program, x, { + none: new Set([Lifecycle.Update]) + }), true); + + strictEqual(isVisible(runner.program, x, { + all: new Set([Lifecycle.Create]), + none: new Set([Dummy.A]) + }), true); + + strictEqual(isVisible(runner.program, x, { + all: new Set([Lifecycle.Create, Dummy.B]), + none: new Set([Dummy.A]) + }), true); + + strictEqual(isVisible(runner.program, x, { + all: new Set([Lifecycle.Create]), + any: new Set([Dummy.A, Dummy.B]), + none: new Set([Lifecycle.Update]) + }), true); + }); + }); +}); From 114580807a0d328898acdd17aab70a8f90fd5d18 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 30 Oct 2024 14:49:04 -0400 Subject: [PATCH 10/28] Format --- .../compiler/src/core/visibility/index.ts | 10 +- packages/compiler/test/visibility.test.ts | 233 +++++++++++------- 2 files changed, 143 insertions(+), 100 deletions(-) diff --git a/packages/compiler/src/core/visibility/index.ts b/packages/compiler/src/core/visibility/index.ts index 10111cbc5f..97ae71a0f7 100644 --- a/packages/compiler/src/core/visibility/index.ts +++ b/packages/compiler/src/core/visibility/index.ts @@ -5,14 +5,14 @@ export { getLifecycleVisibilityEnum } from "./lifecycle.js"; export { addVisibilityModifiers, - removeVisibilityModifiers, clearVisibilityModifiersForClass, - resetVisibilityModifiersForClass, - sealVisibilityModifiers, - sealVisibilityModifiersForProgram, - isSealed, getVisibility, getVisibilityForClass, hasVisibility, + isSealed, isVisible, + removeVisibilityModifiers, + resetVisibilityModifiersForClass, + sealVisibilityModifiers, + sealVisibilityModifiersForProgram, } from "./core.js"; diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 1be174239c..5a383f05c0 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -1,11 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { strictEqual, ok } from "assert"; +import { ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../src/testing/index.js"; -import { addVisibilityModifiers, clearVisibilityModifiersForClass, Enum, getLifecycleVisibilityEnum, getVisibilityForClass, hasVisibility, isSealed, isVisible, Model, ModelProperty, removeVisibilityModifiers, resetVisibilityModifiersForClass, sealVisibilityModifiers, sealVisibilityModifiersForProgram } from "../src/index.js"; import { VisibilityFilter } from "../src/core/visibility/core.js"; +import { + addVisibilityModifiers, + clearVisibilityModifiersForClass, + Enum, + getLifecycleVisibilityEnum, + getVisibilityForClass, + hasVisibility, + isSealed, + isVisible, + Model, + ModelProperty, + removeVisibilityModifiers, + resetVisibilityModifiersForClass, + sealVisibilityModifiers, + sealVisibilityModifiersForProgram, +} from "../src/index.js"; +import { BasicTestRunner, createTestRunner, expectDiagnostics } from "../src/testing/index.js"; describe("compiler: visibility core", () => { let runner: BasicTestRunner; @@ -15,27 +30,27 @@ describe("compiler: visibility core", () => { }); it("produces correct lifecycle visibility enum reference", async () => { - const { lifecycle } = await runner.compile(` + const { lifecycle } = (await runner.compile(` model X { @test lifecycle: TypeSpec.Lifecycle; } - `) as { lifecycle: ModelProperty }; + `)) as { lifecycle: ModelProperty }; const lifecycleEnum = getLifecycleVisibilityEnum(runner.program); strictEqual(lifecycleEnum, lifecycle.type); - strictEqual(lifecycleEnum, runner.program.resolveTypeReference("TypeSpec.Lifecycle")[0]) + strictEqual(lifecycleEnum, runner.program.resolveTypeReference("TypeSpec.Lifecycle")[0]); }); describe("visibility seals", () => { it("seals visibility modifiers for a program", async () => { - const { Example, Dummy } = await runner.compile(` + const { Example, Dummy } = (await runner.compile(` @test model Example { x: string; } @test enum Dummy {} - `) as { Example: Model, Dummy: Enum }; + `)) as { Example: Model; Dummy: Enum }; const x = Example.properties.get("x")!; @@ -53,13 +68,13 @@ describe("compiler: visibility core", () => { }); it("seals visibility modifiers for a visibility class", async () => { - const { Example, Dummy } = await runner.compile(` + const { Example, Dummy } = (await runner.compile(` @test model Example { x: string; } @test enum Dummy {} - `) as { Example: Model, Dummy: Enum }; + `)) as { Example: Model; Dummy: Enum }; const x = Example.properties.get("x")!; @@ -77,14 +92,14 @@ describe("compiler: visibility core", () => { }); it("seals visibility modifiers for a property", async () => { - const { Example, Dummy } = await runner.compile(` + const { Example, Dummy } = (await runner.compile(` @test model Example { x: string; y: string; } @test enum Dummy {} - `) as { Example: Model, Dummy: Enum }; + `)) as { Example: Model; Dummy: Enum }; const x = Example.properties.get("x")!; const y = Example.properties.get("y")!; @@ -111,11 +126,11 @@ describe("compiler: visibility core", () => { }); it("correctly diagnoses modifying sealed visibility", async () => { - const { Example } = await runner.compile(` + const { Example } = (await runner.compile(` @test model Example { x: string; } - `) as { Example: Model }; + `)) as { Example: Model }; const x = Example.properties.get("x")!; @@ -131,16 +146,25 @@ describe("compiler: visibility core", () => { ok(runner.program.diagnostics.length === 3); expectDiagnostics(runner.program.diagnostics, [ - { code: "visibility-sealed", message: "Visibility of property 'x' is sealed and cannot be changed."}, - { code: "visibility-sealed", message: "Visibility of property 'x' is sealed and cannot be changed."}, - { code: "visibility-sealed", message: "Visibility of property 'x' is sealed and cannot be changed."}, + { + code: "visibility-sealed", + message: "Visibility of property 'x' is sealed and cannot be changed.", + }, + { + code: "visibility-sealed", + message: "Visibility of property 'x' is sealed and cannot be changed.", + }, + { + code: "visibility-sealed", + message: "Visibility of property 'x' is sealed and cannot be changed.", + }, ]); }); }); describe("visibility modifiers", () => { it("default visibility modifiers are all modifiers", async () => { - const { Example, Dummy } = await runner.compile(` + const { Example, Dummy } = (await runner.compile(` @test model Example { x: string; } @@ -151,7 +175,7 @@ describe("compiler: visibility core", () => { A, B, } - `) as { Example: Model, Dummy: Enum }; + `)) as { Example: Model; Dummy: Enum }; const x = Example.properties.get("x")!; @@ -162,7 +186,7 @@ describe("compiler: visibility core", () => { strictEqual(visibility.size, Lifecycle.members.size); for (const member of Lifecycle.members.values()) { ok(visibility.has(member)); - ok(hasVisibility(runner.program, x, member)) + ok(hasVisibility(runner.program, x, member)); } const dummyVisibility = getVisibilityForClass(runner.program, x, Dummy); @@ -175,11 +199,11 @@ describe("compiler: visibility core", () => { }); it("adds a visibility modifier", async () => { - const { Example } = await runner.compile(` + const { Example } = (await runner.compile(` @test model Example { x: string; } - `) as { Example: Model }; + `)) as { Example: Model }; const x = Example.properties.get("x")!; @@ -204,11 +228,11 @@ describe("compiler: visibility core", () => { }); it("removes a visibility modifier", async () => { - const { Example } = await runner.compile(` + const { Example } = (await runner.compile(` @test model Example { x: string; } - `) as { Example: Model }; + `)) as { Example: Model }; const x = Example.properties.get("x")!; const Lifecycle = getLifecycleVisibilityEnum(runner.program); @@ -219,7 +243,7 @@ describe("compiler: visibility core", () => { const visibility = getVisibilityForClass(runner.program, x, Lifecycle); strictEqual(visibility.size, Lifecycle.members.size - 1); - + for (const member of Lifecycle.members.values()) { if (member !== Create) { ok(visibility.has(member)); @@ -230,11 +254,11 @@ describe("compiler: visibility core", () => { }); it("clears visibility modifiers for a class", async () => { - const { Example } = await runner.compile(` + const { Example } = (await runner.compile(` @test model Example { x: string; } - `) as { Example: Model }; + `)) as { Example: Model }; const x = Example.properties.get("x")!; const Lifecycle = getLifecycleVisibilityEnum(runner.program); @@ -252,12 +276,12 @@ describe("compiler: visibility core", () => { }); it("resets visibility modifiers for a class", async () => { - const { Example } = await runner.compile(` + const { Example } = (await runner.compile(` @test model Example { @visibility(Lifecycle.Create) x: string; } - `) as { Example: Model }; + `)) as { Example: Model }; const x = Example.properties.get("x")!; @@ -282,7 +306,7 @@ describe("compiler: visibility core", () => { }); it("preserves visibility for other classes", async () => { - const { Example, Dummy } = await runner.compile(` + const { Example, Dummy } = (await runner.compile(` @test model Example { x: string; } @@ -291,11 +315,11 @@ describe("compiler: visibility core", () => { A, B, } - `) as { Example: Model, Dummy: Enum }; + `)) as { Example: Model; Dummy: Enum }; const x = Example.properties.get("x")!; const Lifecycle = getLifecycleVisibilityEnum(runner.program); - + clearVisibilityModifiersForClass(runner.program, x, Dummy); const visibility = getVisibilityForClass(runner.program, x, Lifecycle); @@ -329,138 +353,139 @@ describe("compiler: visibility core", () => { name: "simple property - all - visible", expect: true, visibility: ["Create", "Read"], - filter: { all: ["Read"] } + filter: { all: ["Read"] }, }, { name: "simple property - all - not visible", expect: false, visibility: ["Create", "Read"], - filter: { all: ["Update"] } + filter: { all: ["Update"] }, }, { name: "simple property - partial all - not visible", expect: false, visibility: ["Create", "Read"], - filter: { all: ["Create", "Update"] } + filter: { all: ["Create", "Update"] }, }, { name: "unmodified visibility - all - visible", expect: true, visibility: [], - filter: { all: ["Create", "Read", "Update"] } + filter: { all: ["Create", "Read", "Update"] }, }, { name: "simple property - any - visible", expect: true, visibility: ["Create", "Read"], - filter: { any: ["Read"] } + filter: { any: ["Read"] }, }, { name: "simple property - partial any - visible", expect: true, visibility: ["Create", "Read"], - filter: { any: ["Create", "Update"] } + filter: { any: ["Create", "Update"] }, }, { name: "simple property - any - not visible", expect: false, visibility: ["Create", "Read"], - filter: { any: ["Update"] } + filter: { any: ["Update"] }, }, { name: "simple property - none - visible", expect: true, visibility: ["Create", "Read"], - filter: { none: ["Update"] } + filter: { none: ["Update"] }, }, { name: "simple property - none - not visible", expect: false, visibility: ["Create", "Read"], - filter: { none: ["Create"] } + filter: { none: ["Create"] }, }, { name: "simple property - partial none - not visible", expect: false, visibility: ["Create", "Read"], - filter: { none: ["Create", "Update"] } + filter: { none: ["Create", "Update"] }, }, { name: "unmodified visibility - none - not visible", expect: false, visibility: [], - filter: { none: ["Create"] } + filter: { none: ["Create"] }, }, { name: "simple property - all/any - visible", expect: true, visibility: ["Create", "Read"], - filter: { all: ["Read"], any: ["Create"] } + filter: { all: ["Read"], any: ["Create"] }, }, { name: "simple property - all/any - not visible", expect: false, visibility: ["Create", "Read"], - filter: { all: ["Read"], any: ["Update"] } + filter: { all: ["Read"], any: ["Update"] }, }, { name: "simple property - all/none - visible", expect: true, visibility: ["Create", "Read"], - filter: { all: ["Read"], none: ["Update"] } + filter: { all: ["Read"], none: ["Update"] }, }, { name: "simple property - all/none - not visible", expect: false, visibility: ["Create", "Read"], - filter: { all: ["Read"], none: ["Create"] } + filter: { all: ["Read"], none: ["Create"] }, }, { name: "simple property - any/none - visible", expect: true, visibility: ["Create", "Read"], - filter: { any: ["Read"], none: ["Update"] } + filter: { any: ["Read"], none: ["Update"] }, }, { name: "simple property - any/none - not visible", expect: false, visibility: ["Create", "Read"], - filter: { any: ["Read"], none: ["Create"] } + filter: { any: ["Read"], none: ["Create"] }, }, { name: "simple property - all/any/none - visible", expect: true, visibility: ["Create", "Read"], - filter: { all: ["Read"], any: ["Create"], none: ["Update"] } + filter: { all: ["Read"], any: ["Create"], none: ["Update"] }, }, { name: "simple property - all/any/none - not visible", expect: false, visibility: ["Create", "Read"], - filter: { all: ["Read"], any: ["Create"], none: ["Create"] } - } + filter: { all: ["Read"], any: ["Create"], none: ["Create"] }, + }, ]; for (const scenario of SCENARIOS) { it(scenario.name, async () => { - - const visibilityDecorator = scenario.visibility.length > 0 - ? `@visibility(${scenario.visibility.map(v => `Lifecycle.${v}`).join(", ")})` - : ""; - const { Example } = await runner.compile(` + const visibilityDecorator = + scenario.visibility.length > 0 + ? `@visibility(${scenario.visibility.map((v) => `Lifecycle.${v}`).join(", ")})` + : ""; + const { Example } = (await runner.compile(` @test model Example { ${visibilityDecorator} x: string; } - `) as { Example: Model }; + `)) as { Example: Model }; const x = Example.properties.get("x")!; const Lifecycle = getLifecycleVisibilityEnum(runner.program); const filter = Object.fromEntries( - Object.entries(scenario.filter).map(([k, vis]) => - [k, new Set((vis as LifecycleVisibilityName[]).map(v => Lifecycle.members.get(v)!))] - ) + Object.entries(scenario.filter).map(([k, vis]) => [ + k, + new Set((vis as LifecycleVisibilityName[]).map((v) => Lifecycle.members.get(v)!)), + ]), ) as VisibilityFilter; strictEqual(isVisible(runner.program, x, filter), scenario.expect); @@ -468,7 +493,7 @@ describe("compiler: visibility core", () => { } it("mixed visibility classes in filter", async () => { - const { Example, Dummy: DummyEnum } = await runner.compile(` + const { Example, Dummy: DummyEnum } = (await runner.compile(` @test model Example { @visibility(Lifecycle.Create, Dummy.B) x: string; @@ -480,7 +505,7 @@ describe("compiler: visibility core", () => { A, B, } - `) as { Example: Model, Dummy: Enum }; + `)) as { Example: Model; Dummy: Enum }; const x = Example.properties.get("x")!; const LifecycleEnum = getLifecycleVisibilityEnum(runner.program); @@ -488,41 +513,59 @@ describe("compiler: visibility core", () => { const Lifecycle = { Create: LifecycleEnum.members.get("Create")!, Read: LifecycleEnum.members.get("Read")!, - Update: LifecycleEnum.members.get("Update")! - } + Update: LifecycleEnum.members.get("Update")!, + }; const Dummy = { A: DummyEnum.members.get("A")!, - B: DummyEnum.members.get("B")! - } - - strictEqual(isVisible(runner.program, x, { - all: new Set([Lifecycle.Create, Dummy.B]) - }), true); - - strictEqual(isVisible(runner.program, x, { - any: new Set([Dummy.A]) - }), false); - - strictEqual(isVisible(runner.program, x, { - none: new Set([Lifecycle.Update]) - }), true); - - strictEqual(isVisible(runner.program, x, { - all: new Set([Lifecycle.Create]), - none: new Set([Dummy.A]) - }), true); - - strictEqual(isVisible(runner.program, x, { - all: new Set([Lifecycle.Create, Dummy.B]), - none: new Set([Dummy.A]) - }), true); - - strictEqual(isVisible(runner.program, x, { - all: new Set([Lifecycle.Create]), - any: new Set([Dummy.A, Dummy.B]), - none: new Set([Lifecycle.Update]) - }), true); + B: DummyEnum.members.get("B")!, + }; + + strictEqual( + isVisible(runner.program, x, { + all: new Set([Lifecycle.Create, Dummy.B]), + }), + true, + ); + + strictEqual( + isVisible(runner.program, x, { + any: new Set([Dummy.A]), + }), + false, + ); + + strictEqual( + isVisible(runner.program, x, { + none: new Set([Lifecycle.Update]), + }), + true, + ); + + strictEqual( + isVisible(runner.program, x, { + all: new Set([Lifecycle.Create]), + none: new Set([Dummy.A]), + }), + true, + ); + + strictEqual( + isVisible(runner.program, x, { + all: new Set([Lifecycle.Create, Dummy.B]), + none: new Set([Dummy.A]), + }), + true, + ); + + strictEqual( + isVisible(runner.program, x, { + all: new Set([Lifecycle.Create]), + any: new Set([Dummy.A, Dummy.B]), + none: new Set([Lifecycle.Update]), + }), + true, + ); }); }); }); From f6930b131fd466514abfc7065897645eab2cea70 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 30 Oct 2024 15:08:17 -0400 Subject: [PATCH 11/28] Test legacy visibility string coercion in core APIs. --- packages/compiler/test/visibility.test.ts | 83 ++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 5a383f05c0..6828c35970 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { ok, strictEqual } from "assert"; +import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { VisibilityFilter } from "../src/core/visibility/core.js"; +import { getVisibility, VisibilityFilter } from "../src/core/visibility/core.js"; import { addVisibilityModifiers, clearVisibilityModifiersForClass, @@ -568,4 +568,83 @@ describe("compiler: visibility core", () => { ); }); }); + + describe("legacy compatibility", () => { + it("converts legacy visibility strings to modifiers", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility("create") + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 1); + + for (const member of Lifecycle.members.values()) { + if (member.name === "Create") { + ok(visibility.has(member)); + ok(hasVisibility(runner.program, x, member)); + } else { + ok(!visibility.has(member)); + ok(!hasVisibility(runner.program, x, member)); + } + } + }); + + it("isVisible correctly coerces legacy visibility modifiers", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create, Lifecycle.Update) + x: string; + y: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + ok(isVisible(runner.program, x, ["create"])); + ok(isVisible(runner.program, x, ["update"])); + ok(!isVisible(runner.program, x, ["read"])); + + const y = Example.properties.get("y")!; + + ok(isVisible(runner.program, y, ["create", "update"])); + ok(isVisible(runner.program, y, ["read"])); + }); + + it("getVisibility correctly coerces visibility modifiers", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility(Lifecycle.Create, Lifecycle.Update) + x: string; + y: string; + @invisible(Lifecycle) + z: string; + @visibility(Lifecycle.Create, Lifecycle.Update, Lifecycle.Read) + a: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + const y = Example.properties.get("y")!; + const z = Example.properties.get("z")!; + const a = Example.properties.get("a")!; + + const xVisibility = getVisibility(runner.program, x); + const yVisibility = getVisibility(runner.program, y); + const zVisibility = getVisibility(runner.program, z); + const aVisibility = getVisibility(runner.program, a); + + deepStrictEqual(xVisibility, ["create", "update"]); + strictEqual(yVisibility, undefined); + deepStrictEqual(zVisibility, ["none"]); + deepStrictEqual(aVisibility, ["create", "update", "read"]); + }); + }); }); From b26a77e1c0690f2b76016755399f978724ac9322 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 30 Oct 2024 15:51:12 -0400 Subject: [PATCH 12/28] Fix an issue with recursive transforms --- packages/compiler/generated-defs/TypeSpec.ts-test.ts | 2 +- packages/compiler/src/lib/visibility.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/compiler/generated-defs/TypeSpec.ts-test.ts b/packages/compiler/generated-defs/TypeSpec.ts-test.ts index f7b58a3be3..a5eb47202e 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts-test.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts-test.ts @@ -1,5 +1,5 @@ /** An error here would mean that the decorator is not exported or doesn't have the right name. */ -import { $decorators } from "../src/core/index.js"; +import { $decorators } from "../src/index.js"; import type { TypeSpecDecorators } from "./TypeSpec.js"; /** An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ... */ const _: TypeSpecDecorators = $decorators["TypeSpec"]; diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index e536efa348..54de8a0f8b 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -390,16 +390,17 @@ export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( Model: { mutate: (model, clone, program, realm) => { for (const [key, prop] of model.properties) { + const cloneProperty = clone.properties.get(key)!; if (!isVisible(program, prop, lifecycleUpdate)) { clone.properties.delete(key); realm.remove(prop); } else if (prop.type.kind === "Model") { const { type } = mutateSubgraph(program, [createOrUpdateMutator], prop.type); - prop.type = type; + cloneProperty.type = type; } - resetVisibilityModifiersForClass(program, prop, lifecycle); + resetVisibilityModifiersForClass(program, cloneProperty, lifecycle); } clone.decorators = clone.decorators.filter((d) => d.decorator !== $withLifecycleUpdate); @@ -420,17 +421,18 @@ function createVisibilityFilterMutator(filter: VisibilityFilter): Mutator { Model: { mutate: (model, clone, program, realm) => { for (const [key, prop] of model.properties) { + const cloneProperty = clone.properties.get(key)!; if (!isVisible(program, prop, filter)) { clone.properties.delete(key); realm.remove(prop); } else if (prop.type.kind === "Model") { const { type } = mutateSubgraph(program, [self], prop.type); - prop.type = type; + cloneProperty.type = type; } for (const visibilityClass of VisibilityFilter.getVisibilityClasses(filter)) { - resetVisibilityModifiersForClass(program, prop, visibilityClass); + resetVisibilityModifiersForClass(program, cloneProperty, visibilityClass); } } From 5b1705c61224d6dd7e7a9fcd1616accb28806a14 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 31 Oct 2024 14:48:31 -0400 Subject: [PATCH 13/28] Fix issue with module instancing for tests, allowing running mutators in tests --- packages/compiler/src/core/visibility/core.ts | 10 +- .../src/experimental/typekit/define-kit.ts | 6 +- packages/compiler/src/lib/visibility.ts | 6 -- packages/compiler/test/visibility.test.ts | 97 +++++++++++++++++++ 4 files changed, 108 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index a943e239a8..4271913b73 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -250,7 +250,8 @@ export function clearLegacyVisibility(program: Program, property: ModelProperty) * - Otherwise, this function will return an array of lowercase strings representing the active Lifecycle visibility * modifiers ("create", "read", "update"). * - * @deprecated Use `getVisibilityForClass` or `getLifecycleVisibility` instead. + * @deprecated Use `getVisibilityForClass` instead. + * * @param program - the program in which the property occurs * @param property - the property to get legacy visibility modifiers for */ @@ -580,7 +581,12 @@ export function isVisible( /** * Determines if a property has any of the specified (legacy) visibility strings. * - * @deprecated This call signature is deprecated. Use the `VisibilityFilter` version instead. + * @deprecated Calling `isVisible` with an array of legacy visibility strings is deprecated. Use a `VisibilityFilter` + * object instead. + * + * @param program - the program in which the property occurs + * @param property - the property to check + * @param visibilities - the visibility strings to check for */ export function isVisible( program: Program, diff --git a/packages/compiler/src/experimental/typekit/define-kit.ts b/packages/compiler/src/experimental/typekit/define-kit.ts index 1f1711464a..47b21eec65 100644 --- a/packages/compiler/src/experimental/typekit/define-kit.ts +++ b/packages/compiler/src/experimental/typekit/define-kit.ts @@ -1,10 +1,10 @@ import { type Program } from "../../core/program.js"; -let currentProgram: Program | undefined; +const CURRENT_PROGRAM = Symbol.for("TypeSpec.currentProgram"); /** @experimental */ export function setCurrentProgram(program: Program): void { - currentProgram = program; + (globalThis as any)[CURRENT_PROGRAM] = program; } /** @experimental */ @@ -21,7 +21,7 @@ export function createTypekit(): TypekitPrototype { Object.defineProperty(tk, "program", { get() { - return currentProgram; + return (globalThis as any)[CURRENT_PROGRAM]; }, }); diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 54de8a0f8b..0eb2864500 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -165,8 +165,6 @@ export const $parameterVisibility: ParameterVisibilityDecorator = ( /** * Returns the visibilities of the parameters of the given operation, if provided with `@parameterVisibility`. * - * @deprecated Use {@link getParameterVisibilityFilter} instead. - * * @see {@link $parameterVisibility} */ export function getParameterVisibility(program: Program, entity: Operation): string[] | undefined { @@ -205,8 +203,6 @@ export const $returnTypeVisibility: ReturnTypeVisibilityDecorator = ( /** * Returns the visibilities of the return type of the given operation, if provided with `@returnTypeVisibility`. * - * @deprecated Use {@link getReturnTypeVisibilityFilter} instead. - * * @see {@link $returnTypeVisibility} */ export function getReturnTypeVisibility(program: Program, entity: Operation): string[] | undefined { @@ -337,8 +333,6 @@ export const $withVisibility: WithVisibilityDecorator = ( /** * Filters a model for properties that are updateable. * - * @deprecated Use `@withVisibilityFilter` or `@withLifecycleVisibility` instead. - * * @param context - the program context * @param target - Model to filter for updateable properties */ diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 6828c35970..40ec5de945 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -647,4 +647,101 @@ describe("compiler: visibility core", () => { deepStrictEqual(aVisibility, ["create", "update", "read"]); }); }); + + describe("lifecycle transforms", () => { + async function compileWithTransform( + transform: "Create" | "Read" | "Update" | "CreateOrUpdate", + ) { + const { Result } = (await runner.compile(` + model Example { + @visibility(Lifecycle.Read) + r: string; + + cru: string; + + @visibility(Lifecycle.Create, Lifecycle.Read) + cr: string; + + @visibility(Lifecycle.Create, Lifecycle.Update) + cu: string; + + @visibility(Lifecycle.Create) + c: string; + + @visibility(Lifecycle.Update, Lifecycle.Read) + ru: string; + + @visibility(Lifecycle.Update) + u: string; + + @invisible(Lifecycle) + invisible: string; + + @visibility(Lifecycle.Update) + nested: { + @visibility(Lifecycle.Read) + r: string; + + cru: string; + + @visibility(Lifecycle.Create, Lifecycle.Read) + cr: string; + + @visibility(Lifecycle.Create, Lifecycle.Update) + cu: string; + + @visibility(Lifecycle.Create) + c: string; + + @visibility(Lifecycle.Update, Lifecycle.Read) + ru: string; + + @visibility(Lifecycle.Update) + u: string; + + @invisible(Lifecycle) + invisible: string; + }; + } + + @test model Result is ${transform}; + `)) as { Result: Model }; + + return Result; + } + + function getProperties(model: Model) { + return { + c: model.properties.get("c"), + cr: model.properties.get("cr"), + cu: model.properties.get("cu"), + cru: model.properties.get("cru"), + r: model.properties.get("r"), + ru: model.properties.get("ru"), + u: model.properties.get("u"), + invisible: model.properties.get("invisible"), + }; + } + + it("correctly applies Read transform", async () => { + const Result = await compileWithTransform("Read"); + const props = getProperties(Result); + + // All properties that do not have Read visibility are removed + strictEqual(props.c, undefined); + strictEqual(props.cu, undefined); + strictEqual(props.u, undefined); + strictEqual(props.invisible, undefined); + + // All properties that have Read visibility are preserved + ok(props.r); + ok(props.cr); + ok(props.cru); + ok(props.ru); + + const nested = Result.properties.get("nested"); + + strictEqual(nested, undefined); + }); + }); }); From 37a2941887b96f5ddafb01e71d9b917d655482b3 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 1 Nov 2024 00:24:29 -0400 Subject: [PATCH 14/28] Actually fix visibility transform mutators and consolidate implementation. --- .../compiler/.scripts/gen-extern-signature.ts | 2 +- packages/compiler/generated-defs/TypeSpec.ts | 26 +++ packages/compiler/lib/std/visibility.tsp | 22 +++ packages/compiler/src/core/visibility/core.ts | 12 ++ packages/compiler/src/lib/tsp-index.ts | 2 + packages/compiler/src/lib/visibility.ts | 163 ++++++++++++++---- packages/compiler/test/visibility.test.ts | 128 +++++++++++++- 7 files changed, 315 insertions(+), 40 deletions(-) diff --git a/packages/compiler/.scripts/gen-extern-signature.ts b/packages/compiler/.scripts/gen-extern-signature.ts index f54907f75f..b01d6795c4 100644 --- a/packages/compiler/.scripts/gen-extern-signature.ts +++ b/packages/compiler/.scripts/gen-extern-signature.ts @@ -17,7 +17,7 @@ const files = await generateExternDecorators(program, "@typespec/compiler"); for (const [name, content] of Object.entries(files)) { const updatedContent = content.replace( /from "\@typespec\/compiler"/g, - `from "../src/core/index.js"`, + name === "TypeSpec.ts-test.ts" ? `from "../src/index.js"` : `from "../src/core/index.js"`, ); const prettierConfig = await resolveConfig(root); diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index f74d77c078..c4206c0dce 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -720,6 +720,31 @@ export type InvisibleDecorator = ( visibilityClass: Enum, ) => void; +/** + * Removes visibility modifiers from a property. + * + * If the visibility modifiers for a visibility class have not been initialized, + * this decorator will use the default visibility modifiers for the visibility + * class as the default modifier set. + * + * @param target The property to remove visibility from. + * @param visibilities The visibility modifiers to remove from the target property. + * @example + * ```typespec + * model Example { + * // This property will have the Create and Update visibilities, but not the + * // Read visibility, since it is removed. + * @removeVisibility(Lifecycle.Read) + * secret_property: string; + * } + * ``` + */ +export type RemoveVisibilityDecorator = ( + context: DecoratorContext, + target: ModelProperty, + ...visibilities: EnumValue[] +) => void; + /** * Removes properties that are not considered to be present or applicable * ("visible") in the given named contexts ("visibilities"). Can be used @@ -898,6 +923,7 @@ export type TypeSpecDecorators = { inspectTypeName: InspectTypeNameDecorator; visibility: VisibilityDecorator; invisible: InvisibleDecorator; + removeVisibility: RemoveVisibilityDecorator; withVisibility: WithVisibilityDecorator; parameterVisibility: ParameterVisibilityDecorator; returnTypeVisibility: ReturnTypeVisibilityDecorator; diff --git a/packages/compiler/lib/std/visibility.tsp b/packages/compiler/lib/std/visibility.tsp index 861d896337..c5ab66101e 100644 --- a/packages/compiler/lib/std/visibility.tsp +++ b/packages/compiler/lib/std/visibility.tsp @@ -59,6 +59,28 @@ extern dec visibility(target: ModelProperty, ...visibilities: valueof (string | */ extern dec invisible(target: ModelProperty, visibilityClass: Enum); +/** + * Removes visibility modifiers from a property. + * + * If the visibility modifiers for a visibility class have not been initialized, + * this decorator will use the default visibility modifiers for the visibility + * class as the default modifier set. + * + * @param target The property to remove visibility from. + * @param visibilities The visibility modifiers to remove from the target property. + * + * @example + * ```typespec + * model Example { + * // This property will have the Create and Update visibilities, but not the + * // Read visibility, since it is removed. + * @removeVisibility(Lifecycle.Read) + * secret_property: string; + * } + * ``` + */ +extern dec removeVisibility(target: ModelProperty, ...visibilities: valueof EnumMember[]); + /** * Removes properties that are not considered to be present or applicable * ("visible") in the given named contexts ("visibilities"). Can be used diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index 4271913b73..730e6c1c13 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -547,6 +547,11 @@ export interface VisibilityFilter { } export const VisibilityFilter = { + /** + * Convert a TypeSpec `GeneratedVisibilityFilter` value to a `VisibilityFilter`. + * @param filter - the decorator argument filter to convert + * @returns a `VisibilityFilter` object that can be consumed by the visibility APIs + */ fromDecoratorArgument(filter: GeneratedVisibilityFilter): VisibilityFilter { return { all: filter.all && new Set(filter.all.map((v) => v.value)), @@ -554,6 +559,13 @@ export const VisibilityFilter = { none: filter.none && new Set(filter.none.map((v) => v.value)), }; }, + /** + * Extracts the unique visibility classes referred to by the modifiers in a + * visibility filter. + * + * @param filter - the visibility filter to extract visibility classes from + * @returns a set of visibility classes referred to by the filter + */ getVisibilityClasses(filter: VisibilityFilter): Set { const classes = new Set(); if (filter.all) filter.all.forEach((v) => classes.add(v.enum)); diff --git a/packages/compiler/src/lib/tsp-index.ts b/packages/compiler/src/lib/tsp-index.ts index dbc1d21061..0f1cc4ecb0 100644 --- a/packages/compiler/src/lib/tsp-index.ts +++ b/packages/compiler/src/lib/tsp-index.ts @@ -41,6 +41,7 @@ import { $defaultVisibility, $invisible, $parameterVisibility, + $removeVisibility, $returnTypeVisibility, $visibility, $withDefaultKeyVisibility, @@ -91,6 +92,7 @@ export const $decorators = { inspectType: $inspectType, inspectTypeName: $inspectTypeName, visibility: $visibility, + removeVisibility: $removeVisibility, invisible: $invisible, defaultVisibility: $defaultVisibility, withVisibility: $withVisibility, diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 0eb2864500..c3eb6d556b 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -5,6 +5,7 @@ import type { DefaultVisibilityDecorator, InvisibleDecorator, ParameterVisibilityDecorator, + RemoveVisibilityDecorator, ReturnTypeVisibilityDecorator, VisibilityDecorator, WithDefaultKeyVisibilityDecorator, @@ -17,7 +18,9 @@ import { validateDecoratorTarget, validateDecoratorUniqueOnNode } from "../core/ import { reportDiagnostic } from "../core/messages.js"; import type { Program } from "../core/program.js"; import { + DecoratorApplication, DecoratorContext, + DecoratorFunction, Enum, EnumMember, EnumValue, @@ -33,6 +36,7 @@ import { GeneratedVisibilityFilter, getVisibility, isVisible, + removeVisibilityModifiers, resetVisibilityModifiersForClass, setDefaultModifierSetForVisibilityClass, setLegacyVisibility, @@ -43,6 +47,7 @@ import { normalizeVisibilityToLegacyLifecycleString, } from "../core/visibility/lifecycle.js"; import { mutateSubgraph, Mutator, MutatorFlow } from "../experimental/mutators.js"; +import { $ } from "../experimental/typekit/index.js"; import { isKey } from "./key.js"; import { filterModelPropertiesInPlace, useStateMap } from "./utils.js"; @@ -213,6 +218,10 @@ export function getReturnTypeVisibility(program: Program, entity: Operation): st .filter((p) => !!p) as string[]; } +// #endregion + +// #region Core Visibility Decorators + // -- @visibility decorator --------------------- export const $visibility: VisibilityDecorator = ( @@ -242,6 +251,20 @@ export const $visibility: VisibilityDecorator = ( } }; +// -- @removeVisibility decorator --------------------- + +export const $removeVisibility: RemoveVisibilityDecorator = ( + context: DecoratorContext, + target: ModelProperty, + ...visibilities: EnumValue[] +) => { + removeVisibilityModifiers( + context.program, + target, + visibilities.map((v) => v.value), + ); +}; + // -- @invisible decorator --------------------- export const $invisible: InvisibleDecorator = ( @@ -277,6 +300,10 @@ export const $defaultVisibility: DefaultVisibilityDecorator = ( setDefaultModifierSetForVisibilityClass(context.program, target, modifierSet); }; +// #endregion + +// #region Legacy Visibility Transforms + // -- @withVisibility decorator --------------------- export const $withVisibility: WithVisibilityDecorator = ( @@ -348,6 +375,10 @@ export const $withUpdateableProperties: WithUpdateablePropertiesDecorator = ( filterModelPropertiesInPlace(target, (p) => isVisible(context.program, p, ["update"])); }; +// #endregion + +// #region Mutator Driven Transforms + // -- @withVisibilityFilter decorator ---------------------- export const $withVisibilityFilter: WithVisibilityFilterDecorator = ( @@ -357,7 +388,9 @@ export const $withVisibilityFilter: WithVisibilityFilterDecorator = ( ) => { const filter = VisibilityFilter.fromDecoratorArgument(_filter); - const vfMutator: Mutator = createVisibilityFilterMutator(filter); + const vfMutator: Mutator = createVisibilityFilterMutator(filter, { + decoratorFn: $withVisibilityFilter, + }); const { type } = mutateSubgraph(context.program, [vfMutator], target); @@ -375,68 +408,124 @@ export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( all: new Set([lifecycle.members.get("Update")!]), }; - const createOrUpdateMutator = createVisibilityFilterMutator({ + const lifecycleCreateOrUpdate: VisibilityFilter = { any: new Set([lifecycle.members.get("Create")!, lifecycle.members.get("Update")!]), - }); - - const updateMutator: Mutator = { - name: "LifecycleUpdate", - Model: { - mutate: (model, clone, program, realm) => { - for (const [key, prop] of model.properties) { - const cloneProperty = clone.properties.get(key)!; - if (!isVisible(program, prop, lifecycleUpdate)) { - clone.properties.delete(key); - realm.remove(prop); - } else if (prop.type.kind === "Model") { - const { type } = mutateSubgraph(program, [createOrUpdateMutator], prop.type); + }; - cloneProperty.type = type; - } + const createOrUpdateMutator = createVisibilityFilterMutator(lifecycleCreateOrUpdate); - resetVisibilityModifiersForClass(program, cloneProperty, lifecycle); - } - - clone.decorators = clone.decorators.filter((d) => d.decorator !== $withLifecycleUpdate); + const updateMutator = createVisibilityFilterMutator(lifecycleUpdate, { + recur: createOrUpdateMutator, + decoratorFn: $withLifecycleUpdate, + }); - return MutatorFlow.DoNotRecurse; - }, - }, - }; + // const updateMutator: Mutator = { + // name: "LifecycleUpdate", + // Model: { + // filter: () => MutatorFlow.DoNotRecurse, + // mutate: (model, clone, program, realm) => { + // for (const [key, prop] of model.properties) { + // const cloneProperty = clone.properties.get(key)!; + // if (!isVisible(program, prop, lifecycleUpdate)) { + // clone.properties.delete(key); + // realm.remove(prop); + // } else if (cloneProperty.type.kind === "Model") { + // const { type } = mutateSubgraph(program, [createOrUpdateMutator], cloneProperty.type); + + // cloneProperty.type = type; + // } + + // resetVisibilityModifiersForClass(program, cloneProperty, lifecycle); + // } + + // clone.decorators = clone.decorators.filter((d) => d.decorator !== $withLifecycleUpdate); + // }, + // }, + // }; const { type } = mutateSubgraph(context.program, [updateMutator], target); target.properties = (type as Model).properties; }; -function createVisibilityFilterMutator(filter: VisibilityFilter): Mutator { +/** + * Options for the `createVisibilityFilterMutator` function. + */ +interface CreateVisibilityFilterMutatorOptions { + /** + * A mutator to apply to the type of visible properties of the model. If not provided, applies the constructed + * visibility filter mutator recursively. + */ + recur?: Mutator; + + /** + * Optionally, a decorator function to remove from the model's decorators, if present. + */ + decoratorFn?: DecoratorFunction; +} + +function createVisibilityFilterMutator( + filter: VisibilityFilter, + options: CreateVisibilityFilterMutatorOptions = {}, +): Mutator { + const visibilityClasses = VisibilityFilter.getVisibilityClasses(filter); const self: Mutator = { name: "VisibilityFilter", Model: { + filter: () => MutatorFlow.DoNotRecurse, mutate: (model, clone, program, realm) => { for (const [key, prop] of model.properties) { - const cloneProperty = clone.properties.get(key)!; if (!isVisible(program, prop, filter)) { clone.properties.delete(key); realm.remove(prop); } else if (prop.type.kind === "Model") { - const { type } = mutateSubgraph(program, [self], prop.type); - - cloneProperty.type = type; - } - - for (const visibilityClass of VisibilityFilter.getVisibilityClasses(filter)) { - resetVisibilityModifiersForClass(program, cloneProperty, visibilityClass); + const { type } = mutateSubgraph(program, [options.recur ?? self], prop.type); + + const clonedProperty = $.type.clone(prop); + + clonedProperty.type = type; + + const decorators = clonedProperty.decorators; + const decoratorsToRemove = new Set(); + + for (const decorator of decorators) { + const decFn = decorator.decorator; + if (decFn === $visibility || decFn === $removeVisibility) { + decorator.args = decorator.args.filter( + (arg) => + !( + arg.value.entityKind === "Value" && + arg.value.valueKind === "EnumValue" && + visibilityClasses.has(arg.value.value.enum) + ), + ); + + if (decorator.args.length === 0) { + decoratorsToRemove.add(decorator); + } + } else if (decFn === $invisible) { + decoratorsToRemove.add(decorator); + } + } + + clonedProperty.decorators = clonedProperty.decorators.filter( + (d) => !decoratorsToRemove.has(d), + ); + + $.type.finishType(clonedProperty); + + clone.properties.set(key, clonedProperty); } } - clone.decorators = clone.decorators.filter((d) => d.decorator !== $withVisibilityFilter); - - return MutatorFlow.DoNotRecurse; + if (options.decoratorFn) { + clone.decorators = clone.decorators.filter((d) => d.decorator !== options.decoratorFn); + } }, }, }; return self; } + // #endregion diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 40ec5de945..229cfa3ee6 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -677,7 +677,6 @@ describe("compiler: visibility core", () => { @invisible(Lifecycle) invisible: string; - @visibility(Lifecycle.Update) nested: { @visibility(Lifecycle.Read) r: string; @@ -704,6 +703,8 @@ describe("compiler: visibility core", () => { }; } + model ReadExample is Read; + @test model Result is ${transform}; `)) as { Result: Model }; @@ -731,6 +732,7 @@ describe("compiler: visibility core", () => { strictEqual(props.c, undefined); strictEqual(props.cu, undefined); strictEqual(props.u, undefined); + strictEqual(props.invisible, undefined); // All properties that have Read visibility are preserved @@ -741,7 +743,129 @@ describe("compiler: visibility core", () => { const nested = Result.properties.get("nested"); - strictEqual(nested, undefined); + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.c, undefined); + strictEqual(nestedProps.cu, undefined); + strictEqual(nestedProps.u, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.r); + ok(nestedProps.cr); + ok(nestedProps.cru); + ok(nestedProps.ru); + }); + + it("correctly applies Create transform", async () => { + const Result = await compileWithTransform("Create"); + const props = getProperties(Result); + + // Properties without Create visibility are removed + strictEqual(props.r, undefined); + strictEqual(props.ru, undefined); + strictEqual(props.u, undefined); + + strictEqual(props.invisible, undefined); + + ok(props.c); + ok(props.cr); + ok(props.cu); + ok(props.cru); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + strictEqual(nestedProps.ru, undefined); + strictEqual(nestedProps.u, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); + }); + + it("correctly applies Update transform", async () => { + const Result = await compileWithTransform("Update"); + const props = getProperties(Result); + + // Properties without Update visibility are removed + strictEqual(props.r, undefined); + strictEqual(props.c, undefined); + strictEqual(props.cr, undefined); + + strictEqual(props.invisible, undefined); + + ok(props.cu); + ok(props.cru); + ok(props.ru); + ok(props.u); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + // Nested properties work differently in Lifecycle Update transforms, requiring nested create-only properties to + // additionally be visible + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); + ok(nestedProps.ru); + ok(nestedProps.u); + }); + + it("correctly applies CreateOrUpdate transform", async () => { + const Result = await compileWithTransform("CreateOrUpdate"); + const props = getProperties(Result); + + // Properties that only have read visibility are removed + strictEqual(props.r, undefined); + + strictEqual(props.invisible, undefined); + + // All other visible properties are preserved + ok(props.c); + ok(props.cr); + ok(props.cu); + ok(props.cru); + ok(props.ru); + ok(props.u); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); + ok(nestedProps.ru); + ok(nestedProps.u); + + strictEqual(nestedProps.invisible, undefined); }); }); }); From 8f3cce509097d806fc3017f9312a5a9a10a385ec Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 1 Nov 2024 13:03:21 -0400 Subject: [PATCH 15/28] Separate mutators into model/property --- packages/compiler/src/lib/visibility.ts | 107 +++++++++------------- packages/compiler/test/visibility.test.ts | 31 ++++++- 2 files changed, 75 insertions(+), 63 deletions(-) diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index c3eb6d556b..54939e5194 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -47,7 +47,6 @@ import { normalizeVisibilityToLegacyLifecycleString, } from "../core/visibility/lifecycle.js"; import { mutateSubgraph, Mutator, MutatorFlow } from "../experimental/mutators.js"; -import { $ } from "../experimental/typekit/index.js"; import { isKey } from "./key.js"; import { filterModelPropertiesInPlace, useStateMap } from "./utils.js"; @@ -419,30 +418,6 @@ export const $withLifecycleUpdate: WithLifecycleUpdateDecorator = ( decoratorFn: $withLifecycleUpdate, }); - // const updateMutator: Mutator = { - // name: "LifecycleUpdate", - // Model: { - // filter: () => MutatorFlow.DoNotRecurse, - // mutate: (model, clone, program, realm) => { - // for (const [key, prop] of model.properties) { - // const cloneProperty = clone.properties.get(key)!; - // if (!isVisible(program, prop, lifecycleUpdate)) { - // clone.properties.delete(key); - // realm.remove(prop); - // } else if (cloneProperty.type.kind === "Model") { - // const { type } = mutateSubgraph(program, [createOrUpdateMutator], cloneProperty.type); - - // cloneProperty.type = type; - // } - - // resetVisibilityModifiersForClass(program, cloneProperty, lifecycle); - // } - - // clone.decorators = clone.decorators.filter((d) => d.decorator !== $withLifecycleUpdate); - // }, - // }, - // }; - const { type } = mutateSubgraph(context.program, [updateMutator], target); target.properties = (type as Model).properties; @@ -469,6 +444,46 @@ function createVisibilityFilterMutator( options: CreateVisibilityFilterMutatorOptions = {}, ): Mutator { const visibilityClasses = VisibilityFilter.getVisibilityClasses(filter); + const mpMutator: Mutator = { + name: "VisibilityFilterProperty", + ModelProperty: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (prop, clone, program) => { + const decorators = prop.decorators; + const decoratorsToRemove = new Set(); + + for (const decorator of decorators) { + const decFn = decorator.decorator; + if (decFn === $visibility || decFn === $removeVisibility) { + decorator.args = decorator.args.filter( + (arg) => + !( + arg.value.entityKind === "Value" && + arg.value.valueKind === "EnumValue" && + visibilityClasses.has(arg.value.value.enum) + ), + ); + + if (decorator.args.length === 0) { + decoratorsToRemove.add(decorator); + } + } else if (decFn === $invisible) { + decoratorsToRemove.add(decorator); + } + } + + for (const visibilityClass of visibilityClasses) { + resetVisibilityModifiersForClass(program, clone, visibilityClass); + } + + if (prop.type.kind === "Model") { + clone.type = mutateSubgraph(program, [options.recur ?? self], prop.type).type; + } + + clone.decorators = prop.decorators.filter((d) => !decoratorsToRemove.has(d)); + }, + }, + }; const self: Mutator = { name: "VisibilityFilter", Model: { @@ -476,45 +491,13 @@ function createVisibilityFilterMutator( mutate: (model, clone, program, realm) => { for (const [key, prop] of model.properties) { if (!isVisible(program, prop, filter)) { + // Property is not visible, remove it clone.properties.delete(key); - realm.remove(prop); - } else if (prop.type.kind === "Model") { - const { type } = mutateSubgraph(program, [options.recur ?? self], prop.type); - - const clonedProperty = $.type.clone(prop); - - clonedProperty.type = type; - - const decorators = clonedProperty.decorators; - const decoratorsToRemove = new Set(); - - for (const decorator of decorators) { - const decFn = decorator.decorator; - if (decFn === $visibility || decFn === $removeVisibility) { - decorator.args = decorator.args.filter( - (arg) => - !( - arg.value.entityKind === "Value" && - arg.value.valueKind === "EnumValue" && - visibilityClasses.has(arg.value.value.enum) - ), - ); - - if (decorator.args.length === 0) { - decoratorsToRemove.add(decorator); - } - } else if (decFn === $invisible) { - decoratorsToRemove.add(decorator); - } - } - - clonedProperty.decorators = clonedProperty.decorators.filter( - (d) => !decoratorsToRemove.has(d), - ); - - $.type.finishType(clonedProperty); + realm.remove(clone); + } else { + const mutated = mutateSubgraph(program, [mpMutator], prop); - clone.properties.set(key, clonedProperty); + clone.properties.set(key, mutated.type as ModelProperty); } } diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 229cfa3ee6..3981f3ba3f 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -5,6 +5,7 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; import { getVisibility, VisibilityFilter } from "../src/core/visibility/core.js"; import { + $visibility, addVisibilityModifiers, clearVisibilityModifiersForClass, Enum, @@ -760,7 +761,7 @@ describe("compiler: visibility core", () => { ok(nestedProps.ru); }); - it("correctly applies Create transform", async () => { + it.only("correctly applies Create transform", async () => { const Result = await compileWithTransform("Create"); const props = getProperties(Result); @@ -868,4 +869,32 @@ describe("compiler: visibility core", () => { strictEqual(nestedProps.invisible, undefined); }); }); + + describe("withVisibilityFilter transforms", () => { + it("correctly makes transformed models immune from further transformation", async () => { + const { ExampleRead, ExampleReadCreate } = (await runner.compile(` + model Example { + @visibility(Lifecycle.Read) + id: string; + } + + @test model ExampleRead is Read; + + @test model ExampleReadCreate is Create; + `)) as { ExampleRead: Model; ExampleReadCreate: Model }; + + const idRead = ExampleRead.properties.get("id")!; + + ok(idRead); + + ok(!idRead.decorators.some((d) => d.decorator === $visibility)); + + // Property should remain present in the Create transform of this model. + const idReadCreate = ExampleReadCreate.properties.get("id")!; + + ok(idReadCreate); + + strictEqual(idRead.type, idReadCreate.type); + }); + }); }); From 2f18683a100a4ef917958bb16662070b667291b6 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 1 Nov 2024 13:43:14 -0400 Subject: [PATCH 16/28] Finally, realy fix the mutator --- packages/compiler/src/lib/visibility.ts | 39 +-- packages/compiler/test/visibility.test.ts | 390 +++++++++++++++------- 2 files changed, 284 insertions(+), 145 deletions(-) diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 54939e5194..c9282b2655 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -449,29 +449,32 @@ function createVisibilityFilterMutator( ModelProperty: { filter: () => MutatorFlow.DoNotRecurse, mutate: (prop, clone, program) => { - const decorators = prop.decorators; - const decoratorsToRemove = new Set(); + // 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: DecoratorApplication[] = []; - for (const decorator of decorators) { + for (const decorator of prop.decorators) { const decFn = decorator.decorator; if (decFn === $visibility || decFn === $removeVisibility) { - decorator.args = decorator.args.filter( - (arg) => - !( - arg.value.entityKind === "Value" && - arg.value.valueKind === "EnumValue" && - visibilityClasses.has(arg.value.value.enum) - ), - ); - - if (decorator.args.length === 0) { - decoratorsToRemove.add(decorator); - } - } else if (decFn === $invisible) { - decoratorsToRemove.add(decorator); + decorators.push({ + ...decorator, + args: decorator.args.filter( + (arg) => + !( + arg.value.entityKind === "Value" && + arg.value.valueKind === "EnumValue" && + visibilityClasses.has(arg.value.value.enum) + ), + ), + }); + } else if (decFn !== $invisible) { + decorators.push(decorator); } } + clone.decorators = decorators; + for (const visibilityClass of visibilityClasses) { resetVisibilityModifiersForClass(program, clone, visibilityClass); } @@ -479,8 +482,6 @@ function createVisibilityFilterMutator( if (prop.type.kind === "Model") { clone.type = mutateSubgraph(program, [options.recur ?? self], prop.type).type; } - - clone.decorators = prop.decorators.filter((d) => !decoratorsToRemove.has(d)); }, }, }; diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 3981f3ba3f..98b624be34 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -652,58 +652,65 @@ describe("compiler: visibility core", () => { describe("lifecycle transforms", () => { async function compileWithTransform( transform: "Create" | "Read" | "Update" | "CreateOrUpdate", + legacy: boolean = false, ) { + const Lifecycle = { + Read: legacy ? `"read"` : "Lifecycle.Read", + Create: legacy ? `"create"` : "Lifecycle.Create", + Update: legacy ? `"update"` : "Lifecycle.Update", + }; const { Result } = (await runner.compile(` model Example { - @visibility(Lifecycle.Read) + @visibility(${Lifecycle.Read}) r: string; cru: string; - @visibility(Lifecycle.Create, Lifecycle.Read) + @visibility(${Lifecycle.Create}, ${Lifecycle.Read}) cr: string; - @visibility(Lifecycle.Create, Lifecycle.Update) + @visibility(${Lifecycle.Create}, ${Lifecycle.Update}) cu: string; - @visibility(Lifecycle.Create) + @visibility(${Lifecycle.Create}) c: string; - @visibility(Lifecycle.Update, Lifecycle.Read) + @visibility(${Lifecycle.Update}, ${Lifecycle.Read}) ru: string; - @visibility(Lifecycle.Update) + @visibility(${Lifecycle.Update}) u: string; - @invisible(Lifecycle) + ${legacy ? `@visibility("none")` : `@invisible(Lifecycle)`} invisible: string; nested: { - @visibility(Lifecycle.Read) + @visibility(${Lifecycle.Read}) r: string; cru: string; - @visibility(Lifecycle.Create, Lifecycle.Read) + @visibility(${Lifecycle.Create}, ${Lifecycle.Read}) cr: string; - @visibility(Lifecycle.Create, Lifecycle.Update) + @visibility(${Lifecycle.Create}, ${Lifecycle.Update}) cu: string; - @visibility(Lifecycle.Create) + @visibility(${Lifecycle.Create}) c: string; - @visibility(Lifecycle.Update, Lifecycle.Read) + @visibility(${Lifecycle.Update}, ${Lifecycle.Read}) ru: string; - @visibility(Lifecycle.Update) + @visibility(${Lifecycle.Update}) u: string; - @invisible(Lifecycle) + ${legacy ? `@visibility("none")` : `@invisible(Lifecycle)`} invisible: string; }; } + // This ensures the transforms are non-side-effecting. model ReadExample is Read; @test model Result is ${transform}; @@ -729,108 +736,21 @@ describe("compiler: visibility core", () => { const Result = await compileWithTransform("Read"); const props = getProperties(Result); - // All properties that do not have Read visibility are removed - strictEqual(props.c, undefined); - strictEqual(props.cu, undefined); - strictEqual(props.u, undefined); - - strictEqual(props.invisible, undefined); - - // All properties that have Read visibility are preserved - ok(props.r); - ok(props.cr); - ok(props.cru); - ok(props.ru); - - const nested = Result.properties.get("nested"); - - ok(nested); - ok(nested.type.kind === "Model"); - - const nestedProps = getProperties(nested.type); - - strictEqual(nestedProps.c, undefined); - strictEqual(nestedProps.cu, undefined); - strictEqual(nestedProps.u, undefined); - - strictEqual(nestedProps.invisible, undefined); - - ok(nestedProps.r); - ok(nestedProps.cr); - ok(nestedProps.cru); - ok(nestedProps.ru); + validateReadTransform(props, Result, getProperties); }); - it.only("correctly applies Create transform", async () => { + it("correctly applies Create transform", async () => { const Result = await compileWithTransform("Create"); const props = getProperties(Result); - // Properties without Create visibility are removed - strictEqual(props.r, undefined); - strictEqual(props.ru, undefined); - strictEqual(props.u, undefined); - - strictEqual(props.invisible, undefined); - - ok(props.c); - ok(props.cr); - ok(props.cu); - ok(props.cru); - - const nested = Result.properties.get("nested"); - - ok(nested); - ok(nested.type.kind === "Model"); - - const nestedProps = getProperties(nested.type); - - strictEqual(nestedProps.r, undefined); - strictEqual(nestedProps.ru, undefined); - strictEqual(nestedProps.u, undefined); - - strictEqual(nestedProps.invisible, undefined); - - ok(nestedProps.c); - ok(nestedProps.cr); - ok(nestedProps.cu); - ok(nestedProps.cru); + validateCreateTransform(props, Result, getProperties); }); it("correctly applies Update transform", async () => { const Result = await compileWithTransform("Update"); const props = getProperties(Result); - // Properties without Update visibility are removed - strictEqual(props.r, undefined); - strictEqual(props.c, undefined); - strictEqual(props.cr, undefined); - - strictEqual(props.invisible, undefined); - - ok(props.cu); - ok(props.cru); - ok(props.ru); - ok(props.u); - - const nested = Result.properties.get("nested"); - - ok(nested); - ok(nested.type.kind === "Model"); - - // Nested properties work differently in Lifecycle Update transforms, requiring nested create-only properties to - // additionally be visible - const nestedProps = getProperties(nested.type); - - strictEqual(nestedProps.r, undefined); - - strictEqual(nestedProps.invisible, undefined); - - ok(nestedProps.c); - ok(nestedProps.cr); - ok(nestedProps.cu); - ok(nestedProps.cru); - ok(nestedProps.ru); - ok(nestedProps.u); + validateUpdateTransform(props, Result, getProperties); }); it("correctly applies CreateOrUpdate transform", async () => { @@ -838,35 +758,38 @@ describe("compiler: visibility core", () => { const props = getProperties(Result); // Properties that only have read visibility are removed - strictEqual(props.r, undefined); + validateCreateOrUpdateTransform(props, Result, getProperties); + }); - strictEqual(props.invisible, undefined); + describe("legacy compatibility", () => { + it("correctly applies Read transform", async () => { + const Result = await compileWithTransform("Read", true); + const props = getProperties(Result); - // All other visible properties are preserved - ok(props.c); - ok(props.cr); - ok(props.cu); - ok(props.cru); - ok(props.ru); - ok(props.u); + validateReadTransform(props, Result, getProperties); + }); - const nested = Result.properties.get("nested"); + it("correctly applies Create transform", async () => { + const Result = await compileWithTransform("Create", true); + const props = getProperties(Result); - ok(nested); - ok(nested.type.kind === "Model"); + validateCreateTransform(props, Result, getProperties); + }); - const nestedProps = getProperties(nested.type); + it("correctly applies Update transform", async () => { + const Result = await compileWithTransform("Update", true); + const props = getProperties(Result); - strictEqual(nestedProps.r, undefined); + validateUpdateTransform(props, Result, getProperties); + }); - ok(nestedProps.c); - ok(nestedProps.cr); - ok(nestedProps.cu); - ok(nestedProps.cru); - ok(nestedProps.ru); - ok(nestedProps.u); + it("correctly applies CreateOrUpdate transform", async () => { + const Result = await compileWithTransform("CreateOrUpdate", true); + const props = getProperties(Result); - strictEqual(nestedProps.invisible, undefined); + // Properties that only have read visibility are removed + validateCreateOrUpdateTransform(props, Result, getProperties); + }); }); }); @@ -898,3 +821,218 @@ describe("compiler: visibility core", () => { }); }); }); +function validateCreateOrUpdateTransform( + props: { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, + Result: Model, + getProperties: (model: Model) => { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, +) { + strictEqual(props.r, undefined); + + strictEqual(props.invisible, undefined); + + // All other visible properties are preserved + ok(props.c); + ok(props.cr); + ok(props.cu); + ok(props.cru); + ok(props.ru); + ok(props.u); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); + ok(nestedProps.ru); + ok(nestedProps.u); + + strictEqual(nestedProps.invisible, undefined); +} + +function validateUpdateTransform( + props: { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, + Result: Model, + getProperties: (model: Model) => { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, +) { + strictEqual(props.r, undefined); + strictEqual(props.c, undefined); + strictEqual(props.cr, undefined); + + strictEqual(props.invisible, undefined); + + ok(props.cu); + ok(props.cru); + ok(props.ru); + ok(props.u); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + // Nested properties work differently in Lifecycle Update transforms, requiring nested create-only properties to + // additionally be visible + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); + ok(nestedProps.ru); + ok(nestedProps.u); +} + +function validateCreateTransform( + props: { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, + Result: Model, + getProperties: (model: Model) => { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, +) { + strictEqual(props.r, undefined); + strictEqual(props.ru, undefined); + strictEqual(props.u, undefined); + + strictEqual(props.invisible, undefined); + + ok(props.c); + ok(props.cr); + ok(props.cu); + ok(props.cru); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.r, undefined); + strictEqual(nestedProps.ru, undefined); + strictEqual(nestedProps.u, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.c); + ok(nestedProps.cr); + ok(nestedProps.cu); + ok(nestedProps.cru); +} + +function validateReadTransform( + props: { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, + Result: Model, + getProperties: (model: Model) => { + c: ModelProperty | undefined; + cr: ModelProperty | undefined; + cu: ModelProperty | undefined; + cru: ModelProperty | undefined; + r: ModelProperty | undefined; + ru: ModelProperty | undefined; + u: ModelProperty | undefined; + invisible: ModelProperty | undefined; + }, +) { + strictEqual(props.c, undefined); + strictEqual(props.cu, undefined); + strictEqual(props.u, undefined); + + strictEqual(props.invisible, undefined); + + // All properties that have Read visibility are preserved + ok(props.r); + ok(props.cr); + ok(props.cru); + ok(props.ru); + + const nested = Result.properties.get("nested"); + + ok(nested); + ok(nested.type.kind === "Model"); + + const nestedProps = getProperties(nested.type); + + strictEqual(nestedProps.c, undefined); + strictEqual(nestedProps.cu, undefined); + strictEqual(nestedProps.u, undefined); + + strictEqual(nestedProps.invisible, undefined); + + ok(nestedProps.r); + ok(nestedProps.cr); + ok(nestedProps.cru); + ok(nestedProps.ru); +} From bc9b464b43a7afbb6bc3b0fbf9a94b85c03947e2 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Fri, 1 Nov 2024 13:50:24 -0400 Subject: [PATCH 17/28] Lint/format --- packages/http/src/metadata.ts | 1 - packages/http/src/operations.ts | 1 + packages/openapi/src/helpers.ts | 16 +++++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/http/src/metadata.ts b/packages/http/src/metadata.ts index 2385a830d4..80bd57af05 100644 --- a/packages/http/src/metadata.ts +++ b/packages/http/src/metadata.ts @@ -206,7 +206,6 @@ export function resolveRequestVisibility( operation: Operation, verb: HttpVerb, ): Visibility { - // eslint-disable-next-line @typescript-eslint/no-deprecated const parameterVisibility = getParameterVisibility(program, operation); const parameterVisibilityArray = arrayToVisibility(parameterVisibility); const defaultVisibility = getDefaultVisibilityForVerb(verb); diff --git a/packages/http/src/operations.ts b/packages/http/src/operations.ts index 3d2a182b06..26657269d2 100644 --- a/packages/http/src/operations.ts +++ b/packages/http/src/operations.ts @@ -253,6 +253,7 @@ function validateProgram(program: Program, diagnostics: DiagnosticCollector) { // itself as that would be a layering violation, putting a REST // interpretation of visibility into the core. function checkForUnsupportedVisibility(property: ModelProperty) { + // eslint-disable-next-line @typescript-eslint/no-deprecated if (getVisibility(program, property)?.includes("write")) { // NOTE: Check for name equality instead of function equality // to deal with multiple copies of core being used. diff --git a/packages/openapi/src/helpers.ts b/packages/openapi/src/helpers.ts index 65d45a6c22..3e1405c994 100644 --- a/packages/openapi/src/helpers.ts +++ b/packages/openapi/src/helpers.ts @@ -1,7 +1,8 @@ import { getFriendlyName, + getLifecycleVisibilityEnum, getTypeName, - getVisibility, + getVisibilityForClass, isGlobalNamespace, isService, isTemplateInstance, @@ -151,16 +152,17 @@ export function resolveOperationId(program: Program, operation: Operation) { } /** - * Determines if a property is read-only, which is defined as being - * decorated `@visibility("read")`. + * Determines if a property is read-only, which is defined as having only the + * `Read` visibility in the Lifecycle visibility class. * - * If there is more than 1 `@visibility` argument, then the property is not - * read-only. For example, `@visibility("read", "update")` does not + * If there is more than one active Lifecycle visibility, then the property is not + * read-only. For example, `@visibility(Lifecycle.Read, Lifecycle.Update)` does not * designate a read-only property. */ export function isReadonlyProperty(program: Program, property: ModelProperty) { - const visibility = getVisibility(program, property); + const lifecycle = getLifecycleVisibilityEnum(program); + const visibility = getVisibilityForClass(program, property, lifecycle); // note: multiple visibilities that include read are not handled using // readonly: true, but using separate schemas. - return visibility?.length === 1 && visibility[0] === "read"; + return visibility.size === 1 && visibility.has(lifecycle.members.get("Read")!); } From f013701e6ec87e7f8925ed28985b3b0e9949229f Mon Sep 17 00:00:00 2001 From: Will Temple Date: Mon, 4 Nov 2024 16:43:57 -0500 Subject: [PATCH 18/28] Revert change to openapi visibility computation --- .../test/decorators/visibility.test.ts | 56 ++++++++++++++++++- packages/openapi/src/helpers.ts | 17 +++--- 2 files changed, 61 insertions(+), 12 deletions(-) diff --git a/packages/compiler/test/decorators/visibility.test.ts b/packages/compiler/test/decorators/visibility.test.ts index 093503c04f..8f83793101 100644 --- a/packages/compiler/test/decorators/visibility.test.ts +++ b/packages/compiler/test/decorators/visibility.test.ts @@ -1,12 +1,62 @@ // Copyright (c) Microsoft Corporation // Licensed under the MIT license. -import { deepStrictEqual } from "assert"; +import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Model } from "../../src/core/types.js"; -import { getVisibility } from "../../src/core/visibility/core.js"; +import { Enum, Model, ModelProperty } from "../../src/core/types.js"; +import { getVisibility, getVisibilityForClass } from "../../src/core/visibility/core.js"; +import { getLifecycleVisibilityEnum } from "../../src/index.js"; import { BasicTestRunner, createTestRunner } from "../../src/testing/index.js"; +function assertSetsEqual(a: Set, b: Set): void { + strictEqual(a.size, b.size); + + for (const item of a) { + ok(b.has(item)); + } +} + +describe("visibility", function () { + let runner: BasicTestRunner; + + beforeEach(async () => { + runner = await createTestRunner(); + }); + + it("default visibility", async () => { + const { name, Dummy } = (await runner.compile(` + @test + @defaultVisibility(Dummy.B) + enum Dummy { + A, + B, + } + + model TestModel { + @test + name: string; + }`)) as { name: ModelProperty; Dummy: Enum }; + + const LifecycleEnum = getLifecycleVisibilityEnum(runner.program); + + const Lifecycle = { + Read: LifecycleEnum.members.get("Read")!, + Create: LifecycleEnum.members.get("Create")!, + Update: LifecycleEnum.members.get("Update")!, + }; + + assertSetsEqual( + getVisibilityForClass(runner.program, name, LifecycleEnum), + new Set([Lifecycle.Read, Lifecycle.Create, Lifecycle.Update]), + ); + + assertSetsEqual( + getVisibilityForClass(runner.program, name, Dummy), + new Set([Dummy.members.get("B")!]), + ); + }); +}); + describe("visibility (legacy)", function () { let runner: BasicTestRunner; diff --git a/packages/openapi/src/helpers.ts b/packages/openapi/src/helpers.ts index 3e1405c994..260562894a 100644 --- a/packages/openapi/src/helpers.ts +++ b/packages/openapi/src/helpers.ts @@ -1,8 +1,7 @@ import { getFriendlyName, - getLifecycleVisibilityEnum, getTypeName, - getVisibilityForClass, + getVisibility, isGlobalNamespace, isService, isTemplateInstance, @@ -152,17 +151,17 @@ export function resolveOperationId(program: Program, operation: Operation) { } /** - * Determines if a property is read-only, which is defined as having only the - * `Read` visibility in the Lifecycle visibility class. + * Determines if a property is read-only, which is defined as being + * decorated `@visibility("read")`. * - * If there is more than one active Lifecycle visibility, then the property is not - * read-only. For example, `@visibility(Lifecycle.Read, Lifecycle.Update)` does not + * If there is more than 1 `@visibility` argument, then the property is not + * read-only. For example, `@visibility("read", "update")` does not * designate a read-only property. */ export function isReadonlyProperty(program: Program, property: ModelProperty) { - const lifecycle = getLifecycleVisibilityEnum(program); - const visibility = getVisibilityForClass(program, property, lifecycle); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const visibility = getVisibility(program, property); // note: multiple visibilities that include read are not handled using // readonly: true, but using separate schemas. - return visibility.size === 1 && visibility.has(lifecycle.members.get("Read")!); + return visibility?.length === 1 && visibility[0] === "read"; } From 7f88db076abf6453b65ef7e70160802cdb7f85e3 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Mon, 4 Nov 2024 16:51:58 -0500 Subject: [PATCH 19/28] satisfy vitest obsession with circular module imports --- packages/compiler/src/core/logger/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler/src/core/logger/logger.ts b/packages/compiler/src/core/logger/logger.ts index ea61fa0236..59566a2041 100644 --- a/packages/compiler/src/core/logger/logger.ts +++ b/packages/compiler/src/core/logger/logger.ts @@ -1,4 +1,4 @@ -import { getSourceLocation } from "../index.js"; +import { getSourceLocation } from "../diagnostics.js"; import type { Logger, LogInfo, LogLevel, LogSink, ProcessedLog } from "../types.js"; const LogLevels = { From d1b4c7094710768271dff9b8e5da6c6551a08cb7 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 6 Nov 2024 16:16:42 -0500 Subject: [PATCH 20/28] Enhance mutator to walk more indirect types. --- .../compiler/src/experimental/mutators.ts | 19 +++ packages/compiler/src/lib/visibility.ts | 37 +++++- packages/compiler/test/visibility.test.ts | 122 ++++++++++++++++++ 3 files changed, 176 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/experimental/mutators.ts b/packages/compiler/src/experimental/mutators.ts index bd948aea66..1d5a0bcf51 100644 --- a/packages/compiler/src/experimental/mutators.ts +++ b/packages/compiler/src/experimental/mutators.ts @@ -94,6 +94,25 @@ export type MutableType = Exclude< | ObjectType | Projection >; + +/** + * Determines if a type is mutable. + */ +export function isMutableType(type: Type): type is MutableType { + switch (type.kind) { + case "TemplateParameter": + case "Intrinsic": + case "Function": + case "Decorator": + case "FunctionParameter": + case "Object": + case "Projection": + return false; + default: + return true; + } +} + const typeId = CustomKeyMap.objectKeyer(); const mutatorId = CustomKeyMap.objectKeyer(); const seen = new CustomKeyMap<[MutableType, Set | Mutator[]], Type>(([type, mutators]) => { diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index c9282b2655..142613f045 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -28,6 +28,7 @@ import { ModelProperty, Operation, Type, + UnionVariant, } from "../core/types.js"; import { addVisibilityModifiers, @@ -46,7 +47,7 @@ import { getLifecycleVisibilityEnum, normalizeVisibilityToLegacyLifecycleString, } from "../core/visibility/lifecycle.js"; -import { mutateSubgraph, Mutator, MutatorFlow } from "../experimental/mutators.js"; +import { isMutableType, mutateSubgraph, Mutator, MutatorFlow } from "../experimental/mutators.js"; import { isKey } from "./key.js"; import { filterModelPropertiesInPlace, useStateMap } from "./utils.js"; @@ -479,7 +480,7 @@ function createVisibilityFilterMutator( resetVisibilityModifiersForClass(program, clone, visibilityClass); } - if (prop.type.kind === "Model") { + if (isMutableType(prop.type)) { clone.type = mutateSubgraph(program, [options.recur ?? self], prop.type).type; } }, @@ -487,6 +488,20 @@ function createVisibilityFilterMutator( }; const self: Mutator = { name: "VisibilityFilter", + Union: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (union, clone, program) => { + for (const [key, member] of union.variants) { + if (member.type.kind === "Model" || member.type.kind === "Union") { + const variant: UnionVariant = { + ...member, + type: mutateSubgraph(program, [self], member.type).type, + }; + clone.variants.set(key, variant); + } + } + }, + }, Model: { filter: () => MutatorFlow.DoNotRecurse, mutate: (model, clone, program, realm) => { @@ -507,6 +522,24 @@ function createVisibilityFilterMutator( } }, }, + ModelProperty: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (prop, clone, program) => { + if (isMutableType(prop.type)) { + clone.type = mutateSubgraph(program, [self], prop.type).type; + } + }, + }, + Tuple: { + filter: () => MutatorFlow.DoNotRecurse, + mutate: (tuple, clone, program) => { + for (const [index, element] of tuple.values.entries()) { + if (isMutableType(element)) { + clone.values[index] = mutateSubgraph(program, [self], element).type; + } + } + }, + }, }; return self; diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 98b624be34..b41d95ae7f 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -761,6 +761,128 @@ describe("compiler: visibility core", () => { validateCreateOrUpdateTransform(props, Result, getProperties); }); + it("correctly transforms a union", async () => { + const { Result } = (await runner.compile(` + model Example { + example: A | B; + } + + model A { + @visibility(Lifecycle.Read) + a: string; + } + + model B { + @invisible(Lifecycle) + b: string; + } + + @test + model Result is Read; + `)) as { Result: Model }; + + const example = Result.properties.get("example"); + + ok(example); + + const union = example.type; + + strictEqual(union.kind, "Union"); + + const [A, B] = [...union.variants.values()].map((v) => v.type); + + strictEqual(A.kind, "Model"); + strictEqual(B.kind, "Model"); + + const a = A.properties.get("a"); + const b = B.properties.get("b"); + + ok(a); + + strictEqual(b, undefined); + }); + + it("correctly transforms a model property reference", async () => { + const { Result } = (await runner.compile(` + model Example { + a: ExampleRef.a; + } + + model ExampleRef { + a: A; + } + + model A { + @visibility(Lifecycle.Read) + a: string; + @visibility(Lifecycle.Create) + b: string; + } + + @test + model Result is Create; + `)) as { Result: Model }; + + const example = Result.properties.get("a"); + + ok(example); + + const ref = example.type; + + strictEqual(ref.kind, "ModelProperty"); + + const A = ref.type; + + ok(A.kind === "Model"); + + const a = A.properties.get("a"); + const b = A.properties.get("b"); + + strictEqual(a, undefined); + ok(b); + }); + + it("correctly transforms a tuple", async () => { + const { Result } = (await runner.compile(` + model Example { + example: [A, B]; + } + + model A { + @visibility(Lifecycle.Read) + a: string; + } + + model B { + @invisible(Lifecycle) + b: string; + } + + @test + model Result is Read; + `)) as { Result: Model }; + + const example = Result.properties.get("example"); + + ok(example); + + const tuple = example.type; + + strictEqual(tuple.kind, "Tuple"); + + const [A, B] = tuple.values; + + strictEqual(A.kind, "Model"); + strictEqual(B.kind, "Model"); + + const a = A.properties.get("a"); + const b = B.properties.get("b"); + + ok(a); + + strictEqual(b, undefined); + }); + describe("legacy compatibility", () => { it("correctly applies Read transform", async () => { const Result = await compileWithTransform("Read", true); From d77ace79295d2e8463120bb853322240b47cef97 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 6 Nov 2024 16:17:12 -0500 Subject: [PATCH 21/28] Add visibility reference documentation --- .../docs/docs/language-basics/visibility.md | 364 ++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 website/src/content/docs/docs/language-basics/visibility.md diff --git a/website/src/content/docs/docs/language-basics/visibility.md b/website/src/content/docs/docs/language-basics/visibility.md new file mode 100644 index 0000000000..dd523a840d --- /dev/null +++ b/website/src/content/docs/docs/language-basics/visibility.md @@ -0,0 +1,364 @@ +--- +id: visibility +title: Visibility +--- + +**Visibility** is a language feature that allows you to share a model between multiple operations and define in which contexts +properties of the model are "visible." Visibility is a very powerful feature that allows you to define different "views" +of a model within different operations or contexts. + +## Basic concepts + +- Visibility applies to _model properties_ only. It is used to determine when an emitter should include or exclude a + property in a certain context. +- Visibility is defined using a _visibility class_. A visibility class is an `enum` that defines the visibility modifiers + (or flags) that can be applied to a property. Any `enum` can serve as a visibility class. +- Visibility classes have a _default_ visibility, which is the set of visibility modifiers that are applied _by default_ + to a property if the visibility is not explicitly set. + +## Lifecycle visibility + +TypeSpec provides a built-in visibility called "resource lifecycle visibility." This visibility allows you to declare +whether properties are visible when a creating, updating, or reading a resource from an API endpoint. For example: + +```typespec +model Example { + /** + * The unique identifier of this resource. + * + * The ID is automatically generated by the service, so it cannot be set when the resource is created or updated, + * but the server will return it when the resource is read. + */ + @visibility(Lifecycle.Read) + id: string; + + /** + * The name of this resource. + * + * The name can be set when the resource is created, but may not be changed. + */ + @visibility(Lifecycle.Create, Lifecycle.Read) + name: string; + + /** + * The description of this resource. + * + * By default, properties are visible in all three lifecycle phases, so this + * property can be set when the resource is created, updated, and read. + */ + description: string; +} +``` + +In the above example, each property of the `Example` model has a lifecycle visibility that instructs emitters to include +or exclude the property when creating, updating, or reading the `Example` resource. + +TypeSpec's HTTP library, OpenAPI emitter, and other standard functionality use the `Lifecycle` visibility to create +different views of the `Example` model based on which lifecycle phase is used in a particular operation. + +In the following example, the type of the input and output of each operation is affected by the lifecycle visibility +of the properties in the `Example` model. + +```typespec +@route("/example") +interface Examples { + /** + * When an operation uses the POST verb, it uses the `Create` lifecycle visibility to determine which properties + * are visible. + */ + @post create(@body example: Example): Created | Error; + + /** + * When an operation uses the GET verb, it uses the `Read` lifecycle visibility to determine which properties + * are visible. + */ + @get read(@path id: string): Ok | Error; + + /** + * When an operation uses the PATCH verb, it uses the `Update` lifecycle visibility to determine which properties + * are visible. + */ + @patch update(@path id: string, @body example: Example): Ok | Error; +} +``` + +The above interface generates the following OpenAPIv3 schemas: + +```yml +paths: + /example: + post: + parameters: [] + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Example" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/Example" + /example/{id}: + get: + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Example" + patch: + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Example" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ExampleUpdate" +components: + schemas: + Example: + type: object + required: + - id + - name + - description + properties: + id: + type: string + readOnly: true + name: + type: string + description: + type: string + ExampleUpdate: + type: object + properties: + description: + type: string +``` + +Notice: + +- The `id` property is marked `readOnly: true` because it is only visible when reading the resource. +- The `ExampleUpdate` schema only includes the `description` property because it is the only property that is visible + when updating the resource. +- Each of the `paths` reference the correct schema based on the lifecycle phase that the operations use. +- The TypeSpec model is only defined _once_, and any changes in the output schemas are derived from the lifecycle + visibility of the properties in the model. + +### Lifecycle visibility transforms + +You can explicitly compute the shape of a model within a _specific_ lifecycle phase by using the four built-in +templates for lifecycle transforms: + +- `Create`: creates a copy of `T` with only the properties that are visible in the `Create` lifecycle + phase, recursively. +- `Read`: creates a copy of `T` with only the properties that are visible in the `Read` lifecycle phase, + recursively. +- `Update`: creates a copy of `T` with only the properties that are visible in the `Update` lifecycle + phase, with the types of the properties set to `CreateOrUpdate`, recursively. +- `CreateOrUpdate`: creates a copy of `T` with only the properties that have _either_ the `Create` or `Update` + visibility modifiers enabled, recursively. + +For example: + +```typespec +model Example { + @visibility(Lifecycle.Create) + id: string; + + @visibility(Lifecycle.Create, Lifecycle.Read) + name: string; + + @visibility(Lifecycle.Update) + description: string; +} + +model ReadExample is Read; + +model CreateExample is Create; + +model UpdateExample is Update; + +model CreateOrUpdateExample is CreateOrUpdate; +``` + +When you use these templates, the resulting models have no `Lifecycle` visibility modifiers applied, so that any +emitters or libraries that use lifecycle visibility will not alter them further. + +## Visibility modifiers + +Each property has its own set of _active visibility modifiers_ for each visibility class. The active modifiers can be +changed using the decorators described in this section. + +**Note**: Changing the visibility for one visibility class _does not_ affect other visibility classes. If you change the +visibility for the `Lifecycle` visibility class, it will not affect the modifiers that are active for _any_ other +visibility classes. + +### `@visibility` + +The `@visibility` decorator _enables_ visibility modifiers. It takes a list of visibility modifiers as arguments and +sets them on the property. For example: + +```typespec +@visibility(Lifecycle.Create, Lifecycle.Read) +name: string; +``` + +In this example, the `name` property has the `Create` and `Read` visibility modifiers enabled. + +If visibility has _already_ been set explicitly on a property, the `@visibility` decorator _ADDS_ its own visibility +modifiers to the currently-active modifiers. It does not _replace_ the existing modifiers. For example: + +```typespec +@visibility(Lifecycle.Create) +@visibility(Lifecycle.Read) +name: string; +``` + +In this example, the `name` property has both the `Create` and `Read` visibility modifiers enabled, but _not_ the `Update` +visibility modifier. The `@visibility` decorator starts from an _empty_ set of modifiers and adds the `Create` modifier, +then adds the `Read` modifier. + +### `@removeVisibility` + +The `@removeVisibility` decorator _disables_ visibility modifiers. It takes a list of visibility modifiers as arguments +and removes them from the property. For example: + +```typespec +@removeVisibility(Lifecycle.Update) +name: string; +``` + +This use of `@removeVisibility` is equivalent to the above examples with the `@visibility` decorator, but it uses the `@removeVisibility` +decorator to remove the `Update` visibility modifier from the `name` property rather than adding the `Create` and `Read` +visibility modifiers. The `@removeVisibility` decorator starts from the _default_ set of visibility modifiers and removes +the `Update` modifier. + +If the visibility has _already_ been set on a property, the `@removeVisibility` decorator _removes_ its visibility from +the currently-active modifiers. It does not _replace_ the existing modifiers. For example: + +```typespec +@removeVisibility(Lifecycle.Update) +@removeVisibility(Lifecycle.Create) +id: string; +``` + +In this example, the `id` property has the `Update` and `Create` visibility modifiers removed, but it retains the `Read` +visibility modifier. + +### `@invisible` + +The `@invisible` decorator _disables all visibility modifiers_ on a property within a given visibility class. For example: + +```typespec +@invisible(Lifecycle) +invisible: string; +``` + +In this example, the `invisible` property has _no_ visibility modifiers enabled in the `Lifecycle` visibility class. + +## Visibility filters + +The `@withVisibilityFilter` decorator allows you to transform a model by applying a visibility filter to it. A +visibility filter is an object that defines constraints on which visibility modifiers must be enabled/disabled for a +property to be visible. For example: + +```typespec +model Example { + @visibility(Lifecycle.Create) + id: string; + + @visibility(Lifecycle.Create, Lifecycle.Read) + name: string; + + @visibility(Lifecycle.Update) + description: string; +} + +@withVisibilityFilter(#{ all: [Lifecycle.Create, Lifecycle.Read] }) +model CreateAndReadExample { + ...Example; +} + +@withVisibilityFilter(#{ any: [Lifecycle.Create, Lifecycle.Update] }) +model CreateOrUpdateExample { + ...Example; +} + +@withVisibilityFilter(#{ none: [Lifecycle.Update] }) +model NonUpdateExample { + ...Example; +} +``` + +In the above example, the `CreateAndReadExample` model is a copy of the `Example` model with only the the properties +that have _BOTH_ the `Create` and `Read` visibility modifiers enabled (i.e. only the `name` property). The +`CreateOrUpdateExample` model is a copy of the `Example` model with only the properties that have _EITHER_ the `Create` +or `Update` visibility modifiers enabled (i.e. the `id` and `name` properties). The `NonUpdateExample` model is a copy +of the `Example` model with only the properties that _do not_ have the `Update` visibility modifier enabled (i.e. the +`id` and `name` properties). + +**Note**: For `Lifecycle` visibility, you should ordinarily use the `Create`, `Read`, `Update`, and `CreateOrUpdate` +templates instead of `@withVisibilityFilter` directly, but you can use `@withVisibilityFilter` to create custom "views" +of a model that use visibility classes other than `Lifecycle` or custom filter logic. + +## Visibility classes + +Any TypeSpec `enum` can serve as a visibility class. The members of the `enum` define the visibility modifiers in the +class. For example, the following is the definition of the `Lifecycle` visibility class defined in the TypeSpec standard +library: + +```typespec +enum Lifecycle { + Create, + Read, + Update, +} +``` + +This visibility class defines three visibility modifiers: `Create`, `Read`, and `Update`. By default, all properties +have _ALL_ three visibilities in the `Lifecycle` enum enabled. + +### Setting default visibility + +You can set the default visibility for a visibility class by declaring it on the enum using the `@defaultVisibility` +decorator: + +```typespec +@defaultVisibility(Example.A) +enum Example { + A, + B, +} +``` + +In this example, any property that does not declare an `Example` visibility modifier will have the `A` visibility by +default. + +**Note**: While you can define your own visibility classes, emitters _will not recognize them_ unless they have been +programmed to do so. You can leverage custom visibility classes in your own emitters, but they will have no effect on +the standard emitters unless those emitters choose to adopt and recognize those visibility classes as meaningful. The +`Lifecycle` visibility class is a standard visibility class that is recognized by several emitters. You can, however, +use your own visibility classes with the built in `@withVisibilityFilter` decorator to transform your models in whatever +ways you see fit. From bc5320a069d2da0a57064b0bf9ad0fbd96b76f05 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 6 Nov 2024 16:47:41 -0500 Subject: [PATCH 22/28] Update generated-defs --- packages/compiler/generated-defs/TypeSpec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/compiler/generated-defs/TypeSpec.ts b/packages/compiler/generated-defs/TypeSpec.ts index f624f58da4..ed9f2aac70 100644 --- a/packages/compiler/generated-defs/TypeSpec.ts +++ b/packages/compiler/generated-defs/TypeSpec.ts @@ -953,6 +953,10 @@ export type ReturnTypeVisibilityDecorator = ( * * The default modifiers are used when a property does not have any visibility decorators * applied to it. + * + * The modifiers passed to this decorator _MUST_ be members of the target Enum. + * + * @param visibilities the list of modifiers to use as the default visibility modifiers. */ export type DefaultVisibilityDecorator = ( context: DecoratorContext, From acc25c0e98a4ba0387ee2ba888d0850eda083b15 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 6 Nov 2024 16:51:12 -0500 Subject: [PATCH 23/28] Chronus --- .../witemple-msft-visibility-enum-2024-10-6-16-50-9.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-50-9.md diff --git a/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-50-9.md b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-50-9.md new file mode 100644 index 0000000000..082815372a --- /dev/null +++ b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-50-9.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/compiler" +--- + +Adds support for enum-driven visibility in the compiler core. From e7d92bc4ace441b2f4c4b8c64005594815a052c5 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 6 Nov 2024 16:53:20 -0500 Subject: [PATCH 24/28] Chronus 2 --- .../witemple-msft-visibility-enum-2024-10-6-16-53-11.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md diff --git a/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md new file mode 100644 index 0000000000..e4c5f80c55 --- /dev/null +++ b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md @@ -0,0 +1,8 @@ +--- +changeKind: internal +packages: + - "@typespec/http" + - "@typespec/openapi3" +--- + +Mask deprecation on getVisibility \ No newline at end of file From 4ce30a5718c304ffe93283307e5f69739db4efc4 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 6 Nov 2024 17:02:43 -0500 Subject: [PATCH 25/28] fix chronus --- .../witemple-msft-visibility-enum-2024-10-6-16-53-11.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md index e4c5f80c55..a2c7cd59bc 100644 --- a/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md +++ b/.chronus/changes/witemple-msft-visibility-enum-2024-10-6-16-53-11.md @@ -2,7 +2,7 @@ changeKind: internal packages: - "@typespec/http" - - "@typespec/openapi3" + - "@typespec/openapi" --- -Mask deprecation on getVisibility \ No newline at end of file +Mask deprecation on getVisibility From 3cf29bc5e1d7e8149f087e136a1dfd0e95869f85 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Wed, 6 Nov 2024 17:38:07 -0500 Subject: [PATCH 26/28] JSDoc improvement --- packages/compiler/src/core/visibility/core.ts | 93 +++++++++++++++++-- packages/compiler/src/lib/visibility.ts | 19 ++++ 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index 730e6c1c13..80b20bcee9 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -70,11 +70,11 @@ function getOrInitializeVisibilityModifiers( * If no visibility modifiers have been set for the given `property` and `visibilityClass`, the function will use the * provided `defaultSet` to initialize the visibility modifiers. * - * @param program - * @param property - * @param visibilityClass + * @param program - the program in which the property occurs + * @param property - the property to get visibility modifiers for + * @param visibilityClass - the visibility class to get visibility modifiers for * @param defaultSet - the default set to use if no set has been initialized - * @returns + * @returns the active visibility modifier set for the given property and visibility class */ function getOrInitializeActiveModifierSetForClass( program: Program, @@ -107,6 +107,13 @@ const [getSealedVisibilityClasses, setSealedVisibilityClasses] = useStateMap< Set >("sealedVisibilityClasses"); +/** + * Seals visibility modifiers for a property in a given visibility class. + * + * @param program - the program in which the property occurs + * @param property - the property to seal visibility modifiers for + * @param visibilityClass - the visibility class to seal visibility modifiers for + */ function sealVisibilityModifiersForClass( program: Program, property: ModelProperty, @@ -129,6 +136,14 @@ const [getDefaultModifiers, setDefaultModifiers] = useStateMap { const cached = getDefaultModifiers(program, visibilityClass); @@ -163,6 +178,9 @@ export function setDefaultModifierSetForVisibilityClass( /** * Convert a sequence of visibility modifiers into a map of visibility classes to their respective modifiers in the * sequence. + * + * @param modifiers - the visibility modifiers to group + * @returns a map of visibility classes to their respective modifiers in the input list */ function groupModifiersByVisibilityClass(modifiers: EnumMember[]): Map> { const enumMap = new Map>(); @@ -393,10 +411,20 @@ export function addVisibilityModifiers( /** * Remove visibility modifiers from a property. - * @param program - * @param property - * @param modifiers - * @param context + * + * This function will remove all the `modifiers` from the active set of visibility modifiers for the given `property`. + * + * If no set of active modifiers exists for the given `property`, the default set for the modifier's visibility class + * will be used. + * + * If the visibility modifiers for `property` in the given modifier's visibility class have been sealed, this function + * will issue a diagnostic and ignore that modifier, but it will still remove the rest of the modifiers whose classes + * have not been sealed. + * + * @param program - the program in which the ModelProperty occurs + * @param property - the property to remove visibility modifiers from + * @param modifiers - the visibility modifiers to remove + * @param context - the optional decorator context to use for displaying diagnostics */ export function removeVisibilityModifiers( program: Program, @@ -432,6 +460,17 @@ export function removeVisibilityModifiers( } } +/** + * Clears the visibility modifiers for a property in a given visibility class. + * + * If the visibility modifiers for the given class are sealed, this function will issue a diagnostic and leave the + * visibility modifiers unchanged. + * + * @param program - the program in which the ModelProperty occurs + * @param property - the property to clear visibility modifiers for + * @param visibilityClass - the visibility class to clear visibility modifiers for + * @param context - the optional decorator context to use for displaying diagnostics + */ export function clearVisibilityModifiersForClass( program: Program, property: ModelProperty, @@ -460,6 +499,21 @@ export function clearVisibilityModifiersForClass( modifierSet.clear(); } +/** + * Resets the visibility modifiers for a property in a given visibility class. + * + * This does not clear the modifiers. It resets them to the _uninitialized_ state. + * + * This is useful when cloning properties and you want to reset the visibility modifiers on the clone. + * + * If the visibility modifiers for this property and given visibility class are sealed, this function will issue a + * diagnostic and leave the visibility modifiers unchanged. + * + * @param program - the program in which the property occurs + * @param property - the property to reset visibility modifiers for + * @param visibilityClass - the visibility class to reset visibility modifiers for + * @param context - the optional decorator context to use for displaying diagnostics + */ export function resetVisibilityModifiersForClass( program: Program, property: ModelProperty, @@ -486,6 +540,17 @@ export function resetVisibilityModifiersForClass( // #region Visibility Analysis API +/** + * Returns the active visibility modifiers for a property in a given visibility class. + * + * This function is infallible. If the visibility modifiers for the given class have not been set explicitly, it will + * return the default visibility modifiers for the class. + * + * @param program - the program in which the property occurs + * @param property - the property to get visibility modifiers for + * @param visibilityClass - the visibility class to get visibility modifiers for + * @returns the set of active modifiers (enum members) for the property and visibility class + */ export function getVisibilityForClass( program: Program, property: ModelProperty, @@ -541,14 +606,24 @@ export function hasVisibility( * - NONE of the visibilities in the `none` set. */ export interface VisibilityFilter { + /** + * If set, the filter considers a property visible if it has ALL of these visibility modifiers. + */ all?: Set; + /** + * If set, the filter considers a property visible if it has ANY of these visibility modifiers. + */ any?: Set; + /** + * If set, the filter considers a property visible if it has NONE of these visibility modifiers. + */ none?: Set; } export const VisibilityFilter = { /** * Convert a TypeSpec `GeneratedVisibilityFilter` value to a `VisibilityFilter`. + * * @param filter - the decorator argument filter to convert * @returns a `VisibilityFilter` object that can be consumed by the visibility APIs */ @@ -590,6 +665,7 @@ export function isVisible( property: ModelProperty, filter: VisibilityFilter, ): boolean; + /** * Determines if a property has any of the specified (legacy) visibility strings. * @@ -605,6 +681,7 @@ export function isVisible( property: ModelProperty, visibilities: readonly string[], ): boolean; + export function isVisible( program: Program, property: ModelProperty, diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 142613f045..c24c73dce8 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -117,8 +117,17 @@ export const $withDefaultKeyVisibility: WithDefaultKeyVisibilityDecorator = ( }); }; +/** + * Visibility configuration of an operation. + */ interface OperationVisibilityConfig { + /** + * Stored parameter visibility configuration. + */ parameters?: string[] | EnumMember[]; + /** + * Stored return type visibility configuration. + */ returnType?: string[] | EnumMember[]; } @@ -436,10 +445,20 @@ interface CreateVisibilityFilterMutatorOptions { /** * Optionally, a decorator function to remove from the model's decorators, if present. + * + * This allows removing a decorator like `withVisibilityFilter` from the model after it has been applied + * to avoid an infinite loop. */ decoratorFn?: DecoratorFunction; } +/** + * 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: VisibilityFilter, options: CreateVisibilityFilterMutatorOptions = {}, From a584fcb8fa516749c42f62e25ebca758ff371d94 Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 7 Nov 2024 15:54:06 -0500 Subject: [PATCH 27/28] Improve legacy backcompat for empty @visibility decorator --- packages/compiler/src/core/visibility/core.ts | 24 ++++++++++++++--- packages/compiler/src/lib/visibility.ts | 27 +++++++++++-------- packages/compiler/test/visibility.test.ts | 23 +++++++++++++++- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index 80b20bcee9..2d62cc8bf4 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -207,6 +207,24 @@ function groupModifiersByVisibilityClass(modifiers: EnumMember[]): Map("legacyVisibility"); +// const [getLegacyVisibilityIsExplicit, _setLegacyVisibilityIsExplicit] = useStateSet( +// "legacyVisibilityIsExplicit", +// ); + +// /** +// * Sets a flag indicating that legacy visibility modifiers should be considered "explicitly initialized." +// * +// * This is used to differentiate between legacy visibility cases where visibility is _not_ set, and legacy +// * `getVisibility` should return `undefined` and cases where visibility is set to an empty array, and legacy +// * visibility should return `[]`. +// * +// * @param context - the decorator context to use +// * @param property - the property to set the flag for +// */ +// export function setLegacyVisibilityIsExplicit(program: Program, property: ModelProperty) { +// _setLegacyVisibilityIsExplicit(program, property); +// } + /** * Sets the legacy visibility modifiers for a property. * @@ -234,7 +252,7 @@ export function setLegacyVisibility( const lifecycleClass = getLifecycleVisibilityEnum(program); - if (visibilities.length === 1 && visibilities[0] === "none") { + if (visibilities.length === 0 || (visibilities.length === 1 && visibilities[0] === "none")) { clearVisibilityModifiersForClass(program, property, lifecycleClass, context); } else { const lifecycleVisibilities = visibilities @@ -287,8 +305,8 @@ export function getVisibility(program: Program, property: ModelProperty): string // Visibility is completely uninitialized, so return undefined to mimic legacy behavior. if (!lifecycleModifiers) return undefined; - // Visibility has been cleared explicitly: return ["none"] to mimic legacy application of visibility "none". - if (lifecycleModifiers.size === 0) return ["none"]; + // Visibility has been cleared explicitly, so return [] to mimic legacy behavior. + if (lifecycleModifiers.size === 0) return []; // Otherwise we just convert the modifiers to strings. return Array.from(lifecycleModifiers).map((v) => v.name.toLowerCase()); diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index c24c73dce8..3fb13c2b07 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -240,7 +240,7 @@ export const $visibility: VisibilityDecorator = ( ) => { const [modifiers, legacyVisibilities] = splitLegacyVisibility(visibilities); - if (legacyVisibilities.length > 0) { + if (legacyVisibilities.length > 0 || visibilities.length === 0) { const isUnique = validateDecoratorUniqueOnNode(context, target, $visibility); if (modifiers.length > 0) { @@ -477,17 +477,22 @@ function createVisibilityFilterMutator( for (const decorator of prop.decorators) { const decFn = decorator.decorator; if (decFn === $visibility || decFn === $removeVisibility) { - decorators.push({ - ...decorator, - args: decorator.args.filter( - (arg) => - !( - arg.value.entityKind === "Value" && - arg.value.valueKind === "EnumValue" && - visibilityClasses.has(arg.value.value.enum) - ), - ), + 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); }); + + if (nextArgs.length > 0) { + decorators.push({ + ...decorator, + args: nextArgs, + }); + } } else if (decFn !== $invisible) { decorators.push(decorator); } diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index b41d95ae7f..031eaa8a95 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -644,9 +644,30 @@ describe("compiler: visibility core", () => { deepStrictEqual(xVisibility, ["create", "update"]); strictEqual(yVisibility, undefined); - deepStrictEqual(zVisibility, ["none"]); + deepStrictEqual(zVisibility, []); deepStrictEqual(aVisibility, ["create", "update", "read"]); }); + + it("correctly preseves explicitness of empty visibility", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const Lifecycle = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, Lifecycle); + + strictEqual(visibility.size, 0); + + const legacyVisibility = getVisibility(runner.program, x); + + deepStrictEqual(legacyVisibility, []); + }); }); describe("lifecycle transforms", () => { From e85bda64c11a4b9db65c7d5bb0910cffeb9186ab Mon Sep 17 00:00:00 2001 From: Will Temple Date: Thu, 7 Nov 2024 16:25:56 -0500 Subject: [PATCH 28/28] Allow recasting legacy visibility through explicitly calling $visibility from another decorator --- packages/compiler/src/core/visibility/core.ts | 33 +++---------- packages/compiler/src/lib/visibility.ts | 7 +++ .../test/decorators/visibility.test.ts | 23 ++++++++- packages/compiler/test/visibility.test.ts | 47 +++++++++++++++++++ 4 files changed, 82 insertions(+), 28 deletions(-) diff --git a/packages/compiler/src/core/visibility/core.ts b/packages/compiler/src/core/visibility/core.ts index 2d62cc8bf4..796705a878 100644 --- a/packages/compiler/src/core/visibility/core.ts +++ b/packages/compiler/src/core/visibility/core.ts @@ -207,23 +207,7 @@ function groupModifiersByVisibilityClass(modifiers: EnumMember[]): Map("legacyVisibility"); -// const [getLegacyVisibilityIsExplicit, _setLegacyVisibilityIsExplicit] = useStateSet( -// "legacyVisibilityIsExplicit", -// ); - -// /** -// * Sets a flag indicating that legacy visibility modifiers should be considered "explicitly initialized." -// * -// * This is used to differentiate between legacy visibility cases where visibility is _not_ set, and legacy -// * `getVisibility` should return `undefined` and cases where visibility is set to an empty array, and legacy -// * visibility should return `[]`. -// * -// * @param context - the decorator context to use -// * @param property - the property to set the flag for -// */ -// export function setLegacyVisibilityIsExplicit(program: Program, property: ModelProperty) { -// _setLegacyVisibilityIsExplicit(program, property); -// } +export { getLegacyVisibility }; /** * Sets the legacy visibility modifiers for a property. @@ -243,26 +227,23 @@ export function setLegacyVisibility( visibilities: string[], ) { const { program } = context; - compilerAssert( - getLegacyVisibility(program, property) === undefined, - "Legacy visibility modifiers have already been set for this property.", - ); setLegacyVisibilityModifiers(program, property, visibilities); const lifecycleClass = getLifecycleVisibilityEnum(program); - if (visibilities.length === 0 || (visibilities.length === 1 && visibilities[0] === "none")) { - clearVisibilityModifiersForClass(program, property, lifecycleClass, context); - } else { + clearVisibilityModifiersForClass(program, property, lifecycleClass, context); + + const isEmpty = + visibilities.length === 0 || (visibilities.length === 1 && visibilities[0] === "none"); + + if (!isEmpty) { const lifecycleVisibilities = visibilities .map((v) => normalizeLegacyLifecycleVisibilityString(program, v)) .filter((v) => !!v); addVisibilityModifiers(program, property, lifecycleVisibilities); } - - sealVisibilityModifiers(program, property, lifecycleClass); } /** diff --git a/packages/compiler/src/lib/visibility.ts b/packages/compiler/src/lib/visibility.ts index 3fb13c2b07..b9821c6059 100644 --- a/packages/compiler/src/lib/visibility.ts +++ b/packages/compiler/src/lib/visibility.ts @@ -35,6 +35,7 @@ import { clearLegacyVisibility, clearVisibilityModifiersForClass, GeneratedVisibilityFilter, + getLegacyVisibility, getVisibility, isVisible, removeVisibilityModifiers, @@ -256,6 +257,12 @@ export const $visibility: VisibilityDecorator = ( // assertion will fail inside the legacy visibility management API. if (isUnique) setLegacyVisibility(context, target, legacyVisibilities); } else { + if (getLegacyVisibility(context.program, target)) { + reportDiagnostic(context.program, { + code: "visibility-mixed-legacy", + target: context.decoratorTarget, + }); + } addVisibilityModifiers(context.program, target, modifiers, context); } }; diff --git a/packages/compiler/test/decorators/visibility.test.ts b/packages/compiler/test/decorators/visibility.test.ts index 8f83793101..af06ede963 100644 --- a/packages/compiler/test/decorators/visibility.test.ts +++ b/packages/compiler/test/decorators/visibility.test.ts @@ -3,9 +3,9 @@ import { deepStrictEqual, ok, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { Enum, Model, ModelProperty } from "../../src/core/types.js"; +import { DecoratorContext, Enum, Model, ModelProperty } from "../../src/core/types.js"; import { getVisibility, getVisibilityForClass } from "../../src/core/visibility/core.js"; -import { getLifecycleVisibilityEnum } from "../../src/index.js"; +import { $visibility, getLifecycleVisibilityEnum } from "../../src/index.js"; import { BasicTestRunner, createTestRunner } from "../../src/testing/index.js"; function assertSetsEqual(a: Set, b: Set): void { @@ -100,5 +100,24 @@ describe("visibility (legacy)", function () { "update", ]); }); + + it("allows overriding legacy visibility", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility("read") + name: string + } + `)) as { Example: Model }; + + const name = Example.properties.get("name")!; + + const decCtx = { + program: runner.program, + } as DecoratorContext; + + $visibility(decCtx, name, "create"); + + deepStrictEqual(getVisibility(runner.program, name), ["create"]); + }); }); }); diff --git a/packages/compiler/test/visibility.test.ts b/packages/compiler/test/visibility.test.ts index 031eaa8a95..837464c1fe 100644 --- a/packages/compiler/test/visibility.test.ts +++ b/packages/compiler/test/visibility.test.ts @@ -8,6 +8,7 @@ import { $visibility, addVisibilityModifiers, clearVisibilityModifiersForClass, + DecoratorContext, Enum, getLifecycleVisibilityEnum, getVisibilityForClass, @@ -668,6 +669,52 @@ describe("compiler: visibility core", () => { deepStrictEqual(legacyVisibility, []); }); + + it("correctly coerces visibility modifiers after rewriting", async () => { + const { Example } = (await runner.compile(` + @test model Example { + @visibility("create") + x: string; + } + `)) as { Example: Model }; + + const x = Example.properties.get("x")!; + + const LifecycleEnum = getLifecycleVisibilityEnum(runner.program); + + const visibility = getVisibilityForClass(runner.program, x, LifecycleEnum); + + strictEqual(visibility.size, 1); + + const Lifecycle = { + Create: LifecycleEnum.members.get("Create")!, + Read: LifecycleEnum.members.get("Read")!, + Update: LifecycleEnum.members.get("Update")!, + }; + + ok(visibility.has(Lifecycle.Create)); + ok(!visibility.has(Lifecycle.Read)); + ok(!visibility.has(Lifecycle.Update)); + + const legacyVisibility = getVisibility(runner.program, x); + + deepStrictEqual(legacyVisibility, ["create"]); + + // Now change the visibility imperatively using the legacy API + $visibility({ program: runner.program } as DecoratorContext, x, "read"); + + const updatedVisibility = getVisibilityForClass(runner.program, x, LifecycleEnum); + + strictEqual(updatedVisibility.size, 1); + + ok(!updatedVisibility.has(Lifecycle.Create)); + ok(updatedVisibility.has(Lifecycle.Read)); + ok(!updatedVisibility.has(Lifecycle.Update)); + + const updatedLegacyVisibility = getVisibility(runner.program, x); + + deepStrictEqual(updatedLegacyVisibility, ["read"]); + }); }); describe("lifecycle transforms", () => {