Skip to content

Commit e6ac94b

Browse files
committed
First pass at coverage
1 parent c168108 commit e6ac94b

14 files changed

+611
-4
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
11
# Hyperjump - JSON Schema Test Coverage
22

3-
TODO
3+
## Usage
4+
5+
```bash
6+
rm ./.nyc_output/*
7+
npm test
8+
npx nyc report --reporter=html --extension .schema.json
9+
npx http-server coverage
10+
```
11+
## Legend
12+
13+
Statements = Keywords
14+
Branches = true/false for each keyword
15+
Functions = Subschemas
16+

package.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
},
1717
"type": "module",
1818
"exports": {
19-
".": "./src/index.js"
19+
".": "./src/index.js",
20+
"./vitest": "./src/vitest/index.js"
2021
},
2122
"scripts": {
2223
"lint": "eslint src",
@@ -26,14 +27,21 @@
2627
},
2728
"devDependencies": {
2829
"@stylistic/eslint-plugin": "*",
30+
"@types/istanbul-lib-coverage": "^2.0.6",
31+
"@types/moo": "^0.5.10",
2932
"@types/node": "*",
33+
"@types/unist": "^3.0.3",
3034
"eslint-import-resolver-typescript": "*",
3135
"eslint-plugin-import": "*",
3236
"typedoc": "*",
3337
"typescript-eslint": "*",
3438
"vitest": "*"
3539
},
3640
"dependencies": {
37-
"@hyperjump/json-schema": "^1.16.0"
41+
"@hyperjump/json-schema": "^1.16.0",
42+
"@hyperjump/uri": "^1.3.1",
43+
"istanbul-lib-coverage": "^3.2.2",
44+
"moo": "^0.5.2",
45+
"vfile": "^6.0.3"
3846
}
3947
}

src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { TestCoverageEvaluationPlugin } from "./test-coverage-evaluation-plugin.d.ts";

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { TestCoverageEvaluationPlugin } from "./test-coverage-evaluation-plugin.js";

src/json-lexer.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import moo from "moo";
2+
import { VFileMessage } from "vfile-message";
3+
4+
/**
5+
* @import { Lexer, Token } from "moo"
6+
*/
7+
8+
/**
9+
* @typedef {"null" | "boolean" | "number" | "string" | "{" | "}" | "[" | "]" | ":" | ","} JsonTokenType
10+
*/
11+
12+
/**
13+
* @template {JsonTokenType} [T=JsonTokenType]
14+
* @typedef {{ [K in T]: Token & { type: K }; }[T]} JsonToken
15+
*/
16+
17+
// String
18+
const unescaped = `[\\x20-\\x21\\x23-\\x5b\\x5d-\\u{10ffff}]`;
19+
const escape = `\\\\`;
20+
const hexdig = `[0-9a-fA-F]`;
21+
const escaped = `${escape}(?:["\\/\\\\brfnt]|u${hexdig}{4})`;
22+
const char = `(?:${unescaped}|${escaped})`;
23+
const string = `"${char}*"`;
24+
25+
// Number
26+
const digit = `[0-9]`;
27+
const digit19 = `[1-9]`;
28+
const int = `(?:0|${digit19}${digit}*)`;
29+
const frac = `\\.${digit}+`;
30+
const e = `[eE]`;
31+
const exp = `${e}[-+]?${digit}+`;
32+
const number = `-?${int}(?:${frac})?(?:${exp})?`;
33+
34+
// Whitespace
35+
const whitespace = `(?:(?:\\r?\\n)|[ \\t])+`;
36+
37+
export class JsonLexer {
38+
/** @type Lexer */
39+
#lexer;
40+
41+
/** @type Generator<JsonToken<JsonTokenType>, void, undefined> */
42+
#iterator;
43+
44+
/**
45+
* @param {string} json
46+
*/
47+
constructor(json) {
48+
this.#lexer = moo.compile({
49+
WS: { match: new RegExp(whitespace, "u"), lineBreaks: true },
50+
boolean: ["true", "false"],
51+
null: "null",
52+
number: { match: new RegExp(number, "u") },
53+
string: { match: new RegExp(string, "u") },
54+
"{": "{",
55+
"}": "}",
56+
"[": "[",
57+
"]": "]",
58+
":": ":",
59+
",": ",",
60+
error: moo.error
61+
});
62+
63+
this.#iterator = (function* (lexer) {
64+
for (const token of lexer.reset(json)) {
65+
if (token.type === "WS") {
66+
continue;
67+
}
68+
69+
yield /** @type JsonToken */ (token);
70+
}
71+
}(this.#lexer));
72+
}
73+
74+
/** @type () => JsonToken */
75+
nextToken() {
76+
const result = this.#iterator.next();
77+
if (result.done) {
78+
throw this.syntaxError("No more tokens");
79+
}
80+
81+
return result.value;
82+
};
83+
84+
done() {
85+
if (!this.#iterator.next().done) {
86+
throw this.syntaxError("Additional tokens found");
87+
}
88+
}
89+
90+
/** @type (message: string, token?: JsonToken) => VFileMessage */
91+
syntaxError(message, token) {
92+
throw new VFileMessage(message, {
93+
source: "json",
94+
ruleId: "syntax-error",
95+
place: {
96+
// @ts-expect-error Line exists in the lexer but isn't included in the type
97+
line: token?.line ?? this.#lexer.line, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
98+
// @ts-expect-error Line exists in the lexer but isn't included in the type
99+
column: token?.col ?? this.#lexer.col // eslint-disable-line @typescript-eslint/no-unsafe-assignment
100+
}
101+
});
102+
};
103+
}

