Skip to content

[tcgc] allow last param in decorator to accept scope or other options - #5305

Open
iscai-msft wants to merge 19 commits into
Azure:mainfrom
iscai-msft:iscai-msft-tcgc-scope-alias-255
Open

[tcgc] allow last param in decorator to accept scope or other options #5305
iscai-msft wants to merge 19 commits into
Azure:mainfrom
iscai-msft:iscai-msft-tcgc-scope-alias-255

Conversation

@iscai-msft

Copy link
Copy Markdown
Member

Summary

First small step toward #5254 (Make TCGC decorator scope arguments evolvable with typed options).

This PR centralizes the type used for the scope argument across all scoped TCGC decorators into a single Azure.ClientGenerator.Core.Scope alias, defined once in decorators.tsp and reused from legacy.tsp. Having one shared alias means the scope type can evolve (e.g. toward a typed options bag as described in the issue) in one place instead of updating every decorator signature individually.

Changes

  • Added alias Scope = string; in decorators.tsp with doc comments describing supported language identifiers and valid patterns (moved from the previous per-decorator @param scope docs).
  • Replaced every scope?: valueof string parameter in decorators.tsp with scope?: valueof Scope.
  • Replaced every scope?: valueof string parameter in legacy.tsp with scope?: valueof Azure.ClientGenerator.Core.Scope (different namespace).
  • Added tests in test/decorators/scope.test.ts verifying the alias can be referenced from user TypeSpec and that scoped decorators continue to behave identically.
  • Added a changeset.

Compatibility

