@webpieces/nx-webpieces-rules 0.4.808 → 0.4.810
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/package.json +7 -6
- package/src/executors/generate/executor.js +5 -0
- package/src/executors/generate/executor.js.map +1 -1
- package/src/lib/api-usage/api-contract-errors.d.ts +59 -0
- package/src/lib/api-usage/api-contract-errors.js +129 -1
- package/src/lib/api-usage/api-contract-errors.js.map +1 -1
- package/src/lib/api-usage/api-doc-rules-scan.d.ts +115 -0
- package/src/lib/api-usage/api-doc-rules-scan.js +372 -0
- package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.d.ts +109 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.js +323 -0
- package/src/lib/api-usage/api-doc-rules-verdicts.js.map +1 -0
- package/src/lib/api-usage/api-doc-rules.d.ts +186 -0
- package/src/lib/api-usage/api-doc-rules.js +298 -0
- package/src/lib/api-usage/api-doc-rules.js.map +1 -0
- package/src/lib/api-usage/api-scanner.d.ts +15 -1
- package/src/lib/api-usage/api-scanner.js +22 -2
- package/src/lib/api-usage/api-scanner.js.map +1 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).
|
|
4
|
+
*
|
|
5
|
+
* The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the
|
|
6
|
+
* per-rule config, so a unit test can construct either without touching a config file and so the
|
|
7
|
+
* config is read exactly once per executor run.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.DisableComment = exports.ApiDocRule = exports.AFFECTED_PROJECT_MODE = exports.ApiDocRulesFindings = exports.McpExclusion = exports.ApiRuleFindings = exports.ApiContractDefect = exports.EXTERNAL_CUSTOMER_API_TYPE = exports.MCP_RULE = exports.OPENAPI_RULE = void 0;
|
|
11
|
+
const tslib_1 = require("tslib");
|
|
12
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
13
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
14
|
+
const rule_gate_1 = require("../rule-gate");
|
|
15
|
+
/** As written in a disable comment and as a config key. */
|
|
16
|
+
exports.OPENAPI_RULE = rules_config_1.RULE_NAMES.API_RULES_FOR_OPENAPI;
|
|
17
|
+
exports.MCP_RULE = rules_config_1.RULE_NAMES.API_RULES_FOR_MCP;
|
|
18
|
+
/** The `@ApiType` value that means "a partner reads this document". */
|
|
19
|
+
exports.EXTERNAL_CUSTOMER_API_TYPE = 'external-customer';
|
|
20
|
+
/**
|
|
21
|
+
* `// webpieces-disable <rule>[, <rule2>] -- <reason>`, with the reason CAPTURED so a reasonless
|
|
22
|
+
* disable can be told apart from an absent one. Identical to the spelling `root-union-scan` reads.
|
|
23
|
+
*/
|
|
24
|
+
const DISABLE_RE = new RegExp(`//\\s*${rules_config_1.WEBPIECES_DISABLE}\\s+([\\w-]+(?:\\s*,\\s*[\\w-]+)*)(?:\\s*--\\s*(.*))?$`);
|
|
25
|
+
/** How far above a declaration a disable comment may sit before it is somebody else's comment. */
|
|
26
|
+
const DISABLE_LOOKBACK_LINES = 40;
|
|
27
|
+
/**
|
|
28
|
+
* ONE defect on ONE contract.
|
|
29
|
+
*
|
|
30
|
+
* `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the
|
|
31
|
+
* SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a
|
|
32
|
+
* partner document costs a partner something, where the same field on a service-to-service contract
|
|
33
|
+
* costs a colleague a question. The refusal sorts and labels on it.
|
|
34
|
+
*/
|
|
35
|
+
class ApiContractDefect {
|
|
36
|
+
api;
|
|
37
|
+
method;
|
|
38
|
+
what;
|
|
39
|
+
at;
|
|
40
|
+
cure;
|
|
41
|
+
exposure;
|
|
42
|
+
constructor(
|
|
43
|
+
/** The `@ApiPath` contract class. */
|
|
44
|
+
api,
|
|
45
|
+
/** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */
|
|
46
|
+
method,
|
|
47
|
+
/** What is wrong, in one line, naming the declaration. */
|
|
48
|
+
what,
|
|
49
|
+
/** `path/to/File.ts:LINE`, workspace-relative. */
|
|
50
|
+
at,
|
|
51
|
+
/** What to do instead, in one sentence. */
|
|
52
|
+
cure,
|
|
53
|
+
/** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */
|
|
54
|
+
exposure) {
|
|
55
|
+
this.api = api;
|
|
56
|
+
this.method = method;
|
|
57
|
+
this.what = what;
|
|
58
|
+
this.at = at;
|
|
59
|
+
this.cure = cure;
|
|
60
|
+
this.exposure = exposure;
|
|
61
|
+
}
|
|
62
|
+
/** True when this contract feeds the PARTNER-facing document. */
|
|
63
|
+
isExternal() {
|
|
64
|
+
return this.exposure.includes(exports.EXTERNAL_CUSTOMER_API_TYPE);
|
|
65
|
+
}
|
|
66
|
+
/** `Api.method` or `Api` — what a reader opens. */
|
|
67
|
+
where() {
|
|
68
|
+
return this.method === '' ? this.api : `${this.api}.${this.method}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.ApiContractDefect = ApiContractDefect;
|
|
72
|
+
/** What ONE rule found: real defects, and disables of it that gave no reason. */
|
|
73
|
+
class ApiRuleFindings {
|
|
74
|
+
violations;
|
|
75
|
+
reasonlessDisables;
|
|
76
|
+
constructor(violations,
|
|
77
|
+
/**
|
|
78
|
+
* Sites that named this rule in a disable and wrote no reason. A reasonless disable is
|
|
79
|
+
* itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only
|
|
80
|
+
* part the next reader can weigh.
|
|
81
|
+
*/
|
|
82
|
+
reasonlessDisables) {
|
|
83
|
+
this.violations = violations;
|
|
84
|
+
this.reasonlessDisables = reasonlessDisables;
|
|
85
|
+
}
|
|
86
|
+
isEmpty() {
|
|
87
|
+
return this.violations.length === 0 && this.reasonlessDisables.length === 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.ApiRuleFindings = ApiRuleFindings;
|
|
91
|
+
/**
|
|
92
|
+
* ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).
|
|
93
|
+
*
|
|
94
|
+
* Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the
|
|
95
|
+
* findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every
|
|
96
|
+
* run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read
|
|
97
|
+
* again, and a build-time warning at add time would be read exactly as little.
|
|
98
|
+
*/
|
|
99
|
+
class McpExclusion {
|
|
100
|
+
api;
|
|
101
|
+
method;
|
|
102
|
+
reason;
|
|
103
|
+
at;
|
|
104
|
+
constructor(api, method,
|
|
105
|
+
/** The decorator's reason, verbatim. Empty only when the argument could not be folded. */
|
|
106
|
+
reason,
|
|
107
|
+
/** `path/to/File.ts:LINE`, workspace-relative. */
|
|
108
|
+
at) {
|
|
109
|
+
this.api = api;
|
|
110
|
+
this.method = method;
|
|
111
|
+
this.reason = reason;
|
|
112
|
+
this.at = at;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.McpExclusion = McpExclusion;
|
|
116
|
+
/** Both rules' findings from ONE scan — one program build, two verdicts. */
|
|
117
|
+
class ApiDocRulesFindings {
|
|
118
|
+
openApi;
|
|
119
|
+
mcp;
|
|
120
|
+
mcpExclusions;
|
|
121
|
+
constructor(openApi, mcp,
|
|
122
|
+
/** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */
|
|
123
|
+
mcpExclusions = []) {
|
|
124
|
+
this.openApi = openApi;
|
|
125
|
+
this.mcp = mcp;
|
|
126
|
+
this.mcpExclusions = mcpExclusions;
|
|
127
|
+
}
|
|
128
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
129
|
+
static empty() {
|
|
130
|
+
return new ApiDocRulesFindings(new ApiRuleFindings([], []), new ApiRuleFindings([], []), []);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.ApiDocRulesFindings = ApiDocRulesFindings;
|
|
134
|
+
/** The mode value that narrows the scan to the projects the diff touched. */
|
|
135
|
+
exports.AFFECTED_PROJECT_MODE = 'AFFECTED_PROJECT';
|
|
136
|
+
/**
|
|
137
|
+
* ONE rule's switches, resolved from webpieces.config.json once per scan.
|
|
138
|
+
*
|
|
139
|
+
* There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an
|
|
140
|
+
* entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,
|
|
141
|
+
* `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test
|
|
142
|
+
* gets is not a default; it is what a caller that configured nothing asked for.
|
|
143
|
+
*/
|
|
144
|
+
class ApiDocRule {
|
|
145
|
+
name;
|
|
146
|
+
enabled;
|
|
147
|
+
allowedPaths;
|
|
148
|
+
changedPaths;
|
|
149
|
+
constructor(name, enabled,
|
|
150
|
+
/** Project roots this rule does not apply to — `allowedPaths` in the config. */
|
|
151
|
+
allowedPaths,
|
|
152
|
+
/**
|
|
153
|
+
* Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under
|
|
154
|
+
* `RUN_EVERY_TIME`, which means every project is in scope.
|
|
155
|
+
*
|
|
156
|
+
* `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no
|
|
157
|
+
* repository at all. A diff that could not be read is not evidence that nothing changed, and
|
|
158
|
+
* the only safe reading of "I do not know what changed" is "look at all of it".
|
|
159
|
+
*/
|
|
160
|
+
changedPaths = null) {
|
|
161
|
+
this.name = name;
|
|
162
|
+
this.enabled = enabled;
|
|
163
|
+
this.allowedPaths = allowedPaths;
|
|
164
|
+
this.changedPaths = changedPaths;
|
|
165
|
+
}
|
|
166
|
+
/** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */
|
|
167
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
168
|
+
static armed(name) {
|
|
169
|
+
return new ApiDocRule(name, true, [], null);
|
|
170
|
+
}
|
|
171
|
+
/** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */
|
|
172
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
173
|
+
static affected(name, changedPaths) {
|
|
174
|
+
return new ApiDocRule(name, true, [], changedPaths);
|
|
175
|
+
}
|
|
176
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
177
|
+
static off(name) {
|
|
178
|
+
return new ApiDocRule(name, false, [], null);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* True when this rule scans the contracts of the project rooted at `root`.
|
|
182
|
+
*
|
|
183
|
+
* ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can
|
|
184
|
+
* never be applied by one caller and skipped by another.
|
|
185
|
+
*/
|
|
186
|
+
coversProject(root) {
|
|
187
|
+
if (!this.enabled)
|
|
188
|
+
return false;
|
|
189
|
+
if ((0, rules_config_1.matchesAnyGlob)(root, this.allowedPaths))
|
|
190
|
+
return false;
|
|
191
|
+
if (this.changedPaths === null)
|
|
192
|
+
return true;
|
|
193
|
+
const prefix = `${root}/`;
|
|
194
|
+
return this.changedPaths.some((each) => each.startsWith(prefix));
|
|
195
|
+
}
|
|
196
|
+
/** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */
|
|
197
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
198
|
+
static fromConfig(workspaceRoot, name) {
|
|
199
|
+
if (new rule_gate_1.RuleGate().isDisabled(workspaceRoot, name, true)) {
|
|
200
|
+
return ApiDocRule.off(name);
|
|
201
|
+
}
|
|
202
|
+
const rule = (0, rules_config_1.loadAndValidate)(workspaceRoot).resolved.rules.get(name);
|
|
203
|
+
const allowed = rule?.options['allowedPaths'];
|
|
204
|
+
const allowedPaths = Array.isArray(allowed) ? allowed : [];
|
|
205
|
+
const affected = rule?.options['mode'] === exports.AFFECTED_PROJECT_MODE
|
|
206
|
+
? ApiDocRule.changedPathsOf(workspaceRoot)
|
|
207
|
+
: null;
|
|
208
|
+
return new ApiDocRule(name, true, allowedPaths, affected);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base
|
|
212
|
+
* with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's
|
|
213
|
+
* contracts can express exactly as an added one does, and a `project.json` edit can change which
|
|
214
|
+
* project owns a contract at all.
|
|
215
|
+
*/
|
|
216
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
217
|
+
static changedPathsOf(workspaceRoot) {
|
|
218
|
+
const scope = new rules_config_1.DiffScope();
|
|
219
|
+
const range = scope.resolveBase(workspaceRoot);
|
|
220
|
+
if (range.base === undefined)
|
|
221
|
+
return null;
|
|
222
|
+
const opts = new rules_config_1.ChangedFilesOptions();
|
|
223
|
+
opts.tsOnly = false;
|
|
224
|
+
opts.includeDeletions = true;
|
|
225
|
+
return scope.getChangedFiles(workspaceRoot, range.base, range.head, opts);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
exports.ApiDocRule = ApiDocRule;
|
|
229
|
+
/** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */
|
|
230
|
+
class DisableComment {
|
|
231
|
+
hasReason;
|
|
232
|
+
constructor(hasReason) {
|
|
233
|
+
this.hasReason = hasReason;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.
|
|
237
|
+
*
|
|
238
|
+
* It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a
|
|
239
|
+
* JSDoc block, and the decorators between them — because that is where an author writes one, and
|
|
240
|
+
* it stops at the first line that is none of those, so a directive belonging to the PREVIOUS
|
|
241
|
+
* declaration can never be read as covering this one.
|
|
242
|
+
*
|
|
243
|
+
* Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`
|
|
244
|
+
* therefore does not silence these rules, which is the point: that rule answers "is this
|
|
245
|
+
* type-safe?" and these answer "is this field PUBLISHED with no shape?" — different questions
|
|
246
|
+
* with different right answers, so the second one wants its own, separately argued line.
|
|
247
|
+
*
|
|
248
|
+
* A file that cannot be read yields "no disable": a defect is still a defect, and inventing a
|
|
249
|
+
* suppression out of an I/O failure is the one wrong answer.
|
|
250
|
+
*/
|
|
251
|
+
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
252
|
+
static readAt(file, line, rule) {
|
|
253
|
+
const lines = DisableComment.linesOf(file);
|
|
254
|
+
if (lines === undefined)
|
|
255
|
+
return undefined;
|
|
256
|
+
const start = Math.max(0, line - 1);
|
|
257
|
+
const onDeclaration = DisableComment.match(lines[start] ?? '', rule);
|
|
258
|
+
if (onDeclaration !== undefined)
|
|
259
|
+
return onDeclaration;
|
|
260
|
+
for (let i = start - 1; i >= 0 && i >= start - DISABLE_LOOKBACK_LINES; i--) {
|
|
261
|
+
const text = (lines[i] ?? '').trim();
|
|
262
|
+
const above = DisableComment.match(text, rule);
|
|
263
|
+
if (above !== undefined)
|
|
264
|
+
return above;
|
|
265
|
+
if (!DisableComment.isTrivia(text))
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
/** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */
|
|
271
|
+
// webpieces-disable no-function-outside-class -- private static predicate of this class
|
|
272
|
+
static isTrivia(text) {
|
|
273
|
+
return (text === '' ||
|
|
274
|
+
text.startsWith('//') ||
|
|
275
|
+
text.startsWith('*') ||
|
|
276
|
+
text.startsWith('/*') ||
|
|
277
|
+
text.startsWith('@'));
|
|
278
|
+
}
|
|
279
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
280
|
+
static match(text, rule) {
|
|
281
|
+
const found = text.trim().match(DISABLE_RE);
|
|
282
|
+
if (found === null)
|
|
283
|
+
return undefined;
|
|
284
|
+
const named = found[1].split(',').map((each) => each.trim());
|
|
285
|
+
if (!named.includes(rule))
|
|
286
|
+
return undefined;
|
|
287
|
+
return new DisableComment((found[2] ?? '').trim() !== '');
|
|
288
|
+
}
|
|
289
|
+
/** The file's lines, or undefined when it cannot be read. */
|
|
290
|
+
// webpieces-disable no-function-outside-class -- private static reader of this class
|
|
291
|
+
static linesOf(file) {
|
|
292
|
+
if (!fs.existsSync(file))
|
|
293
|
+
return undefined;
|
|
294
|
+
return fs.readFileSync(file, 'utf8').split('\n');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
exports.DisableComment = DisableComment;
|
|
298
|
+
//# sourceMappingURL=api-doc-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-doc-rules.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-doc-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA and the SWITCHES of `api-rules-for-openapi` and `api-rules-for-mcp` (#1011).\n *\n * The scan itself is in `api-doc-rules-scan.ts`; this file holds what a caller reads back and the\n * per-rule config, so a unit test can construct either without touching a config file and so the\n * config is read exactly once per executor run.\n */\n\nimport * as fs from 'fs';\nimport {\n ChangedFilesOptions,\n DiffScope,\n loadAndValidate,\n matchesAnyGlob,\n RULE_NAMES,\n WEBPIECES_DISABLE,\n} from '@webpieces/rules-config';\nimport { RuleGate } from '../rule-gate';\n\n/** As written in a disable comment and as a config key. */\nexport const OPENAPI_RULE = RULE_NAMES.API_RULES_FOR_OPENAPI;\nexport const MCP_RULE = RULE_NAMES.API_RULES_FOR_MCP;\n\n/** The `@ApiType` value that means \"a partner reads this document\". */\nexport const EXTERNAL_CUSTOMER_API_TYPE = 'external-customer';\n\n/**\n * `// webpieces-disable <rule>[, <rule2>] -- <reason>`, with the reason CAPTURED so a reasonless\n * disable can be told apart from an absent one. Identical to the spelling `root-union-scan` reads.\n */\nconst DISABLE_RE = new RegExp(\n `//\\\\s*${WEBPIECES_DISABLE}\\\\s+([\\\\w-]+(?:\\\\s*,\\\\s*[\\\\w-]+)*)(?:\\\\s*--\\\\s*(.*))?$`,\n);\n\n/** How far above a declaration a disable comment may sit before it is somebody else's comment. */\nconst DISABLE_LOOKBACK_LINES = 40;\n\n/**\n * ONE defect on ONE contract.\n *\n * `exposure` is the contract's `@ApiType` list, and it is carried rather than derived because the\n * SAME defect is louder on a contract that reaches `external-customer`: an unshaped field on a\n * partner document costs a partner something, where the same field on a service-to-service contract\n * costs a colleague a question. The refusal sorts and labels on it.\n */\nexport class ApiContractDefect {\n constructor(\n /** The `@ApiPath` contract class. */\n public readonly api: string,\n /** The `@Endpoint` method, or `''` for a contract-level or DTO-level defect. */\n public readonly method: string,\n /** What is wrong, in one line, naming the declaration. */\n public readonly what: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n /** What to do instead, in one sentence. */\n public readonly cure: string,\n /** The contract's `@ApiType` list — `svc-to-svc` when it declares none. */\n public readonly exposure: readonly string[],\n ) {}\n\n /** True when this contract feeds the PARTNER-facing document. */\n isExternal(): boolean {\n return this.exposure.includes(EXTERNAL_CUSTOMER_API_TYPE);\n }\n\n /** `Api.method` or `Api` — what a reader opens. */\n where(): string {\n return this.method === '' ? this.api : `${this.api}.${this.method}`;\n }\n}\n\n/** What ONE rule found: real defects, and disables of it that gave no reason. */\nexport class ApiRuleFindings {\n constructor(\n public readonly violations: readonly ApiContractDefect[],\n /**\n * Sites that named this rule in a disable and wrote no reason. A reasonless disable is\n * itself a violation: the point of the per-site hatch is the ARGUMENT, which is the only\n * part the next reader can weigh.\n */\n public readonly reasonlessDisables: readonly ApiContractDefect[],\n ) {}\n\n isEmpty(): boolean {\n return this.violations.length === 0 && this.reasonlessDisables.length === 0;\n }\n}\n\n/**\n * ONE endpoint declared PERMANENTLY outside MCP by `@InvalidEndpointForMcp('<reason>')` (#1014).\n *\n * Not a violation and not a suppression — a DECLARATION, which is why it is carried beside the\n * findings rather than in them. `api-rules-for-mcp` restates the whole list, with reasons, on every\n * run: an exclusion announced once at the moment somebody added it is an exclusion nobody will read\n * again, and a build-time warning at add time would be read exactly as little.\n */\nexport class McpExclusion {\n constructor(\n public readonly api: string,\n public readonly method: string,\n /** The decorator's reason, verbatim. Empty only when the argument could not be folded. */\n public readonly reason: string,\n /** `path/to/File.ts:LINE`, workspace-relative. */\n public readonly at: string,\n ) {}\n}\n\n/** Both rules' findings from ONE scan — one program build, two verdicts. */\nexport class ApiDocRulesFindings {\n constructor(\n public readonly openApi: ApiRuleFindings,\n public readonly mcp: ApiRuleFindings,\n /** Every `@InvalidEndpointForMcp` endpoint the MCP rule looked at, in declaration order. */\n public readonly mcpExclusions: readonly McpExclusion[] = [],\n ) {}\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static empty(): ApiDocRulesFindings {\n return new ApiDocRulesFindings(\n new ApiRuleFindings([], []),\n new ApiRuleFindings([], []),\n [],\n );\n }\n}\n\n/** The mode value that narrows the scan to the projects the diff touched. */\nexport const AFFECTED_PROJECT_MODE = 'AFFECTED_PROJECT';\n\n/**\n * ONE rule's switches, resolved from webpieces.config.json once per scan.\n *\n * There is NO default (#1017). Both rules are schema'd, so `webpieces.config.json` must carry an\n * entry for each or the config FAILS TO LOAD naming them — a consumer states `OFF`,\n * `AFFECTED_PROJECT` or `RUN_EVERY_TIME` out loud. The `off()` a bare constructor or a unit test\n * gets is not a default; it is what a caller that configured nothing asked for.\n */\nexport class ApiDocRule {\n constructor(\n public readonly name: string,\n public readonly enabled: boolean,\n /** Project roots this rule does not apply to — `allowedPaths` in the config. */\n public readonly allowedPaths: readonly string[],\n /**\n * Workspace-relative paths the diff touched, under `AFFECTED_PROJECT`; `null` under\n * `RUN_EVERY_TIME`, which means every project is in scope.\n *\n * `null` is also what an UNCOMPUTABLE diff resolves to — no merge-base, a shallow clone, no\n * repository at all. A diff that could not be read is not evidence that nothing changed, and\n * the only safe reading of \"I do not know what changed\" is \"look at all of it\".\n */\n public readonly changedPaths: readonly string[] | null = null,\n ) {}\n\n /** ARMED over every project — what a unit test constructs, and what RUN_EVERY_TIME resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static armed(name: string): ApiDocRule {\n return new ApiDocRule(name, true, [], null);\n }\n\n /** ARMED over the projects owning one of `changedPaths` — what AFFECTED_PROJECT resolves to. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static affected(name: string, changedPaths: readonly string[]): ApiDocRule {\n return new ApiDocRule(name, true, [], changedPaths);\n }\n\n // webpieces-disable no-function-outside-class -- static factory of this class\n static off(name: string): ApiDocRule {\n return new ApiDocRule(name, false, [], null);\n }\n\n /**\n * True when this rule scans the contracts of the project rooted at `root`.\n *\n * ONE place answers it, for both rules, so `allowedPaths` and the affected-project narrowing can\n * never be applied by one caller and skipped by another.\n */\n coversProject(root: string): boolean {\n if (!this.enabled) return false;\n if (matchesAnyGlob(root, this.allowedPaths)) return false;\n if (this.changedPaths === null) return true;\n const prefix = `${root}/`;\n return this.changedPaths.some((each: string): boolean => each.startsWith(prefix));\n }\n\n /** `mode: OFF` and the time-box/branch hatches come from RuleGate, so there is ONE reading. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromConfig(workspaceRoot: string, name: string): ApiDocRule {\n if (new RuleGate().isDisabled(workspaceRoot, name, true)) {\n return ApiDocRule.off(name);\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get(name);\n const allowed = rule?.options['allowedPaths'];\n const allowedPaths = Array.isArray(allowed) ? (allowed as string[]) : [];\n const affected =\n rule?.options['mode'] === AFFECTED_PROJECT_MODE\n ? ApiDocRule.changedPathsOf(workspaceRoot)\n : null;\n return new ApiDocRule(name, true, allowedPaths, affected);\n }\n\n /**\n * Every path the diff touched, against the base nx itself uses (`NX_BASE`, else the merge-base\n * with origin/main). NOT ts-only and INCLUDING deletions: a deleted DTO changes what a project's\n * contracts can express exactly as an added one does, and a `project.json` edit can change which\n * project owns a contract at all.\n */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static changedPathsOf(workspaceRoot: string): readonly string[] | null {\n const scope = new DiffScope();\n const range = scope.resolveBase(workspaceRoot);\n if (range.base === undefined) return null;\n const opts = new ChangedFilesOptions();\n opts.tsOnly = false;\n opts.includeDeletions = true;\n return scope.getChangedFiles(workspaceRoot, range.base, range.head, opts);\n }\n}\n\n/** ONE `webpieces-disable` naming a rule, and whether it gave a reason. */\nexport class DisableComment {\n constructor(public readonly hasReason: boolean) {}\n\n /**\n * The disable naming `rule` on the declaration at `line` (1-based) of `file`, or undefined.\n *\n * It walks UPWARD over the declaration's own leading trivia — blank lines, `//` comments, a\n * JSDoc block, and the decorators between them — because that is where an author writes one, and\n * it stops at the first line that is none of those, so a directive belonging to the PREVIOUS\n * declaration can never be read as covering this one.\n *\n * Only a directive NAMING this rule counts. An existing `// webpieces-disable no-any-unknown`\n * therefore does not silence these rules, which is the point: that rule answers \"is this\n * type-safe?\" and these answer \"is this field PUBLISHED with no shape?\" — different questions\n * with different right answers, so the second one wants its own, separately argued line.\n *\n * A file that cannot be read yields \"no disable\": a defect is still a defect, and inventing a\n * suppression out of an I/O failure is the one wrong answer.\n */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static readAt(file: string, line: number, rule: string): DisableComment | undefined {\n const lines = DisableComment.linesOf(file);\n if (lines === undefined) return undefined;\n const start = Math.max(0, line - 1);\n const onDeclaration = DisableComment.match(lines[start] ?? '', rule);\n if (onDeclaration !== undefined) return onDeclaration;\n for (let i = start - 1; i >= 0 && i >= start - DISABLE_LOOKBACK_LINES; i--) {\n const text = (lines[i] ?? '').trim();\n const above = DisableComment.match(text, rule);\n if (above !== undefined) return above;\n if (!DisableComment.isTrivia(text)) return undefined;\n }\n return undefined;\n }\n\n /** Leading trivia a disable comment is allowed to sit above: blanks, comments and decorators. */\n // webpieces-disable no-function-outside-class -- private static predicate of this class\n private static isTrivia(text: string): boolean {\n return (\n text === '' ||\n text.startsWith('//') ||\n text.startsWith('*') ||\n text.startsWith('/*') ||\n text.startsWith('@')\n );\n }\n\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static match(text: string, rule: string): DisableComment | undefined {\n const found = text.trim().match(DISABLE_RE);\n if (found === null) return undefined;\n const named = found[1].split(',').map((each: string): string => each.trim());\n if (!named.includes(rule)) return undefined;\n return new DisableComment((found[2] ?? '').trim() !== '');\n }\n\n /** The file's lines, or undefined when it cannot be read. */\n // webpieces-disable no-function-outside-class -- private static reader of this class\n private static linesOf(file: string): string[] | undefined {\n if (!fs.existsSync(file)) return undefined;\n return fs.readFileSync(file, 'utf8').split('\\n');\n }\n}\n"]}
|
|
@@ -31,6 +31,7 @@ import type { EnhancedGraph } from '../graph-sorter';
|
|
|
31
31
|
import { ProjectInfo } from '../project-info';
|
|
32
32
|
import { ApiClassInfo, ApiContracts, EmptiedApiContract, NonLiteralDecoratorArg, ProjectApiRelations, UndeclaredExternalCaller, UndeclaredEndpointOperation, UnresolvedApiCall, UnresolvedEndpointPath } from './api-relations';
|
|
33
33
|
import { RootUnionFindings, RootUnionRule } from './root-union-scan';
|
|
34
|
+
import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
|
|
34
35
|
/** The whole-workspace result of a scan. */
|
|
35
36
|
export interface ApiScanResult {
|
|
36
37
|
/** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */
|
|
@@ -76,6 +77,11 @@ export interface ApiScanResult {
|
|
|
76
77
|
undeclaredEndpointOperations: UndeclaredEndpointOperation[];
|
|
77
78
|
/** `no-root-union-api-type`'s findings. Fatal in buildApiContracts — see root-union-scan.ts. */
|
|
78
79
|
rootUnions: RootUnionFindings;
|
|
80
|
+
/**
|
|
81
|
+
* `api-rules-for-openapi` / `api-rules-for-mcp` findings (#1011). Both ship OFF, so this is
|
|
82
|
+
* EMPTY unless a repo stated otherwise — no rule in this framework has a default (#1017).
|
|
83
|
+
*/
|
|
84
|
+
apiDocRules: ApiDocRulesFindings;
|
|
79
85
|
}
|
|
80
86
|
/** Statically scans every project for its api-lib implements/uses relationships. */
|
|
81
87
|
export declare class ApiUsageScanner {
|
|
@@ -85,6 +91,10 @@ export declare class ApiUsageScanner {
|
|
|
85
91
|
private readonly externalApiPaths;
|
|
86
92
|
/** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
|
|
87
93
|
private readonly rootUnionRule;
|
|
94
|
+
/** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
|
|
95
|
+
private readonly openApiRule;
|
|
96
|
+
/** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
|
|
97
|
+
private readonly mcpRule;
|
|
88
98
|
private readonly locator;
|
|
89
99
|
private readonly relationsByProject;
|
|
90
100
|
private readonly scannedProjects;
|
|
@@ -95,7 +105,11 @@ export declare class ApiUsageScanner {
|
|
|
95
105
|
/** Globs of project roots whose exported `*Api` types are contracts for outside systems. */
|
|
96
106
|
externalApiPaths?: readonly string[],
|
|
97
107
|
/** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
|
|
98
|
-
rootUnionRule?: RootUnionRule
|
|
108
|
+
rootUnionRule?: RootUnionRule,
|
|
109
|
+
/** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
|
|
110
|
+
openApiRule?: ApiDocRule,
|
|
111
|
+
/** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
|
|
112
|
+
mcpRule?: ApiDocRule);
|
|
99
113
|
scan(): ApiScanResult;
|
|
100
114
|
private scanProject;
|
|
101
115
|
private visit;
|
|
@@ -44,6 +44,8 @@ const bindings_1 = require("../di-graph/bindings");
|
|
|
44
44
|
const api_relations_1 = require("./api-relations");
|
|
45
45
|
const api_contract_errors_1 = require("./api-contract-errors");
|
|
46
46
|
const root_union_scan_1 = require("./root-union-scan");
|
|
47
|
+
const api_doc_rules_1 = require("./api-doc-rules");
|
|
48
|
+
const api_doc_rules_scan_1 = require("./api-doc-rules-scan");
|
|
47
49
|
const api_ast_1 = require("./api-ast");
|
|
48
50
|
const RPC_CLIENT_METHOD = 'createRpcClient';
|
|
49
51
|
const PUBSUB_CLIENT_METHOD = 'createPubSubClient';
|
|
@@ -206,6 +208,8 @@ class ApiUsageScanner {
|
|
|
206
208
|
projectInfos;
|
|
207
209
|
externalApiPaths;
|
|
208
210
|
rootUnionRule;
|
|
211
|
+
openApiRule;
|
|
212
|
+
mcpRule;
|
|
209
213
|
locator;
|
|
210
214
|
relationsByProject = new Map();
|
|
211
215
|
scannedProjects = new Set();
|
|
@@ -216,11 +220,17 @@ class ApiUsageScanner {
|
|
|
216
220
|
/** Globs of project roots whose exported `*Api` types are contracts for outside systems. */
|
|
217
221
|
externalApiPaths = [],
|
|
218
222
|
/** `no-root-union-api-type`'s switches — ARMED unless scanAndAttachApiRelations read otherwise. */
|
|
219
|
-
rootUnionRule = root_union_scan_1.RootUnionRule.enabledEverywhere()
|
|
223
|
+
rootUnionRule = root_union_scan_1.RootUnionRule.enabledEverywhere(),
|
|
224
|
+
/** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
|
|
225
|
+
openApiRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.OPENAPI_RULE),
|
|
226
|
+
/** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
|
|
227
|
+
mcpRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.MCP_RULE)) {
|
|
220
228
|
this.workspaceRoot = workspaceRoot;
|
|
221
229
|
this.projectInfos = projectInfos;
|
|
222
230
|
this.externalApiPaths = externalApiPaths;
|
|
223
231
|
this.rootUnionRule = rootUnionRule;
|
|
232
|
+
this.openApiRule = openApiRule;
|
|
233
|
+
this.mcpRule = mcpRule;
|
|
224
234
|
this.locator = new ProjectLocator(workspaceRoot, projectInfos);
|
|
225
235
|
this.decoratorArgDiagnostics = new api_ast_1.DecoratorArgDiagnostics(workspaceRoot);
|
|
226
236
|
}
|
|
@@ -245,6 +255,7 @@ class ApiUsageScanner {
|
|
|
245
255
|
undeclaredExternalCallers: this.decoratorArgDiagnostics.undeclaredExternalCallers(),
|
|
246
256
|
undeclaredEndpointOperations: this.decoratorArgDiagnostics.undeclaredEndpointOperations(),
|
|
247
257
|
rootUnions: new root_union_scan_1.RootUnionScan(this.workspaceRoot, this.projectInfos, this.rootUnionRule).run(),
|
|
258
|
+
apiDocRules: new api_doc_rules_scan_1.ApiDocRulesScan(this.workspaceRoot, this.projectInfos, this.openApiRule, this.mcpRule).run(),
|
|
248
259
|
};
|
|
249
260
|
}
|
|
250
261
|
scanProject(info) {
|
|
@@ -389,7 +400,7 @@ exports.ApiUsageScanner = ApiUsageScanner;
|
|
|
389
400
|
*/
|
|
390
401
|
// webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings
|
|
391
402
|
function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths = []) {
|
|
392
|
-
const result = new ApiUsageScanner(workspaceRoot, projectInfos, externalApiPaths, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot)).scan();
|
|
403
|
+
const result = new ApiUsageScanner(workspaceRoot, projectInfos, externalApiPaths, root_union_scan_1.RootUnionRule.fromConfig(workspaceRoot), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.OPENAPI_RULE), api_doc_rules_1.ApiDocRule.fromConfig(workspaceRoot, api_doc_rules_1.MCP_RULE)).scan();
|
|
393
404
|
for (const projectName of result.relationsByProject.keys()) {
|
|
394
405
|
const entry = graph[projectName];
|
|
395
406
|
if (entry)
|
|
@@ -432,6 +443,15 @@ function buildApiContracts(scan) {
|
|
|
432
443
|
// are contracts the scan could not READ at all.
|
|
433
444
|
if (!scan.rootUnions.isEmpty())
|
|
434
445
|
throw new api_contract_errors_1.RootUnionApiTypeError(scan.rootUnions);
|
|
446
|
+
// Contract shapes that are not PUBLISHABLE, read by the generator's own extractor (#1011).
|
|
447
|
+
// After the root union, which is the one shape no function-calling API will accept at all, and
|
|
448
|
+
// before the caller checks below, which are about the architecture graph rather than a document.
|
|
449
|
+
if (!scan.apiDocRules.openApi.isEmpty()) {
|
|
450
|
+
throw new api_contract_errors_1.ApiRulesForOpenApiError(scan.apiDocRules.openApi);
|
|
451
|
+
}
|
|
452
|
+
if (!scan.apiDocRules.mcp.isEmpty()) {
|
|
453
|
+
throw new api_contract_errors_1.ApiRulesForMcpError(scan.apiDocRules.mcp, scan.apiDocRules.mcpExclusions);
|
|
454
|
+
}
|
|
435
455
|
// After the two above: an unreadable path is what empties a contract, and a contract that lost
|
|
436
456
|
// every method has no external endpoint left to complain about.
|
|
437
457
|
if (scan.undeclaredExternalCallers.length > 0) {
|