src/json-util.js

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import * as JsonPointer from "@hyperjump/json-pointer";
2+
import { JsonLexer } from "./json-lexer.js";
3+
4+
/**
5+
* @import { Node, Position } from "unist"
6+
* @import { JsonToken } from "./json-lexer.js"
7+
* @import {
8+
* JsonArrayNode,
9+
* JsonNode,
10+
* JsonObjectNode,
11+
* JsonPropertyNameNode,
12+
* JsonPropertyNode
13+
* } from "./jsonast.d.ts"
14+
*/
15+
16+
/** @type (json: string, location?: string) => JsonNode */
17+
export const fromJson = (json, location = "") => {
18+
const lexer = new JsonLexer(json);
19+
20+
const token = lexer.nextToken();
21+
const jsonValue = parseValue(token, lexer, undefined, `${location}#`);
22+
23+
lexer.done();
24+
25+
return jsonValue;
26+
};
27+
28+
/** @type (token: JsonToken, lexer: JsonLexer, key: string | undefined, location: string) => JsonNode */
29+
const parseValue = (token, lexer, _key, location) => {
30+
switch (token.type) {
31+
case "null":
32+
case "boolean":
33+
case "number":
34+
case "string":
35+
return parseScalar(token, location);
36+
case "[":
37+
return parseArray(token, lexer, location);
38+
case "{":
39+
return parseObject(token, lexer, location);
40+
default:
41+
throw lexer.syntaxError("Expected a JSON value", token);
42+
}
43+
};
44+
45+
/** @type (token: JsonToken<"null" | "boolean" | "number" | "string">, location: string) => JsonNode */
46+
const parseScalar = (token, location) => {
47+
return {
48+
type: "json",
49+
jsonType: token.type,
50+
value: JSON.parse(token.value), // eslint-disable-line @typescript-eslint/no-unsafe-assignment
51+
location: location,
52+
position: tokenPosition(token)
53+
};
54+
};
55+
56+
/** @type (token: JsonToken, lexer: JsonLexer, key: string, location: string) => JsonPropertyNode */
57+
const parseProperty = (token, lexer, _key, location) => {
58+
if (token.type !== "string") {
59+
throw lexer.syntaxError("Expected a propertry", token);
60+
}
61+
62+
/** @type JsonPropertyNameNode */
63+
const keyNode = {
64+
type: "json-property-name",
65+
jsonType: "string",
66+
value: JSON.parse(token.value), // eslint-disable-line @typescript-eslint/no-unsafe-assignment
67+
position: tokenPosition(token)
68+
};
69+
70+
if (lexer.nextToken().type !== ":") {
71+
throw lexer.syntaxError("Expected :", token);
72+
}
73+
74+
const valueNode = parseValue(lexer.nextToken(), lexer, keyNode.value, JsonPointer.append(keyNode.value, location));
75+
76+
return {
77+
type: "json-property",
78+
children: [keyNode, valueNode],
79+
position: {
80+
start: keyNode.position.start,
81+
end: valueNode.position.end
82+
}
83+
};
84+
};
85+
86+
/**
87+
* @template A
88+
* @typedef {Node & { children: A[] }} ParentNode
89+
*/
90+
91+
/**
92+
* @type <P extends ParentNode<C>, C extends JsonNode | JsonPropertyNode>(
93+
* parseChild: (token: JsonToken, lexer: JsonLexer, key: string, location: string) => C,
94+
* endToken: string
95+
* ) => (lexer: JsonLexer, node: P, location: string) => P
96+
*/
97+
const parseCommaSeparated = (parseChild, endToken) => (lexer, node, location) => {
98+
for (let index = 0; true; index++) {
99+
let token = lexer.nextToken();
100+
101+
if (token.type === endToken) {
102+
/** @type Position */ (node.position).end = tokenPosition(token).end;
103+
return node;
104+
}
105+
106+
if (index > 0) {
107+
if (token.type === ",") {
108+
token = lexer.nextToken();
109+
} else {
110+
throw lexer.syntaxError(`Expected , or ${endToken}`, token);
111+
}
112+
}
113+
114+
const childNode = parseChild(token, lexer, `${index}`, location);
115+
if (childNode) {
116+
node.children.push(childNode);
117+
}
118+
}
119+
};
120+
121+
/** @type (openToken: JsonToken, lexer: JsonLexer, location: string) => JsonArrayNode */
122+
const parseArray = (openToken, lexer, location) => {
123+
return parseItems(lexer, {
124+
type: "json",
125+
jsonType: "array",
126+
children: [],
127+
location: location,
128+
position: tokenPosition(openToken)
129+
}, location);
130+
};
131+
132+
/** @type (token: JsonToken, lexer: JsonLexer, key: string, location: string) => JsonNode */
133+
const parseItem = (token, lexer, key, location) => {
134+
return parseValue(token, lexer, key, JsonPointer.append(key, location));
135+
};
136+
137+
/** @type (lexer: JsonLexer, node: { type: "json" } & JsonArrayNode, location: string) => JsonArrayNode */
138+
const parseItems = parseCommaSeparated(parseItem, "]");
139+
140+
/** @type (openToken: JsonToken, lexer: JsonLexer, location: string) => JsonObjectNode */
141+
const parseObject = (openToken, lexer, location) => {
142+
return parseProperties(lexer, {
143+
type: "json",
144+
jsonType: "object",
145+
children: [],
146+
location: location,
147+
position: tokenPosition(openToken)
148+
}, location);
149+
};
150+
151+
/** @type (lexer: JsonLexer, node: { type: "json" } & JsonObjectNode, location: string) => JsonObjectNode */
152+
const parseProperties = parseCommaSeparated(parseProperty, "}");
153+
154+
/** @type (startToken: JsonToken, endToken?: JsonToken) => Position */
155+
const tokenPosition = (startToken, endToken) => {
156+
endToken ??= startToken;
157+
158+
return {
159+
start: {
160+
line: startToken.line,
161+
column: startToken.col,
162+
offset: startToken.offset
163+
},
164+
end: {
165+
line: endToken.line,
166+
column: endToken.col + endToken.text.length - 1,
167+
offset: endToken.offset + endToken.text.length
168+
}
169+
};
170+
};
171+
172+
/** @type (tree: JsonNode, pointer: string) => JsonNode | JsonPropertyNode */
173+
export const getNodeFromPointer = (tree, pointer) => {
174+
/** @type JsonNode | JsonPropertyNode | undefined */
175+
let node = tree;
176+
177+
for (const segment of JsonPointer.pointerSegments(pointer)) {
178+
if (node.type === "json-property") {
179+
node = node.children[1];
180+
}
181+
182+
switch (node.jsonType) {
183+
case "object":
184+
node = node.children.find((property) => property.children[0].value === segment);
185+
break;
186+
case "array":
187+
node = node.children[parseInt(segment, 10)];
188+
break;
189+
}
190+
191+
if (!node) {
192+
throw Error("Invalid pointer");
193+
}
194+
}
195+
196+
return node;
197+
};

0 commit comments

Comments
 (0)