Repository: stoplightio/spectral (ts)

Issue:
Fail to load external reference file because a '/' is prepend to the path

**Describe the bug**
Spectral is unable to load external reference file because a `/` is prepend to the path.

**To Reproduce**

1. Given this OpenAPI document and referenced document in same folder:

index.yaml

```yaml
openapi: 3.0.0
info:
  title: Test
  version: 1.0.0
paths:
  '/':
    get:
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: 'common.yaml#/components/schemas/ResponseType'
                # Below doesn't work as well
                # $ref: './common.yaml#/components/schemas/ResponseType'
```

common.yaml

```yaml
openapi: 3.0.0
info:
  title: Common schema definition
  version: 1.0.0
components:
  schema:
    ResponseType:
      type: string
```

2. Run this CLI command 
```
spectral lint index.yaml
```

3. See error

```
Adding OpenAPI 3.x functions
OpenAPI 3.x detected

index.yaml
  1:1   warning  api-servers            OpenAPI `servers` must be present and non-empty array.
  2:6   warning  info-contact           Info object should contain `contact` object.
  2:6   warning  info-description       OpenAPI object info `description` must be present and non-empty string.
  7:9   warning  operation-description  Operation `description` must be present and non-empty string.
  7:9   warning  operation-operationId  Operation should have an `operationId`.
  7:9   warning  operation-tags         Operation should have non-empty `tags` array.
 14:23    error  invalid-ref            ENOENT: no such file or directory, open '/common.yaml'

✖ 7 problems (1 error, 6 warnings, 0 infos)

```

**Expected behavior**
There should not be an `invalid-ref` error. The reference file path should be `./common.yaml` instead of  `/common.yaml`

**Environment (remove any that are not applicable):**

@stoplight/spectral/4.0.2 darwin-x64 node-v12.8.0


Pull request:
fix(cli): resolve relative document paths
Fixes #474

**Checklist**

- [x] Tests added / updated
- [ ] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No



Patch:
diff --git a/package.json b/package.json
index fd88664c..a65c7041 100644
--- a/package.json
+++ b/package.json
@@ -62,8 +62,8 @@
     "@oclif/command": "^1.5.17",
     "@oclif/config": "^1.13.2",
     "@oclif/plugin-help": "^2.0",
-    "@stoplight/json": "^3.0.2",
-    "@stoplight/json-ref-resolver": "^2.0.3",
+    "@stoplight/json": "^3.0.4",
+    "@stoplight/json-ref-resolver": "^2.1.2",
     "@stoplight/path": "^1.1.0",
     "@stoplight/types": "^11.0.0",
     "@stoplight/yaml": "^3.0.0",
