@cplieger/deadset-ts 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * One run's passes, composed: the projects discovery names, opened in one snapshot
3
+ * of one client, each read through the stages in the order they depend on each
4
+ * other, and the per-project answers merged into the answer for the run.
5
+ *
6
+ * The stages take a project's view, which only this composition makes: a view
7
+ * carries a project's identity in its type so a handle cannot cross to another
8
+ * project's checker, and that holds only for views the session itself hands out.
9
+ */
10
+
11
+ import type { Diagnostic } from "@typescript/native/unstable/sync";
12
+ import type { Config, Provenance } from "./config.ts";
13
+ import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.ts";
14
+ import type { Finding } from "./finding.ts";
15
+ import type { Host } from "./host.ts";
16
+ import { inventory } from "./inventory.ts";
17
+ import { readManifest } from "./manifest.ts";
18
+ import { relativePath, resolvePath } from "./paths.ts";
19
+ import { byPosition, type Position } from "./position.ts";
20
+ import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind } from "./roots.ts";
21
+ import type { Scope } from "./scope.ts";
22
+ import { diagnosticsOf, runSession, type Engine } from "./session.ts";
23
+
24
+ /** The setting whose source decides where a finding about a configured root sits. */
25
+ const ROOTS_SETTING = "roots.patterns";
26
+
27
+ /** One root of the run, merged across the projects that hold it. */
28
+ export interface RunRoot {
29
+ /** The stable reference of the declaration the root names. */
30
+ readonly ref: string;
31
+ /** Where that declaration is written. */
32
+ readonly position: Position;
33
+ readonly kind: RootKind;
34
+ readonly source: string;
35
+ /** The configurations whose project holds this root, in the run's order. */
36
+ readonly configurations: readonly string[];
37
+ }
38
+
39
+ /** The root set of one run, and the findings about the roots the configuration names. */
40
+ export interface RunRoots {
41
+ /**
42
+ * The configurations the run analyzed: each compiler configuration file, by its
43
+ * path below the target root, in discovery order.
44
+ */
45
+ readonly configurations: readonly string[];
46
+ /**
47
+ * Every root of the run, ordered by the site of its declaration, then by kind, then
48
+ * by the string that named it. A root several projects hold is one entry naming each.
49
+ */
50
+ readonly roots: readonly RunRoot[];
51
+ /** One finding per configured root or pattern that named nothing in any project. */
52
+ readonly findings: readonly Finding[];
53
+ }
54
+
55
+ /**
56
+ * The document that supplied one setting, below the target root, or the empty
57
+ * string where the setting came from no document of the target. Both paths are
58
+ * resolved against the working directory before one is read below the other, so a
59
+ * relative target and a relative document name the same place.
60
+ */
61
+ function documentOf(
62
+ host: Host,
63
+ provenance: Provenance,
64
+ setting: string,
65
+ targetRoot: string,
66
+ ): string {
67
+ const origin = provenance.get(setting);
68
+ if (origin === undefined || origin.source === "default" || origin.source === "flag") {
69
+ return "";
70
+ }
71
+ const document = resolvePath(host.workingDirectory(), origin.label);
72
+ return relativePath(resolvePath(host.workingDirectory(), targetRoot), document) ?? "";
73
+ }
74
+
75
+ /** Two strings ordered bytewise. */
76
+ function compare(a: string, b: string): number {
77
+ if (a === b) {
78
+ return 0;
79
+ }
80
+ return a < b ? -1 : 1;
81
+ }
82
+
83
+ /**
84
+ * The root set of the run the scope and the configuration describe.
85
+ *
86
+ * Every project is read for errors before anything else, and a project carrying one
87
+ * fails the run with no root set, as every verb that reads declarations does. A
88
+ * configured string is a finding when it names nothing in every project, and the
89
+ * finding sits in the document that declared the roots.
90
+ */
91
+ export function runRoots(
92
+ engine: Engine,
93
+ host: Host,
94
+ scope: Scope,
95
+ config: Config,
96
+ provenance: Provenance,
97
+ ): RunRoots {
98
+ const targetRoot = scope.target.path;
99
+ const discovered = discoverProjects(engine, host, scope);
100
+ const manifest = readManifest(host, targetRoot);
101
+ const options = {
102
+ manifest,
103
+ patterns: config.rootPatterns,
104
+ entryFiles: config.ts.entryFiles,
105
+ testFiles: config.ts.testFiles,
106
+ publishedAPI: config.targetKind === "library",
107
+ };
108
+ const failures: Diagnostic[] = [];
109
+ const { projects } = runSession(engine, discovered.configFiles, (project) => {
110
+ const errors = diagnosticErrors(diagnosticsOf(project));
111
+ if (errors.length > 0) {
112
+ failures.push(...errors);
113
+ return undefined;
114
+ }
115
+ const held = inventory(project, host, targetRoot);
116
+ return { held, rooted: roots(project, held, targetRoot, options) };
117
+ });
118
+ if (failures.length > 0) {
119
+ throw new DiscoveryError(
120
+ `${String(failures.length)} error(s); no answer was produced`,
121
+ failures,
122
+ );
123
+ }
124
+
125
+ const configurations: string[] = [];
126
+ const merged = new Map<string, Omit<RunRoot, "configurations"> & { configurations: string[] }>();
127
+ for (const answer of projects) {
128
+ if (answer === undefined) {
129
+ continue;
130
+ }
131
+ const configuration =
132
+ relativePath(targetRoot, answer.rooted.configFile) ?? answer.rooted.configFile;
133
+ configurations.push(configuration);
134
+ const symbols = new Map(answer.held.symbols.map((symbol) => [symbol.id, symbol]));
135
+ for (const root of answer.rooted.liveUnderReachability) {
136
+ const symbol = symbols.get(root.id);
137
+ if (symbol === undefined) {
138
+ continue;
139
+ }
140
+ const key = [root.id, root.kind, root.source].join("\u0000");
141
+ const held = merged.get(key);
142
+ if (held === undefined) {
143
+ merged.set(key, {
144
+ ref: symbol.ref,
145
+ position: symbol.position,
146
+ kind: root.kind,
147
+ source: root.source,
148
+ configurations: [configuration],
149
+ });
150
+ } else if (!held.configurations.includes(configuration)) {
151
+ held.configurations.push(configuration);
152
+ }
153
+ }
154
+ }
155
+
156
+ const ordered = [...merged.values()].sort(
157
+ (a, b) =>
158
+ byPosition(a.position, b.position) || compare(a.kind, b.kind) || compare(a.source, b.source),
159
+ );
160
+ const unmatched = unmatchedEverywhere(
161
+ config.rootPatterns,
162
+ projects.flatMap((answer) => (answer === undefined ? [] : [answer.rooted])),
163
+ );
164
+ return {
165
+ configurations,
166
+ roots: ordered,
167
+ findings: unmatchedRoots(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
168
+ };
169
+ }
package/src/config.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { CONTRACT_VERSION } from "./version.ts";
2
2
 
3
+ /**
4
+ * The name of the repository configuration at the target root, which is also the
5
+ * document a finding about the configuration names where no document of the
6
+ * target supplied the setting it is about.
7
+ */
8
+ export const REPOSITORY_DOCUMENT = "deadset.json";
9
+
3
10
  /**
4
11
  * Whether the target is an application, whose every caller is in the analyzed
5
12
  * graph, or a library, whose published API has callers outside it.
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Every entry-point convention this analyzer declines, each with the tool it belongs
3
+ * to and why. A file only a declined convention enters is reported as never
4
+ * imported, so a project that relies on one names the file in `ts.entry_files` or
5
+ * `roots.patterns`, and this list is what tells it to.
6
+ *
7
+ * Every row carries a reason, because the list is read where the rows are shown
8
+ * one at a time: beside a finding, or in a report's declared gaps.
9
+ */
10
+
11
+ /** One declined entry-point convention. */
12
+ export interface DeclinedConvention {
13
+ /** The tool the convention belongs to, by the name of the package or service. */
14
+ readonly tool: string;
15
+ /** What is declined: one convention of a tool whose others are entered, or all of them. */
16
+ readonly convention: string;
17
+ readonly reason: string;
18
+ }
19
+
20
+ /** What is declined for a tool none of whose conventions is entered. */
21
+ const EVERY_CONVENTION = "its configuration and every file it loads by its own convention";
22
+
23
+ /** Why a tool none of whose conventions is entered declines them. */
24
+ const UNREAD =
25
+ "No rule reads it, so a file only it loads is reported as never imported unless " +
26
+ "ts.entry_files or roots.patterns names it.";
27
+
28
+ /** The tools none of whose conventions is entered. */
29
+ const UNREAD_TOOLS: readonly string[] = [
30
+ "angular",
31
+ "astro",
32
+ "astro-db",
33
+ "astro-markdoc",
34
+ "astro-og-canvas",
35
+ "ava",
36
+ "babel",
37
+ "biome",
38
+ "borp",
39
+ "bumpp",
40
+ "bun",
41
+ "c8",
42
+ "capacitor",
43
+ "catalyst",
44
+ "changelogen",
45
+ "changelogithub",
46
+ "changesets",
47
+ "commitizen",
48
+ "commitlint",
49
+ "convex",
50
+ "create-typescript-app",
51
+ "cspell",
52
+ "cucumber",
53
+ "cypress",
54
+ "danger",
55
+ "dependency-cruiser",
56
+ "docusaurus",
57
+ "dotenv",
58
+ "drizzle",
59
+ "electron-vite",
60
+ "eleventy",
61
+ "esbuild",
62
+ "eve",
63
+ "execa",
64
+ "expo",
65
+ "expressive-code",
66
+ "fast",
67
+ "fumadocs",
68
+ "gatsby",
69
+ "github-action",
70
+ "github-actions",
71
+ "glob",
72
+ "graphql-codegen",
73
+ "hardhat",
74
+ "husky",
75
+ "i18next-parser",
76
+ "jest",
77
+ "karma",
78
+ "knex",
79
+ "ladle",
80
+ "laravel-vite-plugin",
81
+ "lefthook",
82
+ "lint-staged",
83
+ "linthtml",
84
+ "lit",
85
+ "lockfile-lint",
86
+ "lost-pixel",
87
+ "lunaria",
88
+ "markdownlint",
89
+ "marko",
90
+ "mdx",
91
+ "mdxlint",
92
+ "metro",
93
+ "mise",
94
+ "mocha",
95
+ "moonrepo",
96
+ "msw",
97
+ "nano-spawn",
98
+ "nano-staged",
99
+ "nest",
100
+ "netlify",
101
+ "next",
102
+ "next-intl",
103
+ "next-mdx",
104
+ "nitro",
105
+ "node",
106
+ "node-modules-inspector",
107
+ "nodemon",
108
+ "npm-package-json-lint",
109
+ "nuxt",
110
+ "nuxtjs-i18n",
111
+ "nx",
112
+ "nyc",
113
+ "oclif",
114
+ "openapi-ts",
115
+ "openclaw",
116
+ "orval",
117
+ "oxfmt",
118
+ "oxlint",
119
+ "panda-css",
120
+ "parcel",
121
+ "payload",
122
+ "pino",
123
+ "playwright-ct",
124
+ "playwright-test",
125
+ "plop",
126
+ "pm2",
127
+ "pnpm",
128
+ "postcss",
129
+ "pre-commit",
130
+ "preconstruct",
131
+ "prettier",
132
+ "prisma",
133
+ "quasar",
134
+ "qwik",
135
+ "railway",
136
+ "raycast",
137
+ "react-cosmos",
138
+ "react-email",
139
+ "react-native",
140
+ "react-router",
141
+ "relay",
142
+ "release-it",
143
+ "remark",
144
+ "remix",
145
+ "rolldown",
146
+ "rollup",
147
+ "rsbuild",
148
+ "rslib",
149
+ "rspack",
150
+ "rstest",
151
+ "sanity",
152
+ "semantic-release",
153
+ "sentry",
154
+ "serverless-framework",
155
+ "simple-git-hooks",
156
+ "size-limit",
157
+ "sst",
158
+ "starlight",
159
+ "stencil",
160
+ "storybook",
161
+ "stylelint",
162
+ "svelte",
163
+ "sveltejs-package",
164
+ "sveltekit",
165
+ "svgo",
166
+ "svgr",
167
+ "swc",
168
+ "syncpack",
169
+ "tailwind",
170
+ "tanstack-router",
171
+ "taskfile",
172
+ "tauri",
173
+ "temporal",
174
+ "textlint",
175
+ "travis",
176
+ "ts-node",
177
+ "tsd",
178
+ "tsdown",
179
+ "tsup",
180
+ "tsx",
181
+ "turbo",
182
+ "typedoc",
183
+ "typescript",
184
+ "typescript-content-mapper",
185
+ "unbuild",
186
+ "unocss",
187
+ "unplugin-auto-import",
188
+ "unplugin-icons",
189
+ "unplugin-vue-components",
190
+ "unplugin-vue-i18n",
191
+ "unplugin-vue-markdown",
192
+ "unplugin-vue-router",
193
+ "varlock",
194
+ "vercel",
195
+ "vercel-og",
196
+ "vike",
197
+ "vite-plugin-pages",
198
+ "vite-plugin-pwa",
199
+ "vite-plugin-vue-layouts-next",
200
+ "vite-plus",
201
+ "vite-pwa-assets-generator",
202
+ "vitepress",
203
+ "vue",
204
+ "webdriver-io",
205
+ "wireit",
206
+ "wrangler",
207
+ "wxt",
208
+ "xo",
209
+ "yarn",
210
+ "yorkie",
211
+ "zx",
212
+ ];
213
+
214
+ /** The conventions declined for a tool some of whose conventions are entered. */
215
+ const PARTLY_ENTERED: readonly DeclinedConvention[] = [
216
+ {
217
+ tool: "eslint",
218
+ convention: "the legacy .eslintrc configuration files",
219
+ reason:
220
+ "Only the flat configuration's file names are entered, so a legacy configuration " +
221
+ "file the program holds is reported as never imported.",
222
+ },
223
+ {
224
+ tool: "playwright",
225
+ convention: "a testMatch written as a regular expression",
226
+ reason:
227
+ "A testMatch that is not a string literal leaves the test-file set unknown, and an " +
228
+ "unknown set enters nothing.",
229
+ },
230
+ {
231
+ tool: "playwright",
232
+ convention: "the globalSetup and globalTeardown files",
233
+ reason:
234
+ "No rule enters them, so a setup file only the runner loads is reported as never imported.",
235
+ },
236
+ {
237
+ tool: "playwright",
238
+ convention: "the testDir and testMatch of an entry in projects",
239
+ reason:
240
+ "Only the configuration object's own testDir and testMatch are read, so a test " +
241
+ "directory a project entry moves is not entered.",
242
+ },
243
+ {
244
+ tool: "stryker",
245
+ convention: "a configuration file named on the command line",
246
+ reason: "Only the file names the tool looks for by default are entered.",
247
+ },
248
+ {
249
+ tool: "vite",
250
+ convention: "its configuration and every entry point it names",
251
+ reason:
252
+ "Only a worker a call addresses by a literal is entered; no rule reads the " +
253
+ "configuration, so a file only it names is reported as never imported.",
254
+ },
255
+ {
256
+ tool: "vitest",
257
+ convention: "the benchmark and type-test files",
258
+ reason:
259
+ "A test file is one ts.test_files classifies, so a *.bench.* or *.test-d.* file is " +
260
+ "entered only where a configured pattern names it.",
261
+ },
262
+ {
263
+ tool: "vitest",
264
+ convention: "the mock files below a __mocks__ directory",
265
+ reason:
266
+ "No rule enters a file by its directory, so a mock only the runner's module mocking " +
267
+ "loads is reported as never imported.",
268
+ },
269
+ {
270
+ tool: "vitest",
271
+ convention: "the setup files of a configuration that moves its root",
272
+ reason:
273
+ "A configuration writing root reads its setup files against a directory its text " +
274
+ "may not state, so none of them is entered.",
275
+ },
276
+ {
277
+ tool: "vitest",
278
+ convention: "the test files a configuration's include member names",
279
+ reason:
280
+ "A test file is one ts.test_files classifies rather than one the runner's include " +
281
+ "reads, so the two are kept equal by configuration.",
282
+ },
283
+ {
284
+ tool: "webpack",
285
+ convention: "its configuration and every entry point it names",
286
+ reason:
287
+ "Only a worker a call addresses by a literal is entered; no rule reads the " +
288
+ "configuration, so a file only it names is reported as never imported.",
289
+ },
290
+ ];
291
+
292
+ /** Two strings ordered bytewise. */
293
+ function compare(a: string, b: string): number {
294
+ if (a === b) {
295
+ return 0;
296
+ }
297
+ return a < b ? -1 : 1;
298
+ }
299
+
300
+ /** Every declined entry-point convention, ordered by tool and then by convention. */
301
+ export const DECLINED_CONVENTIONS: readonly DeclinedConvention[] = [
302
+ ...UNREAD_TOOLS.map((tool) => ({ tool, convention: EVERY_CONVENTION, reason: UNREAD })),
303
+ ...PARTLY_ENTERED,
304
+ ].sort((a, b) => compare(a.tool, b.tool) || compare(a.convention, b.convention));