carrick 0.3.105 → 0.3.107
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/dist/hook/refresh.js +8 -1
- package/dist/hook/refresh.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +22 -2
- package/sidecar/dist/src/capture/anchors.js +178 -34
- package/sidecar/dist/src/capture/api.d.ts +42 -1
- package/sidecar/dist/src/capture/check-classify.d.ts +9 -1
- package/sidecar/dist/src/capture/check-classify.js +15 -0
- package/sidecar/dist/src/capture/check-poison.js +4 -14
- package/sidecar/dist/src/capture/check.js +13 -1
- package/sidecar/dist/src/capture/index.js +55 -29
- package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
- package/sidecar/dist/src/capture/installed-package.js +2 -1
- package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
- package/sidecar/dist/src/capture/outside-root.js +39 -2
- package/sidecar/dist/src/capture/self-check.js +12 -13
- package/sidecar/dist/src/capture/service-config.d.ts +2 -0
- package/sidecar/dist/src/capture/service-config.js +1 -1
- package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
- package/sidecar/dist/src/capture/specifiers.js +25 -0
- package/sidecar/dist/src/failure-path.d.ts +34 -3
- package/sidecar/dist/src/failure-path.js +59 -35
- package/sidecar/dist/src/function-line-index.d.ts +30 -0
- package/sidecar/dist/src/function-line-index.js +162 -0
- package/sidecar/dist/src/index.d.ts +5 -0
- package/sidecar/dist/src/index.js +29 -5
- package/sidecar/dist/src/line-index.d.ts +9 -0
- package/sidecar/dist/src/line-index.js +26 -0
- package/sidecar/dist/src/printed-names.d.ts +43 -0
- package/sidecar/dist/src/printed-names.js +186 -0
- package/sidecar/dist/src/progress.d.ts +22 -0
- package/sidecar/dist/src/progress.js +31 -0
- package/sidecar/dist/src/retype.js +58 -128
- package/sidecar/dist/src/type-inferrer.d.ts +124 -13
- package/sidecar/dist/src/type-inferrer.js +531 -146
- package/sidecar/dist/src/type-structural-expander.js +10 -1
- package/sidecar/dist/src/types.d.ts +37 -6
- package/sidecar/dist/src/validators.d.ts +76 -0
- package/sidecar/dist/src/validators.js +8 -0
|
@@ -34,13 +34,13 @@ import { entryRelativeSpecifier, resolveAnchor } from './anchors.js';
|
|
|
34
34
|
import { findAugmentationFiles } from './augmentations.js';
|
|
35
35
|
import { installedVersions, lockfileVersions } from './lockfile.js';
|
|
36
36
|
import { rewriteEmittedSpecifiers } from './paths-rewrite.js';
|
|
37
|
-
import { typesPackageOf, withInstalledPackages } from './installed-package.js';
|
|
37
|
+
import { installedPackageSpecifier, typesPackageOf, withInstalledPackages } from './installed-package.js';
|
|
38
38
|
import { selfCheckStub } from './self-check.js';
|
|
39
39
|
import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
|
|
40
40
|
import { DenoProject, findDenoConfig } from './deno-project.js';
|
|
41
41
|
import { emitsAlike, ProjectGraph } from './project-references.js';
|
|
42
42
|
import { findServiceTsconfig } from './service-config.js';
|
|
43
|
-
import { placeEmittedTree } from './outside-root.js';
|
|
43
|
+
import { placeEmittedTree, surfaceModuleInTree } from './outside-root.js';
|
|
44
44
|
import { WriteGuard } from './guarded-fs.js';
|
|
45
45
|
export { DenoProject, findDenoConfig } from './deno-project.js';
|
|
46
46
|
export { serviceConfigPath } from './project-references.js';
|
|
@@ -351,7 +351,14 @@ export function captureStub(opts) {
|
|
|
351
351
|
if (emitPartial) {
|
|
352
352
|
errors.push(`declaration emit was partial: kept ${emitted.size} emitted file(s); ` +
|
|
353
353
|
'aliases referencing unemitted modules are demoted to structural_fallback');
|
|
354
|
-
resolved = demoteDanglingAliases({
|
|
354
|
+
resolved = demoteDanglingAliases({
|
|
355
|
+
resolved,
|
|
356
|
+
emitted,
|
|
357
|
+
declarationSources,
|
|
358
|
+
staging,
|
|
359
|
+
entryDir,
|
|
360
|
+
surfaceDeclaration,
|
|
361
|
+
});
|
|
355
362
|
}
|
|
356
363
|
// ---- Relocate the emitted tree into the stub package ----
|
|
357
364
|
const typesDir = path.join(stubDir, 'types');
|
|
@@ -520,43 +527,47 @@ export function captureStub(opts) {
|
|
|
520
527
|
};
|
|
521
528
|
}
|
|
522
529
|
/**
|
|
523
|
-
* Partial-emit demotion:
|
|
524
|
-
*
|
|
525
|
-
* whose alias text references a relative module absent from that set, and
|
|
530
|
+
* Partial-emit demotion: demote every anchor whose alias text names, by a
|
|
531
|
+
* relative or an absolute path, a module the stub will not resolve, and
|
|
526
532
|
* rewrite the demoted aliases' lines in the emitted surface to `unknown`.
|
|
533
|
+
*
|
|
534
|
+
* A path resolves when the tree holds its module (`surfaceModuleInTree`: an
|
|
535
|
+
* emitted declaration inside rootDir or outside it, or a verbatim declaration
|
|
536
|
+
* source), read from where the entry was written, not from where the surface
|
|
537
|
+
* sits in the tree. An absolute path into an installed package resolves too
|
|
538
|
+
* (carrick#1773): no emit writes that module, and the specifier rewrite turns
|
|
539
|
+
* the path into the package's bare specifier and a pin, as it does when the
|
|
540
|
+
* emit is whole. A relative path into a package is not rewritten, so it stays
|
|
541
|
+
* demoted (carrick#1857).
|
|
542
|
+
*
|
|
527
543
|
* Anchors already demoted stay as they are; anchors whose text is
|
|
528
544
|
* self-contained (node-builder structural prints, literal object text) are
|
|
529
545
|
* untouched even when their source file failed to emit — their surface line
|
|
530
546
|
* references nothing that can dangle.
|
|
531
547
|
*/
|
|
532
548
|
function demoteDanglingAliases(args) {
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const moduleInTree = (spec) => {
|
|
550
|
-
const id = path.posix.normalize(path.posix.join(surfaceDir, spec));
|
|
551
|
-
if (id.startsWith('..'))
|
|
552
|
-
return false;
|
|
553
|
-
return treeModules.has(id) || treeModules.has(`${id}/index`);
|
|
549
|
+
const surfaceKey = [...args.emitted.keys()].find((fileName) => path.basename(fileName) === args.surfaceDeclaration);
|
|
550
|
+
const inTree = surfaceModuleInTree({
|
|
551
|
+
emitted: args.emitted.keys(),
|
|
552
|
+
declarationSources: args.declarationSources.keys(),
|
|
553
|
+
staging: args.staging,
|
|
554
|
+
entryDir: args.entryDir,
|
|
555
|
+
surfaceDeclaration: args.surfaceDeclaration,
|
|
556
|
+
});
|
|
557
|
+
const resolves = new Map();
|
|
558
|
+
const moduleResolves = (spec) => {
|
|
559
|
+
let answer = resolves.get(spec);
|
|
560
|
+
if (answer === undefined) {
|
|
561
|
+
answer = inTree(spec) || (spec.startsWith('/') && installedPackageSpecifier(spec) !== undefined);
|
|
562
|
+
resolves.set(spec, answer);
|
|
563
|
+
}
|
|
564
|
+
return answer;
|
|
554
565
|
};
|
|
555
566
|
const demoted = new Set();
|
|
556
567
|
const next = args.resolved.map((anchor) => {
|
|
557
568
|
if (anchor.failureReason !== undefined)
|
|
558
569
|
return anchor;
|
|
559
|
-
const dangling = [...collectSpecifiers(anchor.aliasText)].find((spec) => isRelative(spec) && !
|
|
570
|
+
const dangling = [...collectSpecifiers(anchor.aliasText)].find((spec) => isRelative(spec) && !moduleResolves(spec));
|
|
560
571
|
if (dangling === undefined)
|
|
561
572
|
return anchor;
|
|
562
573
|
demoted.add(anchor.request.alias);
|
|
@@ -661,6 +672,20 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
661
672
|
placeholders.set(stmt.name.text, stmt);
|
|
662
673
|
}
|
|
663
674
|
}
|
|
675
|
+
// A module specifier as the entry resolves it, under the program's own
|
|
676
|
+
// resolution (the Deno graph's, for a Deno service). Asked once per
|
|
677
|
+
// specifier: every anchor of a module asks for the same one.
|
|
678
|
+
const entryMode = entrySource?.impliedNodeFormat;
|
|
679
|
+
const fromEntry = new Map();
|
|
680
|
+
const resolveFromEntry = (specifier) => {
|
|
681
|
+
if (!fromEntry.has(specifier)) {
|
|
682
|
+
fromEntry.set(specifier, (deno
|
|
683
|
+
? deno.resolve(specifier, ctx.entryPath, options)
|
|
684
|
+
: ts.resolveModuleName(specifier, ctx.entryPath, options, ts.sys, undefined, undefined, entryMode)
|
|
685
|
+
.resolvedModule)?.resolvedFileName);
|
|
686
|
+
}
|
|
687
|
+
return fromEntry.get(specifier);
|
|
688
|
+
};
|
|
664
689
|
// A literal anchor whose text is a bare identifier resolves through a
|
|
665
690
|
// sibling symbol anchor's module when one names the same symbol.
|
|
666
691
|
const siblingSymbolSpecs = new Map();
|
|
@@ -668,7 +693,7 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
668
693
|
if (anchor.kind !== 'symbol')
|
|
669
694
|
continue;
|
|
670
695
|
if (!siblingSymbolSpecs.has(anchor.symbol_name)) {
|
|
671
|
-
siblingSymbolSpecs.set(anchor.symbol_name, entryRelativeSpecifier(ctx.entryDir, ctx.repoRoot, anchor.source_file));
|
|
696
|
+
siblingSymbolSpecs.set(anchor.symbol_name, entryRelativeSpecifier(ctx.entryDir, ctx.repoRoot, anchor.source_file, resolveFromEntry));
|
|
672
697
|
}
|
|
673
698
|
}
|
|
674
699
|
return opts.anchors.map((request) => resolveAnchor(program, request, {
|
|
@@ -676,6 +701,7 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
676
701
|
entryDir: ctx.entryDir,
|
|
677
702
|
placeholder: placeholders.get(request.alias),
|
|
678
703
|
siblingSymbolSpecs,
|
|
704
|
+
resolveFromEntry,
|
|
679
705
|
}));
|
|
680
706
|
}
|
|
681
707
|
finally {
|
|
@@ -41,6 +41,8 @@ export interface InstalledPackageSpecifier {
|
|
|
41
41
|
* deciding whether a runtime name is covered by a pinned types package.
|
|
42
42
|
*/
|
|
43
43
|
export declare function typesPackageOf(name: string): string;
|
|
44
|
+
/** A path or specifier without its script, source or declaration extension. */
|
|
45
|
+
export declare function withoutExtension(subpath: string): string;
|
|
44
46
|
/**
|
|
45
47
|
* A compiler host that also resolves the packages an absolute specifier was
|
|
46
48
|
* rewritten into, each from its own install directory. The capture's
|
|
@@ -101,7 +101,8 @@ function existingFile(spec) {
|
|
|
101
101
|
}
|
|
102
102
|
return undefined;
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
/** A path or specifier without its script, source or declaration extension. */
|
|
105
|
+
export function withoutExtension(subpath) {
|
|
105
106
|
return subpath.replace(/(?:\.d)?\.(?:ts|mts|cts|js|mjs|cjs|tsx|jsx)$/, '');
|
|
106
107
|
}
|
|
107
108
|
/** Every string target an exports value names, whatever its conditions. */
|
|
@@ -39,3 +39,22 @@ export declare function placeEmittedTree(args: {
|
|
|
39
39
|
entryDir: string;
|
|
40
40
|
surfaceDeclaration: string;
|
|
41
41
|
}): PlacedTree;
|
|
42
|
+
/**
|
|
43
|
+
* A test for whether the tree holds the module a specifier in the surface
|
|
44
|
+
* entry names (carrick#1773).
|
|
45
|
+
*
|
|
46
|
+
* The specifier is read the way the placement above reads one: resolved from
|
|
47
|
+
* the surface's SOURCE-side directory, so an absolute path stays what it is
|
|
48
|
+
* and `../` leaves rootDir, then looked up among the source-side paths of the
|
|
49
|
+
* declarations the tree holds. Those are every emitted declaration, inside
|
|
50
|
+
* rootDir or placed under `OUTSIDE_DIR`, and the declaration sources shipped
|
|
51
|
+
* verbatim (`declarationSources`, relative to rootDir).
|
|
52
|
+
*/
|
|
53
|
+
export declare function surfaceModuleInTree(args: {
|
|
54
|
+
/** tsc's file name for each emitted declaration. */
|
|
55
|
+
emitted: Iterable<string>;
|
|
56
|
+
declarationSources: Iterable<string>;
|
|
57
|
+
staging: string;
|
|
58
|
+
entryDir: string;
|
|
59
|
+
surfaceDeclaration: string;
|
|
60
|
+
}): (spec: string) => boolean;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* A capture whose program stays inside rootDir is placed exactly as before.
|
|
16
16
|
*/
|
|
17
17
|
import * as path from 'node:path';
|
|
18
|
+
import { withoutExtension } from './installed-package.js';
|
|
18
19
|
import { rewriteSpecifiers } from './specifiers.js';
|
|
19
20
|
/** Tree directory holding declarations of sources outside rootDir. */
|
|
20
21
|
export const OUTSIDE_DIR = '__outside__';
|
|
@@ -31,13 +32,12 @@ export function placeEmittedTree(args) {
|
|
|
31
32
|
const sourceSideOf = new Map();
|
|
32
33
|
const outsideFiles = [];
|
|
33
34
|
for (const fileName of args.emitted.keys()) {
|
|
35
|
+
sourceSideOf.set(fileName, sourceSidePath(fileName, args.staging, args.entryDir));
|
|
34
36
|
const rel = posix(path.relative(args.staging, fileName));
|
|
35
37
|
if (escapes(rel)) {
|
|
36
38
|
outsideFiles.push(fileName);
|
|
37
|
-
sourceSideOf.set(fileName, path.resolve(fileName));
|
|
38
39
|
continue;
|
|
39
40
|
}
|
|
40
|
-
sourceSideOf.set(fileName, path.join(args.entryDir, rel));
|
|
41
41
|
relOf.set(fileName, path.basename(rel) === args.surfaceDeclaration ? 'surface.d.ts' : rel);
|
|
42
42
|
}
|
|
43
43
|
const outside = new Map();
|
|
@@ -82,6 +82,43 @@ export function placeEmittedTree(args) {
|
|
|
82
82
|
}
|
|
83
83
|
return { relOf, textOf, outside, rewrites };
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Where tsc would have written an emitted declaration in the source tree. A
|
|
87
|
+
* file under the staging dir mirrors rootDir; one outside it arrived at its
|
|
88
|
+
* source's own path.
|
|
89
|
+
*/
|
|
90
|
+
function sourceSidePath(fileName, staging, entryDir) {
|
|
91
|
+
const rel = posix(path.relative(staging, fileName));
|
|
92
|
+
return escapes(rel) ? path.resolve(fileName) : path.join(entryDir, rel);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A test for whether the tree holds the module a specifier in the surface
|
|
96
|
+
* entry names (carrick#1773).
|
|
97
|
+
*
|
|
98
|
+
* The specifier is read the way the placement above reads one: resolved from
|
|
99
|
+
* the surface's SOURCE-side directory, so an absolute path stays what it is
|
|
100
|
+
* and `../` leaves rootDir, then looked up among the source-side paths of the
|
|
101
|
+
* declarations the tree holds. Those are every emitted declaration, inside
|
|
102
|
+
* rootDir or placed under `OUTSIDE_DIR`, and the declaration sources shipped
|
|
103
|
+
* verbatim (`declarationSources`, relative to rootDir).
|
|
104
|
+
*/
|
|
105
|
+
export function surfaceModuleInTree(args) {
|
|
106
|
+
const held = new Set();
|
|
107
|
+
let surfaceDir = args.entryDir;
|
|
108
|
+
for (const fileName of args.emitted) {
|
|
109
|
+
const sourceSide = sourceSidePath(fileName, args.staging, args.entryDir);
|
|
110
|
+
if (path.basename(sourceSide) === args.surfaceDeclaration)
|
|
111
|
+
surfaceDir = path.dirname(sourceSide);
|
|
112
|
+
held.add(sourceSide.replace(DECLARATION_EXT, ''));
|
|
113
|
+
}
|
|
114
|
+
for (const rel of args.declarationSources) {
|
|
115
|
+
held.add(path.join(args.entryDir, rel).replace(DECLARATION_EXT, ''));
|
|
116
|
+
}
|
|
117
|
+
return (spec) => {
|
|
118
|
+
const target = path.resolve(surfaceDir, withoutExtension(spec));
|
|
119
|
+
return held.has(target) || held.has(path.join(target, 'index'));
|
|
120
|
+
};
|
|
121
|
+
}
|
|
85
122
|
function posix(p) {
|
|
86
123
|
return p.split(path.sep).join('/');
|
|
87
124
|
}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import ts from 'typescript';
|
|
33
33
|
import * as fs from 'node:fs';
|
|
34
34
|
import * as path from 'node:path';
|
|
35
|
-
import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
|
|
35
|
+
import { collectSpecifiers, declarationCandidates, isDeclarationFileName, isRelative, packageNameOf, } from './specifiers.js';
|
|
36
36
|
import { repairDanglingImports } from './repair-dangling.js';
|
|
37
37
|
import { findDisqualifyingTopTypes, findUnresolvedPlaceholders, isErrorPlaceholder, provenanceOf, unemittedModuleProvenance, unresolvedInTreeProvenance, } from './deep-walk.js';
|
|
38
38
|
export function selfCheckStub(args) {
|
|
@@ -43,7 +43,7 @@ export function selfCheckStub(args) {
|
|
|
43
43
|
const p = path.join(dir, entry.name);
|
|
44
44
|
if (entry.isDirectory())
|
|
45
45
|
walk(p);
|
|
46
|
-
else if (entry.name
|
|
46
|
+
else if (isDeclarationFileName(entry.name))
|
|
47
47
|
treeFiles.push(p);
|
|
48
48
|
}
|
|
49
49
|
};
|
|
@@ -247,6 +247,13 @@ function buildSurfaceSpanIndex(surfaceSource) {
|
|
|
247
247
|
return spans.find((span) => position >= span.start && position <= span.end)?.alias;
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* carrick#1842: a literal anchor whose text is a body read as raw text passes
|
|
252
|
+
* that mark to its record, where the check phase reads it.
|
|
253
|
+
*/
|
|
254
|
+
function rawTextMark(request) {
|
|
255
|
+
return request.kind === 'literal' && request.raw_text_read ? { raw_text_read: true } : {};
|
|
256
|
+
}
|
|
250
257
|
/** An alias that never reached a capture-native tier: the failure reason was
|
|
251
258
|
* recorded at demotion time; the surface line is `unknown` by construction. */
|
|
252
259
|
function demotedRecord(anchor) {
|
|
@@ -267,6 +274,7 @@ function demotedRecord(anchor) {
|
|
|
267
274
|
...(anchor.request.kind === 'literal' && anchor.namesUnemittedModule
|
|
268
275
|
? { unresolved_in_tree: [unemittedModuleProvenance()] }
|
|
269
276
|
: {}),
|
|
277
|
+
...rawTextMark(anchor.request),
|
|
270
278
|
};
|
|
271
279
|
}
|
|
272
280
|
function checkedRecord(anchor, ctx) {
|
|
@@ -448,6 +456,7 @@ function checkedRecord(anchor, ctx) {
|
|
|
448
456
|
.map((p) => unresolvedInTreeProvenance(p, [...unfoundNames].sort())),
|
|
449
457
|
}
|
|
450
458
|
: {}),
|
|
459
|
+
...rawTextMark(anchor.request),
|
|
451
460
|
};
|
|
452
461
|
}
|
|
453
462
|
/** Resolve a relative specifier from `fromAbs` to a tree file, if present. */
|
|
@@ -455,15 +464,5 @@ function resolveTreeSpecifier(fromAbs, spec, tree) {
|
|
|
455
464
|
if (!isRelative(spec))
|
|
456
465
|
return undefined;
|
|
457
466
|
const base = path.resolve(path.dirname(fromAbs), spec);
|
|
458
|
-
|
|
459
|
-
`${base}.d.ts`,
|
|
460
|
-
path.join(base, 'index.d.ts'),
|
|
461
|
-
base.endsWith('.js') ? `${base.slice(0, -3)}.d.ts` : undefined,
|
|
462
|
-
base, // already .d.ts
|
|
463
|
-
].filter((c) => c !== undefined);
|
|
464
|
-
for (const candidate of candidates) {
|
|
465
|
-
if (tree.has(candidate))
|
|
466
|
-
return candidate;
|
|
467
|
-
}
|
|
468
|
-
return undefined;
|
|
467
|
+
return declarationCandidates(base).find((candidate) => tree.has(candidate));
|
|
469
468
|
}
|
|
@@ -17,3 +17,5 @@
|
|
|
17
17
|
*/
|
|
18
18
|
/** Absolute path of the config that types the service, or undefined for none. */
|
|
19
19
|
export declare function findServiceTsconfig(serviceRoot: string, scanRoot?: string): string | undefined;
|
|
20
|
+
/** The path with every symlink resolved; the path as given when it does not exist. */
|
|
21
|
+
export declare function realPath(p: string): string;
|
|
@@ -47,7 +47,7 @@ export function findServiceTsconfig(serviceRoot, scanRoot) {
|
|
|
47
47
|
return undefined;
|
|
48
48
|
}
|
|
49
49
|
/** The path with every symlink resolved; the path as given when it does not exist. */
|
|
50
|
-
function realPath(p) {
|
|
50
|
+
export function realPath(p) {
|
|
51
51
|
try {
|
|
52
52
|
return fs.realpathSync(p);
|
|
53
53
|
}
|
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
* pattern matching for the post-emit rewrite pass.
|
|
5
5
|
*/
|
|
6
6
|
export declare function isRelative(spec: string): boolean;
|
|
7
|
+
/** A declaration file of any module format: `.d.ts`, `.d.mts`, `.d.cts`. */
|
|
8
|
+
export declare function isDeclarationFileName(name: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The declaration files a relative specifier can name in a stub's tree, in
|
|
11
|
+
* the order tried, given the path it resolves to from the file that holds it.
|
|
12
|
+
*
|
|
13
|
+
* A specifier names its module with no extension (`./a`: `a.d.ts`, or the
|
|
14
|
+
* directory's `index.d.ts`), by the file the module compiles to (`./a.js`:
|
|
15
|
+
* `a.d.ts`, `./a.mjs`: `a.d.mts`, `./a.cjs`: `a.d.cts`), or by the
|
|
16
|
+
* declaration file itself. The surface names an anchor's module the second
|
|
17
|
+
* way where the first does not resolve (carrick#1911), and so does any source
|
|
18
|
+
* written for `node16`..`nodenext`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function declarationCandidates(resolved: string): string[];
|
|
7
21
|
/** zod -> zod, @scope/pkg/sub -> @scope/pkg, pkg/sub -> pkg */
|
|
8
22
|
export declare function packageNameOf(spec: string): string;
|
|
9
23
|
/** Extract every module specifier mentioned in a .d.ts text: `from "x"`,
|
|
@@ -3,9 +3,34 @@
|
|
|
3
3
|
* declaration text, external/internal classification, and tsconfig-`paths`
|
|
4
4
|
* pattern matching for the post-emit rewrite pass.
|
|
5
5
|
*/
|
|
6
|
+
import * as path from 'node:path';
|
|
6
7
|
export function isRelative(spec) {
|
|
7
8
|
return spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/');
|
|
8
9
|
}
|
|
10
|
+
/** A declaration file of any module format: `.d.ts`, `.d.mts`, `.d.cts`. */
|
|
11
|
+
export function isDeclarationFileName(name) {
|
|
12
|
+
return /\.d\.[cm]?ts$/.test(name);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The declaration files a relative specifier can name in a stub's tree, in
|
|
16
|
+
* the order tried, given the path it resolves to from the file that holds it.
|
|
17
|
+
*
|
|
18
|
+
* A specifier names its module with no extension (`./a`: `a.d.ts`, or the
|
|
19
|
+
* directory's `index.d.ts`), by the file the module compiles to (`./a.js`:
|
|
20
|
+
* `a.d.ts`, `./a.mjs`: `a.d.mts`, `./a.cjs`: `a.d.cts`), or by the
|
|
21
|
+
* declaration file itself. The surface names an anchor's module the second
|
|
22
|
+
* way where the first does not resolve (carrick#1911), and so does any source
|
|
23
|
+
* written for `node16`..`nodenext`.
|
|
24
|
+
*/
|
|
25
|
+
export function declarationCandidates(resolved) {
|
|
26
|
+
const output = /\.([cm]?)jsx?$/.exec(resolved);
|
|
27
|
+
return [
|
|
28
|
+
`${resolved}.d.ts`,
|
|
29
|
+
path.join(resolved, 'index.d.ts'),
|
|
30
|
+
...(output ? [`${resolved.slice(0, output.index)}.d.${output[1]}ts`] : []),
|
|
31
|
+
resolved,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
9
34
|
/** zod -> zod, @scope/pkg/sub -> @scope/pkg, pkg/sub -> pkg */
|
|
10
35
|
export function packageNameOf(spec) {
|
|
11
36
|
const parts = spec.split('/');
|
|
@@ -20,17 +20,48 @@
|
|
|
20
20
|
* any other condition lets everything through. A node is on the failure path
|
|
21
21
|
* only when NO status in 200-299 can reach it.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `res.status === 200` as the failure path so it can find a success read to
|
|
23
|
+
* The retype check (retype.ts) reads the same tests through `testsOnPath` and
|
|
24
|
+
* keeps its own policy on top: it also reads the false side of
|
|
25
|
+
* `res.status === 200` as the failure path, so it can find a success read to
|
|
26
26
|
* judge. Here a decided failure REMOVES a read, so the reading has to be sound
|
|
27
27
|
* the other way round: after `if (res.status === 204) return null`, 200 still
|
|
28
28
|
* gets through, and the json read that follows is the payload.
|
|
29
29
|
*/
|
|
30
30
|
import { Node } from 'ts-morph';
|
|
31
|
+
/** A set of HTTP statuses, as the predicate that admits them. */
|
|
32
|
+
export type Statuses = (status: number) => boolean;
|
|
33
|
+
/** The statuses `ok` is true for. */
|
|
34
|
+
export declare const SUCCEEDED: Statuses;
|
|
35
|
+
/** The statuses from 100 to 599 that `statuses` admits, in order. */
|
|
36
|
+
export declare function statusesIn(statuses: Statuses): number[];
|
|
37
|
+
/** One test of the response on the path to a node, as the side the node is on. */
|
|
38
|
+
export interface SideTaken {
|
|
39
|
+
/** Every status that can take this side, and maybe more (see `inexact`). */
|
|
40
|
+
admits: Statuses;
|
|
41
|
+
/**
|
|
42
|
+
* The test reads the response's `ok` or `status` in a form this reading
|
|
43
|
+
* cannot follow (`res.status === OK`, `codes.includes(res.status)`), or
|
|
44
|
+
* combines it with a condition that is not about the response
|
|
45
|
+
* (`res.ok && fresh`). `admits` then lets through more statuses than the
|
|
46
|
+
* side does: still sound for "no success status reaches the node", but no
|
|
47
|
+
* longer only the statuses the source singled out.
|
|
48
|
+
*/
|
|
49
|
+
inexact: boolean;
|
|
50
|
+
}
|
|
31
51
|
/**
|
|
32
52
|
* True when the source reaches `node` only after the response named by
|
|
33
53
|
* `isResponse` failed. The walk climbs from `node` to `boundary` (the function
|
|
34
54
|
* the call sits in) and never looks at a test outside it.
|
|
35
55
|
*/
|
|
36
56
|
export declare function reachedOnlyOnFailure(node: Node, boundary: Node, isResponse: (node: Node) => boolean): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Each test of the response on the path from `node` up to `boundary` (the
|
|
59
|
+
* whole file when it is `undefined`), as the side `node` is on: a branch of
|
|
60
|
+
* an `if` or a conditional expression it sits in, or an earlier `if` in an
|
|
61
|
+
* enclosing block one of whose branches cannot complete, which leaves the
|
|
62
|
+
* rest of the block to the other side. A test that does not read the
|
|
63
|
+
* response's `ok` or `status` is not listed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function testsOnPath(node: Node, boundary: Node | undefined, isResponse: (node: Node) => boolean): SideTaken[];
|
|
66
|
+
/** `node` reads the response's `ok` or `status`, itself or anywhere inside it. */
|
|
67
|
+
export declare function readsResponseStatus(node: Node, isResponse: (node: Node) => boolean): boolean;
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* any other condition lets everything through. A node is on the failure path
|
|
21
21
|
* only when NO status in 200-299 can reach it.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `res.status === 200` as the failure path so it can find a success read to
|
|
23
|
+
* The retype check (retype.ts) reads the same tests through `testsOnPath` and
|
|
24
|
+
* keeps its own policy on top: it also reads the false side of
|
|
25
|
+
* `res.status === 200` as the failure path, so it can find a success read to
|
|
26
26
|
* judge. Here a decided failure REMOVES a read, so the reading has to be sound
|
|
27
27
|
* the other way round: after `if (res.status === 204) return null`, 200 still
|
|
28
28
|
* gets through, and the json read that follows is the payload.
|
|
@@ -32,38 +32,62 @@ const FIRST_STATUS = 100;
|
|
|
32
32
|
const LAST_STATUS = 599;
|
|
33
33
|
const EVERY_STATUS = () => true;
|
|
34
34
|
/** The statuses `ok` is true for. */
|
|
35
|
-
const SUCCEEDED = (status) => status >= 200 && status <= 299;
|
|
36
|
-
|
|
35
|
+
export const SUCCEEDED = (status) => status >= 200 && status <= 299;
|
|
36
|
+
/** The statuses from 100 to 599 that `statuses` admits, in order. */
|
|
37
|
+
export function statusesIn(statuses) {
|
|
38
|
+
const admitted = [];
|
|
39
|
+
for (let status = FIRST_STATUS; status <= LAST_STATUS; status++) {
|
|
40
|
+
if (statuses(status))
|
|
41
|
+
admitted.push(status);
|
|
42
|
+
}
|
|
43
|
+
return admitted;
|
|
44
|
+
}
|
|
45
|
+
/** A condition that does not read the response's `ok` or `status`. */
|
|
46
|
+
const UNRELATED = { whenTrue: EVERY_STATUS, whenFalse: EVERY_STATUS, inexact: false };
|
|
47
|
+
/** A condition that reads the response's status in a form this reading cannot follow. */
|
|
48
|
+
const UNREAD = { whenTrue: EVERY_STATUS, whenFalse: EVERY_STATUS, inexact: true };
|
|
37
49
|
/**
|
|
38
50
|
* True when the source reaches `node` only after the response named by
|
|
39
51
|
* `isResponse` failed. The walk climbs from `node` to `boundary` (the function
|
|
40
52
|
* the call sits in) and never looks at a test outside it.
|
|
41
53
|
*/
|
|
42
54
|
export function reachedOnlyOnFailure(node, boundary, isResponse) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
const sides = testsOnPath(node, boundary, isResponse);
|
|
56
|
+
if (sides.length === 0)
|
|
57
|
+
return false;
|
|
58
|
+
const reaching = statusesIn((status) => sides.every((side) => side.admits(status)));
|
|
59
|
+
return reaching.length > 0 && !reaching.some(SUCCEEDED);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Each test of the response on the path from `node` up to `boundary` (the
|
|
63
|
+
* whole file when it is `undefined`), as the side `node` is on: a branch of
|
|
64
|
+
* an `if` or a conditional expression it sits in, or an earlier `if` in an
|
|
65
|
+
* enclosing block one of whose branches cannot complete, which leaves the
|
|
66
|
+
* rest of the block to the other side. A test that does not read the
|
|
67
|
+
* response's `ok` or `status` is not listed.
|
|
68
|
+
*/
|
|
69
|
+
export function testsOnPath(node, boundary, isResponse) {
|
|
70
|
+
const sides = [];
|
|
71
|
+
const take = (test, whenTrue) => {
|
|
72
|
+
if (test === UNRELATED)
|
|
47
73
|
return;
|
|
48
|
-
|
|
49
|
-
admitted = (status) => before(status) && by(status);
|
|
50
|
-
narrowed = true;
|
|
74
|
+
sides.push({ admits: whenTrue ? test.whenTrue : test.whenFalse, inexact: test.inexact });
|
|
51
75
|
};
|
|
52
76
|
for (let child = node, parent = node.getParent(); parent && parent !== boundary; child = parent, parent = parent.getParent()) {
|
|
53
77
|
if (Node.isIfStatement(parent)) {
|
|
54
78
|
if (child === parent.getThenStatement()) {
|
|
55
|
-
|
|
79
|
+
take(readTest(parent.getExpression(), isResponse), true);
|
|
56
80
|
}
|
|
57
81
|
else if (child === parent.getElseStatement()) {
|
|
58
|
-
|
|
82
|
+
take(readTest(parent.getExpression(), isResponse), false);
|
|
59
83
|
}
|
|
60
84
|
}
|
|
61
85
|
else if (Node.isConditionalExpression(parent)) {
|
|
62
86
|
if (child === parent.getWhenTrue()) {
|
|
63
|
-
|
|
87
|
+
take(readTest(parent.getCondition(), isResponse), true);
|
|
64
88
|
}
|
|
65
89
|
else if (child === parent.getWhenFalse()) {
|
|
66
|
-
|
|
90
|
+
take(readTest(parent.getCondition(), isResponse), false);
|
|
67
91
|
}
|
|
68
92
|
}
|
|
69
93
|
if (Node.isBlock(parent) ||
|
|
@@ -79,25 +103,15 @@ export function reachedOnlyOnFailure(node, boundary, isResponse) {
|
|
|
79
103
|
const thenLeaves = cannotComplete(statement.getThenStatement());
|
|
80
104
|
const elseLeaves = otherwise !== undefined && cannotComplete(otherwise);
|
|
81
105
|
if (thenLeaves && !elseLeaves) {
|
|
82
|
-
|
|
106
|
+
take(readTest(statement.getExpression(), isResponse), false);
|
|
83
107
|
}
|
|
84
108
|
else if (elseLeaves && !thenLeaves) {
|
|
85
|
-
|
|
109
|
+
take(readTest(statement.getExpression(), isResponse), true);
|
|
86
110
|
}
|
|
87
111
|
}
|
|
88
112
|
}
|
|
89
113
|
}
|
|
90
|
-
|
|
91
|
-
return false;
|
|
92
|
-
let reachable = false;
|
|
93
|
-
for (let status = FIRST_STATUS; status <= LAST_STATUS; status++) {
|
|
94
|
-
if (!admitted(status))
|
|
95
|
-
continue;
|
|
96
|
-
if (SUCCEEDED(status))
|
|
97
|
-
return false;
|
|
98
|
-
reachable = true;
|
|
99
|
-
}
|
|
100
|
-
return reachable;
|
|
114
|
+
return sides;
|
|
101
115
|
}
|
|
102
116
|
/** The statuses `condition` lets through when it is true and when it is false. */
|
|
103
117
|
function readTest(condition, isResponse) {
|
|
@@ -107,10 +121,12 @@ function readTest(condition, isResponse) {
|
|
|
107
121
|
if (Node.isPrefixUnaryExpression(test) &&
|
|
108
122
|
test.getOperatorToken() === SyntaxKind.ExclamationToken) {
|
|
109
123
|
const inner = readTest(test.getOperand(), isResponse);
|
|
110
|
-
|
|
124
|
+
if (inner === UNRELATED || inner === UNREAD)
|
|
125
|
+
return inner;
|
|
126
|
+
return { whenTrue: inner.whenFalse, whenFalse: inner.whenTrue, inexact: inner.inexact };
|
|
111
127
|
}
|
|
112
128
|
if (isMemberOfResponse(test, 'ok', isResponse)) {
|
|
113
|
-
return { whenTrue: SUCCEEDED, whenFalse: (status) => !SUCCEEDED(status) };
|
|
129
|
+
return { whenTrue: SUCCEEDED, whenFalse: (status) => !SUCCEEDED(status), inexact: false };
|
|
114
130
|
}
|
|
115
131
|
if (Node.isBinaryExpression(test)) {
|
|
116
132
|
const operator = test.getOperatorToken().getKind();
|
|
@@ -118,24 +134,27 @@ function readTest(condition, isResponse) {
|
|
|
118
134
|
operator === SyntaxKind.BarBarToken) {
|
|
119
135
|
const left = readTest(test.getLeft(), isResponse);
|
|
120
136
|
const right = readTest(test.getRight(), isResponse);
|
|
121
|
-
if (left ===
|
|
122
|
-
return
|
|
137
|
+
if (left === UNRELATED && right === UNRELATED)
|
|
138
|
+
return UNRELATED;
|
|
139
|
+
const inexact = left.inexact || right.inexact || left === UNRELATED || right === UNRELATED;
|
|
123
140
|
return operator === SyntaxKind.AmpersandAmpersandToken
|
|
124
141
|
? {
|
|
125
142
|
whenTrue: (status) => left.whenTrue(status) && right.whenTrue(status),
|
|
126
143
|
whenFalse: (status) => left.whenFalse(status) || right.whenFalse(status),
|
|
144
|
+
inexact,
|
|
127
145
|
}
|
|
128
146
|
: {
|
|
129
147
|
whenTrue: (status) => left.whenTrue(status) || right.whenTrue(status),
|
|
130
148
|
whenFalse: (status) => left.whenFalse(status) && right.whenFalse(status),
|
|
149
|
+
inexact,
|
|
131
150
|
};
|
|
132
151
|
}
|
|
133
152
|
const compared = statusComparison(test.getLeft(), operator, test.getRight(), isResponse);
|
|
134
153
|
if (compared) {
|
|
135
|
-
return { whenTrue: compared, whenFalse: (status) => !compared(status) };
|
|
154
|
+
return { whenTrue: compared, whenFalse: (status) => !compared(status), inexact: false };
|
|
136
155
|
}
|
|
137
156
|
}
|
|
138
|
-
return
|
|
157
|
+
return readsResponseStatus(test, isResponse) ? UNREAD : UNRELATED;
|
|
139
158
|
}
|
|
140
159
|
/**
|
|
141
160
|
* `res.status <op> N` or `N <op> res.status`, as the statuses it is true for;
|
|
@@ -186,6 +205,11 @@ function isMemberOfResponse(node, name, isResponse) {
|
|
|
186
205
|
node.getName() === name &&
|
|
187
206
|
isResponse(node.getExpression()));
|
|
188
207
|
}
|
|
208
|
+
/** `node` reads the response's `ok` or `status`, itself or anywhere inside it. */
|
|
209
|
+
export function readsResponseStatus(node, isResponse) {
|
|
210
|
+
return [node, ...node.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression)].some((inner) => isMemberOfResponse(inner, 'ok', isResponse) ||
|
|
211
|
+
isMemberOfResponse(inner, 'status', isResponse));
|
|
212
|
+
}
|
|
189
213
|
/**
|
|
190
214
|
* A statement that never runs on into the statement after it: it returns,
|
|
191
215
|
* throws, breaks or continues, or every path through it does. A loop, a
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The function a line names, from an index built once per source file
|
|
3
|
+
* (carrick#1915).
|
|
4
|
+
*
|
|
5
|
+
* A signature request carries only the line its function starts on, and a
|
|
6
|
+
* signature pass sends one request per unannotated slot: thousands of
|
|
7
|
+
* lookups, several per function and many per file. Answering each by walking
|
|
8
|
+
* the file's every node cost the file's size per slot, and was 81% of the
|
|
9
|
+
* pass on a 2,345-file program. Here the file is walked once, over the
|
|
10
|
+
* compiler's own nodes, and each lookup reads the few entries near its line.
|
|
11
|
+
*
|
|
12
|
+
* The index belongs to the compiler's source file node, not to the file's
|
|
13
|
+
* name: replacing a file's text gives it a new node, so a file rewritten for
|
|
14
|
+
* one reading (the unwidened reading, the retype check) and then restored is
|
|
15
|
+
* indexed again each time, and never answered from positions it no longer has.
|
|
16
|
+
*/
|
|
17
|
+
import { type ArrowFunction, type FunctionDeclaration, type FunctionExpression, type MethodDeclaration, type SourceFile } from 'ts-morph';
|
|
18
|
+
/** The declarations a line can name. */
|
|
19
|
+
export type LineFunction = FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration;
|
|
20
|
+
/**
|
|
21
|
+
* The function whose declaration starts at, or within `LINE_TOLERANCE` lines
|
|
22
|
+
* of, the given line: the closest, then the smallest, then the first in the
|
|
23
|
+
* file. A function starting after the line is passed over when a statement
|
|
24
|
+
* that opens on the line or after it, on a line before the function's, does
|
|
25
|
+
* not contain the function. Undefined when no function is left.
|
|
26
|
+
*
|
|
27
|
+
* These are `TypeInferrer.findFunctionByLine`'s rules, which says why each is
|
|
28
|
+
* there; this is where they are computed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function functionAtLine(sourceFile: SourceFile, line: number): LineFunction | undefined;
|