Repository: stoplightio/spectral
Pull request #1144 (2020-05-05 13:58:42)

chore(ruleset): improve then validation
This is a complementary PR to https://github.com/stoplightio/spectral/pull/1136

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No

**Additional context**

Enhances the ruleset validation to make sure all known `then` with a built-in function are validated according to the typings.
The benefit of that change is the lack of need for any types clobbering as well as any other custom validation (like we had to do in case of casing fn, for instance)


P0lip: @nulltoken bump :slightly_smiling_face: 
P0lip: Since there is only the doc change that hasn't been reviewed, I'll go ahead and merge it.
IF you have any concerns about it, LMK and I'll address it in another PR.

Patch:
diff --git a/.editorconfig b/.editorconfig
index e76d119d..0d010b0e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,7 +3,7 @@ root = true
 [*]
 charset = utf-8
 
-[*.{ts,js,yaml,scenario,md}]
+[*.{ts,js,yaml,yml,json,scenario,md}]
 indent_style = space
 indent_size = 2
 
diff --git a/docs/reference/functions.md b/docs/reference/functions.md
index 16323e7c..cba014d7 100644
--- a/docs/reference/functions.md
+++ b/docs/reference/functions.md
@@ -8,9 +8,9 @@ Enforce alphabetical content, for simple arrays, or for objects by passing a key
 
 <!-- title: functionOptions -->
 
-name | description | required?
----------|----------|---------
-keyedBy | key to sort an object by | no
+| name    | description              | type     | required? |
+| ------- | ------------------------ | -------- | --------- |
+| keyedBy | key to sort an object by | `string` | no        |
 
 <!-- title: example -->
 
@@ -33,9 +33,9 @@ Does the field value exist in this set of possible values?
 
 <!-- title: functionOptions -->
 
-name | description | required?
----------|----------|---------
-values | an array of possible values | yes
+| name   | description                 | type                   | required? |
+| ------ | --------------------------- | ---------------------- | --------- |
+| values | an array of possible values | `Array<number,string>` | yes       |
 
 <!-- title: example -->
 
@@ -83,22 +83,22 @@ Regular expressions!
 
 <!-- title: functionOptions -->
 
-name | description | required?
----------|----------|---------
-match | if provided, value must match this regex | no
-notMatch | if provided, value must _not_ match this regex | no
+| name     | description                                    | type     | required? |
+| -------- | ---------------------------------------------- | -------- | --------- |
+| match    | if provided, value must match this regex       | `string` | no        |
+| notMatch | if provided, value must _not_ match this regex | `string` | no        |
 
 <!-- title: example -->
 
 ```yaml
-  path-no-trailing-slash:
-    description: Paths should not end with `#/`.
-    type: style
-    given: "$.paths[*]~"
-    then:
-      function: pattern
-      functionOptions:
-        notMatch: ".+\/$"
+path-no-trailing-slash:
+  description: Paths should not end with `#/`.
+  type: style
+  given: "$.paths[*]~"
+  then:
+    function: pattern
+    functionOptions:
+      notMatch: ".+\/$"
 ```
 
 ## casing
@@ -107,22 +107,23 @@ Text must match a certain case, like `camelCase` or `snake_case`.
 
 <!-- title: functionOptions -->
 
-name                   | description                                             | required?
------------------------|---------------------------------------------------------|----------
-type                   | the casing type to match against                        | yes
-disallowDigits         | if not truthy, digits are allowed                       | no
-separator.char         | additional char to separate groups of words             | no
-separator.allowLeading | can the group separator char be used at the first char? | no
+| name                   | description                                             | type         | required? |
+| ---------------------- | ------------------------------------------------------- | ------------ | --------- |
+| type                   | the casing type to match against                        | `CasingType` | yes       |
+| disallowDigits         | if not truthy, digits are allowed                       | `boolean`    | no        |
+| separator.char         | additional char to separate groups of words             | `string`     | no        |
+| separator.allowLeading | can the group separator char be used at the first char? | `boolean`    | no        |
 
 **Note:** In advanced scenarios, `separator.char` and `separator.allowLeading` can be leveraged to validate certain naming conventions.
 For instance, the following naming style could be enforced:
- - Headers _(eg. `X-YourMighty-Header`)_: type: `pascal`, separator.char: `-`
- - Camel cased paths _(eg. `/path/toThe/amazingResource`)_: type: `camel`, separator.char: `/`, separator.allowLeading: `true`
+
+- Headers _(eg. `X-YourMighty-Header`)_: type: `pascal`, separator.char: `-`
+- Camel cased paths _(eg. `/path/toThe/amazingResource`)_: type: `camel`, separator.char: `/`, separator.allowLeading: `true`
 
 Available types are:
 
 | name   | sample         |
-|--------|----------------|
+| ------ | -------------- |
 | flat   | verylongname   |
 | camel  | veryLongName   |
 | pascal | VeryLongName   |
@@ -146,14 +147,14 @@ camel-case-name:
 
 ## schema
 
-Use JSON Schema (draft 4, 6 or 7) to treat the contents of the $given JSON Path as a JSON instance.
+Use JSON Schema (draft 4, 6 or 7) to treat the contents of the \$given JSON Path as a JSON instance.
 
 <!-- title: functionOptions -->
 
-name | description | required?
----------|----------|---------
-schema | a valid JSON Schema document | yes
-allErrors | returns all errors when `true`; otherwise only returns the first error | no
+| name      | description                                                            | type         | required? |
+| --------- | ---------------------------------------------------------------------- | ------------ | --------- |
+| schema    | a valid JSON Schema document                                           | `JSONSchema` | yes       |
+| allErrors | returns all errors when `true`; otherwise only returns the first error | `boolean`    | no        |
 
 <!-- title: example -->
 
@@ -180,27 +181,27 @@ The schema-path rule is very meta. It is an extension of the schema rule, but it
 
 <!-- title: functionOptions -->
 
-name | description | required?
----------|----------|---------
-field | the field to check | yes
-schemaPath | a json path pointing to the json schema to use | yes
-allErrors | returns all errors when `true`; otherwise only returns the first error | no
+| name       | description                                                            | type      | required? |
+| ---------- | ---------------------------------------------------------------------- | --------- | --------- |
+| field      | the field to check                                                     | `string`  | yes       |
+| schemaPath | a json path pointing to the json schema to use                         | `string`  | yes       |
+| allErrors  | returns all errors when `true`; otherwise only returns the first error | `boolean` | no        |
 
 <!-- title: example -->
 
-``` yaml
-  valid-oas-example-in-parameters:
-    description: Examples must be valid against their defined schema.
-    message: "{{error}}"
-    recommended: true
-    severity: 0
-    type: validation
-    given: "$..parameters..[?(@.example && @.schema)]"
-    then:
-      function: schemaPath
-      functionOptions:
-        field: example
-        schemaPath: "$.schema"
+```yaml
+valid-oas-example-in-parameters:
+  description: Examples must be valid against their defined schema.
+  message: "{{error}}"
+  recommended: true
+  severity: 0
+  type: validation
+  given: "$..parameters..[?(@.example && @.schema)]"
+  then:
+    function: schemaPath
+    functionOptions:
+      field: example
+      schemaPath: "$.schema"
 ```
 
 ## truthy
