docsgov 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -0
- package/dist/apispec/apispec.js +401 -0
- package/dist/apispec/apispec.test.js +444 -0
- package/dist/apispec/errors.js +17 -0
- package/dist/apispec/index.js +2 -0
- package/dist/check/doclinks.js +167 -0
- package/dist/check/index.js +8 -0
- package/dist/check/run.js +391 -0
- package/dist/check/run.test.js +513 -0
- package/dist/check/suggest.js +134 -0
- package/dist/check/suggest.test.js +92 -0
- package/dist/check/tokens.js +125 -0
- package/dist/cmd/main.js +330 -0
- package/dist/cmd/main.test.js +422 -0
- package/dist/codeq/cache.js +71 -0
- package/dist/codeq/cache.test.js +67 -0
- package/dist/codeq/errors.js +52 -0
- package/dist/codeq/grammars/tree-sitter-go.wasm +0 -0
- package/dist/codeq/grammars/tree-sitter-java.wasm +0 -0
- package/dist/codeq/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/codeq/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/codeq/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/codeq/index.js +11 -0
- package/dist/codeq/resolve.test.js +109 -0
- package/dist/codeq/resolver.js +128 -0
- package/dist/codeq/resolver.test.js +124 -0
- package/dist/codeq/resolvers/go.js +242 -0
- package/dist/codeq/resolvers/go.test.js +143 -0
- package/dist/codeq/resolvers/java.js +349 -0
- package/dist/codeq/resolvers/java.test.js +138 -0
- package/dist/codeq/resolvers/java_queries.js +63 -0
- package/dist/codeq/resolvers/javascript.js +412 -0
- package/dist/codeq/resolvers/javascript.test.js +125 -0
- package/dist/codeq/resolvers/javascript_queries.js +46 -0
- package/dist/codeq/resolvers/typescript.js +366 -0
- package/dist/codeq/resolvers/typescript.test.js +180 -0
- package/dist/codeq/resolvers/typescript_queries.js +78 -0
- package/dist/codeq/signature.js +50 -0
- package/dist/codeq/signature.test.js +50 -0
- package/dist/codeq/suggest.js +96 -0
- package/dist/codeq/treesitter.js +122 -0
- package/dist/codeq/treesitter.test.js +118 -0
- package/dist/config/config.js +74 -0
- package/dist/config/config.test.js +98 -0
- package/dist/config/fs.js +116 -0
- package/dist/config/glob.js +82 -0
- package/dist/config/glob.test.js +61 -0
- package/dist/config/index.js +4 -0
- package/dist/dedup/analyzer/analyzer.js +533 -0
- package/dist/dedup/analyzer/analyzer.test.js +530 -0
- package/dist/dedup/analyzer/canonical.js +74 -0
- package/dist/dedup/analyzer/canonical.test.js +70 -0
- package/dist/dedup/analyzer/cosine_clusters.js +169 -0
- package/dist/dedup/analyzer/cosine_clusters.test.js +131 -0
- package/dist/dedup/analyzer/distinctive.js +85 -0
- package/dist/dedup/analyzer/distinctive.test.js +49 -0
- package/dist/dedup/analyzer/exact_clusters.js +63 -0
- package/dist/dedup/analyzer/exact_clusters.test.js +81 -0
- package/dist/dedup/analyzer/index.js +14 -0
- package/dist/dedup/analyzer/multiplicity.js +110 -0
- package/dist/dedup/analyzer/multiplicity.test.js +123 -0
- package/dist/dedup/analyzer/order.js +22 -0
- package/dist/dedup/analyzer/partial_overlaps.js +65 -0
- package/dist/dedup/analyzer/partial_overlaps.test.js +161 -0
- package/dist/dedup/analyzer/preview.js +84 -0
- package/dist/dedup/analyzer/preview.test.js +46 -0
- package/dist/dedup/analyzer/safety.js +27 -0
- package/dist/dedup/analyzer/safety.test.js +39 -0
- package/dist/dedup/config.js +18 -0
- package/dist/dedup/configload.js +299 -0
- package/dist/dedup/configload.test.js +410 -0
- package/dist/dedup/dedup.index.test.js +203 -0
- package/dist/dedup/dedup.js +143 -0
- package/dist/dedup/dedup.test.js +212 -0
- package/dist/dedup/dedupcfg/config.js +112 -0
- package/dist/dedup/dedupcfg/config.test.js +70 -0
- package/dist/dedup/dedupcfg/index.js +1 -0
- package/dist/dedup/deduptypes/index.js +1 -0
- package/dist/dedup/deduptypes/types.js +9 -0
- package/dist/dedup/deduptypes/types.test.js +34 -0
- package/dist/dedup/embedder/cache.js +23 -0
- package/dist/dedup/embedder/cache.test.js +50 -0
- package/dist/dedup/embedder/constants.js +10 -0
- package/dist/dedup/embedder/embedder.js +76 -0
- package/dist/dedup/embedder/embedder.mock.test.js +128 -0
- package/dist/dedup/embedder/embedder.test.js +96 -0
- package/dist/dedup/embedder/errors.js +20 -0
- package/dist/dedup/embedder/errors.test.js +35 -0
- package/dist/dedup/embedder/index.js +4 -0
- package/dist/dedup/embedder/session.js +78 -0
- package/dist/dedup/embedder/session.test.js +172 -0
- package/dist/dedup/gitignore.js +97 -0
- package/dist/dedup/gitignore.test.js +98 -0
- package/dist/dedup/index.js +11 -0
- package/dist/dedup/indexdb/errors.js +48 -0
- package/dist/dedup/indexdb/index.js +6 -0
- package/dist/dedup/indexdb/indexdb.js +302 -0
- package/dist/dedup/indexdb/indexdb.test.js +739 -0
- package/dist/dedup/indexdb/load.js +110 -0
- package/dist/dedup/indexdb/migrations.js +58 -0
- package/dist/dedup/indexdb/schema.js +83 -0
- package/dist/dedup/indexer/index.js +9 -0
- package/dist/dedup/indexer/indexer.js +501 -0
- package/dist/dedup/indexer/indexer.test.js +510 -0
- package/dist/dedup/indexer/links.js +89 -0
- package/dist/dedup/mdsection/anchor.js +60 -0
- package/dist/dedup/mdsection/anchor.test.js +39 -0
- package/dist/dedup/mdsection/blocks.js +409 -0
- package/dist/dedup/mdsection/blocks.test.js +359 -0
- package/dist/dedup/mdsection/index.js +4 -0
- package/dist/dedup/mdsection/parse.js +21 -0
- package/dist/dedup/mdsection/section.js +234 -0
- package/dist/dedup/mdsection/section.test.js +221 -0
- package/dist/dedup/report/floatfmt.js +71 -0
- package/dist/dedup/report/floatfmt.test.js +42 -0
- package/dist/dedup/report/index.js +8 -0
- package/dist/dedup/report/quote.js +77 -0
- package/dist/dedup/report/quote.test.js +67 -0
- package/dist/dedup/report/text.js +251 -0
- package/dist/dedup/report/text.test.js +420 -0
- package/dist/dedup/report_types.js +8 -0
- package/dist/dedup/sectionid/index.js +1 -0
- package/dist/dedup/sectionid/sectionid.js +16 -0
- package/dist/dedup/sectionid/sectionid.test.js +49 -0
- package/dist/guard/api/errors.js +12 -0
- package/dist/guard/api/index.js +2 -0
- package/dist/guard/api/parser.js +81 -0
- package/dist/guard/api/parser.test.js +58 -0
- package/dist/guard/api/types.js +1 -0
- package/dist/guard/code/errors.js +16 -0
- package/dist/guard/code/index.js +2 -0
- package/dist/guard/code/parser.js +54 -0
- package/dist/guard/code/parser.test.js +111 -0
- package/dist/guard/code/types.js +6 -0
- package/dist/index.js +1 -0
- package/dist/index.test.js +5 -0
- package/dist/repo/boundary.js +92 -0
- package/dist/repo/boundary.test.js +65 -0
- package/dist/repo/errors.js +56 -0
- package/dist/repo/errors.test.js +85 -0
- package/dist/repo/exists.test.js +72 -0
- package/dist/repo/filename.js +46 -0
- package/dist/repo/filename.test.js +39 -0
- package/dist/repo/fs.js +53 -0
- package/dist/repo/index.js +7 -0
- package/dist/repo/overlay.js +36 -0
- package/dist/repo/overlay.test.js +80 -0
- package/dist/repo/repo.js +353 -0
- package/dist/repo/repo.test.js +255 -0
- package/dist/repo/testutil.js +27 -0
- package/dist/repo/write.test.js +125 -0
- package/dist/report/color.js +73 -0
- package/dist/report/index.js +1 -0
- package/dist/report/report.js +112 -0
- package/dist/report/report.test.js +368 -0
- package/dist/violation/index.js +1 -0
- package/dist/violation/types.js +22 -0
- package/dist/violation/types.test.js +70 -0
- package/package.json +48 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
// Port of internal/check/run.go — the three-pass Run pipeline (Stage 5 of
|
|
2
|
+
// minimize-docgov) and the {{api:…}} qualifier dispatch from apitokens' caller.
|
|
3
|
+
//
|
|
4
|
+
// run() runs three independent passes (code, doc, api) over the scopes declared
|
|
5
|
+
// in cfg. Only present (non-undefined) sections are executed. Violations are
|
|
6
|
+
// collected from all passes and returned sorted by (file, line) for
|
|
7
|
+
// determinism. Only operational errors (FS walk failures, etc.) reject.
|
|
8
|
+
//
|
|
9
|
+
// FS RECONCILIATION: this is the integration point. config and repo now share
|
|
10
|
+
// repo's ASYNC FS (see config/fs.ts), so run() — and every pass — is async.
|
|
11
|
+
import { parse as parseSpec } from "../apispec/index.js";
|
|
12
|
+
import { inScope, walkMarkdown } from "../config/index.js";
|
|
13
|
+
import { parseApiRef } from "../guard/api/index.js";
|
|
14
|
+
import { parseCodeRef } from "../guard/code/index.js";
|
|
15
|
+
import { MalformedRefError as ApiMalformedRefError } from "../guard/api/errors.js";
|
|
16
|
+
import { MalformedRefError as CodeMalformedRefError } from "../guard/code/errors.js";
|
|
17
|
+
import { isNotExist } from "../repo/index.js";
|
|
18
|
+
import { Rules } from "../violation/index.js";
|
|
19
|
+
import { checkDocLinks } from "./doclinks.js";
|
|
20
|
+
import { suggestionSuffix } from "./suggest.js";
|
|
21
|
+
import { iterApiTokens, iterCodeTokens } from "./tokens.js";
|
|
22
|
+
/**
|
|
23
|
+
* run executes three independent guard passes (code, doc, api) as configured in
|
|
24
|
+
* cfg, collecting all violations. Passes for absent sections are skipped.
|
|
25
|
+
* Returns violations sorted by (file, line). Only operational errors (FS walk
|
|
26
|
+
* failures, etc.) reject.
|
|
27
|
+
*/
|
|
28
|
+
export async function run(cfg, r, resolver) {
|
|
29
|
+
const fsys = r.fs();
|
|
30
|
+
let vs = [];
|
|
31
|
+
if (cfg.Code !== undefined) {
|
|
32
|
+
vs = vs.concat(await runCodePass(cfg.Code, fsys, resolver));
|
|
33
|
+
}
|
|
34
|
+
if (cfg.Doc !== undefined) {
|
|
35
|
+
vs = vs.concat(await runDocPass(cfg.Doc, fsys));
|
|
36
|
+
}
|
|
37
|
+
if (cfg.API !== undefined) {
|
|
38
|
+
vs = vs.concat(await runAPIPass(cfg.API, fsys));
|
|
39
|
+
}
|
|
40
|
+
vs.sort((a, b) => {
|
|
41
|
+
if (a.file !== b.file) {
|
|
42
|
+
return a.file < b.file ? -1 : 1;
|
|
43
|
+
}
|
|
44
|
+
return a.line - b.line;
|
|
45
|
+
});
|
|
46
|
+
return vs;
|
|
47
|
+
}
|
|
48
|
+
/** decodeBytes turns raw file bytes into a string (CRLF already normalised by repo). */
|
|
49
|
+
function decodeBytes(data) {
|
|
50
|
+
return new TextDecoder().decode(data);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* runCodePass executes the code guard pass over all .md files in scope.boundary.
|
|
54
|
+
* For each {{code:…}} token: parse it, check the path is under scope.source, then
|
|
55
|
+
* resolve it. No status/sigil gating.
|
|
56
|
+
*/
|
|
57
|
+
async function runCodePass(scope, fsys, resolver) {
|
|
58
|
+
const files = await walkMarkdown(fsys, scope.boundary);
|
|
59
|
+
const vs = [];
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
let src;
|
|
62
|
+
try {
|
|
63
|
+
src = decodeBytes(await fsys.readFile(file));
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
vs.push({
|
|
67
|
+
rule: Rules.guardCode,
|
|
68
|
+
file,
|
|
69
|
+
line: 0,
|
|
70
|
+
sectionID: "",
|
|
71
|
+
expected: "",
|
|
72
|
+
actual: "",
|
|
73
|
+
message: "cannot read file: " + errMsg(err),
|
|
74
|
+
});
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const loc of iterCodeTokens(src)) {
|
|
78
|
+
const v = await checkCodeToken(file, loc, scope.source, fsys, resolver);
|
|
79
|
+
if (v !== null) {
|
|
80
|
+
vs.push(v);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return vs;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* checkCodeToken evaluates one code token: parse, source-check, resolve. Returns
|
|
88
|
+
* null when the token passes all checks.
|
|
89
|
+
*/
|
|
90
|
+
async function checkCodeToken(file, loc, source, fsys, resolver) {
|
|
91
|
+
let ref;
|
|
92
|
+
try {
|
|
93
|
+
ref = parseCodeRef(loc.token);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (err instanceof CodeMalformedRefError) {
|
|
97
|
+
return {
|
|
98
|
+
rule: Rules.guardCode,
|
|
99
|
+
file,
|
|
100
|
+
line: loc.line,
|
|
101
|
+
sectionID: "",
|
|
102
|
+
expected: "",
|
|
103
|
+
actual: loc.token,
|
|
104
|
+
message: "malformed code ref: " + err.message,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
if (!inScope(source, ref.Path)) {
|
|
110
|
+
return {
|
|
111
|
+
rule: Rules.guardCode,
|
|
112
|
+
file,
|
|
113
|
+
line: loc.line,
|
|
114
|
+
sectionID: "",
|
|
115
|
+
expected: "under code.source",
|
|
116
|
+
actual: loc.token,
|
|
117
|
+
message: "ref points outside source",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
let found;
|
|
121
|
+
try {
|
|
122
|
+
found = await resolver.resolve(fsys, ref);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
rule: Rules.guardCode,
|
|
127
|
+
file,
|
|
128
|
+
line: loc.line,
|
|
129
|
+
sectionID: "",
|
|
130
|
+
expected: "",
|
|
131
|
+
actual: loc.token,
|
|
132
|
+
message: "resolver error: " + errMsg(err),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (!found) {
|
|
136
|
+
let message = "referenced symbol not found";
|
|
137
|
+
// Best-effort "did you mean": never let a suggestion fault drop the
|
|
138
|
+
// violation itself — the base message must always survive.
|
|
139
|
+
if (resolver.suggest !== undefined) {
|
|
140
|
+
try {
|
|
141
|
+
message += suggestionSuffix(ref, await resolver.suggest(fsys, ref));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// keep the base message
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
rule: Rules.guardCode,
|
|
149
|
+
file,
|
|
150
|
+
line: loc.line,
|
|
151
|
+
sectionID: "",
|
|
152
|
+
expected: "symbol exists",
|
|
153
|
+
actual: loc.token,
|
|
154
|
+
message,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* runDocPass executes the doc existence pass over all .md files in
|
|
161
|
+
* scope.boundary. It collects every link and image destination from the mdast
|
|
162
|
+
* tree, then applies the existence algorithm (URL/fragment skip, absolute
|
|
163
|
+
* reject, escape reject, existence check).
|
|
164
|
+
*/
|
|
165
|
+
async function runDocPass(scope, fsys) {
|
|
166
|
+
const files = await walkMarkdown(fsys, scope.boundary);
|
|
167
|
+
const vs = [];
|
|
168
|
+
for (const file of files) {
|
|
169
|
+
let src;
|
|
170
|
+
try {
|
|
171
|
+
src = decodeBytes(await fsys.readFile(file));
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
vs.push({
|
|
175
|
+
rule: Rules.guardDocs,
|
|
176
|
+
file,
|
|
177
|
+
line: 0,
|
|
178
|
+
sectionID: "",
|
|
179
|
+
expected: "",
|
|
180
|
+
actual: "",
|
|
181
|
+
message: "cannot read file: " + errMsg(err),
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const fv = await checkDocLinks(file, src, fsys);
|
|
186
|
+
for (const v of fv) {
|
|
187
|
+
vs.push(v);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return vs;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* runAPIPass executes the api guard pass:
|
|
194
|
+
* 1. Load all .json specs under scope.source.
|
|
195
|
+
* 2. For each .md in scope.boundary, scan {{api:…}} tokens and validate.
|
|
196
|
+
*/
|
|
197
|
+
async function runAPIPass(scope, fsys) {
|
|
198
|
+
const { specs, specVs } = await loadAPISpecs(scope.source, fsys);
|
|
199
|
+
// If there were spec-load violations but some specs loaded, continue checking.
|
|
200
|
+
// If no specs loaded at all (and no load error), add a zero-spec violation.
|
|
201
|
+
const vs = [...specVs];
|
|
202
|
+
if (specs.length === 0 && specVs.length === 0) {
|
|
203
|
+
vs.push({
|
|
204
|
+
rule: Rules.guardAPI,
|
|
205
|
+
file: "",
|
|
206
|
+
line: 0,
|
|
207
|
+
sectionID: "",
|
|
208
|
+
expected: "",
|
|
209
|
+
actual: "",
|
|
210
|
+
message: "no OpenAPI spec found under api.source",
|
|
211
|
+
});
|
|
212
|
+
// Without specs we can still scan tokens for malformed-ref errors.
|
|
213
|
+
}
|
|
214
|
+
const files = await walkMarkdown(fsys, scope.boundary);
|
|
215
|
+
for (const file of files) {
|
|
216
|
+
let src;
|
|
217
|
+
try {
|
|
218
|
+
src = decodeBytes(await fsys.readFile(file));
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
vs.push({
|
|
222
|
+
rule: Rules.guardAPI,
|
|
223
|
+
file,
|
|
224
|
+
line: 0,
|
|
225
|
+
sectionID: "",
|
|
226
|
+
expected: "",
|
|
227
|
+
actual: "",
|
|
228
|
+
message: "cannot read file: " + errMsg(err),
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const loc of iterApiTokens(src)) {
|
|
233
|
+
for (const v of checkAPIToken(file, loc, specs)) {
|
|
234
|
+
vs.push(v);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return vs;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* loadAPISpecs enumerates every .json file under source patterns, loads each,
|
|
242
|
+
* and returns the loaded specs plus any per-file load violations.
|
|
243
|
+
*/
|
|
244
|
+
async function loadAPISpecs(source, fsys) {
|
|
245
|
+
const specs = [];
|
|
246
|
+
const specVs = [];
|
|
247
|
+
// Collect every .json path in the tree (slash paths), then filter by scope.
|
|
248
|
+
const jsonPaths = [];
|
|
249
|
+
async function descend(dirSlash) {
|
|
250
|
+
let entries;
|
|
251
|
+
try {
|
|
252
|
+
entries = await fsys.readDir(dirSlash === "" ? "." : dirSlash);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
if (isNotExist(err)) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
260
|
+
for (const entry of entries) {
|
|
261
|
+
const childSlash = dirSlash === "" ? entry.name() : `${dirSlash}/${entry.name()}`;
|
|
262
|
+
if (entry.isDir()) {
|
|
263
|
+
await descend(childSlash);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (childSlash.endsWith(".json")) {
|
|
267
|
+
jsonPaths.push(childSlash);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
await descend("");
|
|
272
|
+
for (const p of jsonPaths) {
|
|
273
|
+
if (!inScope(source, p)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
let s;
|
|
277
|
+
try {
|
|
278
|
+
const data = decodeBytes(await fsys.readFile(p));
|
|
279
|
+
s = parseSpec(data, p);
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
specVs.push({
|
|
283
|
+
rule: Rules.guardAPI,
|
|
284
|
+
file: p,
|
|
285
|
+
line: 0,
|
|
286
|
+
sectionID: "",
|
|
287
|
+
expected: "",
|
|
288
|
+
actual: "",
|
|
289
|
+
message: errMsg(err),
|
|
290
|
+
});
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
specs.push(s);
|
|
294
|
+
}
|
|
295
|
+
return { specs, specVs };
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* checkAPIToken validates one {{api:…}} token against the loaded specs. Returns
|
|
299
|
+
* one violation when: malformed, operation missing, or qualifier not satisfied.
|
|
300
|
+
*/
|
|
301
|
+
function checkAPIToken(file, loc, specs) {
|
|
302
|
+
let ref;
|
|
303
|
+
try {
|
|
304
|
+
ref = parseApiRef(loc.token);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (err instanceof ApiMalformedRefError) {
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
rule: Rules.guardAPI,
|
|
311
|
+
file,
|
|
312
|
+
line: loc.line,
|
|
313
|
+
sectionID: "",
|
|
314
|
+
expected: "",
|
|
315
|
+
actual: loc.token,
|
|
316
|
+
message: "malformed api ref: " + err.message,
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
// Check that at least one spec has the operation.
|
|
323
|
+
let opFound = false;
|
|
324
|
+
for (const s of specs) {
|
|
325
|
+
if (s.hasOperation(ref.Method, ref.Path)) {
|
|
326
|
+
opFound = true;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!opFound) {
|
|
331
|
+
return [
|
|
332
|
+
{
|
|
333
|
+
rule: Rules.guardAPI,
|
|
334
|
+
file,
|
|
335
|
+
line: loc.line,
|
|
336
|
+
sectionID: "",
|
|
337
|
+
expected: "",
|
|
338
|
+
actual: loc.token,
|
|
339
|
+
message: `operation ${ref.Method} ${ref.Path} not found in spec`,
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
}
|
|
343
|
+
// No qualifier — operation existence is sufficient.
|
|
344
|
+
if (ref.Qualifier === null) {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
// Qualifier dispatch: pass if ANY spec satisfies it.
|
|
348
|
+
const name = ref.Qualifier.Path.join(".");
|
|
349
|
+
let satisfied = false;
|
|
350
|
+
for (const s of specs) {
|
|
351
|
+
if (qualifierSatisfied(s, ref, name)) {
|
|
352
|
+
satisfied = true;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!satisfied) {
|
|
357
|
+
return [
|
|
358
|
+
{
|
|
359
|
+
rule: Rules.guardAPI,
|
|
360
|
+
file,
|
|
361
|
+
line: loc.line,
|
|
362
|
+
sectionID: "",
|
|
363
|
+
expected: "",
|
|
364
|
+
actual: loc.token,
|
|
365
|
+
message: `${ref.Qualifier.Kind} ${name} not found on ${ref.Method} ${ref.Path}`,
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
/** qualifierSatisfied checks whether the given spec satisfies the qualifier on ref. */
|
|
372
|
+
function qualifierSatisfied(s, ref, name) {
|
|
373
|
+
const q = ref.Qualifier;
|
|
374
|
+
if (q === null) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
switch (q.Kind) {
|
|
378
|
+
case "param":
|
|
379
|
+
return s.hasParam(ref.Method, ref.Path, name);
|
|
380
|
+
case "header":
|
|
381
|
+
return s.hasHeader(ref.Method, ref.Path, name);
|
|
382
|
+
case "body":
|
|
383
|
+
return s.hasBodyField(ref.Method, ref.Path, q.Path);
|
|
384
|
+
case "response":
|
|
385
|
+
return s.hasResponseField(ref.Method, ref.Path, q.Path);
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
function errMsg(err) {
|
|
390
|
+
return err instanceof Error ? err.message : String(err);
|
|
391
|
+
}
|