Repository: stoplightio/spectral (ts)

Issue:
Can't silence reports
**Describe the bug**

I just installed 5.2.0 with the new silencing feature introduced by @nulltoken. I'm trying to silence the errors I am getting for my openapi.yaml described here: #915

To start with broad example, I added
```
except:
  "openapi.yaml#":
    - oas3-valid-schema-example
    - oas3-valid-content-schema-example
```
Unfortunately, it still shows the errors. I also tried some more paths, but except for an error that the source is missing for path `#` nothing changed.

What I really want to achieve:
```
except:
  "openapi.yaml#/components/schemas/process_graph":
    - oas3-valid-schema-example
  "openapi.yaml#/paths/\/processes/get":
    - oas3-valid-content-schema-example
  "openapi.yaml#/paths/\/process_graphs/get":
    - oas3-valid-content-schema-example
  "openapi.yaml#/paths/\/service_types/get":
    - oas3-valid-content-schema-example
```
Although for this example I'm not sure whether the forward slashes are correctly escaped.

I'm using JSON Reference here because the example in the docs look like JSON Reference although the doc says JSON Path. I'm confused, but tried both...

**To Reproduce**

See #915 and add the code given above.

**Expected behavior**

Errors should be silent. Either I don't understand the new feature/docs and that needs improvement or there's a bug in the code.

**Environment (remove any that are not applicable):**
 - Library version: 5.2.0
 - OS: Windows 10


Pull request description:
fix(exceptions): avoid swallowing results
Fix #1018

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No

/cc @m-mohr 

Patch:
diff --git a/docs/getting-started/rulesets.md b/docs/getting-started/rulesets.md
index cf1177e9..355b9b90 100644
--- a/docs/getting-started/rulesets.md
+++ b/docs/getting-started/rulesets.md
@@ -256,7 +256,8 @@ the rule entirely. This may happen, for instance, when working with legacy APIs.
 
 The ruleset can be extended for that purpose through the optional `except` property.
 
