@cplieger/deadset-ts 2.2.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -16
- package/dist/src/analysis.js +142 -0
- package/dist/src/components.js +289 -0
- package/dist/src/config.js +6 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/entry-point-gaps.js +278 -0
- package/dist/src/entry-points.js +385 -0
- package/dist/src/finding.js +12 -0
- package/dist/src/glob.js +65 -0
- package/dist/src/graph.js +116 -0
- package/dist/src/index.js +1 -2
- package/dist/src/manifest.js +120 -0
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +156 -106
- package/dist/src/resolve.js +82 -20
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +59 -10
- package/dist/src/schema.js +1 -1
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/dist/src/sweep.js +156 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +236 -0
- package/src/components.ts +372 -0
- package/src/config.ts +30 -2
- package/src/discover.ts +144 -29
- package/src/entry-point-gaps.ts +304 -0
- package/src/entry-points.ts +496 -0
- package/src/finding.ts +43 -0
- package/src/glob.ts +67 -0
- package/src/graph.ts +178 -0
- package/src/index.ts +1 -20
- package/src/manifest.ts +174 -0
- package/src/print.ts +5 -6
- package/src/references.ts +201 -111
- package/src/resolve.ts +117 -24
- package/src/roots.ts +467 -0
- package/src/run.ts +66 -11
- package/src/schema.ts +5 -2
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
- package/src/sweep.ts +230 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,278 @@
|
|
|
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
|
+
/** What is declined for a tool none of whose conventions is entered. */
|
|
11
|
+
const EVERY_CONVENTION = "its configuration and every file it loads by its own convention";
|
|
12
|
+
/** Why a tool none of whose conventions is entered declines them. */
|
|
13
|
+
const UNREAD = "No rule reads it, so a file only it loads is reported as never imported unless " +
|
|
14
|
+
"ts.entry_files or roots.patterns names it.";
|
|
15
|
+
/** The tools none of whose conventions is entered. */
|
|
16
|
+
const UNREAD_TOOLS = [
|
|
17
|
+
"angular",
|
|
18
|
+
"astro",
|
|
19
|
+
"astro-db",
|
|
20
|
+
"astro-markdoc",
|
|
21
|
+
"astro-og-canvas",
|
|
22
|
+
"ava",
|
|
23
|
+
"babel",
|
|
24
|
+
"biome",
|
|
25
|
+
"borp",
|
|
26
|
+
"bumpp",
|
|
27
|
+
"bun",
|
|
28
|
+
"c8",
|
|
29
|
+
"capacitor",
|
|
30
|
+
"catalyst",
|
|
31
|
+
"changelogen",
|
|
32
|
+
"changelogithub",
|
|
33
|
+
"changesets",
|
|
34
|
+
"commitizen",
|
|
35
|
+
"commitlint",
|
|
36
|
+
"convex",
|
|
37
|
+
"create-typescript-app",
|
|
38
|
+
"cspell",
|
|
39
|
+
"cucumber",
|
|
40
|
+
"cypress",
|
|
41
|
+
"danger",
|
|
42
|
+
"dependency-cruiser",
|
|
43
|
+
"docusaurus",
|
|
44
|
+
"dotenv",
|
|
45
|
+
"drizzle",
|
|
46
|
+
"electron-vite",
|
|
47
|
+
"eleventy",
|
|
48
|
+
"esbuild",
|
|
49
|
+
"eve",
|
|
50
|
+
"execa",
|
|
51
|
+
"expo",
|
|
52
|
+
"expressive-code",
|
|
53
|
+
"fast",
|
|
54
|
+
"fumadocs",
|
|
55
|
+
"gatsby",
|
|
56
|
+
"github-action",
|
|
57
|
+
"github-actions",
|
|
58
|
+
"glob",
|
|
59
|
+
"graphql-codegen",
|
|
60
|
+
"hardhat",
|
|
61
|
+
"husky",
|
|
62
|
+
"i18next-parser",
|
|
63
|
+
"jest",
|
|
64
|
+
"karma",
|
|
65
|
+
"knex",
|
|
66
|
+
"ladle",
|
|
67
|
+
"laravel-vite-plugin",
|
|
68
|
+
"lefthook",
|
|
69
|
+
"lint-staged",
|
|
70
|
+
"linthtml",
|
|
71
|
+
"lit",
|
|
72
|
+
"lockfile-lint",
|
|
73
|
+
"lost-pixel",
|
|
74
|
+
"lunaria",
|
|
75
|
+
"markdownlint",
|
|
76
|
+
"marko",
|
|
77
|
+
"mdx",
|
|
78
|
+
"mdxlint",
|
|
79
|
+
"metro",
|
|
80
|
+
"mise",
|
|
81
|
+
"mocha",
|
|
82
|
+
"moonrepo",
|
|
83
|
+
"msw",
|
|
84
|
+
"nano-spawn",
|
|
85
|
+
"nano-staged",
|
|
86
|
+
"nest",
|
|
87
|
+
"netlify",
|
|
88
|
+
"next",
|
|
89
|
+
"next-intl",
|
|
90
|
+
"next-mdx",
|
|
91
|
+
"nitro",
|
|
92
|
+
"node",
|
|
93
|
+
"node-modules-inspector",
|
|
94
|
+
"nodemon",
|
|
95
|
+
"npm-package-json-lint",
|
|
96
|
+
"nuxt",
|
|
97
|
+
"nuxtjs-i18n",
|
|
98
|
+
"nx",
|
|
99
|
+
"nyc",
|
|
100
|
+
"oclif",
|
|
101
|
+
"openapi-ts",
|
|
102
|
+
"openclaw",
|
|
103
|
+
"orval",
|
|
104
|
+
"oxfmt",
|
|
105
|
+
"oxlint",
|
|
106
|
+
"panda-css",
|
|
107
|
+
"parcel",
|
|
108
|
+
"payload",
|
|
109
|
+
"pino",
|
|
110
|
+
"playwright-ct",
|
|
111
|
+
"playwright-test",
|
|
112
|
+
"plop",
|
|
113
|
+
"pm2",
|
|
114
|
+
"pnpm",
|
|
115
|
+
"postcss",
|
|
116
|
+
"pre-commit",
|
|
117
|
+
"preconstruct",
|
|
118
|
+
"prettier",
|
|
119
|
+
"prisma",
|
|
120
|
+
"quasar",
|
|
121
|
+
"qwik",
|
|
122
|
+
"railway",
|
|
123
|
+
"raycast",
|
|
124
|
+
"react-cosmos",
|
|
125
|
+
"react-email",
|
|
126
|
+
"react-native",
|
|
127
|
+
"react-router",
|
|
128
|
+
"relay",
|
|
129
|
+
"release-it",
|
|
130
|
+
"remark",
|
|
131
|
+
"remix",
|
|
132
|
+
"rolldown",
|
|
133
|
+
"rollup",
|
|
134
|
+
"rsbuild",
|
|
135
|
+
"rslib",
|
|
136
|
+
"rspack",
|
|
137
|
+
"rstest",
|
|
138
|
+
"sanity",
|
|
139
|
+
"semantic-release",
|
|
140
|
+
"sentry",
|
|
141
|
+
"serverless-framework",
|
|
142
|
+
"simple-git-hooks",
|
|
143
|
+
"size-limit",
|
|
144
|
+
"sst",
|
|
145
|
+
"starlight",
|
|
146
|
+
"stencil",
|
|
147
|
+
"storybook",
|
|
148
|
+
"stylelint",
|
|
149
|
+
"svelte",
|
|
150
|
+
"sveltejs-package",
|
|
151
|
+
"sveltekit",
|
|
152
|
+
"svgo",
|
|
153
|
+
"svgr",
|
|
154
|
+
"swc",
|
|
155
|
+
"syncpack",
|
|
156
|
+
"tailwind",
|
|
157
|
+
"tanstack-router",
|
|
158
|
+
"taskfile",
|
|
159
|
+
"tauri",
|
|
160
|
+
"temporal",
|
|
161
|
+
"textlint",
|
|
162
|
+
"travis",
|
|
163
|
+
"ts-node",
|
|
164
|
+
"tsd",
|
|
165
|
+
"tsdown",
|
|
166
|
+
"tsup",
|
|
167
|
+
"tsx",
|
|
168
|
+
"turbo",
|
|
169
|
+
"typedoc",
|
|
170
|
+
"typescript",
|
|
171
|
+
"typescript-content-mapper",
|
|
172
|
+
"unbuild",
|
|
173
|
+
"unocss",
|
|
174
|
+
"unplugin-auto-import",
|
|
175
|
+
"unplugin-icons",
|
|
176
|
+
"unplugin-vue-components",
|
|
177
|
+
"unplugin-vue-i18n",
|
|
178
|
+
"unplugin-vue-markdown",
|
|
179
|
+
"unplugin-vue-router",
|
|
180
|
+
"varlock",
|
|
181
|
+
"vercel",
|
|
182
|
+
"vercel-og",
|
|
183
|
+
"vike",
|
|
184
|
+
"vite-plugin-pages",
|
|
185
|
+
"vite-plugin-pwa",
|
|
186
|
+
"vite-plugin-vue-layouts-next",
|
|
187
|
+
"vite-plus",
|
|
188
|
+
"vite-pwa-assets-generator",
|
|
189
|
+
"vitepress",
|
|
190
|
+
"vue",
|
|
191
|
+
"webdriver-io",
|
|
192
|
+
"wireit",
|
|
193
|
+
"wrangler",
|
|
194
|
+
"wxt",
|
|
195
|
+
"xo",
|
|
196
|
+
"yarn",
|
|
197
|
+
"yorkie",
|
|
198
|
+
"zx",
|
|
199
|
+
];
|
|
200
|
+
/** The conventions declined for a tool some of whose conventions are entered. */
|
|
201
|
+
const PARTLY_ENTERED = [
|
|
202
|
+
{
|
|
203
|
+
tool: "eslint",
|
|
204
|
+
convention: "the legacy .eslintrc configuration files",
|
|
205
|
+
reason: "Only the flat configuration's file names are entered, so a legacy configuration " +
|
|
206
|
+
"file the program holds is reported as never imported.",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
tool: "playwright",
|
|
210
|
+
convention: "a testMatch written as a regular expression",
|
|
211
|
+
reason: "A testMatch that is not a string literal leaves the test-file set unknown, and an " +
|
|
212
|
+
"unknown set enters nothing.",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
tool: "playwright",
|
|
216
|
+
convention: "the globalSetup and globalTeardown files",
|
|
217
|
+
reason: "No rule enters them, so a setup file only the runner loads is reported as never imported.",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
tool: "playwright",
|
|
221
|
+
convention: "the testDir and testMatch of an entry in projects",
|
|
222
|
+
reason: "Only the configuration object's own testDir and testMatch are read, so a test " +
|
|
223
|
+
"directory a project entry moves is not entered.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
tool: "stryker",
|
|
227
|
+
convention: "a configuration file named on the command line",
|
|
228
|
+
reason: "Only the file names the tool looks for by default are entered.",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
tool: "vite",
|
|
232
|
+
convention: "its configuration and every entry point it names",
|
|
233
|
+
reason: "Only a worker a call addresses by a literal is entered; no rule reads the " +
|
|
234
|
+
"configuration, so a file only it names is reported as never imported.",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
tool: "vitest",
|
|
238
|
+
convention: "the benchmark and type-test files",
|
|
239
|
+
reason: "A test file is one ts.test_files classifies, so a *.bench.* or *.test-d.* file is " +
|
|
240
|
+
"entered only where a configured pattern names it.",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
tool: "vitest",
|
|
244
|
+
convention: "the mock files below a __mocks__ directory",
|
|
245
|
+
reason: "No rule enters a file by its directory, so a mock only the runner's module mocking " +
|
|
246
|
+
"loads is reported as never imported.",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
tool: "vitest",
|
|
250
|
+
convention: "the setup files of a configuration that moves its root",
|
|
251
|
+
reason: "A configuration writing root reads its setup files against a directory its text " +
|
|
252
|
+
"may not state, so none of them is entered.",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
tool: "vitest",
|
|
256
|
+
convention: "the test files a configuration's include member names",
|
|
257
|
+
reason: "A test file is one ts.test_files classifies rather than one the runner's include " +
|
|
258
|
+
"reads, so the two are kept equal by configuration.",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
tool: "webpack",
|
|
262
|
+
convention: "its configuration and every entry point it names",
|
|
263
|
+
reason: "Only a worker a call addresses by a literal is entered; no rule reads the " +
|
|
264
|
+
"configuration, so a file only it names is reported as never imported.",
|
|
265
|
+
},
|
|
266
|
+
];
|
|
267
|
+
/** Two strings ordered bytewise. */
|
|
268
|
+
function compare(a, b) {
|
|
269
|
+
if (a === b) {
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
return a < b ? -1 : 1;
|
|
273
|
+
}
|
|
274
|
+
/** Every declined entry-point convention, ordered by tool and then by convention. */
|
|
275
|
+
export const DECLINED_CONVENTIONS = [
|
|
276
|
+
...UNREAD_TOOLS.map((tool) => ({ tool, convention: EVERY_CONVENTION, reason: UNREAD })),
|
|
277
|
+
...PARTLY_ENTERED,
|
|
278
|
+
].sort((a, b) => compare(a.tool, b.tool) || compare(a.convention, b.convention));
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files a tool or the platform enters without any import naming them: the
|
|
3
|
+
* configuration files the test runner, the linter, the mutation tester and the
|
|
4
|
+
* browser-test runner load by their file name, the test files the runner executes,
|
|
5
|
+
* and a worker a call addresses by a string literal.
|
|
6
|
+
*
|
|
7
|
+
* Each rule is read from the program and nothing else: a file name the tool
|
|
8
|
+
* itself looks for, a literal written in a configuration file, or a literal
|
|
9
|
+
* argument of a call the checker resolves to the platform's own declaration. A
|
|
10
|
+
* value that is not a literal is not evaluated and not guessed, so a file it would
|
|
11
|
+
* have named is entered by nothing here.
|
|
12
|
+
*/
|
|
13
|
+
import { isArrayLiteralExpression, isAsExpression, isCallExpression, isExportAssignment, isIdentifier, isMetaProperty, isNewExpression, isNoSubstitutionTemplateLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPropertyAccessExpression, isPropertyAssignment, isSatisfiesExpression, isStringLiteral, } from "@typescript/native/unstable/ast";
|
|
14
|
+
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
15
|
+
import { globExpression } from "./glob.js";
|
|
16
|
+
import { dirnamePath, relativePath, resolvePath } from "./paths.js";
|
|
17
|
+
/** The extensions the test runner and the browser-test runner load a configuration from. */
|
|
18
|
+
const MODULE_EXTENSIONS = ["js", "mjs", "cjs", "ts", "mts", "cts"];
|
|
19
|
+
/** The extensions the linter's flat configuration and the mutation tester's are loaded from. */
|
|
20
|
+
const SCRIPT_EXTENSIONS = ["js", "mjs", "cjs"];
|
|
21
|
+
const CONVENTIONS = [
|
|
22
|
+
{
|
|
23
|
+
rule: "test-runner",
|
|
24
|
+
stems: ["vitest.config", "vitest.workspace", "vitest.projects"],
|
|
25
|
+
extensions: MODULE_EXTENSIONS,
|
|
26
|
+
testRunner: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
rule: "lint-configuration",
|
|
30
|
+
stems: ["eslint.config"],
|
|
31
|
+
extensions: MODULE_EXTENSIONS,
|
|
32
|
+
testRunner: false,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
rule: "mutation-testing",
|
|
36
|
+
stems: ["stryker.config", "stryker.conf", ".stryker.config", ".stryker.conf"],
|
|
37
|
+
extensions: SCRIPT_EXTENSIONS,
|
|
38
|
+
testRunner: false,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
rule: "mutation-testing",
|
|
42
|
+
stems: ["vitest.stryker.config"],
|
|
43
|
+
extensions: MODULE_EXTENSIONS,
|
|
44
|
+
testRunner: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
rule: "browser-tests",
|
|
48
|
+
stems: ["playwright.config"],
|
|
49
|
+
extensions: MODULE_EXTENSIONS,
|
|
50
|
+
testRunner: false,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
/** The browser-test runner's own test-file pattern, which holds where a configuration names none. */
|
|
54
|
+
const BROWSER_TEST_MATCH = "**/*.{spec,test}.{js,ts,jsx,tsx,cjs,cts,cjsx,ctsx,mjs,mts,mjsx,mtsx}";
|
|
55
|
+
/** The members of a test-runner configuration's `test` object that name setup files. */
|
|
56
|
+
const SETUP_MEMBERS = ["setupFiles", "globalSetup"];
|
|
57
|
+
/** A member that moves the directory every path of a test-runner configuration is read from. */
|
|
58
|
+
const ROOT_MEMBER = "root";
|
|
59
|
+
/** The prefixes a literal is relative with, in a module specifier and a URL reference alike. */
|
|
60
|
+
const RELATIVE_PREFIXES = ["./", "../"];
|
|
61
|
+
/** The constructors whose first argument addresses a worker script. */
|
|
62
|
+
const WORKER_CONSTRUCTORS = new Set(["Worker", "SharedWorker"]);
|
|
63
|
+
/** The file's name, the last segment of its path. */
|
|
64
|
+
function basename(path) {
|
|
65
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
66
|
+
}
|
|
67
|
+
/** The convention one file name is, with the spelling a root reports it by. */
|
|
68
|
+
function conventionOf(name) {
|
|
69
|
+
for (const convention of CONVENTIONS) {
|
|
70
|
+
for (const stem of convention.stems) {
|
|
71
|
+
if (convention.extensions.some((ext) => name === `${stem}.${ext}`)) {
|
|
72
|
+
return { convention, spelled: `${stem}.*` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/** The text a string literal or a template with no substitution denotes. */
|
|
79
|
+
function literalOf(node) {
|
|
80
|
+
if (node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node))) {
|
|
81
|
+
return node.text;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/** The expression a wrapper that changes no value holds: parentheses and type assertions. */
|
|
86
|
+
function unwrapped(node) {
|
|
87
|
+
let at = node;
|
|
88
|
+
while (isParenthesizedExpression(at) || isAsExpression(at) || isSatisfiesExpression(at)) {
|
|
89
|
+
at = at.expression;
|
|
90
|
+
}
|
|
91
|
+
return at;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The object literals one configuration file's default export is written as: the
|
|
95
|
+
* literal itself, or each literal argument of the call it is, at any depth of calls.
|
|
96
|
+
* A default export written as anything else, a name or a computation, yields none,
|
|
97
|
+
* because its value is not in the file's text.
|
|
98
|
+
*/
|
|
99
|
+
function configObjects(file) {
|
|
100
|
+
const found = [];
|
|
101
|
+
const collect = (node) => {
|
|
102
|
+
const at = unwrapped(node);
|
|
103
|
+
if (isObjectLiteralExpression(at)) {
|
|
104
|
+
found.push(at);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (isCallExpression(at)) {
|
|
108
|
+
for (const argument of at.arguments) {
|
|
109
|
+
collect(argument);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
for (const statement of file.statements) {
|
|
114
|
+
if (isExportAssignment(statement) && !statement.isExportEquals) {
|
|
115
|
+
collect(statement.expression);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The value one member of an object literal is written with, when it is written as
|
|
122
|
+
* `name: value`, and undefined where the literal writes no such member.
|
|
123
|
+
*/
|
|
124
|
+
function memberOf(object, name) {
|
|
125
|
+
if (!isObjectLiteralExpression(object)) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
for (const property of object.properties) {
|
|
129
|
+
if (isPropertyAssignment(property) &&
|
|
130
|
+
isIdentifier(property.name) &&
|
|
131
|
+
property.name.text === name) {
|
|
132
|
+
return property.initializer;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The strings one member holds, when it is written as a literal or an array of
|
|
139
|
+
* literals. `undefined` is a value the text does not state.
|
|
140
|
+
*/
|
|
141
|
+
function literalsOf(value) {
|
|
142
|
+
const at = unwrapped(value);
|
|
143
|
+
const single = literalOf(at);
|
|
144
|
+
if (single !== undefined) {
|
|
145
|
+
return [single];
|
|
146
|
+
}
|
|
147
|
+
if (!isArrayLiteralExpression(at)) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const found = [];
|
|
151
|
+
for (const element of at.elements) {
|
|
152
|
+
const text = literalOf(unwrapped(element));
|
|
153
|
+
if (text === undefined) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
found.push(text);
|
|
157
|
+
}
|
|
158
|
+
return found;
|
|
159
|
+
}
|
|
160
|
+
/** Whether a literal is relative in the sense a module specifier and a URL reference share. */
|
|
161
|
+
function isRelative(text) {
|
|
162
|
+
return RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
163
|
+
}
|
|
164
|
+
/** A URL reference with its query and its fragment removed, which name no part of the file. */
|
|
165
|
+
function pathPart(text) {
|
|
166
|
+
const cut = text.search(/[?#]/u);
|
|
167
|
+
return cut < 0 ? text : text.slice(0, cut);
|
|
168
|
+
}
|
|
169
|
+
/** The own files one relative literal names, read against a directory. */
|
|
170
|
+
function filesNamed(files, dir, text) {
|
|
171
|
+
if (!isRelative(text)) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
return files.named(resolvePath(dir, pathPart(text)));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The setup files one test-runner configuration names: every literal under
|
|
178
|
+
* `test.setupFiles` and `test.globalSetup`, read against the configuration's own
|
|
179
|
+
* directory. A configuration that moves its root names them against a directory its
|
|
180
|
+
* text may not state, so none is read from it.
|
|
181
|
+
*/
|
|
182
|
+
function setupFilesOf(file, files) {
|
|
183
|
+
const objects = configObjects(file);
|
|
184
|
+
const tests = objects
|
|
185
|
+
.map((object) => memberOf(object, "test"))
|
|
186
|
+
.filter((value) => value !== undefined)
|
|
187
|
+
.map(unwrapped);
|
|
188
|
+
if ([...objects, ...tests].some((object) => memberOf(object, ROOT_MEMBER) !== undefined)) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const dir = dirnamePath(file.fileName);
|
|
192
|
+
const found = [];
|
|
193
|
+
for (const test of tests) {
|
|
194
|
+
for (const name of SETUP_MEMBERS) {
|
|
195
|
+
const value = memberOf(test, name);
|
|
196
|
+
for (const text of value === undefined ? [] : (literalsOf(value) ?? [])) {
|
|
197
|
+
for (const named of filesNamed(files, dir, text)) {
|
|
198
|
+
found.push({ file: named, rule: "test-runner", source: text, exports: false });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return found;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The test files one browser-test configuration names: every own file below its
|
|
207
|
+
* test directory that its test-file pattern matches. The directory is the literal
|
|
208
|
+
* `testDir` reads against the configuration's directory, or that directory where no
|
|
209
|
+
* object of the configuration writes one; the pattern is the literal `testMatch`, or
|
|
210
|
+
* the runner's own where none is written. A member whose value is not a literal
|
|
211
|
+
* leaves the set unknown, and an unknown set enters nothing.
|
|
212
|
+
*/
|
|
213
|
+
function browserTestsOf(file, files) {
|
|
214
|
+
const objects = configObjects(file);
|
|
215
|
+
if (objects.length === 0) {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
let testDir = ".";
|
|
219
|
+
let patterns = [BROWSER_TEST_MATCH];
|
|
220
|
+
for (const object of objects) {
|
|
221
|
+
const dir = memberOf(object, "testDir");
|
|
222
|
+
if (dir !== undefined) {
|
|
223
|
+
testDir = literalOf(unwrapped(dir));
|
|
224
|
+
}
|
|
225
|
+
const match = memberOf(object, "testMatch");
|
|
226
|
+
if (match !== undefined) {
|
|
227
|
+
patterns = literalsOf(match);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (testDir === undefined || patterns === undefined) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
const under = resolvePath(dirnamePath(file.fileName), testDir);
|
|
234
|
+
const found = [];
|
|
235
|
+
for (const pattern of patterns) {
|
|
236
|
+
const expression = globExpression(pattern);
|
|
237
|
+
for (const candidate of files.byName.values()) {
|
|
238
|
+
const below = relativePath(under, candidate.fileName);
|
|
239
|
+
if (below !== undefined && expression.test(below)) {
|
|
240
|
+
found.push({ file: candidate, rule: "browser-tests", source: pattern, exports: false });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return found;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* The address one worker argument writes: a relative literal, or a `URL` built from
|
|
248
|
+
* a literal against `import.meta.url`. Any other argument is computed and addresses
|
|
249
|
+
* nothing the text states.
|
|
250
|
+
*/
|
|
251
|
+
function workerAddress(argument) {
|
|
252
|
+
if (argument === undefined) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
const at = unwrapped(argument);
|
|
256
|
+
const literal = literalOf(at);
|
|
257
|
+
if (literal !== undefined) {
|
|
258
|
+
return { literal, names: [] };
|
|
259
|
+
}
|
|
260
|
+
if (!isNewExpression(at) || !isIdentifier(at.expression) || at.expression.text !== "URL") {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
const [reference, base] = at.arguments ?? [];
|
|
264
|
+
const text = literalOf(reference === undefined ? undefined : unwrapped(reference));
|
|
265
|
+
if (text === undefined || base === undefined) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
const meta = unwrapped(base);
|
|
269
|
+
if (!isPropertyAccessExpression(meta) ||
|
|
270
|
+
!isMetaProperty(meta.expression) ||
|
|
271
|
+
!isIdentifier(meta.name) ||
|
|
272
|
+
meta.name.text !== "url") {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return { literal: text, names: [at.expression] };
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Every call one file writes that addresses a worker: `new Worker(...)`,
|
|
279
|
+
* `new SharedWorker(...)` and `navigator.serviceWorker.register(...)`, each with an
|
|
280
|
+
* address its text states.
|
|
281
|
+
*/
|
|
282
|
+
function workerSitesOf(file) {
|
|
283
|
+
const found = [];
|
|
284
|
+
const visit = (node) => {
|
|
285
|
+
if (isNewExpression(node) &&
|
|
286
|
+
isIdentifier(node.expression) &&
|
|
287
|
+
WORKER_CONSTRUCTORS.has(node.expression.text)) {
|
|
288
|
+
const address = workerAddress(node.arguments?.[0]);
|
|
289
|
+
if (address !== undefined) {
|
|
290
|
+
found.push({
|
|
291
|
+
literal: address.literal,
|
|
292
|
+
platformNames: [node.expression, ...address.names],
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (isCallExpression(node)) {
|
|
297
|
+
const callee = node.expression;
|
|
298
|
+
if (isPropertyAccessExpression(callee) &&
|
|
299
|
+
isIdentifier(callee.name) &&
|
|
300
|
+
callee.name.text === "register" &&
|
|
301
|
+
isPropertyAccessExpression(callee.expression) &&
|
|
302
|
+
isIdentifier(callee.expression.name) &&
|
|
303
|
+
callee.expression.name.text === "serviceWorker" &&
|
|
304
|
+
isIdentifier(callee.expression.expression) &&
|
|
305
|
+
callee.expression.expression.text === "navigator") {
|
|
306
|
+
const address = workerAddress(node.arguments[0]);
|
|
307
|
+
if (address !== undefined) {
|
|
308
|
+
found.push({
|
|
309
|
+
literal: address.literal,
|
|
310
|
+
platformNames: [callee.expression.expression, ...address.names],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
node.forEachChild(visit);
|
|
316
|
+
};
|
|
317
|
+
file.forEachChild(visit);
|
|
318
|
+
return found;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The workers one file addresses. A call's names must resolve to the platform's own
|
|
322
|
+
* declarations, which no file of the target declares and no import brings in; a
|
|
323
|
+
* name the target binds itself is the target's own constructor or object, which
|
|
324
|
+
* addresses no worker this rule knows. One batch per file that writes such a call.
|
|
325
|
+
*/
|
|
326
|
+
function workersOf(project, file, files) {
|
|
327
|
+
const sites = workerSitesOf(file);
|
|
328
|
+
if (sites.length === 0) {
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
const names = sites.flatMap((site) => site.platformNames);
|
|
332
|
+
const symbols = project.symbolsAt(names.map((name) => project.handle(name)));
|
|
333
|
+
const platform = new Set();
|
|
334
|
+
names.forEach((name, index) => {
|
|
335
|
+
const symbol = symbols[index];
|
|
336
|
+
if (symbol !== undefined &&
|
|
337
|
+
(symbol.flags & SymbolFlags.Alias) === 0 &&
|
|
338
|
+
symbol.declarations.length > 0 &&
|
|
339
|
+
symbol.declarations.every((handle) => !files.byName.has(handle.path))) {
|
|
340
|
+
platform.add(name);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
const dir = dirnamePath(file.fileName);
|
|
344
|
+
const found = [];
|
|
345
|
+
for (const site of sites) {
|
|
346
|
+
if (!site.platformNames.every((name) => platform.has(name))) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
for (const named of filesNamed(files, dir, site.literal)) {
|
|
350
|
+
found.push({ file: named, rule: "worker", source: site.literal, exports: false });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return found;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Every file of one project the lifted rules enter, in no particular order and
|
|
357
|
+
* possibly more than once: a file two rules enter appears once per rule.
|
|
358
|
+
*
|
|
359
|
+
* `testFiles` is the configuration's test-file patterns, matched against each
|
|
360
|
+
* file's path below the target root; a file one of them matches is a test file the
|
|
361
|
+
* runner executes.
|
|
362
|
+
*/
|
|
363
|
+
export function entryPoints(project, files, testFiles) {
|
|
364
|
+
const found = [];
|
|
365
|
+
const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
|
|
366
|
+
for (const [path, file] of files.byPath) {
|
|
367
|
+
const named = conventionOf(basename(path));
|
|
368
|
+
if (named !== undefined) {
|
|
369
|
+
found.push({ file, rule: named.convention.rule, source: named.spelled, exports: true });
|
|
370
|
+
if (named.convention.testRunner) {
|
|
371
|
+
found.push(...setupFilesOf(file, files));
|
|
372
|
+
}
|
|
373
|
+
if (named.convention.rule === "browser-tests") {
|
|
374
|
+
found.push(...browserTestsOf(file, files));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const { pattern, expression } of tests) {
|
|
378
|
+
if (expression.test(path)) {
|
|
379
|
+
found.push({ file, rule: "test-runner", source: pattern, exports: false });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
found.push(...workersOf(project, file, files));
|
|
383
|
+
}
|
|
384
|
+
return found;
|
|
385
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The members of a finding its emitter decides: the code, where the subject is,
|
|
3
|
+
* what the subject is, and the sentence the finding reports. They are the
|
|
4
|
+
* Contract's finding members of the same names, spelled in this language's case.
|
|
5
|
+
*
|
|
6
|
+
* Every other member the finding schema requires follows from these and from the
|
|
7
|
+
* run: the kind's name, its fixability and its ceiling come from the code's row of
|
|
8
|
+
* the issue-kind vocabulary, the severity and the confidence from the resolved
|
|
9
|
+
* configuration, the language from the analyzer. A report completes them, so an
|
|
10
|
+
* emitter cannot state one that disagrees with the vocabulary.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|