Repository: stoplightio/spectral
Pull request #2016 (2022-01-04 12:37:43)

feat(core): support multiple JSONPath expressions for aliases
Related to https://github.com/stoplightio/platform-internal/issues/8141

Needed by https://github.com/stoplightio/spectral/pull/2018
Example usage https://github.com/stoplightio/spectral/blob/a2019f14f7db93d31effd795882c30d060a196fe/packages/rulesets/src/oas/index.ts#L38

I believe that once this one is in, we can consider the aliases frozen and won't need to make any further changes.

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No




Patch:
diff --git a/docs/guides/4-custom-rulesets.md b/docs/guides/4-custom-rulesets.md
index 38aa27c6..60b611ca 100644
--- a/docs/guides/4-custom-rulesets.md
+++ b/docs/guides/4-custom-rulesets.md
@@ -27,11 +27,13 @@ Spectral has [built-in functions](../reference/functions.md) such as `truthy` or
 
 The `given` property is conceptually quite like a selector in CSS, in that it picks the part of the document to apply rules to.
 
-It has a specific syntax known as [JSON Path](https://jsonpath.com/), which if you are familiar with XPath is quite similar. JSON Path is not yet a standard (it [will be](https://tools.ietf.org/html/draft-normington-jsonpath-00) someday), and has a few competing implementations. Spectral uses [jsonpath-plus](https://www.npmjs.com/package/jsonpath-plus) for the implementation, which supports all the main JSON Path functionality and a little bit more, but this syntax may differ slightly from other JSON Path implementations.
+It has a specific syntax known as [JSONPath](https://goessner.net/articles/JsonPath/index.html), which if you are familiar with XPath is quite similar. JSONPath is not yet a standard (it [will be](https://tools.ietf.org/html/draft-normington-jsonpath-00) someday), and has a few competing implementations. Spectral uses [nimma](https://www.npmjs.com/package/nimma) as its main implementation, and sometimes resorts to [jsonpath-plus](https://www.npmjs.com/package/jsonpath-plus) to ensure a good backwards-compatibility.
+Both of them support all the main JSONPath functionality and a little bit more, but this syntax may differ slightly from other JSONPath implementations.
 
-Your `given` value can be a string containing any valid JSON Path Plus expression, or an array of expressions to apply a rule to multiple parts of a document.
+Your `given` value can be a string containing any valid JSONPath expression, or an array of expressions to apply a rule to multiple parts of a document.
+You can also consume your [aliases][#aliases] here if you have some defined.
 
-Use the [JSON Path Online Evaluator](http://jsonpath.com/) to determine what `given` path you want.
+Use the [JSONPath Online Evaluator](http://jsonpath.com/) to determine what `given` path you want.
 
 ### Severity
 
@@ -92,7 +94,7 @@ then:
   function: truthy
 ```
 
-The `field` keyword is optional, and is for applying the function to a specific property in an object. If omitted the function will be applied to the entire target of the `given` JSON Path. The value can also be `@key` to apply the rule to a keys of an object.
+The `field` keyword is optional, and is for applying the function to a specific property in an object. If omitted the function will be applied to the entire target of the `given` JSONPath. The value can also be `@key` to apply the rule to a keys of an object.
 
 ```yaml
 given: "$.responses"
@@ -322,15 +324,11 @@ For now the JSON, YAML, and JS, are all being maintained, and there are no curre
 
 ## Aliases
 
-Targeting certain parts of an OpenAPI spec is powerful but it can become cumbersome to write and repeat complex JSON path expressions across various rules. Define aliases for commonly used JSON paths on a global level which can then be reused across the ruleset.
+Targeting certain parts of an OpenAPI spec is powerful but it can become cumbersome to write and repeat complex JSONPath expressions across various rules.
+Define aliases for commonly used JSONPath expressions on a global level which can then be reused across the ruleset.
 
 Aliases can be defined in an array of key-value pairs at the root level of the ruleset.
-
-```
-aliases: {
-  {Name}: '{JSONPath}'
-}
-```
+It's a superset of `given`, with the notable difference being the possibility to distinguish between different formats.
 
 **Example**
 
@@ -343,7 +341,51 @@ aliases:
   Paths: "$.paths[*]~"
 ```
 
-Rulesets can then reference aliases in the [given](#given) keyword, either in full: `"given": "#Paths"`, or use it as a prefix for further JSON Path syntax, like dot notation: `"given": "#InfoContact.name"`.
+If you deal with a variety of different spec, you may find the above approach insufficient, particularly when the shape of the document is notably different.
+In such a case, you may want to consider using scoped aliases.
+
+```yaml
+aliases:
+  SharedParameterObject:
+    description: an optional property describing the purpose of the alias
+    targets:
+      - formats:
+          - oas2
+        given: $.parameters[*]
+      - formats:
+          - oas3
+        given: $.components.parameters[*]
+```
+
+Now, if you referenced `SharedParameterObject` alias, the chosen path would be determined based on the document you use.
+For instance, if a given document matched OpenAPI 2.x, `$.parameters[*]` would be used as the JSONPath expression.
+
+Having a closer look on the example above, one may notice that it'd be still somewhat complicated to target _all_ Parameter Objects
+that a specific OpenAPI document may contain.
+To make it more feasible and avoid overly complex JSONPath expressions, `given` can be an array.
+
+```yaml
+aliases:
+  PathItemObject: $.paths[*]
+  OperationObject: "#PathItem[get,put,post,delete,options,head,patch,trace]"
+  ParameterObject:
+    description: an optional property describing the purpose of the alias
+    targets:
+      - formats:
+          - oas2
+        given:
+          - "#PathItemObject.parameters[*]"
+          - "#OperationObject.parameters[*]"
+          - $.parameters[*]
+      - formats:
+          - oas3
+        given:
+          - "#PathItemObject.parameters[*]"
+          - "#OperationObject.parameters[*]"
+          - $.components.parameters[*]
+```
+
+Rulesets can then reference aliases in the [given](#given) keyword, either in full: `"given": "#Paths"`, or use it as a prefix for further JSONPath syntax, like dot notation: `"given": "#ParameterObject.name"`.
 
 > This will be followed by our core rulesets providing a common set of aliases for OpenAPI and AsyncAPI so that our users don't have to do the work at all. If you have ideas about what kind of aliases could be useful leave your thoughts [here](https://roadmap.stoplight.io).
 
@@ -354,7 +396,7 @@ Previously Spectral supported exceptions, which were limited in their ability to
 Overrides can be used to:
 
 - Override rulesets to apply on particular files/folders `files: ['schemas/**/*.draft7.json']`
-- Override rulesets to apply on particular JSON Path's `files: ['**#/components/schemas/Item']`
+- Override rulesets to apply on particular JSONPath's `files: ['**#/components/schemas/Item']`
 - Override rulesets to apply on particular formats `formats: [jsonSchemaDraft7]`
 - Override particular rules
 
@@ -377,7 +419,7 @@ overrides:
             type: number
 ```
 
-One can also combine a glob for a filepath with a JSON Path after the anchor, i.e.:
+One can also combine a glob for a filepath with a JSONPath after the anchor, i.e.:
 
 ```yaml
 overrides:
diff --git a/packages/core/src/meta/rule.schema.json b/packages/core/src/meta/rule.schema.json
index 5b40e90e..d49fb758 100644
--- a/packages/core/src/meta/rule.schema.json
+++ b/packages/core/src/meta/rule.schema.json
@@ -38,17 +38,7 @@
           "type": "boolean"
         },
         "given": {
-          "oneOf": [
-            {
-              "type": "string"
-            },
-            {
-              "type": "array",
-              "items": {
-                "type": "string"
-              }
-            }
-          ]
+          "$ref": "shared#given"
         },
         "resolved": {
           "type": "boolean"
diff --git a/packages/core/src/meta/ruleset.schema.json b/packages/core/src/meta/ruleset.schema.json
index 8713a652..2ae5dae5 100644
--- a/packages/core/src/meta/ruleset.schema.json
+++ b/packages/core/src/meta/ruleset.schema.json
@@ -172,46 +172,44 @@
         }
       },
       "additionalProperties": {
-        "oneOf": [
-          {
-            "$id": "alias-value",
-            "type": "string",
-            "pattern": "^[$#]",
-            "errorMessage": {
-              "_": "the value of an alias must be a valid JSON Path expression, a reference to the existing Alias optionally paired with a JSON Path expression subset, or contain a valid set of targets",
-              "pattern": "the value of an alias must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset"
-            }
-          },
-          {
-            "type": "object",
-            "properties": {
-              "description": {
-                "type": "string"
-              },
-              "targets": {
-                "type": "array",
-                "minItems": 1,
-                "items": {
-                  "type": "object",
-                  "properties": {
-                    "formats": {
-                      "$ref": "shared#formats"
-                    },
-                    "given": {
-                      "$ref": "alias-value"
-                    }
+        "if": {
+          "type": "object"
+        },
+        "then": {
+          "type": "object",
+          "properties": {
+            "description": {
+              "type": "string"
+            },
+            "targets": {
+              "type": "array",
+              "minItems": 1,
+              "items": {
+                "type": "object",
+                "properties": {
+                  "formats": {
+                    "$ref": "shared#formats"
                   },
-                  "required": ["formats", "given"],
-                  "errorMessage": "a valid target must contain given and non-empty formats"
+                  "given": {
+                    "$ref": "shared#given"
+                  }
                 },
-                "errorMessage": {
-                  "minItems": "targets must have at least a single alias definition"
-                }
+                "required": ["formats", "given"],
+                "errorMessage": "a valid target must contain given and non-empty formats"
+              },
+              "errorMessage": {
+                "minItems": "targets must have at least a single alias definition"
               }
-            },
-            "required": ["targets"]
+            }
+          },
+          "required": ["targets"],
+          "errorMessage": {
+            "required": "targets must be present and have at least a single alias definition"
           }
-        ]
+        },
+        "else": {
+          "$ref": "shared#given"
+        }
       }
     }
   },
diff --git a/packages/core/src/meta/shared.json b/packages/core/src/meta/shared.json
index 8d9d8183..9cacc71e 100644
--- a/packages/core/src/meta/shared.json
+++ b/packages/core/src/meta/shared.json
@@ -32,6 +32,31 @@
         }
       ],
       "errorMessage": "the value has to be one of: 0, 1, 2, 3 or \"error\", \"warn\", \"info\", \"hint\", \"off\""
+    },
+    "Given": {
+      "$anchor": "given",
+      "if": {
+        "type": "array"
+      },
+      "then": {
+        "type": "array",
+        "items": {
+          "$ref": "path-expression"
+        },
+        "minItems": 1,
+        "errorMessage": {
+          "minItems": "must be a non-empty array of expressions"
+        }
+      },
+      "else": {
+        "$ref": "path-expression"
+      }
+    },
+    "PathExpression": {
+      "$id": "path-expression",
+      "type": "string",
+      "pattern": "^[$#]",
+      "errorMessage": "must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset"
     }
   }
 }
diff --git a/packages/core/src/ruleset/mergers/rules.ts b/packages/core/src/ruleset/mergers/rules.ts
index c195e5e2..04396b5b 100644
--- a/packages/core/src/ruleset/mergers/rules.ts
+++ b/packages/core/src/ruleset/mergers/rules.ts
@@ -1,6 +1,6 @@
 import { Optional } from '@stoplight/types';
 import { assertValidRule } from '../validation';
-import { Rule } from '../rule/rule';
+import { Rule } from '../rule';
 import type { Ruleset } from '../ruleset';
 import { FileRuleDefinition } from '../types';
 
diff --git a/packages/core/src/ruleset/rule/rule.ts b/packages/core/src/ruleset/rule.ts
similarity index 68%
rename from packages/core/src/ruleset/rule/rule.ts
rename to packages/core/src/ruleset/rule.ts
index 91564d37..9adb23dd 100644
--- a/packages/core/src/ruleset/rule/rule.ts
+++ b/packages/core/src/ruleset/rule.ts
@@ -1,20 +1,16 @@
 import { isString } from 'lodash';
-import { JsonPath, Optional, DiagnosticSeverity } from '@stoplight/types';
+import { DiagnosticSeverity, JsonPath, Optional } from '@stoplight/types';
 import { dirname, relative } from '@stoplight/path';
 import { pathToPointer } from '@stoplight/json';
 import { printValue } from '@stoplight/spectral-runtime';
 
-import { getDiagnosticSeverity, DEFAULT_SEVERITY_LEVEL } from '../utils/severity';
-import { Ruleset } from '../ruleset';
-import { Format } from '../format';
-import type {
-  HumanReadableDiagnosticSeverity,
-  IRuleThen,
-  RuleDefinition,
-  RulesetScopedAliasDefinition,
-} from '../types';
-import { minimatch } from '../utils/minimatch';
-import { FormatsSet } from '../utils/formatsSet';
+import { DEFAULT_SEVERITY_LEVEL, getDiagnosticSeverity } from './utils/severity';
+import { Ruleset } from './ruleset';
+import { Format } from './format';
+import type { HumanReadableDiagnosticSeverity, IRuleThen, RuleDefinition, RulesetScopedAliasDefinition } from './types';
+import { minimatch } from './utils/minimatch';
+import { FormatsSet } from './utils/formatsSet';
+import { isSimpleAliasDefinition } from './utils/guards';
 
 const ALIAS = /^#([A-Za-z0-9_-]+)/;
 
@@ -139,61 +135,74 @@ export class Rule implements IRule {
     const actualGiven = Array.isArray(given) ? given : [given];
     this.#given = this.owner.hasComplexAliases
       ? actualGiven
-      : actualGiven.map(expr => this.#resolveAlias(expr, null)).filter(isString);
+      : actualGiven.flatMap(expr => this.#resolveAlias(expr, null)).filter(isString);
   }
 
-  public getGivenForFormats(formats: Set<Format<any>> | null): string[] {
-    return this.owner.hasComplexAliases
-      ? this.#given.map(expr => this.#resolveAlias(expr, formats)).filter(isString)
-      : this.#given;
+  public getGivenForFormats(formats: Set<Format> | null): string[] {
+    return this.owner.hasComplexAliases ? this.#given.flatMap(expr => this.#resolveAlias(expr, formats)) : this.#given;
   }
 
-  #resolveAlias(expr: string, formats: Set<Format> | null): string | null {
-    let resolvedExpr = expr;
+  #resolveAlias(expr: string, formats: Set<Format> | null): string[] {
+    const expressions: string[] = [expr];
+    const resolvedExpressions = [];
 
     const stack = new Set<string>();
+    resolve: for (const expression of expressions) {
+      let resolvedExpr = expression;
 
-    while (resolvedExpr.startsWith('#')) {
-      const alias = ALIAS.exec(resolvedExpr)?.[1];
+      while (resolvedExpr.startsWith('#')) {
+        const alias = ALIAS.exec(resolvedExpr)?.[1];
 
-      if (alias === void 0 || alias === null) {
-        throw new ReferenceError(`"${this.name}" rule references an invalid alias`);
-      }
+        if (alias === void 0 || alias === null) {
+          throw new ReferenceError(`"${this.name}" rule references an invalid alias`);
+        }
 
-      if (stack.has(alias)) {
-        const _stack = [...stack, alias];
-        throw new ReferenceError(`Alias "${_stack[0]}" is circular. Resolution stack: ${_stack.join(' -> ')}`);
-      }
+        if (stack.has(alias)) {
+          const _stack = [...stack, alias];
+          throw new ReferenceError(`Alias "${_stack[0]}" is circular. Resolution stack: ${_stack.join(' -> ')}`);
+        }
 
-      stack.add(alias);
+        stack.add(alias);
 
-      if (this.owner.aliases === null || !(alias in this.owner.aliases)) {
-        throw new ReferenceError(`Alias "${alias}" does not exist`);
-      }
+        if (this.owner.aliases === null || !(alias in this.owner.aliases)) {
+          throw new ReferenceError(`Alias "${alias}" does not exist`);
+        }
 
-      const aliasValue = this.owner.aliases[alias];
-      let actualAliasValue;
-      if (typeof aliasValue === 'string') {
-        actualAliasValue = aliasValue;
-      } else {
-        actualAliasValue = this.#resolveAliasForFormats(aliasValue, formats);
-      }
+        const aliasValue = this.owner.aliases[alias];
+        let actualAliasValue;
+        if (isSimpleAliasDefinition(aliasValue)) {
+          actualAliasValue = aliasValue;
+        } else {
+          actualAliasValue = Rule.#resolveAliasForFormats(aliasValue, formats);
+        }
 
-      if (actualAliasValue === null) {
-        return null;
-      }
+        if (actualAliasValue === null) {
+          continue resolve;
+        }
 
-      if (actualAliasValue.length + 1 === expr.length) {
-        resolvedExpr = actualAliasValue;
-      } else {
-        resolvedExpr = actualAliasValue + resolvedExpr.slice(alias.length + 1);
+        if (Array.isArray(actualAliasValue)) {
+          expressions.push(...actualAliasValue.map(item => item + resolvedExpr.slice(alias.length + 1)));
+          continue resolve;
+        }
+
+        if (actualAliasValue.length + 1 === expression.length) {
+          resolvedExpr = actualAliasValue;
+        } else {
+          resolvedExpr = actualAliasValue + resolvedExpr.slice(alias.length + 1);
+        }
       }
+
+      stack.clear();
+      resolvedExpressions.push(resolvedExpr);
     }
 
-    return resolvedExpr;
+    return resolvedExpressions;
   }
 
-  #resolveAliasForFormats({ targets }: RulesetScopedAliasDefinition, formats: Set<Format> | null): string | null {
+  static #resolveAliasForFormats(
+    { targets }: RulesetScopedAliasDefinition,
+    formats: Set<Format> | null,
+  ): string | string[] | null {
     if (formats === null || formats.size === 0) {
       return null;
     }
diff --git a/packages/core/src/ruleset/rule/index.ts b/packages/core/src/ruleset/rule/index.ts
deleted file mode 100644
index 502c8239..00000000
--- a/packages/core/src/ruleset/rule/index.ts
+++ /dev/null
@@ -1,1 +0,0 @@
-export { Rule } from './rule';
diff --git a/packages/core/src/ruleset/ruleset.ts b/packages/core/src/ruleset/ruleset.ts
index 93c0739a..0dbf80c0 100644
--- a/packages/core/src/ruleset/ruleset.ts
+++ b/packages/core/src/ruleset/ruleset.ts
@@ -1,6 +1,6 @@
 import { dirname, relative } from '@stoplight/path';
 import { minimatch } from './utils/minimatch';
-import { Rule, StringifiedRule } from './rule/rule';
+import { Rule, StringifiedRule } from './rule';
 import {
   FileRulesetSeverityDefinition,
   ParserOptions,
@@ -15,6 +15,7 @@ import { mergeRulesets } from './mergers/rulesets';
 import { isPlainObject, extractPointerFromRef, extractSourceFromRef } from '@stoplight/json';
 import { DiagnosticSeverity } from '@stoplight/types';
 import { FormatsSet } from './utils/formatsSet';
+import { isSimpleAliasDefinition } from './utils/guards';
 
 const STACK_SYMBOL = Symbol('@stoplight/spectral/ruleset/#stack');
 const DEFAULT_RULESET_FILE = /^\.?spectral\.(ya?ml|json|m?js)$/;
@@ -78,7 +79,7 @@ export class Ruleset {
             Object.entries(definition.aliases).map(alias => {
               const [name, value] = alias;
 
-              if (typeof value === 'string') {
+              if (isSimpleAliasDefinition(value)) {
                 return alias;
               }
 
diff --git a/packages/core/src/ruleset/types.ts b/packages/core/src/ruleset/types.ts
index ebc6e2fc..844d3d67 100644
--- a/packages/core/src/ruleset/types.ts
+++ b/packages/core/src/ruleset/types.ts
@@ -35,7 +35,7 @@ export type RuleDefinition = {
   recommended?: boolean;
 
   // Filter the target down to a subset[] with a JSON path
-  given: string | string[];
+  given: GivenDefinition;
 
   // If false, rule will operate on original (unresolved) data
   // If undefined or true, resolved data will be supplied
@@ -57,6 +57,8 @@ export interface IRuleThen {
   functionOptions?: unknown;
 }
 
+export type GivenDefinition = string | string[];
+
 export type RulesetExtendsDefinition =
   | RulesetDefinition
   | (RulesetDefinition | [RulesetDefinition, FileRulesetSeverityDefinition])[];
@@ -79,18 +81,18 @@ export type RulesetOverridesDefinition = ReadonlyArray<{ files: string[] } & Rul
 export type RulesetScopedAliasDefinition = {
   description?: string;
   targets: {
-    formats: FormatsSet;
-    given: string;
+    formats: FormatsSet | Format[];
+    given: GivenDefinition;
   }[];
 };
 
-export type RulesetAliasesDefinition = Record<string, string | RulesetScopedAliasDefinition>;
+export type RulesetAliasesDefinition = Record<string, GivenDefinition | RulesetScopedAliasDefinition>;
 
 export type RulesetDefinition = Readonly<
   {
     documentationUrl?: string;
     description?: string;
-    formats?: Format<any>[];
+    formats?: FormatsSet | Format[];
     parserOptions?: Partial<ParserOptions>;
     overrides?: RulesetOverridesDefinition;
     aliases?: RulesetAliasesDefinition;
diff --git a/packages/core/src/ruleset/utils/guards.ts b/packages/core/src/ruleset/utils/guards.ts
new file mode 100644
index 00000000..8bc05279
--- /dev/null
+++ b/packages/core/src/ruleset/utils/guards.ts
@@ -0,0 +1,7 @@
+import { RulesetScopedAliasDefinition } from '../types';
+
+export function isSimpleAliasDefinition(
+  alias: string | string[] | RulesetScopedAliasDefinition,
+): alias is string | string[] {
+  return typeof alias === 'string' || Array.isArray(alias);
+}
diff --git a/packages/core/src/runner/lintNode.ts b/packages/core/src/runner/lintNode.ts
index b094e21d..f8cd3806 100644
--- a/packages/core/src/runner/lintNode.ts
+++ b/packages/core/src/runner/lintNode.ts
@@ -6,7 +6,7 @@ import { Document } from '../document';
 import { IFunctionResult, IGivenNode, RulesetFunctionContext } from '../types';
 import { IRunnerInternalContext } from './types';
 import { getLintTargets, MessageVars, message } from './utils';
-import { Rule } from '../ruleset/rule/rule';
+import { Rule } from '../ruleset/rule';
 
 export const lintNode = (context: IRunnerInternalContext, node: IGivenNode, rule: Rule): void => {
   const fnContext: RulesetFunctionContext = {
diff --git a/packages/core/src/types/function.ts b/packages/core/src/types/function.ts
index fef8aa1c..b4e2aae5 100644
--- a/packages/core/src/types/function.ts
+++ b/packages/core/src/types/function.ts
@@ -1,6 +1,6 @@
 import type { JsonPath } from '@stoplight/types';
 import type { IDocumentInventory } from '../documentInventory';
-import type { IRule } from '../ruleset/rule/rule';
+import type { IRule } from '../ruleset/rule';
 import type { IDocument } from '../document';
 import type { JSONSchema7 } from 'json-schema';
 


Test patch:
diff --git a/packages/core/src/ruleset/__tests__/__helpers__/print.ts b/packages/core/src/ruleset/__tests__/__helpers__/print.ts
index 6dba5ae2..694db087 100644
--- a/packages/core/src/ruleset/__tests__/__helpers__/print.ts
+++ b/packages/core/src/ruleset/__tests__/__helpers__/print.ts
@@ -1,5 +1,5 @@
 import { asTree } from 'treeify';
-import { Rule } from '../../rule/rule';
+import { Rule } from '../../rule';
 import { Ruleset } from '../../ruleset';
 import { RulesetDefinition } from '../../types';
 import { Format } from '../../format';
diff --git a/packages/core/src/ruleset/__tests__/ruleset.test.ts b/packages/core/src/ruleset/__tests__/ruleset.test.ts
index 1677bff4..a0bbdb63 100644
--- a/packages/core/src/ruleset/__tests__/ruleset.test.ts
+++ b/packages/core/src/ruleset/__tests__/ruleset.test.ts
@@ -1313,13 +1313,17 @@ describe('Ruleset', () => {
     });
 
     describe('scoped aliases', () => {
+      const createStubFormat =
+        (): Format<unknown> =>
+        (input): input is unknown =>
+          true;
+
       it('should resolve locally defined aliases according to their targets', () => {
-        const draft4: Format<JSONSchema4> = (input): input is JSONSchema4 =>
-          isPlainObject(input) && input.$schema === 'http://json-schema.org/draft-04/schema#';
-        const draft6: Format<JSONSchema6> = (input): input is JSONSchema6 =>
-          isPlainObject(input) && input.$schema === 'http://json-schema.org/draft-06/schema#';
-        const draft7: Format<JSONSchema7> = (input): input is JSONSchema7 =>
-          isPlainObject(input) && input.$schema === 'http://json-schema.org/draft-07/schema#';
+        const oas2 = createStubFormat();
+        const oas3 = createStubFormat();
+        const draft4 = createStubFormat();
+        const draft6 = createStubFormat();
+        const draft7 = createStubFormat();
 
         const ruleset = new Ruleset({
           aliases: {
@@ -1335,8 +1339,33 @@ describe('Ruleset', () => {
                 },
               ],
             },
+
+            PathItem: '$.paths[*]',
+            OperationObject: '#PathItem[get,put,post,delete,options,head,patch,trace]',
+            ParametersDefinitionsObject: {
+              targets: [
+                { formats: [oas2], given: ['$.parameters'] },
+                { formats: [oas3], given: ['$.components.parameters'] },
+              ],
+            },
+            ParametersObject: {
+              targets: [
+                { formats: [oas2], given: ['#PathItem.parameters', '#OperationObject.parameters'] },
+                {
+                  formats: [oas3],
+                  given: ['#PathItem.parameters', '#OperationObject.parameters'],
+                },
+              ],
+            },
+            ParameterObject: ['#ParametersDefinitionsObject[*]', '#ParametersObject[?(@ && !@.$ref)]'],
           },
           rules: {
+            'valid-parameter': {
+              given: '#ParameterObject',
+              then: {
+                function: truthy,
+              },
+            },
             'valid-id': {
               given: '#Id',
               then: {
@@ -1355,6 +1384,69 @@ describe('Ruleset', () => {
         expect(ruleset.rules['valid-id'].getGivenForFormats(new FormatsSet([draft6, draft7]))).toStrictEqual([
           '$..$id',
         ]);
+
+        expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas2]))).toStrictEqual([
+          '$.parameters[*]',
+          '$.paths[*].parameters[?(@ && !@.$ref)]',
+          '$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]',
+        ]);
+        expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas3]))).toStrictEqual([
+          '$.components.parameters[*]',
+          '$.paths[*].parameters[?(@ && !@.$ref)]',
+          '$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]',
+        ]);
+        expect(ruleset.rules['valid-parameter'].getGivenForFormats(new FormatsSet([oas2, oas3]))).toStrictEqual([
+          '$.components.parameters[*]',
+          '$.paths[*].parameters[?(@ && !@.$ref)]',
+          '$.paths[*][get,put,post,delete,options,head,patch,trace].parameters[?(@ && !@.$ref)]',
+        ]);
+      });
+
+      it('given circular alias, should throw', () => {
+        const oas3 = createStubFormat();
+
+        const ruleset = new Ruleset({
+          aliases: {
+            Components: {
+              targets: [
+                {
+                  formats: [oas3],
+                  given: ['#HeaderObject'],
+                },
+              ],
+            },
+            HeaderObjects: {
+              targets: [
+                {
+                  formats: [oas3],
+                  given: ['#Components.headers'],
+                },
+              ],
+            },
+            HeaderObject: {
+              targets: [
+                {
+                  formats: [oas3],
+                  given: ['#HeaderObjects[*]'],
+                },
+              ],
+            },
+          },
+          rules: {
+            'valid-header': {
+              given: '#HeaderObject',
+              then: {
+                function: truthy,
+              },
+            },
+          },
+        });
+
+        expect(() => ruleset.rules['valid-header'].getGivenForFormats(new FormatsSet([oas3]))).toThrowError(
+          ReferenceError(
+            'Alias "HeaderObject" is circular. Resolution stack: HeaderObject -> HeaderObjects -> Components -> HeaderObject',
+          ),
+        );
       });
 
       it('should drop aliases not matching any target', () => {
diff --git a/packages/core/src/ruleset/__tests__/validation.test.ts b/packages/core/src/ruleset/__tests__/validation.test.ts
index daf59a8b..b4bee4cf 100644
--- a/packages/core/src/ruleset/__tests__/validation.test.ts
+++ b/packages/core/src/ruleset/__tests__/validation.test.ts
@@ -78,7 +78,7 @@ Error at #/rules/rule-with-invalid-enum/severity: the value has to be one of: 0,
         rules: {
           rule: {
             documentationUrl: 'https://stoplight.io/p/docs/gh/stoplightio/spectral/docs/reference/openapi-rules.md',
-            given: '',
+            given: '$',
             then: {
               function: '',
             },
@@ -372,7 +372,7 @@ Error at #/rules/rule/formats/1: must be a valid format`,
       ).toThrow(new RulesetValidationError('Error at #/aliases: must be object'));
     });
 
-    it.each([null, 5, [], {}])('recognizes %p as an invalid type of aliases', alias => {
+    it.each([null, 5])('recognizes %p as an invalid type of aliases', alias => {
       expect(
         assertValidRuleset.bind(null, {
           rules: {},
@@ -382,7 +382,7 @@ Error at #/rules/rule/formats/1: must be a valid format`,
         }),
       ).toThrow(
         new RulesetValidationError(
-          'Error at #/aliases/alias: the value of an alias must be a valid JSON Path expression, a reference to the existing Alias optionally paired with a JSON Path expression subset, or contain a valid set of targets',
+          'Error at #/aliases/alias: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
         ),
       );
     });
@@ -402,7 +402,21 @@ Error at #/rules/rule/formats/1: must be a valid format`,
       );
     });
 
-    it.each(['', 'foo'])('given %s value used as an alias, throws', value => {
+    it.each<[unknown, string]>([
+      [
+        '',
+        'Error at #/aliases/PathItem: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
+      ],
+      [
+        'foo',
+        'Error at #/aliases/PathItem: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
+      ],
+      [[], 'Error at #/aliases/PathItem: must be a non-empty array of expressions'],
+      [
+        [0],
+        'Error at #/aliases/PathItem/0: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
+      ],
+    ])('given %s value used as an alias, throws', (value, error) => {
       expect(
         assertValidRuleset.bind(null, {
           rules: {},
@@ -410,14 +424,25 @@ Error at #/rules/rule/formats/1: must be a valid format`,
             PathItem: value,
           },
         }),
-      ).toThrow(
-        new RulesetValidationError(
-          'Error at #/aliases/PathItem: the value of an alias must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset',
-        ),
-      );
+      ).toThrow(new RulesetValidationError(error));
     });
 
     describe('given scoped aliases', () => {
+      it('demands targets to be present', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {},
+            aliases: {
+              alias: {},
+            },
+          }),
+        ).toThrow(
+          new RulesetValidationError(
+            'Error at #/aliases/alias: targets must be present and have at least a single alias definition',
+          ),
+        );
+      });
+
       it.each(['Info', 'Info-Description', 'Info_Description', 'Response404', 'errorMessage'])(
         'recognizes %s as a valid key of an alias',
         alias => {
@@ -552,7 +577,7 @@ Error at #/aliases/SchemaObject/targets/1/formats/1: must be a valid format`,
           }),
         ).toThrow(
           new RulesetValidationError(
-            `Error at #/aliases/SchemaObject/targets/1/given: the value of an alias must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset`,
+            `Error at #/aliases/SchemaObject/targets/1/given: must be a valid JSON Path expression or a reference to the existing Alias optionally paired with a JSON Path expression subset`,
           ),
         );
       });