@@ -209,7 +210,7 @@ The value should not be `false`, `""`, `0`, `null` or `undefined`. Basically any
 
 <!-- title: example -->
 
-``` yaml
+```yaml
 important-fields:
   description: Absolutely must have a title and a description
   message: "Missing the {{property}}"
@@ -226,7 +227,7 @@ important-fields:
 
 The value must be `undefined`. When combined with `field: foo` on an object the `foo` property must be undefined.
 
-_**Note:** Due to the way YAML works, just having `foo: ` with no value set is not the same as being `undefined`. This would be `falsy`._
+_**Note:** Due to the way YAML works, just having `foo:` with no value set is not the same as being `undefined`. This would be `falsy`._
 
 ## unreferencedReusableObject
 
@@ -238,13 +239,13 @@ _Warning:_ This function may identify false positives when used against a specif
 
 <!-- title: functionOptions -->
 
-name | description | required?
----------|----------|---------
-reusableObjectsLocation | a local json pointer to the document member holding the reusable objects (eg. `#/definitions` for an OAS2 document, `#/components/schemas` for an OAS3 document). | yes
+| name                    | description                                                                                                                                                       | type     | required? |
+| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
+| reusableObjectsLocation | a local json pointer to the document member holding the reusable objects (eg. `#/definitions` for an OAS2 document, `#/components/schemas` for an OAS3 document). | `string` | yes       |
 
 <!-- title: example -->
 
-``` yaml
+```yaml
 unused-definition:
   description: Potentially unused definition has been detected.
   recommended: true
@@ -263,9 +264,9 @@ Communicate that one of these properties is required, and no more than one is al
 
 <!-- title: functionOptions -->
 
-name | description | required?
----------|----------|---------
-properties | the properties to check | yes
+| name       | description             | type       | required? |
+| ---------- | ----------------------- | ---------- | --------- |
+| properties | the properties to check | `string[]` | yes       |
 
 <!-- title: example -->
 
@@ -278,8 +279,8 @@ components-examples-value-or-externalValue:
     function: xor
     functionOptions:
       properties:
