Skip to content

Commit

Permalink
Implement scope negation for TCGC (#1783)
Browse files Browse the repository at this point in the history
Resolves #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.
  • Loading branch information
live1206 authored Nov 8, 2024
1 parent ec0dd0e commit d0d9485
Show file tree
Hide file tree
Showing 8 changed files with 463 additions and 100 deletions.
7 changes: 7 additions & 0 deletions .chronus/changes/scope-negation-2024-10-1-10-19-14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

Implement scope negation for TCGC decorators
90 changes: 45 additions & 45 deletions packages/typespec-client-generator-core/README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions packages/typespec-client-generator-core/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
68 changes: 58 additions & 10 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
clientNamespaceKey,
getValidApiVersion,
isAzureCoreTspModel,
negationScopesKey,
parseEmitterName,
} from "./internal-utils.js";
import { createStateSymbol, reportDiagnostic } from "./lib.js";
Expand All @@ -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
}
Expand Down Expand Up @@ -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<string | symbol, any> =
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");
Expand Down
2 changes: 2 additions & 0 deletions packages/typespec-client-generator-core/src/internal-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit d0d9485

Please sign in to comment.