@webpieces/nx-webpieces-rules 0.4.832 → 0.4.834

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.
Files changed (49) hide show
  1. package/package.json +8 -8
  2. package/src/executors/generate/executor.js +2 -0
  3. package/src/executors/generate/executor.js.map +1 -1
  4. package/src/executors/openapi-components-generate/executor.d.ts +2 -1
  5. package/src/executors/openapi-components-generate/executor.js +5 -2
  6. package/src/executors/openapi-components-generate/executor.js.map +1 -1
  7. package/src/executors/openapi-generate/executor.d.ts +15 -1
  8. package/src/executors/openapi-generate/executor.js +30 -2
  9. package/src/executors/openapi-generate/executor.js.map +1 -1
  10. package/src/executors/validate-api-lib-tag/executor.d.ts +5 -3
  11. package/src/executors/validate-api-lib-tag/executor.js +7 -5
  12. package/src/executors/validate-api-lib-tag/executor.js.map +1 -1
  13. package/src/executors/validate-architecture-unchanged/executor.js +2 -0
  14. package/src/executors/validate-architecture-unchanged/executor.js.map +1 -1
  15. package/src/lib/api-docs/components-wiring.d.ts +6 -0
  16. package/src/lib/api-docs/components-wiring.js +18 -1
  17. package/src/lib/api-docs/components-wiring.js.map +1 -1
  18. package/src/lib/api-usage/api-doc-rules-scan.d.ts +17 -1
  19. package/src/lib/api-usage/api-doc-rules-scan.js +28 -3
  20. package/src/lib/api-usage/api-doc-rules-scan.js.map +1 -1
  21. package/src/lib/api-usage/api-lib-tag-validator.d.ts +51 -14
  22. package/src/lib/api-usage/api-lib-tag-validator.js +272 -22
  23. package/src/lib/api-usage/api-lib-tag-validator.js.map +1 -1
  24. package/src/lib/api-usage/api-scanner.d.ts +3 -1
  25. package/src/lib/api-usage/api-scanner.js +6 -3
  26. package/src/lib/api-usage/api-scanner.js.map +1 -1
  27. package/src/lib/api-usage/wire-closure.d.ts +49 -0
  28. package/src/lib/api-usage/wire-closure.js +126 -0
  29. package/src/lib/api-usage/wire-closure.js.map +1 -0
  30. package/src/lib/dep-usage-scanner.d.ts +6 -2
  31. package/src/lib/dep-usage-scanner.js +45 -28
  32. package/src/lib/dep-usage-scanner.js.map +1 -1
  33. package/src/lib/di-graph/analyzer-strategy.d.ts +1 -0
  34. package/src/lib/di-graph/analyzer-strategy.js +3 -0
  35. package/src/lib/di-graph/analyzer-strategy.js.map +1 -1
  36. package/src/lib/framework-resolver.d.ts +1 -1
  37. package/src/lib/framework-resolver.js +5 -2
  38. package/src/lib/framework-resolver.js.map +1 -1
  39. package/src/lib/graph-metadata.d.ts +9 -2
  40. package/src/lib/graph-metadata.js +12 -3
  41. package/src/lib/graph-metadata.js.map +1 -1
  42. package/src/lib/graph-visualizer.js +6 -0
  43. package/src/lib/graph-visualizer.js.map +1 -1
  44. package/src/lib/role-resolver.d.ts +9 -3
  45. package/src/lib/role-resolver.js +12 -4
  46. package/src/lib/role-resolver.js.map +1 -1
  47. package/src/lib/tag-truth.d.ts +106 -0
  48. package/src/lib/tag-truth.js +323 -0
  49. package/src/lib/tag-truth.js.map +1 -0
@@ -2,36 +2,274 @@
2
2
  /**
3
3
  * API-lib tag validator (two-way)
4
4
  *
5
- * Keeps the `role:api-lib` tag and the CODE in sync, both directions:
6
- * - a project tagged `role:api-lib` MUST export ≥1 API contract (an `abstract class` carrying
7
- * `@ApiPath`/`@Rpc`/`@PubSub`) — else the tag is a lie;
8
- * - a project that DOES export such a contract MUST be tagged `role:api-lib` — else the arch
9
- * graph, the edge line-styles, and validate-api-relations can't treat it as an api-lib.
5
+ * Keeps the two api roles and the CODE in sync, both directions (#1064, D1 + D3):
6
+ * - a project that exports an `@ApiPath`/`@Rpc`/`@PubSub` contract (or a vendor contract under
7
+ * `externalApiPaths`) MUST be tagged `role:api-lib` or `role:api-client` — else the arch graph, the
8
+ * edge line-styles, and validate-api-relations can't treat it as one;
9
+ * - a project tagged `role:api-lib` MUST export a contract or ONLY wire types — else the tag is a lie.
10
+ * A contract is any of: an `@ApiPath`/`@Rpc`/`@PubSub` class (from the scan), an IPC contract
11
+ * (`@WpInternal` / `@WpIpcEndpoint`), or an in-process abstract `…Api` behind a DI token. A DTO-only
12
+ * library exports interfaces, type aliases, enums, data classes (no methods), and data-only constants;
13
+ * - a project tagged `role:api-client` MUST export a contract: either an abstract `XxxApi`, or an
14
+ * exported `XxxApi` interface implemented by a client registered with `@provideSingletonDefaultForApi`.
15
+ * The bundled client beside it is the point of the role, so it is not judged as a wire type.
10
16
  *
11
- * "Exports an API contract" is answered by the same source scan that owns apiRelations
12
- * (scan.apiLibProjects), so the tag can never drift from the code.
17
+ * "Exports an @ApiPath contract" is answered by the same source scan that owns apiRelations
18
+ * (scan.apiLibProjects), so the tag can never drift from the code. The IPC / in-process / DTO-only
19
+ * reading is a parser-only pass over the project's own non-test `src/**` — {@link ApiLibExportShape}.
13
20
  */
