|
1 | | -import { readFileSync } from "node:fs"; |
| 1 | +import { existsSync, readFileSync } from "node:fs"; |
2 | 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"; |
| 3 | +import { toAbsoluteIri } from "@hyperjump/uri"; |
| 4 | +import { createHash } from "node:crypto"; |
| 5 | +import { resolve } from "node:path"; |
6 | 6 |
|
7 | 7 | /** |
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" |
| 8 | + * @import { CoverageMapData } from "istanbul-lib-coverage" |
| 9 | + * @import { EvaluationPlugin } from "@hyperjump/json-schema/experimental" |
12 | 10 | */ |
13 | 11 |
|
14 | 12 | /** @implements EvaluationPlugin */ |
15 | 13 | export class TestCoverageEvaluationPlugin { |
16 | | - /** @type Record<string, JsonNode> */ |
17 | | - #schemaCache = {}; |
| 14 | + /** @type Record<string, string> */ |
| 15 | + #filePathFor = {}; |
18 | 16 |
|
19 | 17 | constructor() { |
20 | 18 | /** @type CoverageMapData */ |
21 | 19 | this.coverageMap = {}; |
22 | 20 | } |
23 | 21 |
|
24 | 22 | /** @type NonNullable<EvaluationPlugin["beforeSchema"]> */ |
25 | | - beforeSchema(_schemaUri, _instance, context) { |
26 | | - this.#buildCoverageMap(context.ast); |
| 23 | + beforeSchema(schemaUri) { |
| 24 | + const schemaLocation = toAbsoluteIri(schemaUri); |
| 25 | + if (!(schemaLocation in this.#filePathFor)) { |
| 26 | + const fileHash = createHash("md5").update(`${schemaLocation}#`).digest("hex"); |
| 27 | + const coverageFilePath = resolve(".json-schema-coverage", fileHash); |
| 28 | + |
| 29 | + if (existsSync(coverageFilePath)) { |
| 30 | + const json = readFileSync(coverageFilePath, "utf-8"); |
| 31 | + /** @type CoverageMapData */ |
| 32 | + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment |
| 33 | + const coverageMapData = JSON.parse(json); |
| 34 | + const fileCoveragePath = Object.keys(coverageMapData)[0]; |
| 35 | + Object.assign(this.coverageMap, coverageMapData); |
| 36 | + this.#filePathFor[schemaLocation] = fileCoveragePath; |
| 37 | + } |
| 38 | + } |
27 | 39 | } |
28 | 40 |
|
29 | 41 | /** @type NonNullable<EvaluationPlugin["afterKeyword"]> */ |
30 | 42 | afterKeyword([, keywordLocation], _instance, _context, valid) { |
31 | | - if (!keywordLocation.startsWith("file:")) { |
| 43 | + const schemaLocation = toAbsoluteIri(keywordLocation); |
| 44 | + const filePath = this.#filePathFor[schemaLocation]; |
| 45 | + if (!(filePath in this.coverageMap)) { |
32 | 46 | return; |
33 | 47 | } |
34 | 48 |
|
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)]++; |
| 49 | + const fileCoverage = this.coverageMap[filePath]; |
| 50 | + fileCoverage.s[keywordLocation]++; |
| 51 | + if (keywordLocation in fileCoverage.b) { |
| 52 | + fileCoverage.b[keywordLocation][Number(valid)]++; |
39 | 53 | } |
40 | 54 | } |
41 | 55 |
|
42 | 56 | /** @type NonNullable<EvaluationPlugin["afterSchema"]> */ |
43 | 57 | afterSchema(schemaUri) { |
44 | | - if (!schemaUri.startsWith("file:")) { |
| 58 | + const schemaLocation = toAbsoluteIri(schemaUri); |
| 59 | + const filePath = fileURLToPath(schemaLocation); |
| 60 | + if (!(filePath in this.coverageMap)) { |
45 | 61 | return; |
46 | 62 | } |
47 | 63 |
|
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 | | - } |
| 64 | + const fileCoverage = this.coverageMap[filePath]; |
| 65 | + fileCoverage.s[schemaUri]++; |
| 66 | + fileCoverage.f[schemaUri]++; |
140 | 67 | } |
141 | 68 | } |
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