Repository: stoplightio/spectral
Pull request #473 (2019-08-21 20:27:38)

feat: $ref cannot have siblings
Closes https://github.com/stoplightio/spectral/issues/394

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No


It's not a rule, as rules have no access to original (unresolved) data.
I don't know if it's a big deal or not, but if we do need a rule, I can make it a rule.
The code is placed in a wrong place and an extra test wouldn't hurt, so it's still a WIP.

P0lip: @philsturgeon 
> It's not a rule, as rules have no access to original (unresolved) data.
> I don't know if it's a big deal or not, but if we do need a rule, I can make it a rule.

How do you feel about not making it a rule and failing all the time same as invalid-ref does?
P0lip: @philsturgeon got it, will make it a rule then.
P0lip: @philsturgeon 
I'm planning to add an extra property to a rule named `unresolved` or something, which would make each rule operate on initial data
P0lip: Hold on with the review, forgot to add one test.

Patch:
diff --git a/docs/getting-started/rulesets.md b/docs/getting-started/rulesets.md
index 9f2d0a4f..f8f530ee 100644
--- a/docs/getting-started/rulesets.md
+++ b/docs/getting-started/rulesets.md
@@ -40,6 +40,24 @@ While running it with this object, it will succeed:
 }
 ```
 
+By default, Spectral processes each rule on resolved document with all $refs resolved.
+If you would like to have an original input supplied to your rule, you can place `resolved` property as follows:
+
+```yaml
+rules:
+  my-rule-name:
+    description: Tags must have a description.
+    given: $.tags[*]
+    severity: error
+    resolved: false # note - if not specified or true, a resolved document will be given
+    then:
+      field: description
+      function: truthy
+```
+
+In most cases, you will want to operate on resolved document and therefore won't specify that property.
+You might find `resolved` useful if your rule requires access to $refs.
+
 ## Formats
 
 Formats are an optional way to specify which API description formats a rule, or ruleset, is applicable to. Currently Spectral supports these two formats:
diff --git a/rollup.config.js b/rollup.config.js
index 05609d35..44db745e 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -7,12 +7,13 @@ import { terser } from 'rollup-plugin-terser';
 const BASE_PATH = process.cwd();
 
 module.exports = [
-  "oasOp2xxResponse",
-  "oasOpFormDataConsumeCheck",
-  "oasOpIdUnique",
-  "oasOpParams",
-  "oasOpSecurityDefined",
-  "oasPathParam"
+  'oasOp2xxResponse',
+  'oasOpFormDataConsumeCheck',
+  'oasOpIdUnique',
+  'oasOpParams',
+  'oasOpSecurityDefined',
+  'oasPathParam',
+  'refSiblings'
 ].map(fn => ({
   input: path.resolve(BASE_PATH, 'dist/rulesets/oas/functions', `${fn}.js`),
   plugins: [
@@ -20,9 +21,7 @@ module.exports = [
       tsconfig: path.join(BASE_PATH, './tsconfig.rollup.json'),
       include: ['dist/**/*.{ts,tsx}'],
     }),
-    resolve({
-      only: ['json-schema-merge-allof', /lodash\/?.*/],
-    }),
+    resolve(),
     commonjs(),
     terser(),
   ],
diff --git a/src/linter.ts b/src/linter.ts
index 9724150a..ff738866 100644
--- a/src/linter.ts
+++ b/src/linter.ts
@@ -67,7 +67,7 @@ export const lintNode = (
   }
 
   if (!targets.length) {
-    // must call then at least once, with no result
+    // must call then at least once, with no resolved
     targets.push({
       path: [],
       value: undefined,
@@ -96,7 +96,10 @@ export const lintNode = (
     results = results.concat(
       targetResults.map<IRuleResult>(result => {
         const escapedJsonPath = (result.path || targetPath).map(segment => decodePointerFragment(String(segment)));
-        const path = getRealJsonPath(resolved.result, escapedJsonPath);
+        const path = getRealJsonPath(
+          rule.resolved === false ? resolved.unresolved : resolved.resolved,
+          escapedJsonPath,
+        );
         const location = resolved.getLocationForJsonPath(path, true);
 
         return {
diff --git a/src/meta/rule.schema.json b/src/meta/rule.schema.json
index b6d03f6b..5963e09d 100644
--- a/src/meta/rule.schema.json
+++ b/src/meta/rule.schema.json
@@ -59,69 +59,72 @@
     "oneOf": [
         {
             "properties": {
-        "description": {
-            "type": "string"
-        },
-        "recommended": {
-            "type": "boolean"
-        },
-        "given": {
-            "type": "string"
-        },
-        "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/IThen<T,O>"
+                "description": {
+                    "type": "string"
                 },
-                {
+                "recommended": {
+                    "type": "boolean"
+                },
+                "given": {
+                    "type": "string"
+                },
+                "resolved": {
+                    "type": "boolean"
+                },
+                "severity": {
+                    "$ref": "#/definitions/Severity"
+                },
+                "message": {
+                    "type": "string"
+                },
+                "tags": {
                     "items": {
-                        "$ref": "#/definitions/IThen<T,O>"
+                        "type": "string"
                     },
                     "type": "array"
-                }
-            ]
-        },
-        "type": {
-            "enum": [
-                "style",
-                "validation"
-            ],
-            "type": "string"
-        },
-        "when": {
-            "properties": {
-                "field": {
-                    "type": "string"
                 },
-                "pattern": {
+                "formats": {
+                    "type": "array",
+                    "items": {
+                        "type": "string",
+                        "minItems": 1
+                    }
+                },
+                "then": {
+                    "anyOf": [
+                        {
+                            "$ref": "#/definitions/IThen<T,O>"
+                        },
+                        {
+                            "items": {
+                                "$ref": "#/definitions/IThen<T,O>"
+                            },
+                            "type": "array"
+                        }
+                    ]
+                },
+                "type": {
+                    "enum": [
+                        "style",
+                        "validation"
+                    ],
                     "type": "string"
+                },
+                "when": {
+                    "properties": {
+                        "field": {
+                            "type": "string"
+                        },
+                        "pattern": {
+                            "type": "string"
+                        }
+                    },
+                    "required": [
+                        "field"
+                    ],
+                    "type": "object"
                 }
             },
-            "required": [
-                "field"
-            ],
-            "type": "object"
-        }
-    },
             "required": [
                 "given",
                 "then"
diff --git a/src/resolved.ts b/src/resolved.ts
index 21e8d90c..98106d29 100644
--- a/src/resolved.ts
+++ b/src/resolved.ts
@@ -1,22 +1,22 @@
-import { IResolveError, IResolveResult, IResolveRunner } from '@stoplight/json-ref-resolver/types';
+import { IResolveError, IResolveResult } from '@stoplight/json-ref-resolver/types';
 import { Dictionary, ILocation, JsonPath } from '@stoplight/types';
 import { Segment } from '@stoplight/types/dist';
 import { get } from 'lodash';
 import { IParseMap, REF_METADATA } from './spectral';
 import { IParsedResult } from './types';
 
-export class Resolved implements IResolveResult {
+export class Resolved {
   public refMap: Dictionary<string>;
-  public result: IResolveResult;
+  public resolved: unknown;
+  public unresolved: unknown;
   public errors: IResolveError[];
-  public runner: IResolveRunner;
   public format?: string | null;
 
-  constructor(public spec: IParsedResult, result: IResolveResult, public parsedMap: IParseMap) {
-    this.refMap = result.refMap;
-    this.result = result.result;
-    this.errors = result.errors;
-    this.runner = result.runner;
+  constructor(public spec: IParsedResult, resolveResult: IResolveResult, public parsedMap: IParseMap) {
+    this.refMap = resolveResult.refMap;
+    this.resolved = resolveResult.result;
+    this.unresolved = spec.parsed.data;
+    this.errors = resolveResult.errors;
     this.format = spec.format;
   }
 
diff --git a/src/rulesets/oas/functions/refSiblings.ts b/src/rulesets/oas/functions/refSiblings.ts
new file mode 100644
index 00000000..7527629f
--- /dev/null
+++ b/src/rulesets/oas/functions/refSiblings.ts
@@ -0,0 +1,35 @@
+import { JsonPath } from '@stoplight/types';
+import { IFunction, IFunctionResult } from '../../../types';
+
+// function is needed because `$..$ref` or `$..[?(@.$ref)]` are not parsed correctly
+// and therefore lead to infinite recursion due to the dollar sign ('$' in '$ref')
+function* siblingIterator(obj: object, path: JsonPath): IterableIterator<JsonPath> {
+  const hasRef = '$ref' in obj;
+  for (const key in obj) {
+    if (!Object.hasOwnProperty.call(obj, key)) continue;
+    const scopedPath = [...path, key];
+    if (hasRef && key !== '$ref') {
+      yield scopedPath;
+    }
+
+    if (key !== '$ref' && typeof obj[key] === 'object' && obj[key] !== null) {
+      yield* siblingIterator(obj[key], scopedPath);
+    }
+  }
+}
+
+const refSiblings: IFunction = (data: unknown) => {
+  const results: IFunctionResult[] = [];
+  if (typeof data !== 'object' || data === null) return results;
+
+  for (const path of siblingIterator(data, [])) {
+    results.push({
+      message: '$ref cannot be placed next to any other properties',
+      path,
+    });
+  }
+
+  return results;
+};
+
+export default refSiblings;
diff --git a/src/rulesets/oas/index.json b/src/rulesets/oas/index.json
index 0e797c7b..3542dcd4 100644
--- a/src/rulesets/oas/index.json
+++ b/src/rulesets/oas/index.json
@@ -7,7 +7,8 @@
     "oasOpIdUnique",
     "oasOpParams",
     "oasOpSecurityDefined",
-    "oasPathParam"
+    "oasPathParam",
+    "refSiblings"
   ],
   "rules": {
     "operation-2xx-response": {
@@ -428,6 +429,18 @@
           "schemaPath": "$"
         }
       }
+    },
+    "no-$ref-siblings": {
+      "description": "Property cannot be placed among $ref",
+      "message": "{{error}}",
+      "type": "validation",
+      "severity": 0,
+      "recommended": true,
+      "resolved": false,
+      "given": "$",
+      "then": {
+        "function": "refSiblings"
+      }
     }
   }
 }
diff --git a/src/rulesets/oas/index.ts b/src/rulesets/oas/index.ts
index 63c18d01..f2cd16fa 100644
--- a/src/rulesets/oas/index.ts
+++ b/src/rulesets/oas/index.ts
@@ -10,6 +10,7 @@ export const commonOasFunctions = (): FunctionCollection => {
     oasOpIdUnique: require('./functions/oasOpIdUnique').oasOpIdUnique,
     oasOpFormDataConsumeCheck: require('./functions/oasOpFormDataConsumeCheck').oasOpFormDataConsumeCheck,
     oasOpParams: require('./functions/oasOpParams').oasOpParams,
+    refSiblings: require('./functions/refSiblings').refSiblings,
   };
 };
 
diff --git a/src/runner.ts b/src/runner.ts
index 48669f5b..941405f8 100644
--- a/src/runner.ts
+++ b/src/runner.ts
@@ -40,9 +40,9 @@ export const runRules = (
 };
 
 const runRule = (resolved: Resolved, rule: IRunRule, functions: FunctionCollection): IRuleResult[] => {
-  const { result: target } = resolved;
+  const target = rule.resolved === false ? resolved.unresolved : resolved.resolved;
 
-  let results: IRuleResult[] = [];
+  const results: IRuleResult[] = [];
   const nodes: IGivenNode[] = [];
 
   // don't have to spend time running jsonpath if given is $ - can just use the root object
@@ -79,7 +79,7 @@ const runRule = (resolved: Resolved, rule: IRunRule, functions: FunctionCollecti
           continue;
         }
 
-        results = results.concat(lintNode(node, rule, then, func, resolved));
+        results.push(...lintNode(node, rule, then, func, resolved));
       }
     } catch (e) {
       console.warn(`Encountered error when running rule '${rule.name}' on node at path '${node.path}':\n${e}`);
diff --git a/src/spectral.ts b/src/spectral.ts
index 2391dd79..cefe0b80 100644
--- a/src/spectral.ts
+++ b/src/spectral.ts
@@ -1,7 +1,8 @@
-import { JsonParserResult, safeStringify } from '@stoplight/json';
 import {
   getLocationForJsonPath as getLocationForJsonPathJSON,
+  JsonParserResult,
   parseWithPointers as parseJSONWithPointers,
+  safeStringify,
 } from '@stoplight/json';
 import { Cache, Resolver } from '@stoplight/json-ref-resolver';
 import { IUriParser } from '@stoplight/json-ref-resolver/types';
@@ -14,6 +15,7 @@ import {
 } from '@stoplight/yaml';
 import { merge, set } from 'lodash';
 
+import { IDiagnostic } from '@stoplight/types/dist';
 import deprecated from 'deprecated-decorator';
 import { formatParserDiagnostics, formatResolverErrors } from './error-messages';
 import { functions as defaultFunctions } from './functions';
@@ -83,48 +85,13 @@ export class Spectral {
       await this._resolver.resolve(parsedResult.parsed.data, {
         uriCache: this._uriCache,
         baseUri: documentUri,
-        parseResolveResult: async resolveOpts => {
-          const ref = resolveOpts.targetAuthority.toString();
-          const ext = extname(ref);
-
-          const content = String(resolveOpts.result);
-          let parsedRefResult:
-            | IParsedResult<YamlParserResult<unknown>>
-            | IParsedResult<JsonParserResult<unknown>>
-            | undefined;
-          if (ext === '.yml' || ext === '.yaml') {
-            parsedRefResult = {
-              parsed: parseYAMLWithPointers(content, { ignoreDuplicateKeys: false }),
-              source: ref,
-              getLocationForJsonPath: getLocationForJsonPathYAML,
-            };
-          } else if (ext === '.json') {
-            parsedRefResult = {
-              parsed: parseJSONWithPointers(content, { ignoreDuplicateKeys: false }),
-              source: ref,
-              getLocationForJsonPath: getLocationForJsonPathJSON,
-            };
-          }
-
-          if (parsedRefResult !== undefined) {
-            resolveOpts.result = parsedRefResult.parsed.data;
-            if (parsedRefResult.parsed.diagnostics.length > 0) {
-              refDiagnostics.push(
-                ...formatParserDiagnostics(parsedRefResult.parsed.diagnostics, parsedRefResult.source),
-              );
-            }
-
-            this._processExternalRef(parsedRefResult, resolveOpts);
-          }
-
-          return resolveOpts;
-        },
+        parseResolveResult: this._parseResolveResult(refDiagnostics),
       }),
       this._parsedMap,
     );
 
     if (resolved.format === void 0) {
-      const foundFormat = Object.keys(this.formats).find(format => this.formats[format](resolved.result));
+      const foundFormat = Object.keys(this.formats).find(format => this.formats[format](resolved.resolved));
       resolved.format = foundFormat === void 0 ? null : foundFormat;
     }
 
@@ -136,7 +103,7 @@ export class Spectral {
     ];
 
     return {
-      resolved: resolved.result,
+      resolved: resolved.resolved,
       results: validationResults,
     };
   }
@@ -260,6 +227,41 @@ export class Spectral {
       }),
     );
   }
+
+  private _parseResolveResult = (refDiagnostics: IDiagnostic[]) => async (resolveOpts: IUriParser) => {
+    const ref = resolveOpts.targetAuthority.toString();
+    const ext = extname(ref);
+
+    const content = String(resolveOpts.result);
+    let parsedRefResult:
+      | IParsedResult<YamlParserResult<unknown>>
+      | IParsedResult<JsonParserResult<unknown>>
+      | undefined;
+    if (ext === '.yml' || ext === '.yaml') {
+      parsedRefResult = {
+        parsed: parseYAMLWithPointers(content, { ignoreDuplicateKeys: false }),
+        source: ref,
+        getLocationForJsonPath: getLocationForJsonPathYAML,
+      };
+    } else if (ext === '.json') {
+      parsedRefResult = {
+        parsed: parseJSONWithPointers(content, { ignoreDuplicateKeys: false }),
+        source: ref,
+        getLocationForJsonPath: getLocationForJsonPathJSON,
+      };
+    }
+
+    if (parsedRefResult !== undefined) {
+      resolveOpts.result = parsedRefResult.parsed.data;
+      if (parsedRefResult.parsed.diagnostics.length > 0) {
+        refDiagnostics.push(...formatParserDiagnostics(parsedRefResult.parsed.diagnostics, parsedRefResult.source));
+      }
+
+      this._processExternalRef(parsedRefResult, resolveOpts);
+    }
+
+    return resolveOpts;
+  };
 }
 
 export const REF_METADATA = Symbol('external_ref_metadata');
diff --git a/src/types/rule.ts b/src/types/rule.ts
index 33191042..6064de80 100644
--- a/src/types/rule.ts
+++ b/src/types/rule.ts
@@ -26,6 +26,10 @@ export interface IRule<T = string, O = any> {
   // Filter the target down to a subset[] with a JSON path
   given: string;
 
+  // If false, rule will operate on original (unresolved) data
+  // If undefined or true, resolved data will be supplied
+  resolved?: boolean;
+
   when?: {
     // the `path.to.prop` to field, or special `@key` value to target keys for matched `given` object
     // EXAMPLE: if the target object is an oas object and given = `$..responses[*]`, then `@key` would be the response code (200, 400, etc)
diff --git a/src/types/spectral.ts b/src/types/spectral.ts
index 548975ab..c85ac6fe 100644
--- a/src/types/spectral.ts
+++ b/src/types/spectral.ts
@@ -1,5 +1,4 @@
 import { Resolver } from '@stoplight/json-ref-resolver';
-import { IResolveResult } from '@stoplight/json-ref-resolver/types';
 import {
   DiagnosticSeverity,
   Dictionary,
@@ -44,7 +43,7 @@ export interface IRuleResult extends IDiagnostic {
 }
 
 export interface ISpectralFullResult {
-  resolved: IResolveResult;
+  resolved: unknown;
   results: IRuleResult[];
 }
 
diff --git a/yarn.lock b/yarn.lock
index 94b15712..04078d16 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -604,21 +604,36 @@
   dependencies:
     any-observable "^0.3.0"
 
+"@stoplight/fast-safe-stringify@^2.1.2":
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@stoplight/fast-safe-stringify/-/fast-safe-stringify-2.1.2.tgz#2811eff4989c3d4c5c9a3789f057e25358db0899"
+  integrity sha512-ayw3qQ9KNn2K5q6ggrLclh+rdZsSLwYIUz3uAer1NzaCpalJLR4g32G845ylgq7Xy2AunMHnNfD3FC3s0rUcsA==
+
 "@stoplight/json-ref-resolver@^2.1.2":
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/@stoplight/json-ref-resolver/-/json-ref-resolver-2.2.0.tgz#f12fed6ca618d9cd6031f5d0405a3ae5ffd54fad"
-  integrity sha512-42z5MNkLKF7Ynq2rM8YSd6SoJvEX5k0ugQujpB8Co7K5m3vhq1LK+/faFhuMfuFy6As9BxVUnDbGchYERXK17Q==
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/@stoplight/json-ref-resolver/-/json-ref-resolver-2.1.2.tgz#56993d3245dba1264ff87052ee960fbe8d259c98"
+  integrity sha512-DaHxK2KI7HmnsI2BTMwdnsdmj65VNFFPTkvhg2FukszxUpcPlgYVbHjtzEigj4Jn4XryTexlWx7Id4fuRUM7Ug==
   dependencies:
-    "@stoplight/json" "^3.1.0"
+    "@stoplight/json" "^2.2.2"
     "@stoplight/types" "^11.0.0"
     dependency-graph "~0.8.0"
     fast-memoize "^2.5.1"
-    immer "^3.2.0"
-    lodash "^4.17.15"
+    immer "^3.1.3"
+    lodash "^4.17"
     urijs "~1.19.1"
-    vscode-uri "^2.0.3"
+    vscode-uri "^2.0.2"
 
-"@stoplight/json@^3.0.4", "@stoplight/json@^3.1.0":
+"@stoplight/json@^2.2.2":
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-2.3.3.tgz#7f461f7e9234a244af7d7e3c0d37fd66fc944eac"
+  integrity sha512-iGRT6N/uUnoXbaGAGlcJJNbI7zZity+vLb1GBUezLeuW1ZHUevxfqK1kvrxK3iZD/2slOPeH23mDHIk56nswSA==
+  dependencies:
+    "@stoplight/fast-safe-stringify" "^2.1.2"
+    "@stoplight/types" "^9.1.2"
+    jsonc-parser "~2.1.0"
+    lodash "^4.10"
+
+"@stoplight/json@^3.0.4":
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.1.0.tgz#a03bd8d065a62eb00cd42cf003df431f23d7a5dd"
   integrity sha512-8Fev+RW+UJNYtcpDgvQPH6MuHEf5cr95KnAyMBNNXlVIeRAIJqYK3r5FWZv4/KFH08ozQy4QpMM9MdsR8J3cZQ==
@@ -640,6 +655,13 @@
   dependencies:
     "@types/json-schema" "^7.0.3"
 
+"@stoplight/types@^9.1.2":
+  version "9.1.2"
+  resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-9.1.2.tgz#0b2ed2032ae901491543b9dd19309ff3a0acf12a"
+  integrity sha512-eNeWwTZ1E47mrprEdgtOutovVKxYTbm8k/B7Lh4JAusGoiHbMmzVT+IDdayBpOCzq+qKLzuP0z3IZo74IXaaEg==
+  dependencies:
+    "@types/json-schema" "^7.0.3"
+
 "@stoplight/yaml@^3.0.0":
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/@stoplight/yaml/-/yaml-3.0.2.tgz#acd567d580b35c7b2b8c9802a448ffcc70698507"
@@ -3544,10 +3566,10 @@ ignore@^5.1.1:
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558"
   integrity sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==
 
-immer@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/immer/-/immer-3.2.0.tgz#53686471e9dd2b070e0fb5500c6fdecd3a99375f"
-  integrity sha512-+a2R8z9eELHst6aht++nzVzJ8LJ+Hsg49qttfg9Kc/vmoxEdPXw5/rV6+4DYWGgnq+B36KbLr4OTaGtS9mDjtg==
+immer@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/immer/-/immer-3.1.3.tgz#59bc742b2aab6e2c676445edb653e588a23c70fc"
+  integrity sha512-HG5SXTXTTVy9lGNwS075cNhQoV375jHsIJO3UtMjuUWJOuwlMr0u42FlsKTJcppt5AzsFAsmj9r4kHvsSHh3hQ==
 
 import-fresh@^2.0.0:
   version "2.0.0"
@@ -4565,7 +4587,7 @@ json5@2.x, json5@^2.1.0:
   dependencies:
     minimist "^1.2.0"
 
-jsonc-parser@~2.1.1:
+jsonc-parser@~2.1.0, jsonc-parser@~2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.1.1.tgz#83dc3d7a6e7186346b889b1280eefa04446c6d3e"
   integrity sha512-VC0CjnWJylKB1iov4u76/W/5Ef0ydDkjtYWxoZ9t3HdWlSnZQwZL5MgFikaB/EtQ4RmMEw3tmQzuYnZA2/Ja1g==
@@ -4923,7 +4945,7 @@ lodash@4.17.14:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
   integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
 
-lodash@>=4.17.5, lodash@^4.17.0, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.5, lodash@^4.2.1:
+lodash@>=4.17.5, lodash@^4.10, lodash@^4.17, lodash@^4.17.0, lodash@^4.17.11, lodash@^4.17.15, lodash@^4.17.5, lodash@^4.2.1:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@@ -7830,7 +7852,7 @@ void-elements@^2.0.0:
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
   integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
 
-vscode-uri@^2.0.3:
+vscode-uri@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.0.3.tgz#25e5f37f552fbee3cec7e5f80cef8469cefc6543"
   integrity sha512-4D3DI3F4uRy09WNtDGD93H9q034OHImxiIcSq664Hq1Y1AScehlP3qqZyTkX/RWxeu0MRMHGkrxYqm2qlDF/aw==


Test patch:
diff --git a/src/__tests__/functions.test.ts b/src/__tests__/functions.test.ts
index a3fed72f..be4da725 100644
--- a/src/__tests__/functions.test.ts
+++ b/src/__tests__/functions.test.ts
@@ -12,7 +12,7 @@ const applyRuleToObject = async (r: Rule, o: object): Promise<IRuleResult[]> =>
 
 describe('functions', () => {
   describe('xor', () => {
-    test('returns result if no properties are present', async () => {
+    test('returns resolved if no properties are present', async () => {
       expect(
         applyRuleToObject(
           {
@@ -35,7 +35,7 @@ describe('functions', () => {
       ).resolves.toHaveLength(1);
     });
 
-    test('returns result if both properties are present', async () => {
+    test('returns resolved if both properties are present', async () => {
       expect(
         applyRuleToObject(
           {
@@ -106,7 +106,7 @@ describe('functions', () => {
       ).resolves.toHaveLength(1);
     });
 
-    test('returns result if pattern is not matched (on object keys)', async () => {
+    test('returns resolved if pattern is not matched (on object keys)', async () => {
       expect(
         applyRuleToObject(
           {
@@ -135,7 +135,7 @@ describe('functions', () => {
       ).resolves.toHaveLength(1);
     });
 
-    test('dont return result if pattern is matched (on string)', async () => {
+    test('dont return resolved if pattern is matched (on string)', async () => {
       expect(
         applyRuleToObject(
           {
@@ -157,7 +157,7 @@ describe('functions', () => {
       ).resolves.toHaveLength(0);
     });
 
-    test('dont return result if pattern is matched (on object keys)', async () => {
+    test('dont return resolved if pattern is matched (on object keys)', async () => {
       expect(
         applyRuleToObject(
           {
@@ -207,7 +207,7 @@ describe('functions', () => {
       },
     ];
 
-    test('return result if string, number, array, or object is greater than max', async () => {
+    test('return resolved if string, number, array, or object is greater than max', async () => {
       expect(
         applyRuleToObject(
           {
@@ -231,7 +231,7 @@ describe('functions', () => {
       ).resolves.toHaveLength(4);
     });
 
-    test('return result if string, number, array, or object is less than min', async () => {
+    test('return resolved if string, number, array, or object is less than min', async () => {
       expect(
         applyRuleToObject(
           {
@@ -255,7 +255,7 @@ describe('functions', () => {
       ).resolves.toHaveLength(4);
     });
 
-    test('dont return a result if string, number, array, or object is between min and max', async () => {
+    test('dont return a resolved if string, number, array, or object is between min and max', async () => {
       expect(
         applyRuleToObject(
           {
diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts
index 0cce0b9c..4d0bb51e 100644
--- a/src/__tests__/linter.test.ts
+++ b/src/__tests__/linter.test.ts
@@ -871,6 +871,81 @@ responses:: !!foo
     });
   });
 
+  test('should report ref siblings', async () => {
+    await spectral.loadRuleset('spectral:oas');
+
+    const results = await spectral.run({
+      $ref: '#/',
+      responses: {
+        200: {
+          description: 'a',
+        },
+        201: {
+          description: 'b',
+        },
+        300: {
+          description: 'c',
+          abc: 'd',
+          $ref: '#/d',
+        },
+      },
+      openapi: '3.0.0',
+    });
+
+    expect(results).toEqual(
+      expect.arrayContaining([
+        {
+          code: 'no-$ref-siblings',
+          message: '$ref cannot be placed next to any other properties',
+          path: ['responses'],
+          range: {
+            end: {
+              character: 19,
+              line: 12,
+            },
+            start: {
+              character: 14,
+              line: 2,
+            },
+          },
+          severity: DiagnosticSeverity.Error,
+        },
+        {
+          code: 'no-$ref-siblings',
+          message: '$ref cannot be placed next to any other properties',
+          path: ['responses', '300', 'description'],
+          range: {
+            end: {
+              character: 24,
+              line: 10,
+            },
+            start: {
+              character: 21,
+              line: 10,
+            },
+          },
+          severity: DiagnosticSeverity.Error,
+        },
+        {
+          code: 'no-$ref-siblings',
+          message: '$ref cannot be placed next to any other properties',
+          path: ['responses', '300', 'abc'],
+          range: {
+            end: {
+              character: 16,
+              line: 11,
+            },
+            start: {
+              character: 13,
+              line: 11,
+            },
+          },
+          severity: DiagnosticSeverity.Error,
+        },
+      ]),
+    );
+  });
+
   describe('runWithResolved', () => {
     test('should include both resolved and validation results', async () => {
       spectral.setRules({
diff --git a/src/rulesets/oas/functions/__tests__/refSiblings.test.ts b/src/rulesets/oas/functions/__tests__/refSiblings.test.ts
new file mode 100644
index 00000000..c98dfe57
--- /dev/null
+++ b/src/rulesets/oas/functions/__tests__/refSiblings.test.ts
@@ -0,0 +1,267 @@
+import { getLocationForJsonPath, parseWithPointers } from '@stoplight/json';
+import { DiagnosticSeverity } from '@stoplight/types';
+import { RuleType, Spectral } from '../../../../index';
+import { rules as oasRules } from '../../../oas/index.json';
+import refSiblings from '../refSiblings';
+
+describe('refSiblings', () => {
+  const s = new Spectral();
+  s.setFunctions({ refSiblings });
+  s.setRules({
+    'no-$ref-siblings': Object.assign(oasRules['no-$ref-siblings'], {
+      recommended: true,
+      type: RuleType[oasRules['no-$ref-siblings'].type],
+    }),
+  });
+
+  test('does not report anything for valid object', async () => {
+    const results = await s.run({
+      swagger: '2.0',
+      securityDefinitions: {
+        apikey: {},
+      },
+      paths: {
+        '/path': {
+          get: {
+            security: [
+              {
+                apikey: [],
+              },
+            ],
+          },
+        },
+      },
+    });
+
+    expect(results.length).toEqual(0);
+  });
+
+  test('reports $ref siblings for oas2 document', async () => {
+    const doc = `{
+  "swagger": "2.0",
+  "securityDefinitions": {
+    "apikey": {},
+    "$ref": "#/securityDefinitions/apikey"
+  },
+  "paths": {
+    "$ref": "#/securityDefinitions/apikey",
+    "/path": {
+      "post": {},
+      "$ref": "#/foo/bar",
+      "get": {
+        "$ref": "#/da",
+        "security": [
+          {
+            "apikey": []
+          }
+        ]
+      }
+    }
+  }
+}`;
+
+    const results = await s.run({
+      parsed: parseWithPointers(doc),
+      getLocationForJsonPath,
+    });
+
+    expect(results).toEqual([
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['securityDefinitions', 'apikey'],
+        range: {
+          end: {
+            character: 16,
+            line: 3,
+          },
+          start: {
+            character: 14,
+            line: 3,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['paths', '/path'],
+        range: {
+          end: {
+            character: 5,
+            line: 19,
+          },
+          start: {
+            character: 13,
+            line: 8,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['paths', '/path', 'post'],
+        range: {
+          end: {
+            character: 16,
+            line: 9,
+          },
+          start: {
+            character: 14,
+            line: 9,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['paths', '/path', 'get'],
+        range: {
+          end: {
+            character: 7,
+            line: 18,
+          },
+          start: {
+            character: 13,
+            line: 11,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['paths', '/path', 'get', 'security'],
+        range: {
+          end: {
+            character: 9,
+            line: 17,
+          },
+          start: {
+            character: 20,
+            line: 13,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+    ]);
+  });
+
+  test('reports $ref siblings for oas3 document', async () => {
+    const doc = `{
+  "openapi": "3.0.0",
+  "components": {
+    "securityDefinitions": {
+      "apikey": {},
+      "$ref": "#/components/securityDefinitions/apikey"
+    }
+  },
+  "paths": {
+    "$ref": "#/components/securityDefinitions/apikey",
+    "/path": {
+      "post": {},
+      "$ref": "#/foo/bar",
+      "get": {
+        "$ref": "#/da",
+        "security": [
+          {
+            "apikey": []
+          }
+        ]
+      }
+    }
+  }
+}`;
+
+    const results = await s.run({
+      parsed: parseWithPointers(doc),
+      getLocationForJsonPath,
+    });
+
+    expect(results).toEqual([
+      {
+        code: 'no-$ref-siblings',
+
+        message: '$ref cannot be placed next to any other properties',
+        path: ['components', 'securityDefinitions', 'apikey'],
+        range: {
+          end: {
+            character: 18,
+            line: 4,
+          },
+          start: {
+            character: 16,
+            line: 4,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['paths', '/path'],
+        range: {
+          end: {
+            character: 5,
+            line: 21,
+          },
+          start: {
+            character: 13,
+            line: 10,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['paths', '/path', 'post'],
+        range: {
+          end: {
+            character: 16,
+            line: 11,
+          },
+          start: {
+            character: 14,
+            line: 11,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['paths', '/path', 'get'],
+        range: {
+          end: {
+            character: 7,
+            line: 20,
+          },
+          start: {
+            character: 13,
+            line: 13,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        code: 'no-$ref-siblings',
+        message: '$ref cannot be placed next to any other properties',
+        path: ['paths', '/path', 'get', 'security'],
+        range: {
+          end: {
+            character: 9,
+            line: 19,
+          },
+          start: {
+            character: 20,
+            line: 15,
+          },
+        },
+        severity: DiagnosticSeverity.Error,
+      },
+    ]);
+  });
+});