Skip to content

Commit bac72f5

Browse files
committed
Add support for self-identifying schemas
1 parent 9f47083 commit bac72f5

File tree

4 files changed

+195
-132
lines changed

4 files changed

+195
-132
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,13 @@
4242
"dependencies": {
4343
"@hyperjump/json-schema": "^1.16.0",
4444
"@hyperjump/uri": "^1.3.1",
45+
"ignore": "^7.0.5",
4546
"istanbul-lib-coverage": "^3.2.2",
4647
"istanbul-lib-report": "^3.0.1",
4748
"istanbul-reports": "^3.1.7",
4849
"moo": "^0.5.2",
4950
"pathe": "^2.0.3",
51+
"tinyglobby": "^0.2.14",
5052
"vfile": "^6.0.3"
5153
}
5254
}

src/coverage-util.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { getKeyword } from "@hyperjump/json-schema/experimental";
2+
import { parseIri } from "@hyperjump/uri";
3+
import { getNodeFromPointer } from "./json-util.js";
4+
5+
/**
6+
* @import { Position } from "unist"
7+
* @import { CompiledSchema } from "@hyperjump/json-schema/experimental"
8+
* @import { CoverageMapData, FileCoverageData, Range } from "istanbul-lib-coverage"
9+
* @import { JsonNode } from "./jsonast.js"
10+
*/
11+
12+
/** @type (compiledSchema: CompiledSchema, schemaPath: string, tree: JsonNode) => CoverageMapData */
13+
export const astToCoverageMap = (compiledSchema, schemaPath, tree) => {
14+
/** @type FileCoverageData */
15+
const fileCoverage = {
16+
path: schemaPath,
17+
statementMap: {},
18+
branchMap: {},
19+
fnMap: {},
20+
s: {},
21+
b: {},
22+
f: {}
23+
};
24+
25+
for (const schemaLocation in compiledSchema.ast) {
26+
if (schemaLocation === "metaData" || schemaLocation === "plugins" || !schemaLocation.startsWith(compiledSchema.schemaUri)) {
27+
continue;
28+
}
29+
30+
const pointer = decodeURI(parseIri(schemaLocation).fragment ?? "");
31+
const node = getNodeFromPointer(tree, pointer);
32+
33+
const declRange = node.type === "json-property"
34+
? positionToRange(node.children[0].position)
35+
: {
36+
start: { line: node.position.start.line, column: node.position.start.column - 1 },
37+
end: { line: node.position.start.line, column: node.position.start.column - 1 }
38+
};
39+
40+
const locRange = positionToRange(node.position);
41+
42+
// Create statement
43+
fileCoverage.statementMap[schemaLocation] = locRange;
44+
fileCoverage.s[schemaLocation] = 0;
45+
46+
// Create function
47+
fileCoverage.fnMap[schemaLocation] = {
48+
name: schemaLocation,
49+
decl: declRange,
50+
loc: locRange,
51+
line: node.position.start.line
52+
};
53+
fileCoverage.f[schemaLocation] = 0;
54+
55+
if (Array.isArray(compiledSchema.ast[schemaLocation])) {
56+
for (const keywordNode of compiledSchema.ast[schemaLocation]) {
57+
if (Array.isArray(keywordNode)) {
58+
const [keywordUri, keywordLocation] = keywordNode;
59+
60+
const pointer = decodeURI(parseIri(keywordLocation).fragment ?? "");
61+
const node = getNodeFromPointer(tree, pointer);
62+
const range = positionToRange(node.position);
63+
64+
// Create statement
65+
fileCoverage.statementMap[keywordLocation] = range;
66+
fileCoverage.s[keywordLocation] = 0;
67+
68+
if (annotationKeywords.has(keywordUri) || getKeyword(keywordUri).simpleApplicator) {
69+
continue;
70+
}
71+
72+
// Create branch
73+
fileCoverage.branchMap[keywordLocation] = {
74+
line: range.start.line,
75+
type: "keyword",
76+
loc: range,
77+
locations: [range, range]
78+
};
79+
fileCoverage.b[keywordLocation] = [0, 0];
80+
}
81+
}
82+
}
83+
}
84+
85+
return { [schemaPath]: fileCoverage };
86+
};
87+
88+
/** @type (position: Position) => Range */
89+
const positionToRange = (position) => {
90+
return {
91+
start: { line: position.start.line, column: position.start.column - 1 },
92+
end: { line: position.end.line, column: position.end.column - 1 }
93+
};
94+
};
95+
96+
const annotationKeywords = new Set([
97+
"https://json-schema.org/keyword/comment",
98+
"https://json-schema.org/keyword/definitions",
99+
"https://json-schema.org/keyword/title",
100+
"https://json-schema.org/keyword/description",
101+
"https://json-schema.org/keyword/default",
102+
"https://json-schema.org/keyword/deprecated",
103+
"https://json-schema.org/keyword/readOnly",
104+
"https://json-schema.org/keyword/writeOnly",
105+
"https://json-schema.org/keyword/examples",
106+
"https://json-schema.org/keyword/format"
107+
]);
Lines changed: 37 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,162 +1,67 @@
1-
import { readFileSync } from "node:fs";
2-
import { fileURLToPath } from "node:url";
3-
import { parseIri } from "@hyperjump/uri";
4-
import { getKeyword } from "@hyperjump/json-schema/experimental";
5-
import { fromJson, getNodeFromPointer } from "./json-util.js";
1+
import { existsSync, readFileSync } from "node:fs";
2+
import { toAbsoluteIri } from "@hyperjump/uri";
3+
import { createHash } from "node:crypto";
4+
import { resolve } from "node:path";
65

