Repository: stoplightio/spectral (ts)

Issue:
Support x-extensions in spectral rulesets
**Describe the bug**
When declaring an anchor in the ruleset.yml, spectral exits with `Error #1: undefined`
In the example below the anchor is `valid-head-pattern`, we would like to avoid a redundant regex declaration.

**To Reproduce**
1. Given this ruleset file:
```yaml
extends: ["spectral:oas"]

x-vars:
  - &valid-header-pattern ^(Accept|Accept-CH|Accept-CH-Lifetime|Accept-Charset|Accept-Encoding)$

rules:

  invalid-response-header:
      description: "Use of headers not listed in NLB API guide is not allowed."
      severity: error
      given: $..responses..headers
      resolved: false
      then:
        field: "@key"
        function: pattern
        functionOptions:
          match: *valid-header-pattern

```

3. Run this CLI command `spectral lint openapi.yml --ruleset ruleset.yml`
Consider `openai.yml` a valid oas file.

4. See error:
```
Error running Spectral!
Use --verbose flag to print the error stack.
Error #1: undefined
```

**Expected behavior**
I would expect that the alias inside the rule is resolved against the corresponding anchor at the beginning of the file.

**Environment (remove any that are not applicable):**
 - Spectral CLI version: [6.6.0]
 - OS: [e.g. macOS 10.15.7] I know 🙄


Pull request description:
feat(core): support x- extensions in the ruleset
Closes #2394 

**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No


Patch:
diff --git a/packages/core/src/ruleset/meta/rule.schema.json b/packages/core/src/ruleset/meta/rule.schema.json
index 48a0edaf..d5c0700b 100644
--- a/packages/core/src/ruleset/meta/rule.schema.json
+++ b/packages/core/src/ruleset/meta/rule.schema.json
@@ -84,6 +84,9 @@
     },
     "required": ["given", "then"],
     "additionalProperties": false,
+    "patternProperties": {
+      "^x-": true
+    },
     "errorMessage": {
       "required": "the rule must have at least \"given\" and \"then\" properties"
     }
diff --git a/packages/core/src/ruleset/meta/ruleset.schema.json b/packages/core/src/ruleset/meta/ruleset.schema.json
index c5965487..6fc6fccd 100644
--- a/packages/core/src/ruleset/meta/ruleset.schema.json
+++ b/packages/core/src/ruleset/meta/ruleset.schema.json
@@ -190,6 +190,9 @@
       }
     }
   },
+  "patternProperties": {
+    "^x-": true
+  },
   "anyOf": [
     {
       "required": ["extends"]


Test patch:
diff --git a/packages/core/src/ruleset/validation/__tests__/validation.test.ts b/packages/core/src/ruleset/validation/__tests__/validation.test.ts
index a78f8eb6..00142766 100644
--- a/packages/core/src/ruleset/validation/__tests__/validation.test.ts
+++ b/packages/core/src/ruleset/validation/__tests__/validation.test.ts
@@ -119,6 +119,18 @@ describe('JS Ruleset Validation', () => {
     ).not.toThrow();
   });
 
+  it('allows x- extensions on a ruleset', () => {
+    expect(
+      assertValidRuleset.bind(null, {
+        rules: {},
+        'x-internal': true,
+        'x-vars': {
+          foo: 'bar',
+        },
+      }),
+    ).not.toThrow();
+  });
+
   it.each(['error', 'warn', 'info', 'hint', 'off'])('recognizes human-readable %s severity', severity => {
     expect(
       assertValidRuleset.bind(null, {
@@ -843,6 +855,26 @@ describe('JS Ruleset Validation', () => {
       );
     });
   });
+
+  it('allows x- extensions on a rule', () => {
+    expect(
+      assertValidRuleset.bind(null, {
+        rules: {
+          rule: {
+            given: '$',
+            then: {
+              field: 'test',
+              function: truthy,
+            },
+            'x-internal': true,
+            'x-vars': {
+              foo: 'bar',
+            },
+          },
+        },
+      }),
+    ).not.toThrow();
+  });
 });
 
 // we only check the most notable differences here, since the rest of the validation process is common to both JS and JSON