Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement scope negation for TCGC #1783

Merged
merged 32 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
29850a2
Implement scope negation
live1206 Nov 1, 2024
8ad4158
Merge branch 'main' into scope-negation
live1206 Nov 1, 2024
d273666
changelog
live1206 Nov 1, 2024
2c9a8dd
lint
live1206 Nov 1, 2024
db6b21c
add more tests
live1206 Nov 1, 2024
8ebaa82
Merge remote-tracking branch 'upstream/main' into scope-negation
live1206 Nov 1, 2024
4384698
refactor to allow multiple separated negation scopes
live1206 Nov 1, 2024
dce6f76
add test for negation scope override
live1206 Nov 1, 2024
014cefc
update doc for scope
live1206 Nov 1, 2024
1ef553b
typo
live1206 Nov 1, 2024
6faecc0
regen docs
live1206 Nov 1, 2024
0626dc3
simplify
live1206 Nov 1, 2024
5b1e1d7
format
live1206 Nov 1, 2024
a6042f6
simplify
live1206 Nov 2, 2024
01d4086
Merge branch 'main' into scope-negation
live1206 Nov 2, 2024
449d010
refine for scope combination and overridden
live1206 Nov 4, 2024
0b83c84
format
live1206 Nov 4, 2024
230e8d3
fix test
live1206 Nov 4, 2024
d4771b1
lint
live1206 Nov 4, 2024
2c48477
only override negation scopes, keep normal scopes unchanged
live1206 Nov 4, 2024
8d53dd3
update test
live1206 Nov 4, 2024
cce6ee5
Merge branch 'main' into scope-negation
live1206 Nov 4, 2024
781a7f1
Merge remote-tracking branch 'upstream/main' into scope-negation
live1206 Nov 6, 2024
1c3ec2c
negation scope should override previous state map
live1206 Nov 7, 2024
dd697af
Merge remote-tracking branch 'upstream/main' into scope-negation
live1206 Nov 7, 2024
3557bb5
update
live1206 Nov 7, 2024
f275e6e
handle case of normal scope and negation scope override
live1206 Nov 7, 2024
a1a7de3
remove error check for scope negation
live1206 Nov 7, 2024
28e0aa1
update docs
live1206 Nov 7, 2024
b47ceb8
update
live1206 Nov 7, 2024
ccddb2a
refine tests
live1206 Nov 7, 2024
517c2e8
fix format
live1206 Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Large diffs are not rendered by default.

90 changes: 45 additions & 45 deletions packages/typespec-client-generator-core/README.md

Large diffs are not rendered by default.

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". Please note that, negation scope and normal scope should not be mixed.
XiaofeiCao marked this conversation as resolved.
Show resolved Hide resolved
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @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". Please note that, negation scope and normal scope should not be mixed.
*
* @example
* ```typespec
Expand Down
69 changes: 59 additions & 10 deletions packages/typespec-client-generator-core/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
clientNameKey,
getValidApiVersion,
isAzureCoreTspModel,
negationScopesKey,
parseEmitterName,
} from "./internal-utils.js";
import { createStateSymbol, reportDiagnostic } from "./lib.js";
Expand All @@ -86,6 +87,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 @@ -113,20 +121,61 @@ 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) {
const newObject = Object.fromEntries([AllScopes].map((scope) => [scope, value]));
newObject[negationScopesKey] = negationScopes;
context.program.stateMap(key).set(target, newObject);
return;
} else if (scopes !== undefined && scopes.length > 0) {
// if scope specified, create or overwrite with the new value
const targetEntry = context.program.stateMap(key).get(target);
const newObject = Object.fromEntries(scopes.map((scope) => [scope, value]));

// if target doesn't exist in decorator map, create a new entry
// otherwise, overwrite existed value
context.program
.stateMap(key)
.set(target, !targetEntry ? newObject : { ...targetEntry, ...newObject });
}
}

// This function is used to parse the scopes from the decorator and return the negation scopes and normal scopes
// When the scope is !(scope1, scope2,...), it will return [negationScopes, undefined]
// When the scope is !scope1, !scope2, ..., it will return [negationScopes, undefined]
// When the scope is !scope1, scope2, ..., it will report error of invalid-negation-scope
// When the scope is scope1, scope2, ..., it will return [undefined, normalScopes]
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, ... syntax and throw on the combination of negation and normal scopes
const splitScopes = scope.split(",").map((s) => s.trim());
const negationScopes = [];
const scopes = [];
for (const s of splitScopes) {
if (s.startsWith("!")) {
negationScopes.push(s.slice(1));
} else {
scopes.push(s);
}
}
if (negationScopes.length > 0 && scopes.length > 0) {
reportDiagnostic(context.program, {
code: "invalid-negation-scope",
target: context.decoratorTarget,
});
return [undefined, undefined];
}
return [negationScopes, scopes];
}

const clientKey = createStateSymbol("client");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export const AllScopes = Symbol.for("@azure-core/typespec-client-generator-core/

export const clientNameKey = createStateSymbol("clientName");

export const negationScopesKey = "negationScopes";

/**
*
* @param emitterName Full emitter name
Expand Down
6 changes: 6 additions & 0 deletions packages/typespec-client-generator-core/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`Decorator ${"decoratorName"} cannot be used twice on the same declaration with same scope.`,
},
},
"invalid-negation-scope": {
severity: "error",
messages: {
default: `Negation scope should not be combined with normal scope.`,
},
},
},
});

Expand Down
6 changes: 4 additions & 2 deletions packages/typespec-client-generator-core/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { DuplicateTracker } from "@typespec/compiler/utils";
import { createTCGCContext, getClientNameOverride } from "./decorators.js";
import { TCGCContext } from "./interfaces.js";
import { AllScopes, clientNameKey } from "./internal-utils.js";
import { AllScopes, clientNameKey, negationScopesKey } from "./internal-utils.js";
import { reportDiagnostic } from "./lib.js";

export function $onValidate(program: Program) {
Expand All @@ -36,7 +36,9 @@ function getDefinedLanguageScopes(program: Program): Set<string | typeof AllScop
languageScopes.add(AllScopes);
}
for (const languageScope of Object.keys(value)) {
languageScopes.add(languageScope);
if (languageScope !== negationScopesKey) {
languageScopes.add(languageScope);
}
}
}
return languageScopes;
Expand Down
Loading
Loading