From e8b5ac6c307acd633dca8cfe724bb119b7d3383c Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 19 Jul 2024 16:23:48 -0400 Subject: [PATCH 01/12] add require-client-suffix --- .../rules/require-client-suffix.md | 44 +++++++++++++++++++ .../src/decorators.ts | 7 --- .../src/index.ts | 1 + .../src/linter.ts | 7 +++ .../src/rules/require-client-suffix.ts | 39 ++++++++++++++++ .../test/rules/require-client-suffix.test.ts | 43 ++++++++++++++++++ .../test/test-host.ts | 13 ++++++ 7 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 docs/libraries/typespec-client-generator-core/rules/require-client-suffix.md create mode 100644 packages/typespec-client-generator-core/src/linter.ts create mode 100644 packages/typespec-client-generator-core/src/rules/require-client-suffix.ts create mode 100644 packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts diff --git a/docs/libraries/typespec-client-generator-core/rules/require-client-suffix.md b/docs/libraries/typespec-client-generator-core/rules/require-client-suffix.md new file mode 100644 index 0000000000..1add6576b7 --- /dev/null +++ b/docs/libraries/typespec-client-generator-core/rules/require-client-suffix.md @@ -0,0 +1,44 @@ +--- +title: "require-client-suffix" +--- + +```text title="Full name" +@azure-tools/typespec-client-generator-core/require-client-suffix +``` + +Verify that the generated client's name will have the suffix `Client` in its name + +#### ❌ Incorrect + +```ts +// main.tsp +namespace MyService; +``` + +```ts +// client.tsp +import "@azure-tools/typespec-client-generator-core"; +import "./main.tsp"; + +using Azure.ClientGenerator.Core; + +namespace MyCustomizations; + +@@client(MyService); +``` + +#### ✅ Ok + +Either you can rename the client using input param `{"name": "MyClient"}`, or if you are completely recreating a client namespace in your `client.tsp`, you can rely on that namespace to end in `Client` + +```ts +// client.tsp +import "@azure-tools/typespec-client-generator-core"; +import "./main.tsp"; + +using Azure.ClientGenerator.Core; + +@client({service: MyService}) +namespace MyClient { +} +``` diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 2a7d8ad373..455f0fca12 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -157,13 +157,6 @@ export const $client: ClientDecorator = ( explicitService?.kind === "Namespace" ? explicitService : findClientService(context.program, target) ?? (target as any); - if (!name.endsWith("Client")) { - reportDiagnostic(context.program, { - code: "client-name", - format: { name }, - target: context.decoratorTarget, - }); - } if (!isService(context.program, service)) { reportDiagnostic(context.program, { diff --git a/packages/typespec-client-generator-core/src/index.ts b/packages/typespec-client-generator-core/src/index.ts index 132e5c11c2..d2bbd20c01 100644 --- a/packages/typespec-client-generator-core/src/index.ts +++ b/packages/typespec-client-generator-core/src/index.ts @@ -1,6 +1,7 @@ export * from "./decorators.js"; export * from "./interfaces.js"; export * from "./lib.js"; +export { $linter } from "./linter.js"; export * from "./public-utils.js"; export * from "./types.js"; export { $onValidate } from "./validate.js"; diff --git a/packages/typespec-client-generator-core/src/linter.ts b/packages/typespec-client-generator-core/src/linter.ts new file mode 100644 index 0000000000..11b746cb3e --- /dev/null +++ b/packages/typespec-client-generator-core/src/linter.ts @@ -0,0 +1,7 @@ +import { defineLinter } from "@typespec/compiler"; +import { requireClientSuffixRule } from "./rules/require-client-suffix.js"; + + +export const $linter = defineLinter({ + rules: [requireClientSuffixRule], +}); diff --git a/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts b/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts new file mode 100644 index 0000000000..b3507a6750 --- /dev/null +++ b/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts @@ -0,0 +1,39 @@ +import { createRule, Interface, Namespace, paramMessage } from "@typespec/compiler"; +import { getClient } from "../decorators.js"; +import { createTCGCContext } from "../internal-utils.js"; + +export const requireClientSuffixRule = createRule({ + name: "require-client-suffix", + description: "Client names should end with 'Client'.", + severity: "warning", + messages: { + default: paramMessage`Client name "${"name"}" must end with Client. Use @client({name: "...Client"}`, + }, + create(context) { + const tcgcContext = createTCGCContext(context.program); + return { + namespace: (namespace: Namespace) => { + const sdkClient = getClient(tcgcContext, namespace); + if (sdkClient && !sdkClient.name.endsWith("Client")) { + context.reportDiagnostic({ + target: namespace, + format: { + name: sdkClient.name, + }, + }); + } + }, + interface: (interfaceType: Interface) => { + const sdkClient = getClient(tcgcContext, interfaceType); + if (sdkClient && !sdkClient.name.endsWith("Client")) { + context.reportDiagnostic({ + target: interfaceType, + format: { + name: sdkClient.name, + }, + }); + } + }, + }; + }, +}); diff --git a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts new file mode 100644 index 0000000000..70655ff162 --- /dev/null +++ b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts @@ -0,0 +1,43 @@ +import { + BasicTestRunner, + createLinterRuleTester, + LinterRuleTester, +} from "@typespec/compiler/testing"; +import { beforeEach, describe, it } from "vitest"; +import { requireClientSuffixRule } from "../../src/rules/require-client-suffix.js"; +import { createTcgcTestRunner } from "../test-host.js"; + +describe("typespec-client-generator-core: client names end with 'Client'", () => { + let runner: BasicTestRunner; + let tester: LinterRuleTester; + + beforeEach(async () => { + runner = await createTcgcTestRunner(); + tester = createLinterRuleTester( + runner, + requireClientSuffixRule, + "@azure-tools/typespec-client-generator-core" + ); + }); + + it("namespace", async () => { + await tester + .expect( + ` + @service + namespace MyService; + + namespace MyCustomizations { + @@client(MyService); + } + ` + ) + .toEmitDiagnostics([ + { + code: "@azure-tools/typespec-client-generator-core/require-client-suffix", + severity: "warning", + message: `Client name "MyNamespace" must end with Client. Use @client({name: "...Client"}`, + }, + ]); + }); +}); diff --git a/packages/typespec-client-generator-core/test/test-host.ts b/packages/typespec-client-generator-core/test/test-host.ts index 2ec027c6c0..64c6851065 100644 --- a/packages/typespec-client-generator-core/test/test-host.ts +++ b/packages/typespec-client-generator-core/test/test-host.ts @@ -68,6 +68,19 @@ export interface CreateSdkTestRunnerOptions extends SdkEmitterOptions { packageName?: string; } +export async function createTcgcTestRunner(): Promise { + const host = await createSdkTestHost(); + const autoUsings = [ + "Azure.ClientGenerator.Core", + "TypeSpec.Rest", + "TypeSpec.Http", + "TypeSpec.Versioning", + ]; + return createTestWrapper(host, { + autoUsings: autoUsings, + }) as SdkTestRunner; +} + export async function createSdkTestRunner( options: CreateSdkTestRunnerOptions = {}, sdkContextOption?: CreateSdkContextOptions From 69bd8f1d0087b434a8dfd728db197846fa70fae5 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 19 Jul 2024 17:18:07 -0400 Subject: [PATCH 02/12] remove unused decorators --- .../src/decorators.ts | 4 +- .../typespec-client-generator-core/src/lib.ts | 38 +------------------ 2 files changed, 4 insertions(+), 38 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 455f0fca12..d0efe96cc2 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -774,7 +774,7 @@ export const $clientFormat: ClientFormatDecorator = ( setScopedDecoratorData(context, $clientFormat, clientFormatKey, target, format, scope); // eslint-disable-line deprecation/deprecation } else { reportDiagnostic(context.program, { - code: "incorrect-client-format", + code: "invalid-client-format", format: { format, expectedTargetTypes: expectedTargetTypes.join('", "') }, target: context.decoratorTarget, }); @@ -922,7 +922,7 @@ export const $access: AccessDecorator = ( ) => { if (typeof value.value !== "string" || (value.value !== "public" && value.value !== "internal")) { reportDiagnostic(context.program, { - code: "access", + code: "incorrect-access", format: {}, target: entity, }); diff --git a/packages/typespec-client-generator-core/src/lib.ts b/packages/typespec-client-generator-core/src/lib.ts index 8d39e92393..34b9d1d1f9 100644 --- a/packages/typespec-client-generator-core/src/lib.ts +++ b/packages/typespec-client-generator-core/src/lib.ts @@ -3,25 +3,13 @@ import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler"; export const $lib = createTypeSpecLibrary({ name: "@azure-tools/typespec-client-generator-core", diagnostics: { - "client-name": { - severity: "warning", - messages: { - default: paramMessage`Client name "${"name"}" must end with Client. Use @client({name: "...Client"}`, - }, - }, "client-service": { severity: "warning", messages: { default: paramMessage`Client "${"name"}" is not inside a service namespace. Use @client({service: MyServiceNS}`, }, }, - "unknown-client-format": { - severity: "error", - messages: { - default: paramMessage`Client format "${"format"}" is unknown. Known values are "${"knownValues"}"`, - }, - }, - "incorrect-client-format": { + "invalid-client-format": { severity: "error", messages: { default: paramMessage`Format "${"format"}" can only apply to "${"expectedTargetTypes"}"`, @@ -33,22 +21,7 @@ export const $lib = createTypeSpecLibrary({ default: "Cannot have a union containing only null types.", }, }, - "union-unsupported": { - severity: "error", - messages: { - default: - "Unions cannot be emitted by our language generators unless all options are literals of the same type.", - null: "Unions containing multiple model types cannot be emitted unless the union is between one model type and 'null'.", - }, - }, - "use-enum-instead": { - severity: "warning", - messages: { - default: - "Use enum instead of union of string or number literals. Falling back to the literal type.", - }, - }, - access: { + "invalid-access": { severity: "error", messages: { default: `Access decorator value must be "public" or "internal".`, @@ -60,13 +33,6 @@ export const $lib = createTypeSpecLibrary({ default: `Usage decorator value must be 2 ("input") or 4 ("output").`, }, }, - "invalid-encode": { - severity: "error", - messages: { - default: "Invalid encoding", - wrongType: paramMessage`Encoding '${"encoding"}' cannot be used on type '${"type"}'`, - }, - }, "conflicting-multipart-model-usage": { severity: "error", messages: { From 667dd62222507bb68c9efb481c4361f4e43bed76 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 19 Jul 2024 18:31:07 -0400 Subject: [PATCH 03/12] add csharp-specific property-name-conflict --- .../reference/linter.md | 31 ++++ .../typespec-client-generator-core/README.md | 26 +++ .../src/decorators.ts | 2 +- .../src/linter.ts | 21 ++- .../src/rules/property-name-conflict.ts | 24 +++ .../src/rules/require-client-suffix.ts | 5 +- .../test/decorators.test.ts | 23 --- .../test/rules/property-name-conflict.test.ts | 159 ++++++++++++++++++ .../test/rules/require-client-suffix.test.ts | 46 ++++- 9 files changed, 305 insertions(+), 32 deletions(-) create mode 100644 docs/libraries/typespec-client-generator-core/reference/linter.md create mode 100644 packages/typespec-client-generator-core/src/rules/property-name-conflict.ts create mode 100644 packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts diff --git a/docs/libraries/typespec-client-generator-core/reference/linter.md b/docs/libraries/typespec-client-generator-core/reference/linter.md new file mode 100644 index 0000000000..0128c8c0f2 --- /dev/null +++ b/docs/libraries/typespec-client-generator-core/reference/linter.md @@ -0,0 +1,31 @@ +--- +title: "Linter usage" +toc_min_heading_level: 2 +toc_max_heading_level: 3 +--- + +# Linter + +## Usage + +Add the following in `tspconfig.yaml`: + +```yaml +linter: + extends: + - "@azure-tools/typespec-client-generator-core/all" +``` + +## RuleSets + +Available ruleSets: + +- `@azure-tools/typespec-client-generator-core/all` +- `@azure-tools/typespec-client-generator-core/best-practices:csharp` + +## Rules + +| Name | Description | +| -------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `@azure-tools/typespec-client-generator-core/require-client-suffix` | Client names should end with 'Client'. | +| `@azure-tools/typespec-client-generator-core/property-name-conflict` | Avoid naming conflicts between a property and a model of the same name. | diff --git a/packages/typespec-client-generator-core/README.md b/packages/typespec-client-generator-core/README.md index 13e4b1f39a..8e2e2d3f3a 100644 --- a/packages/typespec-client-generator-core/README.md +++ b/packages/typespec-client-generator-core/README.md @@ -8,6 +8,32 @@ TypeSpec Data Plane Generation library npm install @azure-tools/typespec-client-generator-core ``` +## Linter + +### Usage + +Add the following in `tspconfig.yaml`: + +```yaml +linter: + extends: + - "@azure-tools/typespec-client-generator-core/all" +``` + +### RuleSets + +Available ruleSets: + +- `@azure-tools/typespec-client-generator-core/all` +- `@azure-tools/typespec-client-generator-core/best-practices:csharp` + +### Rules + +| Name | Description | +| -------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `@azure-tools/typespec-client-generator-core/require-client-suffix` | Client names should end with 'Client'. | +| `@azure-tools/typespec-client-generator-core/property-name-conflict` | Avoid naming conflicts between a property and a model of the same name. | + ## Decorators ### Azure.ClientGenerator.Core diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 16feea2410..5042d89045 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -932,7 +932,7 @@ export const $access: AccessDecorator = ( ) => { if (typeof value.value !== "string" || (value.value !== "public" && value.value !== "internal")) { reportDiagnostic(context.program, { - code: "incorrect-access", + code: "invalid-access", format: {}, target: entity, }); diff --git a/packages/typespec-client-generator-core/src/linter.ts b/packages/typespec-client-generator-core/src/linter.ts index 11b746cb3e..f2c9e4f6f8 100644 --- a/packages/typespec-client-generator-core/src/linter.ts +++ b/packages/typespec-client-generator-core/src/linter.ts @@ -1,7 +1,26 @@ import { defineLinter } from "@typespec/compiler"; import { requireClientSuffixRule } from "./rules/require-client-suffix.js"; +import { propertyNameConflictRule } from "./rules/property-name-conflict.js"; + +const genericRules = [ + requireClientSuffixRule, +] + +const csharpRules = [ + propertyNameConflictRule +] export const $linter = defineLinter({ - rules: [requireClientSuffixRule], + rules: [ + ...genericRules, + ...csharpRules, + ], + ruleSets: { + "best-practices:csharp": { + enable: { + ...Object.fromEntries(csharpRules.map(rule => [`@azure-tools/typespec-client-generator-core/${rule.name}`, true])), + } + } + } }); diff --git a/packages/typespec-client-generator-core/src/rules/property-name-conflict.ts b/packages/typespec-client-generator-core/src/rules/property-name-conflict.ts new file mode 100644 index 0000000000..2ae9173cfb --- /dev/null +++ b/packages/typespec-client-generator-core/src/rules/property-name-conflict.ts @@ -0,0 +1,24 @@ +import { ModelProperty, createRule, paramMessage } from "@typespec/compiler"; + +export const propertyNameConflictRule = createRule({ + name: "property-name-conflict", + description: "Avoid naming conflicts between a property and a model of the same name.", + severity: "warning", + messages: { + default: paramMessage`Property '${"propertyName"}' having the same name as its enclosing model will cause problems with C# code generation. Consider renaming the property directly or using the @clientName("newName", "csharp") decorator to rename the property for C#.`, + }, + create(context) { + return { + modelProperty: (property: ModelProperty) => { + const modelName = property.model?.name.toLocaleLowerCase(); + const propertyName = property.name.toLocaleLowerCase(); + if (propertyName === modelName) { + context.reportDiagnostic({ + format: { propertyName: property.name }, + target: property, + }); + } + }, + }; + }, +}); diff --git a/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts b/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts index b3507a6750..66922cd6eb 100644 --- a/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts +++ b/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts @@ -1,6 +1,5 @@ import { createRule, Interface, Namespace, paramMessage } from "@typespec/compiler"; -import { getClient } from "../decorators.js"; -import { createTCGCContext } from "../internal-utils.js"; +import { getClient, createTCGCContext } from "../decorators.js"; export const requireClientSuffixRule = createRule({ name: "require-client-suffix", @@ -10,7 +9,7 @@ export const requireClientSuffixRule = createRule({ default: paramMessage`Client name "${"name"}" must end with Client. Use @client({name: "...Client"}`, }, create(context) { - const tcgcContext = createTCGCContext(context.program); + const tcgcContext = createTCGCContext(context.program, "@azure-tools/typespec-client-generator-core"); return { namespace: (namespace: Namespace) => { const sdkClient = getClient(tcgcContext, namespace); diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 2ef945a06e..0672cf79c0 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -81,29 +81,6 @@ describe("typespec-client-generator-core: decorators", () => { }, ]); }); - it("emit diagnostic if the client namespace doesn't ends with client", async () => { - const diagnostics = await runner.diagnose(` - @client - @service({}) - @test namespace MyService; - `); - - expectDiagnostics(diagnostics, { - code: "@azure-tools/typespec-client-generator-core/client-name", - }); - }); - - it("emit diagnostic if the client explicit name doesn't ends with Client", async () => { - const diagnostics = await runner.diagnose(` - @client({name: "MySDK"}) - @service({}) - @test namespace MyService; - `); - - expectDiagnostics(diagnostics, { - code: "@azure-tools/typespec-client-generator-core/client-name", - }); - }); it("@client with scope", async () => { const testCode = ` diff --git a/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts b/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts new file mode 100644 index 0000000000..602fbcc01a --- /dev/null +++ b/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts @@ -0,0 +1,159 @@ +import { + BasicTestRunner, + LinterRuleTester, + createLinterRuleTester, +} from "@typespec/compiler/testing"; +import { beforeEach, it } from "vitest"; +import { createSdkTestRunner } from "../test-host.js"; +import { propertyNameConflictRule } from "../../src/rules/property-name-conflict.js"; + +let runner: BasicTestRunner; +let tester: LinterRuleTester; + +beforeEach(async () => { + runner = await createSdkTestRunner(); + tester = createLinterRuleTester(runner, propertyNameConflictRule, "@azure-tools/typespec-azure-core"); +}); + +it("emit warning if property name conflicts with model name", async () => { + await tester.expect(`model Foo {foo: string}`).toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-core/property-name-conflict", + message: `Property 'foo' having the same name as its enclosing model will cause problems with C# code generation. Consider renaming the property directly or using the @clientName("newName", "csharp") decorator to rename the property for C#.`, + }); +}); + +// TODO: reenable when rule is moved to tcgc and can resolve if clientName was set +it.skip(`is valid if conflict resolved through @clientName("newName", "csharp")`, async () => { + await tester + .expect( + `model Foo { + @clientName("bar", "csharp") foo: string + }` + ) + .toBeValid(); +}); + +// TODO: reenable when rule is moved to tcgc and can resolve if clientName was set +it.skip(`is valid if conflict resolved through @clientName("newName")`, async () => { + await tester + .expect( + `model Foo { + @clientName("bar") foo: string + }` + ) + .toBeValid(); +}); + +// TODO: reenable when rule is moved to tcgc and can resolve if clientName was set +it.skip("emit warning if conflict not resolved through @clientName", async () => { + await tester + .expect( + `model Foo { + @clientName("bar", "python") foo: string + }` + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-core/property-name-conflict", + message: `Property 'foo' having the same name as its enclosing model will cause problems with C# code generation. Consider renaming the property directly or using the @clientName("newName", "csharp") decorator to rename the property for C#.`, + }); +}); + +// TODO: reenable when rule is moved to tcgc and can resolve if clientName was set +it.skip(`emit warning if @clientName("newName") introduces conflict`, async () => { + await tester + .expect( + `model Foo { + @clientName("foo") bar: string; + }` + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-core/property-name-conflict", + message: `Use of @clientName on property 'bar' results in 'bar' having the same name as its enclosing type in C#. Please use a different @clientName("newName", "csharp") value.`, + }); +}); + +// TODO: reenable when rule is moved to tcgc and can resolve if clientName was set +it.skip(`emit warning if @clientName("newName", "csharp") introduces conflict`, async () => { + await tester + .expect( + `model Foo { + @clientName("foo", "csharp") bar: string; + }` + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-core/property-name-conflict", + message: `Use of @clientName on property 'bar' results in 'bar' having the same name as its enclosing type in C#. Please use a different @clientName("newName", "csharp") value.`, + }); +}); + +// TODO: reenable when rule is moved to tcgc and can resolve if clientName was set +it.skip(`is valid if @clientName("newName") causes conflict but @clientName("newName", "csharp") resolves it`, async () => { + await tester + .expect( + `model Foo { + @clientName("foo") @"client", ("baz", "csharp") bar: string; + }` + ) + .toBeValid(); +}); + +it("emit warning if @friendlyName would resolve a conflict (friendlyName is ignored)", async () => { + await tester.expect(`model Foo { @friendlyName("bar") foo: string }`).toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-core/property-name-conflict", + message: `Property 'foo' having the same name as its enclosing model will cause problems with C# code generation. Consider renaming the property directly or using the @clientName("newName", "csharp") decorator to rename the property for C#.`, + }); +}); + +it("is valid if @friendlyName introduces conflict (friendlyName is ignored)", async () => { + await tester.expect(`model Foo { @friendlyName("foo") bar: string }`).toBeValid(); +}); + +it("emit warning if property name conflicts with model name when using `is`", async () => { + await tester + .expect( + ` + model Base { + foo: string + } + + model Foo is Base {} + ` + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-core/property-name-conflict", + message: `Property 'foo' having the same name as its enclosing model will cause problems with C# code generation. Consider renaming the property directly or using the @clientName("newName", "csharp") decorator to rename the property for C#.`, + }); +}); + +it("is valid if inherited property name conflicts with model name", async () => { + await tester + .expect( + ` + model Base { + foo: string + } + + model Foo extends Base {} + ` + ) + .toBeValid(); +}); + +it("emit warning if spread property name conflicts with model name", async () => { + await tester + .expect( + ` + model Base { + foo: string + } + + model Foo { + ...Base; + } + ` + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-core/property-name-conflict", + message: `Property 'foo' having the same name as its enclosing model will cause problems with C# code generation. Consider renaming the property directly or using the @clientName("newName", "csharp") decorator to rename the property for C#.`, + }); +}); diff --git a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts index 70655ff162..21aad1256f 100644 --- a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts +++ b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts @@ -7,7 +7,7 @@ import { beforeEach, describe, it } from "vitest"; import { requireClientSuffixRule } from "../../src/rules/require-client-suffix.js"; import { createTcgcTestRunner } from "../test-host.js"; -describe("typespec-client-generator-core: client names end with 'Client'", () => { +describe("typespec-client-generator-core: require-client-suffix", () => { let runner: BasicTestRunner; let tester: LinterRuleTester; @@ -20,7 +20,43 @@ describe("typespec-client-generator-core: client names end with 'Client'", () => ); }); - it("namespace", async () => { + it("namespace doesn't end in client", async () => { + await tester + .expect( + ` + @client + @service({}) + namespace MyService; + ` + ) + .toEmitDiagnostics([ + { + code: "@azure-tools/typespec-client-generator-core/require-client-suffix", + severity: "warning", + message: `Client name "MyService" must end with Client. Use @client({name: "...Client"}`, + }, + ]); + }); + + it("explicit client name doesn't ends with Client", async () => { + await tester + .expect( + ` + @client({name: "MySDK"}) + @service({}) + namespace MyService; + ` + ) + .toEmitDiagnostics([ + { + code: "@azure-tools/typespec-client-generator-core/require-client-suffix", + severity: "warning", + message: `Client name "MySDK" must end with Client. Use @client({name: "...Client"}`, + }, + ]); + }); + + it("interface", async () => { await tester .expect( ` @@ -28,7 +64,9 @@ describe("typespec-client-generator-core: client names end with 'Client'", () => namespace MyService; namespace MyCustomizations { - @@client(MyService); + @client({service: MyService}) + interface MyInterface { + }; } ` ) @@ -36,7 +74,7 @@ describe("typespec-client-generator-core: client names end with 'Client'", () => { code: "@azure-tools/typespec-client-generator-core/require-client-suffix", severity: "warning", - message: `Client name "MyNamespace" must end with Client. Use @client({name: "...Client"}`, + message: `Client name "MyInterface" must end with Client. Use @client({name: "...Client"}`, }, ]); }); From e35c3e46016c350158789dd721f2b3ebd46f9f20 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 19 Jul 2024 18:34:38 -0400 Subject: [PATCH 04/12] add test to best-practices:csharp --- packages/typespec-client-generator-core/src/linter.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/typespec-client-generator-core/src/linter.ts b/packages/typespec-client-generator-core/src/linter.ts index f2c9e4f6f8..3ffcfc28f8 100644 --- a/packages/typespec-client-generator-core/src/linter.ts +++ b/packages/typespec-client-generator-core/src/linter.ts @@ -2,8 +2,9 @@ import { defineLinter } from "@typespec/compiler"; import { requireClientSuffixRule } from "./rules/require-client-suffix.js"; import { propertyNameConflictRule } from "./rules/property-name-conflict.js"; -const genericRules = [ +const rules = [ requireClientSuffixRule, + propertyNameConflictRule, ] const csharpRules = [ @@ -12,10 +13,7 @@ const csharpRules = [ export const $linter = defineLinter({ - rules: [ - ...genericRules, - ...csharpRules, - ], + rules, ruleSets: { "best-practices:csharp": { enable: { From b5cce2819a2d75b674060efdb492fbbed310c836 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 19 Jul 2024 18:39:18 -0400 Subject: [PATCH 05/12] add docs and url to docs --- .../rules/property-name-conflict.md | 27 +++++++++++++++++++ .../src/rules/property-name-conflict.ts | 1 + .../src/rules/require-client-suffix.ts | 1 + 3 files changed, 29 insertions(+) create mode 100644 docs/libraries/typespec-client-generator-core/rules/property-name-conflict.md diff --git a/docs/libraries/typespec-client-generator-core/rules/property-name-conflict.md b/docs/libraries/typespec-client-generator-core/rules/property-name-conflict.md new file mode 100644 index 0000000000..e1efede9e1 --- /dev/null +++ b/docs/libraries/typespec-client-generator-core/rules/property-name-conflict.md @@ -0,0 +1,27 @@ +--- +title: "property-name-conflict" +--- + +```text title="Full name" +@azure-tools/typespec-client-generator-core/property-name-conflict +``` + +Verify that there isn't a name conflict between a property in the model, and the name of the model itself + +#### ❌ Incorrect + +```ts +model Widget { + widget: string; +} +``` + +#### ✅ Ok + +Using items from a private namespace within the same library is allowed. + +```ts +model Widget { + widgetName: string; +} +``` diff --git a/packages/typespec-client-generator-core/src/rules/property-name-conflict.ts b/packages/typespec-client-generator-core/src/rules/property-name-conflict.ts index 2ae9173cfb..52f880766b 100644 --- a/packages/typespec-client-generator-core/src/rules/property-name-conflict.ts +++ b/packages/typespec-client-generator-core/src/rules/property-name-conflict.ts @@ -4,6 +4,7 @@ export const propertyNameConflictRule = createRule({ name: "property-name-conflict", description: "Avoid naming conflicts between a property and a model of the same name.", severity: "warning", + url: "https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/rules/property-name-conflict", messages: { default: paramMessage`Property '${"propertyName"}' having the same name as its enclosing model will cause problems with C# code generation. Consider renaming the property directly or using the @clientName("newName", "csharp") decorator to rename the property for C#.`, }, diff --git a/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts b/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts index 66922cd6eb..1a853bfb07 100644 --- a/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts +++ b/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts @@ -5,6 +5,7 @@ export const requireClientSuffixRule = createRule({ name: "require-client-suffix", description: "Client names should end with 'Client'.", severity: "warning", + url: "https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/rules/require-client-suffix", messages: { default: paramMessage`Client name "${"name"}" must end with Client. Use @client({name: "...Client"}`, }, From 57d2f48067f2fe7c794d6298847b36bf15902f76 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 19 Jul 2024 18:57:53 -0400 Subject: [PATCH 06/12] add contributing doc --- .../rules/CONTRIBUTING.md | 38 +++++++++++++++++++ .../src/linter.ts | 4 +- ...lict.ts => property-name-conflict.rule.ts} | 0 ...uffix.ts => require-client-suffix.rule.ts} | 0 .../test/rules/require-client-suffix.test.ts | 2 +- 5 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md rename packages/typespec-client-generator-core/src/rules/{property-name-conflict.ts => property-name-conflict.rule.ts} (100%) rename packages/typespec-client-generator-core/src/rules/{require-client-suffix.ts => require-client-suffix.rule.ts} (100%) diff --git a/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md b/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md new file mode 100644 index 0000000000..9089469c38 --- /dev/null +++ b/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing Linter Rules to the @azure-tools/typespec-client-generator-core Package + +For information about linter rules in typespec in general, view [here][generic-linter] + +In the `@azure-tools/typespec-client-generator-core` library, we have two main types of rules: + +1. Generic rule that applies to all emitted languages +2. Specific rule that applies to a subset of all language: it can even apply to just one language + +The process for adding a rule starts off the same for both: for language-specific rules, there is an extra step + +1. Write the rule in `typespec-azure/packages/typespec-client-generator-core/src/rules/[rule-name].rule.ts +2. Add reference to the rule in the `rules` array in [`typespec-azure/packages/typespec-client-generator-core/src/linter.ts`][tcgc-linter] + - This will automatically add it to a ruleset called `:all` for `@azure-tools/typespec-client-generator-core` +3. Add the rule to the enable list for [`data-plane.ts`][data-plane-ruleset] and/or [`resource-manager.ts`][resource-manager-ruleset] in the [rulesets][rulesets] package. You can set `enable` to `false` here, if you want to delay enabling + +**If you are adding a language-specific rule**, you will also need this extra step +4. Add reference to the rule in the `[language]Rules` array in [`typespec-azure/packages/typespec-client-generator-core/src/linter.ts`][tcgc-linter] + +For Azure generations then, all rules, including all language-specific rules, will be run on the specs. +For unbranded generations, since we've added the rules into specific `best-practices:[language]` rulesets, you can explicitly specify a subset of rules in your `tsp-config.yaml`, i.e. if I only want Python best-practices, I could add this in my `tsp-config.yaml`: + +```yaml +linter: + extends: + - best-practices: python +``` + + +Finally, we recommend that every warning or error you throw in your language emitter has a corresponding warning in TCGC. This is part of our shift-left policy, so tsp authors can catch these potential pitfalls earlier in the process. + +### Links + +[generic-linter]: https://typespec.io/docs/next/extending-typespec/linters "Generic Linter Docs" +[tcgc-linter]: https://github.com/typespec-azure/packages/typespec-client-generator-core/src/linter.ts "Linter TS File" +[rulesets]: https://github.com/typespec-azure/packages/typespec-azure-rulesets "Rulesets package" +[data-plane-ruleset]: https://github.com/typespec-azure/packages/typespec-azure-rulesets/src/rulesets/data-plane.ts "Data Plane Ruleset" +[resource-manager-ruleset]: https://github.com/typespec-azure/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts "Resource Manager Ruleset" diff --git a/packages/typespec-client-generator-core/src/linter.ts b/packages/typespec-client-generator-core/src/linter.ts index 3ffcfc28f8..7ce1632706 100644 --- a/packages/typespec-client-generator-core/src/linter.ts +++ b/packages/typespec-client-generator-core/src/linter.ts @@ -1,6 +1,6 @@ import { defineLinter } from "@typespec/compiler"; -import { requireClientSuffixRule } from "./rules/require-client-suffix.js"; -import { propertyNameConflictRule } from "./rules/property-name-conflict.js"; +import { requireClientSuffixRule } from "./rules/require-client-suffix.rule.js"; +import { propertyNameConflictRule } from "./rules/property-name-conflict.rule.js"; const rules = [ requireClientSuffixRule, diff --git a/packages/typespec-client-generator-core/src/rules/property-name-conflict.ts b/packages/typespec-client-generator-core/src/rules/property-name-conflict.rule.ts similarity index 100% rename from packages/typespec-client-generator-core/src/rules/property-name-conflict.ts rename to packages/typespec-client-generator-core/src/rules/property-name-conflict.rule.ts diff --git a/packages/typespec-client-generator-core/src/rules/require-client-suffix.ts b/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts similarity index 100% rename from packages/typespec-client-generator-core/src/rules/require-client-suffix.ts rename to packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts diff --git a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts index 21aad1256f..baadc71c3b 100644 --- a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts +++ b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts @@ -4,7 +4,7 @@ import { LinterRuleTester, } from "@typespec/compiler/testing"; import { beforeEach, describe, it } from "vitest"; -import { requireClientSuffixRule } from "../../src/rules/require-client-suffix.js"; +import { requireClientSuffixRule } from "../../src/rules/require-client-suffix.rule.js"; import { createTcgcTestRunner } from "../test-host.js"; describe("typespec-client-generator-core: require-client-suffix", () => { From 1684417205cbd9764a2a7650479f8227ca62b1ea Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 19 Jul 2024 19:00:36 -0400 Subject: [PATCH 07/12] add changeset --- .../changes/typespec_azure_rulsets-2024-6-19-19-0-31.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/typespec_azure_rulsets-2024-6-19-19-0-31.md diff --git a/.chronus/changes/typespec_azure_rulsets-2024-6-19-19-0-31.md b/.chronus/changes/typespec_azure_rulsets-2024-6-19-19-0-31.md new file mode 100644 index 0000000000..d657c7c0ff --- /dev/null +++ b/.chronus/changes/typespec_azure_rulsets-2024-6-19-19-0-31.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +add linter rulesets to TCGC, for both generic and language-specific linter rules \ No newline at end of file From 2de1f09286f77fe9926535b0a3a12eb7c621389f Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Mon, 22 Jul 2024 10:22:36 -0400 Subject: [PATCH 08/12] address comments --- .../rules/CONTRIBUTING.md | 6 ++--- .../rules/require-client-suffix.md | 24 ++++++++++-------- .../src/linter.ts | 25 +++++++++---------- .../src/rules/require-client-suffix.rule.ts | 7 ++++-- .../test/rules/property-name-conflict.test.ts | 8 ++++-- .../test/rules/require-client-suffix.test.ts | 4 +-- .../test/test-host.ts | 13 ---------- 7 files changed, 40 insertions(+), 47 deletions(-) diff --git a/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md b/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md index 9089469c38..e8ffe55b05 100644 --- a/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md +++ b/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md @@ -11,11 +11,10 @@ The process for adding a rule starts off the same for both: for language-specifi 1. Write the rule in `typespec-azure/packages/typespec-client-generator-core/src/rules/[rule-name].rule.ts 2. Add reference to the rule in the `rules` array in [`typespec-azure/packages/typespec-client-generator-core/src/linter.ts`][tcgc-linter] - - This will automatically add it to a ruleset called `:all` for `@azure-tools/typespec-client-generator-core` + - This will automatically add it to a ruleset called `:all` for `@azure-tools/typespec-client-generator-core` 3. Add the rule to the enable list for [`data-plane.ts`][data-plane-ruleset] and/or [`resource-manager.ts`][resource-manager-ruleset] in the [rulesets][rulesets] package. You can set `enable` to `false` here, if you want to delay enabling -**If you are adding a language-specific rule**, you will also need this extra step -4. Add reference to the rule in the `[language]Rules` array in [`typespec-azure/packages/typespec-client-generator-core/src/linter.ts`][tcgc-linter] +**If you are adding a language-specific rule**, you will also need this extra step 4. Add reference to the rule in the `[language]Rules` array in [`typespec-azure/packages/typespec-client-generator-core/src/linter.ts`][tcgc-linter] For Azure generations then, all rules, including all language-specific rules, will be run on the specs. For unbranded generations, since we've added the rules into specific `best-practices:[language]` rulesets, you can explicitly specify a subset of rules in your `tsp-config.yaml`, i.e. if I only want Python best-practices, I could add this in my `tsp-config.yaml`: @@ -26,7 +25,6 @@ linter: - best-practices: python ``` - Finally, we recommend that every warning or error you throw in your language emitter has a corresponding warning in TCGC. This is part of our shift-left policy, so tsp authors can catch these potential pitfalls earlier in the process. ### Links diff --git a/docs/libraries/typespec-client-generator-core/rules/require-client-suffix.md b/docs/libraries/typespec-client-generator-core/rules/require-client-suffix.md index 1add6576b7..d5824af51f 100644 --- a/docs/libraries/typespec-client-generator-core/rules/require-client-suffix.md +++ b/docs/libraries/typespec-client-generator-core/rules/require-client-suffix.md @@ -17,28 +17,30 @@ namespace MyService; ```ts // client.tsp -import "@azure-tools/typespec-client-generator-core"; -import "./main.tsp"; - -using Azure.ClientGenerator.Core; - namespace MyCustomizations; @@client(MyService); ``` -#### ✅ Ok +#### ✅ Recommended -Either you can rename the client using input param `{"name": "MyClient"}`, or if you are completely recreating a client namespace in your `client.tsp`, you can rely on that namespace to end in `Client` +If you would not like to make any changes to the generated client besides its name, you can rely on the [`@clientName`][client-name] decorator to rename the main namespace of the service. ```ts -// client.tsp -import "@azure-tools/typespec-client-generator-core"; -import "./main.tsp"; +@@clientName(MyService, "MyClient"); +``` -using Azure.ClientGenerator.Core; +#### ✅ Ok +If you are completely recreating a client namespace in your `client.tsp`, you can rely on that namespace to end in `Client` + +```ts +// client.tsp @client({service: MyService}) namespace MyClient { } ``` + +### Links + +[client-name]: https://azure.github.io/typespec-azure/docs/libraries/typespec-client-generator-core/reference/decorators#@Azure.ClientGenerator.Core.clientName "@clientName Decorator" diff --git a/packages/typespec-client-generator-core/src/linter.ts b/packages/typespec-client-generator-core/src/linter.ts index 7ce1632706..d007c629f6 100644 --- a/packages/typespec-client-generator-core/src/linter.ts +++ b/packages/typespec-client-generator-core/src/linter.ts @@ -1,24 +1,23 @@ import { defineLinter } from "@typespec/compiler"; -import { requireClientSuffixRule } from "./rules/require-client-suffix.rule.js"; import { propertyNameConflictRule } from "./rules/property-name-conflict.rule.js"; +import { requireClientSuffixRule } from "./rules/require-client-suffix.rule.js"; -const rules = [ - requireClientSuffixRule, - propertyNameConflictRule, -] - -const csharpRules = [ - propertyNameConflictRule -] +const rules = [requireClientSuffixRule, propertyNameConflictRule]; +const csharpRules = [propertyNameConflictRule]; export const $linter = defineLinter({ rules, ruleSets: { "best-practices:csharp": { enable: { - ...Object.fromEntries(csharpRules.map(rule => [`@azure-tools/typespec-client-generator-core/${rule.name}`, true])), - } - } - } + ...Object.fromEntries( + csharpRules.map((rule) => [ + `@azure-tools/typespec-client-generator-core/${rule.name}`, + true, + ]) + ), + }, + }, + }, }); diff --git a/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts b/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts index 1a853bfb07..1e3221d851 100644 --- a/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts +++ b/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts @@ -1,5 +1,5 @@ import { createRule, Interface, Namespace, paramMessage } from "@typespec/compiler"; -import { getClient, createTCGCContext } from "../decorators.js"; +import { createTCGCContext, getClient } from "../decorators.js"; export const requireClientSuffixRule = createRule({ name: "require-client-suffix", @@ -10,7 +10,10 @@ export const requireClientSuffixRule = createRule({ default: paramMessage`Client name "${"name"}" must end with Client. Use @client({name: "...Client"}`, }, create(context) { - const tcgcContext = createTCGCContext(context.program, "@azure-tools/typespec-client-generator-core"); + const tcgcContext = createTCGCContext( + context.program, + "@azure-tools/typespec-client-generator-core" + ); return { namespace: (namespace: Namespace) => { const sdkClient = getClient(tcgcContext, namespace); diff --git a/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts b/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts index 602fbcc01a..7a2c07fe62 100644 --- a/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts +++ b/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts @@ -4,15 +4,19 @@ import { createLinterRuleTester, } from "@typespec/compiler/testing"; import { beforeEach, it } from "vitest"; +import { propertyNameConflictRule } from "../../src/rules/property-name-conflict.rule.js"; import { createSdkTestRunner } from "../test-host.js"; -import { propertyNameConflictRule } from "../../src/rules/property-name-conflict.js"; let runner: BasicTestRunner; let tester: LinterRuleTester; beforeEach(async () => { runner = await createSdkTestRunner(); - tester = createLinterRuleTester(runner, propertyNameConflictRule, "@azure-tools/typespec-azure-core"); + tester = createLinterRuleTester( + runner, + propertyNameConflictRule, + "@azure-tools/typespec-azure-core" + ); }); it("emit warning if property name conflicts with model name", async () => { diff --git a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts index baadc71c3b..dab328b990 100644 --- a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts +++ b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts @@ -5,14 +5,14 @@ import { } from "@typespec/compiler/testing"; import { beforeEach, describe, it } from "vitest"; import { requireClientSuffixRule } from "../../src/rules/require-client-suffix.rule.js"; -import { createTcgcTestRunner } from "../test-host.js"; +import { createSdkTestRunner } from "../test-host.js"; describe("typespec-client-generator-core: require-client-suffix", () => { let runner: BasicTestRunner; let tester: LinterRuleTester; beforeEach(async () => { - runner = await createTcgcTestRunner(); + runner = await createSdkTestRunner(); tester = createLinterRuleTester( runner, requireClientSuffixRule, diff --git a/packages/typespec-client-generator-core/test/test-host.ts b/packages/typespec-client-generator-core/test/test-host.ts index 64c6851065..2ec027c6c0 100644 --- a/packages/typespec-client-generator-core/test/test-host.ts +++ b/packages/typespec-client-generator-core/test/test-host.ts @@ -68,19 +68,6 @@ export interface CreateSdkTestRunnerOptions extends SdkEmitterOptions { packageName?: string; } -export async function createTcgcTestRunner(): Promise { - const host = await createSdkTestHost(); - const autoUsings = [ - "Azure.ClientGenerator.Core", - "TypeSpec.Rest", - "TypeSpec.Http", - "TypeSpec.Versioning", - ]; - return createTestWrapper(host, { - autoUsings: autoUsings, - }) as SdkTestRunner; -} - export async function createSdkTestRunner( options: CreateSdkTestRunnerOptions = {}, sdkContextOption?: CreateSdkContextOptions From a46581eb96dbe9f4993e63a95c7fec8ea1a83787 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 17 Oct 2024 16:04:31 -0400 Subject: [PATCH 09/12] address initial comments --- .../rules/CONTRIBUTING.md | 10 +++++----- .../rules/property-name-conflict.md | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md b/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md index e8ffe55b05..b5be70939a 100644 --- a/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md +++ b/docs/libraries/typespec-client-generator-core/rules/CONTRIBUTING.md @@ -5,7 +5,7 @@ For information about linter rules in typespec in general, view [here][generic-l In the `@azure-tools/typespec-client-generator-core` library, we have two main types of rules: 1. Generic rule that applies to all emitted languages -2. Specific rule that applies to a subset of all language: it can even apply to just one language +2. Specific rule that applies to a subset of all languages: it can even apply to just one language The process for adding a rule starts off the same for both: for language-specific rules, there is an extra step @@ -30,7 +30,7 @@ Finally, we recommend that every warning or error you throw in your language emi ### Links [generic-linter]: https://typespec.io/docs/next/extending-typespec/linters "Generic Linter Docs" -[tcgc-linter]: https://github.com/typespec-azure/packages/typespec-client-generator-core/src/linter.ts "Linter TS File" -[rulesets]: https://github.com/typespec-azure/packages/typespec-azure-rulesets "Rulesets package" -[data-plane-ruleset]: https://github.com/typespec-azure/packages/typespec-azure-rulesets/src/rulesets/data-plane.ts "Data Plane Ruleset" -[resource-manager-ruleset]: https://github.com/typespec-azure/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts "Resource Manager Ruleset" +[tcgc-linter]: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-client-generator-core/src/linter.ts "Linter TS File" +[rulesets]: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-rulesets "Rulesets package" +[data-plane-ruleset]: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-rulesets/src/rulesets/data-plane.ts "Data Plane Ruleset" +[resource-manager-ruleset]: https://github.com/Azure/typespec-azure/blob/main/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts "Resource Manager Ruleset" diff --git a/docs/libraries/typespec-client-generator-core/rules/property-name-conflict.md b/docs/libraries/typespec-client-generator-core/rules/property-name-conflict.md index e1efede9e1..a9254d2963 100644 --- a/docs/libraries/typespec-client-generator-core/rules/property-name-conflict.md +++ b/docs/libraries/typespec-client-generator-core/rules/property-name-conflict.md @@ -18,8 +18,6 @@ model Widget { #### ✅ Ok -Using items from a private namespace within the same library is allowed. - ```ts model Widget { widgetName: string; From a83e2c2f340044f93b2c290175a1838d0f783474 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 17 Oct 2024 16:54:34 -0400 Subject: [PATCH 10/12] format and lint --- .../src/linter.ts | 2 +- .../src/rules/require-client-suffix.rule.ts | 2 +- .../test/rules/property-name-conflict.test.ts | 20 +++++++++---------- .../test/rules/require-client-suffix.test.ts | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/typespec-client-generator-core/src/linter.ts b/packages/typespec-client-generator-core/src/linter.ts index d007c629f6..b1c13a60ac 100644 --- a/packages/typespec-client-generator-core/src/linter.ts +++ b/packages/typespec-client-generator-core/src/linter.ts @@ -15,7 +15,7 @@ export const $linter = defineLinter({ csharpRules.map((rule) => [ `@azure-tools/typespec-client-generator-core/${rule.name}`, true, - ]) + ]), ), }, }, diff --git a/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts b/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts index 1e3221d851..754d0faea5 100644 --- a/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts +++ b/packages/typespec-client-generator-core/src/rules/require-client-suffix.rule.ts @@ -12,7 +12,7 @@ export const requireClientSuffixRule = createRule({ create(context) { const tcgcContext = createTCGCContext( context.program, - "@azure-tools/typespec-client-generator-core" + "@azure-tools/typespec-client-generator-core", ); return { namespace: (namespace: Namespace) => { diff --git a/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts b/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts index 7a2c07fe62..2750042834 100644 --- a/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts +++ b/packages/typespec-client-generator-core/test/rules/property-name-conflict.test.ts @@ -15,7 +15,7 @@ beforeEach(async () => { tester = createLinterRuleTester( runner, propertyNameConflictRule, - "@azure-tools/typespec-azure-core" + "@azure-tools/typespec-azure-core", ); }); @@ -32,7 +32,7 @@ it.skip(`is valid if conflict resolved through @clientName("newName", "csharp")` .expect( `model Foo { @clientName("bar", "csharp") foo: string - }` + }`, ) .toBeValid(); }); @@ -43,7 +43,7 @@ it.skip(`is valid if conflict resolved through @clientName("newName")`, async () .expect( `model Foo { @clientName("bar") foo: string - }` + }`, ) .toBeValid(); }); @@ -54,7 +54,7 @@ it.skip("emit warning if conflict not resolved through @clientName", async () => .expect( `model Foo { @clientName("bar", "python") foo: string - }` + }`, ) .toEmitDiagnostics({ code: "@azure-tools/typespec-azure-core/property-name-conflict", @@ -68,7 +68,7 @@ it.skip(`emit warning if @clientName("newName") introduces conflict`, async () = .expect( `model Foo { @clientName("foo") bar: string; - }` + }`, ) .toEmitDiagnostics({ code: "@azure-tools/typespec-azure-core/property-name-conflict", @@ -82,7 +82,7 @@ it.skip(`emit warning if @clientName("newName", "csharp") introduces conflict`, .expect( `model Foo { @clientName("foo", "csharp") bar: string; - }` + }`, ) .toEmitDiagnostics({ code: "@azure-tools/typespec-azure-core/property-name-conflict", @@ -96,7 +96,7 @@ it.skip(`is valid if @clientName("newName") causes conflict but @clientName("new .expect( `model Foo { @clientName("foo") @"client", ("baz", "csharp") bar: string; - }` + }`, ) .toBeValid(); }); @@ -121,7 +121,7 @@ it("emit warning if property name conflicts with model name when using `is`", as } model Foo is Base {} - ` + `, ) .toEmitDiagnostics({ code: "@azure-tools/typespec-azure-core/property-name-conflict", @@ -138,7 +138,7 @@ it("is valid if inherited property name conflicts with model name", async () => } model Foo extends Base {} - ` + `, ) .toBeValid(); }); @@ -154,7 +154,7 @@ it("emit warning if spread property name conflicts with model name", async () => model Foo { ...Base; } - ` + `, ) .toEmitDiagnostics({ code: "@azure-tools/typespec-azure-core/property-name-conflict", diff --git a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts index dab328b990..293a43e6bf 100644 --- a/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts +++ b/packages/typespec-client-generator-core/test/rules/require-client-suffix.test.ts @@ -16,7 +16,7 @@ describe("typespec-client-generator-core: require-client-suffix", () => { tester = createLinterRuleTester( runner, requireClientSuffixRule, - "@azure-tools/typespec-client-generator-core" + "@azure-tools/typespec-client-generator-core", ); }); @@ -27,7 +27,7 @@ describe("typespec-client-generator-core: require-client-suffix", () => { @client @service({}) namespace MyService; - ` + `, ) .toEmitDiagnostics([ { @@ -45,7 +45,7 @@ describe("typespec-client-generator-core: require-client-suffix", () => { @client({name: "MySDK"}) @service({}) namespace MyService; - ` + `, ) .toEmitDiagnostics([ { @@ -68,7 +68,7 @@ describe("typespec-client-generator-core: require-client-suffix", () => { interface MyInterface { }; } - ` + `, ) .toEmitDiagnostics([ { From 23e0e774a1955fbddaff953452fd3b7900d3fc72 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Thu, 17 Oct 2024 16:54:56 -0400 Subject: [PATCH 11/12] add changeset --- .../changes/typespec_azure_rulsets-2024-9-17-16-54-52.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/typespec_azure_rulsets-2024-9-17-16-54-52.md diff --git a/.chronus/changes/typespec_azure_rulsets-2024-9-17-16-54-52.md b/.chronus/changes/typespec_azure_rulsets-2024-9-17-16-54-52.md new file mode 100644 index 0000000000..0a1b0eb33f --- /dev/null +++ b/.chronus/changes/typespec_azure_rulsets-2024-9-17-16-54-52.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-azure-rulesets" +--- + +add some tcgc rules to the list \ No newline at end of file From ae9e97bbb113a7a8edb857f0310b887e83779943 Mon Sep 17 00:00:00 2001 From: iscai-msft Date: Fri, 18 Oct 2024 11:18:39 -0400 Subject: [PATCH 12/12] remove merge conflicts --- .../src/decorators.ts | 180 --- .../test/decorators.test.ts | 1241 ----------------- 2 files changed, 1421 deletions(-) diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index f85252bf02..741f5acff1 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -692,186 +692,6 @@ export function shouldGenerateConvenient(context: TCGCContext, entity: Operation return value ?? Boolean(context.generateConvenienceMethods); } -<<<<<<< HEAD -const excludeKey = createStateSymbol("exclude"); - -/** - * @deprecated Use `usage` and `access` decorator instead. - */ -export const $exclude: ExcludeDecorator = ( - context: DecoratorContext, - entity: Model, - scope?: LanguageScopes -) => { - setScopedDecoratorData(context, $exclude, excludeKey, entity, true, scope); // eslint-disable-line deprecation/deprecation -}; - -const includeKey = createStateSymbol("include"); - -/** - * @deprecated Use `usage` and `access` decorator instead. - */ -export const $include: IncludeDecorator = ( - context: DecoratorContext, - entity: Model, - scope?: LanguageScopes -) => { - modelTransitiveSet(context, $include, includeKey, entity, true, scope); // eslint-disable-line deprecation/deprecation -}; - -/** - * @deprecated This function is unused and will be removed in a future release. - */ -export function isExclude(context: TCGCContext, entity: Model): boolean { - return getScopedDecoratorData(context, excludeKey, entity) ?? false; -} - -/** - * @deprecated This function is unused and will be removed in a future release. - */ -export function isInclude(context: TCGCContext, entity: Model): boolean { - return getScopedDecoratorData(context, includeKey, entity) ?? false; -} - -function modelTransitiveSet( - context: DecoratorContext, - decorator: DecoratorFunction, - key: symbol, - entity: Model, - value: unknown, - scope?: LanguageScopes, - transitivity: boolean = false -) { - if (!setScopedDecoratorData(context, decorator, key, entity, value, scope, transitivity)) { - return; - } - - if (entity.baseModel) { - modelTransitiveSet(context, decorator, key, entity.baseModel, value, scope, true); - } - - entity.properties.forEach((p) => { - if (p.kind === "ModelProperty" && p.type.kind === "Model") { - modelTransitiveSet(context, decorator, key, p.type, value, scope, true); - } - }); -} - -const clientFormatKey = createStateSymbol("clientFormat"); - -const allowedClientFormatToTargetTypeMap: Record = { - unixtime: ["int32", "int64"], - iso8601: ["utcDateTime", "offsetDateTime", "duration"], - rfc1123: ["utcDateTime", "offsetDateTime"], - seconds: ["duration"], -}; - -export type ClientFormat = "unixtime" | "iso8601" | "rfc1123" | "seconds"; - -/** - * @deprecated Use `encode` decorator in `@typespec/core` instead. - */ -export const $clientFormat: ClientFormatDecorator = ( - context: DecoratorContext, - target: ModelProperty, - format: ClientFormat, - scope?: LanguageScopes -) => { - const expectedTargetTypes = allowedClientFormatToTargetTypeMap[format]; - if ( - context.program.checker.isStdType(target.type) && - expectedTargetTypes.includes(target.type.name) - ) { - setScopedDecoratorData(context, $clientFormat, clientFormatKey, target, format, scope); // eslint-disable-line deprecation/deprecation - } else { - reportDiagnostic(context.program, { - code: "invalid-client-format", - format: { format, expectedTargetTypes: expectedTargetTypes.join('", "') }, - target: context.decoratorTarget, - }); - } -}; - -/** - * Gets additional information on how to serialize / deserialize TYPESPEC standard types depending - * on whether additional serialization information is provided or needed - * - * @param context the Sdk Context - * @param entity the entity whose client format we are going to get - * @returns the format in which to serialize the typespec type or undefined - * @deprecated This function is unused and will be removed in a future release. - */ -export function getClientFormat( - context: TCGCContext, - entity: ModelProperty -): ClientFormat | undefined { - let retval: ClientFormat | undefined = getScopedDecoratorData(context, clientFormatKey, entity); - if (retval === undefined && context.program.checker.isStdType(entity.type)) { - if (entity.type.name === "utcDateTime" || entity.type.name === "offsetDateTime") { - // if it's a date-time we have the following defaults - retval = isHeader(context.program, entity) ? "rfc1123" : "iso8601"; - context.program.stateMap(clientFormatKey).set(entity, retval); - } else if (entity.type.name === "duration") { - retval = "iso8601"; - } - } - - return retval; -} -const internalKey = createStateSymbol("internal"); - -/** - * Whether a operation is internal and should not be exposed - * to end customers - * - * @param context DecoratorContext - * @param target Operation to mark as internal - * @param scope Names of the projection (e.g. "python", "csharp", "java", "javascript") - * @deprecated Use `access` decorator instead. - * - * @internal - */ -export const $internal: InternalDecorator = ( - context: DecoratorContext, - target: Operation, - scope?: LanguageScopes -) => { - setScopedDecoratorData(context, $internal, internalKey, target, true, scope); // eslint-disable-line deprecation/deprecation -}; - -/** - * Whether a model / operation is internal or not. If it's internal, emitters - * should not expose them to users - * - * @param context TCGCContext - * @param entity model / operation that we want to check is internal or not - * @returns whether the entity is internal - * @deprecated This function is unused and will be removed in a future release. - */ -export function isInternal( - context: TCGCContext, - entity: Model | Operation | Enum | Union -): boolean { - const found = getScopedDecoratorData(context, internalKey, entity) ?? false; - if (entity.kind === "Operation" || found) { - return found; - } - const operationModels = context.operationModelsMap!; - let referredByInternal = false; - for (const [operation, modelMap] of operationModels) { - // eslint-disable-next-line deprecation/deprecation - if (isInternal(context, operation) && modelMap.get(entity)) { - referredByInternal = true; - // eslint-disable-next-line deprecation/deprecation - } else if (!isInternal(context, operation) && modelMap.get(entity)) { - return false; - } - } - return referredByInternal; -} - -======= ->>>>>>> 0f289160fadd84b6e0935d014740407f7e10354e const usageKey = createStateSymbol("usage"); export const $usage: UsageDecorator = ( diff --git a/packages/typespec-client-generator-core/test/decorators.test.ts b/packages/typespec-client-generator-core/test/decorators.test.ts index 3142e21b63..511d1cab8a 100644 --- a/packages/typespec-client-generator-core/test/decorators.test.ts +++ b/packages/typespec-client-generator-core/test/decorators.test.ts @@ -30,1247 +30,6 @@ describe("typespec-client-generator-core: decorators", () => { runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python" }); }); -<<<<<<< HEAD - describe("@client", () => { - it("mark an namespace as a client", async () => { - const { MyClient } = await runner.compile(` - @client - @service({}) - @test namespace MyClient; - `); - - const clients = listClients(runner.context); - deepStrictEqual(clients, [ - { - kind: "SdkClient", - name: "MyClient", - service: MyClient, - type: MyClient, - arm: false, - crossLanguageDefinitionId: "MyClient.MyClient", - }, - ]); - }); - - it("mark an interface as a client", async () => { - const { MyService, MyClient } = await runner.compile(` - @service({}) - @test namespace MyService; - @client({service: MyService}) - @test interface MyClient {} - `); - - const clients = listClients(runner.context); - deepStrictEqual(clients, [ - { - kind: "SdkClient", - name: "MyClient", - service: MyService, - type: MyClient, - arm: false, - crossLanguageDefinitionId: "MyService.MyClient", - }, - ]); - }); - - it("@client with scope", async () => { - const testCode = ` - @service({ - title: "DeviceUpdateClient", - }) - namespace Azure.IoT.DeviceUpdate; - - @client({name: "DeviceUpdateClient"}, "java") - @client({name: "DeviceUpdateClient"}, "csharp") - @client({name: "DeviceUpdateClient"}, "javascript") - interface DeviceUpdateOperations { - } - - @client({name: "DeviceManagementClient"}, "java") - @client({name: "DeviceManagementClient"}, "csharp") - @client({name: "DeviceManagementClient"}, "javascript") - interface DeviceManagementOperations { - } - - @client({name: "InstanceManagementClient"}, "java") - @client({name: "InstanceManagementClient"}, "csharp") - @client({name: "InstanceManagementClient"}, "javascript") - interface InstanceManagementOperations { - } - `; - - // java should get three clients - { - const runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-java" }); - await runner.compile(testCode); - strictEqual(listClients(runner.context).length, 3); - } - - // csharp should get three clients - { - const runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-csharp" }); - await runner.compile(testCode); - strictEqual(listClients(runner.context).length, 3); - } - - // python should get one client - { - const runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python" }); - await runner.compile(testCode); - strictEqual(listClients(runner.context).length, 1); - } - - // typescript should get three clients - { - const runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-ts" }); - await runner.compile(testCode); - strictEqual(listClients(runner.context).length, 3); - } - }); - }); - - describe("listClients without @client", () => { - it("use service namespace if there is not clients and append Client to service name", async () => { - const { MyService } = await runner.compile(` - @service({}) - @test - namespace MyService; - `); - - const clients = listClients(runner.context); - deepStrictEqual(clients, [ - { - kind: "SdkClient", - name: "MyServiceClient", - service: MyService, - type: MyService, - arm: false, - crossLanguageDefinitionId: "MyService.MyServiceClient", - }, - ]); - }); - }); - - describe("@operationGroup", () => { - it("mark an namespace as an operation group", async () => { - const { MyClient, MyGroup } = (await runner.compile(` - @client - @service({}) - @test namespace MyClient; - - @operationGroup - @test namespace MyGroup {} - `)) as { MyClient: Namespace; MyGroup: Namespace }; - - const groups = listOperationGroups(runner.context, getClient(runner.context, MyClient)!); - deepStrictEqual(groups, [ - { - kind: "SdkOperationGroup", - type: MyGroup, - groupPath: "MyClient.MyGroup", - service: MyClient, - }, - ]); - }); - - it("mark an interface as an operationGroup", async () => { - const { MyClient, MyGroup } = (await runner.compile(` - @client - @service({}) - @test namespace MyClient; - @operationGroup - @test - interface MyGroup {} - `)) as { MyClient: Namespace; MyGroup: Interface }; - - const groups = listOperationGroups(runner.context, getClient(runner.context, MyClient)!); - deepStrictEqual(groups, [ - { - kind: "SdkOperationGroup", - type: MyGroup, - groupPath: "MyClient.MyGroup", - service: MyClient, - }, - ]); - }); - - it("list operations at root of client outside of operation group", async () => { - const { MyClient } = (await runner.compile(` - @client - @service({}) - @test namespace MyClient; - - @route("/root1") op atRoot1(): void; - @route("/root2") op atRoot2(): void; - - @operationGroup - @test interface MyGroup { - @route("/one") inOpGroup1(): void; - @route("/two") inOpGroup2(): void; - } - `)) as { MyClient: Namespace }; - - const operations = listOperationsInOperationGroup( - runner.context, - getClient(runner.context, MyClient)! - ); - deepStrictEqual( - operations.map((x) => x.name), - ["atRoot1", "atRoot2"] - ); - }); - - it("list operations in an operation group", async () => { - const { MyGroup } = (await runner.compile(` - @client - @service({}) - @test namespace MyClient; - - @route("/root1") op atRoot1(): void; - @route("/root2") op atRoot2(): void; - - @operationGroup - @test interface MyGroup { - @route("/one") inOpGroup1(): void; - @route("/two") inOpGroup2(): void; - } - `)) as { MyGroup: Interface }; - - const group = getOperationGroup(runner.context, MyGroup); - ok(group); - const operations = listOperationsInOperationGroup(runner.context, group); - deepStrictEqual( - operations.map((x) => x.name), - ["inOpGroup1", "inOpGroup2"] - ); - }); - - it("crossLanguageDefinitionId basic", async () => { - const { one } = (await runner.compile(` - @client - @service({}) - @test namespace MyClient; - - @test op one(): void; - `)) as { one: Operation }; - - strictEqual(getCrossLanguageDefinitionId(runner.context, one), "MyClient.one"); - }); - - it("crossLanguageDefinitionId with interface", async () => { - const { one } = (await runner.compile(` - @client - @service({}) - @test namespace MyClient; - - interface Widgets { - @test op one(): void; - } - `)) as { one: Operation }; - - strictEqual(getCrossLanguageDefinitionId(runner.context, one), "MyClient.Widgets.one"); - }); - - it("crossLanguageDefinitionId with subnamespace", async () => { - const { one } = (await runner.compile(` - @client - @service({}) - @test namespace MyClient; - - namespace Widgets { - @test op one(): void; - } - `)) as { one: Operation }; - - strictEqual(getCrossLanguageDefinitionId(runner.context, one), "MyClient.Widgets.one"); - }); - - it("crossLanguageDefinitionId with subnamespace and interface", async () => { - const { one } = (await runner.compile(` - @client - @service({}) - @test namespace MyClient; - - namespace SubNamespace { - interface Widgets { - @test op one(): void; - } - } - `)) as { one: Operation }; - - strictEqual( - getCrossLanguageDefinitionId(runner.context, one), - "MyClient.SubNamespace.Widgets.one" - ); - }); - - it("crossLanguagePackageId", async () => { - await runner.compile(` - @client({name: "MyPackageClient"}) - @service({}) - namespace My.Package.Namespace; - - namespace SubNamespace { - interface Widgets { - @test op one(): void; - } - } - `); - strictEqual( - ignoreDiagnostics(getCrossLanguagePackageId(runner.context)), - "My.Package.Namespace" - ); - }); - - it("@operationGroup with scope", async () => { - const testCode = ` - @service({ - title: "DeviceUpdateClient", - }) - namespace Azure.IoT.DeviceUpdate; - - @operationGroup("java") - @operationGroup("csharp") - @operationGroup("javascript") - interface DeviceUpdateOperations { - } - - @operationGroup("java") - @operationGroup("csharp") - @operationGroup("javascript") - interface DeviceManagementOperations { - } - - @operationGroup("java") - @operationGroup("csharp") - @operationGroup("javascript") - interface InstanceManagementOperations { - } - - @client({name: "DeviceUpdateClient", service: Azure.IoT.DeviceUpdate}, "python") - namespace Customizations{} - `; - - // java should get three operation groups - { - const runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-java" }); - await runner.compile(testCode); - const client = listClients(runner.context)[0]; - strictEqual(listOperationGroups(runner.context, client).length, 3); - } - - // csharp should get three operation groups - { - const runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-csharp" }); - await runner.compile(testCode); - const client = listClients(runner.context)[0]; - strictEqual(listOperationGroups(runner.context, client).length, 3); - } - - // python should get three operation groups - { - const runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-python" }); - await runner.compile(testCode); - const client = listClients(runner.context)[0]; - strictEqual(listOperationGroups(runner.context, client).length, 0); - } - - // typescript should get three operation groups - { - const runner = await createSdkTestRunner({ emitterName: "@azure-tools/typespec-ts" }); - await runner.compile(testCode); - const client = listClients(runner.context)[0]; - strictEqual(listOperationGroups(runner.context, client).length, 3); - } - }); - - it("use service namespace if there is not clients and append Client to service name", async () => { - const { MyService } = await runner.compile(` - @service({}) - @test - namespace MyService; - `); - - const clients = listClients(runner.context); - deepStrictEqual(clients, [ - { - kind: "SdkClient", - name: "MyServiceClient", - service: MyService, - type: MyService, - arm: false, - crossLanguageDefinitionId: "MyService.MyServiceClient", - }, - ]); - }); - }); - - describe("listOperationGroups without @client and @operationGroup", () => { - it("list operations in namespace or interface", async () => { - await runner.compile(` - @service({}) - @test namespace MyClient; - - @route("/root1") op atRoot1(): void; - @route("/root2") op atRoot2(): void; - - @test interface MyGroup { - @route("/one") inOpGroup1(): void; - @route("/two") inOpGroup2(): void; - } - `); - const clients = listClients(runner.context); - strictEqual(clients.length, 1); - - let operations = listOperationsInOperationGroup(runner.context, clients[0]); - deepStrictEqual( - operations.map((x) => x.name), - ["atRoot1", "atRoot2"] - ); - - const ogs = listOperationGroups(runner.context, clients[0]); - strictEqual(ogs.length, 1); - strictEqual(ogs[0].subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, ogs[0]).length, 0); - - operations = listOperationsInOperationGroup(runner.context, ogs[0]); - deepStrictEqual( - operations.map((x) => x.name), - ["inOpGroup1", "inOpGroup2"] - ); - }); - - it("namespace and interface hierarchy", async () => { - const { A, AA, AAA, AAB, AG, AAG, AABGroup1, AABGroup2 } = (await runner.compile(` - @service({}) - @route("/a") - @test namespace A { - @route("/o1") op a_o1(): void; - @route("/o2") op a_o2(): void; - - @route("/g") - @test interface AG { - @route("/o1") a_g_o1(): void; - @route("/o2") a_g_o2(): void; - } - - @route("/a") - @test namespace AA { - @route("/o1") op aa_o1(): void; - @route("/o2") op aa_o2(): void; - - @route("/g") - @test interface AAG { - @route("/o1") aa_g_o1(): void; - @route("/o2") aa_g_o2(): void; - } - - @route("/a") - @test namespace AAA{}; - - @route("/b") - @test namespace AAB{ - @route("/o1") op aab_o1(): void; - @route("/o2") op aab_o2(): void; - - @route("/g1") - @test interface AABGroup1 { - @route("/o1") aab_g1_o1(): void; - @route("/o2") aab_g1_o2(): void; - } - - @route("/g2") - @test interface AABGroup2 { - } - }; - } - }; - `)) as { - A: Namespace; - AA: Namespace; - AAA: Namespace; - AAB: Namespace; - AG: Interface; - AAG: Interface; - AABGroup1: Interface; - AABGroup2: Interface; - }; - - const ag: SdkOperationGroup = { - kind: "SdkOperationGroup", - type: AG, - groupPath: "AClient.AG", - service: A, - }; - - const aag: SdkOperationGroup = { - kind: "SdkOperationGroup", - type: AAG, - groupPath: "AClient.AA.AAG", - service: A, - }; - - const aabGroup1: SdkOperationGroup = { - kind: "SdkOperationGroup", - type: AABGroup1, - groupPath: "AClient.AA.AAB.AABGroup1", - service: A, - }; - - const aabGroup2: SdkOperationGroup = { - kind: "SdkOperationGroup", - type: AABGroup2, - groupPath: "AClient.AA.AAB.AABGroup2", - service: A, - }; - - const aaa: SdkOperationGroup = { - kind: "SdkOperationGroup", - type: AAA, - groupPath: "AClient.AA.AAA", - service: A, - }; - - const aab: SdkOperationGroup = { - kind: "SdkOperationGroup", - type: AAB, - subOperationGroups: [aabGroup1, aabGroup2], - groupPath: "AClient.AA.AAB", - service: A, - }; - - const aa: SdkOperationGroup = { - kind: "SdkOperationGroup", - type: AA, - subOperationGroups: [aaa, aab, aag], - groupPath: "AClient.AA", - service: A, - }; - - const client = getClient(runner.context, A); - ok(client); - let operations = listOperationsInOperationGroup(runner.context, client); - deepStrictEqual( - operations.map((x) => x.name), - ["a_o1", "a_o2"] - ); - - let group = getOperationGroup(runner.context, AA); - deepStrictEqual(group, aa); - operations = listOperationsInOperationGroup(runner.context, group); - deepStrictEqual( - operations.map((x) => x.name), - ["aa_o1", "aa_o2"] - ); - - group = getOperationGroup(runner.context, AAA); - deepStrictEqual(group, aaa); - deepStrictEqual(listOperationsInOperationGroup(runner.context, group), []); - - group = getOperationGroup(runner.context, AAB); - deepStrictEqual(group, aab); - operations = listOperationsInOperationGroup(runner.context, group); - deepStrictEqual( - operations.map((x) => x.name), - ["aab_o1", "aab_o2"] - ); - - group = getOperationGroup(runner.context, AG); - deepStrictEqual(group, ag); - operations = listOperationsInOperationGroup(runner.context, group); - deepStrictEqual( - operations.map((x) => x.name), - ["a_g_o1", "a_g_o2"] - ); - - group = getOperationGroup(runner.context, AAG); - deepStrictEqual(group, aag); - operations = listOperationsInOperationGroup(runner.context, group); - deepStrictEqual( - operations.map((x) => x.name), - ["aa_g_o1", "aa_g_o2"] - ); - - group = getOperationGroup(runner.context, AABGroup1); - deepStrictEqual(group, aabGroup1); - operations = listOperationsInOperationGroup(runner.context, group); - deepStrictEqual( - operations.map((x) => x.name), - ["aab_g1_o1", "aab_g1_o2"] - ); - - group = getOperationGroup(runner.context, AABGroup2); - deepStrictEqual(group, aabGroup2); - deepStrictEqual(listOperationsInOperationGroup(runner.context, group), []); - - let allOperationGroups = listOperationGroups(runner.context, client); - deepStrictEqual(allOperationGroups, [aa, ag]); - allOperationGroups = listOperationGroups(runner.context, aa); - deepStrictEqual(allOperationGroups, [aaa, aab, aag]); - allOperationGroups = listOperationGroups(runner.context, aaa); - deepStrictEqual(allOperationGroups, []); - allOperationGroups = listOperationGroups(runner.context, aab); - deepStrictEqual(allOperationGroups, [aabGroup1, aabGroup2]); - allOperationGroups = listOperationGroups(runner.context, aag); - deepStrictEqual(allOperationGroups, []); - allOperationGroups = listOperationGroups(runner.context, aabGroup1); - deepStrictEqual(allOperationGroups, []); - allOperationGroups = listOperationGroups(runner.context, aabGroup2); - deepStrictEqual(allOperationGroups, []); - deepStrictEqual(listOperationGroups(runner.context, ag), []); - - allOperationGroups = listOperationGroups(runner.context, client, true); - deepStrictEqual(allOperationGroups, [aa, aaa, aab, aabGroup1, aabGroup2, aag, ag]); - allOperationGroups = listOperationGroups(runner.context, aa, true); - deepStrictEqual(allOperationGroups, [aaa, aab, aabGroup1, aabGroup2, aag]); - allOperationGroups = listOperationGroups(runner.context, aaa, true); - deepStrictEqual(allOperationGroups, []); - allOperationGroups = listOperationGroups(runner.context, aab, true); - deepStrictEqual(allOperationGroups, [aabGroup1, aabGroup2]); - allOperationGroups = listOperationGroups(runner.context, aag, true); - deepStrictEqual(allOperationGroups, []); - allOperationGroups = listOperationGroups(runner.context, aabGroup1, true); - deepStrictEqual(allOperationGroups, []); - allOperationGroups = listOperationGroups(runner.context, aabGroup2, true); - deepStrictEqual(allOperationGroups, []); - deepStrictEqual(listOperationGroups(runner.context, ag, true), []); - - let allOperations = listOperationsInOperationGroup(runner.context, client, true); - deepStrictEqual( - allOperations.map((x) => x.name), - [ - "a_o1", - "a_o2", - "aa_o1", - "aa_o2", - "aab_o1", - "aab_o2", - "aab_g1_o1", - "aab_g1_o2", - "aa_g_o1", - "aa_g_o2", - "a_g_o1", - "a_g_o2", - ] - ); - allOperations = listOperationsInOperationGroup(runner.context, aa, true); - deepStrictEqual( - allOperations.map((x) => x.name), - ["aa_o1", "aa_o2", "aab_o1", "aab_o2", "aab_g1_o1", "aab_g1_o2", "aa_g_o1", "aa_g_o2"] - ); - allOperations = listOperationsInOperationGroup(runner.context, aaa, true); - deepStrictEqual(allOperations, []); - allOperations = listOperationsInOperationGroup(runner.context, aab, true); - deepStrictEqual( - allOperations.map((x) => x.name), - ["aab_o1", "aab_o2", "aab_g1_o1", "aab_g1_o2"] - ); - allOperations = listOperationsInOperationGroup(runner.context, aag, true); - deepStrictEqual( - allOperations.map((x) => x.name), - ["aa_g_o1", "aa_g_o2"] - ); - allOperations = listOperationsInOperationGroup(runner.context, aabGroup1, true); - deepStrictEqual( - allOperations.map((x) => x.name), - ["aab_g1_o1", "aab_g1_o2"] - ); - allOperations = listOperationsInOperationGroup(runner.context, aabGroup2, true); - deepStrictEqual(allOperations, []); - allOperations = listOperationsInOperationGroup(runner.context, ag, true); - deepStrictEqual( - allOperations.map((x) => x.name), - ["a_g_o1", "a_g_o2"] - ); - }); - - it("interface without operation", async () => { - const { MyGroup, MyClient } = (await runner.compile(` - @service({}) - @test namespace MyClient; - - @route("/root1") op atRoot1(): void; - - @test interface MyGroup { - } - `)) as { MyGroup: Interface; MyClient: Namespace }; - - deepStrictEqual(getOperationGroup(runner.context, MyGroup), { - kind: "SdkOperationGroup", - type: MyGroup, - groupPath: "MyClient.MyGroup", - service: MyClient, - }); - - const clients = listClients(runner.context); - const ogs = listOperationGroups(runner.context, clients[0]); - deepStrictEqual(ogs.length, 1); - }); - - it("empty namespaces and interfaces", async () => { - await runner.compile(` - @service({}) - @test - namespace MyService { - namespace A { - namespace B { - interface B1 {} - interface B2 {} - } - - interface A1 {} - interface A2 {} - } - namespace C { - interface C1 {} - } - namespace D {} - namespace E { - namespace F {} - } - namespace G { - namespace H { - interface H1 {} - } - } - }; - `); - - const clients = listClients(runner.context); - const ogs = listOperationGroups(runner.context, clients[0]); - let countFromProperty = ogs.length; - let countFromList = ogs.length; - const q = [...ogs]; - while (q.length > 0) { - const og = q.pop()!; - countFromProperty += og.subOperationGroups?.length ?? 0; - countFromList += listOperationGroups(runner.context, og).length; - q.push(...(og.subOperationGroups ?? [])); - } - deepStrictEqual(countFromProperty, 14); - deepStrictEqual(countFromList, 14); - }); - }); - - describe("client hierarchy", () => { - it("multi clients ", async () => { - await runner.compile(` - @service({}) - namespace Test1Client { - op x(): void; - } - @service({}) - namespace Test2Client { - op y(): void; - } - `); - - const clients = listClients(runner.context); - deepStrictEqual(clients.length, 2); - - const client1 = clients.find((x) => x.name === "Test1Client"); - ok(client1); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, client1).map((x) => x.name), - ["x"] - ); - deepStrictEqual(listOperationGroups(runner.context, client1).length, 0); - - const client2 = clients.find((x) => x.name === "Test2Client"); - ok(client2); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, client2).map((x) => x.name), - ["y"] - ); - deepStrictEqual(listOperationGroups(runner.context, client2).length, 0); - }); - - it("no client", async () => { - await runner.compile(` - namespace Test1Client { - op x(): void; - } - `); - - deepStrictEqual(listClients(runner.context).length, 0); - }); - - it("omit one namespace", async () => { - await runner.compile(` - @service({}) - namespace Test1Client { - op x(): void; - } - - namespace Test2 { - op y(): void; - } - `); - - const clients = listClients(runner.context); - deepStrictEqual(clients.length, 1); - - const client1 = clients.find((x) => x.name === "Test1Client"); - ok(client1); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, client1).map((x) => x.name), - ["x"] - ); - deepStrictEqual(listOperationGroups(runner.context, client1).length, 0); - }); - - it("nested namespace", async () => { - await runner.compile(` - @service({}) - namespace Test1Client { - namespace B { - op x(): void; - } - } - `); - - const clients = listClients(runner.context); - strictEqual(clients.length, 1); - - const client1 = clients.find((x) => x.name === "Test1Client"); - ok(client1); - strictEqual(listOperationsInOperationGroup(runner.context, client1).length, 0); - - const client1Ogs = listOperationGroups(runner.context, client1); - strictEqual(client1Ogs.length, 1); - const b = client1Ogs.find((x) => x.type.name === "B"); - ok(b); - strictEqual(b.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, b).length, 0); - strictEqual(b.groupPath, "Test1Client.B"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, b).map((x) => x.name), - ["x"] - ); - }); - - it("nested namespace and interface with naming change", async () => { - await runner.compile(` - @service({}) - namespace Test1Client { - @route("/b") - @clientName("BRename") - namespace B { - op x(): void; - - @route("/c") - interface C { - op y(): void; - } - } - } - `); - - const clients = listClients(runner.context); - strictEqual(clients.length, 1); - - const client1 = clients.find((x) => x.name === "Test1Client"); - ok(client1); - strictEqual(listOperationsInOperationGroup(runner.context, client1).length, 0); - - const client1Ogs = listOperationGroups(runner.context, client1); - strictEqual(client1Ogs.length, 1); - const b = client1Ogs.find((x) => x.type.name === "B"); - ok(b); - strictEqual(b.subOperationGroups?.length, 1); - strictEqual(listOperationGroups(runner.context, b).length, 1); - strictEqual(b.groupPath, "Test1Client.BRename"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, b).map((x) => x.name), - ["x"] - ); - - const c = b.subOperationGroups?.find((x) => x.type.name === "C"); - ok(c); - strictEqual(c.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, c).length, 0); - strictEqual(c.groupPath, "Test1Client.BRename.C"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, c).map((x) => x.name), - ["y"] - ); - }); - - it("nested empty namespace and interface", async () => { - await runner.compile(` - @service({}) - namespace Test1Client { - namespace B { - interface C { - } - } - } - `); - - const clients = listClients(runner.context); - strictEqual(clients.length, 1); - - const client1 = clients.find((x) => x.name === "Test1Client"); - ok(client1); - strictEqual(listOperationsInOperationGroup(runner.context, client1).length, 0); - - const client1Ogs = listOperationGroups(runner.context, client1); - strictEqual(client1Ogs.length, 1); - const b = client1Ogs.find((x) => x.type.name === "B"); - ok(b); - strictEqual(b.subOperationGroups?.length, 1); - strictEqual(listOperationGroups(runner.context, b).length, 1); - strictEqual(b.groupPath, "Test1Client.B"); - strictEqual(listOperationsInOperationGroup(runner.context, b).length, 0); - - const c = b.subOperationGroups?.find((x) => x.type.name === "C"); - ok(c); - strictEqual(c.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, c).length, 0); - strictEqual(c.groupPath, "Test1Client.B.C"); - strictEqual(listOperationsInOperationGroup(runner.context, c).length, 0); - }); - - it("rename client name", async () => { - await runner.compileWithCustomization( - ` - @service({}) - namespace A { - op x(): void; - } - `, - ` - namespace Customizations; - - @@clientName(A, "Test1Client"); - ` - ); - - const clients = listClients(runner.context); - deepStrictEqual(clients.length, 1); - - const client1 = clients.find((x) => x.name === "Test1Client"); - ok(client1); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, client1).map((x) => x.name), - ["x"] - ); - deepStrictEqual(listOperationGroups(runner.context, client1).length, 0); - }); - - it("rename client name - diagnostics", async () => { - const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( - ` - @service({}) - namespace A { - op x(): void; - } - `, - ` - @@client(A, { - name: "Test1Client", - service: A - }); - ` - ); - - expectDiagnostics(diagnostics, { - code: "@azure-tools/typespec-client-generator-core/wrong-client-decorator", - }); - - const clients = listClients(runner.context); - deepStrictEqual(clients.length, 1); - - const client1 = clients.find((x) => x.name === "AClient"); - ok(client1); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, client1).map((x) => x.name), - ["x"] - ); - deepStrictEqual(listOperationGroups(runner.context, client1).length, 0); - }); - - it("split into two clients", async () => { - await runner.compileWithCustomization( - ` - @service({}) - namespace A { - @route("/b") - interface B { - op x(): void; - } - - @route("/c") - interface C { - op y(): void; - } - }`, - ` - @client({name: "Test1Client", service: A}) - interface Test1Client { - x is A.B.x; - } - - @client({name: "Test2Client", service: A}) - interface Test2Client { - y is A.C.y; - } - ` - ); - - const clients = listClients(runner.context); - deepStrictEqual(clients.length, 2); - - const client1 = clients.find((x) => x.name === "Test1Client"); - ok(client1); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, client1).map((x) => x.name), - ["x"] - ); - deepStrictEqual(listOperationGroups(runner.context, client1).length, 0); - - const client2 = clients.find((x) => x.name === "Test2Client"); - ok(client2); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, client2).map((x) => x.name), - ["y"] - ); - deepStrictEqual(listOperationGroups(runner.context, client2).length, 0); - }); - - it("split into two clients - diagnostics", async () => { - const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( - ` - @service({}) - namespace A { - @route("/b") - namespace B { - op x(): void; - } - - @route("/c") - namespace C { - op y(): void; - } - }`, - ` - @@client(A.B, { - name: "Test1Client", - }); - - @@client(A.C, { - name: "Test2Client", - }); - ` - ); - - expectDiagnostics(diagnostics, [ - { - code: "@azure-tools/typespec-client-generator-core/wrong-client-decorator", - }, - { - code: "@azure-tools/typespec-client-generator-core/wrong-client-decorator", - }, - ]); - - const clients = listClients(runner.context); - strictEqual(clients.length, 1); - - const client = clients.find((x) => x.name === "AClient"); - ok(client); - strictEqual(listOperationsInOperationGroup(runner.context, client).length, 0); - - const clientOgs = listOperationGroups(runner.context, client); - strictEqual(clientOgs.length, 2); - - const og1 = clientOgs.find((x) => x.type.name === "B"); - ok(og1); - strictEqual(og1.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, og1).length, 0); - strictEqual(og1.groupPath, "AClient.B"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, og1).map((x) => x.name), - ["x"] - ); - - const og2 = clientOgs.find((x) => x.type.name === "C"); - ok(og2); - strictEqual(og2.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, og2).length, 0); - strictEqual(og2.groupPath, "AClient.C"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, og2).map((x) => x.name), - ["y"] - ); - }); - - it("one client and two operation groups", async () => { - await runner.compileWithCustomization( - ` - @service({}) - namespace PetStore { - @route("/feed") - op feed(): void; - - @route("/pet") - op pet(): void; - }`, - ` - @client({ - name: "PetStoreClient", - service: PetStore - }) - namespace Customizations; - - @operationGroup - interface OpGrp1{ - feed is PetStore.feed - } - - @operationGroup - interface OpGrp2 { - pet is PetStore.pet - } - ` - ); - - const clients = listClients(runner.context); - strictEqual(clients.length, 1); - - const client = clients.find((x) => x.name === "PetStoreClient"); - ok(client); - strictEqual(listOperationsInOperationGroup(runner.context, client).length, 0); - - const clientOgs = listOperationGroups(runner.context, client); - strictEqual(clientOgs.length, 2); - - const og1 = clientOgs.find((x) => x.type.name === "OpGrp1"); - ok(og1); - strictEqual(og1.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, og1).length, 0); - strictEqual(og1.groupPath, "PetStoreClient.OpGrp1"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, og1).map((x) => x.name), - ["feed"] - ); - - const og2 = clientOgs.find((x) => x.type.name === "OpGrp2"); - ok(og2); - strictEqual(og2.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, og2).length, 0); - strictEqual(og2.groupPath, "PetStoreClient.OpGrp2"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, og2).map((x) => x.name), - ["pet"] - ); - }); - - it("operation group - diagnostics", async () => { - const [_, diagnostics] = await runner.compileAndDiagnoseWithCustomization( - ` - @service({}) - namespace A { - @route("/b") - namespace B { - op x(): void; - } - - @route("/c") - namespace C { - op y(): void; - } - }`, - ` - @@operationGroup(A.B); - ` - ); - - expectDiagnostics(diagnostics, { - code: "@azure-tools/typespec-client-generator-core/wrong-client-decorator", - }); - - const clients = listClients(runner.context); - strictEqual(clients.length, 1); - - const client = clients.find((x) => x.name === "AClient"); - ok(client); - strictEqual(listOperationsInOperationGroup(runner.context, client).length, 0); - - const clientOgs = listOperationGroups(runner.context, client); - strictEqual(clientOgs.length, 2); - - const og1 = clientOgs.find((x) => x.type.name === "B"); - ok(og1); - strictEqual(og1.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, og1).length, 0); - strictEqual(og1.groupPath, "AClient.B"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, og1).map((x) => x.name), - ["x"] - ); - - const og2 = clientOgs.find((x) => x.type.name === "C"); - ok(og2); - strictEqual(og2.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, og2).length, 0); - strictEqual(og2.groupPath, "AClient.C"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, og2).map((x) => x.name), - ["y"] - ); - }); - - it("rearrange operations", async () => { - await runner.compile(` - @service({}) - namespace A { - @route("/b") - namespace B { - op x(): void; - - @route("/c") - interface C { - op y(): void; - } - } - } - - @client({ - name: "Test1Client", - service: A - }) - namespace Customization { - @operationGroup - namespace B { - op x is A.B.x; - op y is A.B.C.y; - } - } - `); - - const clients = listClients(runner.context); - strictEqual(clients.length, 1); - - const client1 = clients.find((x) => x.name === "Test1Client"); - ok(client1); - strictEqual(listOperationsInOperationGroup(runner.context, client1).length, 0); - - const client1Ogs = listOperationGroups(runner.context, client1); - strictEqual(client1Ogs.length, 1); - const b = client1Ogs.find((x) => x.type.name === "B"); - ok(b); - strictEqual(b.subOperationGroups, undefined); - strictEqual(listOperationGroups(runner.context, b).length, 0); - strictEqual(b.groupPath, "Test1Client.B"); - deepStrictEqual( - listOperationsInOperationGroup(runner.context, b).map((x) => x.name), - ["x", "y"] - ); - }); - }); - -======= ->>>>>>> 0f289160fadd84b6e0935d014740407f7e10354e async function protocolAPITestHelper( runner: SdkTestRunner, protocolValue: boolean,