Repository: stoplightio/spectral
Pull request #900 (2020-01-09 12:03:49)

fix: generate proper paths for rules operating on unresolved documents
Fixes https://github.com/stoplightio/spectral/issues/891

**Checklist**

- [x] Tests added / updated
- [x] Docs added / updated

**Does this PR introduce a breaking change?**

- [ ] Yes
- [x] No



nulltoken: @P0lip Awesome! 
P0lip: (had conflicts)

Patch:
diff --git a/src/documentInventory.ts b/src/documentInventory.ts
index 289d8975..33358f42 100644
--- a/src/documentInventory.ts
+++ b/src/documentInventory.ts
@@ -63,7 +63,17 @@ export class DocumentInventory {
     this.errors = formatResolverErrors(this.document, resolveResult.errors);
   }
 
-  public findAssociatedItemForPath(path: JsonPath): DocumentInventoryItem | null {
+  public findAssociatedItemForPath(path: JsonPath, resolved: boolean): DocumentInventoryItem | null {
+    if (!resolved) {
+      const newPath: JsonPath = getClosestJsonPath(this.unresolved, path);
+
+      return {
+        document: this.document,
+        path: newPath,
+        missingPropertyPath: path,
+      };
+    }
+
     try {
       const newPath: JsonPath = getClosestJsonPath(this.resolved, path);
       let $ref = traverseObjUntilRef(this.unresolved, newPath);
diff --git a/src/errorMessages.ts b/src/errorMessages.ts
index 802a9760..9bb84f3f 100644
--- a/src/errorMessages.ts
+++ b/src/errorMessages.ts
@@ -43,7 +43,7 @@ export function formatParserDiagnostics(diagnostics: ReadonlyArray<IDiagnostic>,
 
 export const formatResolverErrors = (document: IDocument, diagnostics: IResolveError[]): IRuleResult[] => {
   return uniqBy(diagnostics, 'message').map<IRuleResult>(error => {
-    const path = [...(error.path || []), '$ref'];
+    const path = [...error.path, '$ref'];
     const range = document.getRangeForJsonPath(path, true) || Document.DEFAULT_RANGE;
     const source = error.uriStack.length > 0 ? error.uriStack[error.uriStack.length - 1] : document.source;
 
diff --git a/src/linter.ts b/src/linter.ts
index 8f6eb1d3..da66f7e1 100644
--- a/src/linter.ts
+++ b/src/linter.ts
@@ -91,7 +91,7 @@ export const lintNode = (
     results.push(
       ...targetResults.map<IRuleResult>(result => {
         const escapedJsonPath = (result.path || targetPath).map(segment => decodePointerFragment(String(segment)));
-        const associatedItem = inventory.findAssociatedItemForPath(escapedJsonPath);
+        const associatedItem = inventory.findAssociatedItemForPath(escapedJsonPath, rule.resolved !== false);
         const path = associatedItem?.path || getClosestJsonPath(inventory.resolved, escapedJsonPath);
         const document = associatedItem?.document || inventory.document;
         const range = document.getRangeForJsonPath(path, true) || Document.DEFAULT_RANGE;


Test patch:
diff --git a/src/__tests__/spectral.jest.test.ts b/src/__tests__/spectral.jest.test.ts
index e1a10b7b..12616691 100644
--- a/src/__tests__/spectral.jest.test.ts
+++ b/src/__tests__/spectral.jest.test.ts
@@ -6,6 +6,7 @@ import * as path from 'path';
 
 import { Document } from '../document';
 import { isOpenApiv2 } from '../formats';
+import { pattern } from '../functions/pattern';
 import * as Parsers from '../parsers';
 import { httpAndFileResolver } from '../resolvers/http-and-file';
 import { IRunRule, Spectral } from '../spectral';
@@ -190,6 +191,90 @@ describe('Spectral', () => {
     );
   });
 
+  test('properly decorates results with metadata pertaining to the document being linted', async () => {
+    const s = new Spectral({ resolver: httpAndFileResolver });
+    s.setFunctions({ pattern });
+    s.setRules({
+      'unsecure-remote-reference': {
+        message: '$ref must not point at unsecured remotes',
+        given: '$..$ref',
+        recommended: true,
+        resolved: false,
+        then: {
+          function: 'pattern',
+          functionOptions: {
+            notMatch: '^http:',
+          },
+        },
+      },
+    });
+
+    nock('http://oas3.library.com')
+      .get('/defs.json')
+      .reply(
+        200,
+        JSON.stringify({
+          openapi: '3.0.0',
+          components: {
+            schemas: {
+              ExternalHttp: {
+                type: 'number',
+              },
+              ExternalUnhooked: {
+                type: 'object',
+              },
+            },
+          },
+        }),
+      );
+
+    const doc = {
+      openapi: '3.0.0',
+      paths: {
+        '/path': {
+          post: {
+            parameters: [
+              {
+                $ref: '#/components/schemas/Hooked',
+              },
+              {
+                $ref: 'http://oas3.library.com/defs.json#/components/schemas/ExternalHttp',
+              },
+            ],
+          },
+        },
+      },
+      components: {
+        schemas: {
+          Hooked: {
+            type: 'object',
+          },
+          Unhooked: {
+            type: 'object',
+          },
+        },
+      },
+    };
+
+    const targetUri = 'test.json';
+
+    const parsedResult = new Document(JSON.stringify(doc), Parsers.Json, targetUri);
+
+    const results = await s.run(parsedResult, {
+      resolve: {
+        documentUri: 'test.json',
+      },
+    });
+
+    expect(results).toEqual([
+      expect.objectContaining({
+        code: 'unsecure-remote-reference',
+        path: ['paths', '/path', 'post', 'parameters', '1', '$ref'],
+        source: targetUri,
+      }),
+    ]);
+  });
+
   test('should recognize the source of remote $refs, and de-dupe results by fingerprint', async () => {
     const s = new Spectral({ resolver: httpAndFileResolver });
     const documentUri = path.join(__dirname, './__fixtures__/gh-658/URIError.yaml');