@webpieces/nx-webpieces-rules 0.4.901 → 0.4.903
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 -7
- package/src/lib/api-usage/wire-closure.d.ts +3 -1
- package/src/lib/api-usage/wire-closure.js +11 -5
- package/src/lib/api-usage/wire-closure.js.map +1 -1
- package/src/lib/api-usage/wire-suffix-entry.d.ts +1 -0
- package/src/lib/api-usage/wire-suffix-entry.js +1 -0
- package/src/lib/api-usage/wire-suffix-entry.js.map +1 -1
- package/src/lib/api-usage/wire-suffix-scope.d.ts +18 -0
- package/src/lib/api-usage/wire-suffix-scope.js +99 -0
- package/src/lib/api-usage/wire-suffix-scope.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.903",
|
|
4
4
|
"description": "Nx-specific webpieces validation rules, graph tooling, executors, and inference plugin.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@webpieces/api-doc-model": "0.4.
|
|
24
|
-
"@webpieces/core-util": "0.4.
|
|
25
|
-
"@webpieces/eslint-rules": "0.4.
|
|
26
|
-
"@webpieces/rules-config": "0.4.
|
|
23
|
+
"@webpieces/api-doc-model": "0.4.903",
|
|
24
|
+
"@webpieces/core-util": "0.4.903",
|
|
25
|
+
"@webpieces/eslint-rules": "0.4.903",
|
|
26
|
+
"@webpieces/rules-config": "0.4.903",
|
|
27
27
|
"madge": "8.0.0",
|
|
28
|
-
"@webpieces/rules-sdk": "0.4.
|
|
28
|
+
"@webpieces/rules-sdk": "0.4.903"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@nx/devkit": ">=18.0.0"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@webpieces/tooling-testkit": "0.4.
|
|
44
|
+
"@webpieces/tooling-testkit": "0.4.903"
|
|
45
45
|
},
|
|
46
46
|
"types": "./src/index.d.ts",
|
|
47
47
|
"bin": {
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
import { DocumentedType } from '@webpieces/api-doc-model';
|
|
21
21
|
import { WireSuffixEntry } from './wire-suffix-entry';
|
|
22
22
|
import { ProjectInfo } from '../project-info';
|
|
23
|
+
import { WireSuffixScope } from './wire-suffix-scope';
|
|
23
24
|
/** The `required-type-suffix` entries the wire closure enforces; empty when that rule is OFF. */
|
|
24
25
|
export declare class WireClosureRule {
|
|
25
26
|
readonly suffixEntries: readonly WireSuffixEntry[];
|
|
26
|
-
|
|
27
|
+
readonly scope: WireSuffixScope | undefined;
|
|
28
|
+
constructor(suffixEntries: readonly WireSuffixEntry[], scope: WireSuffixScope | undefined);
|
|
27
29
|
/** Only the role check — what a unit test or a repo with `required-type-suffix` OFF gets. */
|
|
28
30
|
static withoutSuffixes(): WireClosureRule;
|
|
29
31
|
/** `required-type-suffix`'s entries, unless that rule is OFF or time-boxed / branch-scoped off. */
|
|
@@ -26,17 +26,21 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
26
26
|
const rules_config_2 = require("@webpieces/rules-config");
|
|
27
27
|
const role_resolver_1 = require("../role-resolver");
|
|
28
28
|
const rule_gate_1 = require("../rule-gate");
|
|
29
|
+
const rules_config_3 = require("@webpieces/rules-config");
|
|
30
|
+
const wire_suffix_scope_1 = require("./wire-suffix-scope");
|
|
29
31
|
const API_LIB_ROLE = 'api-lib';
|
|
30
32
|
/** The `required-type-suffix` entries the wire closure enforces; empty when that rule is OFF. */
|
|
31
33
|
class WireClosureRule {
|
|
32
34
|
suffixEntries;
|
|
33
|
-
|
|
35
|
+
scope;
|
|
36
|
+
constructor(suffixEntries, scope) {
|
|
34
37
|
this.suffixEntries = suffixEntries;
|
|
38
|
+
this.scope = scope;
|
|
35
39
|
}
|
|
36
40
|
/** Only the role check — what a unit test or a repo with `required-type-suffix` OFF gets. */
|
|
37
41
|
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
38
42
|
static withoutSuffixes() {
|
|
39
|
-
return new WireClosureRule([]);
|
|
43
|
+
return new WireClosureRule([], undefined);
|
|
40
44
|
}
|
|
41
45
|
/** `required-type-suffix`'s entries, unless that rule is OFF or time-boxed / branch-scoped off. */
|
|
42
46
|
// webpieces-disable no-function-outside-class -- static factory of this class
|
|
@@ -45,8 +49,7 @@ class WireClosureRule {
|
|
|
45
49
|
return WireClosureRule.withoutSuffixes();
|
|
46
50
|
}
|
|
47
51
|
const rule = (0, rules_config_2.loadAndValidate)(workspaceRoot).resolved.rules.get('required-type-suffix');
|
|
48
|
-
|
|
49
|
-
return new WireClosureRule(Array.isArray(entries) ? entries : []);
|
|
52
|
+
return new WireClosureRule(rule.options['entries'], new wire_suffix_scope_1.WireSuffixScope(workspaceRoot, rule.options['mode'], rule.options['allowedPaths'] ?? [], new rules_config_3.DiffScope()));
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
exports.WireClosureRule = WireClosureRule;
|
|
@@ -103,7 +106,10 @@ class WireClosure {
|
|
|
103
106
|
'contracts and wire types (validate-api-lib-tag refuses a role:api-lib with an implementation).'));
|
|
104
107
|
}
|
|
105
108
|
const governing = this.picker.winner(relFile, this.rule.suffixEntries.map((entry) => new rules_config_1.PathMatchCandidate(entry.paths, entry)));
|
|
106
|
-
if (governing !== undefined &&
|
|
109
|
+
if (governing !== undefined &&
|
|
110
|
+
!governing.entry.exempt &&
|
|
111
|
+
this.rule.scope?.includes(type.name, relFile, owner.root, type.location, this.roots.map((root) => root.info.root)) === true &&
|
|
112
|
+
!this.endsInSuffix(type.name, governing.entry.suffixes)) {
|
|
107
113
|
verdicts.push(new WireClosureVerdict(`'${type.name}' goes over the wire from ${relFile} but does not end in one of ` +
|
|
108
114
|
`[${governing.entry.suffixes.join(', ')}], which the required-type-suffix entry ` +
|
|
109
115
|
`'${governing.glob}' demands of it`, `Rename '${type.name}' to end in one of ${governing.entry.suffixes.join(', ')} — the suffix tells a ` +
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire-closure.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/wire-closure.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The WIRE CLOSURE of a contract (#1064, D4): every named type an `@ApiPath` contract reaches must be\n * DECLARED in a `role:api-lib` project, and must end in a suffix that project's `required-type-suffix`\n * entry demands.\n *\n * Why it lives here and not in the dependency rule: `api-lib-dependencies` only sees project EDGES. A\n * relative-path import into another library, or a DTO library that itself reaches outside, puts a type\n * on the wire without any edge the graph rule could refuse. The extractor already follows every type a\n * contract reaches through the compiler — the same walk `api-rules-for-openapi` / `api-rules-for-mcp`\n * run, with no generate tag needed — so this is one more question asked of each type that walk found.\n *\n * Every api-lib rule before this one (`required-type-suffix`, `one-enum-spelling-in-api-lib`,\n * `no-utility-types-in-api-lib`, `no-inline-import-in-api-lib`) reads only source files INSIDE the api\n * library. A type declared in another package was, by construction, outside every one of them — which is\n * how `AiProvider` and the `BrowserTelemetry` trio reached the wire from a general library unchecked.\n *\n * A type declared OUTSIDE the workspace (a published package under node_modules) is not judged here:\n * its own package owns its schema, and the generator's fail-closed components check covers it.\n */\n\nimport * as path from 'path';\nimport { DocumentedType } from '@webpieces/api-doc-model';\nimport { FirstPathMatch, PathMatchCandidate } from '@webpieces/rules-config';\nimport { loadAndValidate } from '@webpieces/rules-config';\nimport { WireSuffixEntry } from './wire-suffix-entry';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { RuleGate } from '../rule-gate';\n\nconst API_LIB_ROLE = 'api-lib';\n\n/** The `required-type-suffix` entries the wire closure enforces; empty when that rule is OFF. */\nexport class WireClosureRule {\n constructor(public readonly suffixEntries: readonly WireSuffixEntry[]) {}\n\n /** Only the role check — what a unit test or a repo with `required-type-suffix` OFF gets. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static withoutSuffixes(): WireClosureRule {\n return new WireClosureRule([]);\n }\n\n /** `required-type-suffix`'s entries, unless that rule is OFF or time-boxed / branch-scoped off. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromConfig(workspaceRoot: string): WireClosureRule {\n if (new RuleGate().skipReason(workspaceRoot, 'required-type-suffix', true) !== null) {\n return WireClosureRule.withoutSuffixes();\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get('required-type-suffix');\n const entries = rule?.options['entries'];\n return new WireClosureRule(Array.isArray(entries) ? (entries as WireSuffixEntry[]) : []);\n }\n}\n\n/** ONE wire-closure finding: what is wrong with a reached type, and what to do. */\nexport class WireClosureVerdict {\n constructor(\n public readonly what: string,\n public readonly cure: string,\n ) {}\n}\n\n/** A workspace project root, absolute, for the longest-prefix owner lookup. */\nclass OwnedRoot {\n constructor(\n public readonly info: ProjectInfo,\n public readonly abs: string,\n ) {}\n}\n\n/** Judges every type a contract reaches against the api-lib role and the suffix rule. */\nexport class WireClosure {\n private readonly roots: OwnedRoot[];\n private readonly picker = new FirstPathMatch();\n\n constructor(\n private readonly workspaceRoot: string,\n projectInfos: Map<string, ProjectInfo>,\n private readonly rule: WireClosureRule,\n ) {\n this.roots = [...projectInfos.values()]\n .filter((info: ProjectInfo) => info.root !== '' && info.root !== '.')\n .map((info: ProjectInfo) => new OwnedRoot(info, path.resolve(workspaceRoot, info.root)))\n // Longest root first so a nested project wins over its parent.\n .sort((a: OwnedRoot, b: OwnedRoot) => b.abs.length - a.abs.length);\n }\n\n /** Every verdict for ONE reached type, declared in the file `absFile`. */\n judge(type: DocumentedType, absFile: string): WireClosureVerdict[] {\n const abs = path.resolve(this.workspaceRoot, absFile);\n if (abs.split(path.sep).includes('node_modules')) return [];\n const owner = this.ownerOf(abs);\n if (owner === null) return [];\n const relFile = path.relative(this.workspaceRoot, abs).split(path.sep).join('/');\n const role = resolveRole(owner).role ?? 'none';\n const declared = `${type.packageName ?? 'no package'} (project '${owner.name}', role:${role})`;\n const verdicts: WireClosureVerdict[] = [];\n if (role !== API_LIB_ROLE) {\n verdicts.push(\n new WireClosureVerdict(\n `'${type.name}' goes over the wire but is declared in ${declared}, which is not a role:api-lib ` +\n 'project — every type a contract reaches must be declared in an api library',\n `Move '${type.name}' into a role:api-lib project — as a '…Dto' string enum when it is a ` +\n `string-literal union. Retagging '${owner.name}' is not a way out unless it holds only ` +\n 'contracts and wire types (validate-api-lib-tag refuses a role:api-lib with an implementation).',\n ),\n );\n }\n const governing = this.picker.winner(\n relFile,\n this.rule.suffixEntries.map(\n (entry: WireSuffixEntry) => new PathMatchCandidate(entry.paths, entry),\n ),\n );\n if (governing !== undefined && !this.endsInSuffix(type.name, governing.entry.suffixes)) {\n verdicts.push(\n new WireClosureVerdict(\n `'${type.name}' goes over the wire from ${relFile} but does not end in one of ` +\n `[${governing.entry.suffixes.join(', ')}], which the required-type-suffix entry ` +\n `'${governing.glob}' demands of it`,\n `Rename '${type.name}' to end in one of ${governing.entry.suffixes.join(', ')} — the suffix tells a ` +\n 'reader which layer a type belongs to, and a wire type is the layer a partner reads.',\n ),\n );\n }\n return verdicts;\n }\n\n private endsInSuffix(name: string, suffixes: readonly string[]): boolean {\n return suffixes.some(\n (suffix: string) => name.length > suffix.length && name.endsWith(suffix),\n );\n }\n\n private ownerOf(abs: string): ProjectInfo | null {\n for (const root of this.roots) {\n if (abs === root.abs || abs.startsWith(root.abs + path.sep)) return root.info;\n }\n return null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"wire-closure.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/wire-closure.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The WIRE CLOSURE of a contract (#1064, D4): every named type an `@ApiPath` contract reaches must be\n * DECLARED in a `role:api-lib` project, and must end in a suffix that project's `required-type-suffix`\n * entry demands.\n *\n * Why it lives here and not in the dependency rule: `api-lib-dependencies` only sees project EDGES. A\n * relative-path import into another library, or a DTO library that itself reaches outside, puts a type\n * on the wire without any edge the graph rule could refuse. The extractor already follows every type a\n * contract reaches through the compiler — the same walk `api-rules-for-openapi` / `api-rules-for-mcp`\n * run, with no generate tag needed — so this is one more question asked of each type that walk found.\n *\n * Every api-lib rule before this one (`required-type-suffix`, `one-enum-spelling-in-api-lib`,\n * `no-utility-types-in-api-lib`, `no-inline-import-in-api-lib`) reads only source files INSIDE the api\n * library. A type declared in another package was, by construction, outside every one of them — which is\n * how `AiProvider` and the `BrowserTelemetry` trio reached the wire from a general library unchecked.\n *\n * A type declared OUTSIDE the workspace (a published package under node_modules) is not judged here:\n * its own package owns its schema, and the generator's fail-closed components check covers it.\n */\n\nimport * as path from 'path';\nimport { DocumentedType } from '@webpieces/api-doc-model';\nimport { FirstPathMatch, PathMatchCandidate } from '@webpieces/rules-config';\nimport { loadAndValidate } from '@webpieces/rules-config';\nimport { WireSuffixEntry } from './wire-suffix-entry';\nimport { ProjectInfo } from '../project-info';\nimport { resolveRole } from '../role-resolver';\nimport { RuleGate } from '../rule-gate';\nimport { DiffScope } from '@webpieces/rules-config';\nimport { ModifiedCodeMode } from '@webpieces/rules-sdk';\nimport { WireSuffixScope } from './wire-suffix-scope';\n\nconst API_LIB_ROLE = 'api-lib';\n\n/** The `required-type-suffix` entries the wire closure enforces; empty when that rule is OFF. */\nexport class WireClosureRule {\n constructor(\n public readonly suffixEntries: readonly WireSuffixEntry[],\n public readonly scope: WireSuffixScope | undefined,\n ) {}\n\n /** Only the role check — what a unit test or a repo with `required-type-suffix` OFF gets. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static withoutSuffixes(): WireClosureRule {\n return new WireClosureRule([], undefined);\n }\n\n /** `required-type-suffix`'s entries, unless that rule is OFF or time-boxed / branch-scoped off. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromConfig(workspaceRoot: string): WireClosureRule {\n if (new RuleGate().skipReason(workspaceRoot, 'required-type-suffix', true) !== null) {\n return WireClosureRule.withoutSuffixes();\n }\n const rule = loadAndValidate(workspaceRoot).resolved.rules.get('required-type-suffix')!;\n return new WireClosureRule(\n rule.options['entries'] as WireSuffixEntry[],\n new WireSuffixScope(\n workspaceRoot,\n rule.options['mode'] as ModifiedCodeMode,\n (rule.options['allowedPaths'] as string[] | undefined) ?? [],\n new DiffScope(),\n ),\n );\n }\n}\n\n/** ONE wire-closure finding: what is wrong with a reached type, and what to do. */\nexport class WireClosureVerdict {\n constructor(\n public readonly what: string,\n public readonly cure: string,\n ) {}\n}\n\n/** A workspace project root, absolute, for the longest-prefix owner lookup. */\nclass OwnedRoot {\n constructor(\n public readonly info: ProjectInfo,\n public readonly abs: string,\n ) {}\n}\n\n/** Judges every type a contract reaches against the api-lib role and the suffix rule. */\nexport class WireClosure {\n private readonly roots: OwnedRoot[];\n private readonly picker = new FirstPathMatch();\n\n constructor(\n private readonly workspaceRoot: string,\n projectInfos: Map<string, ProjectInfo>,\n private readonly rule: WireClosureRule,\n ) {\n this.roots = [...projectInfos.values()]\n .filter((info: ProjectInfo) => info.root !== '' && info.root !== '.')\n .map((info: ProjectInfo) => new OwnedRoot(info, path.resolve(workspaceRoot, info.root)))\n // Longest root first so a nested project wins over its parent.\n .sort((a: OwnedRoot, b: OwnedRoot) => b.abs.length - a.abs.length);\n }\n\n /** Every verdict for ONE reached type, declared in the file `absFile`. */\n judge(type: DocumentedType, absFile: string): WireClosureVerdict[] {\n const abs = path.resolve(this.workspaceRoot, absFile);\n if (abs.split(path.sep).includes('node_modules')) return [];\n const owner = this.ownerOf(abs);\n if (owner === null) return [];\n const relFile = path.relative(this.workspaceRoot, abs).split(path.sep).join('/');\n const role = resolveRole(owner).role ?? 'none';\n const declared = `${type.packageName ?? 'no package'} (project '${owner.name}', role:${role})`;\n const verdicts: WireClosureVerdict[] = [];\n if (role !== API_LIB_ROLE) {\n verdicts.push(\n new WireClosureVerdict(\n `'${type.name}' goes over the wire but is declared in ${declared}, which is not a role:api-lib ` +\n 'project — every type a contract reaches must be declared in an api library',\n `Move '${type.name}' into a role:api-lib project — as a '…Dto' string enum when it is a ` +\n `string-literal union. Retagging '${owner.name}' is not a way out unless it holds only ` +\n 'contracts and wire types (validate-api-lib-tag refuses a role:api-lib with an implementation).',\n ),\n );\n }\n const governing = this.picker.winner(\n relFile,\n this.rule.suffixEntries.map(\n (entry: WireSuffixEntry) => new PathMatchCandidate(entry.paths, entry),\n ),\n );\n if (\n governing !== undefined &&\n !governing.entry.exempt &&\n this.rule.scope?.includes(\n type.name,\n relFile,\n owner.root,\n type.location,\n this.roots.map((root: OwnedRoot) => root.info.root),\n ) === true &&\n !this.endsInSuffix(type.name, governing.entry.suffixes)\n ) {\n verdicts.push(\n new WireClosureVerdict(\n `'${type.name}' goes over the wire from ${relFile} but does not end in one of ` +\n `[${governing.entry.suffixes.join(', ')}], which the required-type-suffix entry ` +\n `'${governing.glob}' demands of it`,\n `Rename '${type.name}' to end in one of ${governing.entry.suffixes.join(', ')} — the suffix tells a ` +\n 'reader which layer a type belongs to, and a wire type is the layer a partner reads.',\n ),\n );\n }\n return verdicts;\n }\n\n private endsInSuffix(name: string, suffixes: readonly string[]): boolean {\n return suffixes.some(\n (suffix: string) => name.length > suffix.length && name.endsWith(suffix),\n );\n }\n\n private ownerOf(abs: string): ProjectInfo | null {\n for (const root of this.roots) {\n if (abs === root.abs || abs.startsWith(root.abs + path.sep)) return root.info;\n }\n return null;\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire-suffix-entry.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/wire-suffix-entry.ts"],"names":[],"mappings":";;;AAAA,qGAAqG;AACrG,MAAa,eAAe;IACxB,KAAK,CAAY;IACjB,QAAQ,CAAY;
|
|
1
|
+
{"version":3,"file":"wire-suffix-entry.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/wire-suffix-entry.ts"],"names":[],"mappings":";;;AAAA,qGAAqG;AACrG,MAAa,eAAe;IACxB,KAAK,CAAY;IACjB,QAAQ,CAAY;IACpB,MAAM,CAAW;CACpB;AAJD,0CAIC","sourcesContent":["/** Structural settings consumed by this contributor; the declared canonical owner validates them. */\nexport class WireSuffixEntry {\n paths!: string[];\n suffixes!: string[];\n exempt!: boolean;\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DiffScope } from '@webpieces/rules-config';
|
|
2
|
+
import { ModifiedCodeMode } from '@webpieces/rules-sdk';
|
|
3
|
+
/** Suffix enforcement keeps its own rollout scope, independently of the enclosing doc rule. */
|
|
4
|
+
export declare class WireSuffixScope {
|
|
5
|
+
private readonly root;
|
|
6
|
+
private readonly mode;
|
|
7
|
+
private readonly allowedPaths;
|
|
8
|
+
private readonly diff;
|
|
9
|
+
private readonly changed;
|
|
10
|
+
private readonly lines;
|
|
11
|
+
private readonly base;
|
|
12
|
+
private readonly head;
|
|
13
|
+
constructor(root: string, mode: ModifiedCodeMode, allowedPaths: readonly string[], diff: DiffScope);
|
|
14
|
+
includes(name: string, relFile: string, projectRoot: string, location: string, projectRoots: readonly string[]): boolean;
|
|
15
|
+
private declarationAt;
|
|
16
|
+
private fileInScope;
|
|
17
|
+
private changedLines;
|
|
18
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WireSuffixScope = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
8
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
9
|
+
/** Suffix enforcement keeps its own rollout scope, independently of the enclosing doc rule. */
|
|
10
|
+
class WireSuffixScope {
|
|
11
|
+
root;
|
|
12
|
+
mode;
|
|
13
|
+
allowedPaths;
|
|
14
|
+
diff;
|
|
15
|
+
changed;
|
|
16
|
+
lines = new Map();
|
|
17
|
+
base;
|
|
18
|
+
head;
|
|
19
|
+
constructor(root, mode, allowedPaths, diff) {
|
|
20
|
+
this.root = root;
|
|
21
|
+
this.mode = mode;
|
|
22
|
+
this.allowedPaths = allowedPaths;
|
|
23
|
+
this.diff = diff;
|
|
24
|
+
const range = mode === 'RUN_EVERY_TIME' ? undefined : diff.resolveBase(root);
|
|
25
|
+
this.base = range?.base;
|
|
26
|
+
this.head = range?.head;
|
|
27
|
+
this.changed =
|
|
28
|
+
mode === 'RUN_EVERY_TIME'
|
|
29
|
+
? null
|
|
30
|
+
: new Set(this.base === undefined
|
|
31
|
+
? []
|
|
32
|
+
: diff.getChangedFiles(root, this.base, this.head, { tsOnly: false }));
|
|
33
|
+
}
|
|
34
|
+
includes(name, relFile, projectRoot, location, projectRoots) {
|
|
35
|
+
if (this.mode === 'OFF' || (0, rules_config_1.isPathExcluded)(relFile, this.allowedPaths))
|
|
36
|
+
return false;
|
|
37
|
+
if (relFile.endsWith('.d.ts') ||
|
|
38
|
+
/\.(spec|test)\.ts$/.test(relFile) ||
|
|
39
|
+
relFile.includes('__tests__/'))
|
|
40
|
+
return false;
|
|
41
|
+
if (!this.fileInScope(relFile, projectRoot, projectRoots))
|
|
42
|
+
return false;
|
|
43
|
+
const file = path.resolve(this.root, relFile);
|
|
44
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
45
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
46
|
+
const declaredLine = Number(/:(\d+):\d+$/.exec(location)?.[1]);
|
|
47
|
+
const declaration = this.declarationAt(source, name, declaredLine);
|
|
48
|
+
if (declaration === undefined)
|
|
49
|
+
return false;
|
|
50
|
+
const named = declaration;
|
|
51
|
+
const line = source.getLineAndCharacterOfPosition(named.name.getStart(source)).line + 1;
|
|
52
|
+
const textLines = text.split('\n');
|
|
53
|
+
if ((0, rules_config_1.hasDisable)(textLines[line - 1] ?? '', 'required-type-suffix') ||
|
|
54
|
+
(0, rules_config_1.hasDisable)(textLines[line - 2] ?? '', 'required-type-suffix'))
|
|
55
|
+
return false;
|
|
56
|
+
return this.mode !== 'NEW_AND_MODIFIED_CODE' || this.changedLines(relFile).has(line);
|
|
57
|
+
}
|
|
58
|
+
declarationAt(source, name, line) {
|
|
59
|
+
let found;
|
|
60
|
+
const visit = (node) => {
|
|
61
|
+
if ((ts.isClassDeclaration(node) ||
|
|
62
|
+
ts.isInterfaceDeclaration(node) ||
|
|
63
|
+
ts.isEnumDeclaration(node) ||
|
|
64
|
+
ts.isTypeAliasDeclaration(node)) &&
|
|
65
|
+
node.name?.text === name &&
|
|
66
|
+
source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1 === line)
|
|
67
|
+
found = node;
|
|
68
|
+
if (found === undefined)
|
|
69
|
+
ts.forEachChild(node, visit);
|
|
70
|
+
};
|
|
71
|
+
visit(source);
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
fileInScope(relFile, projectRoot, projectRoots) {
|
|
75
|
+
if (this.changed === null)
|
|
76
|
+
return true;
|
|
77
|
+
if (this.mode !== 'MODIFIED_PROJECTS')
|
|
78
|
+
return this.changed.has(relFile);
|
|
79
|
+
return [...this.changed].some((file) => {
|
|
80
|
+
const owner = projectRoots
|
|
81
|
+
.filter((root) => file === root || file.startsWith(`${root}/`))
|
|
82
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
83
|
+
return owner === projectRoot;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
changedLines(relFile) {
|
|
87
|
+
let lines = this.lines.get(relFile);
|
|
88
|
+
if (lines === undefined) {
|
|
89
|
+
lines =
|
|
90
|
+
this.base === undefined
|
|
91
|
+
? new Set()
|
|
92
|
+
: this.diff.getChangedLineNumbers(this.diff.getFileDiff(this.root, relFile, this.base, this.head));
|
|
93
|
+
this.lines.set(relFile, lines);
|
|
94
|
+
}
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.WireSuffixScope = WireSuffixScope;
|
|
99
|
+
//# sourceMappingURL=wire-suffix-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-suffix-scope.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/api-usage/wire-suffix-scope.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAC7B,uDAAiC;AACjC,0DAAgF;AAGhF,+FAA+F;AAC/F,MAAa,eAAe;IAOH;IACA;IACA;IACA;IATJ,OAAO,CAAqB;IAC5B,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IACvC,IAAI,CAAqB;IACzB,IAAI,CAAqB;IAE1C,YACqB,IAAY,EACZ,IAAsB,EACtB,YAA+B,EAC/B,IAAe;QAHf,SAAI,GAAJ,IAAI,CAAQ;QACZ,SAAI,GAAJ,IAAI,CAAkB;QACtB,iBAAY,GAAZ,YAAY,CAAmB;QAC/B,SAAI,GAAJ,IAAI,CAAW;QAEhC,MAAM,KAAK,GAAG,IAAI,KAAK,gBAAgB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC7E,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE,IAAI,CAAC;QACxB,IAAI,CAAC,OAAO;YACR,IAAI,KAAK,gBAAgB;gBACrB,CAAC,CAAC,IAAI;gBACN,CAAC,CAAC,IAAI,GAAG,CACH,IAAI,CAAC,IAAI,KAAK,SAAS;oBACnB,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAC5E,CAAC;IAChB,CAAC;IAED,QAAQ,CACJ,IAAY,EACZ,OAAe,EACf,WAAmB,EACnB,QAAgB,EAChB,YAA+B;QAE/B,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,IAAA,6BAAc,EAAC,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC;YAAE,OAAO,KAAK,CAAC;QACpF,IACI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;YACzB,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC;YAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;YAE9B,OAAO,KAAK,CAAC;QACjB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,YAAY,CAAC;YAAE,OAAO,KAAK,CAAC;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC9C,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC7E,MAAM,YAAY,GAAG,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/D,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;QACnE,IAAI,WAAW,KAAK,SAAS;YAAE,OAAO,KAAK,CAAC;QAC5C,MAAM,KAAK,GAAG,WAIe,CAAC;QAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,6BAA6B,CAAC,KAAK,CAAC,IAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QACzF,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IACI,IAAA,yBAAU,EAAC,SAAS,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,sBAAsB,CAAC;YAC7D,IAAA,yBAAU,EAAC,SAAS,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,sBAAsB,CAAC;YAE7D,OAAO,KAAK,CAAC;QACjB,OAAO,IAAI,CAAC,IAAI,KAAK,uBAAuB,IAAI,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzF,CAAC;IAEO,aAAa,CACjB,MAAqB,EACrB,IAAY,EACZ,IAAY;QAEZ,IAAI,KAA0C,CAAC;QAC/C,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;YAClC,IACI,CAAC,EAAE,CAAC,kBAAkB,CAAC,IAAI,CAAC;gBACxB,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC;gBAC/B,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC;gBAC1B,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC;gBACpC,IAAI,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI;gBACxB,MAAM,CAAC,6BAA6B,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,KAAK,IAAI;gBAE7E,KAAK,GAAG,IAAI,CAAC;YACjB,IAAI,KAAK,KAAK,SAAS;gBAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC1D,CAAC,CAAC;QACF,KAAK,CAAC,MAAM,CAAC,CAAC;QACd,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,WAAW,CACf,OAAe,EACf,WAAmB,EACnB,YAA+B;QAE/B,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QACvC,IAAI,IAAI,CAAC,IAAI,KAAK,mBAAmB;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxE,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE;YAC3C,MAAM,KAAK,GAAG,YAAY;iBACrB,MAAM,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;iBACtE,IAAI,CAAC,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5D,OAAO,KAAK,KAAK,WAAW,CAAC;QACjC,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,YAAY,CAAC,OAAe;QAChC,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACtB,KAAK;gBACD,IAAI,CAAC,IAAI,KAAK,SAAS;oBACnB,CAAC,CAAC,IAAI,GAAG,EAAU;oBACnB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAC3B,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAClE,CAAC;YACZ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ;AA/GD,0CA+GC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport * as ts from 'typescript';\nimport { DiffScope, hasDisable, isPathExcluded } from '@webpieces/rules-config';\nimport { ModifiedCodeMode } from '@webpieces/rules-sdk';\n\n/** Suffix enforcement keeps its own rollout scope, independently of the enclosing doc rule. */\nexport class WireSuffixScope {\n private readonly changed: Set<string> | null;\n private readonly lines = new Map<string, Set<number>>();\n private readonly base: string | undefined;\n private readonly head: string | undefined;\n\n constructor(\n private readonly root: string,\n private readonly mode: ModifiedCodeMode,\n private readonly allowedPaths: readonly string[],\n private readonly diff: DiffScope,\n ) {\n const range = mode === 'RUN_EVERY_TIME' ? undefined : diff.resolveBase(root);\n this.base = range?.base;\n this.head = range?.head;\n this.changed =\n mode === 'RUN_EVERY_TIME'\n ? null\n : new Set(\n this.base === undefined\n ? []\n : diff.getChangedFiles(root, this.base, this.head, { tsOnly: false }),\n );\n }\n\n includes(\n name: string,\n relFile: string,\n projectRoot: string,\n location: string,\n projectRoots: readonly string[],\n ): boolean {\n if (this.mode === 'OFF' || isPathExcluded(relFile, this.allowedPaths)) return false;\n if (\n relFile.endsWith('.d.ts') ||\n /\\.(spec|test)\\.ts$/.test(relFile) ||\n relFile.includes('__tests__/')\n )\n return false;\n if (!this.fileInScope(relFile, projectRoot, projectRoots)) return false;\n const file = path.resolve(this.root, relFile);\n const text = fs.readFileSync(file, 'utf8');\n const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);\n const declaredLine = Number(/:(\\d+):\\d+$/.exec(location)?.[1]);\n const declaration = this.declarationAt(source, name, declaredLine);\n if (declaration === undefined) return false;\n const named = declaration as\n | ts.ClassDeclaration\n | ts.InterfaceDeclaration\n | ts.EnumDeclaration\n | ts.TypeAliasDeclaration;\n const line = source.getLineAndCharacterOfPosition(named.name!.getStart(source)).line + 1;\n const textLines = text.split('\\n');\n if (\n hasDisable(textLines[line - 1] ?? '', 'required-type-suffix') ||\n hasDisable(textLines[line - 2] ?? '', 'required-type-suffix')\n )\n return false;\n return this.mode !== 'NEW_AND_MODIFIED_CODE' || this.changedLines(relFile).has(line);\n }\n\n private declarationAt(\n source: ts.SourceFile,\n name: string,\n line: number,\n ): ts.DeclarationStatement | undefined {\n let found: ts.DeclarationStatement | undefined;\n const visit = (node: ts.Node): void => {\n if (\n (ts.isClassDeclaration(node) ||\n ts.isInterfaceDeclaration(node) ||\n ts.isEnumDeclaration(node) ||\n ts.isTypeAliasDeclaration(node)) &&\n node.name?.text === name &&\n source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1 === line\n )\n found = node;\n if (found === undefined) ts.forEachChild(node, visit);\n };\n visit(source);\n return found;\n }\n\n private fileInScope(\n relFile: string,\n projectRoot: string,\n projectRoots: readonly string[],\n ): boolean {\n if (this.changed === null) return true;\n if (this.mode !== 'MODIFIED_PROJECTS') return this.changed.has(relFile);\n return [...this.changed].some((file: string) => {\n const owner = projectRoots\n .filter((root: string) => file === root || file.startsWith(`${root}/`))\n .sort((a: string, b: string) => b.length - a.length)[0];\n return owner === projectRoot;\n });\n }\n\n private changedLines(relFile: string): ReadonlySet<number> {\n let lines = this.lines.get(relFile);\n if (lines === undefined) {\n lines =\n this.base === undefined\n ? new Set<number>()\n : this.diff.getChangedLineNumbers(\n this.diff.getFileDiff(this.root, relFile, this.base, this.head),\n );\n this.lines.set(relFile, lines);\n }\n return lines;\n }\n}\n"]}
|