Skip to content

Commit 0829ffb

Browse files
committed
Refactor to use CoverageMapService
1 parent 134a08e commit 0829ffb

16 files changed

+477
-347
lines changed

src/coverage-map-service.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { CoverageMapData } from "istanbul-lib-coverage";
2+
3+
export class CoverageMapService {
4+
addFromFile(schemaPath: string): Promise<string>;
5+
addCoverageMap(coverageMap: CoverageMapData): void;
6+
getSchemaPath(schemaUri: string): string;
7+
getCoverageMap(schemaUri: string): CoverageMapData;
8+
}

src/coverage-map-service.js

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
import { readFile } from "node:fs/promises";
2+
import { extname } from "node:path";
3+
import { compile, getKeyword, getSchema } from "@hyperjump/json-schema/experimental";
4+
import { jrefTypeOf, Reference } from "@hyperjump/browser/jref";
5+
import * as JsonPointer from "@hyperjump/json-pointer";
6+
import { parseIri, toAbsoluteIri } from "@hyperjump/uri";
7+
import { fromJson, fromYaml, getNodeFromPointer } from "./json-util.js";
8+
9+
/**
10+
* @import { CoverageMapData, FileCoverageData, Range } from "istanbul-lib-coverage"
11+
* @import { Position } from "unist"
12+
* @import { CompiledSchema } from "@hyperjump/json-schema/experimental";
13+
* @import { JRef } from "@hyperjump/browser/jref"
14+
* @import { JsonNode } from "./jsonast.js"
15+
*/
16+
17+
export class CoverageMapService {
18+
/** @type Record<string, CoverageMapData> */
19+
#coverageMaps = {};
20+
21+
/** @type Record<string, string> */
22+
#filePathFor = {};
23+
24+
#nonStatementKeywords = new Set([
25+
"https://json-schema.org/keyword/comment",
26+
"https://json-schema.org/keyword/definitions"
27+
]);
28+
29+
#nonBranchingKeywords = new Set([
30+
"https://json-schema.org/keyword/title",
31+
"https://json-schema.org/keyword/description",
32+
"https://json-schema.org/keyword/default",
33+
"https://json-schema.org/keyword/deprecated",
34+
"https://json-schema.org/keyword/readOnly",
35+
"https://json-schema.org/keyword/writeOnly",
36+
"https://json-schema.org/keyword/examples",
37+
"https://json-schema.org/keyword/format",
38+
"https://json-schema.org/keyword/if"
39+
]);
40+
41+
/** @type (schemaPath: string) => Promise<string> */
42+
async addFromFile(schemaPath) {
43+
const schema = await getSchema(schemaPath);
44+
const compiledSchema = await compile(schema);
45+
const tree = await this.#parseToAst(schemaPath);
46+
47+
/** @type Record<string, JsonNode> */
48+
const schemaNodes = {};
49+
for (const schemaUri in schema.document.embedded ?? {}) {
50+
const pointer = this.#findEmbedded(schema.document.root, schemaUri);
51+
schemaNodes[schemaUri] = getNodeFromPointer(tree, pointer);
52+
}
53+
const coverageMap = this.#astToCoverageMap(compiledSchema, schemaPath, schemaNodes);
54+
this.addCoverageMap(coverageMap);
55+
56+
return compiledSchema.schemaUri;
57+
}
58+
59+
/** @type (coverageMap: CoverageMapData) => void */
60+
addCoverageMap(coverageMap) {
61+
for (const filePath in coverageMap) {
62+
const [schemaUri] = Object.keys(coverageMap[filePath].statementMap);
63+
this.#coverageMaps[schemaUri] = coverageMap;
64+
65+
for (const fileCoveragePath in coverageMap) {
66+
for (const location in coverageMap[fileCoveragePath].statementMap) {
67+
this.#filePathFor[location] = fileCoveragePath;
68+
}
69+
}
70+
}
71+
}
72+
73+
/** @type (schemaUri: string) => string */
74+
getSchemaPath(schemaUri) {
75+
return this.#filePathFor[schemaUri];
76+
}
77+
78+
/** @type (schemaUri: string) => CoverageMapData */
79+
getCoverageMap(schemaUri) {
80+
return this.#coverageMaps[schemaUri];
81+
}
82+
83+
/** @type (schemaPath: string) => Promise<JsonNode> */
84+
async #parseToAst(schemaPath) {
85+
const text = await readFile(schemaPath, "utf-8");
86+
const extension = extname(schemaPath);
87+
88+
switch (extension) {
89+
case ".json":
90+
return fromJson(text);
91+
case ".yaml":
92+
case ".yml":
93+
return fromYaml(text);
94+
default:
95+
throw Error(`File of type '${extension}' is not supported.`);
96+
}
97+
};
98+
99+
/** @type (root: JRef, uri: string) => string */
100+
#findEmbedded = (root, uri) => {
101+
for (const [pointer, node] of this.#allSchemaNodes(root, uri)) {
102+
if (node instanceof Reference) {
103+
const json = node.toJSON();
104+
if (typeof json === "object" && json !== null && !("$ref" in json) && node.href === uri) {
105+
return pointer;
106+
}
107+
}
108+
}
109+
110+
return "";
111+
};
112+
113+
/** @type (node: JRef, uri: string, pointer?: string) => Generator<[string, JRef]> */
114+
* #allSchemaNodes(node, uri, pointer = "") {
115+
yield [pointer, node];
116+
117+
switch (jrefTypeOf(node)) {
118+
case "object":
119+
const jrefObject = /** @type Record<string, JRef> */ (node);
120+
for (const key in jrefObject) {
121+
yield* this.#allSchemaNodes(jrefObject[key], uri, JsonPointer.append(key, pointer));
122+
}
123+
break;
124+
125+
case "array":
126+
const jrefArray = /** @type JRef[] */ (node);
127+
let index = 0;
128+
for (const item of jrefArray) {
129+
yield* this.#allSchemaNodes(item, uri, JsonPointer.append(`${index++}`, pointer));
130+
}
131+
break;
132+
}
133+
};
134+
135+
/** @type (compiledSchema: CompiledSchema, schemaPath: string, schemaNodes: Record<string, JsonNode>) => CoverageMapData */
136+
#astToCoverageMap(compiledSchema, schemaPath, schemaNodes) {
137+
/** @type FileCoverageData */
138+
const fileCoverage = {
139+
path: schemaPath,
140+
statementMap: {},
141+
branchMap: {},
142+
fnMap: {},
143+
s: {},
144+
b: {},
145+
f: {}
146+
};
147+
148+
for (const schemaLocation in compiledSchema.ast) {
149+
if (schemaLocation === "metaData" || schemaLocation === "plugins") {
150+
continue;
151+
}
152+
153+
if (!(toAbsoluteIri(schemaLocation) in schemaNodes)) {
154+
continue;
155+
}
156+
157+
const pointer = decodeURI(parseIri(schemaLocation).fragment ?? "");
158+
const node = getNodeFromPointer(schemaNodes[toAbsoluteIri(schemaLocation)], pointer, true);
159+
160+
const declRange = node.type === "json-property"
161+
? this.#positionToRange(node.children[0].position)
162+
: {
163+
start: { line: node.position.start.line, column: node.position.start.column - 1 },
164+
end: { line: node.position.start.line, column: node.position.start.column - 1 }
165+
};
166+
167+
const locRange = this.#positionToRange(node.position);
168+
169+
// Create statement
170+
fileCoverage.statementMap[schemaLocation] = locRange;
171+
fileCoverage.s[schemaLocation] = 0;
172+
173+
// Create function
174+
fileCoverage.fnMap[schemaLocation] = {
175+
name: schemaLocation,
176+
decl: declRange,
177+
loc: locRange,
178+
line: node.position.start.line
179+
};
180+
fileCoverage.f[schemaLocation] = 0;
181+
182+
if (Array.isArray(compiledSchema.ast[schemaLocation])) {
183+
for (const keywordNode of compiledSchema.ast[schemaLocation]) {
184+
if (Array.isArray(keywordNode)) {
185+
const [keywordUri, keywordLocation] = keywordNode;
186+
187+
if (this.#nonStatementKeywords.has(keywordUri)) {
188+
continue;
189+
}
190+
191+
const pointer = decodeURI(parseIri(keywordLocation).fragment ?? "");
192+
const node = getNodeFromPointer(schemaNodes[toAbsoluteIri(keywordLocation)], pointer, true);
193+
const range = this.#positionToRange(node.position);
194+
195+
// Create statement
196+
fileCoverage.statementMap[keywordLocation] = range;
197+
fileCoverage.s[keywordLocation] = 0;
198+
199+
if (this.#nonBranchingKeywords.has(keywordUri) || getKeyword(keywordUri).simpleApplicator) {
200+
continue;
201+
}
202+
203+
// Create branch
204+
fileCoverage.branchMap[keywordLocation] = {
205+
line: range.start.line,
206+
type: "keyword",
207+
loc: range,
208+
locations: [range, range]
209+
};
210+
fileCoverage.b[keywordLocation] = [0, 0];
211+
}
212+
}
213+
}
214+
}
215+
216+
return { [schemaPath]: fileCoverage };
217+
};
218+
219+
/** @type (position: Position) => Range */
220+
#positionToRange(position) {
221+
return {
222+
start: { line: position.start.line, column: position.start.column - 1 },
223+
end: { line: position.end.line, column: position.end.column - 1 }
224+
};
225+
};
226+
}

src/coverage-util.d.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)