|
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"; |
6 | 5 |
|
7 | 6 | /**
|
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" |
12 | 9 | */
|
13 | 10 |
|
14 | 11 | /** @implements EvaluationPlugin */
|
15 | 12 | export class TestCoverageEvaluationPlugin {
|
16 |
| - /** @type Record<string, JsonNode> */ |
17 |
| - #schemaCache = {}; |
| 13 | + /** @type Record<string, string> */ |
| 14 | + #filePathFor = {}; |
18 | 15 |
|
19 | 16 | constructor() {
|
20 | 17 | /** @type CoverageMapData */
|
21 | 18 | this.coverageMap = {};
|
22 | 19 | }
|
23 | 20 |
|
24 | 21 | /** @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 | + } |
27 | 38 | }
|
28 | 39 |
|
29 | 40 | /** @type NonNullable<EvaluationPlugin["afterKeyword"]> */
|
30 | 41 | 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)) { |
32 | 45 | return;
|
33 | 46 | }
|
34 | 47 |
|
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)]++; |
39 | 52 | }
|
40 | 53 | }
|
41 | 54 |
|
42 | 55 | /** @type NonNullable<EvaluationPlugin["afterSchema"]> */
|
43 | 56 | afterSchema(schemaUri) {
|
44 |
| - if (!schemaUri.startsWith("file:")) { |
| 57 | + const schemaLocation = toAbsoluteIri(schemaUri); |
| 58 | + const filePath = this.#filePathFor[schemaLocation]; |
| 59 | + if (!(filePath in this.coverageMap)) { |
45 | 60 | return;
|
46 | 61 | }
|
47 | 62 |
|
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]++; |
140 | 66 | }
|
141 | 67 | }
|
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