diff --git a/src/cli/commands/lint.ts b/src/cli/commands/lint.ts
index 4f4f5220..9a132dce 100644
--- a/src/cli/commands/lint.ts
+++ b/src/cli/commands/lint.ts
@@ -102,7 +102,7 @@ linting ./openapi.yaml
 
     if (args.source) {
       try {
-        await lint(args.source, lintConfig, this, rules);
+        await lint(isAbsolute(args.source) ? args.source : resolve(cwd, args.source), lintConfig, this, rules);
       } catch (ex) {
         this.error(ex.message);
       }
diff --git a/src/spectral.ts b/src/spectral.ts
index e7661f43..29ece0f9 100644
--- a/src/spectral.ts
+++ b/src/spectral.ts
@@ -3,7 +3,7 @@ import {
   getLocationForJsonPath as getLocationForJsonPathJSON,
   parseWithPointers as parseJSONWithPointers,
 } from '@stoplight/json';
-import { Resolver } from '@stoplight/json-ref-resolver';
+import { Cache, Resolver } from '@stoplight/json-ref-resolver';
 import { IUriParser } from '@stoplight/json-ref-resolver/types';
 import { extname } from '@stoplight/path';
 import { Dictionary } from '@stoplight/types';
@@ -42,12 +42,14 @@ export class Spectral {
   private _rules: RuleCollection = {};
   private _functions: FunctionCollection = defaultFunctions;
   private _resolver: Resolver;
+  private _uriCache: Cache;
 
   public formats: RegisteredFormats;
 
   constructor(opts?: IConstructorOpts) {
     this._resolver = opts && opts.resolver ? opts.resolver : new Resolver();
     this.formats = {};
+    this._uriCache = new Cache();
   }
 
   public async runWithResolved(
@@ -77,6 +79,7 @@ export class Spectral {
     const resolved = new Resolved(
       parsedResult,
       await this._resolver.resolve(parsedResult.parsed.data, {
+        uriCache: this._uriCache,
         baseUri: documentUri,
         parseResolveResult: async resolveOpts => {
           const ref = resolveOpts.targetAuthority.toString();
diff --git a/yarn.lock b/yarn.lock
index e74f1107..a3eb478e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -609,13 +609,13 @@
   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.0.3":
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/@stoplight/json-ref-resolver/-/json-ref-resolver-2.1.1.tgz#442b935b9d7cab9f42f7548bd7c2e6cb2599a5cf"
-  integrity sha512-bI90+DK7OjQYf4mOCZdvabuYyaWDQhr30YyuUcNB688hO+5qNekzJfq/GgkImfHe+E/b2vVpeqS2ST1Cu/WB+g==
+"@stoplight/json-ref-resolver@^2.1.2":
+  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" "^2.2.2"
-    "@stoplight/types" "^9.1.2"
+    "@stoplight/types" "^11.0.0"
     dependency-graph "~0.8.0"
     fast-memoize "^2.5.1"
     immer "^3.1.3"
@@ -633,10 +633,10 @@
     jsonc-parser "~2.1.0"
     lodash "^4.10"
 
-"@stoplight/json@^3.0.2":
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.0.3.tgz#8ea6614e6704940ce154276574578d4d9d7adf34"
-  integrity sha512-OiPBzSxsDnc32pQ20+68lTHqGhIhNP/LCrOI6cM2Y/PEDW1KwjhWcF3rjJtIgpObuQeMhCKk3djxHZaB/MzNDA==
+"@stoplight/json@^3.0.4":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.0.4.tgz#53f0e22aa9392003f70f35a42091ae80491b5a89"
+  integrity sha512-ptFgfHiyHzOwHvHzAH0VojYXUFSWhKUPW8KORmxlnA0qz11bzGoZFS2+7r3B0o8ZzyoOp4GU2AU4SGnZJjln8Q==
   dependencies:
     "@stoplight/types" "^10.0.1"
     jsonc-parser "~2.1"


Test patch:
diff --git a/src/__tests__/__fixtures__/gh-474/common.yaml b/src/__tests__/__fixtures__/gh-474/common.yaml
new file mode 100644
index 00000000..69e96586
--- /dev/null
+++ b/src/__tests__/__fixtures__/gh-474/common.yaml
@@ -0,0 +1,10 @@
+openapi: 3.0.0
+info:
+  title: Common schema definition
+  version: 1.0.0
+  contact:
+    name: null
+components:
+  schemas:
+    ResponseType:
+      type: string
diff --git a/src/__tests__/__fixtures__/gh-474/spec-2.yaml b/src/__tests__/__fixtures__/gh-474/spec-2.yaml
new file mode 100644
index 00000000..15f6c051
--- /dev/null
+++ b/src/__tests__/__fixtures__/gh-474/spec-2.yaml
@@ -0,0 +1,16 @@
+openapi: 3.0.0
+info:
+  title: Test
+  version: 1.0.0
+  contact:
+    $ref: './common.yaml#/info/contact'
+paths:
+  '/':
+    get:
+      responses:
+        '200':
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: './common.yaml#/components/schemas/ResponseType'
diff --git a/src/__tests__/__fixtures__/gh-474/spec.yaml b/src/__tests__/__fixtures__/gh-474/spec.yaml
new file mode 100644
index 00000000..eb44f877
--- /dev/null
+++ b/src/__tests__/__fixtures__/gh-474/spec.yaml
@@ -0,0 +1,16 @@
+openapi: 3.0.0
+info:
+  title: Test
+  version: 1.0.0
+  contact:
+    $ref: 'common.yaml#/info/contact'
+paths:
+  '/':
+    get:
+      responses:
+        '200':
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: 'common.yaml#/components/schemas/ResponseType'
diff --git a/src/__tests__/spectral.test.ts b/src/__tests__/spectral.test.ts
index a925a5a4..d20d16af 100644
--- a/src/__tests__/spectral.test.ts
+++ b/src/__tests__/spectral.test.ts
@@ -1,3 +1,4 @@
+import { Cache } from '@stoplight/json-ref-resolver';
 import { DiagnosticSeverity, Dictionary } from '@stoplight/types';
 import { isParsedResult, Spectral } from '../spectral';
 import { IParsedResult, RuleFunction } from '../types';
@@ -164,6 +165,7 @@ describe('spectral', () => {
 
       expect(fakeResolver.resolve).toBeCalledWith(target, {
         authority: undefined,
+        uriCache: expect.any(Cache),
         parseResolveResult: expect.any(Function),
       });
     });
diff --git a/src/cli/commands/__tests__/lint.test.ts b/src/cli/commands/__tests__/lint.test.ts
index 5aba7823..f2bf04c5 100644
--- a/src/cli/commands/__tests__/lint.test.ts
+++ b/src/cli/commands/__tests__/lint.test.ts
@@ -96,6 +96,24 @@ describe('lint', () => {
         });
       });
     });
+
+    test
+      .stdout()
+      .command(['lint', 'src/__tests__/__fixtures__/gh-474/spec.yaml'])
+      .it('should handle relative path to a document', ctx => {
+        expect(ctx.stdout).not.toContain('invalid-ref');
+        expect(ctx.stdout).toContain(`/__tests__/__fixtures__/gh-474/common.yaml
+ 6:11  error  oas3-schema  type should be string`);
+      });
+
+    test
+      .stdout()
+      .command(['lint', 'src/__tests__/__fixtures__/gh-474/spec-2.yaml'])
+      .it('should handle relative path to a document #2', ctx => {
+        expect(ctx.stdout).not.toContain('invalid-ref');
+        expect(ctx.stdout).toContain(`/__tests__/__fixtures__/gh-474/common.yaml
+ 6:11  error  oas3-schema  type should be string`);
+      });
   });
 
   describe('--ruleset', () => {