No behavior or public API change — decorators still accept the existing string scope syntax ("python", "python, java", "!csharp", etc.). Verified generated-defs/*.ts is unchanged after rebuilding, and the full TCGC test suite (1405 tests) passes.

Testing

  • pnpm --filter @azure-tools/typespec-client-generator-core build
  • pnpm --filter @azure-tools/typespec-client-generator-core test (1405 passed, 2 skipped)
  • pnpm --filter @azure-tools/typespec-client-generator-core lint
  • pnpm format

Closes part of the first workstream in #5254; the remaining deliverables (typed options bag, diagnostics for conflicting scopes, codefixes, migration docs) are left for follow-up PRs.

iscai-msft and others added 3 commits April 30, 2025 16:17
Addresses Azure#5254 by centralizing the type used for the
`scope` argument across all scoped TCGC decorators into a single
`Azure.ClientGenerator.Core.Scope` alias, defined once in decorators.tsp
and reused by legacy.tsp. This lets the scope type evolve (e.g. toward a
typed options bag) in one place instead of updating every decorator
signature individually. No behavior change; all decorators keep accepting
the existing string scope syntax.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
iscai-msft and others added 2 commits August 25, 2026 13:47
…alias-255

# Conflicts:
#	packages/typespec-client-generator-core/lib/decorators.tsp
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • @azure-tools/typespec-client-generator-core
Show changes

@azure-tools/typespec-client-generator-core - feature ✏️

Make the scope argument accepted by scoped TCGC decorators evolvable via a shared, typed Azure.ClientGenerator.Core.DecoratorOptions model. Every scoped decorator now accepts either the legacy plain-string scope (e.g. "csharp") or a typed options bag (e.g. #{ scope: "csharp" }), and individual decorators can later grow their own options model that extends DecoratorOptions without breaking others.,> ,> @client's ClientOptions and @clientInitialization's ClientInitializationOptions now also accept scope directly (both extend DecoratorOptions). If the options bag scope disagrees with the legacy positional argument, TCGC reports a conflicting-scope warning and prefers the options bag value. Decorators that already have an options bag keep a single options bag — the legacy positional scope stays a plain string purely for backward compatibility. See design-docs/decorator-options-migration.md for migration guidance and deprecation policy.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📦 Package size report

4 packages changed size, +15.68 KB (+0.1%) packed overall.

Package Packed (base → head) Δ Packed Unpacked (base → head) Δ Unpacked
@azure-tools/typespec-ts 532.69 KB → 540.54 KB +7.84 KB (+1.5%) 🔴 2.56 MB → 2.60 MB +38.87 KB (+1.5%) 🔴
@azure-tools/typespec-client-generator-core 229.25 KB → 233.45 KB +4.20 KB (+1.8%) 🔴 1.23 MB → 1.25 MB +24.30 KB (+1.9%) 🔴
@azure-tools/typespec-go 259.66 KB → 261.39 KB +1.73 KB (+0.7%) 🔴 1.33 MB → 1.34 MB +8.24 KB (+0.6%) 🔴
@azure-tools/typespec-azure-rulesets 5.16 KB → 5.27 KB +112 B (+2.1%) 32.09 KB → 32.97 KB +902 B (+2.7%) 🔴
9 package(s) with no notable change
Package Packed (base → head) Δ Packed Unpacked (base → head) Δ Unpacked
@azure-tools/typespec-java 13.51 MB → 13.51 MB +1.38 KB (+0.0%) 15.03 MB → 15.03 MB +3.05 KB (+0.0%)
@azure-tools/typespec-azure-resource-manager 174.56 KB → 174.78 KB +225 B (+0.1%) 1.06 MB → 1.06 MB +1.88 KB (+0.2%)
@azure-tools/typespec-autorest 80.93 KB → 81.01 KB +85 B (+0.1%) 395.06 KB → 395.40 KB +348 B (+0.1%)
@azure-tools/azure-http-specs 146.63 KB → 146.71 KB +76 B (+0.1%) 1.16 MB → 1.16 MB +415 B (+0.0%)
@azure-tools/typespec-azure-core 130.81 KB → 130.84 KB +39 B (+0.0%) 709.51 KB → 709.84 KB +336 B (+0.0%)
@azure-tools/typespec-azure-portal-core 42.40 KB → 42.40 KB -3 B (-0.0%) 192.91 KB → 192.91 KB
@azure-tools/typespec-autorest-canonical 7.42 KB → 7.42 KB 26.00 KB → 26.00 KB
@azure-tools/typespec-metadata 15.91 KB → 15.91 KB 62.26 KB → 62.26 KB
@azure-tools/typespec-python 42.22 KB → 42.22 KB 164.91 KB → 164.91 KB

Packed = gzipped .tgz published to npm. Unpacked = total extracted size. 🆕 added, 🗑️ removed. Packages from the core/ submodule are not included.
🔴 grew · 🟢 shrank — only changes of at least 512 B and 0.5% are marked.

@iscai-msft iscai-msft changed the title [tcgc] Introduce shared Scope alias for decorator scope arguments [tcgc] allow last param in decorator to accept scope or other options Aug 25, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@azure-tools/typespec-client-generator-core@5305

commit: bc0758c

Per issue feedback, the shared Scope alias is now ScopeOptions | string
instead of a plain string alias, so decorators accept either the legacy
string scope or a typed options bag (e.g. #{ scope: "csharp" }).
Individual decorators can later define their own options model extending
ScopeOptions without breaking others. Centralizes scope normalization in
internal-utils.ts, updates all decorator implementations and generated
defs/docs, and adds tests covering the options-bag form and its
equivalence with the legacy string form.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a9d81d1b-44c2-44db-87a1-87df6a805d39
@microsoft-github-policy-service microsoft-github-policy-service Bot added the meta:website TypeSpec.io updates label Aug 25, 2026
@azure-sdk-automation

Copy link
Copy Markdown
Contributor

You can try these changes here

🛝 Playground 🌐 Website

- Add scope to @client's ClientOptions with a conflicting-scope diagnostic when it disagrees with the legacy positional argument
- Add an invalid-scope diagnostic for malformed scope strings (empty, whitespace-only, empty grouped negation, empty comma-list entries), for both legacy string and options-bag forms
- Add a scope-options-migration linter rule (enabled by default) that flags legacy positional string scope arguments and offers a code fix to rewrite them as the typed options bag
- Document the deprecation policy and migration guidance in design-docs/scope-options-migration.md
- Expand scope compatibility tests to cover more decorators and grouped/negated/mixed forms
- Update the changeset to describe the full set of changes

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a9d81d1b-44c2-44db-87a1-87df6a805d39
@microsoft-github-policy-service microsoft-github-policy-service Bot added int:azure-specs Run integration tests against azure-rest-api-specs linter Issues related to linter rules labels Aug 25, 2026
iscai-msft and others added 4 commits August 25, 2026 15:02
…-options behavior

- Removed the default-enabled scope-options-migration linter rule and its
  codefix, doc, and tests. The issue only asked for migration guidance, not
  a default-on rule that would surface warnings across existing specs.
- Trimmed the changeset description to the main points.
- Documented that passing a value typed with additional properties beyond
  ScopeOptions to a decorator still using the shared Scope alias will not
  type-check; a decorator must update its own scope parameter type to
  accept an extended options model.
- Regenerated linter reference docs and README after removing the rule.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a9d81d1b-44c2-44db-87a1-87df6a805d39
- Removed the invalid-scope diagnostic and isValidScopeString validation.
  There was no prior scope-format validation, and the issue didn't call for
  adding it; malformed scope strings are now handled the same way they
  always were (parsed as-is, with no diagnostic).
- Changed conflicting-scope from an error to a warning. When @client's
  ClientOptions.scope disagrees with the legacy positional scope argument,
  TCGC now uses the options bag value and ignores the legacy argument,
  instead of skipping the decorator entirely.
- Updated diagnostic docs, design doc, changeset, and tests accordingly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a9d81d1b-44c2-44db-87a1-87df6a805d39
Updated the stale doc comment for @client's options param (still said
"produces a diagnostic" without noting it's now a warning that prefers the
options bag value and regenerated derived docs/generated-defs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a9d81d1b-44c2-44db-87a1-87df6a805d39
EOF
)
- Rename the shared scoped decorator options model to DecoratorOptions.
- Remove the public Scope alias and use DecoratorOptions | string directly
  in scoped decorator signatures to avoid collisions with service models named Scope.
- Update legacy signatures, generated definitions, tests, reference docs,
  and migration/deprecation guidance.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a9d81d1b-44c2-44db-87a1-87df6a805d39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR advances TCGC’s “scoped decorator” story by introducing a shared, typed DecoratorOptions options-bag model for decorator scope, updating all scoped decorator signatures to accept either the legacy string scope or an options bag, and extending @client to also accept scope on ClientOptions with a warning diagnostic when the two scope mechanisms conflict.

Changes:

  • Add Azure.ClientGenerator.Core.DecoratorOptions and update all scoped decorators (including Legacy) to accept DecoratorOptions | string for the final scope parameter.
  • Centralize scope-shape normalization in normalizeScope and introduce the conflicting-scope warning for @client when ClientOptions.scope disagrees with the legacy positional scope argument.
  • Add/extend unit tests, update generated defs, and refresh documentation + a changeset to reflect the new options-bag shape.

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/index.mdx Adds DecoratorOptions to the reference index.
website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/decorators.md Updates decorator signatures/docs to reflect `DecoratorOptions
website/src/content/docs/docs/libraries/typespec-client-generator-core/reference/data-types.md Documents the new DecoratorOptions model.
packages/typespec-client-generator-core/test/decorators/scope.test.ts Adds coverage for legacy vs options-bag scope equivalence across decorators.
packages/typespec-client-generator-core/test/decorators/client.test.ts Adds coverage for @client scope via ClientOptions.scope and conflict warnings.
packages/typespec-client-generator-core/src/lib.ts Registers the conflicting-scope warning diagnostic message.
packages/typespec-client-generator-core/src/internal-utils.ts Adds normalizeScope helper to unify handling of string vs options-bag scope.
packages/typespec-client-generator-core/src/interfaces.ts Introduces runtime TS DecoratorOptions interface for decorator implementations.
packages/typespec-client-generator-core/src/diagnostics/conflicting-scope.md Adds diagnostic documentation for conflicting @client scopes.
packages/typespec-client-generator-core/src/decorators.ts Updates all scoped decorators to accept options-bag scope and implements @client conflict warning behavior.
packages/typespec-client-generator-core/README.md Updates README decorator reference signatures to include the options-bag scope type.
packages/typespec-client-generator-core/lib/legacy.tsp Updates legacy extern decorator signatures to accept `DecoratorOptions
packages/typespec-client-generator-core/lib/decorators.tsp Defines DecoratorOptions, updates extern decorator signatures, and makes ClientOptions extend DecoratorOptions.
packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.ts Updates generated decorator types and emits DecoratorOptions interface.
packages/typespec-client-generator-core/generated-defs/Azure.ClientGenerator.Core.Legacy.ts Updates generated legacy decorator types to accept `DecoratorOptions
packages/typespec-client-generator-core/design-docs/decorator-options-migration.md Adds migration/deprecation policy guidance for the options-bag approach.
.chronus/changes/tcgc-scope-alias-2026-8-25-13-40-0.md Adds a feature changeset describing the new evolvable scope/options shape.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/typespec-client-generator-core/src/decorators.ts Outdated
…on doc

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a9d81d1b-44c2-44db-87a1-87df6a805d39
Comment thread packages/typespec-client-generator-core/src/decorators.ts Outdated
Comment thread packages/typespec-client-generator-core/src/decorators.ts
Comment thread packages/typespec-client-generator-core/src/decorators.ts Outdated
Comment thread packages/typespec-client-generator-core/src/decorators.ts
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a9d81d1b-44c2-44db-87a1-87df6a805d39
Comment thread packages/typespec-client-generator-core/src/decorators.ts Outdated
iscai-msft and others added 4 commits September 1, 2026 13:57
Canonicalize each scope string into the set of emitters it actually
selects before reporting a conflict, so mixed forms like "!java" and
"csharp, !java" no longer produce a false-positive warning.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Revert @client, @operationGroup, and @clientInitialization scope parameters to accept only string (LanguageScopes), not DecoratorOptions
- Make ClientInitializationOptions extend DecoratorOptions for consistency
- Move scope configuration to the options model properties instead of the scope parameter
- This eliminates confusion from having two option bags and provides a single, unified options interface

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81756fca-9e84-4b3d-8480-b53d855093d2
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81756fca-9e84-4b3d-8480-b53d855093d2
…zation

Both @client (ClientOptions) and @clientInitialization (ClientInitializationOptions)
already have their own options bag. Keep a single options bag per decorator: scope
lives in that bag (model extends DecoratorOptions) and the legacy positional scope
argument stays a plain string purely for backward compatibility.

- Extract a shared resolveScopeFromOptions helper that reads options.scope, reports
  the conflicting-scope diagnostic when it disagrees with the legacy positional scope,
  and prefers the options bag value.
- Wire both $client and $clientInitialization through the helper. Previously
  ClientInitializationOptions extended DecoratorOptions but its scope property was
  silently ignored.
- Regenerate extern signatures and reference docs; update changeset and migration doc.
- Add @clientInitialization scope-in-options tests (positive, conflict, equivalent).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81756fca-9e84-4b3d-8480-b53d855093d2
Comment thread packages/typespec-client-generator-core/src/internal-utils.ts
Comment thread packages/typespec-client-generator-core/src/decorators.ts
Comment thread packages/typespec-client-generator-core/src/decorators.ts Outdated
Comment thread .chronus/changes/tcgc-scope-alias-2026-8-25-13-40-0.md
Addresses JoshLove-msft's review batch:

- P1: getTypeDecorators now normalizes the scope argument (legacy string or
  DecoratorOptions bag) before materializing decorator arguments, so an
  options-bag scope on an allowlisted decorator like @clientoption no longer
  crashes SDK generation with 'scope.match is not a function'.
- P1: @clientInitialization only reads scope from a genuine ClientInitializationOptions
  bag. A legacy raw client-parameters model that happens to contain a scope
  property is preserved as a real client parameter instead of being consumed as
  an emitter selector.
- P2: a scope-only options bag ({ scope: 'csharp' }) is no longer misclassified as
  the legacy parameters model, so it does not surface an unintended 'scope' client
  initialization parameter.
- P2: @operationGroup now accepts 'DecoratorOptions | string' and normalizes the
  scope before delegating to @client, so @operationGroup(#{ scope: 'python' }) works.

Adds regression tests for each case and regenerates extern signatures and docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81756fca-9e84-4b3d-8480-b53d855093d2
Comment thread packages/typespec-client-generator-core/src/decorators.ts Outdated
…ns provenance

Distinguish a real `ClientInitializationOptions` options bag from a legacy raw
client-parameters model using model provenance (ancestry) instead of the model
name. A named model that derives from `ClientInitializationOptions` is an options
bag even when it only sets `scope`, so its scope selects the emitter and never
leaks as a client parameter. An anonymous scope-only literal is kept as a legacy
raw parameters model so its `scope` remains a real client parameter, preserving
backward compatibility for that previously supported form.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81756fca-9e84-4b3d-8480-b53d855093d2
return false;
}
for (
let current: Model | undefined = options.baseModel;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Traverse source-model provenance when identifying initialization options

This follows only baseModel, so standard model transformations lose the options-bag identity. For example:

model Base extends ClientInitializationOptions {
  scope: "csharp";
}

model Copy is PickProperties<Base, "scope">;
@@clientInitialization(MyService, Copy);

Copy has source-model provenance but no baseModel, so it is treated as legacy client parameters. The generated clients expose a public scope parameter for both C# and Python instead of using it to select C#. Please traverse explicit sourceModel/sourceModels provenance for named models as well as baseModel, and add coverage for PickProperties/OmitProperties or model is.

--generated by Copilot

legacyScope?: string,
): string | undefined {
const optionsScopeConfig =
options?.kind === "Model" ? options.properties.get("scope")?.type : undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Resolve scope through the effective options inheritance chain

This reads only the leaf model's properties, which excludes properties inherited with extends. For example:

model Base extends ClientInitializationOptions {
  scope: "csharp";
}

model Final extends Base {
  parameters: Params;
}

Using Final with @clientInitialization applies the customization to Python as well as C#, and an inherited scope conflicting with the positional argument produces no warning. The same issue affects an inherited ClientOptions.scope on @client. Please resolve the nearest effective scope through the model inheritance chain and add multi-level inheritance tests for both decorators.

--generated by Copilot

// remaining decorator arguments, otherwise the options-bag form is run through
// `getDecoratorArgValue` (which converts it to an SDK model) and later handed to the
// string-only `isScopeApplicable`, crashing with `scope.match is not a function`.
if (parameterName === "scope") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Preserve type-level string scopes on allowlisted decorators

This special-cases every parameter named scope, but existing custom decorators can declare a type-level parameter:

extern dec scopedMarker(target: unknown, scope: string);

@scopedMarker("python")
model Payload {}

For this signature, jsValue is a compiler string-literal type rather than a JavaScript string. Passing it directly to normalizeScope produces undefined, so emitted decorator metadata contains scope: undefined and the decorator is included for every emitter instead of only Python. Please preserve the normal getDecoratorArgValue conversion for compiler string literals, or restrict this normalization path to primitive strings and actual DecoratorOptions values.

--generated by Copilot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

int:azure-specs Run integration tests against azure-rest-api-specs lib:tcgc Issues for @azure-tools/typespec-client-generator-core library linter Issues related to linter rules meta:website TypeSpec.io updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants