From d0d948517562c17b066ae7d523392a139e321d21 Mon Sep 17 00:00:00 2001 From: Wei Hu Date: Fri, 8 Nov 2024 10:34:33 +0800 Subject: [PATCH] Implement scope negation for TCGC (#1783) Resolves https://github.com/Azure/typespec-azure/issues/1596 There are 2 formats of scope negation: ``` @clientName("A", "!(python, java)") Model foo ``` is equivalent to ``` @clientName("A", "!python, !java") Model foo ``` We allow combination of normal scope and negation scope for different scopes ``` @clientName("A", "!python, csharp") Model foo ``` Combination of same scope is also allowed ``` @clientName("A", "!python, python") Model foo ``` is equivalent to ``` @clientName("A", "!python") @clientName("A", "python") Model foo ``` and equivalent to ``` @clientName("A") Model foo ``` The rule for decorator override: - for the same scope, the later decorator value wins regardless it's defined with normal scope or scope negation Detailed override cases can be found in the tests of this PR. --- .../scope-negation-2024-10-1-10-19-14.md | 7 + .../typespec-client-generator-core/README.md | 90 +++--- .../Azure.ClientGenerator.Core.ts | 12 + .../lib/decorators.tsp | 12 + .../src/decorators.ts | 68 ++++- .../src/internal-utils.ts | 2 + .../test/decorators.test.ts | 282 ++++++++++++++++++ .../reference/decorators.md | 90 +++--- 8 files changed, 463 insertions(+), 100 deletions(-) create mode 100644 .chronus/changes/scope-negation-2024-10-1-10-19-14.md diff --git a/.chronus/changes/scope-negation-2024-10-1-10-19-14.md b/.chronus/changes/scope-negation-2024-10-1-10-19-14.md new file mode 100644 index 0000000000..15f26002f6 --- /dev/null +++ b/.chronus/changes/scope-negation-2024-10-1-10-19-14.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Implement scope negation for TCGC decorators \ No newline at end of file diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index b4a717b32e..e83afdb5bc 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -52,10 +52,10 @@ otherwise a warning will be added to diagnostics list. ##### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `EnumMember` | The access info you want to set for this model or operation. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `EnumMember` | The access info you want to set for this model or operation. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -181,10 +181,10 @@ Create a ClientGenerator.Core client out of a namespace or interface ##### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `Model` | Optional configuration for the service. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `Model` | Optional configuration for the service. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -233,10 +233,10 @@ Client parameters you would like to add to the client. By default, we apply endp ##### Parameters -| Name | Type | Description | -| ------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| options | `Model` | | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | `Model` | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -272,10 +272,10 @@ Changes the name of a method, parameter, property, or model generated in the cli ##### Parameters -| Name | Type | Description | -| ------ | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| rename | `valueof string` | The rename you want applied to the object | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| rename | `valueof string` | The rename you want applied to the object | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -341,10 +341,10 @@ Whether you want to generate an operation as a convenient operation. ##### Parameters -| Name | Type | Description | -| ----- | ----------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `valueof boolean` | Whether to generate the operation as convenience method or not. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `valueof boolean` | Whether to generate the operation as convenience method or not. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -369,9 +369,9 @@ Set whether a model property should be flattened or not. ##### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -397,9 +397,9 @@ Create a ClientGenerator.Core operation group out of a namespace or interface ##### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -423,10 +423,10 @@ Override the default client method generated by TCGC from your service definitio ##### Parameters -| Name | Type | Description | -| -------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| override | `Operation` | : The override method definition that specifies the exact client method you want | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| -------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| override | `Operation` | : The override method definition that specifies the exact client method you want | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -482,10 +482,10 @@ Alias the name of a client parameter to a different name. This permits you to ha ##### Parameters -| Name | Type | Description | -| ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| paramAlias | `valueof string` | | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ---------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| paramAlias | `valueof string` | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -522,10 +522,10 @@ Whether you want to generate an operation as a protocol operation. ##### Parameters -| Name | Type | Description | -| ----- | ----------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `valueof boolean` | Whether to generate the operation as protocol or not. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `valueof boolean` | Whether to generate the operation as protocol or not. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -562,10 +562,10 @@ otherwise a warning will be added to diagnostics list. ##### Parameters -| Name | Type | Description | -| ----- | --------------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `EnumMember \| Union` | The usage info you want to set for this model. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `EnumMember \| Union` | The usage info you want to set for this model. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples @@ -633,9 +633,9 @@ Whether a model needs the custom JSON converter, this is only used for backward ##### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | ##### Examples diff --git a/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts b/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts index 7e3793d0e8..b4392e7210 100644 --- a/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts +++ b/packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts @@ -16,6 +16,7 @@ import type { * * @param rename The rename you want applied to the object * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * @clientName("nameInClient") @@ -42,6 +43,7 @@ export type ClientNameDecorator = ( * * @param value Whether to generate the operation as convenience method or not. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * @convenientAPI(false) @@ -60,6 +62,7 @@ export type ConvenientAPIDecorator = ( * * @param value Whether to generate the operation as protocol or not. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * @protocolAPI(false) @@ -78,6 +81,7 @@ export type ProtocolAPIDecorator = ( * * @param value Optional configuration for the service. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example Basic client setting * ```typespec * @client @@ -110,6 +114,7 @@ export type ClientDecorator = ( * Create a ClientGenerator.Core operation group out of a namespace or interface * * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * @operationGroup @@ -141,6 +146,7 @@ export type OperationGroupDecorator = ( * * @param value The usage info you want to set for this model. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example Expand usage for model * ```typespec * op test(): OutputModel; @@ -212,6 +218,7 @@ export type UsageDecorator = ( * * @param value The access info you want to set for this model or operation. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example Set access * ```typespec * // Access.internal @@ -347,6 +354,7 @@ export type AccessDecorator = ( * Set whether a model property should be flattened or not. * * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * model Foo { @@ -369,6 +377,7 @@ export type FlattenPropertyDecorator = ( * @param original : The original service definition * @param override : The override method definition that specifies the exact client method you want * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * // main.tsp @@ -419,6 +428,7 @@ export type OverrideDecorator = ( * Whether a model needs the custom JSON converter, this is only used for backward compatibility for csharp. * * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * @useSystemTextJsonConverter @@ -437,6 +447,7 @@ export type UseSystemTextJsonConverterDecorator = ( * Client parameters you would like to add to the client. By default, we apply endpoint, credential, and api-version parameters. If you add clientInitialization, we will append those to the default list of parameters. * * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * // main.tsp @@ -467,6 +478,7 @@ export type ClientInitializationDecorator = ( * Alias the name of a client parameter to a different name. This permits you to have a different name for the parameter in client initialization then on individual methods and still refer to the same parameter. * * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * @example * ```typespec * // main.tsp diff --git a/packages/typespec-client-generator-core/lib/decorators.tsp b/packages/typespec-client-generator-core/lib/decorators.tsp index 53670bd750..6c344cb287 100644 --- a/packages/typespec-client-generator-core/lib/decorators.tsp +++ b/packages/typespec-client-generator-core/lib/decorators.tsp @@ -6,6 +6,7 @@ namespace Azure.ClientGenerator.Core; * Changes the name of a method, parameter, property, or model generated in the client SDK * @param rename The rename you want applied to the object * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec @@ -28,6 +29,7 @@ extern dec clientName(target: unknown, rename: valueof string, scope?: valueof s * Whether you want to generate an operation as a convenient operation. * @param value Whether to generate the operation as convenience method or not. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec @@ -41,6 +43,7 @@ extern dec convenientAPI(target: Operation, value?: valueof boolean, scope?: val * Whether you want to generate an operation as a protocol operation. * @param value Whether to generate the operation as protocol or not. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec @@ -54,6 +57,7 @@ extern dec protocolAPI(target: Operation, value?: valueof boolean, scope?: value * Create a ClientGenerator.Core client out of a namespace or interface * @param value Optional configuration for the service. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example Basic client setting * ```typespec @@ -82,6 +86,7 @@ extern dec client(target: Namespace | Interface, value?: Model, scope?: valueof /** * Create a ClientGenerator.Core operation group out of a namespace or interface * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec @@ -124,6 +129,7 @@ enum Usage { * otherwise a warning will be added to diagnostics list. * @param value The usage info you want to set for this model. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example Expand usage for model * ```typespec @@ -210,6 +216,7 @@ enum Access { * otherwise a warning will be added to diagnostics list. * @param value The access info you want to set for this model or operation. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example Set access * ```typespec @@ -344,6 +351,7 @@ extern dec access( /** * Set whether a model property should be flattened or not. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec @@ -363,6 +371,7 @@ extern dec flattenProperty(target: ModelProperty, scope?: valueof string); * @param original: The original service definition * @param override: The override method definition that specifies the exact client method you want * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec @@ -409,6 +418,7 @@ extern dec override(original: Operation, override: Operation, scope?: valueof st /** * Whether a model needs the custom JSON converter, this is only used for backward compatibility for csharp. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec @@ -423,6 +433,7 @@ extern dec useSystemTextJsonConverter(target: Model, scope?: valueof string); /** * Client parameters you would like to add to the client. By default, we apply endpoint, credential, and api-version parameters. If you add clientInitialization, we will append those to the default list of parameters. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec @@ -452,6 +463,7 @@ extern dec clientInitialization( /** * Alias the name of a client parameter to a different name. This permits you to have a different name for the parameter in client initialization then on individual methods and still refer to the same parameter. * @param scope The language scope you want this decorator to apply to. If not specified, will apply to all language emitters + * You can use "!" to specify negation such as "!(java, python)" or "!java, !python". * * @example * ```typespec diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index fbee98ce08..abff5e1dcd 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -65,6 +65,7 @@ import { clientNamespaceKey, getValidApiVersion, isAzureCoreTspModel, + negationScopesKey, parseEmitterName, } from "./internal-utils.js"; import { createStateSymbol, reportDiagnostic } from "./lib.js"; @@ -88,6 +89,13 @@ function getScopedDecoratorData( if (languageScope === undefined || typeof languageScope === "string") { const scope = languageScope ?? context.emitterName; if (Object.keys(retval).includes(scope)) return retval[scope]; + + // if the scope is negated, we should return undefined + // if the scope is not negated, we should return the value for AllScopes + const negationScopes = retval[negationScopesKey]; + if (negationScopes !== undefined && negationScopes.includes(scope)) { + return undefined; + } } return retval[AllScopes]; // in this case it applies to all languages } @@ -115,20 +123,60 @@ function setScopedDecoratorData( return; } - // if scope specified, create or overwrite with the new value - const splitScopes = scope.split(",").map((s) => s.trim()); const targetEntry = context.program.stateMap(key).get(target); - - // if target doesn't exist in decorator map, create a new entry - if (!targetEntry) { - const newObject = Object.fromEntries(splitScopes.map((scope) => [scope, value])); + const [negationScopes, scopes] = parseScopes(context, scope); + if (negationScopes !== undefined && negationScopes.length > 0) { + // override the previous value for negation scopes + const newObject: Record = + scopes !== undefined && scopes.length > 0 + ? Object.fromEntries([AllScopes, ...scopes].map((scope) => [scope, value])) + : Object.fromEntries([[AllScopes, value]]); + newObject[negationScopesKey] = negationScopes; context.program.stateMap(key).set(target, newObject); - return; + + // if a scope exists in the target entry and it overlaps with the negation scope, it means negation scope doesn't override it + if (targetEntry !== undefined) { + const existingScopes = Object.getOwnPropertyNames(targetEntry); + const intersections = existingScopes.filter((x) => negationScopes.includes(x)); + if (intersections !== undefined && intersections.length > 0) { + for (const scopeToKeep of intersections) { + newObject[scopeToKeep] = targetEntry[scopeToKeep]; + } + } + } + } else if (scopes !== undefined && scopes.length > 0) { + // for normal scopes, add them incrementally + const newObject = Object.fromEntries(scopes.map((scope) => [scope, value])); + context.program + .stateMap(key) + .set(target, !targetEntry ? newObject : { ...targetEntry, ...newObject }); + } +} + +function parseScopes(context: DecoratorContext, scope?: LanguageScopes): [string[]?, string[]?] { + if (scope === undefined) { + return [undefined, undefined]; + } + + // handle !(scope1, scope2,...) syntax + const negationScopeRegex = new RegExp(/!\((.*?)\)/); + const negationScopeMatch = scope.match(negationScopeRegex); + if (negationScopeMatch) { + return [negationScopeMatch[1].split(",").map((s) => s.trim()), undefined]; } - // if target exists, overwrite existed value - const newObject = Object.fromEntries(splitScopes.map((scope) => [scope, value])); - context.program.stateMap(key).set(target, { ...targetEntry, ...newObject }); + // handle !scope1, !scope2, scope3, ... syntax + const splitScopes = scope.split(",").map((s) => s.trim()); + const negationScopes: string[] = []; + const scopes: string[] = []; + for (const s of splitScopes) { + if (s.startsWith("!")) { + negationScopes.push(s.slice(1)); + } else { + scopes.push(s); + } + } + return [negationScopes, scopes]; } const clientKey = createStateSymbol("client"); diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index e14ac4d92e..945e35470a 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -52,6 +52,8 @@ export const AllScopes = Symbol.for("@azure-core/typespec-client-generator-core/ export const clientNameKey = createStateSymbol("clientName"); export const clientNamespaceKey = createStateSymbol("clientNamespace"); +export const negationScopesKey = createStateSymbol("negationScopes"); + /** * * @param emitterName Full emitter name diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 511d1cab8a..0cae24b40b 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -3027,4 +3027,286 @@ describe("typespec-client-generator-core: decorators", () => { strictEqual(uploadOp.parameters[0].correspondingMethodParams[0], blobName); }); }); + + describe("scope negation", () => { + it("single scope negation", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamed", "!csharp") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "Test"); + ok(testModel); + }); + + it("multiple scopes negation", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamed", "!(csharp, java)") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "Test"); + ok(testModel); + }); + + it("non-negation scope", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamed", "!(python, java)") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "TestRenamed"); + ok(testModel); + }); + + it("allow combination of negation scope and normal scope", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamed", "csharp, !java") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "TestRenamed"); + ok(testModel); + }); + + it("allow combination of negation scope and normal scope for the same scope", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamed", "!csharp, csharp") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "TestRenamed"); + ok(testModel); + }); + + it("allow combination of negation scope and normal scope for the same multiple scopes", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamed", "!csharp, csharp, python, !python, java") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "TestRenamed"); + ok(testModel); + }); + + it("allow multiple separated negation scopes", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamed", "!csharp, !java") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "Test"); + ok(testModel); + }); + + it("negation scope override normal scope", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamedAgain", "!python, !java") + @clientName("TestRenamed", "csharp") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "TestRenamedAgain"); + ok(testModel); + }); + + it("normal scope incrementally add", async () => { + const tsp = ` + @service + @test namespace MyService { + @test + @clientName("TestRenamedAgain", "csharp") + @clientName("TestRenamed", "!python, !java") + model Test { + prop: string; + } + @test + @access(Access.internal) + op func( + @body body: Test + ): void; + } + `; + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(tsp); + const csharpSdkPackage = runnerWithCSharp.context.sdkPackage; + const csharpTestModel = csharpSdkPackage.models.find((x) => x.name === "TestRenamedAgain"); + ok(csharpTestModel); + + const runnerWithPython = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-python", + }); + await runnerWithPython.compile(tsp); + const pythonSdkPackage = runnerWithPython.context.sdkPackage; + const pythonTestModel = pythonSdkPackage.models.find((x) => x.name === "Test"); + ok(pythonTestModel); + }); + + it("negation scope override negation scope", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamedAgain", "!python, !java") + @clientName("TestRenamed", "!go") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "TestRenamedAgain"); + ok(testModel); + }); + + it("negation scope override normal scope with the same scope", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamedAgain", "!csharp") + @clientName("TestRenamed", "csharp") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "TestRenamed"); + ok(testModel); + }); + + it("normal scope override negation scope with the same scope", async () => { + const runnerWithCSharp = await createSdkTestRunner({ + emitterName: "@azure-tools/typespec-csharp", + }); + await runnerWithCSharp.compile(` + @service + namespace MyService { + @clientName("TestRenamedAgain", "csharp") + @clientName("TestRenamed", "!csharp") + model Test { + prop: string; + } + op func( + @body body: Test + ): void; + } + `); + + const sdkPackage = runnerWithCSharp.context.sdkPackage; + const testModel = sdkPackage.models.find((x) => x.name === "TestRenamedAgain"); + ok(testModel); + }); + }); }); diff --git a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md index 4aa1ebd1a5..3fc955f9ee 100644 --- a/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md +++ b/website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md @@ -32,10 +32,10 @@ otherwise a warning will be added to diagnostics list. #### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `EnumMember` | The access info you want to set for this model or operation. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `EnumMember` | The access info you want to set for this model or operation. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -161,10 +161,10 @@ Create a ClientGenerator.Core client out of a namespace or interface #### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `Model` | Optional configuration for the service. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `Model` | Optional configuration for the service. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -213,10 +213,10 @@ Client parameters you would like to add to the client. By default, we apply endp #### Parameters -| Name | Type | Description | -| ------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| options | `Model` | | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | `Model` | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -252,10 +252,10 @@ Changes the name of a method, parameter, property, or model generated in the cli #### Parameters -| Name | Type | Description | -| ------ | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| rename | `valueof string` | The rename you want applied to the object | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| rename | `valueof string` | The rename you want applied to the object | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -321,10 +321,10 @@ Whether you want to generate an operation as a convenient operation. #### Parameters -| Name | Type | Description | -| ----- | ----------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `valueof boolean` | Whether to generate the operation as convenience method or not. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `valueof boolean` | Whether to generate the operation as convenience method or not. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -351,9 +351,9 @@ Set whether a model property should be flattened or not. #### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -379,9 +379,9 @@ Create a ClientGenerator.Core operation group out of a namespace or interface #### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -405,10 +405,10 @@ Override the default client method generated by TCGC from your service definitio #### Parameters -| Name | Type | Description | -| -------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| override | `Operation` | : The override method definition that specifies the exact client method you want | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| -------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| override | `Operation` | : The override method definition that specifies the exact client method you want | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -464,10 +464,10 @@ Alias the name of a client parameter to a different name. This permits you to ha #### Parameters -| Name | Type | Description | -| ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| paramAlias | `valueof string` | | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ---------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| paramAlias | `valueof string` | | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -504,10 +504,10 @@ Whether you want to generate an operation as a protocol operation. #### Parameters -| Name | Type | Description | -| ----- | ----------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `valueof boolean` | Whether to generate the operation as protocol or not. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `valueof boolean` | Whether to generate the operation as protocol or not. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -544,10 +544,10 @@ otherwise a warning will be added to diagnostics list. #### Parameters -| Name | Type | Description | -| ----- | --------------------- | ------------------------------------------------------------------------------------------------------------- | -| value | `EnumMember \| Union` | The usage info you want to set for this model. | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | `EnumMember \| Union` | The usage info you want to set for this model. | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples @@ -615,9 +615,9 @@ Whether a model needs the custom JSON converter, this is only used for backward #### Parameters -| Name | Type | Description | -| ----- | ---------------- | ------------------------------------------------------------------------------------------------------------- | -| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters | +| Name | Type | Description | +| ----- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| scope | `valueof string` | The language scope you want this decorator to apply to. If not specified, will apply to all language emitters
You can use "!" to specify negation such as "!(java, python)" or "!java, !python". | #### Examples