Skip to content

Commit f493b8e

Browse files
markcowlCopilot
andcommitted
Add @featureFile decorators in Azure.ResourceManager and fix template suppression propagation
- Add @featureFile, @featureFiles, @featureFileOptions decorators in Azure.ResourceManager namespace as alternatives to the Legacy @feature, @features, @featureOptions decorators - Fix arm-custom-resource-usage-discourage rule to propagate suppressions from model templates to their instantiations - Regenerate reference docs - Update tests to use new decorator names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent aa83552 commit f493b8e

12 files changed

Lines changed: 353 additions & 36 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@azure-tools/typespec-azure-resource-manager"
5+
- "@azure-tools/typespec-autorest"
6+
---
7+
8+
Add `@featureFile`, `@featureFiles`, and `@featureFileOptions` decorators in `Azure.ResourceManager` namespace as alternatives to the Legacy `@feature`, `@features`, and `@featureOptions` decorators. Fix `arm-custom-resource-usage-discourage` rule to propagate suppressions from model templates to their instantiations.

packages/typespec-autorest/test/arm/arm.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ it("generates PATCH bodies for resource patch of common resource envelope mixins
509509
it("can split resources and operations by feature", async () => {
510510
const { privateLink, privateEndpoint } = await CompileOpenApiWithFeatures(
511511
`
512-
@Azure.ResourceManager.Legacy.features(Features)
512+
@Azure.ResourceManager.featureFiles(Features)
513513
@armProviderNamespace
514514
@armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v5)
515515
namespace Microsoft.PrivateLinkTest;
@@ -522,7 +522,7 @@ it("can split resources and operations by feature", async () => {
522522
523523
interface Operations extends Azure.ResourceManager.Operations {}
524524
525-
@Azure.ResourceManager.Legacy.feature(Features.privateEndpoint)
525+
@Azure.ResourceManager.featureFile(Features.privateEndpoint)
526526
@tenantResource
527527
model PrivateEndpointConnectionResource is ProxyResource<PrivateEndpointConnectionProperties> {
528528
@path
@@ -531,20 +531,20 @@ it("can split resources and operations by feature", async () => {
531531
name: string;
532532
}
533533
534-
@Azure.ResourceManager.Legacy.feature(Features.privateEndpoint)
534+
@Azure.ResourceManager.featureFile(Features.privateEndpoint)
535535
@armResourceOperations(PrivateEndpointConnectionResource)
536536
interface PrivateEndpointConnections {
537537
#suppress "deprecated" "PrivateLinkResourceListResultV5 validation"
538538
listConnections is ArmResourceListByParent<PrivateEndpointConnectionResource,
539539
Response = ArmResponse<Azure.ResourceManager.CommonTypes.PrivateEndpointConnectionListResultV5>>;
540540
}
541541
542-
@Azure.ResourceManager.Legacy.feature(Features.privateLink)
542+
@Azure.ResourceManager.featureFile(Features.privateLink)
543543
model PrivateLinkResource is ProxyResource<PrivateLinkResourceProperties> {
544544
...PrivateLinkResourceParameter;
545545
}
546546
547-
@Azure.ResourceManager.Legacy.feature(Features.privateLink)
547+
@Azure.ResourceManager.featureFile(Features.privateLink)
548548
@armResourceOperations(PrivateLinkResource)
549549
interface PrivateLinkResources {
550550
#suppress "deprecated" "PrivateLinkResourceListResultV5 validation"
@@ -576,49 +576,49 @@ it("can represent type references within and between features", async () => {
576576
const { featureA, featureB, shared } = await CompileOpenApiWithFeatures(
577577
`
578578
579-
@Azure.ResourceManager.Legacy.features(Features)
579+
@Azure.ResourceManager.featureFiles(Features)
580580
@armProviderNamespace("Microsoft.Test")
581581
namespace Microsoft.Test;
582582
enum Features {
583-
@Azure.ResourceManager.Legacy.featureOptions(#{featureName: "Common", fileName: "shared", description: "The data for common features"})
583+
@Azure.ResourceManager.featureFileOptions(#{featureName: "Common", fileName: "shared", description: "The data for common features"})
584584
Common: "Common",
585-
@Azure.ResourceManager.Legacy.featureOptions(#{featureName: "FeatureA", fileName: "featureA", description: "The data for feature A"})
585+
@Azure.ResourceManager.featureFileOptions(#{featureName: "FeatureA", fileName: "featureA", description: "The data for feature A"})
586586
FeatureA: "Feature A",
587-
@Azure.ResourceManager.Legacy.featureOptions(#{featureName: "FeatureB", fileName: "featureB", description: "The data for feature B"})
587+
@Azure.ResourceManager.featureFileOptions(#{featureName: "FeatureB", fileName: "featureB", description: "The data for feature B"})
588588
FeatureB: "Feature B",
589589
}
590590
@secret
591591
scalar secretString extends string;
592592
593-
@Azure.ResourceManager.Legacy.feature(Features.FeatureA)
593+
@Azure.ResourceManager.featureFile(Features.FeatureA)
594594
model FooResource is TrackedResource<FooResourceProperties> {
595595
...ResourceNameParameter<FooResource>;
596596
}
597597
598-
@Azure.ResourceManager.Legacy.feature(Features.FeatureA)
598+
@Azure.ResourceManager.featureFile(Features.FeatureA)
599599
model FooResourceProperties {
600600
...DefaultProvisioningStateProperty;
601601
password: secretString;
602602
}
603603
604-
@Azure.ResourceManager.Legacy.feature(Features.FeatureB)
604+
@Azure.ResourceManager.featureFile(Features.FeatureB)
605605
model BarResource is ProxyResource<BarResourceProperties> {
606606
...ResourceNameParameter<BarResource>;
607607
}
608-
@Azure.ResourceManager.Legacy.feature(Features.FeatureB)
608+
@Azure.ResourceManager.featureFile(Features.FeatureB)
609609
model BarResourceProperties {
610610
...DefaultProvisioningStateProperty;
611611
password: secretString;
612612
}
613613
614-
@Azure.ResourceManager.Legacy.feature(Features.FeatureA)
614+
@Azure.ResourceManager.featureFile(Features.FeatureA)
615615
@armResourceOperations
616616
interface Foos extends Azure.ResourceManager.TrackedResourceOperations<FooResource, FooResourceProperties> {}
617617
618-
@Azure.ResourceManager.Legacy.feature(Features.FeatureB)
618+
@Azure.ResourceManager.featureFile(Features.FeatureB)
619619
@armResourceOperations
620620
interface Bars extends Azure.ResourceManager.TrackedResourceOperations<BarResource, BarResourceProperties> {}
621-
@@Azure.ResourceManager.Legacy.feature(Bars.get, Features.FeatureA);
621+
@@Azure.ResourceManager.featureFile(Bars.get, Features.FeatureA);
622622
`,
623623
["featureA", "featureB", "shared"],
624624
{ preset: "azure" },

packages/typespec-azure-resource-manager/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ Available ruleSets:
8787
- [`@armResourceUpdate`](#@armresourceupdate)
8888
- [`@armVirtualResource`](#@armvirtualresource)
8989
- [`@extensionResource`](#@extensionresource)
90+
- [`@featureFile`](#@featurefile)
91+
- [`@featureFileOptions`](#@featurefileoptions)
92+
- [`@featureFiles`](#@featurefiles)
9093
- [`@identifiers`](#@identifiers)
9194
- [`@locationResource`](#@locationresource)
9295
- [`@resourceBaseType`](#@resourcebasetype)
@@ -398,6 +401,63 @@ See more details on [different Azure Resource Manager resource type here.](https
398401

399402
None
400403

404+
#### `@featureFile`
405+
406+
Decorator to associate a feature file with a model, interface, or namespace
407+
408+
```typespec
409+
@Azure.ResourceManager.featureFile(featureName: EnumMember)
410+
```
411+
412+
##### Target
413+
414+
The target to associate the feature file with
415+
`Model | Operation | Interface | Namespace`
416+
417+
##### Parameters
418+
419+
| Name | Type | Description |
420+
| ----------- | ------------ | ---------------------------------------- |
421+
| featureName | `EnumMember` | The feature to associate with the target |
422+
423+
#### `@featureFileOptions`
424+
425+
Decorator to define options for a specific feature file
426+
427+
```typespec
428+
@Azure.ResourceManager.featureFileOptions(options: valueof Azure.ResourceManager.Legacy.ArmFeatureOptions)
429+
```
430+
431+
##### Target
432+
433+
The enum member that represents the feature
434+
`EnumMember`
435+
436+
##### Parameters
437+
438+
| Name | Type | Description |
439+
| ------- | ------------------------------------------------- | -------------------------------- |
440+
| options | [valueof `ArmFeatureOptions`](#armfeatureoptions) | The options for the feature file |
441+
442+
#### `@featureFiles`
443+
444+
Decorator to define a set of feature files for splitting output
445+
446+
```typespec
447+
@Azure.ResourceManager.featureFiles(features: Enum)
448+
```
449+
450+
##### Target
451+
452+
The service namespace
453+
`Namespace`
454+
455+
##### Parameters
456+
457+
| Name | Type | Description |
458+
| -------- | ------ | ----------------------------------- |
459+
| features | `Enum` | The enum that contains the features |
460+
401461
#### `@identifiers`
402462

403463
This decorator is used to indicate the identifying properties of objects in the array, e.g. size

packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {
22
DecoratorContext,
33
DecoratorValidatorCallbacks,
4+
Enum,
45
EnumMember,
56
EnumValue,
67
Interface,
@@ -17,6 +18,14 @@ export interface ResourceOperationOptions {
1718
readonly omitTags?: boolean;
1819
}
1920

21+
export interface ArmFeatureOptions {
22+
readonly featureName: string;
23+
readonly fileName: string;
24+
readonly description: string;
25+
readonly title?: string;
26+
readonly termsOfService?: string;
27+
}
28+
2029
/**
2130
* Marks the operation as being a collection action
2231
*/
@@ -348,6 +357,42 @@ export type ResourceBaseTypeDecorator = (
348357
baseTypeIt: Type,
349358
) => DecoratorValidatorCallbacks | void;
350359

360+
/**
361+
* Decorator to define a set of feature files for splitting output
362+
*
363+
* @param target The service namespace
364+
* @param features The enum that contains the features
365+
*/
366+
export type FeatureFilesDecorator = (
367+
context: DecoratorContext,
368+
target: Namespace,
369+
features: Enum,
370+
) => DecoratorValidatorCallbacks | void;
371+
372+
/**
373+
* Decorator to define options for a specific feature file
374+
*
375+
* @param target The enum member that represents the feature
376+
* @param options The options for the feature file
377+
*/
378+
export type FeatureFileOptionsDecorator = (
379+
context: DecoratorContext,
380+
target: EnumMember,
381+
options: ArmFeatureOptions,
382+
) => DecoratorValidatorCallbacks | void;
383+
384+
/**
385+
* Decorator to associate a feature file with a model, interface, or namespace
386+
*
387+
* @param target The target to associate the feature file with
388+
* @param featureName The feature to associate with the target
389+
*/
390+
export type FeatureFileDecorator = (
391+
context: DecoratorContext,
392+
target: Model | Operation | Interface | Namespace,
393+
featureName: EnumMember,
394+
) => DecoratorValidatorCallbacks | void;
395+
351396
export type AzureResourceManagerDecorators = {
352397
armResourceCollectionAction: ArmResourceCollectionActionDecorator;
353398
armProviderNameValue: ArmProviderNameValueDecorator;
@@ -372,4 +417,7 @@ export type AzureResourceManagerDecorators = {
372417
armCommonTypesVersion: ArmCommonTypesVersionDecorator;
373418
armVirtualResource: ArmVirtualResourceDecorator;
374419
resourceBaseType: ResourceBaseTypeDecorator;
420+
featureFiles: FeatureFilesDecorator;
421+
featureFileOptions: FeatureFileOptionsDecorator;
422+
featureFile: FeatureFileDecorator;
375423
};

packages/typespec-azure-resource-manager/lib/decorators.tsp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,27 @@ extern dec resourceBaseType(
274274
* ```
275275
*/
276276
extern dec identifiers(entity: ModelProperty | Array<unknown>, properties: valueof string[]);
277+
278+
/**
279+
* Decorator to define a set of feature files for splitting output
280+
* @param target The service namespace
281+
* @param features The enum that contains the features
282+
*/
283+
extern dec featureFiles(target: Namespace, features: Enum);
284+
285+
/**
286+
* Decorator to define options for a specific feature file
287+
* @param target The enum member that represents the feature
288+
* @param options The options for the feature file
289+
*/
290+
extern dec featureFileOptions(
291+
target: EnumMember,
292+
options: valueof Azure.ResourceManager.Legacy.ArmFeatureOptions
293+
);
294+
295+
/**
296+
* Decorator to associate a feature file with a model, interface, or namespace
297+
* @param target The target to associate the feature file with
298+
* @param featureName The feature to associate with the target
299+
*/
300+
extern dec featureFile(target: Model | Operation | Interface | Namespace, featureName: EnumMember);

packages/typespec-azure-resource-manager/src/resource.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import {
3333
ArmResourceOperationsDecorator,
3434
ArmVirtualResourceDecorator,
3535
ExtensionResourceDecorator,
36+
FeatureFileDecorator,
37+
FeatureFileOptionsDecorator,
38+
FeatureFilesDecorator,
3639
IdentifiersDecorator,
3740
LocationResourceDecorator,
3841
ResourceBaseTypeDecorator,
@@ -1659,3 +1662,9 @@ export const $featureOptions: FeatureOptionsDecorator = (
16591662
) => {
16601663
setResourceFeatureOptions(context.program, entity, options);
16611664
};
1665+
1666+
// New Azure.ResourceManager namespace decorators (aliases of Legacy decorators)
1667+
export const $featureFile: FeatureFileDecorator = $feature as unknown as FeatureFileDecorator;
1668+
export const $featureFiles: FeatureFilesDecorator = $features as unknown as FeatureFilesDecorator;
1669+
export const $featureFileOptions: FeatureFileOptionsDecorator =
1670+
$featureOptions as unknown as FeatureFileOptionsDecorator;

packages/typespec-azure-resource-manager/src/rules/arm-custom-resource-usage-discourage.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { createRule, Model } from "@typespec/compiler";
2-
import { isCustomAzureResource } from "../resource.js";
1+
import { createRule, isTemplateInstance, Model, Program } from "@typespec/compiler";
2+
import { SyntaxKind, type Node } from "@typespec/compiler/ast";
3+
import { getCustomResourceOptions, isCustomAzureResource } from "../resource.js";
4+
5+
const armCustomResourceUsageDiscourageCode =
6+
"@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage";
37

48
export const armCustomResourceUsageDiscourage = createRule({
59
name: "arm-custom-resource-usage-discourage",
@@ -12,7 +16,10 @@ export const armCustomResourceUsageDiscourage = createRule({
1216
create(context) {
1317
return {
1418
model: (model: Model) => {
15-
if (isCustomAzureResource(context.program, model)) {
19+
if (
20+
isCustomAzureResource(context.program, model) &&
21+
!hasSuppressedCustomResourceTemplateBase(context.program, model)
22+
) {
1623
context.reportDiagnostic({
1724
code: "arm-custom-resource-usage-discourage",
1825
target: model,
@@ -22,3 +29,59 @@ export const armCustomResourceUsageDiscourage = createRule({
2229
};
2330
},
2431
});
32+
33+
function hasSuppressedCustomResourceTemplateBase(program: Program, model: Model): boolean {
34+
return hasSuppressedCustomResourceTemplate(program, model, new Set<Model>());
35+
}
36+
37+
function hasSuppressedCustomResourceTemplate(
38+
program: Program,
39+
model: Model,
40+
visited: Set<Model>,
41+
): boolean {
42+
if (visited.has(model)) {
43+
return false;
44+
}
45+
visited.add(model);
46+
47+
if (
48+
getCustomResourceOptions(program, model) &&
49+
isTemplateInstance(model) &&
50+
hasSuppression(model.node)
51+
) {
52+
return true;
53+
}
54+
55+
if (model.baseModel && hasSuppressedCustomResourceTemplate(program, model.baseModel, visited)) {
56+
return true;
57+
}
58+
59+
for (const sourceModel of model.sourceModels) {
60+
if (hasSuppressedCustomResourceTemplate(program, sourceModel.model, visited)) {
61+
return true;
62+
}
63+
}
64+
65+
return false;
66+
}
67+
68+
function hasSuppression(node: Node | undefined): boolean {
69+
let current = node;
70+
while (current) {
71+
for (const directive of current.directives ?? []) {
72+
const firstArgument = directive.arguments[0];
73+
if (
74+
directive.target.sv === "suppress" &&
75+
firstArgument &&
76+
((firstArgument.kind === SyntaxKind.StringLiteral &&
77+
firstArgument.value === armCustomResourceUsageDiscourageCode) ||
78+
(firstArgument.kind === SyntaxKind.Identifier &&
79+
firstArgument.sv === armCustomResourceUsageDiscourageCode))
80+
) {
81+
return true;
82+
}
83+
}
84+
current = current.parent;
85+
}
86+
return false;
87+
}

0 commit comments

Comments
 (0)