-      - externalValue
-      - value
+        - externalValue
+        - value
 ```
 
 ## typedEnum
diff --git a/karma-jest.ts b/karma-jest.ts
index 3fe2bbd9..9d5f6568 100644
--- a/karma-jest.ts
+++ b/karma-jest.ts
@@ -29,3 +29,6 @@ test.each = input => (name: string, fn: Function) => {
     }
   }
 };
+
+// @ts-ignore
+describe.each = test.each;
diff --git a/src/functions/alphabetical.ts b/src/functions/alphabetical.ts
index cbc810ee..81a2e412 100644
--- a/src/functions/alphabetical.ts
+++ b/src/functions/alphabetical.ts
@@ -28,7 +28,7 @@ const getUnsortedItems = <T>(arr: T[], compareFn: (a: T, B: T) => number): null
   return null;
 };
 
-export const alphabetical: IFunction<IAlphaRuleOptions> = (targetVal, opts, paths, { documentInventory }) => {
+export const alphabetical: IFunction<IAlphaRuleOptions | null> = (targetVal, opts, paths, { documentInventory }) => {
   const results: IFunctionResult[] = [];
 
   if (!isObject(targetVal)) {
@@ -49,7 +49,7 @@ export const alphabetical: IFunction<IAlphaRuleOptions> = (targetVal, opts, path
     return results;
   }
 
-  const { keyedBy } = opts;
+  const keyedBy = opts?.keyedBy;
 
   const unsortedItems = getUnsortedItems<unknown>(
     targetArray,
diff --git a/src/functions/casing.ts b/src/functions/casing.ts
index 7f8d15a9..fffe480a 100644
--- a/src/functions/casing.ts
+++ b/src/functions/casing.ts
@@ -1,5 +1,4 @@
 import { Dictionary } from '@stoplight/types';
-import { AssertionError } from 'assert';
 import { escapeRegExp } from 'lodash';
 import { IFunction } from '../types';
 
@@ -37,8 +36,6 @@ export const casing: IFunction<ICasingOptions> = (targetVal, opts) => {
     return;
   }
 
-  assertValidOptions(opts);
-
   const casingValidator = buildFrom(CASES[opts.type], opts);
 
   if (casingValidator.test(targetVal)) {
@@ -52,26 +49,6 @@ export const casing: IFunction<ICasingOptions> = (targetVal, opts) => {
   ];
 };
 
-function assertValidOptions(opts: ICasingOptions): asserts opts is ICasingOptions {
-  if (!(opts.type in CASES)) {
-    throw new AssertionError({ message: `Invalid '${opts.type}' type value.` });
-  }
-
-  if (opts.separator === undefined) {
-    return;
-  }
-
-  if (opts.separator.allowLeading !== undefined && opts.separator.char === undefined) {
-    throw new AssertionError({
-      message: "'separator.allowLeading' can only be valued when 'separator.char' is defined.",
-    });
-  }
-
-  if (opts.separator.char.length !== 1) {
-    throw new AssertionError({ message: "When valued, 'separator.char' should only be one character long." });
-  }
-}
-
 const DIGITS_PATTERN = '0-9';
 
 const buildFrom = (basePattern: string, overrides: ICasingOptions): RegExp => {
diff --git a/src/functions/schema-path.ts b/src/functions/schema-path.ts
index 853813d1..e665d8f4 100644
--- a/src/functions/schema-path.ts
+++ b/src/functions/schema-path.ts
@@ -20,7 +20,7 @@ export interface ISchemaPathOptions {
   // the `path.to.prop` to field, or special `@key` value to target keys for matched `given` object
   field?: string;
   // The oasVersion, either 2 or 3 for OpenAPI Spec versions, could also be 3.1 or a larger number if there's a need for it, otherwise JSON Schema
-  oasVersion?: Optional<number>;
+  oasVersion?: Optional<2 | 3 | 3.1>;
   allErrors?: boolean;
 }
 
diff --git a/src/functions/schema.ts b/src/functions/schema.ts
index 8a9db9f6..5574d70e 100644
--- a/src/functions/schema.ts
+++ b/src/functions/schema.ts
@@ -24,7 +24,7 @@ export interface ISchemaFunction extends IFunction<ISchemaOptions> {
 export interface ISchemaOptions {
   schema: object;
   // The oasVersion, either 2 or 3 for OpenAPI Spec versions, could also be 3.1 or a larger number if there's a need for it, otherwise JSON Schema
-  oasVersion?: Optional<number>;
+  oasVersion?: Optional<2 | 3 | 3.1>;
   allErrors?: boolean;
   ajv?: ValidateFunction;
 
diff --git a/src/meta/rule.schema.json b/src/meta/rule.schema.json
index 5dbd7612..de6a1537 100644
--- a/src/meta/rule.schema.json
+++ b/src/meta/rule.schema.json
@@ -1,150 +1,464 @@
 {
-    "$id": "http://stoplight.io/schemas/rule.schema.json",
-    "$schema": "http://json-schema.org/draft-07/schema#",
-    "definitions": {
-        "IThen<T,O>": {
+  "$id": "http://stoplight.io/schemas/rule.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "definitions": {
+    "Then": {
+      "allOf": [
+        {
+          "properties": {
+            "field": {
+              "type": "string"
+            }
+          }
+        },
+
+        {
+          "if": {
+           "properties": {
+             "function": {
+               "const": "alphabetical"
+             }
+           }
+          },
+          "then": {
             "properties": {
-                "field": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "keyedBy": {
                     "type": "string"
+                  }
                 },
-                "function": {
-                    "$ref": "#/definitions/T"
+                "type": ["object", "null"]
+              }
+            }
+          }
+        },
+
+        {
+          "if": {
+            "properties": {
+              "function": {
+                "const": "casing"
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "type": {
+                    "type": "string",
+                    "enum": ["flat", "camel", "pascal", "kebab", "cobol", "snake", "macro"]
+                  },
+                  "disallowDigits": {
+                    "type": "boolean"
+                  },
+                  "separator": {
+                    "additionalProperties": false,
+                    "properties": {
+                      "char": {
+                        "type": "string",
+                        "maxLength": 1
+                      },
+                      "allowLeading": {
+                        "type": "boolean"
+                      }
+                    },
+                    "required": ["char"],
+                    "type": "object"
+                  }
                 },
-                "functionOptions": {
-                    "$ref": "#/definitions/O"
-                }
+                "required": ["type"],
+                "type": "object"
+              }
             },
-            "required": [
-                "function"
-            ],
-            "type": "object"
-        },
-        "O": {
-            "type": "object"
+            "required": ["functionOptions"]
+          }
         },
-        "T": {
-            "type": "string"
+
+        {
+          "if": {
+            "properties": {
+              "function": {
+                "const": "enumeration"
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "values": {
+                    "type": "array",
+                    "items": {
+                      "type": ["string", "number"]
+                    }
+                  }
+                },
+                "type": "object",
+                "required": ["values"]
+              }
+            },
+            "required": ["functionOptions"]
+          }
         },
-        "DiagnosticSeverity": {
-            "enum": [
-                -1,
-                0,
-                1,
-                2,
-                3
-            ],
-            "type": "number"
-        },
-        "HumanReadableSeverity": {
-            "type": "string",
-            "enum": [
-                "error",
-                "warn",
-                "info",
-                "hint",
-                "off"
-            ]
-        },
-        "Severity": {
-            "oneOf": [
-                {
-                    "$ref": "#/definitions/DiagnosticSeverity"
+
+        {
+          "if": {
+            "properties": {
+              "function": {
+                "const": "length"
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "min": {
+                    "type": "number"
+                  },
+                  "max": {
+                    "type": "number"
+                  }
                 },
-                {
-                    "$ref": "#/definitions/HumanReadableSeverity"
-                }
-            ]
-        }
-    },
-    "oneOf": [
+                "minProperties": 1,
+                "type": "object"
+              }
+            },
+            "required": ["functionOptions"]
+          }
+        },
+
         {
+          "if": {
             "properties": {
-                "description": {
+              "function": {
+                "const": "pattern"
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "match": {
+                    "type": "string"
+                  },
+                  "notMatch": {
                     "type": "string"
+                  }
                 },
-                "recommended": {
+                "minProperties": 1,
+                "type": "object"
+              }
+            },
+            "required": ["functionOptions"]
+          }
+        },
+
+        {
+          "if": {
+            "properties": {
+              "function": {
+                "const": "schema"
+              }
+            }
+          },
+          "then": {
+            "required": ["functionOptions"],
+            "properties": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "schema": {
+                    "type": "object"
+                  },
+                  "oasVersion": {
+                    "enum": [
+                      2,
+                      3,
+                      3.1
+                    ],
+                    "type": "number"
+                  },
+                  "allErrors": {
                     "type": "boolean"
+                  }
                 },
-                "given": {
-                    "oneOf": [
-                        {
-                            "type": "string"
-                        },
-                        {
-                            "type": "array",
-                            "items": {
-                                "type": "string"
-                            }
-                        }
-                    ]
-                },
-                "resolved": {
+                "required": [
+                  "schema"
+                ],
+                "type": "object"
+              }
+            }
+          }
+        },
+
+        {
+          "if": {
+            "properties": {
+              "function": {
+                "const": "schema-path"
+              }
+            }
+          },
+          "then": {
+            "required": ["functionOptions"],
+            "properties": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "schemaPath": {
+                    "type": "string"
+                  },
+                  "field": {
+                    "type": "string"
+                  },
+                  "oasVersion": {
+                    "enum": [
+                      2,
+                      3,
+                      3.1
+                    ],
+                    "type": "number"
+                  },
+                  "allErrors": {
                     "type": "boolean"
+                  }
                 },
-                "severity": {
-                    "$ref": "#/definitions/Severity"
-                },
-                "message": {
+                "required": [
+                  "schemaPath"
+                ],
+                "type": "object"
+              }
+            }
+          }
+        },
+
+        {
+          "if": {
+            "properties": {
+              "function": {
+                "const": "unreferencedReusableObject"
+              }
+            }
+          },
+          "then": {
+            "required": ["functionOptions"],
+            "properties": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "reusableObjectsLocation": {
                     "type": "string"
+                  }
                 },
-                "tags": {
-                    "items": {
-                        "type": "string"
-                    },
-                    "type": "array"
-                },
-                "formats": {
+                "required": [
+                  "reusableObjectsLocation"
+                ],
+                "type": "object"
+              }
+            }
+          }
+        },
+
+        {
+          "if": {
+            "properties": {
+              "function": {
+                "const": "xor"
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "functionOptions": {
+                "additionalProperties": false,
+                "properties": {
+                  "properties": {
                     "type": "array",
                     "items": {
-                        "type": "string",
-                        "minItems": 1
-                    }
-                },
-                "then": {
-                    "anyOf": [
-                        {
-                            "$ref": "#/definitions/IThen<T,O>"
-                        },
-                        {
-                            "items": {
-                                "$ref": "#/definitions/IThen<T,O>"
-                            },
-                            "type": "array"
-                        }
-                    ]
+                      "type": "string"
+                    },
+                    "minItems": 2
+                  }
                 },
-                "type": {
-                    "enum": [
-                        "style",
-                        "validation"
-                    ],
-                    "type": "string"
-                }
+                "type": "object",
+                "required": ["properties"]
+              }
             },
-            "required": [
-                "given",
-                "then"
-            ],
-            "type": "object"
+            "required": ["functionOptions"]
+          }
         },
+
         {
-            "$ref": "#/definitions/HumanReadableSeverity"
+          "if": {
+            "properties": {
+              "function": {
+                "type": "string",
+                "enum": [
+                  "truthy",
+                  "falsy",
+                  "undefined"
+                ]
+              }
+            }
+          },
+          "then": {
+            "type": "object",
+            "properties": {
+              "functionOptions": {
+                "type": ["null"]
+              }
+            }
+          }
         },
+
         {
-            "type": "boolean"
+          "if": {
+            "properties": {
+              "function": {
+                "type": "string"
+              }
+            }
+          },
+          "then": {
+            "properties": {
+              "functionOptions": {
+                "type": ["object", "null"]
+              }
+            }
+          }
+        }
+      ],
+      "type": "object"
+    },
+    "DiagnosticSeverity": {
+      "enum": [
+        -1,
+        0,
+        1,
+        2,
+        3
+      ],
+      "type": "number"
+    },
+    "HumanReadableSeverity": {
+      "type": "string",
+      "enum": [
+        "error",
+        "warn",
+        "info",
+        "hint",
+        "off"
+      ]
+    },
+    "Severity": {
+      "oneOf": [
+        {
+          "$ref": "#/definitions/DiagnosticSeverity"
         },
         {
-            "type": "array",
-            "additionalItems": false,
-            "minItems": 1,
-            "items": [
-                {
-                    "$ref": "#/definitions/Severity"
-                },
-                {
-                    "type": "object"
-                }
-            ]
+          "$ref": "#/definitions/HumanReadableSeverity"
+        }
+      ]
+    }
+  },
+  "oneOf": [
+    {
+      "properties": {
+        "description": {
+          "type": "string"
+        },
+        "recommended": {
+          "type": "boolean"
+        },
+        "given": {
+          "oneOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "array",
+              "items": {
+                "type": "string"
+              }
+            }
+          ]
+        },
+        "resolved": {
+          "type": "boolean"
+        },
+        "severity": {
+          "$ref": "#/definitions/Severity"
+        },
+        "message": {
+          "type": "string"
+        },
+        "tags": {
+          "items": {
+            "type": "string"
+          },
+          "type": "array"
+        },
+        "formats": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "minItems": 1
+          }
+        },
+        "then": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Then"
+            },
+            {
+              "items": {
+                "$ref": "#/definitions/Then"
+              },
+              "type": "array"
+            }
+          ]
+        },
+        "type": {
+          "enum": [
+            "style",
+            "validation"
+          ],
+          "type": "string"
+        }
+      },
+      "required": [
+        "given",
+        "then"
+      ],
+      "type": "object"
+    },
+    {
+      "$ref": "#/definitions/HumanReadableSeverity"
+    },
+    {
+      "type": "boolean"
+    },
+    {
+      "type": "array",
+      "additionalItems": false,
+      "minItems": 1,
+      "items": [
+        {
+          "$ref": "#/definitions/Severity"
+        },
+        {
+          "type": "object"
         }
-    ]
+      ]
+    }
+  ]
 }
diff --git a/src/meta/ruleset.schema.json b/src/meta/ruleset.schema.json
index 55a80628..a4ec140d 100644
--- a/src/meta/ruleset.schema.json
+++ b/src/meta/ruleset.schema.json
@@ -6,7 +6,7 @@
         "rules": {
             "type": "object",
             "additionalProperties": {
-                "$ref": "rule.schema.json"
+                "$ref": "rule.schema.json#"
             }
         },
         "formats": {
diff --git a/src/rulesets/asyncapi/functions/asyncApi2PayloadValidation.ts b/src/rulesets/asyncapi/functions/asyncApi2PayloadValidation.ts
index 04885653..2d94ee71 100644
--- a/src/rulesets/asyncapi/functions/asyncApi2PayloadValidation.ts
+++ b/src/rulesets/asyncapi/functions/asyncApi2PayloadValidation.ts
@@ -28,7 +28,7 @@ const buildAsyncApi2SchemaObjectValidator = (schemaFn: ISchemaFunction): Validat
   return validator;
 };
 
-export const asyncApi2PayloadValidation: IFunction = function(
+export const asyncApi2PayloadValidation: IFunction<null> = function(
   this: IFunctionContext,
   targetVal,
   _opts,
diff --git a/src/rulesets/validation.ts b/src/rulesets/validation.ts
index 9c3bdb02..f2ee5286 100644
--- a/src/rulesets/validation.ts
+++ b/src/rulesets/validation.ts
@@ -42,7 +42,7 @@ export function isValidRule(rule: FileRule): rule is IRule {
   return typeof rule === 'object' && rule !== null && !Array.isArray(rule) && ('given' in rule || 'then' in rule);
 }
 
-export function decorateIFunctionWithSchemaValidation(fn: IFunction, schema: JSONSchema) {
+export function decorateIFunctionWithSchemaValidation(fn: IFunction<any>, schema: JSONSchema) {
   return (data: unknown, opts: unknown, ...args: [IFunctionPaths, IFunctionValues]) => {
     if (!ajv.validate(schema, opts)) {
       throw new ValidationError(ajv.errors);
diff --git a/src/runner/lintNode.ts b/src/runner/lintNode.ts
index 90eb6490..711b2793 100644
--- a/src/runner/lintNode.ts
+++ b/src/runner/lintNode.ts
@@ -40,7 +40,7 @@ export const lintNode = (
       try {
         targetResults = func(
           target.value,
-          then.functionOptions || {},
+          then.functionOptions ?? null,
           {
             given: givenPath,
             target: targetPath,
diff --git a/src/types/function.ts b/src/types/function.ts
index a3689aef..b0ae93e0 100644
--- a/src/types/function.ts
+++ b/src/types/function.ts
@@ -7,9 +7,9 @@ export interface IFunctionContext {
   cache: Map<unknown, unknown>;
 }
 
-export type IFunction<O = any> = (
+export type IFunction<O extends object | null = null> = (
   targetValue: any,
-  options: O,
+  options: O extends null ? null : O,
   paths: IFunctionPaths,
   otherValues: IFunctionValues,
 ) => void | IFunctionResult[] | Promise<void | IFunctionResult[]>;
diff --git a/src/types/spectral.ts b/src/types/spectral.ts
index 36b78684..a764a0cb 100644
--- a/src/types/spectral.ts
+++ b/src/types/spectral.ts
@@ -5,7 +5,7 @@ import { IFunction, IRule } from '.';
 import { Rule } from '../rule';
 import { ComputeFingerprintFunc } from '../utils';
 
-export type FunctionCollection = Dictionary<IFunction, string>;
+export type FunctionCollection = Dictionary<IFunction<any>, string>;
 export type RuleCollection = Dictionary<IRule, string>;
 export type PartialRuleCollection = Dictionary<Partial<IRule>, string>;
 export type RunRuleCollection = Dictionary<Rule, string>;


Test patch:
diff --git a/src/__tests__/linter.jest.test.ts b/src/__tests__/linter.jest.test.ts
index 181a08c0..7d4a779c 100644
--- a/src/__tests__/linter.jest.test.ts
+++ b/src/__tests__/linter.jest.test.ts
@@ -10,7 +10,7 @@ import { httpAndFileResolver } from '../resolvers/http-and-file';
 import { readRuleset } from '../rulesets';
 import { setFunctionContext } from '../rulesets/evaluators';
 import oasDocumentSchema from '../rulesets/oas/functions/oasDocumentSchema';
-import { Spectral } from '../spectral';
+import { IFunctionResult, Spectral } from '../spectral';
 import { IRuleset, RulesetExceptionCollection } from '../types/ruleset';
 
 const customFunctionOASRuleset = path.join(__dirname, './__fixtures__/custom-functions-oas-ruleset.json');
@@ -257,7 +257,7 @@ console.log(this.cache.get('test') || this.cache.set('test', []).get('test'));
       it('should handle basic example', async () => {
         spectral.setFunctions({
           [fnName]() {
-            return new Promise(resolve => {
+            return new Promise<IFunctionResult[]>(resolve => {
               setTimeout(resolve, 200, [
                 {
                   message: 'Error reported by async fn',
@@ -289,7 +289,7 @@ console.log(this.cache.get('test') || this.cache.set('test', []).get('test'));
       it('should handle rejections', async () => {
         spectral.setFunctions({
           [fnName]() {
-            return new Promise((resolve, reject) => {
+            return new Promise<void>((resolve, reject) => {
               setTimeout(reject, 1000, new Error('Some unknown error'));
             });
           },
diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts
index f003808b..571280d9 100644
--- a/src/__tests__/linter.test.ts
+++ b/src/__tests__/linter.test.ts
@@ -60,7 +60,7 @@ describe('linter', () => {
     const message = '4xx responses require a description';
 
     spectral.setFunctions({
-      func1: val => {
+      func1: (val: unknown) => {
         if (!val) {
           return [
             {
diff --git a/src/functions/__tests__/casing.test.ts b/src/functions/__tests__/casing.test.ts
index dd6e32de..b5a774f1 100644
--- a/src/functions/__tests__/casing.test.ts
+++ b/src/functions/__tests__/casing.test.ts
@@ -1,4 +1,3 @@
-import { AssertionError } from 'assert';
 import { casing, CasingType } from '../casing';
 
 function runCasing(target: unknown, type: CasingType, disallowDigits?: boolean, separator?: any) {
@@ -20,10 +19,6 @@ describe('casing', () => {
     expect(runCasing('', CasingType.camel)).toBeUndefined();
   });
 
-  test('given unknown case type should return nothing', () => {
-    expect(() => runCasing('2', 'foo' as any)).toThrowError(AssertionError);
-  });
-
   describe('casing type', () => {
     describe('flat', () => {
       const invalid = ['foo_test', 'Foo', '123', '1d', 'foo-bar'];
@@ -220,39 +215,6 @@ describe('casing', () => {
   });
 
   describe('casing with custom separator configuration', () => {
-    describe('parameters validation', () => {
-      const casingTypes = Object.keys(CasingType);
-
-      test.each([...casingTypes])(
-        'with "%s" type - throws when allowLeadingSeparator option is used without specifying separator',
-        type => {
-          expect(() =>
-            runCasing('irrelevant value', CasingType[type], undefined, { char: undefined, allowLeading: true }),
-          ).toThrow(AssertionError);
-        },
-      );
-
-      const invalidSepLengthCombinations = Object.values(casingTypes).reduce<Array<[string, number]>>(
-        (combinations, type) => {
-          for (const len of [0, 2, 3, 17]) {
-            combinations.push([type, len]);
-          }
-
-          return combinations;
-        },
-        [],
-      );
-
-      test.each(invalidSepLengthCombinations)(
-        'with type "%s" - throws when separator value is "%i" (not exactly one char long)',
-        (type, len) => {
-          expect(() => runCasing('irrelevant value', CasingType[type], undefined, { char: '-'.repeat(len) })).toThrow(
-            AssertionError,
-          );
-        },
-      );
-    });
-
     const baseData: Array<[string, string, string, string]> = [[CasingType.flat, 'flat', 'flat01', 'Nope']];
 
     const testCases: Array<[string, boolean, string, boolean, string, string, string]> = [];
diff --git a/src/functions/__tests__/schema.test.ts b/src/functions/__tests__/schema.test.ts
index 418864d3..e75a15ae 100644
--- a/src/functions/__tests__/schema.test.ts
+++ b/src/functions/__tests__/schema.test.ts
@@ -2,7 +2,7 @@ import { Optional } from '@stoplight/types';
 import { JSONSchema4, JSONSchema6 } from 'json-schema';
 import { schema } from '../schema';
 
-function runSchema(target: any, schemaObj: object, oasVersion?: Optional<number>) {
+function runSchema(target: any, schemaObj: object, oasVersion?: Optional<2 | 3 | 3.1>) {
   return schema(target, { schema: schemaObj, oasVersion }, { given: [] }, { given: null, original: null } as any);
 }
 
diff --git a/src/rulesets/__tests__/validation.test.ts b/src/rulesets/__tests__/validation.test.ts
index cde59420..3e3a051e 100644
--- a/src/rulesets/__tests__/validation.test.ts
+++ b/src/rulesets/__tests__/validation.test.ts
@@ -256,6 +256,777 @@ describe('Ruleset Validation', () => {
       }).toThrow(ValidationError);
     });
   });
+
+  describe('then validation', () => {
+    describe.each([
+      'casing',
+      'enumeration',
+      'length',
+      'pattern',
+      'schema',
+      'schema-path',
+      'unreferencedReusableObject',
+      'xor',
+    ])('%s function', name => {
+      it('complains about empty options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: name,
+                  functionOptions: {},
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about missing options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: name,
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about nullified options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: name,
+                  functionOptions: null,
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+
+    describe.each(['truthy', 'falsy', 'undefined'])('%s function', name => {
+      it('given valid then, does not complain', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: name,
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  field: 'test',
+                  function: name,
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it('complains about the presence of functionOptions', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: name,
+                  functionOptions: {},
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+
+    describe('alphabetical function', () => {
+      it('given valid then, does not complain', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'alphabetical',
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  field: 'test',
+                  function: 'alphabetical',
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  field: 'test',
+                  function: 'alphabetical',
+                  functionOptions: {
+                    keyedBy: 'bar',
+                  },
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  field: 'test',
+                  function: 'alphabetical',
+                  functionOptions: null,
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it('does not complain about empty functionOptions', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'alphabetical',
+                  functionOptions: {},
+                },
+              },
+            },
+          }),
+        ).not.toThrow(ValidationError);
+      });
+
+      it('complains about extra options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'alphabetical',
+                  functionOptions: {
+                    foo: true,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about invalid keyedBy', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'alphabetical',
+                  functionOptions: {
+                    keyedBy: 2,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+
+    describe('casing function', () => {
+      it.each([
+        { type: 'cobol' },
+        { type: 'macro', disallowDigits: true },
+        { type: 'snake', disallowDigits: true, separator: { char: 'a' } },
+        { type: 'pascal', disallowDigits: false, separator: { char: 'b', allowLeading: true } },
+      ])('given valid then %s', functionOptions => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'casing',
+                  functionOptions,
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it.each([
+        { type: 'macro', foo: true },
+        { type: 'snake', separator: { char: 'a', foo: true } },
+      ])('complains about extra options %s', functionOptions => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'casing',
+                  functionOptions,
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about invalid type', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'casing',
+                  functionOptions: {
+                    type: 'foo',
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about missing char', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'casing',
+                  functionOptions: {
+                    type: 'pascal',
+                    disallowDigits: false,
+                    separator: {},
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'casing',
+                  functionOptions: {
+                    type: 'pascal',
+                    disallowDigits: false,
+                    separator: { allowLeading: true },
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about too length char', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'casing',
+                  functionOptions: {
+                    type: 'pascal',
+                    separator: {
+                      char: 'fo',
+                    },
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about too invalid char', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'casing',
+                  functionOptions: {
+                    type: 'pascal',
+                    separator: {
+                      char: null,
+                    },
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+
+    describe('enumeration function', () => {
+      it('given valid then, does not complain', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'enumeration',
+                  functionOptions: {
+                    values: ['foo', 2],
+                  },
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it('complains about extra options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'enumeration',
+                  functionOptions: {
+                    values: ['foo', 2],
+                    foo: true,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it.each([[null], 2, null])('complains about invalid values %s', functionOptions => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'enumeration',
+                  functionOptions,
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+
+    describe('length function', () => {
+      it.each([{ min: 2 }, { max: 4 }, { min: 2, max: 4 }])('given valid then %s', functionOptions => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'length',
+                  functionOptions,
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it('complains about extra options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'length',
+                  functionOptions: {
+                    min: 2,
+                    foo: true,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it.each([{ min: '2' }, { max: '2' }, { min: '4', max: '2' }])(
+        'complains about invalid options %s',
+        functionOptions => {
+          expect(
+            assertValidRuleset.bind(null, {
+              rules: {
+                rule: {
+                  given: '$',
+                  then: {
+                    function: 'length',
+                    functionOptions,
+                  },
+                },
+              },
+            }),
+          ).toThrow(ValidationError);
+        },
+      );
+    });
+
+    describe('pattern function', () => {
+      it.each([{ match: 'foo' }, { notMatch: 'foo' }, { match: 'foo', notMatch: 'bar' }])(
+        'given valid then %s',
+        functionOptions => {
+          expect(
+            assertValidRuleset.bind(null, {
+              rules: {
+                rule: {
+                  given: '$',
+                  then: {
+                    function: 'pattern',
+                    functionOptions,
+                  },
+                },
+              },
+            }),
+          ).not.toThrow();
+        },
+      );
+
+      it('complains about extra options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'pattern',
+                  functionOptions: {
+                    match: 'foo',
+                    foo: true,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it.each([{ match: 2 }, { notMatch: null }, { match: 4, notMatch: 'bar' }])(
+        'complains about invalid options %s',
+        functionOptions => {
+          expect(
+            assertValidRuleset.bind(null, {
+              rules: {
+                rule: {
+                  given: '$',
+                  then: {
+                    function: 'pattern',
+                    functionOptions,
+                  },
+                },
+              },
+            }),
+          ).toThrow(ValidationError);
+        },
+      );
+    });
+
+    describe('schema function', () => {
+      it.each([
+        { schema: { type: 'object' } },
+        { schema: { type: 'string' }, oasVersion: 2 },
+        { schema: { type: 'string' }, allErrors: true },
+        { schema: { type: 'string' }, oasVersion: 3.1, allErrors: false },
+      ])('given valid then %s', functionOptions => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'schema',
+                  functionOptions,
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it('complains about extra options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'schema',
+                  functionOptions: {
+                    schema: { type: 'object' },
+                    foo: true,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it.each([
+        { schema: { type: 'object' }, oasVersion: 1 },
+        { schema: { type: 'object' }, allErrors: null },
+        { schema: null, allErrors: null },
+      ])('complains about invalid options %s', functionOptions => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'schema',
+                  functionOptions,
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+
+    describe('unreferencedReusableObject function', () => {
+      it('given valid then, does not complain', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'unreferencedReusableObject',
+                  functionOptions: {
+                    reusableObjectsLocation: 'foo',
+                  },
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it('complains about extra options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'unreferencedReusableObject',
+                  functionOptions: {
+                    reusableObjectsLocation: 'foo',
+                    foo: true,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about invalid options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'unreferencedReusableObject',
+                  functionOptions: {
+                    reusableObjectsLocation: 2,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+
+    describe('xor function', () => {
+      it('given valid then, does not complain', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'xor',
+                  functionOptions: {
+                    properties: ['foo', 'bar'],
+                  },
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it('complains about extra options', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'xor',
+                  functionOptions: {
+                    properties: ['foo', 2],
+                    foo: true,
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it('complains about too few properties', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'xor',
+                  functionOptions: {
+                    properties: ['foo'],
+                  },
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+
+      it.each([[null], 2, null])('complains about invalid properties %s', functionOptions => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'xor',
+                  functionOptions,
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+
+    describe('custom function', () => {
+      it('given valid then, does not complain', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'foo',
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  field: 'test',
+                  function: 'foo',
+                },
+              },
+            },
+          }),
+        ).not.toThrow();
+      });
+
+      it('complains about non-object functionOptions', () => {
+        expect(
+          assertValidRuleset.bind(null, {
+            rules: {
+              rule: {
+                given: '$',
+                then: {
+                  function: 'foo',
+                  functionOptions: true,
+                },
+              },
+            },
+          }),
+        ).toThrow(ValidationError);
+      });
+    });
+  });
 });
 
 describe('Function Validation', () => {
diff --git a/src/rulesets/asyncapi/functions/__tests__/asyncApi2PayloadValidation.test.ts b/src/rulesets/asyncapi/functions/__tests__/asyncApi2PayloadValidation.test.ts
index 97383744..c86d68a4 100644
--- a/src/rulesets/asyncapi/functions/__tests__/asyncApi2PayloadValidation.test.ts
+++ b/src/rulesets/asyncapi/functions/__tests__/asyncApi2PayloadValidation.test.ts
@@ -1,11 +1,11 @@
 import { functions } from '../../../../functions';
 import { asyncApi2PayloadValidation } from '../asyncApi2PayloadValidation';
 
-function runPayloadValidation(targetVal: any, field: string) {
+function runPayloadValidation(targetVal: any) {
   return asyncApi2PayloadValidation.call(
     { functions },
     targetVal,
-    { field },
+    null,
     { given: ['$', 'components', 'messages', 'aMessage'] },
     { given: null, original: null, documentInventory: {} as any },
   );
@@ -18,7 +18,7 @@ describe('asyncApi2PayloadValidation', () => {
       deprecated: 14,
     };
 
-    const results = runPayloadValidation(payload, '$');
+    const results = runPayloadValidation(payload);
 
     expect(results).toEqual([
       {
diff --git a/src/rulesets/oas/functions/__tests__/typedEnum.test.ts b/src/rulesets/oas/functions/__tests__/typedEnum.test.ts
index 4155c44a..22168bdb 100644
--- a/src/rulesets/oas/functions/__tests__/typedEnum.test.ts
+++ b/src/rulesets/oas/functions/__tests__/typedEnum.test.ts
@@ -1,13 +1,11 @@
 import { functions } from '../../../../functions';
 import { typedEnum } from '../typedEnum';
 
-const defaultReportingThreshold = 3;
-
-function runTypedEnum(targetVal: any, reportingThreshold: any) {
+function runTypedEnum(targetVal: any) {
   return typedEnum.call(
     { functions },
     targetVal,
-    { reportingThreshold },
+    null,
     { given: ['$'] },
     { given: null, original: null, documentInventory: {} as any },
   );
@@ -21,7 +19,7 @@ describe('typedEnum', () => {
         enum: enumContent,
       };
 
-      expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
+      expect(runTypedEnum(schema)).toBeUndefined();
     });
   });
 
@@ -31,7 +29,7 @@ describe('typedEnum', () => {
       enum: [],
     };
 
-    expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
+    expect(runTypedEnum(schema)).toBeUndefined();
   });
 
   describe('basic', () => {
@@ -41,7 +39,7 @@ describe('typedEnum', () => {
         enum: [123, 456],
       };
 
-      expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
+      expect(runTypedEnum(schema)).toBeUndefined();
     });
 
     test('is undefined when all enum values respect the type', () => {
@@ -50,7 +48,7 @@ describe('typedEnum', () => {
         enum: [123, 456],
       };
 
-      expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
+      expect(runTypedEnum(schema)).toBeUndefined();
     });
 
     test.each([undefined])('is undefined when type is "%s"', (typeValue: unknown) => {
@@ -59,7 +57,7 @@ describe('typedEnum', () => {
         enum: [123, 456],
       };
 
-      expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
+      expect(runTypedEnum(schema)).toBeUndefined();
     });
 
     test('identifies enum values which do not respect the type', () => {
@@ -68,7 +66,7 @@ describe('typedEnum', () => {
         enum: [123, 'a string!', 456, 'and another one!'],
       };
 
-      expect(runTypedEnum(schema, defaultReportingThreshold)).toEqual([
+      expect(runTypedEnum(schema)).toEqual([
         {
           message: 'Enum value `a string!` does not respect the specified type `integer`.',
           path: ['$', 'enum', 1],
@@ -97,7 +95,7 @@ describe('typedEnum', () => {
           enum: valids,
         };
 
-        expect(runTypedEnum(schema, defaultReportingThreshold)).toBeUndefined();
+        expect(runTypedEnum(schema)).toBeUndefined();
       },
     );
 
@@ -109,7 +107,7 @@ describe('typedEnum', () => {
           enum: [valids[0], invalid],
         };
 
-        const results = runTypedEnum(schema, defaultReportingThreshold);
+        const results = runTypedEnum(schema);
 
         expect(results[0].message).toContain(`value \`${invalid}\``);
       },