diff --git a/packages/typespec-lintdiff/src/rules/get-in-operation-name.ts b/packages/typespec-lintdiff/src/rules/get-in-operation-name.ts index cc02f4d237..2cbe074a6b 100644 --- a/packages/typespec-lintdiff/src/rules/get-in-operation-name.ts +++ b/packages/typespec-lintdiff/src/rules/get-in-operation-name.ts @@ -1,23 +1,6 @@ -import { - createTCGCContext, - getClientLocation, - getClientNameOverride, - type TCGCContext, -} from "@azure-tools/typespec-client-generator-core"; -import { - createRule, - isGlobalNamespace, - isService, - isTemplateDeclarationOrInstance, - paramMessage, - type Interface, - type Namespace, - type Operation, - type Program, -} from "@typespec/compiler"; -import { capitalize } from "@typespec/compiler/casing"; +import { createRule, isTemplateDeclarationOrInstance, paramMessage } from "@typespec/compiler"; import { getHttpOperation } from "@typespec/http"; -import { getOperationId } from "@typespec/openapi"; +import { createAutorestOperationIdResolver } from "./utils/resolve-autorest-operation-id.js"; const validGetOperationId = /^(?:\w+_(?:Get|List)|Get|List)/; @@ -29,9 +12,7 @@ export const getInOperationNameRule = createRule({ default: paramMessage`'GET' operation '${"operationId"}' should use method name 'Get' or method name starting with 'List'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change.`, }, create(context) { - const tcgcContext = createTCGCContext(context.program, "@azure-tools/typespec-autorest", { - mutateNamespace: false, - }); + const resolveOperationId = createAutorestOperationIdResolver(context.program); return { operation: (operation) => { @@ -44,7 +25,7 @@ export const getInOperationNameRule = createRule({ return; } - const operationId = resolveAutorestOperationId(context.program, operation, tcgcContext); + const operationId = resolveOperationId(operation); if (operationId.length === 0 || validGetOperationId.test(operationId)) { return; } @@ -57,58 +38,3 @@ export const getInOperationNameRule = createRule({ }; }, }); - -function resolveAutorestOperationId( - program: Program, - operation: Operation, - tcgcContext: TCGCContext, -): string { - const explicitOperationId = getOperationId(program, operation); - if (explicitOperationId) { - return explicitOperationId; - } - - const operationName = getClientName(tcgcContext, operation); - const clientLocation = getClientLocation(tcgcContext, operation); - - if (clientLocation) { - if (typeof clientLocation === "string") { - return standardizeOperationId(`${clientLocation}_${operationName}`); - } - if (clientLocation.kind === "Interface") { - return standardizeOperationId( - `${getClientName(tcgcContext, clientLocation)}_${operationName}`, - ); - } - if (isGlobalNamespace(program, clientLocation) || isService(program, clientLocation)) { - return standardizeOperationId(operationName); - } - return standardizeOperationId(`${getClientName(tcgcContext, clientLocation)}_${operationName}`); - } - - let operationId: string; - if (operation.interface) { - operationId = `${getClientName(tcgcContext, operation.interface)}_${operationName}`; - } else if ( - operation.namespace === undefined || - isGlobalNamespace(program, operation.namespace) || - isService(program, operation.namespace) - ) { - operationId = operationName; - } else { - operationId = `${getClientName(tcgcContext, operation.namespace)}_${operationName}`; - } - - return standardizeOperationId(operationId); -} - -function standardizeOperationId(operationId: string): string { - return operationId - .split("_") - .map((part) => capitalize(part)) - .join("_"); -} - -function getClientName(tcgcContext: TCGCContext, type: Operation | Interface | Namespace): string { - return getClientNameOverride(tcgcContext, type) ?? type.name; -} diff --git a/packages/typespec-lintdiff/src/rules/list-in-operation-name.ts b/packages/typespec-lintdiff/src/rules/list-in-operation-name.ts index 65fac3bc47..146c1042e8 100644 --- a/packages/typespec-lintdiff/src/rules/list-in-operation-name.ts +++ b/packages/typespec-lintdiff/src/rules/list-in-operation-name.ts @@ -1,12 +1,20 @@ import { createRule, getPagingOperation, + ignoreDiagnostics, isArrayModelType, + isList, + isTemplateDeclarationOrInstance, paramMessage, type Model, + type Operation, + type Program, } from "@typespec/compiler"; -import { getHttpOperation, type HttpOperationResponse } from "@typespec/http"; +import { getHttpOperation, type HttpOperation, type HttpOperationResponse } from "@typespec/http"; import { getExtensions } from "@typespec/openapi"; +import { createAutorestOperationIdResolver } from "./utils/resolve-autorest-operation-id.js"; + +const validListOperationId = /^(?:(?:\w+_List\w*)|List)$/; export const listInOperationNameRule = createRule({ name: "list-in-operation-name", @@ -17,55 +25,55 @@ export const listInOperationNameRule = createRule({ default: paramMessage`Operation '${"operationName"}' returns a list/pageable response and should use method name starting with 'list'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change.`, }, create(context) { + const resolveOperationId = createAutorestOperationIdResolver(context.program); + return { operation: (operation) => { - const name = operation.name.toLowerCase(); - if (name.startsWith("list")) { + if (operation.interface === undefined && isTemplateDeclarationOrInstance(operation)) { + return; + } + + const [httpOperation] = getHttpOperation(context.program, operation); + if (httpOperation.verb !== "get" && httpOperation.verb !== "post") { return; } - if (!isListOperation(context.program, operation)) { + const operationId = resolveOperationId(operation); + if (validListOperationId.test(operationId)) { + return; + } + + if (!emitsListResponse(context.program, operation, httpOperation)) { return; } context.reportDiagnostic({ - target: operation, - format: { operationName: operation.name }, + target: getDiagnosticTarget(operation), + format: { operationName: operationId }, }); }, }; }, }); -function isListOperation(program: any, operation: any): boolean { - // Check for TypeSpec paging metadata - const [pagingOperation] = getPagingOperation(program, operation); - if (pagingOperation !== undefined) { +function emitsListResponse( + program: Program, + operation: Operation, + httpOperation: HttpOperation, +): boolean { + if (getExtensions(program, operation).has("x-ms-pageable")) { return true; } - // Check for x-ms-pageable extension - if (getExtensions(program, operation).has("x-ms-pageable")) { + const pagingOperation = ignoreDiagnostics(getPagingOperation(program, operation)); + if (isListOperation(program, operation) && pagingOperation?.output.nextLink !== undefined) { return true; } - // Check if any success response has a collection-shaped body - // (a model with a "value" array property and at most one other property) - const [httpOperation] = getHttpOperation(program, operation); - return httpOperation.responses.some((response) => - isCollectionResponse(program, response), - ); + return httpOperation.responses.some((response) => isCollectionResponse(program, response)); } -function isCollectionResponse( - program: any, - response: HttpOperationResponse, -): boolean { - const statusCode = response.statusCodes; - if (typeof statusCode === "number" && statusCode >= 300) { - return false; - } - +function isCollectionResponse(program: Program, response: HttpOperationResponse): boolean { for (const content of response.responses) { const body = content.body; if (body === undefined || body.type.kind !== "Model") { @@ -78,10 +86,7 @@ function isCollectionResponse( continue; } - if ( - valueProp.type.kind === "Model" && - isArrayModelType(program, valueProp.type) - ) { + if (valueProp.type.kind === "Model" && isArrayModelType(program, valueProp.type)) { const propCount = model.properties.size; if (propCount <= 2) { return true; @@ -91,3 +96,25 @@ function isCollectionResponse( return false; } + +function isListOperation(program: Program, operation: Operation): boolean { + for (let current: Operation | undefined = operation; current; current = current.sourceOperation) { + if (isList(program, current)) { + return true; + } + } + return false; +} + +function getDiagnosticTarget( + operation: Operation, +): Operation | NonNullable { + const operationInterface = operation.interface; + if ( + operationInterface?.node !== undefined && + operation.node?.parent !== operationInterface.node + ) { + return operationInterface; + } + return operation; +} diff --git a/packages/typespec-lintdiff/src/rules/utils/resolve-autorest-operation-id.ts b/packages/typespec-lintdiff/src/rules/utils/resolve-autorest-operation-id.ts new file mode 100644 index 0000000000..12217ae126 --- /dev/null +++ b/packages/typespec-lintdiff/src/rules/utils/resolve-autorest-operation-id.ts @@ -0,0 +1,79 @@ +import { + createTCGCContext, + getClientLocation, + getClientNameOverride, + type TCGCContext, +} from "@azure-tools/typespec-client-generator-core"; +import { + isGlobalNamespace, + isService, + type Interface, + type Namespace, + type Operation, + type Program, +} from "@typespec/compiler"; +import { capitalize } from "@typespec/compiler/casing"; +import { getOperationId } from "@typespec/openapi"; + +export function createAutorestOperationIdResolver(program: Program) { + const tcgcContext = createTCGCContext(program, "@azure-tools/typespec-autorest", { + mutateNamespace: false, + }); + + return (operation: Operation) => resolveAutorestOperationId(program, operation, tcgcContext); +} + +function resolveAutorestOperationId( + program: Program, + operation: Operation, + tcgcContext: TCGCContext, +): string { + const explicitOperationId = getOperationId(program, operation); + if (explicitOperationId) { + return explicitOperationId; + } + + const operationName = getClientName(tcgcContext, operation); + const clientLocation = getClientLocation(tcgcContext, operation); + + if (clientLocation) { + if (typeof clientLocation === "string") { + return standardizeOperationId(`${clientLocation}_${operationName}`); + } + if (clientLocation.kind === "Interface") { + return standardizeOperationId( + `${getClientName(tcgcContext, clientLocation)}_${operationName}`, + ); + } + if (isGlobalNamespace(program, clientLocation) || isService(program, clientLocation)) { + return standardizeOperationId(operationName); + } + return standardizeOperationId(`${getClientName(tcgcContext, clientLocation)}_${operationName}`); + } + + let operationId: string; + if (operation.interface) { + operationId = `${getClientName(tcgcContext, operation.interface)}_${operationName}`; + } else if ( + operation.namespace === undefined || + isGlobalNamespace(program, operation.namespace) || + isService(program, operation.namespace) + ) { + operationId = operationName; + } else { + operationId = `${getClientName(tcgcContext, operation.namespace)}_${operationName}`; + } + + return standardizeOperationId(operationId); +} + +function standardizeOperationId(operationId: string): string { + return operationId + .split("_") + .map((part) => capitalize(part)) + .join("_"); +} + +function getClientName(tcgcContext: TCGCContext, type: Operation | Interface | Namespace): string { + return getClientNameOverride(tcgcContext, type) ?? type.name; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/expect.json new file mode 100644 index 0000000000..ba21e936b5 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/expect.json @@ -0,0 +1,3 @@ +{ + "violation": true +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/main.tsp new file mode 100644 index 0000000000..f9294201eb --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/main.tsp @@ -0,0 +1,20 @@ +import "../../lib/imports.tsp"; +using TypeSpec.Http; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + @pageItems + items: string[]; + + @nextLink + nextLink: url; +} + +interface Widgets { + @list + @route("/widgets") + @get + getWidgets(): Result; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/output.json new file mode 100644 index 0000000000..bfb0772755 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/output.json @@ -0,0 +1,64 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/widgets": { + "get": { + "operationId": "Widgets_GetWidgets", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink", + "itemName": "items" + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + }, + "nextLink": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "items", + "nextLink" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/tsp-diagnostics.json new file mode 100644 index 0000000000..6b69a05ceb --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/tsp-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "tsp-lintdiff-local-linter/list-in-operation-name", + "severity": "warning", + "message": "Operation 'Widgets_GetWidgets' returns a list/pageable response and should use method name starting with 'list'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." + }, + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "severity": "warning", + "message": "Per the Noun_Verb convention for Operation Ids, the noun 'Widgets' should not appear after the underscore." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/validator-diagnostics.json new file mode 100644 index 0000000000..655326f035 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-with-next-link/validator-diagnostics.json @@ -0,0 +1,12 @@ +[ + { + "code": "ListInOperationName", + "message": "Since operation response has model definition in array type, it should be of the form \"_list\".", + "path": [ + "paths", + "/widgets", + "get" + ], + "severity": 0 + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/expect.json new file mode 100644 index 0000000000..396bb58c89 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/expect.json @@ -0,0 +1,13 @@ +{ + "violation": false, + "ambientDiagnostics": [ + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "count": 1 + } + ] +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/main.tsp new file mode 100644 index 0000000000..f80dbad562 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/main.tsp @@ -0,0 +1,17 @@ +import "../../lib/imports.tsp"; +using TypeSpec.Http; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + @pageItems + items: string[]; +} + +interface Widgets { + @list + @route("/widgets") + @get + getWidgets(): Result; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/output.json new file mode 100644 index 0000000000..c37e82a2d9 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/output.json @@ -0,0 +1,55 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/widgets": { + "get": { + "operationId": "Widgets_GetWidgets", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "items" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/tsp-diagnostics.json new file mode 100644 index 0000000000..28624a01cb --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/tsp-diagnostics.json @@ -0,0 +1,12 @@ +[ + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "severity": "warning", + "message": "Per the Noun_Verb convention for Operation Ids, the noun 'Widgets' should not appear after the underscore." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/validator-diagnostics.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/alternate-items-without-next-link/validator-diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/expect.json new file mode 100644 index 0000000000..ba21e936b5 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/expect.json @@ -0,0 +1,3 @@ +{ + "violation": true +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/main.tsp new file mode 100644 index 0000000000..117f6e9bf2 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/main.tsp @@ -0,0 +1,25 @@ +import "../../lib/imports.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Azure.ClientGenerator.Core; +using TypeSpec.Http; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + @pageItems + value: string[]; + + @nextLink + nextLink: url; +} + +interface Widgets { + @list + @route("/widgets") + @get + listWidgets(): Result; +} + +@@clientName(Widgets.listWidgets, "Get"); diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/output.json new file mode 100644 index 0000000000..5a462dc142 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/output.json @@ -0,0 +1,63 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/widgets": { + "get": { + "operationId": "Widgets_Get", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "string" + } + }, + "nextLink": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "value", + "nextLink" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/tsp-diagnostics.json new file mode 100644 index 0000000000..36be598246 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/tsp-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "tsp-lintdiff-local-linter/list-in-operation-name", + "severity": "warning", + "message": "Operation 'Widgets_Get' returns a list/pageable response and should use method name starting with 'list'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." + }, + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "severity": "warning", + "message": "Per the Noun_Verb convention for Operation Ids, the noun 'Widgets' should not appear after the underscore." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/validator-diagnostics.json new file mode 100644 index 0000000000..655326f035 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/client-name-violation/validator-diagnostics.json @@ -0,0 +1,12 @@ +[ + { + "code": "ListInOperationName", + "message": "Since operation response has model definition in array type, it should be of the form \"_list\".", + "path": [ + "paths", + "/widgets", + "get" + ], + "severity": 0 + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/compliant-with-template/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/compliant-with-template/main.tsp index f9f9024bc5..56b0a1d6c2 100644 --- a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/compliant-with-template/main.tsp +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/compliant-with-template/main.tsp @@ -1,7 +1,12 @@ import "../../lib/imports.tsp"; -using TypeSpec.Http; using TypeSpec.Rest; using TypeSpec.Versioning; using Azure.ResourceManager; +using TypeSpec.Http; +using TypeSpec.Rest; +using TypeSpec.Versioning; +using Azure.ResourceManager; -@armProviderNamespace @service(#{ title: "Test Service" }) @versioned(Versions) +@armProviderNamespace +@service(#{ title: "Test Service" }) +@versioned(Versions) @armCommonTypesVersion(CommonTypes.Versions.v5) namespace Microsoft.TestService; @@ -11,7 +16,10 @@ enum Versions { } model Widget is TrackedResource { - @key("widgetName") @segment("widgets") @doc("The name of the widget") @path + @key("widgetName") + @segment("widgets") + @doc("The name of the widget") + @path @pattern("^[a-zA-Z0-9_-]+$") name: string; } @@ -19,7 +27,9 @@ model Widget is TrackedResource { @doc("Widget resource properties.") model WidgetProperties { @doc("Description") description?: string; - @doc("Resource provisioning state") @visibility(Lifecycle.Read) + + @doc("Resource provisioning state") + @visibility(Lifecycle.Read) provisioningState?: ResourceProvisioningState; } @@ -36,5 +46,7 @@ interface Widgets { @doc("Get all widget configs") @route("configs") @get - getAllConfigs(...ResourceInstanceParameters): ArmResponse> | ErrorResponse; + getAllConfigs(...ResourceInstanceParameters): + | ArmResponse> + | ErrorResponse; } diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/compliant-with-template/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/compliant-with-template/tsp-diagnostics.json index be084d5973..d5b1554779 100644 --- a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/compliant-with-template/tsp-diagnostics.json +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/compliant-with-template/tsp-diagnostics.json @@ -72,7 +72,7 @@ { "code": "tsp-lintdiff-local-linter/list-in-operation-name", "severity": "warning", - "message": "Operation 'getAllConfigs' returns a list/pageable response and should use method name starting with 'List'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." + "message": "Operation 'Widgets_GetAllConfigs' returns a list/pageable response and should use method name starting with 'list'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." }, { "code": "tsp-lintdiff-local-linter/xms-examples-required", diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/expect.json new file mode 100644 index 0000000000..afa375e098 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/expect.json @@ -0,0 +1,13 @@ +{ + "violation": false, + "ambientDiagnostics": [ + { + "code": "tsp-lintdiff-local-linter/pagination-response", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "count": 1 + } + ] +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/main.tsp new file mode 100644 index 0000000000..c62e86571c --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/main.tsp @@ -0,0 +1,17 @@ +import "../../lib/imports.tsp"; +using TypeSpec.Http; +using TypeSpec.OpenAPI; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + value: string[]; +} + +interface Widgets { + @operationId("Widgets_ListConfigs") + @route("/configs") + @get + getConfigs(): Result; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/output.json new file mode 100644 index 0000000000..1d78c00cc1 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/output.json @@ -0,0 +1,55 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/configs": { + "get": { + "operationId": "Widgets_ListConfigs", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "value" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/tsp-diagnostics.json new file mode 100644 index 0000000000..b678e2026e --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/tsp-diagnostics.json @@ -0,0 +1,12 @@ +[ + { + "code": "tsp-lintdiff-local-linter/pagination-response", + "severity": "warning", + "message": "Operation might be pageable. Consider adding the x-ms-pageable extension." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/validator-diagnostics.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-compliant/validator-diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/expect.json new file mode 100644 index 0000000000..ba21e936b5 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/expect.json @@ -0,0 +1,3 @@ +{ + "violation": true +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/main.tsp new file mode 100644 index 0000000000..69142328e6 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/main.tsp @@ -0,0 +1,17 @@ +import "../../lib/imports.tsp"; +using TypeSpec.Http; +using TypeSpec.OpenAPI; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + value: string[]; +} + +interface Widgets { + @operationId("Widgets_GetConfigs") + @route("/configs") + @get + listConfigs(): Result; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/output.json new file mode 100644 index 0000000000..cfdc4d9da2 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/output.json @@ -0,0 +1,55 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/configs": { + "get": { + "operationId": "Widgets_GetConfigs", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "value" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/tsp-diagnostics.json new file mode 100644 index 0000000000..8ec1d4957b --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/tsp-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "tsp-lintdiff-local-linter/list-in-operation-name", + "severity": "warning", + "message": "Operation 'Widgets_GetConfigs' returns a list/pageable response and should use method name starting with 'list'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." + }, + { + "code": "tsp-lintdiff-local-linter/pagination-response", + "severity": "warning", + "message": "Operation might be pageable. Consider adding the x-ms-pageable extension." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/validator-diagnostics.json new file mode 100644 index 0000000000..98921592fd --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/emitted-operation-id-violation/validator-diagnostics.json @@ -0,0 +1,12 @@ +[ + { + "code": "ListInOperationName", + "message": "Since operation response has model definition in array type, it should be of the form \"_list\".", + "path": [ + "paths", + "/configs", + "get" + ], + "severity": 0 + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/expect.json new file mode 100644 index 0000000000..ba21e936b5 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/expect.json @@ -0,0 +1,3 @@ +{ + "violation": true +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/main.tsp new file mode 100644 index 0000000000..24268d8c51 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/main.tsp @@ -0,0 +1,14 @@ +import "../../lib/imports.tsp"; + +using TypeSpec.Http; +using TypeSpec.OpenAPI; + +@service(#{ title: "Test Service" }) +namespace TestService; + +interface Widgets { + @extension("x-ms-pageable", #{ nextLinkName: "nextLink" }) + @route("/widgets") + @post + getWidgets(): string[]; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/output.json new file mode 100644 index 0000000000..046437c899 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/output.json @@ -0,0 +1,46 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/widgets": { + "post": { + "operationId": "Widgets_GetWidgets", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "definitions": {}, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/tsp-diagnostics.json new file mode 100644 index 0000000000..6b69a05ceb --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/tsp-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "tsp-lintdiff-local-linter/list-in-operation-name", + "severity": "warning", + "message": "Operation 'Widgets_GetWidgets' returns a list/pageable response and should use method name starting with 'list'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." + }, + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "severity": "warning", + "message": "Per the Noun_Verb convention for Operation Ids, the noun 'Widgets' should not appear after the underscore." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/validator-diagnostics.json new file mode 100644 index 0000000000..8ee416ac9d --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/explicit-pageable-extension-violation/validator-diagnostics.json @@ -0,0 +1,12 @@ +[ + { + "code": "ListInOperationName", + "message": "Since operation response has model definition in array type, it should be of the form \"_list\".", + "path": [ + "paths", + "/widgets", + "post" + ], + "severity": 0 + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/expect.json new file mode 100644 index 0000000000..c904133eff --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/expect.json @@ -0,0 +1,17 @@ +{ + "violation": false, + "ambientDiagnostics": [ + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/pagination-response", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "count": 1 + } + ] +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/main.tsp new file mode 100644 index 0000000000..d361f9b56e --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/main.tsp @@ -0,0 +1,17 @@ +import "../../lib/imports.tsp"; +using TypeSpec.Http; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + value: string[]; + nextLink?: url; + count: int32; +} + +interface Widgets { + @route("/widgets") + @get + getWidgets(): Result; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/output.json new file mode 100644 index 0000000000..af444a5cbb --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/output.json @@ -0,0 +1,64 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/widgets": { + "get": { + "operationId": "Widgets_GetWidgets", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "string" + } + }, + "nextLink": { + "type": "string", + "format": "uri" + }, + "count": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "value", + "count" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/tsp-diagnostics.json new file mode 100644 index 0000000000..0fb6375baf --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/tsp-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "severity": "warning", + "message": "Per the Noun_Verb convention for Operation Ids, the noun 'Widgets' should not appear after the underscore." + }, + { + "code": "tsp-lintdiff-local-linter/pagination-response", + "severity": "warning", + "message": "Operation might be pageable. Consider adding the x-ms-pageable extension." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/validator-diagnostics.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/extra-collection-properties-compliant/validator-diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/expect.json new file mode 100644 index 0000000000..c904133eff --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/expect.json @@ -0,0 +1,17 @@ +{ + "violation": false, + "ambientDiagnostics": [ + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/pagination-response", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "count": 1 + } + ] +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/main.tsp new file mode 100644 index 0000000000..b17a12ed6d --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/main.tsp @@ -0,0 +1,15 @@ +import "../../lib/imports.tsp"; +using TypeSpec.Http; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + value: string[]; +} + +interface Widgets { + @route("/widgets") + @get + listWidgets(): Result; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/output.json new file mode 100644 index 0000000000..c948d38500 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/output.json @@ -0,0 +1,55 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/widgets": { + "get": { + "operationId": "Widgets_ListWidgets", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "value" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/tsp-diagnostics.json new file mode 100644 index 0000000000..0fb6375baf --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/tsp-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "severity": "warning", + "message": "Per the Noun_Verb convention for Operation Ids, the noun 'Widgets' should not appear after the underscore." + }, + { + "code": "tsp-lintdiff-local-linter/pagination-response", + "severity": "warning", + "message": "Operation might be pageable. Consider adding the x-ms-pageable extension." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/validator-diagnostics.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/grouped-list-compliant/validator-diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/expect.json new file mode 100644 index 0000000000..ba21e936b5 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/expect.json @@ -0,0 +1,3 @@ +{ + "violation": true +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/main.tsp new file mode 100644 index 0000000000..80a460d91c --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/main.tsp @@ -0,0 +1,30 @@ +import "../../lib/imports.tsp"; +import "@azure-tools/typespec-client-generator-core"; + +using Azure.ClientGenerator.Core; +using Azure.ResourceManager; +using TypeSpec.Http; +using TypeSpec.Versioning; + +@armProviderNamespace +@service(#{ title: "Test Service" }) +@versioned(Versions) +@armCommonTypesVersion(CommonTypes.Versions.v6) +namespace Microsoft.TestService; + +enum Versions { + @useDependency(Azure.ResourceManager.CommonTypes.Versions.v6) + v2024_01_01: "2024-01-01", +} + +model OperationList { + @pageItems + value: string[]; + + @nextLink + nextLink?: string; +} + +interface Operations extends Azure.ResourceManager.Legacy.Operations> {} + +@@clientName(Operations.list, "Get"); diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/output.json new file mode 100644 index 0000000000..97f38befcc --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/output.json @@ -0,0 +1,98 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "2024-01-01", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "host": "management.azure.com", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "security": [ + { + "azure_auth": [ + "user_impersonation" + ] + } + ], + "securityDefinitions": { + "azure_auth": { + "type": "oauth2", + "description": "Azure Active Directory OAuth2 Flow.", + "flow": "implicit", + "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize", + "scopes": { + "user_impersonation": "impersonate your user account" + } + } + }, + "tags": [ + { + "name": "Operations" + } + ], + "paths": { + "/providers/Microsoft.TestService/operations": { + "get": { + "operationId": "Operations_Get", + "tags": [ + "Operations" + ], + "description": "List the operations for the provider", + "parameters": [ + { + "$ref": "../../../../../common-types/resource-management/v6/types.json#/parameters/ApiVersionParameter" + } + ], + "responses": { + "200": { + "description": "Azure operation completed successfully.", + "schema": { + "$ref": "#/definitions/OperationList" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "../../../../../common-types/resource-management/v6/types.json#/definitions/ErrorResponse" + } + } + }, + "x-ms-pageable": { + "nextLinkName": "nextLink" + } + } + } + }, + "definitions": { + "OperationList": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "string" + } + }, + "nextLink": { + "type": "string" + } + }, + "required": [ + "value" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/tsp-diagnostics.json new file mode 100644 index 0000000000..1ff380c232 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/tsp-diagnostics.json @@ -0,0 +1,22 @@ +[ + { + "code": "@azure-tools/typespec-azure-core/documentation-required", + "severity": "warning", + "message": "The Model named 'OperationList' should have a documentation or description, use doc comment /** */ to provide it." + }, + { + "code": "@azure-tools/typespec-azure-core/documentation-required", + "severity": "warning", + "message": "The ModelProperty named 'value' should have a documentation or description, use doc comment /** */ to provide it." + }, + { + "code": "@azure-tools/typespec-azure-core/documentation-required", + "severity": "warning", + "message": "The ModelProperty named 'nextLink' should have a documentation or description, use doc comment /** */ to provide it." + }, + { + "code": "tsp-lintdiff-local-linter/list-in-operation-name", + "severity": "warning", + "message": "Operation 'Operations_Get' returns a list/pageable response and should use method name starting with 'list'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/validator-diagnostics.json new file mode 100644 index 0000000000..3e9c7dea35 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/inherited-client-name-violation/validator-diagnostics.json @@ -0,0 +1,12 @@ +[ + { + "code": "ListInOperationName", + "message": "Since operation response has model definition in array type, it should be of the form \"_list\".", + "path": [ + "paths", + "/providers/Microsoft.TestService/operations", + "get" + ], + "severity": 0 + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/migration.md b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/migration.md new file mode 100644 index 0000000000..cfa8c544a9 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/migration.md @@ -0,0 +1,306 @@ +# ListInOperationName migration + +## Conclusion + +The updated `tsp-lintdiff-local-linter/list-in-operation-name` rule is +functionally equivalent to the Swagger `ListInOperationName` rule over the +assessed population and the closed fixture matrix. + +**TypeSpec rule update required:** yes. The previous implementation checked the +TypeSpec operation name instead of the emitted AutoRest `operationId`, treated +all `@pageItems` operations as pageable, and did not recover inherited `@list` +metadata. It therefore missed emitted names such as `Operations_Get` and +reported internal ARM template instances. + +The production rule and fixtures were updated to: + +- resolve explicit and AutoRest-computed operation IDs, including client names, + client locations, interfaces, and namespaces; +- recognize explicit `x-ms-pageable`, emitted pageable responses with a next + link, and the validator's `value`-array response heuristic; +- apply those checks only to GET and POST operations, matching the validator's + JSONPath scope; +- traverse inherited operations for `@list` metadata; +- ignore internal top-level template instances while retaining authored + interface operations; and +- report inherited library operations on the authored interface. + +No additional production-rule changes are required by the final corpus +results. Six projects did not compile and remain explicit dataset uncertainty. + +## Evidence revisions and populations + +| Evidence | Revision/population | Meaning | +| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| [`docs/coverage_old.md`](../../../docs/coverage_old.md) | Local snapshot of [external gist](https://gist.github.com/catalinaperalta/b2e7d29a33b4b451bcfcc87e8314565a); 450 compiled projects, 210 validator rules. The snapshot does not record a generation date or generator commit. | Credits local and official coverage, not necessarily same-project observed diagnostics. | +| [`specs/coverage-breakdown.md`](../../../specs/coverage-breakdown.md) | Specs commit `f6b53f105b95da05276530a0754a1c71b4f16397`; full run generated `2026-09-02T09:13:40.666Z`; 468 selected, 462 compiled, 6 failed, 215 known validator rules. | Compares selected-version validator findings with diagnostics from successfully compiled TypeSpec projects. | +| Validator implementation | `Azure/azure-openapi-validator` checkout `a970d991d2785184d2786b85e0a345dc3f37bc25` | Source behavior used for fixture and corpus comparison. | +| TypeSpec base | `origin/feature/lintdiff-migration-new` at `e1e79db864fe17acb5bbbe3b3eb33fb5857c737d` | Base for the migrated-rule change. | + +Both ARM and data-plane services are in scope. The retained Swagger side uses +the dataset-selected latest API version. Ordinary TypeSpec linting visits all +declared operations, including declarations removed from that selected version +and client-generator-only override operations; those diagnostics are attributed +below rather than treated as Swagger semantic gaps. + +The failed projects were: + +- `specification/deviceprovisioningservices/resource-manager/Microsoft.Devices/DeviceProvisioningServices` +- `specification/monitor/resource-manager/Microsoft.Insights/Insights/TenantActionGroups` +- `specification/network/resource-manager/Microsoft.Network/Network/Network` +- `specification/quota/resource-manager/Microsoft.Quota/Quota` +- `specification/resources/resource-manager/Microsoft.Resources/deployments` +- `specification/servicelinker/resource-manager/Microsoft.ServiceLinker/ServiceLinker` + +They were excluded from both sides of the behavioral comparison. The retained +all-version validator shard contains one `ListInOperationName` finding for +Device Provisioning Services and 166 for Network, so the compile failures are +not silently treated as covered. + +## Report reconciliation + +| Report | Category | Validator projects | Local TypeSpec projects | Official projects | Overlap | Validator-only | TypeSpec-only | Diagnostics | +| --------------------------- | ------------------------ | -----------------: | ----------------------: | ----------------: | -------------: | ------------------: | ------------------: | ---------------------------: | +| External snapshot | 80-99% coverage / `lint` | 52 | 46 | 0 | Aggregate only | Not reconstructable | Not reconstructable | Not reported | +| Final lint-diff run | `production` / `partial` | 53 | 57 | 0 | 53 | 0 | 4 | 172 validator / 213 TypeSpec | +| Final attributed comparison | `production` / `partial` | 53 | 53 | 0 | 53 | 0 | 0 | 172 / 172 | + +The report differences have concrete causes: + +1. **Different snapshots and populations.** The external snapshot has 450 + compiled projects and 210 rules; the final lint-diff run has 462 compiled + projects and 215 rules at the pinned specs commit. +2. **Different coverage definitions.** The external snapshot's `46` is an + aggregate local-lint coverage count. It does not provide per-project results, + so its six unmatched projects cannot be reconstructed by subtraction. The + lint-diff report requires observed same-project overlap. +3. **Rule semantics changed.** The corrected emitted-operation-ID and inherited + list handling covers all 53 assessable validator projects, including Hybrid + Kubernetes and Solutions. +4. **Different API-version and reachability populations.** The raw TypeSpec + count includes 39 client-generator-only override declarations and two + operations removed from the selected latest API version. None exists in the + compared emitted Swagger. + +## Project-set comparison + +The final aligned population has 53 validator projects and 53 attributable +TypeSpec projects, with complete overlap and no one-sided projects. + +The raw TypeSpec-only projects were: + +- `specification/cdn/resource-manager/Microsoft.Cdn/Cdn` +- `specification/databoxedge/resource-manager/Microsoft.DataBoxEdge/DataBoxEdge` +- `specification/netapp/resource-manager/Microsoft.NetApp/NetApp` +- `specification/search/resource-manager/Microsoft.Search/Search` + +CDN and NetApp each contribute one operation removed from the selected API +version. Data Box Edge contributes 13 SDK override declarations and Search +contributes seven. After those non-emitted declarations are excluded, the +TypeSpec-only set is empty. + +## Diagnostic cardinality + +The conservative identities are: + +- validator raw identity: project + Swagger file + JSON path; +- validator file-independent identity: project + JSON path; and +- TypeSpec source identity: project + source file + line + column. + +| Population | Validator raw | Validator raw identity | Validator file-independent | TypeSpec raw | TypeSpec source identity | +| --------------------------------------- | ------------: | ---------------------: | -------------------------: | -----------: | -----------------------: | +| Successful projects, before attribution | 172 | 172 | 172 | 213 | 213 | +| Selected-version emitted operations | 172 | 172 | 172 | 172 | 172 | + +Before attribution, 49 of 57 TypeSpec-firing projects had equal counts, no +project had more validator findings, and eight projects had 41 additional +TypeSpec findings. The positive differences were: + +| Project | Validator | TypeSpec | Cause | +| ------------------------ | --------: | -------: | ----------------------------------------- | +| Data Box Edge | 0 | 13 | SDK-only override operations | +| App Service | 41 | 50 | 9 SDK-only override operations | +| Recovery Services Backup | 1 | 8 | 7 SDK-only override operations | +| Search | 0 | 7 | SDK-only override operations | +| Guest Configuration | 2 | 4 | 2 SDK-only override operations | +| Cognitive Services | 2 | 3 | 1 SDK-only override operation | +| CDN | 0 | 1 | Operation removed before selected version | +| NetApp | 0 | 1 | Operation removed before selected version | + +Thus the total positive TypeSpec difference is 41 and the total positive +validator difference is zero. Excluding 39 non-emitted SDK overrides and two +older-version operations yields equal counts in all 53 overlapping projects. +Raw count equality is not the migration criterion, but the attributed equality +corroborates the semantic and project-set evidence. + +## Emission and fixture matrix + +| Authored shape | Emitted field/branch | Expected result | Fixture | +| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------- | --------------------------------------- | +| POST with explicit `@extension("x-ms-pageable", ...)` | Explicit `x-ms-pageable`; POST is in validator scope | Violation for a non-list operation ID | `explicit-pageable-extension-violation` | +| `@list` with `@pageItems` and `@nextLink` using a non-`value` item property | AutoRest pageable branch emits `x-ms-pageable` | Violation | `alternate-items-with-next-link` | +| `@list` with `@pageItems` but no next link | No emitted `x-ms-pageable`; no `value` array | Compliant | `alternate-items-without-next-link` | +| Response model has a `value` array and at most two properties | Validator response-schema heuristic | Violation | `emitted-operation-id-violation` | +| Response model has a `value` array and three properties | Outside validator response-schema heuristic | Compliant | `extra-collection-properties-compliant` | +| PUT response model has a `value` array | PUT is outside validator JSONPath scope | Compliant | `put-value-array-compliant` | +| Explicit emitted operation ID is `Widgets_GetConfigs` | Explicit `@operationId` | Violation | `emitted-operation-id-violation` | +| Explicit emitted operation ID is `Widgets_ListConfigs` | Explicit `@operationId` | Compliant | `emitted-operation-id-compliant` | +| Interface grouping emits `Widgets_ListWidgets` | AutoRest interface grouping | Compliant | `grouped-list-compliant` | +| Root operation emits `ListWidgets` | Root operation ID; validator regex accepts only exact root `List` | Violation | `root-list-prefix-violation` | +| `@@clientName(..., "Get")` emits `Widgets_Get` | AutoRest client-name override | Violation | `client-name-violation` | +| Legacy inherited `@list` plus client name emits `Operations_Get` | Inherited list metadata and authored-interface diagnostic target | Violation | `inherited-client-name-violation` | +| Standard ARM list template plus authored custom operation | Template instance is compliant and ignored; authored custom operation is checked | One intended violation only | `compliant-with-template` | + +The matrix covers GET/POST verb selection, excluded verbs, the explicit +extension, pageable-emission, response-schema, operation-ID resolution, +inheritance, diagnostic-target, and template-filter branches used by the +implementation. + +## Code-backed gap examples + +### Gap example: inherited AutoRest operation ID + +- **Classification:** validator-only +- **Status:** fixed +- **Project/API version:** `HybridKubernetes` / `2026-05-01` +- **Source:** `main.tsp:66` and `back-compatible.tsp:36` + +**TypeSpec source** + +```typespec +interface Operations + extends Azure.ResourceManager.Legacy.Operations< + ArmResponse, + Error = ErrorResponse + > {} + +@@clientLocation(Operations.list, Operations); +@@clientName(Operations.list, "Get", "!javascript"); +``` + +**Emitted OpenAPI or validator behavior** + +```json +{ + "operationId": "Operations_Get", + "x-ms-pageable": { + "nextLinkName": "nextLink" + } +} +``` + +| Engine | Observed result | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------- | +| Swagger validator | Diagnostic because `Operations_Get` is pageable and does not match `Noun_List*`. | +| TypeSpec lint | Diagnostic at the authored `Operations` interface after following inherited `@list` state and resolving the emitted ID. | + +**Explanation:** The old TypeSpec rule checked the inherited operation's +semantic name and did not recover the emitted client name. The operation node +also originates in library code, so targeting the authored interface is needed +for a stable visible diagnostic. + +**Disposition:** Fixed by the shared AutoRest operation-ID resolver, inherited +list traversal, and authored-interface diagnostic target. The same correction +covers `Solutions.Management`. + +### Gap example: SDK-only override declarations + +- **Classification:** TypeSpec-only and count-only +- **Status:** population mismatch +- **Project/API version:** `DataBoxEdge` / `2023-12-01` +- **Source:** `client.tsp:74` + +**TypeSpec source** + +```typespec +op UsersListByDataBoxEdgeDeviceCustomized( + ...Azure.ResourceManager.ProviderNamespace, + ...Azure.ResourceManager.CommonTypes.ApiVersionParameter, + ...Azure.ResourceManager.CommonTypes.SubscriptionIdParameter, + @path deviceName: string, + @query("$filter") $filter?: string, + ...Azure.ResourceManager.CommonTypes.ResourceGroupNameParameter, +): UserList; + +@@override( + Users.listByDataBoxEdgeDevice, + UsersListByDataBoxEdgeDeviceCustomized, + "python,go,javascript" +); +``` + +**Emitted OpenAPI or validator behavior** + +```json +{ + "operationId": "Users_ListByDataBoxEdgeDevice" +} +``` + +The compared Swagger contains the original operation ID and no +`UsersListByDataBoxEdgeDeviceCustomized` operation. + +| Engine | Observed result | +| ----------------- | ------------------------------------------------------------------------------------------ | +| Swagger validator | No finding for the customization because it is not emitted as an OpenAPI operation. | +| TypeSpec lint | Visits the standalone SDK override declaration and reports its semantic AutoRest-style ID. | + +**Explanation:** `@@override` declares an alternate SDK method shape for named +languages; it does not add a corresponding operation to the compared OpenAPI. +This cause accounts for 39 diagnostics across Data Box Edge, Search, App +Service, Recovery Services Backup, Guest Configuration, and Cognitive +Services. + +**Disposition:** Excluded from the emitted-operation behavioral population. The +production lint remains useful on authored declarations and is not weakened to +hide client customization code. + +### Gap example: operation removed from selected API version + +- **Classification:** TypeSpec-only +- **Status:** population mismatch +- **Project/API version:** `Cdn` / selected `2026-04-01-preview` +- **Source:** `DeploymentVersion.tsp:19` + +**TypeSpec source** + +```typespec +@added(Versions.v2025_09_01_preview) +@removed(Versions.v2025_12_01) +@parentResource(Profile) +model DeploymentVersion is Azure.ResourceManager.ProxyResource; +``` + +The `DeploymentVersions.compare` operation is part of this resource's +operations and receives a raw TypeSpec diagnostic. + +**Projection/report metadata** + +```json +{ + "apiVersion": "2026-04-01-preview" +} +``` + +| Engine | Observed result | +| ----------------- | ---------------------------------------------------------------------------------------- | +| Swagger validator | No diagnostic; `DeploymentVersions_Compare` is absent from the selected-version OpenAPI. | +| TypeSpec lint | Raw diagnostic while visiting declarations from all service versions. | + +**Explanation:** The retained Swagger is projected to a version after the +resource was removed, while ordinary lint output includes its declaration. +NetApp's `oldlistReplications`, removed at `2025-08-01`, is the second instance +of the same cause. + +**Disposition:** Excluded from the selected-version comparison. No production +rule suppression is appropriate for a valid diagnostic in an older API +version. + +## Remaining uncertainty + +No validator-only project or unexplained count outlier remains in the 462 +successfully compiled projects. The six compile failures prevent a corpus claim +for those projects, although the focused emission matrix is complete for the +rule's authorable branches. Functional equivalence therefore applies to the +assessed population and rule semantics; it does not assert raw all-version +diagnostic equality or silently cover the failed projects. diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/expect.json new file mode 100644 index 0000000000..9a815cd2c2 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/expect.json @@ -0,0 +1,21 @@ +{ + "violation": false, + "ambientDiagnostics": [ + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/put-in-operation-name", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/put-path", + "count": 1 + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "count": 1 + } + ] +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/main.tsp new file mode 100644 index 0000000000..082ad1b69c --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/main.tsp @@ -0,0 +1,16 @@ +import "../../lib/imports.tsp"; + +using TypeSpec.Http; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + value: string[]; +} + +interface Widgets { + @route("/widgets") + @put + getWidgets(): Result; +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/output.json new file mode 100644 index 0000000000..fb76b8df91 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/output.json @@ -0,0 +1,55 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/widgets": { + "put": { + "operationId": "Widgets_GetWidgets", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "value" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/tsp-diagnostics.json new file mode 100644 index 0000000000..e0e3db9d43 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/tsp-diagnostics.json @@ -0,0 +1,22 @@ +[ + { + "code": "tsp-lintdiff-local-linter/operation-id-noun-verb", + "severity": "warning", + "message": "Per the Noun_Verb convention for Operation Ids, the noun 'Widgets' should not appear after the underscore." + }, + { + "code": "tsp-lintdiff-local-linter/put-in-operation-name", + "severity": "warning", + "message": "'PUT' operation 'getWidgets' should use method name 'create'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." + }, + { + "code": "tsp-lintdiff-local-linter/put-path", + "severity": "warning", + "message": "The path for a put should have a final path parameter." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/validator-diagnostics.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/put-value-array-compliant/validator-diagnostics.json @@ -0,0 +1 @@ +[] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/expect.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/expect.json new file mode 100644 index 0000000000..ba21e936b5 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/expect.json @@ -0,0 +1,3 @@ +{ + "violation": true +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/main.tsp b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/main.tsp new file mode 100644 index 0000000000..0a89907597 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/main.tsp @@ -0,0 +1,13 @@ +import "../../lib/imports.tsp"; +using TypeSpec.Http; + +@service(#{ title: "Test Service" }) +namespace TestService; + +model Result { + value: string[]; +} + +@route("/widgets") +@get +op listWidgets(): Result; diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/output.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/output.json new file mode 100644 index 0000000000..0285527f76 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/output.json @@ -0,0 +1,55 @@ +{ + "swagger": "2.0", + "info": { + "title": "Test Service", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/widgets": { + "get": { + "operationId": "ListWidgets", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Result" + } + } + } + } + } + }, + "definitions": { + "Result": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "value" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/tsp-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/tsp-diagnostics.json new file mode 100644 index 0000000000..525a37dbc8 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/tsp-diagnostics.json @@ -0,0 +1,17 @@ +[ + { + "code": "tsp-lintdiff-local-linter/list-in-operation-name", + "severity": "warning", + "message": "Operation 'ListWidgets' returns a list/pageable response and should use method name starting with 'list'. Note: If you have already shipped an SDK on top of this spec, fixing this warning may introduce a breaking change." + }, + { + "code": "tsp-lintdiff-local-linter/pagination-response", + "severity": "warning", + "message": "Operation might be pageable. Consider adding the x-ms-pageable extension." + }, + { + "code": "tsp-lintdiff-local-linter/xms-examples-required", + "severity": "warning", + "message": "Please provide x-ms-examples describing minimum/maximum property set for response/request payloads for operations." + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/validator-diagnostics.json b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/validator-diagnostics.json new file mode 100644 index 0000000000..655326f035 --- /dev/null +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/root-list-prefix-violation/validator-diagnostics.json @@ -0,0 +1,12 @@ +[ + { + "code": "ListInOperationName", + "message": "Since operation response has model definition in array type, it should be of the form \"_list\".", + "path": [ + "paths", + "/widgets", + "get" + ], + "severity": 0 + } +] diff --git a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/rule.md b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/rule.md index 107d1951aa..107526acaa 100644 --- a/packages/typespec-lintdiff/test/fixtures/ListInOperationName/rule.md +++ b/packages/typespec-lintdiff/test/fixtures/ListInOperationName/rule.md @@ -1,9 +1,11 @@ --- validatorRuleId: ListInOperationName engine: spectral -coverageKind: lint +coverageKind: partial tspLints: - tsp-lintdiff-local-linter/list-in-operation-name +officialTspLints: + - "@azure-tools/typespec-azure-core/use-standard-names" --- # ListInOperationName @@ -11,3 +13,14 @@ tspLints: **Severity:** warning **Applies to:** Both ARM and DataPlane + +**Original rule:** [Azure/azure-openapi-validator `ListInOperationName`](https://github.com/Azure/azure-openapi-validator/blob/main/docs/list-in-operation-name.md) + +Operations whose emitted OpenAPI contains `x-ms-pageable`, or whose response +schema has a `value` array and no more than one other property, must emit an +`operationId` matching `Noun_List*` or exactly `List`. + +Standard ARM list templates produce compliant operation IDs, while the official +`use-standard-names` rule covers data-plane services but is disabled by the ARM +ruleset. This direct rule covers custom ARM operations that can emit the same +Swagger shapes without using those templates.