Repository: stoplightio/spectral (ts)

Issue:
Support more than 2 logicals in XOR core function
**Describe the bug**
In Spectral5.9.2, the core XOR function could accept more than 2 items in its properties' list. In Spectral6+, the core XOR function only appears to support two logical.

**To Reproduce**
Create a ruleset that leverages the core XOR function:

  each-component-property-must-contain-only-one:
    given: $.components.*.*.properties.*
    recommended: true
    severity: warn
    then:
      function: xor
      functionOptions:
        properties:
          - type
          - oneOf
          - $ref
          
This rule running on 5.9.2 will work with no issue
This rule running on 6+ will result in the following error: 
Error #1: "xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]

**Expected behavior**
The XOR function will behave in the way that it always has, allowing more than just 2 logicals.

**Additional context**
I understand that from an electrical perspective, an XOR gate only takes two inputs, but XOR functionality can support an unlimited number of inputs in practical use. If this was an intended behavior change, then so be it, it just seems like an odd decision to make as it breaks compatibility and the docs page still lists the function as supporting a list of logicals.

Judging by commit histories, it looks like the xor function was completely redesigned in 6.0.0alpha1, this redesign seems to have limited it to two logicals


Pull request:
feat(rulesets): add multiple xor
Closes #2396 

**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [x] Yes
- [ ] No

> This PR enhances the XOR core function to support scenarios involving more than two properties, allowing for expanded logical evaluations across multiple attributes.



Patch:
diff --git a/packages/functions/src/optionSchemas.ts b/packages/functions/src/optionSchemas.ts
index f9a93c15..7a3a2741 100644
--- a/packages/functions/src/optionSchemas.ts
+++ b/packages/functions/src/optionSchemas.ts
@@ -207,8 +207,7 @@ export const optionSchemas: Record<string, CustomFunctionOptionsSchema> = {
           type: 'string',
         },
         minItems: 2,
-        maxItems: 2,
-        errorMessage: `"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]`,
+        errorMessage: `"xor" and its "properties" option require at least 2-item tuples, i.e. ["id", "name"]`,
         description: 'The properties to check.',
       },
     },
diff --git a/packages/functions/src/xor.ts b/packages/functions/src/xor.ts
index e9a12225..5d5d68b7 100644
--- a/packages/functions/src/xor.ts
+++ b/packages/functions/src/xor.ts
@@ -16,16 +16,20 @@ export default createRulesetFunction<Record<string, unknown>, Options>(
     options: optionSchemas.xor,
   },
   function xor(targetVal, { properties }) {
-    if (properties.length !== 2) return;
-
     const results: IFunctionResult[] = [];
 
-    const intersection = Object.keys(targetVal).filter(value => -1 !== properties.indexOf(value));
+    const intersection = Object.keys(targetVal).filter(key => properties.includes(key));
+
     if (intersection.length !== 1) {
+      const formattedProperties = properties.map(prop => printValue(prop));
+
+      const lastProperty = formattedProperties.pop();
+      let message = formattedProperties.join(', ') + (lastProperty != undefined ? ` and ${lastProperty}` : '');
+
+      message += ' must not be both defined or both undefined';
+
       results.push({
-        message: `${printValue(properties[0])} and ${printValue(
-          properties[1],
-        )} must not be both defined or both undefined`,
+        message,
       });
     }
 


Test patch:
diff --git a/packages/functions/src/__tests__/xor.test.ts b/packages/functions/src/__tests__/xor.test.ts
index 6ea29e23..49d328f3 100644
--- a/packages/functions/src/__tests__/xor.test.ts
+++ b/packages/functions/src/__tests__/xor.test.ts
@@ -26,6 +26,24 @@ describe('Core Functions / Xor', () => {
     ]);
   });
 
+  it('given multiple properties that do not match, should return an error message', async () => {
+    expect(
+      await runXor(
+        {
+          version: '1.0.0',
+          title: 'Swagger Petstore',
+          termsOfService: 'http://swagger.io/terms/',
+        },
+        { properties: ['yada-yada', 'whatever', 'foo'] },
+      ),
+    ).toEqual([
+      {
+        message: '"yada-yada", "whatever" and "foo" must not be both defined or both undefined',
+        path: [],
+      },
+    ]);
+  });
+
   it('given both properties, should return an error message', async () => {
     expect(
       await runXor(
@@ -99,22 +117,12 @@ describe('Core Functions / Xor', () => {
           ]),
         ],
       ],
-      [
-        { properties: ['foo', 'bar', 'baz'] },
-        [
-          new RulesetValidationError(
-            'invalid-function-options',
-            '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]',
-            ['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
-          ),
-        ],
-      ],
       [
         { properties: ['foo', {}] },
         [
           new RulesetValidationError(
             'invalid-function-options',
-            '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]',
+            '"xor" and its "properties" option require at least 2-item tuples, i.e. ["id", "name"]',
             ['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
           ),
         ],
@@ -124,7 +132,7 @@ describe('Core Functions / Xor', () => {
         [
           new RulesetValidationError(
             'invalid-function-options',
-            '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]',
+            '"xor" and its "properties" option require at least 2-item tuples, i.e. ["id", "name"]',
             ['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
           ),
         ],
@@ -134,7 +142,7 @@ describe('Core Functions / Xor', () => {
         [
           new RulesetValidationError(
             'invalid-function-options',
-            '"xor" and its "properties" option support 2-item tuples, i.e. ["id", "name"]',
+            '"xor" and its "properties" option require at least 2-item tuples, i.e. ["id", "name"]',
             ['rules', 'my-rule', 'then', 'functionOptions', 'properties'],
           ),
         ],