@ontrails/adapter-kit 0.2.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/package.json +35 -0
- package/src/catalog.ts +880 -0
- package/src/check.ts +2495 -0
- package/src/index.ts +41 -0
- package/src/overlay.ts +158 -0
- package/src/source.ts +684 -0
package/src/check.ts
ADDED
|
@@ -0,0 +1,2495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared adapter readiness checks for Warden and local author tooling.
|
|
3
|
+
*
|
|
4
|
+
* The engine reads package manifests and source files. It does not import
|
|
5
|
+
* runtime adapter packages, and runtime adapters must not import it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
statSync,
|
|
14
|
+
} from 'node:fs';
|
|
15
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { escapeRegExp, listWorkspacePackages } from '@ontrails/core';
|
|
18
|
+
import type {
|
|
19
|
+
DiagnosticBase,
|
|
20
|
+
WorkspacePackage as CoreWorkspacePackage,
|
|
21
|
+
} from '@ontrails/core';
|
|
22
|
+
import {
|
|
23
|
+
identifierName,
|
|
24
|
+
isShadowed,
|
|
25
|
+
parse,
|
|
26
|
+
walkWithScopes,
|
|
27
|
+
} from '@ontrails/source';
|
|
28
|
+
import type { AstNode } from '@ontrails/source';
|
|
29
|
+
|
|
30
|
+
import { deriveAdapterTargetCatalog } from './catalog.js';
|
|
31
|
+
import type {
|
|
32
|
+
AdapterTargetCatalog,
|
|
33
|
+
AdapterTargetCatalogDiagnosticCode,
|
|
34
|
+
AdapterTargetCatalogEntry,
|
|
35
|
+
AdapterTargetPlacement,
|
|
36
|
+
} from './catalog.js';
|
|
37
|
+
|
|
38
|
+
export type AdapterCheckDiagnosticCode =
|
|
39
|
+
| AdapterTargetCatalogDiagnosticCode
|
|
40
|
+
| 'dependency-direction'
|
|
41
|
+
| 'invalid-adapter-metadata'
|
|
42
|
+
| 'missing-conformance'
|
|
43
|
+
| 'missing-owner-conformance'
|
|
44
|
+
| 'missing-package-export'
|
|
45
|
+
| 'tooling-boundary'
|
|
46
|
+
| 'unknown-adapter-target'
|
|
47
|
+
| 'unsupported-placement';
|
|
48
|
+
|
|
49
|
+
export type AdapterCheckDiagnosticSeverity = DiagnosticBase['severity'];
|
|
50
|
+
|
|
51
|
+
export interface AdapterCheckDiagnostic extends DiagnosticBase<AdapterCheckDiagnosticCode> {
|
|
52
|
+
readonly code: AdapterCheckDiagnosticCode;
|
|
53
|
+
readonly message: string;
|
|
54
|
+
readonly packageJsonPath: string;
|
|
55
|
+
readonly packageName?: string | undefined;
|
|
56
|
+
readonly placement?: AdapterTargetPlacement | undefined;
|
|
57
|
+
readonly target?: string | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AdapterCheckSubject {
|
|
61
|
+
readonly adapterType?: string | undefined;
|
|
62
|
+
readonly conformanceTestPaths: readonly string[];
|
|
63
|
+
readonly key: string;
|
|
64
|
+
readonly ownerPackage: string;
|
|
65
|
+
readonly packageJsonPath: string;
|
|
66
|
+
readonly packageName: string;
|
|
67
|
+
readonly packageRoot: string;
|
|
68
|
+
readonly placement: AdapterTargetPlacement;
|
|
69
|
+
readonly target: string;
|
|
70
|
+
readonly targetKey: string;
|
|
71
|
+
readonly testingImport?: string | undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type AdapterFactKind = 'available' | 'configured' | 'observed' | 'used';
|
|
75
|
+
|
|
76
|
+
export type AdapterFactProvenanceSource =
|
|
77
|
+
| 'adapter-package-manifest'
|
|
78
|
+
| 'conformance-test'
|
|
79
|
+
| 'owner-package-manifest'
|
|
80
|
+
| 'runtime-observation';
|
|
81
|
+
|
|
82
|
+
export interface AdapterFactProvenance {
|
|
83
|
+
readonly packageJsonPath?: string | undefined;
|
|
84
|
+
readonly paths?: readonly string[] | undefined;
|
|
85
|
+
readonly source: AdapterFactProvenanceSource;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface AdapterFact {
|
|
89
|
+
readonly adapterType?: string | undefined;
|
|
90
|
+
readonly key: string;
|
|
91
|
+
readonly kind: AdapterFactKind;
|
|
92
|
+
readonly ownerPackage?: string | undefined;
|
|
93
|
+
readonly packageName?: string | undefined;
|
|
94
|
+
readonly placement?: AdapterTargetPlacement | undefined;
|
|
95
|
+
readonly placements?: readonly AdapterTargetPlacement[] | undefined;
|
|
96
|
+
readonly provenance: AdapterFactProvenance;
|
|
97
|
+
readonly target: string;
|
|
98
|
+
readonly targetKey?: string | undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AdapterCheckReport {
|
|
102
|
+
readonly diagnostics: readonly AdapterCheckDiagnostic[];
|
|
103
|
+
readonly facts: readonly AdapterFact[];
|
|
104
|
+
readonly subjects: readonly AdapterCheckSubject[];
|
|
105
|
+
readonly targets: readonly AdapterTargetCatalogEntry[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface AdapterCheckPackageManifest {
|
|
109
|
+
readonly dependencies?: unknown;
|
|
110
|
+
readonly devDependencies?: unknown;
|
|
111
|
+
readonly exports?: unknown;
|
|
112
|
+
readonly name?: unknown;
|
|
113
|
+
readonly optionalDependencies?: unknown;
|
|
114
|
+
readonly peerDependencies?: unknown;
|
|
115
|
+
readonly trails?: unknown;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type WorkspacePackage = CoreWorkspacePackage<AdapterCheckPackageManifest>;
|
|
119
|
+
|
|
120
|
+
interface AdapterMetadata {
|
|
121
|
+
readonly target: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface SubpathAdapterMetadata extends AdapterMetadata {
|
|
125
|
+
readonly exportKey: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface AdapterCheckCandidate {
|
|
129
|
+
readonly exportKey?: string | undefined;
|
|
130
|
+
readonly key: string;
|
|
131
|
+
readonly metadata: AdapterMetadata;
|
|
132
|
+
readonly packageName: string;
|
|
133
|
+
readonly placement: AdapterTargetPlacement;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const adapterKitPackageName = '@ontrails/adapter-kit';
|
|
137
|
+
|
|
138
|
+
const targetIdPattern = /^[a-z][a-z0-9-]*$/u;
|
|
139
|
+
const subpathAdapterExportKeyPattern =
|
|
140
|
+
/^\.\/[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/u;
|
|
141
|
+
|
|
142
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
143
|
+
Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
144
|
+
|
|
145
|
+
const normalizePath = (path: string): string => path.replaceAll('\\', '/');
|
|
146
|
+
|
|
147
|
+
const normalizeRealPath = (path: string): string => {
|
|
148
|
+
try {
|
|
149
|
+
return normalizePath(realpathSync(path));
|
|
150
|
+
} catch {
|
|
151
|
+
return normalizePath(resolve(path));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const workspacePackages = (rootDir: string): readonly WorkspacePackage[] =>
|
|
156
|
+
listWorkspacePackages<AdapterCheckPackageManifest>(rootDir);
|
|
157
|
+
|
|
158
|
+
const resolveExportTarget = (
|
|
159
|
+
target: unknown,
|
|
160
|
+
depth = 0
|
|
161
|
+
): string | undefined => {
|
|
162
|
+
if (typeof target === 'string') {
|
|
163
|
+
return target;
|
|
164
|
+
}
|
|
165
|
+
if (!isRecord(target) || depth > 8) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const condition of ['bun', 'import', 'default', 'require'] as const) {
|
|
170
|
+
const resolvedTarget = resolveExportTarget(target[condition], depth + 1);
|
|
171
|
+
if (resolvedTarget) {
|
|
172
|
+
return resolvedTarget;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const isPackageRelativePath = (path: string): boolean =>
|
|
179
|
+
path.startsWith('./') && !normalizePath(path).includes('/../');
|
|
180
|
+
|
|
181
|
+
const exportTargetIsFile = (packageRoot: string, target: string): boolean => {
|
|
182
|
+
if (!isPackageRelativePath(target)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
return statSync(resolve(packageRoot, target)).isFile();
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const resolvableExportTarget = (
|
|
194
|
+
workspace: WorkspacePackage,
|
|
195
|
+
key: string
|
|
196
|
+
): string | undefined => {
|
|
197
|
+
const { exports: exportsValue } = workspace.manifest;
|
|
198
|
+
if (typeof exportsValue === 'string') {
|
|
199
|
+
return key === '.' &&
|
|
200
|
+
exportTargetIsFile(workspace.packageRoot, exportsValue)
|
|
201
|
+
? normalizeRealPath(resolve(workspace.packageRoot, exportsValue))
|
|
202
|
+
: undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!isRecord(exportsValue)) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!Object.hasOwn(exportsValue, key)) {
|
|
210
|
+
if (key !== '.') {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const rootTarget = resolveExportTarget(exportsValue);
|
|
215
|
+
return rootTarget !== undefined &&
|
|
216
|
+
exportTargetIsFile(workspace.packageRoot, rootTarget)
|
|
217
|
+
? normalizeRealPath(resolve(workspace.packageRoot, rootTarget))
|
|
218
|
+
: undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const target = resolveExportTarget(exportsValue[key]);
|
|
222
|
+
return target !== undefined &&
|
|
223
|
+
exportTargetIsFile(workspace.packageRoot, target)
|
|
224
|
+
? normalizeRealPath(resolve(workspace.packageRoot, target))
|
|
225
|
+
: undefined;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const hasResolvableExport = (
|
|
229
|
+
workspace: WorkspacePackage,
|
|
230
|
+
key: string
|
|
231
|
+
): boolean => resolvableExportTarget(workspace, key) !== undefined;
|
|
232
|
+
|
|
233
|
+
const dependencyMap = (value: unknown): Readonly<Record<string, unknown>> =>
|
|
234
|
+
isRecord(value) ? value : {};
|
|
235
|
+
|
|
236
|
+
const runtimeDependencyNames = (
|
|
237
|
+
manifest: AdapterCheckPackageManifest
|
|
238
|
+
): ReadonlySet<string> =>
|
|
239
|
+
new Set([
|
|
240
|
+
...Object.keys(dependencyMap(manifest.dependencies)),
|
|
241
|
+
...Object.keys(dependencyMap(manifest.optionalDependencies)),
|
|
242
|
+
...Object.keys(dependencyMap(manifest.peerDependencies)),
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
const trailAdapterMetadata = (
|
|
246
|
+
manifest: AdapterCheckPackageManifest
|
|
247
|
+
): AdapterMetadata | undefined | null => {
|
|
248
|
+
const trails = isRecord(manifest.trails) ? manifest.trails : undefined;
|
|
249
|
+
const adapter = trails?.['adapter'];
|
|
250
|
+
if (adapter === undefined) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
if (!isRecord(adapter)) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const { target } = adapter;
|
|
258
|
+
return typeof target === 'string' && targetIdPattern.test(target)
|
|
259
|
+
? { target }
|
|
260
|
+
: null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const trailSubpathAdapterMetadata = (
|
|
264
|
+
manifest: AdapterCheckPackageManifest
|
|
265
|
+
): readonly SubpathAdapterMetadata[] | undefined | null => {
|
|
266
|
+
const trails = isRecord(manifest.trails) ? manifest.trails : undefined;
|
|
267
|
+
const adapters = trails?.['adapters'];
|
|
268
|
+
if (adapters === undefined) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
if (!isRecord(adapters)) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const metadata: SubpathAdapterMetadata[] = [];
|
|
276
|
+
for (const [exportKey, adapter] of Object.entries(adapters).toSorted()) {
|
|
277
|
+
if (!subpathAdapterExportKeyPattern.test(exportKey)) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (!isRecord(adapter)) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const { target } = adapter;
|
|
285
|
+
if (typeof target !== 'string' || !targetIdPattern.test(target)) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
metadata.push({ exportKey, target });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return metadata;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const placementForWorkspace = (
|
|
295
|
+
workspacePath: string
|
|
296
|
+
): AdapterTargetPlacement | undefined =>
|
|
297
|
+
workspacePath.startsWith('adapters/') ? 'extracted' : undefined;
|
|
298
|
+
|
|
299
|
+
const diagnostic = (
|
|
300
|
+
packageJsonPath: string,
|
|
301
|
+
packageName: string | undefined,
|
|
302
|
+
code: AdapterCheckDiagnosticCode,
|
|
303
|
+
message: string,
|
|
304
|
+
target?: string,
|
|
305
|
+
placement?: AdapterTargetPlacement
|
|
306
|
+
): AdapterCheckDiagnostic => ({
|
|
307
|
+
code,
|
|
308
|
+
message,
|
|
309
|
+
packageJsonPath,
|
|
310
|
+
...(packageName === undefined ? {} : { packageName }),
|
|
311
|
+
...(placement === undefined ? {} : { placement }),
|
|
312
|
+
severity: 'error',
|
|
313
|
+
...(target === undefined ? {} : { target }),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const catalogDiagnostics = (
|
|
317
|
+
catalog: AdapterTargetCatalog
|
|
318
|
+
): readonly AdapterCheckDiagnostic[] =>
|
|
319
|
+
catalog.diagnostics.map((entry) =>
|
|
320
|
+
diagnostic(
|
|
321
|
+
entry.packageJsonPath,
|
|
322
|
+
entry.packageName,
|
|
323
|
+
entry.code,
|
|
324
|
+
entry.message,
|
|
325
|
+
entry.target
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const targetEntriesByTarget = (
|
|
330
|
+
targets: readonly AdapterTargetCatalogEntry[]
|
|
331
|
+
): ReadonlyMap<string, AdapterTargetCatalogEntry> =>
|
|
332
|
+
new Map(targets.map((target) => [target.target, target]));
|
|
333
|
+
|
|
334
|
+
const adapterFacts = (
|
|
335
|
+
targets: readonly AdapterTargetCatalogEntry[],
|
|
336
|
+
subjects: readonly AdapterCheckSubject[]
|
|
337
|
+
): readonly AdapterFact[] => {
|
|
338
|
+
const facts: AdapterFact[] = [];
|
|
339
|
+
|
|
340
|
+
for (const target of targets) {
|
|
341
|
+
facts.push({
|
|
342
|
+
key: `${target.key}:available`,
|
|
343
|
+
kind: 'available',
|
|
344
|
+
ownerPackage: target.ownerPackage,
|
|
345
|
+
placements: target.placements,
|
|
346
|
+
provenance: {
|
|
347
|
+
packageJsonPath: target.packageJsonPath,
|
|
348
|
+
source: 'owner-package-manifest',
|
|
349
|
+
},
|
|
350
|
+
target: target.target,
|
|
351
|
+
targetKey: target.key,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const subject of subjects) {
|
|
356
|
+
facts.push({
|
|
357
|
+
key: `${subject.key}:${subject.target}:configured`,
|
|
358
|
+
kind: 'configured',
|
|
359
|
+
ownerPackage: subject.ownerPackage,
|
|
360
|
+
packageName: subject.packageName,
|
|
361
|
+
placement: subject.placement,
|
|
362
|
+
provenance: {
|
|
363
|
+
packageJsonPath: subject.packageJsonPath,
|
|
364
|
+
source:
|
|
365
|
+
subject.placement === 'subpath'
|
|
366
|
+
? 'owner-package-manifest'
|
|
367
|
+
: 'adapter-package-manifest',
|
|
368
|
+
},
|
|
369
|
+
target: subject.target,
|
|
370
|
+
targetKey: subject.targetKey,
|
|
371
|
+
...(subject.adapterType ? { adapterType: subject.adapterType } : {}),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (subject.conformanceTestPaths.length > 0) {
|
|
375
|
+
facts.push({
|
|
376
|
+
key: `${subject.key}:${subject.target}:used`,
|
|
377
|
+
kind: 'used',
|
|
378
|
+
ownerPackage: subject.ownerPackage,
|
|
379
|
+
packageName: subject.packageName,
|
|
380
|
+
placement: subject.placement,
|
|
381
|
+
provenance: {
|
|
382
|
+
paths: subject.conformanceTestPaths,
|
|
383
|
+
source: 'conformance-test',
|
|
384
|
+
},
|
|
385
|
+
target: subject.target,
|
|
386
|
+
targetKey: subject.targetKey,
|
|
387
|
+
...(subject.adapterType ? { adapterType: subject.adapterType } : {}),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return facts;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const collectSourceFiles = (dir: string): readonly string[] => {
|
|
396
|
+
if (!existsSync(dir)) {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const files: string[] = [];
|
|
401
|
+
const visit = (current: string): void => {
|
|
402
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
403
|
+
const child = join(current, entry.name);
|
|
404
|
+
if (entry.isDirectory()) {
|
|
405
|
+
visit(child);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (entry.isFile() && child.endsWith('.ts')) {
|
|
409
|
+
files.push(normalizeRealPath(child));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
visit(dir);
|
|
415
|
+
return files.toSorted();
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const isIdentifierChar = (char: string | undefined): boolean =>
|
|
419
|
+
char !== undefined && /[$\w]/u.test(char);
|
|
420
|
+
|
|
421
|
+
const skipWhitespace = (source: string, start: number): number => {
|
|
422
|
+
let index = start;
|
|
423
|
+
while (/\s/u.test(source[index] ?? '')) {
|
|
424
|
+
index += 1;
|
|
425
|
+
}
|
|
426
|
+
return index;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const previousNonWhitespaceChar = (
|
|
430
|
+
source: string,
|
|
431
|
+
start: number
|
|
432
|
+
): string | undefined => {
|
|
433
|
+
let index = start - 1;
|
|
434
|
+
while (index >= 0) {
|
|
435
|
+
const char = source[index];
|
|
436
|
+
if (!/\s/u.test(char ?? '')) {
|
|
437
|
+
return char;
|
|
438
|
+
}
|
|
439
|
+
index -= 1;
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const regexLiteralCanStartAfter = (char: string | undefined): boolean =>
|
|
445
|
+
char === undefined || /[([{:;,=!?&|+\-*%^~<>]/u.test(char);
|
|
446
|
+
|
|
447
|
+
const regexLiteralCanStartAfterKeyword = (
|
|
448
|
+
source: string,
|
|
449
|
+
start: number
|
|
450
|
+
): boolean =>
|
|
451
|
+
/\b(?:await|case|delete|do|else|in|instanceof|of|return|throw|typeof|void|yield)\s*$/u.test(
|
|
452
|
+
source.slice(0, start).trimEnd()
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const startsRegexLiteral = (source: string, start: number): boolean => {
|
|
456
|
+
if (
|
|
457
|
+
source[start] !== '/' ||
|
|
458
|
+
source.startsWith('//', start) ||
|
|
459
|
+
source.startsWith('/*', start)
|
|
460
|
+
) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const previousChar = previousNonWhitespaceChar(source, start);
|
|
465
|
+
return (
|
|
466
|
+
regexLiteralCanStartAfter(previousChar) ||
|
|
467
|
+
regexLiteralCanStartAfterKeyword(source, start)
|
|
468
|
+
);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const skipRegexLiteral = (source: string, start: number): number => {
|
|
472
|
+
let index = start + 1;
|
|
473
|
+
let inCharacterClass = false;
|
|
474
|
+
while (index < source.length) {
|
|
475
|
+
const char = source[index];
|
|
476
|
+
if (char === '\\') {
|
|
477
|
+
index += 2;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (char === '[') {
|
|
481
|
+
inCharacterClass = true;
|
|
482
|
+
index += 1;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (char === ']' && inCharacterClass) {
|
|
486
|
+
inCharacterClass = false;
|
|
487
|
+
index += 1;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (char === '/' && !inCharacterClass) {
|
|
491
|
+
index += 1;
|
|
492
|
+
while (/[a-z]/iu.test(source[index] ?? '')) {
|
|
493
|
+
index += 1;
|
|
494
|
+
}
|
|
495
|
+
return index;
|
|
496
|
+
}
|
|
497
|
+
index += 1;
|
|
498
|
+
}
|
|
499
|
+
return source.length;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const readQuotedString = (
|
|
503
|
+
source: string,
|
|
504
|
+
start: number
|
|
505
|
+
): { readonly end: number; readonly value: string } | undefined => {
|
|
506
|
+
const quote = source[start];
|
|
507
|
+
if (quote !== '"' && quote !== "'") {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
let index = start + 1;
|
|
512
|
+
let value = '';
|
|
513
|
+
while (index < source.length) {
|
|
514
|
+
const char = source[index];
|
|
515
|
+
if (char === '\\') {
|
|
516
|
+
value += source[index + 1] ?? '';
|
|
517
|
+
index += 2;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (char === quote) {
|
|
521
|
+
return { end: index + 1, value };
|
|
522
|
+
}
|
|
523
|
+
value += char;
|
|
524
|
+
index += 1;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return undefined;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const skipBlockComment = (source: string, start: number): number => {
|
|
531
|
+
const end = source.indexOf('*/', start + 2);
|
|
532
|
+
return end === -1 ? source.length : end + 2;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const skipLineComment = (source: string, start: number): number => {
|
|
536
|
+
const end = source.indexOf('\n', start + 2);
|
|
537
|
+
return end === -1 ? source.length : end + 1;
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const skipTrivia = (source: string, start: number): number => {
|
|
541
|
+
let index = skipWhitespace(source, start);
|
|
542
|
+
while (index < source.length) {
|
|
543
|
+
if (source.startsWith('//', index)) {
|
|
544
|
+
index = skipWhitespace(source, skipLineComment(source, index));
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (source.startsWith('/*', index)) {
|
|
548
|
+
index = skipWhitespace(source, skipBlockComment(source, index));
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
return index;
|
|
552
|
+
}
|
|
553
|
+
return index;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const skipQuotedLiteral = (source: string, start: number): number => {
|
|
557
|
+
const quote = source[start];
|
|
558
|
+
let index = start + 1;
|
|
559
|
+
while (index < source.length) {
|
|
560
|
+
if (source[index] === '\\') {
|
|
561
|
+
index += 2;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (source[index] === quote) {
|
|
565
|
+
return index + 1;
|
|
566
|
+
}
|
|
567
|
+
index += 1;
|
|
568
|
+
}
|
|
569
|
+
return source.length;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const skipImportScanIgnoredToken = (
|
|
573
|
+
source: string,
|
|
574
|
+
start: number
|
|
575
|
+
): number | undefined => {
|
|
576
|
+
if (source.startsWith('//', start)) {
|
|
577
|
+
return skipLineComment(source, start);
|
|
578
|
+
}
|
|
579
|
+
if (source.startsWith('/*', start)) {
|
|
580
|
+
return skipBlockComment(source, start);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const char = source[start];
|
|
584
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
585
|
+
return skipQuotedLiteral(source, start);
|
|
586
|
+
}
|
|
587
|
+
if (char === '/' && startsRegexLiteral(source, start)) {
|
|
588
|
+
return skipRegexLiteral(source, start);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return undefined;
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const importClauseHasRuntimeBinding = (
|
|
595
|
+
clause: string,
|
|
596
|
+
options: { readonly emptyNamedCounts?: boolean } = {}
|
|
597
|
+
): boolean => {
|
|
598
|
+
const emptyNamedCounts = options.emptyNamedCounts ?? true;
|
|
599
|
+
const uncommented = clause
|
|
600
|
+
.replaceAll(/\/\*[\s\S]*?\*\//g, ' ')
|
|
601
|
+
.replaceAll(/\/\/[^\n\r]*/g, ' ');
|
|
602
|
+
const trimmed = uncommented.trim();
|
|
603
|
+
if (!trimmed) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const namedStart = trimmed.indexOf('{');
|
|
608
|
+
if (namedStart === -1) {
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (trimmed.slice(0, namedStart).replaceAll(',', '').trim()) {
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const namedEnd = trimmed.indexOf('}', namedStart + 1);
|
|
617
|
+
if (namedEnd === -1) {
|
|
618
|
+
return emptyNamedCounts;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const namedBindings = trimmed
|
|
622
|
+
.slice(namedStart + 1, namedEnd)
|
|
623
|
+
.split(',')
|
|
624
|
+
.map((binding) => binding.trim())
|
|
625
|
+
.filter(Boolean);
|
|
626
|
+
if (namedBindings.length === 0) {
|
|
627
|
+
return emptyNamedCounts;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return namedBindings.some((binding) => !/^type(?:\s|$)/u.test(binding));
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
const previousStatementStart = (source: string, start: number): number => {
|
|
634
|
+
let index = start - 1;
|
|
635
|
+
while (index >= 0) {
|
|
636
|
+
if (source[index] === ';' || source[index] === '}') {
|
|
637
|
+
return index + 1;
|
|
638
|
+
}
|
|
639
|
+
index -= 1;
|
|
640
|
+
}
|
|
641
|
+
return 0;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const importAppearsInTypePosition = (
|
|
645
|
+
source: string,
|
|
646
|
+
start: number
|
|
647
|
+
): boolean => {
|
|
648
|
+
const prefix = source
|
|
649
|
+
.slice(previousStatementStart(source, start), start)
|
|
650
|
+
.replaceAll(/\/\*[\s\S]*?\*\//g, ' ')
|
|
651
|
+
.replaceAll(/\/\/[^\n\r]*/g, ' ')
|
|
652
|
+
.trimStart();
|
|
653
|
+
const lastLineStart =
|
|
654
|
+
Math.max(prefix.lastIndexOf('\n'), prefix.lastIndexOf('\r')) + 1;
|
|
655
|
+
const lastLine = prefix.slice(lastLineStart).trimStart();
|
|
656
|
+
if (
|
|
657
|
+
/^(?:export\s+)?(?:type|interface)\b/u.test(prefix) &&
|
|
658
|
+
lastLineStart > 0 &&
|
|
659
|
+
/^(?:const|let|var|using|await|return|throw|void|yield|new)\b/u.test(
|
|
660
|
+
lastLine
|
|
661
|
+
)
|
|
662
|
+
) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (/^(?:export\s+)?(?:type|interface)\b/u.test(prefix)) {
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const trimmed = prefix.trimEnd();
|
|
671
|
+
const lastAssignment = prefix.lastIndexOf('=');
|
|
672
|
+
const colonLooksLikeTypeAnnotation = (colonIndex: number): boolean => {
|
|
673
|
+
const objectLiteralStart = prefix.indexOf('{', lastAssignment + 1);
|
|
674
|
+
if (objectLiteralStart !== -1 && objectLiteralStart < colonIndex) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const blockStart = prefix.lastIndexOf('{');
|
|
679
|
+
if (blockStart > colonIndex) {
|
|
680
|
+
const blockTail = prefix.slice(blockStart + 1).trimStart();
|
|
681
|
+
if (
|
|
682
|
+
/^(?:const|let|var|using|await|return|throw|void|yield|new)\b/u.test(
|
|
683
|
+
blockTail
|
|
684
|
+
)
|
|
685
|
+
) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const questionIndex = prefix.indexOf('?', lastAssignment + 1);
|
|
691
|
+
return questionIndex === -1 || questionIndex > colonIndex;
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
if (trimmed.endsWith('<') && isIdentifierChar(trimmed.at(-2))) {
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (trimmed.endsWith(':')) {
|
|
699
|
+
const trailingColon = prefix.lastIndexOf(':');
|
|
700
|
+
return colonLooksLikeTypeAnnotation(trailingColon);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (/\b(?:as|extends|implements|satisfies|typeof)\s*$/u.test(trimmed)) {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const annotationColon = prefix.indexOf(':', lastAssignment + 1);
|
|
708
|
+
if (annotationColon === -1) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return colonLooksLikeTypeAnnotation(annotationColon);
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const importDeclarationSpecifier = (
|
|
716
|
+
source: string,
|
|
717
|
+
start: number
|
|
718
|
+
): string | undefined => {
|
|
719
|
+
let index = skipTrivia(source, start + 'import'.length);
|
|
720
|
+
if (source[index] === '.') {
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
if (
|
|
724
|
+
source.startsWith('type', index) &&
|
|
725
|
+
!isIdentifierChar(source[index + 'type'.length])
|
|
726
|
+
) {
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
const sideEffect = readQuotedString(source, index);
|
|
730
|
+
if (sideEffect) {
|
|
731
|
+
return sideEffect.value;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (source[index] === '(') {
|
|
735
|
+
if (importAppearsInTypePosition(source, start)) {
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
index = skipTrivia(source, index + 1);
|
|
739
|
+
return readQuotedString(source, index)?.value;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const clauseStart = index;
|
|
743
|
+
while (index < source.length) {
|
|
744
|
+
if (source.startsWith('//', index)) {
|
|
745
|
+
index = skipLineComment(source, index);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (source.startsWith('/*', index)) {
|
|
749
|
+
index = skipBlockComment(source, index);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const char = source[index];
|
|
753
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
754
|
+
index = skipQuotedLiteral(source, index);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (
|
|
758
|
+
source.startsWith('from', index) &&
|
|
759
|
+
!isIdentifierChar(source[index - 1]) &&
|
|
760
|
+
!isIdentifierChar(source[index + 'from'.length])
|
|
761
|
+
) {
|
|
762
|
+
if (!importClauseHasRuntimeBinding(source.slice(clauseStart, index))) {
|
|
763
|
+
return undefined;
|
|
764
|
+
}
|
|
765
|
+
index = skipTrivia(source, index + 'from'.length);
|
|
766
|
+
return readQuotedString(source, index)?.value;
|
|
767
|
+
}
|
|
768
|
+
if (char === ';') {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
index += 1;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return undefined;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const boundImportDeclarationSpecifier = (
|
|
778
|
+
source: string,
|
|
779
|
+
start: number
|
|
780
|
+
): string | undefined => {
|
|
781
|
+
let index = skipTrivia(source, start + 'import'.length);
|
|
782
|
+
if (source[index] === '.') {
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
if (
|
|
786
|
+
source.startsWith('type', index) &&
|
|
787
|
+
!isIdentifierChar(source[index + 'type'.length])
|
|
788
|
+
) {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
if (readQuotedString(source, index)) {
|
|
792
|
+
return undefined;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (source[index] === '(') {
|
|
796
|
+
if (importAppearsInTypePosition(source, start)) {
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
index = skipTrivia(source, index + 1);
|
|
800
|
+
return readQuotedString(source, index)?.value;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const clauseStart = index;
|
|
804
|
+
while (index < source.length) {
|
|
805
|
+
if (source.startsWith('//', index)) {
|
|
806
|
+
index = skipLineComment(source, index);
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (source.startsWith('/*', index)) {
|
|
810
|
+
index = skipBlockComment(source, index);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const char = source[index];
|
|
814
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
815
|
+
index = skipQuotedLiteral(source, index);
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (
|
|
819
|
+
source.startsWith('from', index) &&
|
|
820
|
+
!isIdentifierChar(source[index - 1]) &&
|
|
821
|
+
!isIdentifierChar(source[index + 'from'.length])
|
|
822
|
+
) {
|
|
823
|
+
if (
|
|
824
|
+
!importClauseHasRuntimeBinding(source.slice(clauseStart, index), {
|
|
825
|
+
emptyNamedCounts: false,
|
|
826
|
+
})
|
|
827
|
+
) {
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
index = skipTrivia(source, index + 'from'.length);
|
|
831
|
+
return readQuotedString(source, index)?.value;
|
|
832
|
+
}
|
|
833
|
+
if (char === ';') {
|
|
834
|
+
return undefined;
|
|
835
|
+
}
|
|
836
|
+
index += 1;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return undefined;
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const reExportDeclarationSpecifier = (
|
|
843
|
+
source: string,
|
|
844
|
+
start: number
|
|
845
|
+
): string | undefined => {
|
|
846
|
+
let index = skipTrivia(source, start + 'export'.length);
|
|
847
|
+
if (
|
|
848
|
+
source.startsWith('type', index) &&
|
|
849
|
+
!isIdentifierChar(source[index + 'type'.length])
|
|
850
|
+
) {
|
|
851
|
+
return undefined;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const clauseStart = index;
|
|
855
|
+
while (index < source.length) {
|
|
856
|
+
if (source.startsWith('//', index)) {
|
|
857
|
+
index = skipLineComment(source, index);
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (source.startsWith('/*', index)) {
|
|
861
|
+
index = skipBlockComment(source, index);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const char = source[index];
|
|
865
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
866
|
+
index = skipQuotedLiteral(source, index);
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (char === '}') {
|
|
870
|
+
const next = skipTrivia(source, index + 1);
|
|
871
|
+
if (
|
|
872
|
+
!source.startsWith('from', next) ||
|
|
873
|
+
isIdentifierChar(source[next + 'from'.length])
|
|
874
|
+
) {
|
|
875
|
+
return undefined;
|
|
876
|
+
}
|
|
877
|
+
index = next;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (
|
|
881
|
+
source.startsWith('from', index) &&
|
|
882
|
+
!isIdentifierChar(source[index - 1]) &&
|
|
883
|
+
!isIdentifierChar(source[index + 'from'.length])
|
|
884
|
+
) {
|
|
885
|
+
if (!importClauseHasRuntimeBinding(source.slice(clauseStart, index))) {
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
index = skipTrivia(source, index + 'from'.length);
|
|
889
|
+
return readQuotedString(source, index)?.value;
|
|
890
|
+
}
|
|
891
|
+
if (char === ';') {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
index += 1;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return undefined;
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
const maskSource = (source: string, options: { strings: boolean }): string => {
|
|
901
|
+
const output = [...source];
|
|
902
|
+
let index = 0;
|
|
903
|
+
|
|
904
|
+
const maskRange = (start: number, end: number): void => {
|
|
905
|
+
for (let cursor = start; cursor < end; cursor += 1) {
|
|
906
|
+
if (output[cursor] !== '\n') {
|
|
907
|
+
output[cursor] = ' ';
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
const skipQuoted = (quote: '"' | "'" | '`'): void => {
|
|
913
|
+
const start = index;
|
|
914
|
+
index += 1;
|
|
915
|
+
while (index < source.length) {
|
|
916
|
+
if (source[index] === '\\') {
|
|
917
|
+
index += 2;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if (source[index] === quote) {
|
|
921
|
+
index += 1;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
index += 1;
|
|
925
|
+
}
|
|
926
|
+
if (options.strings) {
|
|
927
|
+
maskRange(start, index);
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
while (index < source.length) {
|
|
932
|
+
if (source.startsWith('//', index)) {
|
|
933
|
+
const end = source.indexOf('\n', index + 2);
|
|
934
|
+
const stop = end === -1 ? source.length : end;
|
|
935
|
+
maskRange(index, stop);
|
|
936
|
+
index = stop;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (source.startsWith('/*', index)) {
|
|
940
|
+
const end = source.indexOf('*/', index + 2);
|
|
941
|
+
const stop = end === -1 ? source.length : end + 2;
|
|
942
|
+
maskRange(index, stop);
|
|
943
|
+
index = stop;
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const char = source[index];
|
|
947
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
948
|
+
skipQuoted(char);
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
index += 1;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
return output.join('');
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
const matchStartsWithAnyKeyword = (
|
|
958
|
+
maskedSource: string,
|
|
959
|
+
match: RegExpMatchArray,
|
|
960
|
+
keywords: readonly string[]
|
|
961
|
+
): boolean => {
|
|
962
|
+
const index = match.index ?? 0;
|
|
963
|
+
return keywords.some(
|
|
964
|
+
(keyword) =>
|
|
965
|
+
maskedSource.startsWith(keyword, index) &&
|
|
966
|
+
!isIdentifierChar(maskedSource[index - 1]) &&
|
|
967
|
+
!isIdentifierChar(maskedSource[index + keyword.length])
|
|
968
|
+
);
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
const importsSpecifier = (
|
|
972
|
+
source: string,
|
|
973
|
+
specifier: string,
|
|
974
|
+
options: { includeReExports?: boolean; requireImportBinding?: boolean } = {}
|
|
975
|
+
): boolean => {
|
|
976
|
+
const includeReExports = options.includeReExports ?? true;
|
|
977
|
+
const importSpecifier = options.requireImportBinding
|
|
978
|
+
? boundImportDeclarationSpecifier
|
|
979
|
+
: importDeclarationSpecifier;
|
|
980
|
+
let index = 0;
|
|
981
|
+
while (index < source.length) {
|
|
982
|
+
const skippedIndex = skipImportScanIgnoredToken(source, index);
|
|
983
|
+
if (skippedIndex !== undefined) {
|
|
984
|
+
index = skippedIndex;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (
|
|
989
|
+
source.startsWith('import', index) &&
|
|
990
|
+
!isIdentifierChar(source[index - 1]) &&
|
|
991
|
+
previousNonWhitespaceChar(source, index) !== '.' &&
|
|
992
|
+
!isIdentifierChar(source[index + 'import'.length])
|
|
993
|
+
) {
|
|
994
|
+
if (importSpecifier(source, index) === specifier) {
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
index += 'import'.length;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
if (
|
|
1001
|
+
includeReExports &&
|
|
1002
|
+
source.startsWith('export', index) &&
|
|
1003
|
+
!isIdentifierChar(source[index - 1]) &&
|
|
1004
|
+
!isIdentifierChar(source[index + 'export'.length])
|
|
1005
|
+
) {
|
|
1006
|
+
if (reExportDeclarationSpecifier(source, index) === specifier) {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
index += 'export'.length;
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
index += 1;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return false;
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
const pathsImporting = (
|
|
1019
|
+
sourceFiles: readonly string[],
|
|
1020
|
+
specifier: string,
|
|
1021
|
+
options?: { includeReExports?: boolean; requireImportBinding?: boolean }
|
|
1022
|
+
): readonly string[] =>
|
|
1023
|
+
sourceFiles.filter((filePath) =>
|
|
1024
|
+
importsSpecifier(readFileSync(filePath, 'utf8'), specifier, options)
|
|
1025
|
+
);
|
|
1026
|
+
|
|
1027
|
+
const staticImportClauseForSpecifier = (
|
|
1028
|
+
source: string,
|
|
1029
|
+
start: number,
|
|
1030
|
+
specifier: string
|
|
1031
|
+
): string | undefined => {
|
|
1032
|
+
let index = skipTrivia(source, start + 'import'.length);
|
|
1033
|
+
if (
|
|
1034
|
+
source.startsWith('type', index) &&
|
|
1035
|
+
!isIdentifierChar(source[index + 'type'.length])
|
|
1036
|
+
) {
|
|
1037
|
+
return undefined;
|
|
1038
|
+
}
|
|
1039
|
+
if (readQuotedString(source, index) || source[index] === '(') {
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const clauseStart = index;
|
|
1044
|
+
while (index < source.length) {
|
|
1045
|
+
if (source.startsWith('//', index)) {
|
|
1046
|
+
index = skipLineComment(source, index);
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
if (source.startsWith('/*', index)) {
|
|
1050
|
+
index = skipBlockComment(source, index);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
const char = source[index];
|
|
1054
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
1055
|
+
index = skipQuotedLiteral(source, index);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (
|
|
1059
|
+
source.startsWith('from', index) &&
|
|
1060
|
+
!isIdentifierChar(source[index - 1]) &&
|
|
1061
|
+
!isIdentifierChar(source[index + 'from'.length])
|
|
1062
|
+
) {
|
|
1063
|
+
const clause = source.slice(clauseStart, index);
|
|
1064
|
+
if (!importClauseHasRuntimeBinding(clause)) {
|
|
1065
|
+
return undefined;
|
|
1066
|
+
}
|
|
1067
|
+
index = skipTrivia(source, index + 'from'.length);
|
|
1068
|
+
return readQuotedString(source, index)?.value === specifier
|
|
1069
|
+
? clause
|
|
1070
|
+
: undefined;
|
|
1071
|
+
}
|
|
1072
|
+
if (char === ';') {
|
|
1073
|
+
return undefined;
|
|
1074
|
+
}
|
|
1075
|
+
index += 1;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return undefined;
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
const staticImportClausesForSpecifier = (
|
|
1082
|
+
source: string,
|
|
1083
|
+
specifier: string
|
|
1084
|
+
): readonly string[] => {
|
|
1085
|
+
const clauses: string[] = [];
|
|
1086
|
+
let index = 0;
|
|
1087
|
+
while (index < source.length) {
|
|
1088
|
+
if (source.startsWith('//', index)) {
|
|
1089
|
+
index = skipLineComment(source, index);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (source.startsWith('/*', index)) {
|
|
1093
|
+
index = skipBlockComment(source, index);
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
const char = source[index];
|
|
1097
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
1098
|
+
index = skipQuotedLiteral(source, index);
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
if (
|
|
1102
|
+
source.startsWith('import', index) &&
|
|
1103
|
+
!isIdentifierChar(source[index - 1]) &&
|
|
1104
|
+
!isIdentifierChar(source[index + 'import'.length])
|
|
1105
|
+
) {
|
|
1106
|
+
const clause = staticImportClauseForSpecifier(source, index, specifier);
|
|
1107
|
+
if (clause) {
|
|
1108
|
+
clauses.push(clause);
|
|
1109
|
+
}
|
|
1110
|
+
index += 'import'.length;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
index += 1;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return clauses;
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
const namedImportBindings = (
|
|
1120
|
+
source: string,
|
|
1121
|
+
specifier: string,
|
|
1122
|
+
exportedName: string
|
|
1123
|
+
): readonly string[] => {
|
|
1124
|
+
const namedBindings: string[] = [];
|
|
1125
|
+
const namespaceBindings: string[] = [];
|
|
1126
|
+
for (const clause of staticImportClausesForSpecifier(source, specifier)) {
|
|
1127
|
+
const code = maskSource(clause, { strings: false });
|
|
1128
|
+
const namespaceImport =
|
|
1129
|
+
/(?:^|,)\s*\*\s+as\s+(?<local>[A-Za-z_$][\w$]*)(?:\s*$|,)/u.exec(
|
|
1130
|
+
code
|
|
1131
|
+
)?.groups;
|
|
1132
|
+
if (namespaceImport?.['local']) {
|
|
1133
|
+
namespaceBindings.push(`${namespaceImport['local']}.${exportedName}`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const namedImports = /\{(?<imports>[\s\S]*?)\}/u.exec(code)?.groups?.[
|
|
1137
|
+
'imports'
|
|
1138
|
+
];
|
|
1139
|
+
if (!namedImports) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
for (const item of namedImports.split(',')) {
|
|
1144
|
+
const specifierText = item.trim();
|
|
1145
|
+
if (specifierText.length === 0 || specifierText.startsWith('type ')) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const imported =
|
|
1150
|
+
/^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
|
|
1151
|
+
specifierText
|
|
1152
|
+
)?.groups;
|
|
1153
|
+
if (imported?.['imported'] === exportedName) {
|
|
1154
|
+
namedBindings.push(imported['local'] ?? imported['imported']);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
return [...new Set([...namedBindings, ...namespaceBindings])];
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
const dynamicImportNamespaceBindings = (
|
|
1163
|
+
source: string,
|
|
1164
|
+
specifier: string
|
|
1165
|
+
): readonly string[] => {
|
|
1166
|
+
const code = maskSource(source, { strings: false });
|
|
1167
|
+
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
1168
|
+
const escapedSpecifier = escapeRegExp(specifier);
|
|
1169
|
+
const pattern = new RegExp(
|
|
1170
|
+
`\\bconst\\s+(?<local>[A-Za-z_$][\\w$]*)(?:\\s*:\\s*[^=;]+)?\\s*=\\s*await\\s+import\\s*\\(\\s*['"]${escapedSpecifier}['"]\\s*\\)`,
|
|
1171
|
+
'gu'
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
return [...code.matchAll(pattern)]
|
|
1175
|
+
.filter((match) =>
|
|
1176
|
+
matchStartsWithAnyKeyword(stringsMaskedCode, match, ['const'])
|
|
1177
|
+
)
|
|
1178
|
+
.map((match) => match.groups?.['local'])
|
|
1179
|
+
.filter((local): local is string => local !== undefined);
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
const topLevelNamespaceAliases = (
|
|
1183
|
+
ast: AstNode,
|
|
1184
|
+
namespaceBindings: ReadonlySet<string>
|
|
1185
|
+
): readonly string[] => {
|
|
1186
|
+
if (ast.type !== 'Program') {
|
|
1187
|
+
return [];
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const aliases = new Set(namespaceBindings);
|
|
1191
|
+
const body = (ast as unknown as { body?: readonly AstNode[] }).body ?? [];
|
|
1192
|
+
let changed = true;
|
|
1193
|
+
while (changed) {
|
|
1194
|
+
changed = false;
|
|
1195
|
+
for (const statement of body) {
|
|
1196
|
+
if (statement.type !== 'VariableDeclaration') {
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
if (statement['kind'] !== 'const') {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
const declarations =
|
|
1203
|
+
(statement as unknown as { declarations?: readonly AstNode[] })
|
|
1204
|
+
.declarations ?? [];
|
|
1205
|
+
for (const declaration of declarations) {
|
|
1206
|
+
const declarator = declaration as unknown as {
|
|
1207
|
+
id?: AstNode;
|
|
1208
|
+
init?: AstNode;
|
|
1209
|
+
};
|
|
1210
|
+
const local = identifierName(declarator.id);
|
|
1211
|
+
const sourceBinding = identifierName(declarator.init);
|
|
1212
|
+
if (
|
|
1213
|
+
local &&
|
|
1214
|
+
sourceBinding &&
|
|
1215
|
+
aliases.has(sourceBinding) &&
|
|
1216
|
+
!aliases.has(local)
|
|
1217
|
+
) {
|
|
1218
|
+
aliases.add(local);
|
|
1219
|
+
changed = true;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
return [...aliases].filter((binding) => !namespaceBindings.has(binding));
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
interface DynamicNamedImportBinding {
|
|
1229
|
+
readonly imported: string;
|
|
1230
|
+
readonly local: string;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const closingDestructuringBrace = (
|
|
1234
|
+
source: string,
|
|
1235
|
+
openingBrace: number
|
|
1236
|
+
): number | undefined => {
|
|
1237
|
+
let depth = 0;
|
|
1238
|
+
for (let index = openingBrace; index < source.length; index += 1) {
|
|
1239
|
+
const skippedIndex = skipImportScanIgnoredToken(source, index);
|
|
1240
|
+
if (skippedIndex !== undefined) {
|
|
1241
|
+
index = skippedIndex - 1;
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
const char = source[index];
|
|
1245
|
+
if (char === '{') {
|
|
1246
|
+
depth += 1;
|
|
1247
|
+
} else if (char === '}') {
|
|
1248
|
+
depth -= 1;
|
|
1249
|
+
if (depth === 0) {
|
|
1250
|
+
return index;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return undefined;
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const splitDestructuringBindings = (source: string): readonly string[] => {
|
|
1258
|
+
const bindings: string[] = [];
|
|
1259
|
+
let start = 0;
|
|
1260
|
+
let depth = 0;
|
|
1261
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
1262
|
+
const skippedIndex = skipImportScanIgnoredToken(source, index);
|
|
1263
|
+
if (skippedIndex !== undefined) {
|
|
1264
|
+
index = skippedIndex - 1;
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
const char = source[index];
|
|
1268
|
+
if (char === '{' || char === '[' || char === '(') {
|
|
1269
|
+
depth += 1;
|
|
1270
|
+
} else if (char === '}' || char === ']' || char === ')') {
|
|
1271
|
+
depth = Math.max(0, depth - 1);
|
|
1272
|
+
} else if (char === ',' && depth === 0) {
|
|
1273
|
+
bindings.push(source.slice(start, index));
|
|
1274
|
+
start = index + 1;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
bindings.push(source.slice(start));
|
|
1278
|
+
return bindings;
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
const dynamicImportNamedBindings = (
|
|
1282
|
+
source: string,
|
|
1283
|
+
specifier: string
|
|
1284
|
+
): readonly DynamicNamedImportBinding[] => {
|
|
1285
|
+
const code = maskSource(source, { strings: false });
|
|
1286
|
+
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
1287
|
+
const escapedSpecifier = escapeRegExp(specifier);
|
|
1288
|
+
const bindings: DynamicNamedImportBinding[] = [];
|
|
1289
|
+
const declarationPattern = /\bconst\s*\{/gu;
|
|
1290
|
+
const assignmentPattern = new RegExp(
|
|
1291
|
+
`^\\s*(?::\\s*typeof\\s+import\\s*\\(\\s*['"]${escapedSpecifier}['"]\\s*\\))?\\s*=\\s*(?:await\\s+)?import\\s*\\(\\s*['"]${escapedSpecifier}['"]\\s*\\)`,
|
|
1292
|
+
'u'
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
for (const match of code.matchAll(declarationPattern)) {
|
|
1296
|
+
if (!matchStartsWithAnyKeyword(stringsMaskedCode, match, ['const'])) {
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const openingBrace = (match.index ?? 0) + match[0].lastIndexOf('{');
|
|
1301
|
+
const closingBrace = closingDestructuringBrace(code, openingBrace);
|
|
1302
|
+
if (
|
|
1303
|
+
closingBrace === undefined ||
|
|
1304
|
+
!assignmentPattern.test(code.slice(closingBrace + 1))
|
|
1305
|
+
) {
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const namedImports = code.slice(openingBrace + 1, closingBrace);
|
|
1310
|
+
for (const item of splitDestructuringBindings(namedImports)) {
|
|
1311
|
+
const specifierText = item.trim();
|
|
1312
|
+
if (specifierText.length === 0 || specifierText.startsWith('...')) {
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const imported =
|
|
1317
|
+
/^(?<imported>[A-Za-z_$][\w$]*)(?:\s*:\s*(?<local>[A-Za-z_$][\w$]*))?(?:\s*=.*)?$/u.exec(
|
|
1318
|
+
specifierText
|
|
1319
|
+
)?.groups;
|
|
1320
|
+
if (imported?.['imported']) {
|
|
1321
|
+
bindings.push({
|
|
1322
|
+
imported: imported['imported'],
|
|
1323
|
+
local: imported['local'] ?? imported['imported'],
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return bindings;
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
const dynamicImportNamedLocals = (
|
|
1333
|
+
source: string,
|
|
1334
|
+
specifier: string,
|
|
1335
|
+
exportedName: string
|
|
1336
|
+
): readonly string[] =>
|
|
1337
|
+
dynamicImportNamedBindings(source, specifier)
|
|
1338
|
+
.filter((binding) => binding.imported === exportedName)
|
|
1339
|
+
.map((binding) => binding.local);
|
|
1340
|
+
|
|
1341
|
+
interface LocalValueExport {
|
|
1342
|
+
readonly identifier: string;
|
|
1343
|
+
readonly sourcePath: string;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
interface LocalReexport {
|
|
1347
|
+
readonly identifier: string;
|
|
1348
|
+
readonly specifier: string;
|
|
1349
|
+
readonly typeOnly: boolean;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
interface LocalImport {
|
|
1353
|
+
readonly identifier: string;
|
|
1354
|
+
readonly specifier: string;
|
|
1355
|
+
readonly typeOnly: boolean;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
interface NamedExportItem {
|
|
1359
|
+
readonly local: string;
|
|
1360
|
+
readonly name: string;
|
|
1361
|
+
readonly typeOnly: boolean;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const parseNamedExportItem = (
|
|
1365
|
+
item: string,
|
|
1366
|
+
declarationTypeOnly: boolean
|
|
1367
|
+
): NamedExportItem | undefined => {
|
|
1368
|
+
const trimmedItem = item.trim();
|
|
1369
|
+
if (!trimmedItem) {
|
|
1370
|
+
return undefined;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const itemTypeOnly = declarationTypeOnly || trimmedItem.startsWith('type ');
|
|
1374
|
+
const specifierText = trimmedItem.replace(/^type\s+/u, '');
|
|
1375
|
+
const exported =
|
|
1376
|
+
/^(?<local>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<name>[A-Za-z_$][\w$]*))?$/u.exec(
|
|
1377
|
+
specifierText
|
|
1378
|
+
)?.groups;
|
|
1379
|
+
if (!exported?.['local']) {
|
|
1380
|
+
return undefined;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
return {
|
|
1384
|
+
local: exported['local'],
|
|
1385
|
+
name: exported['name'] ?? exported['local'],
|
|
1386
|
+
typeOnly: itemTypeOnly,
|
|
1387
|
+
};
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
const declaresValueBinding = (source: string, identifier: string): boolean => {
|
|
1391
|
+
const code = maskSource(source, { strings: true });
|
|
1392
|
+
const escapedIdentifier = escapeRegExp(identifier);
|
|
1393
|
+
return new RegExp(
|
|
1394
|
+
`\\b(?:export\\s+)?(?:(?:async\\s+)?function|const|let|var|class|enum)\\s+${escapedIdentifier}\\b`,
|
|
1395
|
+
'u'
|
|
1396
|
+
).test(code);
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
const declaresValueExport = (source: string, identifier: string): boolean => {
|
|
1400
|
+
const code = maskSource(source, { strings: true });
|
|
1401
|
+
const escapedIdentifier = escapeRegExp(identifier);
|
|
1402
|
+
return new RegExp(
|
|
1403
|
+
`\\bexport\\s+(?:(?:async\\s+)?function|const|let|var|class|enum)\\s+${escapedIdentifier}\\b`,
|
|
1404
|
+
'u'
|
|
1405
|
+
).test(code);
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
const sameFileValueExportLocal = (
|
|
1409
|
+
source: string,
|
|
1410
|
+
identifier: string
|
|
1411
|
+
): string | undefined => {
|
|
1412
|
+
const code = maskSource(source, { strings: true });
|
|
1413
|
+
const pattern =
|
|
1414
|
+
/\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}(?!\s+from\b)/gu;
|
|
1415
|
+
|
|
1416
|
+
for (const match of code.matchAll(pattern)) {
|
|
1417
|
+
const declarationTypeOnly = Boolean(match.groups?.['typeOnly']);
|
|
1418
|
+
const namedExports = match.groups?.['exports'] ?? '';
|
|
1419
|
+
for (const item of namedExports.split(',')) {
|
|
1420
|
+
const exported = parseNamedExportItem(item, declarationTypeOnly);
|
|
1421
|
+
if (!exported || exported.typeOnly || exported.name !== identifier) {
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return exported.local;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
return undefined;
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
const namedLocalImports = (
|
|
1433
|
+
source: string,
|
|
1434
|
+
identifier: string
|
|
1435
|
+
): readonly LocalImport[] => {
|
|
1436
|
+
const code = maskSource(source, { strings: false });
|
|
1437
|
+
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
1438
|
+
const imports: LocalImport[] = [];
|
|
1439
|
+
const pattern =
|
|
1440
|
+
/\bimport\s+(?<typeOnly>type\s+)?\{(?<imports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
|
|
1441
|
+
|
|
1442
|
+
for (const match of code.matchAll(pattern)) {
|
|
1443
|
+
if (!stringsMaskedCode.startsWith('import', match.index ?? 0)) {
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const specifier = match.groups?.['specifier'];
|
|
1448
|
+
if (!specifier?.startsWith('.')) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const namedImports = match.groups?.['imports'] ?? '';
|
|
1453
|
+
for (const item of namedImports.split(',')) {
|
|
1454
|
+
const trimmedItem = item.trim();
|
|
1455
|
+
if (!trimmedItem) {
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const itemTypeOnly =
|
|
1460
|
+
Boolean(match.groups?.['typeOnly']) || trimmedItem.startsWith('type ');
|
|
1461
|
+
const specifierText = trimmedItem.replace(/^type\s+/u, '');
|
|
1462
|
+
const imported =
|
|
1463
|
+
/^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
|
|
1464
|
+
specifierText
|
|
1465
|
+
)?.groups;
|
|
1466
|
+
if (
|
|
1467
|
+
!imported?.['imported'] ||
|
|
1468
|
+
(imported['local'] ?? imported['imported']) !== identifier
|
|
1469
|
+
) {
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
imports.push({
|
|
1474
|
+
identifier: imported['imported'],
|
|
1475
|
+
specifier,
|
|
1476
|
+
typeOnly: itemTypeOnly,
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return imports;
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
const namedLocalReexports = (
|
|
1485
|
+
source: string,
|
|
1486
|
+
identifier: string
|
|
1487
|
+
): readonly LocalReexport[] => {
|
|
1488
|
+
const code = maskSource(source, { strings: false });
|
|
1489
|
+
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
1490
|
+
const exports: LocalReexport[] = [];
|
|
1491
|
+
const pattern =
|
|
1492
|
+
/\bexport\s+(?<typeOnly>type\s+)?\{(?<exports>[\s\S]*?)\}\s+from\s+['"](?<specifier>[^'"]+)['"]/gu;
|
|
1493
|
+
|
|
1494
|
+
for (const match of code.matchAll(pattern)) {
|
|
1495
|
+
if (!matchStartsWithAnyKeyword(stringsMaskedCode, match, ['export'])) {
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const specifier = match.groups?.['specifier'];
|
|
1500
|
+
if (!specifier?.startsWith('.')) {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const namedExports = match.groups?.['exports'] ?? '';
|
|
1505
|
+
for (const item of namedExports.split(',')) {
|
|
1506
|
+
const exported = parseNamedExportItem(
|
|
1507
|
+
item,
|
|
1508
|
+
Boolean(match.groups?.['typeOnly'])
|
|
1509
|
+
);
|
|
1510
|
+
if (!exported || exported.name !== identifier) {
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
exports.push({
|
|
1515
|
+
identifier: exported.local,
|
|
1516
|
+
specifier,
|
|
1517
|
+
typeOnly: exported.typeOnly,
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return exports;
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
const starLocalReexports = (source: string): readonly LocalReexport[] => {
|
|
1526
|
+
const code = maskSource(source, { strings: false });
|
|
1527
|
+
const stringsMaskedCode = maskSource(source, { strings: true });
|
|
1528
|
+
return [
|
|
1529
|
+
...code.matchAll(
|
|
1530
|
+
/\bexport\s+(?<typeOnly>type\s+)?\*\s+from\s+['"](?<specifier>[^'"]+)['"]/gu
|
|
1531
|
+
),
|
|
1532
|
+
]
|
|
1533
|
+
.filter((match) =>
|
|
1534
|
+
matchStartsWithAnyKeyword(stringsMaskedCode, match, ['export'])
|
|
1535
|
+
)
|
|
1536
|
+
.map((match) => ({
|
|
1537
|
+
identifier: '',
|
|
1538
|
+
specifier: match.groups?.['specifier'] ?? '',
|
|
1539
|
+
typeOnly: Boolean(match.groups?.['typeOnly']),
|
|
1540
|
+
}))
|
|
1541
|
+
.filter((entry) => entry.specifier.startsWith('.'));
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
const resolveLocalModuleSpecifier = (
|
|
1545
|
+
sourcePath: string,
|
|
1546
|
+
specifier: string
|
|
1547
|
+
): string | undefined => {
|
|
1548
|
+
const basePath = resolve(dirname(sourcePath), specifier);
|
|
1549
|
+
const candidates = [
|
|
1550
|
+
basePath,
|
|
1551
|
+
basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.ts` : undefined,
|
|
1552
|
+
basePath.endsWith('.js') ? `${basePath.slice(0, -3)}.tsx` : undefined,
|
|
1553
|
+
basePath.endsWith('.mjs') ? `${basePath.slice(0, -4)}.mts` : undefined,
|
|
1554
|
+
`${basePath}.ts`,
|
|
1555
|
+
`${basePath}.tsx`,
|
|
1556
|
+
join(basePath, 'index.ts'),
|
|
1557
|
+
join(basePath, 'index.tsx'),
|
|
1558
|
+
].filter((candidate): candidate is string => candidate !== undefined);
|
|
1559
|
+
|
|
1560
|
+
return candidates.find((candidate) => existsSync(candidate));
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
const resolveLocalValueExport = (
|
|
1564
|
+
sourcePath: string,
|
|
1565
|
+
identifier: string,
|
|
1566
|
+
visited = new Set<string>()
|
|
1567
|
+
): LocalValueExport | undefined => {
|
|
1568
|
+
const normalizedSourcePath = normalizeRealPath(sourcePath);
|
|
1569
|
+
const visitKey = `${normalizedSourcePath}:${identifier}`;
|
|
1570
|
+
if (visited.has(visitKey)) {
|
|
1571
|
+
return undefined;
|
|
1572
|
+
}
|
|
1573
|
+
visited.add(visitKey);
|
|
1574
|
+
|
|
1575
|
+
let source: string;
|
|
1576
|
+
try {
|
|
1577
|
+
source = readFileSync(normalizedSourcePath, 'utf8');
|
|
1578
|
+
} catch {
|
|
1579
|
+
return undefined;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (declaresValueExport(source, identifier)) {
|
|
1583
|
+
return { identifier, sourcePath: normalizedSourcePath };
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
const sameFileLocal = sameFileValueExportLocal(source, identifier);
|
|
1587
|
+
if (sameFileLocal && declaresValueBinding(source, sameFileLocal)) {
|
|
1588
|
+
return { identifier: sameFileLocal, sourcePath: normalizedSourcePath };
|
|
1589
|
+
}
|
|
1590
|
+
if (sameFileLocal) {
|
|
1591
|
+
for (const localImport of namedLocalImports(source, sameFileLocal)) {
|
|
1592
|
+
if (localImport.typeOnly) {
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const targetPath = resolveLocalModuleSpecifier(
|
|
1596
|
+
normalizedSourcePath,
|
|
1597
|
+
localImport.specifier
|
|
1598
|
+
);
|
|
1599
|
+
const resolved =
|
|
1600
|
+
targetPath &&
|
|
1601
|
+
resolveLocalValueExport(targetPath, localImport.identifier, visited);
|
|
1602
|
+
if (resolved) {
|
|
1603
|
+
return resolved;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
for (const reexport of namedLocalReexports(source, identifier)) {
|
|
1609
|
+
if (reexport.typeOnly) {
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
const targetPath = resolveLocalModuleSpecifier(
|
|
1613
|
+
normalizedSourcePath,
|
|
1614
|
+
reexport.specifier
|
|
1615
|
+
);
|
|
1616
|
+
const resolved =
|
|
1617
|
+
targetPath &&
|
|
1618
|
+
resolveLocalValueExport(targetPath, reexport.identifier, visited);
|
|
1619
|
+
if (resolved) {
|
|
1620
|
+
return resolved;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
for (const reexport of starLocalReexports(source)) {
|
|
1625
|
+
if (reexport.typeOnly) {
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
const targetPath = resolveLocalModuleSpecifier(
|
|
1629
|
+
normalizedSourcePath,
|
|
1630
|
+
reexport.specifier
|
|
1631
|
+
);
|
|
1632
|
+
const resolved =
|
|
1633
|
+
targetPath && resolveLocalValueExport(targetPath, identifier, visited);
|
|
1634
|
+
if (resolved) {
|
|
1635
|
+
return resolved;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
return undefined;
|
|
1640
|
+
};
|
|
1641
|
+
|
|
1642
|
+
const findClosingParen = (source: string, openIndex: number): number => {
|
|
1643
|
+
let depth = 0;
|
|
1644
|
+
for (let index = openIndex; index < source.length; index += 1) {
|
|
1645
|
+
if (source[index] === '(') {
|
|
1646
|
+
depth += 1;
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
if (source[index] !== ')') {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
depth -= 1;
|
|
1653
|
+
if (depth === 0) {
|
|
1654
|
+
return index;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
return -1;
|
|
1659
|
+
};
|
|
1660
|
+
|
|
1661
|
+
const previousNonWhitespace = (source: string, index: number): string => {
|
|
1662
|
+
let previousIndex = index - 1;
|
|
1663
|
+
while (previousIndex >= 0 && /\s/u.test(source[previousIndex] ?? '')) {
|
|
1664
|
+
previousIndex -= 1;
|
|
1665
|
+
}
|
|
1666
|
+
return source[previousIndex] ?? '';
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
const containsCall = (source: string, identifier: string): boolean => {
|
|
1670
|
+
const escapedIdentifier = escapeRegExp(identifier);
|
|
1671
|
+
const callPattern = new RegExp(`${escapedIdentifier}\\s*\\(`, 'gu');
|
|
1672
|
+
for (const match of source.matchAll(callPattern)) {
|
|
1673
|
+
const index = match.index ?? 0;
|
|
1674
|
+
if (
|
|
1675
|
+
!isIdentifierChar(source[index - 1]) &&
|
|
1676
|
+
previousNonWhitespace(source, index) !== '.'
|
|
1677
|
+
) {
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return false;
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
const promiseMethodNames = new Set(['then', 'catch', 'finally']);
|
|
1685
|
+
|
|
1686
|
+
const inlineDynamicImportMemberIsCalled = (
|
|
1687
|
+
source: string,
|
|
1688
|
+
specifier: string
|
|
1689
|
+
): boolean => {
|
|
1690
|
+
const searchableCode = maskSource(source, { strings: false });
|
|
1691
|
+
const codePositions = maskSource(source, { strings: true });
|
|
1692
|
+
const escapedSpecifier = escapeRegExp(specifier);
|
|
1693
|
+
const pattern = new RegExp(
|
|
1694
|
+
`\\(\\s*await\\s+import\\s*\\(\\s*['"]${escapedSpecifier}['"]\\s*\\)\\s*\\)\\s*\\.\\s*(?<member>[A-Za-z_$][\\w$]*)\\s*\\(`,
|
|
1695
|
+
'gu'
|
|
1696
|
+
);
|
|
1697
|
+
return [...searchableCode.matchAll(pattern)].some(
|
|
1698
|
+
(match) =>
|
|
1699
|
+
codePositions[match.index ?? 0] !== ' ' &&
|
|
1700
|
+
!promiseMethodNames.has(match.groups?.['member'] ?? '')
|
|
1701
|
+
);
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
interface RuntimeImportBindings {
|
|
1705
|
+
readonly direct: readonly string[];
|
|
1706
|
+
readonly namespaces: readonly string[];
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
const staticRuntimeImportBindings = (
|
|
1710
|
+
source: string,
|
|
1711
|
+
specifier: string
|
|
1712
|
+
): RuntimeImportBindings => {
|
|
1713
|
+
const direct = new Set<string>();
|
|
1714
|
+
const namespaces = new Set<string>();
|
|
1715
|
+
for (const clause of staticImportClausesForSpecifier(source, specifier)) {
|
|
1716
|
+
const importCode = maskSource(clause, { strings: false });
|
|
1717
|
+
const namespaceBinding =
|
|
1718
|
+
/(?:^|,)\s*\*\s+as\s+(?<local>[A-Za-z_$][\w$]*)(?:\s*$|,)/u.exec(
|
|
1719
|
+
importCode
|
|
1720
|
+
)?.groups?.['local'];
|
|
1721
|
+
if (namespaceBinding) {
|
|
1722
|
+
namespaces.add(namespaceBinding);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const namedImports = /\{(?<imports>[\s\S]*?)\}/u.exec(importCode)?.groups?.[
|
|
1726
|
+
'imports'
|
|
1727
|
+
];
|
|
1728
|
+
for (const item of namedImports?.split(',') ?? []) {
|
|
1729
|
+
const trimmedItem = item.trim();
|
|
1730
|
+
const imported =
|
|
1731
|
+
/^(?<imported>[A-Za-z_$][\w$]*)(?:\s+as\s+(?<local>[A-Za-z_$][\w$]*))?$/u.exec(
|
|
1732
|
+
trimmedItem.replace(/^type\s+/u, '')
|
|
1733
|
+
)?.groups;
|
|
1734
|
+
if (!trimmedItem.startsWith('type ') && imported?.['imported']) {
|
|
1735
|
+
direct.add(imported['local'] ?? imported['imported']);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
const defaultBinding = /^(?<local>[A-Za-z_$][\w$]*)(?:\s*,|\s*$)/u.exec(
|
|
1740
|
+
importCode.trim()
|
|
1741
|
+
)?.groups?.['local'];
|
|
1742
|
+
if (defaultBinding) {
|
|
1743
|
+
direct.add(defaultBinding);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
return { direct: [...direct], namespaces: [...namespaces] };
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
interface RunnerCall {
|
|
1751
|
+
readonly arguments: readonly string[];
|
|
1752
|
+
readonly argumentNodes: readonly AstNode[];
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
const namespacedCalleeName = (
|
|
1756
|
+
callee: AstNode
|
|
1757
|
+
): { readonly property: string; readonly receiver: string } | undefined => {
|
|
1758
|
+
if (
|
|
1759
|
+
(callee.type !== 'MemberExpression' &&
|
|
1760
|
+
callee.type !== 'StaticMemberExpression') ||
|
|
1761
|
+
callee['computed'] === true
|
|
1762
|
+
) {
|
|
1763
|
+
return undefined;
|
|
1764
|
+
}
|
|
1765
|
+
const receiver = identifierName(callee['object'] as AstNode | undefined);
|
|
1766
|
+
const property = identifierName(callee['property'] as AstNode | undefined);
|
|
1767
|
+
return receiver && property ? { property, receiver } : undefined;
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
interface ProvenBindingCall {
|
|
1771
|
+
readonly arguments: readonly AstNode[];
|
|
1772
|
+
readonly end: number;
|
|
1773
|
+
readonly start: number;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
const provenBindingCalls = (
|
|
1777
|
+
ast: AstNode,
|
|
1778
|
+
binding: string
|
|
1779
|
+
): readonly ProvenBindingCall[] => {
|
|
1780
|
+
const calls: ProvenBindingCall[] = [];
|
|
1781
|
+
walkWithScopes(ast, (node, scopes) => {
|
|
1782
|
+
if (node.type !== 'CallExpression') {
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
const callee = node['callee'] as AstNode | undefined;
|
|
1786
|
+
if (!callee) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const member = namespacedCalleeName(callee);
|
|
1791
|
+
const bare = identifierName(callee);
|
|
1792
|
+
const matches = member
|
|
1793
|
+
? `${member.receiver}.${member.property}` === binding &&
|
|
1794
|
+
!isShadowed(member.receiver, scopes)
|
|
1795
|
+
: bare === binding && !isShadowed(binding, scopes);
|
|
1796
|
+
if (!matches) {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
calls.push({
|
|
1801
|
+
arguments:
|
|
1802
|
+
(node as unknown as { arguments?: readonly AstNode[] }).arguments ?? [],
|
|
1803
|
+
end: node.end,
|
|
1804
|
+
start: node.start,
|
|
1805
|
+
});
|
|
1806
|
+
});
|
|
1807
|
+
return calls;
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
const provenNamespaceIsCalled = (ast: AstNode, binding: string): boolean => {
|
|
1811
|
+
let called = false;
|
|
1812
|
+
walkWithScopes(ast, (node, scopes) => {
|
|
1813
|
+
if (called || node.type !== 'CallExpression') {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
const callee = node['callee'] as AstNode | undefined;
|
|
1817
|
+
const member = callee && namespacedCalleeName(callee);
|
|
1818
|
+
if (
|
|
1819
|
+
member?.receiver === binding &&
|
|
1820
|
+
!promiseMethodNames.has(member.property) &&
|
|
1821
|
+
!isShadowed(binding, scopes)
|
|
1822
|
+
) {
|
|
1823
|
+
called = true;
|
|
1824
|
+
}
|
|
1825
|
+
});
|
|
1826
|
+
return called;
|
|
1827
|
+
};
|
|
1828
|
+
|
|
1829
|
+
const runnerCallArguments = (
|
|
1830
|
+
source: string,
|
|
1831
|
+
runner: string,
|
|
1832
|
+
ast: AstNode
|
|
1833
|
+
): readonly RunnerCall[] =>
|
|
1834
|
+
provenBindingCalls(ast, runner).map((call) => ({
|
|
1835
|
+
argumentNodes: call.arguments,
|
|
1836
|
+
arguments: call.arguments.map((argument) =>
|
|
1837
|
+
source.slice(argument.start, argument.end)
|
|
1838
|
+
),
|
|
1839
|
+
}));
|
|
1840
|
+
|
|
1841
|
+
const argumentContainsProvenCall = (
|
|
1842
|
+
argument: AstNode | undefined,
|
|
1843
|
+
ast: AstNode,
|
|
1844
|
+
binding: string
|
|
1845
|
+
): boolean =>
|
|
1846
|
+
argument !== undefined &&
|
|
1847
|
+
provenBindingCalls(ast, binding).some(
|
|
1848
|
+
(call) => call.start >= argument.start && call.end <= argument.end
|
|
1849
|
+
);
|
|
1850
|
+
|
|
1851
|
+
const importedRuntimeBindingIsCalled = (
|
|
1852
|
+
source: string,
|
|
1853
|
+
specifier: string,
|
|
1854
|
+
ast: AstNode
|
|
1855
|
+
): boolean => {
|
|
1856
|
+
const staticBindings = staticRuntimeImportBindings(source, specifier);
|
|
1857
|
+
const directBindings = new Set([
|
|
1858
|
+
...staticBindings.direct,
|
|
1859
|
+
...dynamicImportNamedBindings(source, specifier).map(
|
|
1860
|
+
(binding) => binding.local
|
|
1861
|
+
),
|
|
1862
|
+
]);
|
|
1863
|
+
if (
|
|
1864
|
+
[...directBindings].some(
|
|
1865
|
+
(binding) => provenBindingCalls(ast, binding).length > 0
|
|
1866
|
+
)
|
|
1867
|
+
) {
|
|
1868
|
+
return true;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
const namespaceBindings = new Set([
|
|
1872
|
+
...staticBindings.namespaces,
|
|
1873
|
+
...dynamicImportNamespaceBindings(source, specifier),
|
|
1874
|
+
]);
|
|
1875
|
+
for (const alias of topLevelNamespaceAliases(ast, namespaceBindings)) {
|
|
1876
|
+
namespaceBindings.add(alias);
|
|
1877
|
+
}
|
|
1878
|
+
return (
|
|
1879
|
+
[...namespaceBindings].some((binding) =>
|
|
1880
|
+
provenNamespaceIsCalled(ast, binding)
|
|
1881
|
+
) || inlineDynamicImportMemberIsCalled(source, specifier)
|
|
1882
|
+
);
|
|
1883
|
+
};
|
|
1884
|
+
|
|
1885
|
+
const isSentinelAdapterArgument = (argument: string): boolean =>
|
|
1886
|
+
/^(?:undefined|null|void\s*(?:0|\(0\)))$/u.test(argument.trim());
|
|
1887
|
+
|
|
1888
|
+
const runnerInvokesCasesFactory = (
|
|
1889
|
+
source: string,
|
|
1890
|
+
ast: AstNode,
|
|
1891
|
+
runner: string,
|
|
1892
|
+
casesFactory: string
|
|
1893
|
+
): boolean =>
|
|
1894
|
+
runnerCallArguments(source, runner, ast).some((call) => {
|
|
1895
|
+
const args = call.arguments;
|
|
1896
|
+
const adapterArgument = args[0]?.trim();
|
|
1897
|
+
return (
|
|
1898
|
+
adapterArgument !== undefined &&
|
|
1899
|
+
adapterArgument.length > 0 &&
|
|
1900
|
+
!isSentinelAdapterArgument(adapterArgument) &&
|
|
1901
|
+
!argumentContainsProvenCall(call.argumentNodes[0], ast, casesFactory) &&
|
|
1902
|
+
call.argumentNodes
|
|
1903
|
+
.slice(1)
|
|
1904
|
+
.some((argument) =>
|
|
1905
|
+
argumentContainsProvenCall(argument, ast, casesFactory)
|
|
1906
|
+
)
|
|
1907
|
+
);
|
|
1908
|
+
});
|
|
1909
|
+
|
|
1910
|
+
const runnerInvokedWithAdapterArgument = (
|
|
1911
|
+
source: string,
|
|
1912
|
+
ast: AstNode,
|
|
1913
|
+
runner: string,
|
|
1914
|
+
casesFactories: readonly string[] = []
|
|
1915
|
+
): boolean =>
|
|
1916
|
+
runnerCallArguments(source, runner, ast).some((call) => {
|
|
1917
|
+
const args = call.arguments;
|
|
1918
|
+
const adapterArgument = args[0]?.trim();
|
|
1919
|
+
return (
|
|
1920
|
+
adapterArgument !== undefined &&
|
|
1921
|
+
adapterArgument.length > 0 &&
|
|
1922
|
+
!isSentinelAdapterArgument(adapterArgument) &&
|
|
1923
|
+
casesFactories.every(
|
|
1924
|
+
(casesFactory) =>
|
|
1925
|
+
!argumentContainsProvenCall(call.argumentNodes[0], ast, casesFactory)
|
|
1926
|
+
)
|
|
1927
|
+
);
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
const ownerRunnerDefaultsCasesFactory = (
|
|
1931
|
+
targetEntry: AdapterTargetCatalogEntry
|
|
1932
|
+
): boolean => {
|
|
1933
|
+
const { conformance, testingExportTarget } = targetEntry;
|
|
1934
|
+
if (!conformance || !testingExportTarget) {
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
const runnerExport = resolveLocalValueExport(
|
|
1939
|
+
testingExportTarget,
|
|
1940
|
+
conformance.runner
|
|
1941
|
+
);
|
|
1942
|
+
if (!runnerExport) {
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
const casesFactoryExport = resolveLocalValueExport(
|
|
1946
|
+
testingExportTarget,
|
|
1947
|
+
conformance.casesFactory
|
|
1948
|
+
);
|
|
1949
|
+
const casesFactoryIdentifiers = [
|
|
1950
|
+
conformance.casesFactory,
|
|
1951
|
+
casesFactoryExport?.identifier,
|
|
1952
|
+
].filter((identifier): identifier is string => identifier !== undefined);
|
|
1953
|
+
|
|
1954
|
+
let source: string;
|
|
1955
|
+
try {
|
|
1956
|
+
source = readFileSync(runnerExport.sourcePath, 'utf8');
|
|
1957
|
+
} catch {
|
|
1958
|
+
return false;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
const code = maskSource(source, { strings: true });
|
|
1962
|
+
const escapedRunner = escapeRegExp(runnerExport.identifier);
|
|
1963
|
+
const declarationPatterns = [
|
|
1964
|
+
new RegExp(
|
|
1965
|
+
`\\b(?:export\\s+)?(?:async\\s+)?function\\s+${escapedRunner}\\b`,
|
|
1966
|
+
'gu'
|
|
1967
|
+
),
|
|
1968
|
+
new RegExp(
|
|
1969
|
+
`\\b(?:export\\s+)?(?:const|let|var)\\s+${escapedRunner}\\b\\s*(?::[^=;]*)?=`,
|
|
1970
|
+
'gu'
|
|
1971
|
+
),
|
|
1972
|
+
];
|
|
1973
|
+
|
|
1974
|
+
for (const pattern of declarationPatterns) {
|
|
1975
|
+
for (const match of code.matchAll(pattern)) {
|
|
1976
|
+
const openIndex = code.indexOf('(', (match.index ?? 0) + match[0].length);
|
|
1977
|
+
if (openIndex === -1) {
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
const closeIndex = findClosingParen(code, openIndex);
|
|
1981
|
+
if (closeIndex === -1) {
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const params = code.slice(openIndex + 1, closeIndex);
|
|
1986
|
+
if (
|
|
1987
|
+
params.includes('=') &&
|
|
1988
|
+
casesFactoryIdentifiers.some((identifier) =>
|
|
1989
|
+
containsCall(params, identifier)
|
|
1990
|
+
)
|
|
1991
|
+
) {
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
return false;
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
const runnerBindingProvesConformance = (
|
|
2001
|
+
source: string,
|
|
2002
|
+
ast: AstNode,
|
|
2003
|
+
targetEntry: AdapterTargetCatalogEntry,
|
|
2004
|
+
runnerBinding: string,
|
|
2005
|
+
casesFactoryBindings: readonly string[]
|
|
2006
|
+
): boolean => {
|
|
2007
|
+
for (const casesFactoryBinding of casesFactoryBindings) {
|
|
2008
|
+
if (
|
|
2009
|
+
runnerInvokesCasesFactory(source, ast, runnerBinding, casesFactoryBinding)
|
|
2010
|
+
) {
|
|
2011
|
+
return true;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
return (
|
|
2016
|
+
ownerRunnerDefaultsCasesFactory(targetEntry) &&
|
|
2017
|
+
runnerInvokedWithAdapterArgument(
|
|
2018
|
+
source,
|
|
2019
|
+
ast,
|
|
2020
|
+
runnerBinding,
|
|
2021
|
+
casesFactoryBindings
|
|
2022
|
+
)
|
|
2023
|
+
);
|
|
2024
|
+
};
|
|
2025
|
+
|
|
2026
|
+
const provesConformance = (
|
|
2027
|
+
source: string,
|
|
2028
|
+
targetEntry: AdapterTargetCatalogEntry
|
|
2029
|
+
): boolean => {
|
|
2030
|
+
const { conformance, testingImport } = targetEntry;
|
|
2031
|
+
if (
|
|
2032
|
+
!testingImport ||
|
|
2033
|
+
!importsSpecifier(source, testingImport, {
|
|
2034
|
+
includeReExports: false,
|
|
2035
|
+
requireImportBinding: true,
|
|
2036
|
+
})
|
|
2037
|
+
) {
|
|
2038
|
+
return false;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const ast = parse('adapter-conformance.ts', source);
|
|
2042
|
+
if (!ast) {
|
|
2043
|
+
return false;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (!conformance) {
|
|
2047
|
+
return importedRuntimeBindingIsCalled(source, testingImport, ast);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
const runnerBindings = new Set(
|
|
2051
|
+
namedImportBindings(source, testingImport, conformance.runner)
|
|
2052
|
+
);
|
|
2053
|
+
const casesFactoryBindings = new Set(
|
|
2054
|
+
namedImportBindings(source, testingImport, conformance.casesFactory)
|
|
2055
|
+
);
|
|
2056
|
+
const dynamicRunnerBindings = dynamicImportNamedLocals(
|
|
2057
|
+
source,
|
|
2058
|
+
testingImport,
|
|
2059
|
+
conformance.runner
|
|
2060
|
+
);
|
|
2061
|
+
for (const dynamicRunnerBinding of dynamicRunnerBindings) {
|
|
2062
|
+
runnerBindings.add(dynamicRunnerBinding);
|
|
2063
|
+
}
|
|
2064
|
+
const dynamicCasesFactoryBindings = dynamicImportNamedLocals(
|
|
2065
|
+
source,
|
|
2066
|
+
testingImport,
|
|
2067
|
+
conformance.casesFactory
|
|
2068
|
+
);
|
|
2069
|
+
for (const dynamicCasesFactoryBinding of dynamicCasesFactoryBindings) {
|
|
2070
|
+
casesFactoryBindings.add(dynamicCasesFactoryBinding);
|
|
2071
|
+
}
|
|
2072
|
+
for (const namespaceBinding of dynamicImportNamespaceBindings(
|
|
2073
|
+
source,
|
|
2074
|
+
testingImport
|
|
2075
|
+
)) {
|
|
2076
|
+
runnerBindings.add(`${namespaceBinding}.${conformance.runner}`);
|
|
2077
|
+
casesFactoryBindings.add(`${namespaceBinding}.${conformance.casesFactory}`);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
const namespaceBindings = new Set(
|
|
2081
|
+
[...runnerBindings]
|
|
2082
|
+
.filter((binding) => binding.endsWith(`.${conformance.runner}`))
|
|
2083
|
+
.map((binding) => binding.slice(0, -conformance.runner.length - 1))
|
|
2084
|
+
);
|
|
2085
|
+
for (const alias of topLevelNamespaceAliases(ast, namespaceBindings)) {
|
|
2086
|
+
runnerBindings.add(`${alias}.${conformance.runner}`);
|
|
2087
|
+
casesFactoryBindings.add(`${alias}.${conformance.casesFactory}`);
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
const allCasesFactoryBindings = [...casesFactoryBindings];
|
|
2091
|
+
return [...runnerBindings].some((runnerBinding) =>
|
|
2092
|
+
runnerBindingProvesConformance(
|
|
2093
|
+
source,
|
|
2094
|
+
ast,
|
|
2095
|
+
targetEntry,
|
|
2096
|
+
runnerBinding,
|
|
2097
|
+
allCasesFactoryBindings
|
|
2098
|
+
)
|
|
2099
|
+
);
|
|
2100
|
+
};
|
|
2101
|
+
|
|
2102
|
+
const pathsProvingConformance = (
|
|
2103
|
+
sourceFiles: readonly string[],
|
|
2104
|
+
targetEntry: AdapterTargetCatalogEntry
|
|
2105
|
+
): readonly string[] =>
|
|
2106
|
+
sourceFiles.filter((filePath) =>
|
|
2107
|
+
provesConformance(readFileSync(filePath, 'utf8'), targetEntry)
|
|
2108
|
+
);
|
|
2109
|
+
|
|
2110
|
+
const isTestFile = (filePath: string): boolean => {
|
|
2111
|
+
const normalizedPath = normalizePath(filePath);
|
|
2112
|
+
return (
|
|
2113
|
+
normalizedPath.includes('/__tests__/') ||
|
|
2114
|
+
normalizedPath.endsWith('.test.ts') ||
|
|
2115
|
+
normalizedPath.endsWith('.test-d.ts')
|
|
2116
|
+
);
|
|
2117
|
+
};
|
|
2118
|
+
|
|
2119
|
+
const assertPackageExports = (
|
|
2120
|
+
workspace: WorkspacePackage,
|
|
2121
|
+
diagnostics: AdapterCheckDiagnostic[]
|
|
2122
|
+
): void => {
|
|
2123
|
+
const packageName = workspace.manifest.name as string;
|
|
2124
|
+
for (const key of ['.', './package.json'] as const) {
|
|
2125
|
+
if (hasResolvableExport(workspace, key)) {
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
diagnostics.push(
|
|
2129
|
+
diagnostic(
|
|
2130
|
+
workspace.packageJsonPath,
|
|
2131
|
+
packageName,
|
|
2132
|
+
'missing-package-export',
|
|
2133
|
+
`${packageName} must export "${key}" so adapter kit and consumers can resolve it.`
|
|
2134
|
+
)
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
|
|
2139
|
+
const assertDependencyDirection = (
|
|
2140
|
+
workspace: WorkspacePackage,
|
|
2141
|
+
targetEntry: AdapterTargetCatalogEntry,
|
|
2142
|
+
diagnostics: AdapterCheckDiagnostic[]
|
|
2143
|
+
): void => {
|
|
2144
|
+
const packageName = workspace.manifest.name as string;
|
|
2145
|
+
const dependencies = dependencyMap(workspace.manifest.dependencies);
|
|
2146
|
+
const devDependencies = dependencyMap(workspace.manifest.devDependencies);
|
|
2147
|
+
const optionalDependencies = dependencyMap(
|
|
2148
|
+
workspace.manifest.optionalDependencies
|
|
2149
|
+
);
|
|
2150
|
+
const peerDependencies = dependencyMap(workspace.manifest.peerDependencies);
|
|
2151
|
+
|
|
2152
|
+
if (
|
|
2153
|
+
Object.hasOwn(dependencies, targetEntry.ownerPackage) ||
|
|
2154
|
+
Object.hasOwn(optionalDependencies, targetEntry.ownerPackage)
|
|
2155
|
+
) {
|
|
2156
|
+
diagnostics.push(
|
|
2157
|
+
diagnostic(
|
|
2158
|
+
workspace.packageJsonPath,
|
|
2159
|
+
packageName,
|
|
2160
|
+
'dependency-direction',
|
|
2161
|
+
`${packageName} must peer-depend on ${targetEntry.ownerPackage}; runtime dependencies invert the adapter boundary.`,
|
|
2162
|
+
targetEntry.target,
|
|
2163
|
+
'extracted'
|
|
2164
|
+
)
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
if (Object.hasOwn(devDependencies, targetEntry.ownerPackage)) {
|
|
2169
|
+
diagnostics.push(
|
|
2170
|
+
diagnostic(
|
|
2171
|
+
workspace.packageJsonPath,
|
|
2172
|
+
packageName,
|
|
2173
|
+
'dependency-direction',
|
|
2174
|
+
`${packageName} must not hide ${targetEntry.ownerPackage} in devDependencies; declare the owner as a peer dependency.`,
|
|
2175
|
+
targetEntry.target,
|
|
2176
|
+
'extracted'
|
|
2177
|
+
)
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
if (!Object.hasOwn(peerDependencies, targetEntry.ownerPackage)) {
|
|
2182
|
+
diagnostics.push(
|
|
2183
|
+
diagnostic(
|
|
2184
|
+
workspace.packageJsonPath,
|
|
2185
|
+
packageName,
|
|
2186
|
+
'dependency-direction',
|
|
2187
|
+
`${packageName} must declare ${targetEntry.ownerPackage} in peerDependencies for extracted adapter placement.`,
|
|
2188
|
+
targetEntry.target,
|
|
2189
|
+
'extracted'
|
|
2190
|
+
)
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
const assertToolingBoundary = (
|
|
2196
|
+
workspace: WorkspacePackage,
|
|
2197
|
+
sourceFiles: readonly string[],
|
|
2198
|
+
diagnostics: AdapterCheckDiagnostic[]
|
|
2199
|
+
): void => {
|
|
2200
|
+
const packageName = workspace.manifest.name as string;
|
|
2201
|
+
if (runtimeDependencyNames(workspace.manifest).has(adapterKitPackageName)) {
|
|
2202
|
+
diagnostics.push(
|
|
2203
|
+
diagnostic(
|
|
2204
|
+
workspace.packageJsonPath,
|
|
2205
|
+
packageName,
|
|
2206
|
+
'tooling-boundary',
|
|
2207
|
+
`${packageName} must not depend on ${adapterKitPackageName}; adapter kit stays out of runtime adapter packages.`
|
|
2208
|
+
)
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
const runtimeSourceFiles = sourceFiles.filter(
|
|
2213
|
+
(sourceFile) => !isTestFile(sourceFile)
|
|
2214
|
+
);
|
|
2215
|
+
const toolingImportPaths = pathsImporting(
|
|
2216
|
+
runtimeSourceFiles,
|
|
2217
|
+
adapterKitPackageName
|
|
2218
|
+
);
|
|
2219
|
+
for (const sourcePath of toolingImportPaths) {
|
|
2220
|
+
diagnostics.push(
|
|
2221
|
+
diagnostic(
|
|
2222
|
+
workspace.packageJsonPath,
|
|
2223
|
+
packageName,
|
|
2224
|
+
'tooling-boundary',
|
|
2225
|
+
`${packageName} imports ${adapterKitPackageName} from ${normalizePath(relative(workspace.packageRoot, sourcePath))}; adapters must not import the adapter kit engine.`
|
|
2226
|
+
)
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
const sourceFilesForCandidate = (
|
|
2232
|
+
workspace: WorkspacePackage,
|
|
2233
|
+
candidate: AdapterCheckCandidate,
|
|
2234
|
+
sourceFiles: readonly string[]
|
|
2235
|
+
): readonly string[] => {
|
|
2236
|
+
if (!candidate.exportKey) {
|
|
2237
|
+
return sourceFiles;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const exportTarget = resolvableExportTarget(workspace, candidate.exportKey);
|
|
2241
|
+
if (!exportTarget) {
|
|
2242
|
+
return sourceFiles;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
const normalizedTarget = normalizePath(exportTarget);
|
|
2246
|
+
const targetDir = normalizePath(dirname(normalizedTarget));
|
|
2247
|
+
if (normalizedTarget.endsWith('/index.ts')) {
|
|
2248
|
+
return sourceFiles.filter((sourceFile) =>
|
|
2249
|
+
normalizePath(sourceFile).startsWith(`${targetDir}/`)
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
const targetStem = normalizedTarget.replace(/\.ts$/u, '');
|
|
2254
|
+
return sourceFiles.filter((sourceFile) => {
|
|
2255
|
+
const normalizedSourceFile = normalizePath(sourceFile);
|
|
2256
|
+
return (
|
|
2257
|
+
normalizedSourceFile === normalizedTarget ||
|
|
2258
|
+
normalizedSourceFile.startsWith(`${targetStem}.`) ||
|
|
2259
|
+
normalizedSourceFile.startsWith(`${targetStem}/`)
|
|
2260
|
+
);
|
|
2261
|
+
});
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
const checkAdapterPackage = (
|
|
2265
|
+
workspace: WorkspacePackage,
|
|
2266
|
+
targetById: ReadonlyMap<string, AdapterTargetCatalogEntry>
|
|
2267
|
+
): {
|
|
2268
|
+
readonly diagnostics: readonly AdapterCheckDiagnostic[];
|
|
2269
|
+
readonly subjects: readonly AdapterCheckSubject[];
|
|
2270
|
+
} => {
|
|
2271
|
+
const packageName = workspace.manifest.name as string;
|
|
2272
|
+
const diagnostics: AdapterCheckDiagnostic[] = [];
|
|
2273
|
+
const placement = placementForWorkspace(workspace.workspacePath);
|
|
2274
|
+
const metadata = trailAdapterMetadata(workspace.manifest);
|
|
2275
|
+
const subpathMetadata = trailSubpathAdapterMetadata(workspace.manifest);
|
|
2276
|
+
|
|
2277
|
+
if ((!placement || metadata === undefined) && subpathMetadata === undefined) {
|
|
2278
|
+
return { diagnostics: [], subjects: [] };
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
assertPackageExports(workspace, diagnostics);
|
|
2282
|
+
const sourceFiles = collectSourceFiles(join(workspace.packageRoot, 'src'));
|
|
2283
|
+
assertToolingBoundary(workspace, sourceFiles, diagnostics);
|
|
2284
|
+
|
|
2285
|
+
const subjects: AdapterCheckSubject[] = [];
|
|
2286
|
+
const checkCandidate = (
|
|
2287
|
+
candidate: AdapterCheckCandidate
|
|
2288
|
+
): AdapterCheckSubject | undefined => {
|
|
2289
|
+
if (
|
|
2290
|
+
candidate.exportKey &&
|
|
2291
|
+
!hasResolvableExport(workspace, candidate.exportKey)
|
|
2292
|
+
) {
|
|
2293
|
+
diagnostics.push(
|
|
2294
|
+
diagnostic(
|
|
2295
|
+
workspace.packageJsonPath,
|
|
2296
|
+
candidate.packageName,
|
|
2297
|
+
'missing-package-export',
|
|
2298
|
+
`${packageName} must export "${candidate.exportKey}" so ${candidate.packageName} can be resolved as a subpath adapter.`,
|
|
2299
|
+
candidate.metadata.target,
|
|
2300
|
+
candidate.placement
|
|
2301
|
+
)
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
const targetEntry = targetById.get(candidate.metadata.target);
|
|
2306
|
+
if (!targetEntry) {
|
|
2307
|
+
diagnostics.push(
|
|
2308
|
+
diagnostic(
|
|
2309
|
+
workspace.packageJsonPath,
|
|
2310
|
+
candidate.packageName,
|
|
2311
|
+
'unknown-adapter-target',
|
|
2312
|
+
`${candidate.packageName} declares unknown adapter target "${candidate.metadata.target}".`,
|
|
2313
|
+
candidate.metadata.target,
|
|
2314
|
+
candidate.placement
|
|
2315
|
+
)
|
|
2316
|
+
);
|
|
2317
|
+
return undefined;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
if (
|
|
2321
|
+
candidate.placement === 'subpath' &&
|
|
2322
|
+
targetEntry.ownerPackage !== packageName
|
|
2323
|
+
) {
|
|
2324
|
+
diagnostics.push(
|
|
2325
|
+
diagnostic(
|
|
2326
|
+
workspace.packageJsonPath,
|
|
2327
|
+
candidate.packageName,
|
|
2328
|
+
'invalid-adapter-metadata',
|
|
2329
|
+
`${candidate.packageName} declares target "${targetEntry.target}", but subpath adapters must live in the owner package ${targetEntry.ownerPackage}.`,
|
|
2330
|
+
targetEntry.target,
|
|
2331
|
+
candidate.placement
|
|
2332
|
+
)
|
|
2333
|
+
);
|
|
2334
|
+
return undefined;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
if (!targetEntry.placements.includes(candidate.placement)) {
|
|
2338
|
+
diagnostics.push(
|
|
2339
|
+
diagnostic(
|
|
2340
|
+
workspace.packageJsonPath,
|
|
2341
|
+
candidate.packageName,
|
|
2342
|
+
'unsupported-placement',
|
|
2343
|
+
`${targetEntry.ownerPackage}:${targetEntry.target} does not support ${candidate.placement} adapter placement.`,
|
|
2344
|
+
targetEntry.target,
|
|
2345
|
+
candidate.placement
|
|
2346
|
+
)
|
|
2347
|
+
);
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
if (candidate.placement === 'extracted') {
|
|
2351
|
+
assertDependencyDirection(workspace, targetEntry, diagnostics);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
const { conformance, testingImport } = targetEntry;
|
|
2355
|
+
const candidateSourceFiles = sourceFilesForCandidate(
|
|
2356
|
+
workspace,
|
|
2357
|
+
candidate,
|
|
2358
|
+
sourceFiles
|
|
2359
|
+
);
|
|
2360
|
+
const conformanceTestPaths = testingImport
|
|
2361
|
+
? pathsProvingConformance(
|
|
2362
|
+
candidateSourceFiles.filter(isTestFile),
|
|
2363
|
+
targetEntry
|
|
2364
|
+
)
|
|
2365
|
+
: [];
|
|
2366
|
+
|
|
2367
|
+
if (!testingImport) {
|
|
2368
|
+
diagnostics.push(
|
|
2369
|
+
diagnostic(
|
|
2370
|
+
workspace.packageJsonPath,
|
|
2371
|
+
candidate.packageName,
|
|
2372
|
+
'missing-owner-conformance',
|
|
2373
|
+
`${targetEntry.ownerPackage}:${targetEntry.target} must declare testingImport before adapters can prove conformance.`,
|
|
2374
|
+
targetEntry.target,
|
|
2375
|
+
candidate.placement
|
|
2376
|
+
)
|
|
2377
|
+
);
|
|
2378
|
+
} else if (conformanceTestPaths.length === 0) {
|
|
2379
|
+
const conformanceHint = conformance
|
|
2380
|
+
? ` and call ${conformance.runner}(adapter, ${conformance.casesFactory}(...))`
|
|
2381
|
+
: '';
|
|
2382
|
+
diagnostics.push(
|
|
2383
|
+
diagnostic(
|
|
2384
|
+
workspace.packageJsonPath,
|
|
2385
|
+
candidate.packageName,
|
|
2386
|
+
'missing-conformance',
|
|
2387
|
+
`${candidate.packageName} must import ${testingImport} from a conformance test${conformanceHint}.`,
|
|
2388
|
+
targetEntry.target,
|
|
2389
|
+
candidate.placement
|
|
2390
|
+
)
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
return {
|
|
2395
|
+
conformanceTestPaths,
|
|
2396
|
+
key: candidate.key,
|
|
2397
|
+
ownerPackage: targetEntry.ownerPackage,
|
|
2398
|
+
packageJsonPath: workspace.packageJsonPath,
|
|
2399
|
+
packageName: candidate.packageName,
|
|
2400
|
+
packageRoot: workspace.packageRoot,
|
|
2401
|
+
placement: candidate.placement,
|
|
2402
|
+
target: targetEntry.target,
|
|
2403
|
+
targetKey: targetEntry.key,
|
|
2404
|
+
...(conformance?.adapterType
|
|
2405
|
+
? { adapterType: conformance.adapterType }
|
|
2406
|
+
: {}),
|
|
2407
|
+
...(testingImport ? { testingImport } : {}),
|
|
2408
|
+
};
|
|
2409
|
+
};
|
|
2410
|
+
|
|
2411
|
+
if (placement && metadata !== undefined) {
|
|
2412
|
+
if (metadata === null) {
|
|
2413
|
+
diagnostics.push(
|
|
2414
|
+
diagnostic(
|
|
2415
|
+
workspace.packageJsonPath,
|
|
2416
|
+
packageName,
|
|
2417
|
+
'invalid-adapter-metadata',
|
|
2418
|
+
`${packageName} must declare trails.adapter as an object with a kebab-case target string.`,
|
|
2419
|
+
undefined,
|
|
2420
|
+
placement
|
|
2421
|
+
)
|
|
2422
|
+
);
|
|
2423
|
+
} else {
|
|
2424
|
+
const subject = checkCandidate({
|
|
2425
|
+
key: packageName,
|
|
2426
|
+
metadata,
|
|
2427
|
+
packageName,
|
|
2428
|
+
placement,
|
|
2429
|
+
});
|
|
2430
|
+
if (subject) {
|
|
2431
|
+
subjects.push(subject);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
if (subpathMetadata === null) {
|
|
2437
|
+
diagnostics.push(
|
|
2438
|
+
diagnostic(
|
|
2439
|
+
workspace.packageJsonPath,
|
|
2440
|
+
packageName,
|
|
2441
|
+
'invalid-adapter-metadata',
|
|
2442
|
+
`${packageName} must declare trails.adapters as an object keyed by exported subpath, with each value declaring a kebab-case target string.`,
|
|
2443
|
+
undefined,
|
|
2444
|
+
'subpath'
|
|
2445
|
+
)
|
|
2446
|
+
);
|
|
2447
|
+
} else if (subpathMetadata !== undefined) {
|
|
2448
|
+
for (const subpathAdapter of subpathMetadata) {
|
|
2449
|
+
const subpathPackageName = `${packageName}/${subpathAdapter.exportKey.slice(2)}`;
|
|
2450
|
+
const targetEntry = targetById.get(subpathAdapter.target);
|
|
2451
|
+
const subpathPlacement =
|
|
2452
|
+
targetEntry?.ownerPackage === packageName
|
|
2453
|
+
? 'subpath'
|
|
2454
|
+
: (placement ?? 'subpath');
|
|
2455
|
+
const subject = checkCandidate({
|
|
2456
|
+
exportKey: subpathAdapter.exportKey,
|
|
2457
|
+
key: subpathPackageName,
|
|
2458
|
+
metadata: subpathAdapter,
|
|
2459
|
+
packageName: subpathPackageName,
|
|
2460
|
+
placement: subpathPlacement,
|
|
2461
|
+
});
|
|
2462
|
+
if (subject) {
|
|
2463
|
+
subjects.push(subject);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
return { diagnostics, subjects };
|
|
2469
|
+
};
|
|
2470
|
+
|
|
2471
|
+
export const checkAdapters = (rootDir: string): AdapterCheckReport => {
|
|
2472
|
+
const catalog = deriveAdapterTargetCatalog(rootDir);
|
|
2473
|
+
const targetById = targetEntriesByTarget(catalog.targets);
|
|
2474
|
+
const diagnostics: AdapterCheckDiagnostic[] = [
|
|
2475
|
+
...catalogDiagnostics(catalog),
|
|
2476
|
+
];
|
|
2477
|
+
const subjects: AdapterCheckSubject[] = [];
|
|
2478
|
+
|
|
2479
|
+
for (const workspace of workspacePackages(rootDir)) {
|
|
2480
|
+
const result = checkAdapterPackage(workspace, targetById);
|
|
2481
|
+
diagnostics.push(...result.diagnostics);
|
|
2482
|
+
subjects.push(...result.subjects);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
const sortedSubjects = subjects.toSorted((left, right) =>
|
|
2486
|
+
left.key.localeCompare(right.key)
|
|
2487
|
+
);
|
|
2488
|
+
|
|
2489
|
+
return {
|
|
2490
|
+
diagnostics,
|
|
2491
|
+
facts: adapterFacts(catalog.targets, sortedSubjects),
|
|
2492
|
+
subjects: sortedSubjects,
|
|
2493
|
+
targets: catalog.targets,
|
|
2494
|
+
};
|
|
2495
|
+
};
|