14
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.ApiLibExportShape = exports.ApiLibTagViolation = void 0;
15
23
  exports.findApiLibTagViolations = findApiLibTagViolations;
16
24
  exports.describeApiLibTagViolation = describeApiLibTagViolation;
25
+ const tslib_1 = require("tslib");
26
+ const fs = tslib_1.__importStar(require("fs"));
27
+ const path = tslib_1.__importStar(require("path"));
28
+ const ts = tslib_1.__importStar(require("typescript"));
17
29
  const role_resolver_1 = require("../role-resolver");
30
+ const api_ast_1 = require("./api-ast");
18
31
  const API_LIB_ROLE = 'api-lib';
19
- /** Both-directions tag ⇔ code check. Uses the scan's detected api-lib set as ground truth. */
32
+ const API_CLIENT_ROLE = 'api-client';
33
+ /** The decorators that make an abstract class an IPC contract. */
34
+ const IPC_DECORATORS = ['WpInternal', 'WpIpcEndpoint'];
35
+ /**
36
+ * A tag/code mismatch:
37
+ * - 'missing-tag' — exports an API contract but is tagged neither role:api-lib nor role:api-client.
38
+ * - 'unnecessary-tag' — tagged role:api-lib / role:api-client but exports no contract, and (for
39
+ * api-lib) is not a DTO-only library either; `offenders` names what it exports.
40
+ */
41
+ class ApiLibTagViolation {
42
+ project;
43
+ kind;
44
+ role;
45
+ offenders;
46
+ constructor(project, kind,
47
+ /** The role it carries (for 'unnecessary-tag'), else the role it should carry. */
48
+ role,
49
+ /** Exports that are neither a contract nor a wire type — empty for 'missing-tag'. */
50
+ offenders = []) {
51
+ this.project = project;
52
+ this.kind = kind;
53
+ this.role = role;
54
+ this.offenders = offenders;
55
+ }
56
+ }
57
+ exports.ApiLibTagViolation = ApiLibTagViolation;
58
+ /** What a library's own source exports, sorted into contracts, wire types and everything else. */
59
+ class ApiLibExportShape {
60
+ exportsContract;
61
+ exportsWireType;
62
+ offenders;
63
+ constructor(
64
+ /** True when it exports an IPC contract or an in-process abstract `…Api`. */
65
+ exportsContract,
66
+ /** True when it exports at least one wire type (interface, alias, enum, data class). */
67
+ exportsWireType,
68
+ /** `kind Name (file)` of every export that is neither — the implementation an api-lib must not hold. */
69
+ offenders) {
70
+ this.exportsContract = exportsContract;
71
+ this.exportsWireType = exportsWireType;
72
+ this.offenders = offenders;
73
+ }
74
+ /** A role:api-lib passes when it holds a contract or wire types, and nothing else. */
75
+ isApiLib() {
76
+ return (this.exportsContract || this.exportsWireType) && this.offenders.length === 0;
77
+ }
78
+ /** Parser-only: every top-level EXPORTED declaration in the project's non-test `src/**`. */
79
+ // webpieces-disable no-function-outside-class -- static factory of this class
80
+ static read(projectDir) {
81
+ const srcDir = path.join(projectDir, 'src');
82
+ const acc = new ExportShapeAccumulator(projectDir);
83
+ if (fs.existsSync(srcDir)) {
84
+ for (const file of (0, api_ast_1.collectTsFiles)(srcDir)) {
85
+ if ((0, api_ast_1.isTestFile)(file))
86
+ continue;
87
+ const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
88
+ for (const statement of source.statements)
89
+ acc.add(statement, file);
90
+ }
91
+ }
92
+ return acc.shape();
93
+ }
94
+ }
95
+ exports.ApiLibExportShape = ApiLibExportShape;
96
+ /** Sorts each exported statement of one library into contract / wire type / offender. */
97
+ class ExportShapeAccumulator {
98
+ projectDir;
99
+ contract = false;
100
+ wire = false;
101
+ offenders = [];
102
+ apiInterfaces = new Set();
103
+ registeredApiImplementations = new Set();
104
+ constructor(projectDir) {
105
+ this.projectDir = projectDir;
106
+ }
107
+ add(statement, file) {
108
+ if (!this.isExported(statement))
109
+ return;
110
+ const where = path.relative(this.projectDir, file);
111
+ if (ts.isInterfaceDeclaration(statement)) {
112
+ this.wire = true;
113
+ if (statement.name.text.endsWith('Api'))
114
+ this.apiInterfaces.add(statement.name.text);
115
+ }
116
+ else if (ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) {
117
+ this.wire = true;
118
+ }
119
+ else if (ts.isClassDeclaration(statement)) {
120
+ this.addClass(statement, where);
121
+ }
122
+ else if (ts.isVariableStatement(statement)) {
123
+ for (const decl of statement.declarationList.declarations) {
124
+ if (this.isDiToken(decl.initializer) || this.isDataConstant(decl.initializer))
125
+ continue;
126
+ this.offenders.push(`const ${decl.name.getText()} (${where})`);
127
+ }
128
+ }
129
+ else if (ts.isFunctionDeclaration(statement)) {
130
+ this.offenders.push(`function ${statement.name?.text ?? '<default>'} (${where})`);
131
+ }
132
+ }
133
+ shape() {
134
+ const registeredInterface = [...this.apiInterfaces].some((name) => this.registeredApiImplementations.has(name));
135
+ return new ApiLibExportShape(this.contract || registeredInterface, this.wire, [...this.offenders].sort());
136
+ }
137
+ addClass(cls, where) {
138
+ const name = cls.name?.text ?? '<default>';
139
+ if (this.hasDecorator(cls, 'provideSingletonDefaultForApi')) {
140
+ for (const heritage of cls.heritageClauses ?? []) {
141
+ if (heritage.token !== ts.SyntaxKind.ImplementsKeyword)
142
+ continue;
143
+ for (const type of heritage.types) {
144
+ const implemented = type.expression.getText().split('.').pop();
145
+ if (implemented !== undefined)
146
+ this.registeredApiImplementations.add(implemented);
147
+ }
148
+ }
149
+ }
150
+ const isAbstract = (ts.getModifiers(cls) ?? []).some((m) => m.kind === ts.SyntaxKind.AbstractKeyword);
151
+ if (isAbstract && (name.endsWith('Api') || this.carriesIpc(cls))) {
152
+ this.contract = true;
153
+ return;
154
+ }
155
+ const hasMethod = cls.members.some((member) => ts.isMethodDeclaration(member) &&
156
+ !(ts.getModifiers(member) ?? []).some((m) => m.kind === ts.SyntaxKind.StaticKeyword));
157
+ if (hasMethod)
158
+ this.offenders.push(`class ${name} (${where}) — it has methods, so it is an implementation`);
159
+ else
160
+ this.wire = true;
161
+ }
162
+ /** `@WpInternal(...)` on the class or `@WpIpcEndpoint(...)` on any member. */
163
+ carriesIpc(cls) {
164
+ const named = (node) => IPC_DECORATORS.some((name) => this.hasDecorator(node, name));
165
+ return named(cls) || cls.members.some((member) => named(member));
166
+ }
167
+ hasDecorator(node, expected) {
168
+ return (ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : []).some((decorator) => {
169
+ const expr = ts.isCallExpression(decorator.expression)
170
+ ? decorator.expression.expression
171
+ : decorator.expression;
172
+ const name = ts.isPropertyAccessExpression(expr) ? expr.name.text : expr.getText();
173
+ return name === expected;
174
+ });
175
+ }
176
+ /** Literal/structural protocol data is contract support, not an executable implementation. */
177
+ isDataConstant(init) {
178
+ if (init === undefined)
179
+ return false;
180
+ const expr = this.unwrap(init);
181
+ if (ts.isStringLiteralLike(expr) ||
182
+ ts.isNumericLiteral(expr) ||
183
+ expr.kind === ts.SyntaxKind.TrueKeyword ||
184
+ expr.kind === ts.SyntaxKind.FalseKeyword ||
185
+ expr.kind === ts.SyntaxKind.NullKeyword ||
186
+ ts.isIdentifier(expr) ||
187
+ ts.isPropertyAccessExpression(expr)) {
188
+ return true;
189
+ }
190
+ if (ts.isPrefixUnaryExpression(expr))
191
+ return this.isDataConstant(expr.operand);
192
+ if (ts.isBinaryExpression(expr)) {
193
+ return this.isDataConstant(expr.left) && this.isDataConstant(expr.right);
194
+ }
195
+ if (ts.isConditionalExpression(expr)) {
196
+ return (this.isDataConstant(expr.condition) &&
197
+ this.isDataConstant(expr.whenTrue) &&
198
+ this.isDataConstant(expr.whenFalse));
199
+ }
200
+ if (ts.isTemplateExpression(expr)) {
201
+ return expr.templateSpans.every((span) => this.isDataConstant(span.expression));
202
+ }
203
+ if (ts.isArrayLiteralExpression(expr)) {
204
+ return expr.elements.every((element) => ts.isOmittedExpression(element) ||
205
+ (ts.isSpreadElement(element)
206
+ ? this.isDataConstant(element.expression)
207
+ : this.isDataConstant(element)));
208
+ }
209
+ if (ts.isObjectLiteralExpression(expr)) {
210
+ return expr.properties.every((prop) => {
211
+ if (ts.isPropertyAssignment(prop))
212
+ return this.isDataConstant(prop.initializer);
213
+ if (ts.isShorthandPropertyAssignment(prop))
214
+ return true;
215
+ if (ts.isSpreadAssignment(prop))
216
+ return this.isDataConstant(prop.expression);
217
+ return false;
218
+ });
219
+ }
220
+ return false;
221
+ }
222
+ unwrap(expr) {
223
+ if (ts.isAsExpression(expr) ||
224
+ ts.isTypeAssertionExpression(expr) ||
225
+ ts.isSatisfiesExpression(expr) ||
226
+ ts.isParenthesizedExpression(expr) ||
227
+ ts.isNonNullExpression(expr)) {
228
+ return this.unwrap(expr.expression);
229
+ }
230
+ return expr;
231
+ }
232
+ /** `Symbol('x')`, `Symbol.for('x')`, or an object literal of them — the token an abstract `…Api` binds behind. */
233
+ isDiToken(init) {
234
+ if (init === undefined)
235
+ return false;
236
+ const expr = ts.isAsExpression(init) || ts.isSatisfiesExpression(init) ? init.expression : init;
237
+ if (ts.isCallExpression(expr)) {
238
+ const callee = expr.expression.getText();
239
+ return callee === 'Symbol' || callee === 'Symbol.for';
240
+ }
241
+ if (ts.isObjectLiteralExpression(expr)) {
242
+ return (expr.properties.length > 0 &&
243
+ expr.properties.every((prop) => ts.isPropertyAssignment(prop) && this.isDiToken(prop.initializer)));
244
+ }
245
+ return false;
246
+ }
247
+ isExported(statement) {
248
+ if (!ts.canHaveModifiers(statement))
249
+ return false;
250
+ return (ts.getModifiers(statement) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
251
+ }
252
+ }
253
+ /** Both-directions tag ⇔ code check. Uses the scan's detected api-lib set as ground truth for @ApiPath. */
20
254
  // webpieces-disable no-function-outside-class -- validator-lib entry point, matches api-relations-validator.ts
21
- function findApiLibTagViolations(projectInfos, scan) {
255
+ function findApiLibTagViolations(projectInfos, scan, workspaceRoot) {
22
256
  const violations = [];
23
257
  for (const projectName of projectInfos.keys()) {
24
258
  const info = projectInfos.get(projectName);
25
- const isTagged = (0, role_resolver_1.resolveRole)(info).role === API_LIB_ROLE;
259
+ const role = (0, role_resolver_1.resolveRole)(info).role;
260
+ const isApiRole = role === API_LIB_ROLE || role === API_CLIENT_ROLE;
26
261
  const exportsApi = scan.apiLibProjects.has(projectName);
27
- if (exportsApi && !isTagged) {
28
- violations.push({ project: projectName, kind: 'missing-tag' });
29
- }
30
- // Only flag an unnecessary tag when we actually SCANNED the project's source — otherwise we
31
- // can't prove it exports no contract (an unscannable project would falsely look empty).
32
- if (isTagged && !exportsApi && scan.scannedProjects.has(projectName)) {
33
- violations.push({ project: projectName, kind: 'unnecessary-tag' });
262
+ if (exportsApi && !isApiRole) {
263
+ violations.push(new ApiLibTagViolation(projectName, 'missing-tag', API_LIB_ROLE));
34
264
  }
265
+ // Only judge a tag when we actually SCANNED the project's source — otherwise we can't prove it
266
+ // exports no contract (an unscannable project would falsely look empty).
267
+ if (!isApiRole || exportsApi || !scan.scannedProjects.has(projectName))
268
+ continue;
269
+ const shape = ApiLibExportShape.read(path.resolve(workspaceRoot, info.root));
270
+ const ok = role === API_CLIENT_ROLE ? shape.exportsContract : shape.isApiLib();
271
+ if (!ok)
272
+ violations.push(new ApiLibTagViolation(projectName, 'unnecessary-tag', role, shape.offenders));
35
273
  }
36
274
  return violations.sort((a, b) => a.project.localeCompare(b.project));
37
275
  }
@@ -40,11 +278,23 @@ function findApiLibTagViolations(projectInfos, scan) {
40
278
  function describeApiLibTagViolation(violation) {
41
279
  if (violation.kind === 'missing-tag') {
42
280
  return (` ❌ '${violation.project}' exports an API contract (an abstract @ApiPath/@Rpc/@PubSub class) ` +
43
- `but is not tagged 'role:api-lib'.\n` +
44
- ` Add "role:api-lib" to its project.json "tags" (replacing any "role:lib").`);
281
+ `but is tagged neither 'role:api-lib' nor 'role:api-client'.\n` +
282
+ ` Add "role:api-lib" to its project.json "tags" (replacing any "role:lib"), or "role:api-client" ` +
283
+ `when it also bundles the default implementation that talks to an outside system.`);
284
+ }
285
+ if (violation.role === API_CLIENT_ROLE) {
286
+ return (` ❌ '${violation.project}' is tagged 'role:api-client' but exports NO contract (no abstract ` +
287
+ `…Api class, no @WpInternal/@WpIpcEndpoint contract, and no exported …Api interface implemented ` +
288
+ `by an @provideSingletonDefaultForApi client).\n` +
289
+ ` An api-client is a contract PLUS its default implementation: export the XxxApi the registered ` +
290
+ `client implements, or retag it (e.g. "role:lib").`);
45
291
  }
46
- return (` ❌ '${violation.project}' is tagged 'role:api-lib' but exports NO API contract ` +
47
- `(no abstract @ApiPath/@Rpc/@PubSub class).\n` +
48
- ` Either add the API contract it should own, or retag it (e.g. "role:lib").`);
292
+ const offenders = violation.offenders.length === 0
293
+ ? ''
294
+ : `\n It exports, besides contracts and wire types: ${violation.offenders.join('; ')}.`;
295
+ return (` ❌ '${violation.project}' is tagged 'role:api-lib' but is neither a contract library (@ApiPath/@Rpc/` +
296
+ `@PubSub, @WpInternal/@WpIpcEndpoint, or an abstract …Api behind a DI token) nor a DTO-only library ` +
297
+ `(interfaces, type aliases, enums and data classes only).${offenders}\n` +
298
+ ` Move the implementation out to a role:lib / role:designed-lib project, or retag it.`);
49
299
  }
50
300
  //# sourceMappingURL=api-lib-tag-validator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-lib-tag-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-lib-tag-validator.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;AAoBH,0DAoBC;AAID,gEAaC;AAtDD,oDAA+C;AAG/C,MAAM,YAAY,GAAG,SAAS,CAAC;AAY/B,8FAA8F;AAC9F,+GAA+G;AAC/G,SAAgB,uBAAuB,CACnC,YAAsC,EACtC,IAAmB;IAEnB,MAAM,UAAU,GAAyB,EAAE,CAAC;IAC5C,KAAK,MAAM,WAAW,IAAI,YAAY,CAAC,IAAI,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,WAAW,CAAE,CAAC;QAC5C,MAAM,QAAQ,GAAG,IAAA,2BAAW,EAAC,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC;QACzD,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAExD,IAAI,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC1B,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;QACnE,CAAC;QACD,4FAA4F;QAC5F,wFAAwF;QACxF,IAAI,QAAQ,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YACnE,UAAU,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACvE,CAAC;IACL,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAqB,EAAE,CAAqB,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;AACjH,CAAC;AAED,qEAAqE;AACrE,oGAAoG;AACpG,SAAgB,0BAA0B,CAAC,SAA6B;IACpE,IAAI,SAAS,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QACnC,OAAO,CACH,QAAQ,SAAS,CAAC,OAAO,sEAAsE;YAC/F,qCAAqC;YACrC,gFAAgF,CACnF,CAAC;IACN,CAAC;IACD,OAAO,CACH,QAAQ,SAAS,CAAC,OAAO,yDAAyD;QAClF,8CAA8C;QAC9C,gFAAgF,CACnF,CAAC;AACN,CAAC","sourcesContent":["/**\n * API-lib tag validator (two-way)\n *\n * Keeps the `role:api-lib` tag and the CODE in sync, both directions:\n * - a project tagged `role:api-lib` MUST export ≥1 API contract (an `abstract class` carrying\n * `@ApiPath`/`@Rpc`/`@PubSub`) — else the tag is a lie;\n * - a project that DOES export such a contract MUST be tagged `role:api-lib` — else the arch\n * graph, the edge line-styles, and validate-api-relations can't treat it as an api-lib.\n *\n * \"Exports an API contract\" is answered by the same source scan that owns apiRelations\n * (scan.apiLibProjects), so the tag can never drift from the code.\n */\n\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult } from './api-scanner';\n\nconst API_LIB_ROLE = 'api-lib';\n\n/**\n * A tag/code mismatch:\n * - 'missing-tag' — exports an API contract but is not tagged role:api-lib.\n * - 'unnecessary-tag' — tagged role:api-lib but exports no API contract.\n */\nexport interface ApiLibTagViolation {\n project: string;\n kind: 'missing-tag' | 'unnecessary-tag';\n}\n\n/** Both-directions tag ⇔ code check. Uses the scan's detected api-lib set as ground truth. */\n// webpieces-disable no-function-outside-class -- validator-lib entry point, matches api-relations-validator.ts\nexport function findApiLibTagViolations(\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n): ApiLibTagViolation[] {\n const violations: ApiLibTagViolation[] = [];\n for (const projectName of projectInfos.keys()) {\n const info = projectInfos.get(projectName)!;\n const isTagged = resolveRole(info).role === API_LIB_ROLE;\n const exportsApi = scan.apiLibProjects.has(projectName);\n\n if (exportsApi && !isTagged) {\n violations.push({ project: projectName, kind: 'missing-tag' });\n }\n // Only flag an unnecessary tag when we actually SCANNED the project's source — otherwise we\n // can't prove it exports no contract (an unscannable project would falsely look empty).\n if (isTagged && !exportsApi && scan.scannedProjects.has(projectName)) {\n violations.push({ project: projectName, kind: 'unnecessary-tag' });\n }\n }\n return violations.sort((a: ApiLibTagViolation, b: ApiLibTagViolation) => a.project.localeCompare(b.project));\n}\n\n/** Human-readable, fix-oriented report for one tag/code mismatch. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches api-relations-validator.ts\nexport function describeApiLibTagViolation(violation: ApiLibTagViolation): string {\n if (violation.kind === 'missing-tag') {\n return (\n ` ❌ '${violation.project}' exports an API contract (an abstract @ApiPath/@Rpc/@PubSub class) ` +\n `but is not tagged 'role:api-lib'.\\n` +\n ` Add \"role:api-lib\" to its project.json \"tags\" (replacing any \"role:lib\").`\n );\n }\n return (\n ` ❌ '${violation.project}' is tagged 'role:api-lib' but exports NO API contract ` +\n `(no abstract @ApiPath/@Rpc/@PubSub class).\\n` +\n ` Either add the API contract it should own, or retag it (e.g. \"role:lib\").`\n );\n}\n"]}
1
+ {"version":3,"file":"api-lib-tag-validator.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/api-lib-tag-validator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * API-lib tag validator (two-way)\n *\n * Keeps the two api roles and the CODE in sync, both directions (#1064, D1 + D3):\n * - a project that exports an `@ApiPath`/`@Rpc`/`@PubSub` contract (or a vendor contract under\n * `externalApiPaths`) MUST be tagged `role:api-lib` or `role:api-client` — else the arch graph, the\n * edge line-styles, and validate-api-relations can't treat it as one;\n * - a project tagged `role:api-lib` MUST export a contract or ONLY wire types — else the tag is a lie.\n * A contract is any of: an `@ApiPath`/`@Rpc`/`@PubSub` class (from the scan), an IPC contract\n * (`@WpInternal` / `@WpIpcEndpoint`), or an in-process abstract `…Api` behind a DI token. A DTO-only\n * library exports interfaces, type aliases, enums, data classes (no methods), and data-only constants;\n * - a project tagged `role:api-client` MUST export a contract: either an abstract `XxxApi`, or an\n * exported `XxxApi` interface implemented by a client registered with `@provideSingletonDefaultForApi`.\n * The bundled client beside it is the point of the role, so it is not judged as a wire type.\n *\n * \"Exports an @ApiPath contract\" is answered by the same source scan that owns apiRelations\n * (scan.apiLibProjects), so the tag can never drift from the code. The IPC / in-process / DTO-only\n * reading is a parser-only pass over the project's own non-test `src/**` — {@link ApiLibExportShape}.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { ApiScanResult } from './api-scanner';\nimport { collectTsFiles, isTestFile } from './api-ast';\n\nconst API_LIB_ROLE = 'api-lib';\nconst API_CLIENT_ROLE = 'api-client';\n\n/** The decorators that make an abstract class an IPC contract. */\nconst IPC_DECORATORS: readonly string[] = ['WpInternal', 'WpIpcEndpoint'];\n\n/**\n * A tag/code mismatch:\n * - 'missing-tag' — exports an API contract but is tagged neither role:api-lib nor role:api-client.\n * - 'unnecessary-tag' — tagged role:api-lib / role:api-client but exports no contract, and (for\n * api-lib) is not a DTO-only library either; `offenders` names what it exports.\n */\nexport class ApiLibTagViolation {\n constructor(\n public readonly project: string,\n public readonly kind: 'missing-tag' | 'unnecessary-tag',\n /** The role it carries (for 'unnecessary-tag'), else the role it should carry. */\n public readonly role: string,\n /** Exports that are neither a contract nor a wire type — empty for 'missing-tag'. */\n public readonly offenders: readonly string[] = [],\n ) {}\n}\n\n/** What a library's own source exports, sorted into contracts, wire types and everything else. */\nexport class ApiLibExportShape {\n constructor(\n /** True when it exports an IPC contract or an in-process abstract `…Api`. */\n public readonly exportsContract: boolean,\n /** True when it exports at least one wire type (interface, alias, enum, data class). */\n public readonly exportsWireType: boolean,\n /** `kind Name (file)` of every export that is neither — the implementation an api-lib must not hold. */\n public readonly offenders: readonly string[],\n ) {}\n\n /** A role:api-lib passes when it holds a contract or wire types, and nothing else. */\n isApiLib(): boolean {\n return (this.exportsContract || this.exportsWireType) && this.offenders.length === 0;\n }\n\n /** Parser-only: every top-level EXPORTED declaration in the project's non-test `src/**`. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static read(projectDir: string): ApiLibExportShape {\n const srcDir = path.join(projectDir, 'src');\n const acc = new ExportShapeAccumulator(projectDir);\n if (fs.existsSync(srcDir)) {\n for (const file of collectTsFiles(srcDir)) {\n if (isTestFile(file)) continue;\n const source = ts.createSourceFile(\n file,\n fs.readFileSync(file, 'utf8'),\n ts.ScriptTarget.Latest,\n true,\n );\n for (const statement of source.statements) acc.add(statement, file);\n }\n }\n return acc.shape();\n }\n}\n\n/** Sorts each exported statement of one library into contract / wire type / offender. */\nclass ExportShapeAccumulator {\n private contract = false;\n private wire = false;\n private readonly offenders: string[] = [];\n private readonly apiInterfaces = new Set<string>();\n private readonly registeredApiImplementations = new Set<string>();\n\n constructor(private readonly projectDir: string) {}\n\n add(statement: ts.Statement, file: string): void {\n if (!this.isExported(statement)) return;\n const where = path.relative(this.projectDir, file);\n if (ts.isInterfaceDeclaration(statement)) {\n this.wire = true;\n if (statement.name.text.endsWith('Api')) this.apiInterfaces.add(statement.name.text);\n } else if (ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement)) {\n this.wire = true;\n } else if (ts.isClassDeclaration(statement)) {\n this.addClass(statement, where);\n } else if (ts.isVariableStatement(statement)) {\n for (const decl of statement.declarationList.declarations) {\n if (this.isDiToken(decl.initializer) || this.isDataConstant(decl.initializer))\n continue;\n this.offenders.push(`const ${decl.name.getText()} (${where})`);\n }\n } else if (ts.isFunctionDeclaration(statement)) {\n this.offenders.push(`function ${statement.name?.text ?? '<default>'} (${where})`);\n }\n }\n\n shape(): ApiLibExportShape {\n const registeredInterface = [...this.apiInterfaces].some((name: string) =>\n this.registeredApiImplementations.has(name),\n );\n return new ApiLibExportShape(\n this.contract || registeredInterface,\n this.wire,\n [...this.offenders].sort(),\n );\n }\n\n private addClass(cls: ts.ClassDeclaration, where: string): void {\n const name = cls.name?.text ?? '<default>';\n if (this.hasDecorator(cls, 'provideSingletonDefaultForApi')) {\n for (const heritage of cls.heritageClauses ?? []) {\n if (heritage.token !== ts.SyntaxKind.ImplementsKeyword) continue;\n for (const type of heritage.types) {\n const implemented = type.expression.getText().split('.').pop();\n if (implemented !== undefined)\n this.registeredApiImplementations.add(implemented);\n }\n }\n }\n const isAbstract = (ts.getModifiers(cls) ?? []).some(\n (m: ts.Modifier) => m.kind === ts.SyntaxKind.AbstractKeyword,\n );\n if (isAbstract && (name.endsWith('Api') || this.carriesIpc(cls))) {\n this.contract = true;\n return;\n }\n const hasMethod = cls.members.some(\n (member: ts.ClassElement) =>\n ts.isMethodDeclaration(member) &&\n !(ts.getModifiers(member) ?? []).some(\n (m: ts.Modifier) => m.kind === ts.SyntaxKind.StaticKeyword,\n ),\n );\n if (hasMethod)\n this.offenders.push(\n `class ${name} (${where}) — it has methods, so it is an implementation`,\n );\n else this.wire = true;\n }\n\n /** `@WpInternal(...)` on the class or `@WpIpcEndpoint(...)` on any member. */\n private carriesIpc(cls: ts.ClassDeclaration): boolean {\n const named = (node: ts.Node): boolean =>\n IPC_DECORATORS.some((name: string) => this.hasDecorator(node, name));\n return named(cls) || cls.members.some((member: ts.ClassElement) => named(member));\n }\n\n private hasDecorator(node: ts.Node, expected: string): boolean {\n return (ts.canHaveDecorators(node) ? (ts.getDecorators(node) ?? []) : []).some(\n (decorator: ts.Decorator) => {\n const expr = ts.isCallExpression(decorator.expression)\n ? decorator.expression.expression\n : decorator.expression;\n const name = ts.isPropertyAccessExpression(expr) ? expr.name.text : expr.getText();\n return name === expected;\n },\n );\n }\n\n /** Literal/structural protocol data is contract support, not an executable implementation. */\n private isDataConstant(init: ts.Expression | undefined): boolean {\n if (init === undefined) return false;\n const expr = this.unwrap(init);\n if (\n ts.isStringLiteralLike(expr) ||\n ts.isNumericLiteral(expr) ||\n expr.kind === ts.SyntaxKind.TrueKeyword ||\n expr.kind === ts.SyntaxKind.FalseKeyword ||\n expr.kind === ts.SyntaxKind.NullKeyword ||\n ts.isIdentifier(expr) ||\n ts.isPropertyAccessExpression(expr)\n ) {\n return true;\n }\n if (ts.isPrefixUnaryExpression(expr)) return this.isDataConstant(expr.operand);\n if (ts.isBinaryExpression(expr)) {\n return this.isDataConstant(expr.left) && this.isDataConstant(expr.right);\n }\n if (ts.isConditionalExpression(expr)) {\n return (\n this.isDataConstant(expr.condition) &&\n this.isDataConstant(expr.whenTrue) &&\n this.isDataConstant(expr.whenFalse)\n );\n }\n if (ts.isTemplateExpression(expr)) {\n return expr.templateSpans.every((span: ts.TemplateSpan) =>\n this.isDataConstant(span.expression),\n );\n }\n if (ts.isArrayLiteralExpression(expr)) {\n return expr.elements.every(\n (element: ts.Expression) =>\n ts.isOmittedExpression(element) ||\n (ts.isSpreadElement(element)\n ? this.isDataConstant(element.expression)\n : this.isDataConstant(element)),\n );\n }\n if (ts.isObjectLiteralExpression(expr)) {\n return expr.properties.every((prop: ts.ObjectLiteralElementLike) => {\n if (ts.isPropertyAssignment(prop)) return this.isDataConstant(prop.initializer);\n if (ts.isShorthandPropertyAssignment(prop)) return true;\n if (ts.isSpreadAssignment(prop)) return this.isDataConstant(prop.expression);\n return false;\n });\n }\n return false;\n }\n\n private unwrap(expr: ts.Expression): ts.Expression {\n if (\n ts.isAsExpression(expr) ||\n ts.isTypeAssertionExpression(expr) ||\n ts.isSatisfiesExpression(expr) ||\n ts.isParenthesizedExpression(expr) ||\n ts.isNonNullExpression(expr)\n ) {\n return this.unwrap(expr.expression);\n }\n return expr;\n }\n\n /** `Symbol('x')`, `Symbol.for('x')`, or an object literal of them — the token an abstract `…Api` binds behind. */\n private isDiToken(init: ts.Expression | undefined): boolean {\n if (init === undefined) return false;\n const expr =\n ts.isAsExpression(init) || ts.isSatisfiesExpression(init) ? init.expression : init;\n if (ts.isCallExpression(expr)) {\n const callee = expr.expression.getText();\n return callee === 'Symbol' || callee === 'Symbol.for';\n }\n if (ts.isObjectLiteralExpression(expr)) {\n return (\n expr.properties.length > 0 &&\n expr.properties.every(\n (prop: ts.ObjectLiteralElementLike) =>\n ts.isPropertyAssignment(prop) && this.isDiToken(prop.initializer),\n )\n );\n }\n return false;\n }\n\n private isExported(statement: ts.Statement): boolean {\n if (!ts.canHaveModifiers(statement)) return false;\n return (ts.getModifiers(statement) ?? []).some(\n (m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword,\n );\n }\n}\n\n/** Both-directions tag ⇔ code check. Uses the scan's detected api-lib set as ground truth for @ApiPath. */\n// webpieces-disable no-function-outside-class -- validator-lib entry point, matches api-relations-validator.ts\nexport function findApiLibTagViolations(\n projectInfos: Map<string, ProjectInfo>,\n scan: ApiScanResult,\n workspaceRoot: string,\n): ApiLibTagViolation[] {\n const violations: ApiLibTagViolation[] = [];\n for (const projectName of projectInfos.keys()) {\n const info = projectInfos.get(projectName)!;\n const role = resolveRole(info).role;\n const isApiRole = role === API_LIB_ROLE || role === API_CLIENT_ROLE;\n const exportsApi = scan.apiLibProjects.has(projectName);\n\n if (exportsApi && !isApiRole) {\n violations.push(new ApiLibTagViolation(projectName, 'missing-tag', API_LIB_ROLE));\n }\n // Only judge a tag when we actually SCANNED the project's source — otherwise we can't prove it\n // exports no contract (an unscannable project would falsely look empty).\n if (!isApiRole || exportsApi || !scan.scannedProjects.has(projectName)) continue;\n const shape = ApiLibExportShape.read(path.resolve(workspaceRoot, info.root));\n const ok = role === API_CLIENT_ROLE ? shape.exportsContract : shape.isApiLib();\n if (!ok)\n violations.push(\n new ApiLibTagViolation(projectName, 'unnecessary-tag', role, shape.offenders),\n );\n }\n return violations.sort((a: ApiLibTagViolation, b: ApiLibTagViolation) =>\n a.project.localeCompare(b.project),\n );\n}\n\n/** Human-readable, fix-oriented report for one tag/code mismatch. */\n// webpieces-disable no-function-outside-class -- pure formatter, matches api-relations-validator.ts\nexport function describeApiLibTagViolation(violation: ApiLibTagViolation): string {\n if (violation.kind === 'missing-tag') {\n return (\n ` ❌ '${violation.project}' exports an API contract (an abstract @ApiPath/@Rpc/@PubSub class) ` +\n `but is tagged neither 'role:api-lib' nor 'role:api-client'.\\n` +\n ` Add \"role:api-lib\" to its project.json \"tags\" (replacing any \"role:lib\"), or \"role:api-client\" ` +\n `when it also bundles the default implementation that talks to an outside system.`\n );\n }\n if (violation.role === API_CLIENT_ROLE) {\n return (\n ` ❌ '${violation.project}' is tagged 'role:api-client' but exports NO contract (no abstract ` +\n `…Api class, no @WpInternal/@WpIpcEndpoint contract, and no exported …Api interface implemented ` +\n `by an @provideSingletonDefaultForApi client).\\n` +\n ` An api-client is a contract PLUS its default implementation: export the XxxApi the registered ` +\n `client implements, or retag it (e.g. \"role:lib\").`\n );\n }\n const offenders =\n violation.offenders.length === 0\n ? ''\n : `\\n It exports, besides contracts and wire types: ${violation.offenders.join('; ')}.`;\n return (\n ` ❌ '${violation.project}' is tagged 'role:api-lib' but is neither a contract library (@ApiPath/@Rpc/` +\n `@PubSub, @WpInternal/@WpIpcEndpoint, or an abstract …Api behind a DI token) nor a DTO-only library ` +\n `(interfaces, type aliases, enums and data classes only).${offenders}\\n` +\n ` Move the implementation out to a role:lib / role:designed-lib project, or retag it.`\n );\n}\n"]}
@@ -32,6 +32,7 @@ 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
34
  import { ApiDocRule, ApiDocRulesFindings } from './api-doc-rules';
35
+ import { WireClosureRule } from './wire-closure';
35
36
  /** The whole-workspace result of a scan. */
36
37
  export interface ApiScanResult {
37
38
  /** projectName -> { apiLibProject -> relation }; only projects with ≥1 relation appear. */
@@ -95,6 +96,7 @@ export declare class ApiUsageScanner {
95
96
  private readonly openApiRule;
96
97
  /** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
97
98
  private readonly mcpRule;
99
+ private readonly wireClosureRule;
98
100
  private readonly locator;
99
101
  private readonly relationsByProject;
100
102
  private readonly scannedProjects;
@@ -109,7 +111,7 @@ export declare class ApiUsageScanner {
109
111
  /** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
110
112
  openApiRule?: ApiDocRule,
111
113
  /** `api-rules-for-mcp`'s switches — read from the config, which MUST state them (#1017). */
112
- mcpRule?: ApiDocRule);
114
+ mcpRule?: ApiDocRule, wireClosureRule?: WireClosureRule);
113
115
  scan(): ApiScanResult;
114
116
  private scanProject;
115
117
  private visit;
@@ -46,6 +46,7 @@ const api_contract_errors_1 = require("./api-contract-errors");
46
46
  const root_union_scan_1 = require("./root-union-scan");
47
47
  const api_doc_rules_1 = require("./api-doc-rules");
48
48
  const api_doc_rules_scan_1 = require("./api-doc-rules-scan");
49
+ const wire_closure_1 = require("./wire-closure");
49
50
  const api_ast_1 = require("./api-ast");
50
51
  const RPC_CLIENT_METHOD = 'createRpcClient';
51
52
  const PUBSUB_CLIENT_METHOD = 'createPubSubClient';
@@ -210,6 +211,7 @@ class ApiUsageScanner {
210
211
  rootUnionRule;
211
212
  openApiRule;
212
213
  mcpRule;
214
+ wireClosureRule;
213
215
  locator;
214
216
  relationsByProject = new Map();
215
217
  scannedProjects = new Set();
@@ -224,13 +226,14 @@ class ApiUsageScanner {
224
226
  /** `api-rules-for-openapi`'s switches — read from the config, which MUST state them (#1017). */
225
227
  openApiRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.OPENAPI_RULE),
226
228
  /** `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)) {
229
+ mcpRule = api_doc_rules_1.ApiDocRule.off(api_doc_rules_1.MCP_RULE), wireClosureRule = wire_closure_1.WireClosureRule.withoutSuffixes()) {
228
230
  this.workspaceRoot = workspaceRoot;
229
231
  this.projectInfos = projectInfos;
230
232
  this.externalApiPaths = externalApiPaths;
231
233
  this.rootUnionRule = rootUnionRule;
232
234
  this.openApiRule = openApiRule;
233
235
  this.mcpRule = mcpRule;
236
+ this.wireClosureRule = wireClosureRule;
234
237
  this.locator = new ProjectLocator(workspaceRoot, projectInfos);
235
238
  this.decoratorArgDiagnostics = new api_ast_1.DecoratorArgDiagnostics(workspaceRoot);
236
239
  }
@@ -255,7 +258,7 @@ class ApiUsageScanner {
255
258
  undeclaredExternalCallers: this.decoratorArgDiagnostics.undeclaredExternalCallers(),
256
259
  undeclaredEndpointOperations: this.decoratorArgDiagnostics.undeclaredEndpointOperations(),
257
260
  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(),
261
+ apiDocRules: new api_doc_rules_scan_1.ApiDocRulesScan(this.workspaceRoot, this.projectInfos, this.openApiRule, this.mcpRule, this.wireClosureRule).run(),
259
262
  };
260
263
  }
261
264
  scanProject(info) {
@@ -400,7 +403,7 @@ exports.ApiUsageScanner = ApiUsageScanner;
400
403
  */
401
404
  // webpieces-disable no-function-outside-class -- module entry point, mirrors generateReducedGraph/collectBindings
402
405
  function scanAndAttachApiRelations(workspaceRoot, graph, projectInfos, externalApiPaths = []) {
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();
406
+ 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), wire_closure_1.WireClosureRule.fromConfig(workspaceRoot)).scan();
404
407
  for (const projectName of result.relationsByProject.keys()) {
405
408
  const entry = graph[projectName];
406
409
  if (entry)