76
/**
8-
* @import { Position } from "unist"
9-
* @import { CoverageMapData, Range } from "istanbul-lib-coverage"
10-
* @import { AST, EvaluationPlugin } from "@hyperjump/json-schema/experimental"
11-
* @import { JsonNode } from "./jsonast.js"
7+
* @import { CoverageMapData } from "istanbul-lib-coverage"
8+
* @import { EvaluationPlugin } from "@hyperjump/json-schema/experimental"
129
*/
1310

1411
/** @implements EvaluationPlugin */
1512
export class TestCoverageEvaluationPlugin {
16-
/** @type Record<string, JsonNode> */
17-
#schemaCache = {};
13+
/** @type Record<string, string> */
14+
#filePathFor = {};
1815

1916
constructor() {
2017
/** @type CoverageMapData */
2118
this.coverageMap = {};
2219
}
2320

2421
/** @type NonNullable<EvaluationPlugin["beforeSchema"]> */
25-
beforeSchema(_schemaUri, _instance, context) {
26-
this.#buildCoverageMap(context.ast);
22+
beforeSchema(schemaUri) {
23+
const schemaLocation = toAbsoluteIri(schemaUri);
24+
if (!(schemaLocation in this.#filePathFor)) {
25+
const fileHash = createHash("md5").update(`${schemaLocation}#`).digest("hex");
26+
const coverageFilePath = resolve(".json-schema-coverage", fileHash);
27+
28+
if (existsSync(coverageFilePath)) {
29+
const json = readFileSync(coverageFilePath, "utf-8");
30+
/** @type CoverageMapData */
31+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
32+
const coverageMapData = JSON.parse(json);
33+
const fileCoveragePath = Object.keys(coverageMapData)[0];
34+
Object.assign(this.coverageMap, coverageMapData);
35+
this.#filePathFor[schemaLocation] = fileCoveragePath;
36+
}
37+
}
2738
}
2839

2940
/** @type NonNullable<EvaluationPlugin["afterKeyword"]> */
3041
afterKeyword([, keywordLocation], _instance, _context, valid) {
31-
if (!keywordLocation.startsWith("file:")) {
42+
const schemaLocation = toAbsoluteIri(keywordLocation);
43+
const filePath = this.#filePathFor[schemaLocation];
44+
if (!(filePath in this.coverageMap)) {
3245
return;
3346
}
3447

35-
const schemaPath = fileURLToPath(keywordLocation);
36-
this.coverageMap[schemaPath].s[keywordLocation]++;
37-
if (keywordLocation in this.coverageMap[schemaPath].b) {
38-
this.coverageMap[schemaPath].b[keywordLocation][Number(valid)]++;
48+
const fileCoverage = this.coverageMap[filePath];
49+
fileCoverage.s[keywordLocation]++;
50+
if (keywordLocation in fileCoverage.b) {
51+
fileCoverage.b[keywordLocation][Number(valid)]++;
3952
}
4053
}
4154

4255
/** @type NonNullable<EvaluationPlugin["afterSchema"]> */
4356
afterSchema(schemaUri) {
44-
if (!schemaUri.startsWith("file:")) {
57+
const schemaLocation = toAbsoluteIri(schemaUri);
58+
const filePath = this.#filePathFor[schemaLocation];
59+
if (!(filePath in this.coverageMap)) {
4560
return;
4661
}
4762

48-
const schemaPath = fileURLToPath(schemaUri);
49-
this.coverageMap[schemaPath].s[schemaUri]++;
50-
this.coverageMap[schemaPath].f[schemaUri]++;
51-
}
52-
53-
/** @type (ast: AST) => void */
54-
#buildCoverageMap(ast) {
55-
for (const schemaLocation in ast) {
56-
if (schemaLocation === "metaData" || schemaLocation === "plugins" || !schemaLocation.startsWith("file:")) {
57-
continue;
58-
}
59-
60-
const schemaPath = fileURLToPath(schemaLocation);
61-
62-
if (!(schemaPath in this.coverageMap)) {
63-
this.coverageMap[schemaPath] = {
64-
path: schemaPath,
65-
statementMap: {},
66-
fnMap: {},
67-
branchMap: {},
68-
s: {},
69-
f: {},
70-
b: {}
71-
};
72-
}
73-
74-
if (!(schemaPath in this.#schemaCache)) {
75-
const file = readFileSync(schemaPath, "utf8");
76-
this.#schemaCache[schemaPath] = fromJson(file);
77-
}
78-
79-
const tree = this.#schemaCache[schemaPath];
80-
const pointer = decodeURI(parseIri(schemaLocation).fragment ?? "");
81-
const node = getNodeFromPointer(tree, pointer);
82-
83-
if (!(schemaLocation in this.coverageMap[schemaPath].fnMap)) {
84-
const declRange = node.type === "json-property"
85-
? positionToRange(node.children[0].position)
86-
: {
87-
start: { line: node.position.start.line, column: node.position.start.column - 1 },
88-
end: { line: node.position.start.line, column: node.position.start.column - 1 }
89-
};
90-
91-
const locRange = positionToRange(node.position);
92-
93-
// Create statement
94-
this.coverageMap[schemaPath].statementMap[schemaLocation] = locRange;
95-
this.coverageMap[schemaPath].s[schemaLocation] = 0;
96-
97-
// Create function
98-
this.coverageMap[schemaPath].fnMap[schemaLocation] = {
99-
name: schemaLocation,
100-
decl: declRange,
101-
loc: locRange,
102-
line: node.position.start.line
103-
};
104-
this.coverageMap[schemaPath].f[schemaLocation] = 0;
105-
}
106-
107-
if (Array.isArray(ast[schemaLocation])) {
108-
for (const keywordNode of ast[schemaLocation]) {
109-
if (Array.isArray(keywordNode)) {
110-
const [keywordUri, keywordLocation] = keywordNode;
111-
112-
if (keywordLocation in this.coverageMap[schemaPath].statementMap) {
113-
continue;
114-
}
115-
116-
const pointer = decodeURI(parseIri(keywordLocation).fragment ?? "");
117-
const node = getNodeFromPointer(tree, pointer);
118-
const range = positionToRange(node.position);
119-
120-
// Create statement
121-
this.coverageMap[schemaPath].statementMap[keywordLocation] = range;
122-
this.coverageMap[schemaPath].s[keywordLocation] = 0;
123-
124-
if (annotationKeywords.has(keywordUri) || getKeyword(keywordUri).simpleApplicator) {
125-
continue;
126-
}
127-
128-
// Create branch
129-
this.coverageMap[schemaPath].branchMap[keywordLocation] = {
130-
line: range.start.line,
131-
type: "keyword",
132-
loc: range,
133-
locations: [range, range]
134-
};
135-
this.coverageMap[schemaPath].b[keywordLocation] = [0, 0];
136-
}
137-
}
138-
}
139-
}
63+
const fileCoverage = this.coverageMap[filePath];
64+
fileCoverage.s[schemaUri]++;
65+
fileCoverage.f[schemaUri]++;
14066
}
14167
}
142-
143-
/** @type (position: Position) => Range */
144-
const positionToRange = (position) => {
145-
return {
146-
start: { line: position.start.line, column: position.start.column - 1 },
147-
end: { line: position.end.line, column: position.end.column - 1 }
148-
};
149-
};
150-
151-
const annotationKeywords = new Set([
152-
"https://json-schema.org/keyword/comment",
153-
"https://json-schema.org/keyword/definitions",
154-
"https://json-schema.org/keyword/title",
155-
"https://json-schema.org/keyword/description",
156-
"https://json-schema.org/keyword/default",
157-
"https://json-schema.org/keyword/deprecated",
158-
"https://json-schema.org/keyword/readOnly",
159-
"https://json-schema.org/keyword/writeOnly",
160-
"https://json-schema.org/keyword/examples",
161-
"https://json-schema.org/keyword/format"
162-
]);

0 commit comments

Comments
 (0)