From 98df3f962a63a126da2c49c884fec8fd269d9dc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:21:37 +0000 Subject: [PATCH 01/16] Initial plan From 6891face83f0b3f74052ab58ed8a356805a1178f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:26:08 +0000 Subject: [PATCH 02/16] Fix MutabilityWithReadOnly rule to avoid out of memory issue by filtering earlier Co-authored-by: AkhilaIlla <36493984+AkhilaIlla@users.noreply.github.com> --- packages/rulesets/generated/spectral/az-arm.js | 2 +- packages/rulesets/generated/spectral/az-common.js | 2 +- packages/rulesets/generated/spectral/az-dataplane.js | 2 +- packages/rulesets/src/spectral/az-common.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rulesets/generated/spectral/az-arm.js b/packages/rulesets/generated/spectral/az-arm.js index 2926c4f06..5be66a0ef 100644 --- a/packages/rulesets/generated/spectral/az-arm.js +++ b/packages/rulesets/generated/spectral/az-arm.js @@ -1067,7 +1067,7 @@ const ruleset$1 = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..?(@property === 'readOnly')^"], + given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/generated/spectral/az-common.js b/packages/rulesets/generated/spectral/az-common.js index 1c428e839..bea1e71f9 100644 --- a/packages/rulesets/generated/spectral/az-common.js +++ b/packages/rulesets/generated/spectral/az-common.js @@ -821,7 +821,7 @@ const ruleset = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..?(@property === 'readOnly')^"], + given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/generated/spectral/az-dataplane.js b/packages/rulesets/generated/spectral/az-dataplane.js index 470b83de1..17998a348 100644 --- a/packages/rulesets/generated/spectral/az-dataplane.js +++ b/packages/rulesets/generated/spectral/az-dataplane.js @@ -851,7 +851,7 @@ const ruleset$1 = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..?(@property === 'readOnly')^"], + given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/src/spectral/az-common.ts b/packages/rulesets/src/spectral/az-common.ts index 560c38ac2..26a97f2dc 100644 --- a/packages/rulesets/src/spectral/az-common.ts +++ b/packages/rulesets/src/spectral/az-common.ts @@ -263,7 +263,7 @@ const ruleset: any = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..?(@property === 'readOnly')^"], + given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], then: { function: mutabilityWithReadOnly, }, From 93c4e14c363d3128b1d10e3ae4885c1fa5787935 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:38:06 +0000 Subject: [PATCH 03/16] Remove redundant checks from mutability-with-read-only function Co-authored-by: AkhilaIlla <36493984+AkhilaIlla@users.noreply.github.com> --- .../rulesets/generated/spectral/az-arm.js | 4 +- .../rulesets/generated/spectral/az-common.js | 4 +- .../generated/spectral/az-dataplane.js | 4 +- .../Extensions/mutability-with-read-only.ts | 8 ++-- .../test/mutability-with-read-only.test.ts | 37 +++++++++++++++++++ 5 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/rulesets/generated/spectral/az-arm.js b/packages/rulesets/generated/spectral/az-arm.js index 5be66a0ef..5b1c4682b 100644 --- a/packages/rulesets/generated/spectral/az-arm.js +++ b/packages/rulesets/generated/spectral/az-arm.js @@ -144,9 +144,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (prop.readOnly === undefined || - prop["x-ms-mutability"] === undefined || - prop["x-ms-mutability"].length === 0) { + if (prop["x-ms-mutability"].length === 0) { return []; } const path = ctx.path || []; diff --git a/packages/rulesets/generated/spectral/az-common.js b/packages/rulesets/generated/spectral/az-common.js index bea1e71f9..828f94750 100644 --- a/packages/rulesets/generated/spectral/az-common.js +++ b/packages/rulesets/generated/spectral/az-common.js @@ -141,9 +141,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (prop.readOnly === undefined || - prop["x-ms-mutability"] === undefined || - prop["x-ms-mutability"].length === 0) { + if (prop["x-ms-mutability"].length === 0) { return []; } const path = ctx.path || []; diff --git a/packages/rulesets/generated/spectral/az-dataplane.js b/packages/rulesets/generated/spectral/az-dataplane.js index 17998a348..63fd59ad0 100644 --- a/packages/rulesets/generated/spectral/az-dataplane.js +++ b/packages/rulesets/generated/spectral/az-dataplane.js @@ -141,9 +141,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (prop.readOnly === undefined || - prop["x-ms-mutability"] === undefined || - prop["x-ms-mutability"].length === 0) { + if (prop["x-ms-mutability"].length === 0) { return []; } const path = ctx.path || []; diff --git a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts index 45f425d41..92d6e1a2d 100644 --- a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts +++ b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts @@ -4,11 +4,9 @@ export const mutabilityWithReadOnly = (prop: any, _opts: any, ctx: any) => { if (prop === null || typeof prop !== "object") { return []; } - if ( - prop.readOnly === undefined || - prop["x-ms-mutability"] === undefined || - prop["x-ms-mutability"].length === 0 - ) { + // The given clause filters for readOnly !== undefined and x-ms-mutability !== undefined + // We only need to check if x-ms-mutability is an empty array + if (prop["x-ms-mutability"].length === 0) { return []; } const path = ctx.path || []; diff --git a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts index 76273f7ea..af5719295 100644 --- a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts +++ b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts @@ -93,3 +93,40 @@ test("MutabilityWithReadOnly should find no errors", () => { expect(results.length).toBe(0); }); }); + +test("MutabilityWithReadOnly should ignore empty x-ms-mutability arrays", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/LroStatusCodeSchema", + }, + }, + }, + }, + }, + }, + definitions: { + LroStatusCodeSchema: { + type: "object", + properties: { + name: { + type: "string", + readOnly: true, + "x-ms-mutability": [], + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + // Empty x-ms-mutability arrays should be ignored (no errors) + expect(results.length).toBe(0); + }); +}); From af3e536bc555288c7bb38b11a7dfef239ad8ae8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:41:21 +0000 Subject: [PATCH 04/16] Add Array.isArray check for x-ms-mutability defensive programming Co-authored-by: AkhilaIlla <36493984+AkhilaIlla@users.noreply.github.com> --- .../rulesets/generated/spectral/az-arm.js | 2 +- .../rulesets/generated/spectral/az-common.js | 2 +- .../generated/spectral/az-dataplane.js | 2 +- .../Extensions/mutability-with-read-only.ts | 4 +- .../test/mutability-with-read-only.test.ts | 37 +++++++++++++++++++ 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/rulesets/generated/spectral/az-arm.js b/packages/rulesets/generated/spectral/az-arm.js index 5b1c4682b..069e7578e 100644 --- a/packages/rulesets/generated/spectral/az-arm.js +++ b/packages/rulesets/generated/spectral/az-arm.js @@ -144,7 +144,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (prop["x-ms-mutability"].length === 0) { + if (!Array.isArray(prop["x-ms-mutability"]) || prop["x-ms-mutability"].length === 0) { return []; } const path = ctx.path || []; diff --git a/packages/rulesets/generated/spectral/az-common.js b/packages/rulesets/generated/spectral/az-common.js index 828f94750..92907c08b 100644 --- a/packages/rulesets/generated/spectral/az-common.js +++ b/packages/rulesets/generated/spectral/az-common.js @@ -141,7 +141,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (prop["x-ms-mutability"].length === 0) { + if (!Array.isArray(prop["x-ms-mutability"]) || prop["x-ms-mutability"].length === 0) { return []; } const path = ctx.path || []; diff --git a/packages/rulesets/generated/spectral/az-dataplane.js b/packages/rulesets/generated/spectral/az-dataplane.js index 63fd59ad0..9b9ec53a8 100644 --- a/packages/rulesets/generated/spectral/az-dataplane.js +++ b/packages/rulesets/generated/spectral/az-dataplane.js @@ -141,7 +141,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (prop["x-ms-mutability"].length === 0) { + if (!Array.isArray(prop["x-ms-mutability"]) || prop["x-ms-mutability"].length === 0) { return []; } const path = ctx.path || []; diff --git a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts index 92d6e1a2d..593b38442 100644 --- a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts +++ b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts @@ -5,8 +5,8 @@ export const mutabilityWithReadOnly = (prop: any, _opts: any, ctx: any) => { return []; } // The given clause filters for readOnly !== undefined and x-ms-mutability !== undefined - // We only need to check if x-ms-mutability is an empty array - if (prop["x-ms-mutability"].length === 0) { + // We need to check if x-ms-mutability is an array and not empty + if (!Array.isArray(prop["x-ms-mutability"]) || prop["x-ms-mutability"].length === 0) { return []; } const path = ctx.path || []; diff --git a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts index af5719295..7c3a03f7f 100644 --- a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts +++ b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts @@ -130,3 +130,40 @@ test("MutabilityWithReadOnly should ignore empty x-ms-mutability arrays", () => expect(results.length).toBe(0); }); }); + +test("MutabilityWithReadOnly should ignore non-array x-ms-mutability values", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/LroStatusCodeSchema", + }, + }, + }, + }, + }, + }, + definitions: { + LroStatusCodeSchema: { + type: "object", + properties: { + name: { + type: "string", + readOnly: true, + "x-ms-mutability": "read", + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + // Non-array x-ms-mutability values should be ignored (no errors) + expect(results.length).toBe(0); + }); +}); From 7142114b0fcda6733e1fd5e43e84b6739e12f89a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:46:54 +0000 Subject: [PATCH 05/16] Move length check to given clause for maximum efficiency Co-authored-by: AkhilaIlla <36493984+AkhilaIlla@users.noreply.github.com> --- packages/rulesets/generated/spectral/az-arm.js | 4 ++-- packages/rulesets/generated/spectral/az-common.js | 4 ++-- packages/rulesets/generated/spectral/az-dataplane.js | 4 ++-- packages/rulesets/src/spectral/az-common.ts | 2 +- .../functions/Extensions/mutability-with-read-only.ts | 9 ++++++--- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/rulesets/generated/spectral/az-arm.js b/packages/rulesets/generated/spectral/az-arm.js index 069e7578e..c35a5d5e4 100644 --- a/packages/rulesets/generated/spectral/az-arm.js +++ b/packages/rulesets/generated/spectral/az-arm.js @@ -144,7 +144,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (!Array.isArray(prop["x-ms-mutability"]) || prop["x-ms-mutability"].length === 0) { + if (!Array.isArray(prop["x-ms-mutability"])) { return []; } const path = ctx.path || []; @@ -1065,7 +1065,7 @@ const ruleset$1 = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], + given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined && @['x-ms-mutability'].length > 0)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/generated/spectral/az-common.js b/packages/rulesets/generated/spectral/az-common.js index 92907c08b..762259a0f 100644 --- a/packages/rulesets/generated/spectral/az-common.js +++ b/packages/rulesets/generated/spectral/az-common.js @@ -141,7 +141,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (!Array.isArray(prop["x-ms-mutability"]) || prop["x-ms-mutability"].length === 0) { + if (!Array.isArray(prop["x-ms-mutability"])) { return []; } const path = ctx.path || []; @@ -819,7 +819,7 @@ const ruleset = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], + given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined && @['x-ms-mutability'].length > 0)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/generated/spectral/az-dataplane.js b/packages/rulesets/generated/spectral/az-dataplane.js index 9b9ec53a8..345da4610 100644 --- a/packages/rulesets/generated/spectral/az-dataplane.js +++ b/packages/rulesets/generated/spectral/az-dataplane.js @@ -141,7 +141,7 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (!Array.isArray(prop["x-ms-mutability"]) || prop["x-ms-mutability"].length === 0) { + if (!Array.isArray(prop["x-ms-mutability"])) { return []; } const path = ctx.path || []; @@ -849,7 +849,7 @@ const ruleset$1 = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], + given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined && @['x-ms-mutability'].length > 0)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/src/spectral/az-common.ts b/packages/rulesets/src/spectral/az-common.ts index 26a97f2dc..529b9b1e6 100644 --- a/packages/rulesets/src/spectral/az-common.ts +++ b/packages/rulesets/src/spectral/az-common.ts @@ -263,7 +263,7 @@ const ruleset: any = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], + given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined && @['x-ms-mutability'].length > 0)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts index 593b38442..096d0b92f 100644 --- a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts +++ b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts @@ -4,9 +4,12 @@ export const mutabilityWithReadOnly = (prop: any, _opts: any, ctx: any) => { if (prop === null || typeof prop !== "object") { return []; } - // The given clause filters for readOnly !== undefined and x-ms-mutability !== undefined - // We need to check if x-ms-mutability is an array and not empty - if (!Array.isArray(prop["x-ms-mutability"]) || prop["x-ms-mutability"].length === 0) { + // The given clause filters for: + // - readOnly !== undefined + // - x-ms-mutability !== undefined + // - x-ms-mutability.length > 0 + // We still check Array.isArray as a defensive measure for type safety + if (!Array.isArray(prop["x-ms-mutability"])) { return []; } const path = ctx.path || []; From b71fdb656d20c0493a54661682c0152ea645d917 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 02:16:45 +0000 Subject: [PATCH 06/16] Remove Array.isArray check that changed behavior - keep performance optimization only Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> --- .../rulesets/generated/spectral/az-arm.js | 3 -- .../rulesets/generated/spectral/az-common.js | 3 -- .../generated/spectral/az-dataplane.js | 3 -- .../Extensions/mutability-with-read-only.ts | 4 -- .../test/mutability-with-read-only.test.ts | 37 ------------------- 5 files changed, 50 deletions(-) diff --git a/packages/rulesets/generated/spectral/az-arm.js b/packages/rulesets/generated/spectral/az-arm.js index c35a5d5e4..ce14c2fee 100644 --- a/packages/rulesets/generated/spectral/az-arm.js +++ b/packages/rulesets/generated/spectral/az-arm.js @@ -144,9 +144,6 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (!Array.isArray(prop["x-ms-mutability"])) { - return []; - } const path = ctx.path || []; const errors = []; let hasErrors = false; diff --git a/packages/rulesets/generated/spectral/az-common.js b/packages/rulesets/generated/spectral/az-common.js index 762259a0f..c80bca70a 100644 --- a/packages/rulesets/generated/spectral/az-common.js +++ b/packages/rulesets/generated/spectral/az-common.js @@ -141,9 +141,6 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (!Array.isArray(prop["x-ms-mutability"])) { - return []; - } const path = ctx.path || []; const errors = []; let hasErrors = false; diff --git a/packages/rulesets/generated/spectral/az-dataplane.js b/packages/rulesets/generated/spectral/az-dataplane.js index 345da4610..e5a053a8b 100644 --- a/packages/rulesets/generated/spectral/az-dataplane.js +++ b/packages/rulesets/generated/spectral/az-dataplane.js @@ -141,9 +141,6 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } - if (!Array.isArray(prop["x-ms-mutability"])) { - return []; - } const path = ctx.path || []; const errors = []; let hasErrors = false; diff --git a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts index 096d0b92f..2a21632ae 100644 --- a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts +++ b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts @@ -8,10 +8,6 @@ export const mutabilityWithReadOnly = (prop: any, _opts: any, ctx: any) => { // - readOnly !== undefined // - x-ms-mutability !== undefined // - x-ms-mutability.length > 0 - // We still check Array.isArray as a defensive measure for type safety - if (!Array.isArray(prop["x-ms-mutability"])) { - return []; - } const path = ctx.path || []; const errors: any = []; let hasErrors = false; diff --git a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts index 7c3a03f7f..af5719295 100644 --- a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts +++ b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts @@ -130,40 +130,3 @@ test("MutabilityWithReadOnly should ignore empty x-ms-mutability arrays", () => expect(results.length).toBe(0); }); }); - -test("MutabilityWithReadOnly should ignore non-array x-ms-mutability values", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/LroStatusCodeSchema", - }, - }, - }, - }, - }, - }, - definitions: { - LroStatusCodeSchema: { - type: "object", - properties: { - name: { - type: "string", - readOnly: true, - "x-ms-mutability": "read", - }, - }, - }, - }, - }; - return linter.run(myOpenApiDocument).then((results) => { - // Non-array x-ms-mutability values should be ignored (no errors) - expect(results.length).toBe(0); - }); -}); From aaf70034777843ac98e497a8e01565c72ea2aa58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 02:36:41 +0000 Subject: [PATCH 07/16] Simplify to minimal change: only filter readOnly and x-ms-mutability in given clause Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> --- packages/rulesets/generated/spectral/az-arm.js | 5 ++++- packages/rulesets/generated/spectral/az-common.js | 5 ++++- packages/rulesets/generated/spectral/az-dataplane.js | 5 ++++- packages/rulesets/src/spectral/az-common.ts | 2 +- .../functions/Extensions/mutability-with-read-only.ts | 8 ++++---- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/rulesets/generated/spectral/az-arm.js b/packages/rulesets/generated/spectral/az-arm.js index ce14c2fee..bb2146d44 100644 --- a/packages/rulesets/generated/spectral/az-arm.js +++ b/packages/rulesets/generated/spectral/az-arm.js @@ -144,6 +144,9 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } + if (prop["x-ms-mutability"].length === 0) { + return []; + } const path = ctx.path || []; const errors = []; let hasErrors = false; @@ -1062,7 +1065,7 @@ const ruleset$1 = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined && @['x-ms-mutability'].length > 0)]"], + given: ["$[paths,'x-ms-paths']..*[?(@.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/generated/spectral/az-common.js b/packages/rulesets/generated/spectral/az-common.js index c80bca70a..8626815ea 100644 --- a/packages/rulesets/generated/spectral/az-common.js +++ b/packages/rulesets/generated/spectral/az-common.js @@ -141,6 +141,9 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } + if (prop["x-ms-mutability"].length === 0) { + return []; + } const path = ctx.path || []; const errors = []; let hasErrors = false; @@ -816,7 +819,7 @@ const ruleset = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined && @['x-ms-mutability'].length > 0)]"], + given: ["$[paths,'x-ms-paths']..*[?(@.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/generated/spectral/az-dataplane.js b/packages/rulesets/generated/spectral/az-dataplane.js index e5a053a8b..9052b283a 100644 --- a/packages/rulesets/generated/spectral/az-dataplane.js +++ b/packages/rulesets/generated/spectral/az-dataplane.js @@ -141,6 +141,9 @@ const mutabilityWithReadOnly = (prop, _opts, ctx) => { if (prop === null || typeof prop !== "object") { return []; } + if (prop["x-ms-mutability"].length === 0) { + return []; + } const path = ctx.path || []; const errors = []; let hasErrors = false; @@ -846,7 +849,7 @@ const ruleset$1 = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined && @['x-ms-mutability'].length > 0)]"], + given: ["$[paths,'x-ms-paths']..*[?(@.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/src/spectral/az-common.ts b/packages/rulesets/src/spectral/az-common.ts index 529b9b1e6..605e344cc 100644 --- a/packages/rulesets/src/spectral/az-common.ts +++ b/packages/rulesets/src/spectral/az-common.ts @@ -263,7 +263,7 @@ const ruleset: any = { severity: "error", resolved: true, formats: [oas2], - given: ["$[paths,'x-ms-paths']..*[?(@ != null && @.readOnly !== undefined && @['x-ms-mutability'] !== undefined && @['x-ms-mutability'].length > 0)]"], + given: ["$[paths,'x-ms-paths']..*[?(@.readOnly !== undefined && @['x-ms-mutability'] !== undefined)]"], then: { function: mutabilityWithReadOnly, }, diff --git a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts index 2a21632ae..e1ad575e7 100644 --- a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts +++ b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts @@ -4,10 +4,10 @@ export const mutabilityWithReadOnly = (prop: any, _opts: any, ctx: any) => { if (prop === null || typeof prop !== "object") { return []; } - // The given clause filters for: - // - readOnly !== undefined - // - x-ms-mutability !== undefined - // - x-ms-mutability.length > 0 + // The given clause filters for readOnly !== undefined and x-ms-mutability !== undefined + if (prop["x-ms-mutability"].length === 0) { + return []; + } const path = ctx.path || []; const errors: any = []; let hasErrors = false; From ab900ea943e60234bf20199392b42fb1c6b91a02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 02:50:24 +0000 Subject: [PATCH 08/16] Add comprehensive tests for all readOnly and x-ms-mutability combinations Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> --- .../test/mutability-with-read-only.test.ts | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts index af5719295..f986ff37e 100644 --- a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts +++ b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts @@ -130,3 +130,183 @@ test("MutabilityWithReadOnly should ignore empty x-ms-mutability arrays", () => expect(results.length).toBe(0); }); }); + +test("MutabilityWithReadOnly: readOnly true with valid combinations", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/Schema", + }, + }, + }, + }, + }, + }, + definitions: { + Schema: { + type: "object", + properties: { + validProp: { + type: "string", + readOnly: true, + "x-ms-mutability": ["read"], + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(0); + }); +}); + +test("MutabilityWithReadOnly: readOnly true with invalid combinations", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/Schema", + }, + }, + }, + }, + }, + }, + definitions: { + Schema: { + type: "object", + properties: { + invalidUpdate: { + type: "string", + readOnly: true, + "x-ms-mutability": ["update"], + }, + invalidCreate: { + type: "string", + readOnly: true, + "x-ms-mutability": ["create"], + }, + invalidReadCreate: { + type: "string", + readOnly: true, + "x-ms-mutability": ["read", "create"], + }, + invalidAll: { + type: "string", + readOnly: true, + "x-ms-mutability": ["read", "create", "update"], + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(4); + // Check that all errors are for readOnly true violations + results.forEach((result) => { + expect(result.message).toContain('When property is modeled as "readOnly": true'); + }); + }); +}); + +test("MutabilityWithReadOnly: readOnly false with valid combinations", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/Schema", + }, + }, + }, + }, + }, + }, + definitions: { + Schema: { + type: "object", + properties: { + validReadCreate: { + type: "string", + readOnly: false, + "x-ms-mutability": ["read", "create"], + }, + validUpdate: { + type: "string", + readOnly: false, + "x-ms-mutability": ["update"], + }, + validCreate: { + type: "string", + readOnly: false, + "x-ms-mutability": ["create"], + }, + validAll: { + type: "string", + readOnly: false, + "x-ms-mutability": ["read", "create", "update"], + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(0); + }); +}); + +test("MutabilityWithReadOnly: readOnly false with only read is invalid", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/Schema", + }, + }, + }, + }, + }, + }, + definitions: { + Schema: { + type: "object", + properties: { + invalidReadOnly: { + type: "string", + readOnly: false, + "x-ms-mutability": ["read"], + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + expect(results.length).toBe(1); + expect(results[0].message).toContain('When property is modeled as "readOnly": false'); + expect(results[0].message).toContain("Extension contains invalid values: 'read'"); + }); +}); From 410cf0973521a73040aab8e60bd03001e6985bb4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:25:56 +0000 Subject: [PATCH 09/16] Add tests for properties with omitted readOnly or x-ms-mutability fields Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> --- .../test/mutability-with-read-only.test.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts index f986ff37e..535ed6878 100644 --- a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts +++ b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts @@ -310,3 +310,121 @@ test("MutabilityWithReadOnly: readOnly false with only read is invalid", () => { expect(results[0].message).toContain("Extension contains invalid values: 'read'"); }); }); + +test("MutabilityWithReadOnly: should ignore properties with only readOnly (no x-ms-mutability)", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/Schema", + }, + }, + }, + }, + }, + }, + definitions: { + Schema: { + type: "object", + properties: { + propWithOnlyReadOnly: { + type: "string", + readOnly: true, + }, + anotherPropWithOnlyReadOnly: { + type: "string", + readOnly: false, + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + // Properties without x-ms-mutability should be ignored by the given clause + expect(results.length).toBe(0); + }); +}); + +test("MutabilityWithReadOnly: should ignore properties with only x-ms-mutability (no readOnly)", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/Schema", + }, + }, + }, + }, + }, + }, + definitions: { + Schema: { + type: "object", + properties: { + propWithOnlyMutability: { + type: "string", + "x-ms-mutability": ["read"], + }, + anotherPropWithOnlyMutability: { + type: "string", + "x-ms-mutability": ["read", "create", "update"], + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + // Properties without readOnly should be ignored by the given clause + expect(results.length).toBe(0); + }); +}); + +test("MutabilityWithReadOnly: should ignore properties with neither readOnly nor x-ms-mutability", () => { + const myOpenApiDocument = { + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/Schema", + }, + }, + }, + }, + }, + }, + definitions: { + Schema: { + type: "object", + properties: { + normalProperty: { + type: "string", + }, + anotherNormalProperty: { + type: "integer", + }, + }, + }, + }, + }; + return linter.run(myOpenApiDocument).then((results) => { + // Properties without both fields should be ignored by the given clause + expect(results.length).toBe(0); + }); +}); From 7d055aa1ec27aa62fb5a1de0e046444efe3a771f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:39:58 +0000 Subject: [PATCH 10/16] Refactor tests to reduce redundancy using helper function and consolidated test cases Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> --- .../test/mutability-with-read-only.test.ts | 482 ++++-------------- 1 file changed, 94 insertions(+), 388 deletions(-) diff --git a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts index 535ed6878..4e4340637 100644 --- a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts +++ b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts @@ -8,423 +8,129 @@ beforeAll(async () => { return linter; }); -test("MutabilityWithReadOnly should find errors", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/LroStatusCodeSchema", - }, +// Helper function to create OpenAPI document with properties +const createOpenApiDoc = (properties: any) => ({ + swagger: "2.0", + paths: { + "/api/Paths": { + put: { + operationId: "Path_Create", + responses: { + 200: { + description: "Success", + schema: { + $ref: "#/definitions/Schema", }, }, }, }, }, - definitions: { - LroStatusCodeSchema: { - type: "object", - properties: { - name: { - type: "string", - readOnly: true, - "x-ms-mutability": ["read", "update"], - }, - length: { - type: "string", - readOnly: false, - "x-ms-mutability": ["read"], - }, - }, - }, + }, + definitions: { + Schema: { + type: "object", + properties, }, - }; - return linter.run(myOpenApiDocument).then((results) => { - expect(results.length).toBe(2); - expect(results[0].message).toBe(`When property is modeled as "readOnly": true then x-ms-mutability extension can only have "read" value. When property is modeled as "readOnly": false then applying x-ms-mutability extension with only "read" value is not allowed. Extension contains invalid values: 'read'.`); - expect(results[0].path.join(".")).toBe("paths./api/Paths.put.responses.200.schema.properties.length"); - expect(results[1].message).toBe(`When property is modeled as "readOnly": true then x-ms-mutability extension can only have "read" value. When property is modeled as "readOnly": false then applying x-ms-mutability extension with only "read" value is not allowed. Extension contains invalid values: 'read, update'.`); - expect(results[1].path.join(".")).toBe("paths./api/Paths.put.responses.200.schema.properties.name"); - }); + }, }); -test("MutabilityWithReadOnly should find no errors", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/LroStatusCodeSchema", - }, - }, - }, - }, - }, - }, - definitions: { - LroStatusCodeSchema: { - type: "object", - properties: { - name: { - type: "string", - readOnly: true, - "x-ms-mutability": ["read"], - }, - length: { - type: "string", - readOnly: false, - "x-ms-mutability": ["read", "update"], - }, - }, - }, +test("MutabilityWithReadOnly: valid combinations", () => { + const myOpenApiDocument = createOpenApiDoc({ + readOnlyTrueValid: { + type: "string", + readOnly: true, + "x-ms-mutability": ["read"], + }, + readOnlyFalseValid1: { + type: "string", + readOnly: false, + "x-ms-mutability": ["read", "create"], + }, + readOnlyFalseValid2: { + type: "string", + readOnly: false, + "x-ms-mutability": ["update"], + }, + readOnlyFalseValid3: { + type: "string", + readOnly: false, + "x-ms-mutability": ["create"], + }, + readOnlyFalseValid4: { + type: "string", + readOnly: false, + "x-ms-mutability": ["read", "create", "update"], }, - }; - return linter.run(myOpenApiDocument).then((results) => { - expect(results.length).toBe(0); }); -}); - -test("MutabilityWithReadOnly should ignore empty x-ms-mutability arrays", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/LroStatusCodeSchema", - }, - }, - }, - }, - }, - }, - definitions: { - LroStatusCodeSchema: { - type: "object", - properties: { - name: { - type: "string", - readOnly: true, - "x-ms-mutability": [], - }, - }, - }, - }, - }; return linter.run(myOpenApiDocument).then((results) => { - // Empty x-ms-mutability arrays should be ignored (no errors) expect(results.length).toBe(0); }); }); -test("MutabilityWithReadOnly: readOnly true with valid combinations", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/Schema", - }, - }, - }, - }, - }, - }, - definitions: { - Schema: { - type: "object", - properties: { - validProp: { - type: "string", - readOnly: true, - "x-ms-mutability": ["read"], - }, - }, - }, +test("MutabilityWithReadOnly: invalid combinations", () => { + const myOpenApiDocument = createOpenApiDoc({ + readOnlyTrueInvalid1: { + type: "string", + readOnly: true, + "x-ms-mutability": ["read", "update"], + }, + readOnlyTrueInvalid2: { + type: "string", + readOnly: true, + "x-ms-mutability": ["update"], + }, + readOnlyTrueInvalid3: { + type: "string", + readOnly: true, + "x-ms-mutability": ["create"], + }, + readOnlyTrueInvalid4: { + type: "string", + readOnly: true, + "x-ms-mutability": ["read", "create"], + }, + readOnlyTrueInvalid5: { + type: "string", + readOnly: true, + "x-ms-mutability": ["read", "create", "update"], + }, + readOnlyFalseInvalid: { + type: "string", + readOnly: false, + "x-ms-mutability": ["read"], }, - }; - return linter.run(myOpenApiDocument).then((results) => { - expect(results.length).toBe(0); }); -}); - -test("MutabilityWithReadOnly: readOnly true with invalid combinations", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/Schema", - }, - }, - }, - }, - }, - }, - definitions: { - Schema: { - type: "object", - properties: { - invalidUpdate: { - type: "string", - readOnly: true, - "x-ms-mutability": ["update"], - }, - invalidCreate: { - type: "string", - readOnly: true, - "x-ms-mutability": ["create"], - }, - invalidReadCreate: { - type: "string", - readOnly: true, - "x-ms-mutability": ["read", "create"], - }, - invalidAll: { - type: "string", - readOnly: true, - "x-ms-mutability": ["read", "create", "update"], - }, - }, - }, - }, - }; return linter.run(myOpenApiDocument).then((results) => { - expect(results.length).toBe(4); - // Check that all errors are for readOnly true violations + // 5 invalid readOnly: true + 1 invalid readOnly: false = 6 total errors + expect(results.length).toBe(6); + // Verify all are error messages (not just checking counts since all errors have same message format) results.forEach((result) => { - expect(result.message).toContain('When property is modeled as "readOnly": true'); + expect(result.message).toContain("Extension contains invalid values:"); }); }); }); -test("MutabilityWithReadOnly: readOnly false with valid combinations", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/Schema", - }, - }, - }, - }, - }, +test("MutabilityWithReadOnly: properties ignored by given clause", () => { + const myOpenApiDocument = createOpenApiDoc({ + emptyMutability: { + type: "string", + readOnly: true, + "x-ms-mutability": [], }, - definitions: { - Schema: { - type: "object", - properties: { - validReadCreate: { - type: "string", - readOnly: false, - "x-ms-mutability": ["read", "create"], - }, - validUpdate: { - type: "string", - readOnly: false, - "x-ms-mutability": ["update"], - }, - validCreate: { - type: "string", - readOnly: false, - "x-ms-mutability": ["create"], - }, - validAll: { - type: "string", - readOnly: false, - "x-ms-mutability": ["read", "create", "update"], - }, - }, - }, - }, - }; - return linter.run(myOpenApiDocument).then((results) => { - expect(results.length).toBe(0); - }); -}); - -test("MutabilityWithReadOnly: readOnly false with only read is invalid", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/Schema", - }, - }, - }, - }, - }, - }, - definitions: { - Schema: { - type: "object", - properties: { - invalidReadOnly: { - type: "string", - readOnly: false, - "x-ms-mutability": ["read"], - }, - }, - }, - }, - }; - return linter.run(myOpenApiDocument).then((results) => { - expect(results.length).toBe(1); - expect(results[0].message).toContain('When property is modeled as "readOnly": false'); - expect(results[0].message).toContain("Extension contains invalid values: 'read'"); - }); -}); - -test("MutabilityWithReadOnly: should ignore properties with only readOnly (no x-ms-mutability)", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/Schema", - }, - }, - }, - }, - }, - }, - definitions: { - Schema: { - type: "object", - properties: { - propWithOnlyReadOnly: { - type: "string", - readOnly: true, - }, - anotherPropWithOnlyReadOnly: { - type: "string", - readOnly: false, - }, - }, - }, + onlyReadOnly: { + type: "string", + readOnly: true, }, - }; - return linter.run(myOpenApiDocument).then((results) => { - // Properties without x-ms-mutability should be ignored by the given clause - expect(results.length).toBe(0); - }); -}); - -test("MutabilityWithReadOnly: should ignore properties with only x-ms-mutability (no readOnly)", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/Schema", - }, - }, - }, - }, - }, + onlyMutability: { + type: "string", + "x-ms-mutability": ["read"], }, - definitions: { - Schema: { - type: "object", - properties: { - propWithOnlyMutability: { - type: "string", - "x-ms-mutability": ["read"], - }, - anotherPropWithOnlyMutability: { - type: "string", - "x-ms-mutability": ["read", "create", "update"], - }, - }, - }, + neitherField: { + type: "string", }, - }; - return linter.run(myOpenApiDocument).then((results) => { - // Properties without readOnly should be ignored by the given clause - expect(results.length).toBe(0); }); -}); - -test("MutabilityWithReadOnly: should ignore properties with neither readOnly nor x-ms-mutability", () => { - const myOpenApiDocument = { - swagger: "2.0", - paths: { - "/api/Paths": { - put: { - operationId: "Path_Create", - responses: { - 200: { - description: "Success", - schema: { - $ref: "#/definitions/Schema", - }, - }, - }, - }, - }, - }, - definitions: { - Schema: { - type: "object", - properties: { - normalProperty: { - type: "string", - }, - anotherNormalProperty: { - type: "integer", - }, - }, - }, - }, - }; return linter.run(myOpenApiDocument).then((results) => { - // Properties without both fields should be ignored by the given clause + // All properties should be ignored: empty array, missing field, or both missing expect(results.length).toBe(0); }); }); From a61fd2fc8fde5b461c703b26eb8be4c3585e1e9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:46:01 +0000 Subject: [PATCH 11/16] Change helper function type from any to unknown for better type safety Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> --- .../src/spectral/test/mutability-with-read-only.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts index 4e4340637..9ccaced3f 100644 --- a/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts +++ b/packages/rulesets/src/spectral/test/mutability-with-read-only.test.ts @@ -9,7 +9,7 @@ beforeAll(async () => { }); // Helper function to create OpenAPI document with properties -const createOpenApiDoc = (properties: any) => ({ +const createOpenApiDoc = (properties: unknown) => ({ swagger: "2.0", paths: { "/api/Paths": { From 3d9eca9466e8ebcf9680b03b215fbdee51ab4ae8 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 28 Jan 2026 21:53:57 -0800 Subject: [PATCH 12/16] improve comment --- .../spectral/functions/Extensions/mutability-with-read-only.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts index e1ad575e7..c06242f9a 100644 --- a/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts +++ b/packages/rulesets/src/spectral/functions/Extensions/mutability-with-read-only.ts @@ -4,7 +4,7 @@ export const mutabilityWithReadOnly = (prop: any, _opts: any, ctx: any) => { if (prop === null || typeof prop !== "object") { return []; } - // The given clause filters for readOnly !== undefined and x-ms-mutability !== undefined + // The "given" clause already filters for "readOnly !== undefined" and "x-ms-mutability !== undefined" if (prop["x-ms-mutability"].length === 0) { return []; } From 7b0dfe49145ec2f7a3c687fbe0eb98e7b7adc758 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:00:20 +0000 Subject: [PATCH 13/16] Add Rush change file for MutabilityWithReadOnly performance fix Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> --- ...lity-readonly-performance-fix_2026-01-29-05-58.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@microsoft.azure/openapi-validator-rulesets/copilot-mutability-readonly-performance-fix_2026-01-29-05-58.json diff --git a/common/changes/@microsoft.azure/openapi-validator-rulesets/copilot-mutability-readonly-performance-fix_2026-01-29-05-58.json b/common/changes/@microsoft.azure/openapi-validator-rulesets/copilot-mutability-readonly-performance-fix_2026-01-29-05-58.json new file mode 100644 index 000000000..9990fe09c --- /dev/null +++ b/common/changes/@microsoft.azure/openapi-validator-rulesets/copilot-mutability-readonly-performance-fix_2026-01-29-05-58.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft.azure/openapi-validator-rulesets", + "comment": "Optimize MutabilityWithReadOnly rule to prevent out-of-memory issues by filtering in JSONPath given clause (fixes #812)", + "type": "patch" + } + ], + "packageName": "@microsoft.azure/openapi-validator-rulesets" +} From 7d9c1ff3b6f375e1a4d5ab8b3eb562ba211220d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:44:17 +0000 Subject: [PATCH 14/16] Update package.json to 2.2.1 and add CHANGELOG entry Co-authored-by: mikeharder <9459391+mikeharder@users.noreply.github.com> --- ...lity-readonly-performance-fix_2026-01-29-05-58.json | 10 ---------- packages/rulesets/CHANGELOG.md | 6 ++++++ packages/rulesets/package.json | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) delete mode 100644 common/changes/@microsoft.azure/openapi-validator-rulesets/copilot-mutability-readonly-performance-fix_2026-01-29-05-58.json diff --git a/common/changes/@microsoft.azure/openapi-validator-rulesets/copilot-mutability-readonly-performance-fix_2026-01-29-05-58.json b/common/changes/@microsoft.azure/openapi-validator-rulesets/copilot-mutability-readonly-performance-fix_2026-01-29-05-58.json deleted file mode 100644 index 9990fe09c..000000000 --- a/common/changes/@microsoft.azure/openapi-validator-rulesets/copilot-mutability-readonly-performance-fix_2026-01-29-05-58.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@microsoft.azure/openapi-validator-rulesets", - "comment": "Optimize MutabilityWithReadOnly rule to prevent out-of-memory issues by filtering in JSONPath given clause (fixes #812)", - "type": "patch" - } - ], - "packageName": "@microsoft.azure/openapi-validator-rulesets" -} diff --git a/packages/rulesets/CHANGELOG.md b/packages/rulesets/CHANGELOG.md index e62e86884..76b4c448c 100644 --- a/packages/rulesets/CHANGELOG.md +++ b/packages/rulesets/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log - @microsoft.azure/openapi-validator-rulesets +## 2.2.1 + +### Patches + +- [MutabilityWithReadOnly] Optimize rule to prevent out-of-memory issues by filtering in JSONPath given clause (fixes #812) + ## 2.2.0 ### Minor changes diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index 85c94750b..d176766ca 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft.azure/openapi-validator-rulesets", - "version": "2.2.0", + "version": "2.2.1", "description": "Azure OpenAPI Validator", "main": "dist/index.js", "scripts": { From 573ada9ae30e8c1cf4a8203b4694f11a411518f5 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 28 Jan 2026 22:47:41 -0800 Subject: [PATCH 15/16] improve changelog --- packages/rulesets/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rulesets/CHANGELOG.md b/packages/rulesets/CHANGELOG.md index 76b4c448c..9aa55a6bd 100644 --- a/packages/rulesets/CHANGELOG.md +++ b/packages/rulesets/CHANGELOG.md @@ -4,7 +4,7 @@ ### Patches -- [MutabilityWithReadOnly] Optimize rule to prevent out-of-memory issues by filtering in JSONPath given clause (fixes #812) +- [MutabilityWithReadOnly] Optimize rule to prevent out-of-memory issues on large specs ## 2.2.0 From c2ad655e55a531422ac25ebf44944964b6e6f058 Mon Sep 17 00:00:00 2001 From: Mike Harder Date: Wed, 28 Jan 2026 22:48:58 -0800 Subject: [PATCH 16/16] Apply suggestion from @mikeharder --- packages/rulesets/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rulesets/CHANGELOG.md b/packages/rulesets/CHANGELOG.md index 9aa55a6bd..6413c3722 100644 --- a/packages/rulesets/CHANGELOG.md +++ b/packages/rulesets/CHANGELOG.md @@ -4,7 +4,7 @@ ### Patches -- [MutabilityWithReadOnly] Optimize rule to prevent out-of-memory issues on large specs +- [MutabilityWithReadOnly] Optimize rule to prevent excessive memory usage on large specs ## 2.2.0