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.
Files changed (40) hide show
  1. package/dist/hook/refresh.js +8 -1
  2. package/dist/hook/refresh.js.map +1 -1
  3. package/package.json +6 -6
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/sidecar/dist/src/capture/anchors.d.ts +22 -2
  6. package/sidecar/dist/src/capture/anchors.js +178 -34
  7. package/sidecar/dist/src/capture/api.d.ts +42 -1
  8. package/sidecar/dist/src/capture/check-classify.d.ts +9 -1
  9. package/sidecar/dist/src/capture/check-classify.js +15 -0
  10. package/sidecar/dist/src/capture/check-poison.js +4 -14
  11. package/sidecar/dist/src/capture/check.js +13 -1
  12. package/sidecar/dist/src/capture/index.js +55 -29
  13. package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
  14. package/sidecar/dist/src/capture/installed-package.js +2 -1
  15. package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
  16. package/sidecar/dist/src/capture/outside-root.js +39 -2
  17. package/sidecar/dist/src/capture/self-check.js +12 -13
  18. package/sidecar/dist/src/capture/service-config.d.ts +2 -0
  19. package/sidecar/dist/src/capture/service-config.js +1 -1
  20. package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
  21. package/sidecar/dist/src/capture/specifiers.js +25 -0
  22. package/sidecar/dist/src/failure-path.d.ts +34 -3
  23. package/sidecar/dist/src/failure-path.js +59 -35
  24. package/sidecar/dist/src/function-line-index.d.ts +30 -0
  25. package/sidecar/dist/src/function-line-index.js +162 -0
  26. package/sidecar/dist/src/index.d.ts +5 -0
  27. package/sidecar/dist/src/index.js +29 -5
  28. package/sidecar/dist/src/line-index.d.ts +9 -0
  29. package/sidecar/dist/src/line-index.js +26 -0
  30. package/sidecar/dist/src/printed-names.d.ts +43 -0
  31. package/sidecar/dist/src/printed-names.js +186 -0
  32. package/sidecar/dist/src/progress.d.ts +22 -0
  33. package/sidecar/dist/src/progress.js +31 -0
  34. package/sidecar/dist/src/retype.js +58 -128
  35. package/sidecar/dist/src/type-inferrer.d.ts +124 -13
  36. package/sidecar/dist/src/type-inferrer.js +531 -146
  37. package/sidecar/dist/src/type-structural-expander.js +10 -1
  38. package/sidecar/dist/src/types.d.ts +37 -6
  39. package/sidecar/dist/src/validators.d.ts +76 -0
  40. 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({ resolved, emitted, declarationSources, staging, surfaceDeclaration });
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: with the set of modules that DID reach the tree
524
- * (emitted .d.ts plus verbatim declaration sources), demote every anchor
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
- // Extensionless, entryDir-relative POSIX module ids present in the tree.
534
- const treeModules = new Set();
535
- let surfaceKey;
536
- for (const fileName of args.emitted.keys()) {
537
- const rel = path.relative(args.staging, fileName).split(path.sep).join('/');
538
- if (path.basename(rel) === args.surfaceDeclaration)
539
- surfaceKey = fileName;
540
- if (rel.endsWith('.d.ts'))
541
- treeModules.add(rel.slice(0, -'.d.ts'.length));
542
- }
543
- for (const rel of args.declarationSources.keys()) {
544
- if (rel.endsWith('.d.ts'))
545
- treeModules.add(rel.slice(0, -'.d.ts'.length));
546
- }
547
- // Deno's temporary surface lives in the cache inside the emitted tree.
548
- const surfaceDir = surfaceKey ? path.posix.dirname(path.relative(args.staging, surfaceKey).split(path.sep).join('/')) : '.';
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) && !moduleInTree(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
- function withoutExtension(subpath) {
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.endsWith('.d.ts'))
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
- const candidates = [
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
- * That is deliberately stricter than the retype check's reading of a status
24
- * test (`okWhenTrue` in retype.ts), which reads the false side of
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
- * That is deliberately stricter than the retype check's reading of a status
24
- * test (`okWhenTrue` in retype.ts), which reads the false side of
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
- const UNDECIDED = { whenTrue: EVERY_STATUS, whenFalse: EVERY_STATUS };
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
- let admitted = EVERY_STATUS;
44
- let narrowed = false;
45
- const narrow = (by) => {
46
- if (by === EVERY_STATUS)
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
- const before = admitted;
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
- narrow(readTest(parent.getExpression(), isResponse).whenTrue);
79
+ take(readTest(parent.getExpression(), isResponse), true);
56
80
  }
57
81
  else if (child === parent.getElseStatement()) {
58
- narrow(readTest(parent.getExpression(), isResponse).whenFalse);
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
- narrow(readTest(parent.getCondition(), isResponse).whenTrue);
87
+ take(readTest(parent.getCondition(), isResponse), true);
64
88
  }
65
89
  else if (child === parent.getWhenFalse()) {
66
- narrow(readTest(parent.getCondition(), isResponse).whenFalse);
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
- narrow(readTest(statement.getExpression(), isResponse).whenFalse);
106
+ take(readTest(statement.getExpression(), isResponse), false);
83
107
  }
84
108
  else if (elseLeaves && !thenLeaves) {
85
- narrow(readTest(statement.getExpression(), isResponse).whenTrue);
109
+ take(readTest(statement.getExpression(), isResponse), true);
86
110
  }
87
111
  }
88
112
  }
89
113
  }
90
- if (!narrowed)
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
- return { whenTrue: inner.whenFalse, whenFalse: inner.whenTrue };
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 === UNDECIDED && right === UNDECIDED)
122
- return UNDECIDED;
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 UNDECIDED;
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;