-`except` describes a map of locations (expressed as Json paths) and rules that should be ignored.
+`except` describes a map of locations (expressed as paths and [JSON Pointers](https://tools.ietf.org/html/rfc6901))
+and rules that should be ignored.
 
 Locations be can either described as relative to the ruleset or absolute paths.
 
@@ -271,6 +272,13 @@ except:
     - info-description
 ```
 
+<!-- theme: info -->
+> As per the [RFC 6901](https://tools.ietf.org/html/rfc6901#section-3), special characters
+> `~` and `/` have to be escaped to `~0` and `~1` respectively. For instance, the location of the `get` method from a
+> `/todos` path in an openapi document `/root/here.yaml` would be expressed as `/root/here.yaml#/paths/~1todos/get`.
+> 
+> _Hint:_ Running Spectral cli with the `--format json` parameter is pretty useful to find out the path segments of each result.
+
 ## Creating custom functions
 
 Learn more about [custom functions](../guides/custom-functions.md).
diff --git a/src/linter.ts b/src/linter.ts
index a230f674..f9eb1282 100644
--- a/src/linter.ts
+++ b/src/linter.ts
@@ -1,6 +1,7 @@
 import { decodePointerFragment } from '@stoplight/json';
 import { get } from 'lodash';
 
+import { JsonPath } from '@stoplight/types';
 import { Document } from './document';
 import { DocumentInventory } from './documentInventory';
 import { IMessageVars, message } from './rulesets/message';
@@ -9,23 +10,29 @@ import { IFunction, IGivenNode, IRuleResult, IRunRule, IThen } from './types';
 import { getClosestJsonPath, getLintTargets, printPath, PrintStyle } from './utils';
 import { IExceptionLocation } from './utils/pivotExceptions';
 
+const arePathsEqual = (one: JsonPath, another: JsonPath): boolean => {
+  if (one.length !== another.length) {
+    return false;
+  }
+
+  for (let i = 0; i < one.length; i++) {
+    if (one[i] !== another[i]) {
+      return false;
+    }
+  }
+
+  return true;
+};
+
 const isAKnownException = (violation: IRuleResult, locations: IExceptionLocation[]): boolean => {
   for (const location of locations) {
     if (violation.source !== location.source) {
       continue;
     }
 
-    if (violation.path.length !== location.path.length) {
-      continue;
+    if (arePathsEqual(violation.path, location.path)) {
+      return true;
     }
-
-    for (let i = 0; i < violation.path.length; i++) {
-      if (location.path[i] !== violation.path[i]) {
-        continue;
-      }
-    }
-
-    return true;
   }
 
   return false;


Test patch:
diff --git a/src/__tests__/__fixtures__/custom-oas-ruleset.json b/src/__tests__/__fixtures__/custom-oas-ruleset.json
index f71f2df6..ef243bcf 100644
--- a/src/__tests__/__fixtures__/custom-oas-ruleset.json
+++ b/src/__tests__/__fixtures__/custom-oas-ruleset.json
@@ -18,6 +18,8 @@
   "except": {
     "/test/file.json#/info": ["info-contact", "info-description"],
     "/test/file.json#": [ "oas3-api-servers"],
+    "/test/file.json#/paths/~1a.two/get": ["operation-2xx-response"],
+    "/test/file.json#/paths/~1b.three/get": ["operation-2xx-response"],
     "another.yaml#": ["dummy-rule", "info-contact"]
   }
 }
diff --git a/src/__tests__/linter.jest.test.ts b/src/__tests__/linter.jest.test.ts
index 8712462b..2c506f03 100644
--- a/src/__tests__/linter.jest.test.ts
+++ b/src/__tests__/linter.jest.test.ts
@@ -230,6 +230,82 @@ describe('Linter', () => {
       );
     });
 
+    it('should not swallow results', async () => {
+      // Cf. https://github.com/stoplightio/spectral/issues/1018
+
+      const document = {
+        openapi: '3.0.2',
+        paths: {
+          '/a.one': { get: 17 },
+          '/a.two': { get: 18 },
+          '/a.three': { get: 19 },
+          '/b.one': { get: 17 },
+          '/b.two': { get: 18 },
+          '/b.three': { get: 19 },
+        },
+      };
+
+      await spectral.loadRuleset(customOASRuleset);
+      spectral.registerFormat('oas3', isOpenApiv3);
+
+      const res = await spectral.run(document, {
+        resolve: {
+          documentUri: '/test/file.json',
+        },
+      });
+
+      expect(res).toEqual(
+        expect.arrayContaining([
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            message: 'Operation must have at least one `2xx` response.',
+            path: ['paths', '/a.one', 'get'],
+            source: '/test/file.json',
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            message: 'Operation must have at least one `2xx` response.',
+            path: ['paths', '/a.three', 'get'],
+            source: '/test/file.json',
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            message: 'Operation must have at least one `2xx` response.',
+            path: ['paths', '/b.one', 'get'],
+            source: '/test/file.json',
+          }),
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            message: 'Operation must have at least one `2xx` response.',
+            path: ['paths', '/b.two', 'get'],
+            source: '/test/file.json',
+          }),
+        ]),
+      );
+
+      expect(res).toEqual(
+        expect.not.arrayContaining([
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            message: 'Operation must have at least one `2xx` response.',
+            path: ['paths', '/a.two', 'get'],
+            source: '/test/file.json',
+          }),
+        ]),
+      );
+
+      expect(res).toEqual(
+        expect.not.arrayContaining([
+          expect.objectContaining({
+            code: 'operation-2xx-response',
+            message: 'Operation must have at least one `2xx` response.',
+            path: ['paths', '/b.three', 'get'],
+            source: '/test/file.json',
+          }),
+        ]),
+      );
+    });
+
     describe('resolving', () => {
       const document = {
         openapi: '3.0.2',
diff --git a/src/__tests__/spectral.jest.test.ts b/src/__tests__/spectral.jest.test.ts
index be535ed1..0165c031 100644
--- a/src/__tests__/spectral.jest.test.ts
+++ b/src/__tests__/spectral.jest.test.ts
@@ -51,6 +51,8 @@ describe('Spectral', () => {
       expect(Object.entries(s.exceptions)).toEqual([
         [expect.stringMatching('^/test/file.json#/info$'), ['info-contact', 'info-description']],
         [expect.stringMatching('^/test/file.json#$'), ['oas3-api-servers']],
+        [expect.stringMatching('^/test/file.json#/paths/~1a.two/get$'), ['operation-2xx-response']],
+        [expect.stringMatching('^/test/file.json#/paths/~1b.three/get$'), ['operation-2xx-response']],
         [expect.stringMatching('/__tests__/__fixtures__/another.yaml#$'), ['dummy-rule', 'info-contact']],
       ]);
     });
diff --git a/test-harness/scenarios/exceptions.circular.reference.oas3.scenario b/test-harness/scenarios/exceptions.circular.reference.oas3.scenario
new file mode 100644
index 00000000..4406fc49
--- /dev/null
+++ b/test-harness/scenarios/exceptions.circular.reference.oas3.scenario
@@ -0,0 +1,92 @@
+====test====
+Can ignore circular refs results (reference scenario)
+====document====
+openapi: 3.0.2
+info:
+  title: Circular circurlarity
+  description: This is a description
+  version: draft
+  contact: 
+    email: me@me.com
+paths:
+  /self_referencing_graphs:
+    get:
+      description: A good one.
+      responses:
+        '200':
+          description: Hello
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/descriptor'
+              example:
+                A weird example.
+  /cyclic_graphs:
+    get:
+      description: A good one.
+      responses:
+        '200':
+          description: Hello
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/descriptor'
+              example:
+                A weird example.
+  /looping_graphs:
+    get:
+      description: A good one.
+      responses:
+        '200':
+          description: Hello
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/descriptor'
+              example:
+                A weird example.
+components:
+  schemas:
+    links:
+      type: object
+      properties:
+        id:
+          type: string
+        references:
+          type: array
+          items:
+            $ref: '#/components/schemas/links'
+    descriptor:
+      type: object
+      properties:
+        id:
+          type: string
+        links:
+          $ref: '#/components/schemas/links'
+      example:
+        processes:
+          - id: 17
+            summary: Enhanced Index
+====asset:ruleset====
+extends: spectral:oas
+====command====
+{bin} lint {document} -r {asset:ruleset}
+====stdout====
+OpenAPI 3.x detected
+
+{document}
+  1:1   warning  oas3-api-servers                   OpenAPI `servers` must be present and non-empty array.
+  1:1   warning  openapi-tags                       OpenAPI object should have non-empty `tags` array.
+  10:9  warning  operation-operationId              Operation should have an `operationId`.
+  10:9  warning  operation-tags                     Operation should have non-empty `tags` array.
+ 20:17    error  oas3-valid-oas-content-example     can't resolve reference #/components/schemas/links from id #
+  22:9  warning  operation-operationId              Operation should have an `operationId`.
+  22:9  warning  operation-tags                     Operation should have non-empty `tags` array.
+ 32:17    error  oas3-valid-oas-content-example     can't resolve reference #/components/schemas/links from id #
+  34:9  warning  operation-operationId              Operation should have an `operationId`.
+  34:9  warning  operation-tags                     Operation should have non-empty `tags` array.
+ 44:17    error  oas3-valid-oas-content-example     can't resolve reference #/components/schemas/links from id #
+ 63:15    error  oas3-valid-content-schema-example  can't resolve reference #/components/schemas/links from id #
+ 63:15    error  oas3-valid-schema-example          can't resolve reference #/components/schemas/links from id #
+
+✖ 13 problems (5 errors, 8 warnings, 0 infos, 0 hints)
diff --git a/test-harness/scenarios/exceptions.circular.silenced.oas3.scenario b/test-harness/scenarios/exceptions.circular.silenced.oas3.scenario
new file mode 100644
index 00000000..51f07dc4
--- /dev/null
+++ b/test-harness/scenarios/exceptions.circular.silenced.oas3.scenario
@@ -0,0 +1,98 @@
+====test====
+Can ignore circular refs results
+====document====
+openapi: 3.0.2
+info:
+  title: Circular circurlarity
+  description: This is a description
+  version: draft
+  contact: 
+    email: me@me.com
+paths:
+  /self_referencing_graphs:
+    get:
+      description: A good one.
+      responses:
+        '200':
+          description: Hello
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/descriptor'
+              example:
+                A weird example.
+  /cyclic_graphs:
+    get:
+      description: A good one.
+      responses:
+        '200':
+          description: Hello
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/descriptor'
+              example:
+                A weird example.
+  /looping_graphs:
+    get:
+      description: A good one.
+      responses:
+        '200':
+          description: Hello
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/descriptor'
+              example:
+                A weird example.
+components:
+  schemas:
+    links:
+      type: object
+      properties:
+        id:
+          type: string
+        references:
+          type: array
+          items:
+            $ref: '#/components/schemas/links'
+    descriptor:
+      type: object
+      properties:
+        id:
+          type: string
+        links:
+          $ref: '#/components/schemas/links'
+      example:
+        processes:
+          - id: 17
+            summary: Enhanced Index
+====asset:ruleset====
+extends: spectral:oas
+
+except:
+  "{document}#/components/schemas/descriptor/example":
+    - oas3-valid-schema-example
+    - oas3-valid-content-schema-example
+  "{document}#/paths/~1looping_graphs/get/responses/200/content/application~1json/example":
+    - oas3-valid-oas-content-example
+  "{document}#/paths/~1cyclic_graphs/get/responses/200/content/application~1json/example":
+    - oas3-valid-oas-content-example
+  "{document}#/paths/~1self_referencing_graphs/get/responses/200/content/application~1json/example":
+    - oas3-valid-oas-content-example
+====command====
+{bin} lint {document} -r {asset:ruleset}
+====stdout====
+OpenAPI 3.x detected
+
+{document}
+  1:1  warning  oas3-api-servers       OpenAPI `servers` must be present and non-empty array.
+  1:1  warning  openapi-tags           OpenAPI object should have non-empty `tags` array.
+ 10:9  warning  operation-operationId  Operation should have an `operationId`.
+ 10:9  warning  operation-tags         Operation should have non-empty `tags` array.
+ 22:9  warning  operation-operationId  Operation should have an `operationId`.
+ 22:9  warning  operation-tags         Operation should have non-empty `tags` array.
+ 34:9  warning  operation-operationId  Operation should have an `operationId`.
+ 34:9  warning  operation-tags         Operation should have non-empty `tags` array.
+
+✖ 8 problems (0 errors, 8 warnings, 0 infos, 0 hints)