diff --git a/.chronus/changes/fix-xms-paths-sorting-2026-03-23-18-18-00.md b/.chronus/changes/fix-xms-paths-sorting-2026-03-23-18-18-00.md new file mode 100644 index 0000000000..5b15f2ef6a --- /dev/null +++ b/.chronus/changes/fix-xms-paths-sorting-2026-03-23-18-18-00.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-autorest" +--- + +Fix sorting of x-ms-paths entries that start with `?` (query-only paths). Previously these paths were not sorted alphabetically. diff --git a/packages/typespec-autorest/src/json-schema-sorter/sorter.ts b/packages/typespec-autorest/src/json-schema-sorter/sorter.ts index 50a6af6b89..1859fa5785 100644 --- a/packages/typespec-autorest/src/json-schema-sorter/sorter.ts +++ b/packages/typespec-autorest/src/json-schema-sorter/sorter.ts @@ -107,8 +107,17 @@ function defaultCompare(a: number | string, b: number | string) { /** Sort urls in a specific way so path with field show up before a fixed segment. */ function compareUrl(leftPath: string, rightPath: string) { - const leftParts = leftPath.split("/").slice(1); - const rightParts = rightPath.split("/").slice(1); + // Separate path and query string portions to handle x-ms-paths entries starting with "?" + const leftQueryIndex = leftPath.indexOf("?"); + const rightQueryIndex = rightPath.indexOf("?"); + const leftPathPortion = leftQueryIndex >= 0 ? leftPath.substring(0, leftQueryIndex) : leftPath; + const rightPathPortion = + rightQueryIndex >= 0 ? rightPath.substring(0, rightQueryIndex) : rightPath; + const leftQuery = leftQueryIndex >= 0 ? leftPath.substring(leftQueryIndex) : ""; + const rightQuery = rightQueryIndex >= 0 ? rightPath.substring(rightQueryIndex) : ""; + + const leftParts = leftPathPortion ? leftPathPortion.split("/").slice(1) : []; + const rightParts = rightPathPortion ? rightPathPortion.split("/").slice(1) : []; for (let i = 0; i < Math.max(leftParts.length, rightParts.length); i++) { // Have we exhausted the path parts of one of them? @@ -137,8 +146,8 @@ function compareUrl(leftPath: string, rightPath: string) { } } - // Must be the same - return 0; + // Path portions are the same, compare query strings + return defaultCompare(leftQuery, rightQuery); } function resolveSchema(schema: JsonSchemaType, reader: JsonSchemaReader): ResolvedJsonSchemaType { diff --git a/packages/typespec-autorest/test/sorting.test.ts b/packages/typespec-autorest/test/sorting.test.ts index e00cd088bc..a254c55368 100644 --- a/packages/typespec-autorest/test/sorting.test.ts +++ b/packages/typespec-autorest/test/sorting.test.ts @@ -80,6 +80,42 @@ describe("typespec-autorest: OpenAPI output should be determinstic", () => { ]); }); + it("sorts x-ms-paths with query-only paths", () => { + const sorted = sortOpenAPIDocument({ + swagger: "2.0", + info: {} as any, + paths: {}, + "x-ms-paths": { + "?b=1": {}, + "?a=1": {}, + "?c=1": {}, + }, + }); + + deepStrictEqual(Object.keys(sorted["x-ms-paths"]!), ["?a=1", "?b=1", "?c=1"]); + }); + + it("sorts x-ms-paths with mixed path and query-only entries", () => { + const sorted = sortOpenAPIDocument({ + swagger: "2.0", + info: {} as any, + paths: {}, + "x-ms-paths": { + "/b?_overload=op1": {}, + "?b=1": {}, + "/a?_overload=op2": {}, + "?a=1": {}, + }, + }); + + deepStrictEqual(Object.keys(sorted["x-ms-paths"]!), [ + "?a=1", + "?b=1", + "/a?_overload=op2", + "/b?_overload=op1", + ]); + }); + it("header already in lexical order", async () => { const res = await openApiFor( `