Skip to content

Commit 15fe0fe

Browse files
author
Phil Sturgeon
authored
Merge pull request #171 from stoplightio/fix/nullish-resolved
Resolved might be null
2 parents ce1bdcd + ce46359 commit 15fe0fe

File tree

6 files changed

+105
-28
lines changed

6 files changed

+105
-28
lines changed

Diff for: lib/index.d.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,8 @@ declare namespace $RefParser {
409409
export type JSONParserErrorType = "EUNKNOWN" | "EPARSER" | "EUNMATCHEDPARSER" | "ERESOLVER" | "EUNMATCHEDRESOLVER" | "EMISSINGPOINTER" | "EINVALIDPOINTER";
410410

411411
export class JSONParserError extends Error {
412+
public constructor(message: string, source: string);
413+
412414
public readonly name: string;
413415
public readonly message: string;
414416
public readonly source: string;
@@ -439,28 +441,40 @@ declare namespace $RefParser {
439441
}
440442

441443
export class ParserError extends JSONParserError {
444+
public constructor(message: string, source: string);
445+
442446
public readonly name = "ParserError";
443447
public readonly code = "EPARSER";
444448
}
445449
export class UnmatchedParserError extends JSONParserError {
450+
public constructor(source: string);
451+
446452
public readonly name = "UnmatchedParserError";
447-
public readonly code ="EUNMATCHEDPARSER";
453+
public readonly code = "EUNMATCHEDPARSER";
448454
}
449455
export class ResolverError extends JSONParserError {
456+
public constructor(ex: Error | NodeJS.ErrnoException, source: string);
457+
450458
public readonly name = "ResolverError";
451-
public readonly code ="ERESOLVER";
459+
public readonly code = "ERESOLVER";
452460
public readonly ioErrorCode?: string;
453461
}
454462
export class UnmatchedResolverError extends JSONParserError {
463+
public constructor(source: string);
464+
455465
public readonly name = "UnmatchedResolverError";
456-
public readonly code ="EUNMATCHEDRESOLVER";
466+
public readonly code = "EUNMATCHEDRESOLVER";
457467
}
458468
export class MissingPointerError extends JSONParserError {
469+
public constructor(token: string | number, source: string);
470+
459471
public readonly name = "MissingPointerError";
460-
public readonly code ="EMISSINGPOINTER";
472+
public readonly code = "EMISSINGPOINTER";
461473
}
462474
export class InvalidPointerError extends JSONParserError {
475+
public constructor(pointer: string, source: string);
476+
463477
public readonly name = "InvalidPointerError";
464-
public readonly code ="EINVALIDPOINTER";
478+
public readonly code = "EINVALIDPOINTER";
465479
}
466480
}

Diff for: lib/pointer.js

+4
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ function resolveIf$Ref (pointer, options) {
235235
}
236236
else {
237237
let resolved = pointer.$ref.$refs._resolve($refPath, pointer.path, options);
238+
if (resolved === null) {
239+
return false;
240+
}
241+
238242
pointer.indirections += resolved.indirections + 1;
239243

240244
if ($Ref.isExtended$Ref(pointer.value)) {

Diff for: lib/ref.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { safePointerToPath, stripHash, getHash } = require("./util/url");
99
/**
1010
* This class represents a single JSON reference and its resolved value.
1111
*
12-
* @constructor
12+
* @class
1313
*/
1414
function $Ref () {
1515
/**
@@ -27,24 +27,28 @@ function $Ref () {
2727
/**
2828
* The resolved value of the JSON reference.
2929
* Can be any JSON type, not just objects. Unknown file types are represented as Buffers (byte arrays).
30+
*
3031
* @type {?*}
3132
*/
3233
this.value = undefined;
3334

3435
/**
3536
* The {@link $Refs} object that contains this {@link $Ref} object.
37+
*
3638
* @type {$Refs}
3739
*/
3840
this.$refs = undefined;
3941

4042
/**
4143
* Indicates the type of {@link $Ref#path} (e.g. "file", "http", etc.)
44+
*
4245
* @type {?string}
4346
*/
4447
this.pathType = undefined;
4548

4649
/**
4750
* List of all errors. Undefined if no errors.
51+
*
4852
* @type {Array<JSONParserError | ResolverError | ParserError | MissingPointerError>}
4953
*/
5054
this.errors = undefined;
@@ -53,25 +57,30 @@ function $Ref () {
5357
/**
5458
* Pushes an error to errors array.
5559
*
56-
* @param {Array<JSONParserError | JSONParserErrorGroup>} error - The error to be pushed
60+
* @param {Array<JSONParserError | JSONParserErrorGroup>} err - The error to be pushed
5761
* @returns {void}
5862
*/
5963
$Ref.prototype.addError = function (err) {
6064
if (this.errors === undefined) {
6165
this.errors = [];
6266
}
6367

68+
const existingErrors = this.errors.map(({ footprint }) => footprint);
69+
6470
// the path has been almost certainly set at this point,
65-
// but just in case something went wrong, let's inject path if necessary
71+
// but just in case something went wrong, normalizeError injects path if necessary
72+
// moreover, certain errors might point at the same spot, so filter them out to reduce noise
6673
if (Array.isArray(err.errors)) {
67-
this.errors.push(...err.errors.map(normalizeError));
74+
this.errors.push(...err.errors
75+
.map(normalizeError)
76+
.filter(({ footprint }) => !existingErrors.includes(footprint)),
77+
);
6878
}
69-
else {
79+
else if (!existingErrors.includes(err.footprint)) {
7080
this.errors.push(normalizeError(err));
7181
}
7282
};
7383

74-
7584
/**
7685
* Determines whether the given JSON reference exists within this {@link $Ref#value}.
7786
*
@@ -106,8 +115,8 @@ $Ref.prototype.get = function (path, options) {
106115
* @param {string} path - The full path being resolved, optionally with a JSON pointer in the hash
107116
* @param {$RefParserOptions} options
108117
* @param {string} friendlyPath - The original user-specified path (used for error messages)
109-
* @param {string} pathFromRoot - The path of `obj` from the schema root
110-
* @returns {Pointer}
118+
* @param {string} pathFromRoot - The path of `obj` from the schema root
119+
* @returns {Pointer | null}
111120
*/
112121
$Ref.prototype.resolve = function (path, options, friendlyPath, pathFromRoot) {
113122
let pointer = new Pointer(this, path, friendlyPath);

Diff for: lib/util/errors.js

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ const JSONParserError = exports.JSONParserError = class JSONParserError extends
1515

1616
Ono.extend(this);
1717
}
18+
19+
get footprint () {
20+
return `${this.path}+${this.source}+${this.code}+${this.message}`;
21+
}
1822
};
1923

2024
setErrorName(JSONParserError);
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
internal1:
2+
$ref: '#/internal2'
3+
internal2:
4+
$ref: '#/external'
5+

Diff for: test/specs/missing-pointers/missing-pointers.spec.js

+56-15
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { expect } = chai;
77
const $RefParser = require("../../../lib");
88
const { JSONParserErrorGroup, MissingPointerError } = require("../../../lib/util/errors");
99
const helper = require("../../utils/helper");
10+
const path = require("../../utils/path");
1011

1112
describe("Schema with missing pointers", () => {
1213
it("should throw an error for missing pointer", async () => {
@@ -20,25 +21,65 @@ describe("Schema with missing pointers", () => {
2021
}
2122
});
2223

23-
it("should throw a grouped error for missing pointer if continueOnError is true", async () => {
24-
const parser = new $RefParser();
24+
it("should throw an error for missing pointer in external file", async () => {
2525
try {
26-
await parser.dereference({ foo: { $ref: "#/baz" }}, { continueOnError: true });
26+
await $RefParser.dereference({ foo: { $ref: path.abs("specs/missing-pointers/external-from-internal.yaml") }});
2727
helper.shouldNotGetCalled();
2828
}
2929
catch (err) {
30-
expect(err).to.be.instanceof(JSONParserErrorGroup);
31-
expect(err.files).to.equal(parser);
32-
expect(err.files.$refs._root$Ref.value).to.deep.equal({ foo: null });
33-
expect(err.message).to.have.string("1 error occurred while reading '");
34-
expect(err.errors).to.containSubset([
35-
{
36-
name: MissingPointerError.name,
37-
message: "Token \"baz\" does not exist.",
38-
path: ["foo"],
39-
source: message => message.endsWith("/test/") || message.startsWith("http://localhost"),
40-
}
41-
]);
30+
expect(err).to.be.an.instanceOf(MissingPointerError);
31+
expect(err.message).to.contain("Token \"external\" does not exist.");
4232
}
4333
});
34+
35+
context("when continueOnError is true", () => {
36+
it("should throw a grouped error for missing pointer", async () => {
37+
const parser = new $RefParser();
38+
try {
39+
await parser.dereference({ foo: { $ref: "#/baz" }}, { continueOnError: true });
40+
helper.shouldNotGetCalled();
41+
}
42+
catch (err) {
43+
expect(err).to.be.instanceof(JSONParserErrorGroup);
44+
expect(err.files).to.equal(parser);
45+
expect(err.files.$refs._root$Ref.value).to.deep.equal({ foo: null });
46+
expect(err.message).to.have.string("1 error occurred while reading '");
47+
expect(err.errors).to.containSubset([
48+
{
49+
name: MissingPointerError.name,
50+
message: "Token \"baz\" does not exist.",
51+
path: ["foo"],
52+
source: message => message.endsWith("/test/") || message.startsWith("http://localhost"),
53+
}
54+
]);
55+
}
56+
});
57+
58+
it("should throw an error for missing pointer in external file", async () => {
59+
const parser = new $RefParser();
60+
try {
61+
await parser.dereference({ foo: { $ref: path.abs("specs/missing-pointers/external-from-internal.yaml") }}, { continueOnError: true });
62+
helper.shouldNotGetCalled();
63+
}
64+
catch (err) {
65+
expect(err).to.be.instanceof(JSONParserErrorGroup);
66+
expect(err.files).to.equal(parser);
67+
expect(err.files.$refs._root$Ref.value).to.deep.equal({
68+
foo: {
69+
internal1: null,
70+
internal2: null,
71+
}
72+
});
73+
expect(err.message).to.have.string("1 error occurred while reading '");
74+
expect(err.errors).to.containSubset([
75+
{
76+
name: MissingPointerError.name,
77+
message: "Token \"external\" does not exist.",
78+
path: ["internal2"],
79+
source: message => message.endsWith("missing-pointers/external-from-internal.yaml") || message.startsWith("http://localhost"),
80+
}
81+
]);
82+
}
83+
});
84+
});
4485
});

0 commit comments

Comments
 (0)