carrick 0.3.116 → 0.3.118

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 (42) hide show
  1. package/dist/contract.d.ts +2 -0
  2. package/dist/contract.js.map +1 -1
  3. package/dist/render.js +2 -1
  4. package/dist/render.js.map +1 -1
  5. package/package.json +6 -6
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/sidecar/dist/src/capture/anchors.d.ts +1 -0
  8. package/sidecar/dist/src/capture/anchors.js +1 -1
  9. package/sidecar/dist/src/capture/api.d.ts +6 -0
  10. package/sidecar/dist/src/capture/check-classify.js +25 -0
  11. package/sidecar/dist/src/capture/check-probe.d.ts +1 -1
  12. package/sidecar/dist/src/capture/check-probe.js +20 -0
  13. package/sidecar/dist/src/capture/checkout.d.ts +3 -0
  14. package/sidecar/dist/src/capture/checkout.js +1 -1
  15. package/sidecar/dist/src/capture/deep-walk.d.ts +10 -0
  16. package/sidecar/dist/src/capture/deep-walk.js +15 -0
  17. package/sidecar/dist/src/capture/guarded-fs.d.ts +6 -0
  18. package/sidecar/dist/src/capture/guarded-fs.js +10 -0
  19. package/sidecar/dist/src/capture/index.d.ts +5 -0
  20. package/sidecar/dist/src/capture/index.js +62 -7
  21. package/sidecar/dist/src/capture/project-references.d.ts +24 -13
  22. package/sidecar/dist/src/capture/project-references.js +53 -16
  23. package/sidecar/dist/src/capture/reference-redirect.d.ts +84 -0
  24. package/sidecar/dist/src/capture/reference-redirect.js +209 -0
  25. package/sidecar/dist/src/capture/unresolved.d.ts +8 -0
  26. package/sidecar/dist/src/capture/unresolved.js +18 -2
  27. package/sidecar/dist/src/declaration-attribution.d.ts +40 -0
  28. package/sidecar/dist/src/declaration-attribution.js +66 -0
  29. package/sidecar/dist/src/index.js +37 -0
  30. package/sidecar/dist/src/library-claims.js +234 -23
  31. package/sidecar/dist/src/module-format.d.ts +19 -1
  32. package/sidecar/dist/src/module-format.js +9 -2
  33. package/sidecar/dist/src/position-rows.d.ts +95 -0
  34. package/sidecar/dist/src/position-rows.js +844 -0
  35. package/sidecar/dist/src/project-loader.d.ts +36 -7
  36. package/sidecar/dist/src/project-loader.js +70 -28
  37. package/sidecar/dist/src/retype.js +13 -0
  38. package/sidecar/dist/src/type-inferrer.d.ts +25 -29
  39. package/sidecar/dist/src/type-inferrer.js +119 -70
  40. package/sidecar/dist/src/types.d.ts +78 -2
  41. package/sidecar/dist/src/validators.d.ts +431 -30
  42. package/sidecar/dist/src/validators.js +45 -1
@@ -38,6 +38,14 @@ export interface LoadResult {
38
38
  error?: string;
39
39
  initTimeMs?: number;
40
40
  }
41
+ /**
42
+ * What a build makes (carrick#2413). `listed` is the project as its config
43
+ * lists it. `scripts` has the same options with JavaScript accepted and lists
44
+ * no file: it is for the files the listed program does not take (a `.js`,
45
+ * `.mjs` or `.cjs` file under a config without `allowJs`), which the caller
46
+ * adds.
47
+ */
48
+ export type BuildKind = 'listed' | 'scripts';
41
49
  /**
42
50
  * ProjectLoader - Manages ts-morph Project initialization and access
43
51
  *
@@ -58,7 +66,10 @@ export interface LoadResult {
58
66
  */
59
67
  export declare class ProjectLoader {
60
68
  private project;
61
- /** Set by `load()`; builds the ts-morph project on first `getProject()`. */
69
+ /**
70
+ * Set by `load()`; builds the ts-morph project on first `getProject()`,
71
+ * and the default project's `scripts` program for `scriptProjectFor`.
72
+ */
62
73
  private buildProject;
63
74
  /**
64
75
  * The Deno graph this project resolved through, when it did (carrick#1260).
@@ -101,15 +112,18 @@ export declare class ProjectLoader {
101
112
  * Nothing at all otherwise, so a project is built as it always was.
102
113
  */
103
114
  private fileSystemOption;
104
- /** A ts-morph project built from one tsconfig and the files it lists. */
115
+ /**
116
+ * A ts-morph project built from one tsconfig: with the files it lists, or
117
+ * as its `scripts` program (`BuildKind`).
118
+ */
105
119
  private projectFromConfig;
106
120
  /**
107
121
  * Which project types `file` (carrick#1604): the key of its owning
108
- * project's program, or `''` for the default one. A tsconfig that
109
- * references other projects owns only the files it lists itself; any other
110
- * file belongs to the first project it references (depth-first, in
111
- * declared order) whose file list includes it, and is typed under that
112
- * project's options. A file no project includes, and every request that
122
+ * project's program, or `''` for the default one. In a tsconfig that
123
+ * references other projects, a file belongs to the first project in search
124
+ * order (the named config, then its references depth-first, in declared
125
+ * order) that lists it while none of its own references does (carrick#2418),
126
+ * and is typed under that project's options. A file no project includes, and every request that
113
127
  * names no file, uses the default project, built from the named tsconfig
114
128
  * as before.
115
129
  */
@@ -119,6 +133,21 @@ export declare class ProjectLoader {
119
133
  * kept: a service whose files all belong to one project builds one.
120
134
  */
121
135
  getProjectFor(key: string): Project;
136
+ /**
137
+ * The `scripts` program (`BuildKind`) of the project behind a key from
138
+ * `projectKeyFor`: its options with JavaScript accepted, built by the same
139
+ * factory, and no file listed. Not kept; the caller drops it. For the walk's
140
+ * files that the owner's program does not take (carrick#2413).
141
+ */
142
+ scriptProjectFor(key: string): Project;
143
+ /**
144
+ * Drop the program behind a key from `projectKeyFor`, so the memory it
145
+ * holds can go before the next one is built. The next `getProjectFor` on
146
+ * the key builds it again. For a walk that reads one owner's program at a
147
+ * time and never comes back to it (carrick#2405); the actions that keep
148
+ * their programs for the life of the process never call it.
149
+ */
150
+ releaseProjectFor(key: string): void;
122
151
  /**
123
152
  * Get the ts-morph Project, building it on first call.
124
153
  *
@@ -12,7 +12,7 @@
12
12
  import { Project, getCompilerOptionsFromTsConfig } from 'ts-morph';
13
13
  import * as path from 'node:path';
14
14
  import * as fs from 'node:fs';
15
- import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath, sidecarCompilerOptions, untrustedReadBound, } from './capture/index.js';
15
+ import { DenoProject, findDenoConfig, findServiceTsconfig, referenceRedirectOf, serviceConfigPath, sidecarCompilerOptions, untrustedReadBound, } from './capture/index.js';
16
16
  import { moduleFormatResolutionHost } from './module-format.js';
17
17
  import { ExternalImports, registerExternalImports } from './origin.js';
18
18
  /**
@@ -44,6 +44,10 @@ const DEFAULT_COMPILER_OPTIONS = {
44
44
  resolveJsonModule: true,
45
45
  isolatedModules: true,
46
46
  };
47
+ /** `options` as `kind` builds them. */
48
+ function optionsFor(kind, options) {
49
+ return kind === 'scripts' ? { ...options, allowJs: true } : options;
50
+ }
47
51
  /**
48
52
  * Map string module values to ts-morph enum values
49
53
  */
@@ -108,7 +112,10 @@ const TARGET_MAP = {
108
112
  */
109
113
  export class ProjectLoader {
110
114
  project = null;
111
- /** Set by `load()`; builds the ts-morph project on first `getProject()`. */
115
+ /**
116
+ * Set by `load()`; builds the ts-morph project on first `getProject()`,
117
+ * and the default project's `scripts` program for `scriptProjectFor`.
118
+ */
112
119
  buildProject = null;
113
120
  /**
114
121
  * The Deno graph this project resolved through, when it did (carrick#1260).
@@ -175,13 +182,14 @@ export class ProjectLoader {
175
182
  if (this.tsconfigSnapshot) {
176
183
  const snapshot = this.tsconfigSnapshot;
177
184
  this.log('Project will load with tsconfig snapshot');
178
- this.buildProject = () => {
185
+ this.buildProject = (kind) => {
179
186
  const project = new Project({
180
187
  ...this.fileSystemOption(),
181
- compilerOptions: sidecarCompilerOptions(this.snapshotToCompilerOptions(snapshot)),
188
+ compilerOptions: sidecarCompilerOptions(optionsFor(kind, this.snapshotToCompilerOptions(snapshot))),
182
189
  skipAddingFilesFromTsConfig: true,
183
190
  });
184
- this.addDefaultSourceFiles(project);
191
+ if (kind === 'listed')
192
+ this.addDefaultSourceFiles(project);
185
193
  return project;
186
194
  };
187
195
  }
@@ -191,7 +199,7 @@ export class ProjectLoader {
191
199
  const denoConfig = findDenoConfig(this.repoRoot, this.tsconfigPath);
192
200
  if (denoConfig) {
193
201
  this.log(`Project will load with Deno config: ${denoConfig.configPath}`);
194
- this.buildProject = () => {
202
+ this.buildProject = (kind) => {
195
203
  const deno = new DenoProject(denoConfig, this.repoRoot);
196
204
  this.denoProject = deno;
197
205
  // The graph's external-library verdicts, kept past the program
@@ -199,7 +207,7 @@ export class ProjectLoader {
199
207
  const imports = new ExternalImports();
200
208
  const project = new Project({
201
209
  ...this.fileSystemOption(),
202
- compilerOptions: sidecarCompilerOptions(deno.parsed.options),
210
+ compilerOptions: sidecarCompilerOptions(optionsFor(kind, deno.parsed.options)),
203
211
  skipAddingFilesFromTsConfig: true,
204
212
  resolutionHost: imports.recording((host, getOptions) => ({
205
213
  resolveModuleNames: (names, from) => names.map(name => deno.resolve(name, from, getOptions(), host)),
@@ -207,28 +215,31 @@ export class ProjectLoader {
207
215
  })),
208
216
  });
209
217
  registerExternalImports(project, imports);
210
- for (const file of deno.parsed.fileNames)
211
- project.addSourceFileAtPath(file);
212
- for (const diagnostic of deno.diagnostics)
213
- this.logError(diagnostic);
218
+ if (kind === 'listed') {
219
+ for (const file of deno.parsed.fileNames)
220
+ project.addSourceFileAtPath(file);
221
+ for (const diagnostic of deno.diagnostics)
222
+ this.logError(diagnostic);
223
+ }
214
224
  return project;
215
225
  };
216
226
  }
217
227
  else if (tsconfigPath) {
218
228
  this.log(`Project will load with tsconfig: ${tsconfigPath}`);
219
229
  this.namedTsconfigPath = tsconfigPath;
220
- this.buildProject = () => this.projectFromConfig(tsconfigPath);
230
+ this.buildProject = (kind) => this.projectFromConfig(tsconfigPath, kind);
221
231
  }
222
232
  else {
223
233
  this.log('No tsconfig.json found, using default compiler options');
224
- this.buildProject = () => {
234
+ this.buildProject = (kind) => {
225
235
  const project = new Project({
226
236
  ...this.fileSystemOption(),
227
- compilerOptions: sidecarCompilerOptions(DEFAULT_COMPILER_OPTIONS),
237
+ compilerOptions: sidecarCompilerOptions(optionsFor(kind, DEFAULT_COMPILER_OPTIONS)),
228
238
  skipAddingFilesFromTsConfig: true,
229
239
  });
230
240
  // Add source files from common locations
231
- this.addDefaultSourceFiles(project);
241
+ if (kind === 'listed')
242
+ this.addDefaultSourceFiles(project);
232
243
  return project;
233
244
  };
234
245
  }
@@ -330,30 +341,34 @@ export class ProjectLoader {
330
341
  const bound = untrustedReadBound(this.repoRoot);
331
342
  return bound ? { fileSystem: boundedFileSystem(bound) } : {};
332
343
  }
333
- /** A ts-morph project built from one tsconfig and the files it lists. */
334
- projectFromConfig(configPath) {
344
+ /**
345
+ * A ts-morph project built from one tsconfig: with the files it lists, or
346
+ * as its `scripts` program (`BuildKind`).
347
+ */
348
+ projectFromConfig(configPath, kind = 'listed') {
335
349
  const imports = new ExternalImports();
336
350
  const project = new Project({
337
351
  ...this.fileSystemOption(),
338
352
  tsConfigFilePath: configPath,
339
353
  // Over the tsconfig's own options: ts-morph lays these on top.
340
- compilerOptions: sidecarCompilerOptions(getCompilerOptionsFromTsConfig(configPath).options),
341
- skipAddingFilesFromTsConfig: false,
354
+ compilerOptions: sidecarCompilerOptions(optionsFor(kind, getCompilerOptionsFromTsConfig(configPath).options)),
355
+ skipAddingFilesFromTsConfig: kind === 'scripts',
342
356
  // Each import resolves in its file's own module format (carrick#1619),
343
- // and every external-library answer is kept past the program rebuilds
344
- // that drop it (carrick#1731).
345
- resolutionHost: imports.recording(moduleFormatResolutionHost),
357
+ // a referenced project's unbuilt output reads as its source, under its
358
+ // own options (carrick#2418), and every external-library answer is kept
359
+ // past the program rebuilds that drop it (carrick#1731).
360
+ resolutionHost: imports.recording(moduleFormatResolutionHost(referenceRedirectOf(configPath, this.repoRoot))),
346
361
  });
347
362
  registerExternalImports(project, imports);
348
363
  return project;
349
364
  }
350
365
  /**
351
366
  * Which project types `file` (carrick#1604): the key of its owning
352
- * project's program, or `''` for the default one. A tsconfig that
353
- * references other projects owns only the files it lists itself; any other
354
- * file belongs to the first project it references (depth-first, in
355
- * declared order) whose file list includes it, and is typed under that
356
- * project's options. A file no project includes, and every request that
367
+ * project's program, or `''` for the default one. In a tsconfig that
368
+ * references other projects, a file belongs to the first project in search
369
+ * order (the named config, then its references depth-first, in declared
370
+ * order) that lists it while none of its own references does (carrick#2418),
371
+ * and is typed under that project's options. A file no project includes, and every request that
357
372
  * names no file, uses the default project, built from the named tsconfig
358
373
  * as before.
359
374
  */
@@ -381,6 +396,33 @@ export class ProjectLoader {
381
396
  }
382
397
  return project;
383
398
  }
399
+ /**
400
+ * The `scripts` program (`BuildKind`) of the project behind a key from
401
+ * `projectKeyFor`: its options with JavaScript accepted, built by the same
402
+ * factory, and no file listed. Not kept; the caller drops it. For the walk's
403
+ * files that the owner's program does not take (carrick#2413).
404
+ */
405
+ scriptProjectFor(key) {
406
+ if (!this.initialized || !this.buildProject) {
407
+ throw new Error('Project not initialized. Call load() first and ensure it succeeds.');
408
+ }
409
+ return key === '' ? this.buildProject('scripts') : this.projectFromConfig(key, 'scripts');
410
+ }
411
+ /**
412
+ * Drop the program behind a key from `projectKeyFor`, so the memory it
413
+ * holds can go before the next one is built. The next `getProjectFor` on
414
+ * the key builds it again. For a walk that reads one owner's program at a
415
+ * time and never comes back to it (carrick#2405); the actions that keep
416
+ * their programs for the life of the process never call it.
417
+ */
418
+ releaseProjectFor(key) {
419
+ if (key === '') {
420
+ this.project = null;
421
+ }
422
+ else {
423
+ this.ownerProjects.delete(key);
424
+ }
425
+ }
384
426
  /**
385
427
  * Get the ts-morph Project, building it on first call.
386
428
  *
@@ -393,7 +435,7 @@ export class ProjectLoader {
393
435
  }
394
436
  if (!this.project) {
395
437
  const startTime = performance.now();
396
- this.project = this.buildProject();
438
+ this.project = this.buildProject('listed');
397
439
  const buildTimeMs = Math.round(performance.now() - startTime);
398
440
  this.log(`Project built in ${buildTimeMs}ms ` +
399
441
  `(${this.project.getSourceFiles().length} source files)`);
@@ -290,6 +290,13 @@ export class Retyper {
290
290
  remaining.set(k, left - 1);
291
291
  continue;
292
292
  }
293
+ // The rewrite only removes or replaces source text, so an unused
294
+ // declaration that is new after it is the rewrite's own doing: the
295
+ // statement it replaced was the declaration's only use
296
+ // (carrick#2426). Unused diagnostics the file already raised are
297
+ // matched out above and never reach here.
298
+ if (UNUSED_DECLARATION_CODES.has(d.code))
299
+ continue;
293
300
  added.push(mapped);
294
301
  }
295
302
  added.sort((a, b) => a.start - b.start || a.code - b.code);
@@ -828,6 +835,12 @@ function bodyReadEdit(read) {
828
835
  function declaresTypeParameters(call) {
829
836
  return (resolvedDeclaration(call)?.typeParameters?.length ?? 0) > 0;
830
837
  }
838
+ /**
839
+ * "Declared but never used" and its grouped forms: unused locals and imports
840
+ * (6133, 6192, 6196, 6198, 6199), unused properties (6138) and unused type
841
+ * parameters (6205).
842
+ */
843
+ const UNUSED_DECLARATION_CODES = new Set([6133, 6138, 6192, 6196, 6198, 6199, 6205]);
831
844
  function key(d) {
832
845
  return `${d.start}:${d.code}`;
833
846
  }
@@ -357,9 +357,13 @@ export declare class TypeInferrer {
357
357
  * Returns `undefined` when this does not apply, which leaves the caller's
358
358
  * own reading of the located expression untouched:
359
359
  * - the row's line is neither a route registration nor a line inside the
360
- * handler the located send sits in;
360
+ * handler the located send sits in, unless the located send states an
361
+ * error or redirect status and the handler keeps another send (carrick#2463);
361
362
  * - the located expression is neither a send nor the argument of one;
362
- * - that send is not one the handler returns;
363
+ * - that send is neither one the handler returns nor a statement of it;
364
+ * - it is a success send the handler does not return (a statement send
365
+ * joins only when the located send is dropped, and only when it ends
366
+ * the handler's path);
363
367
  * - it is a success send and the only one that survives, so the join would
364
368
  * be the located body anyway;
365
369
  * - no joined body is the located one (a text body beside JSON ones).
@@ -370,6 +374,23 @@ export declare class TypeInferrer {
370
374
  * or the redirect location back in.
371
375
  */
372
376
  private inferFromResponseSites;
377
+ /**
378
+ * The response sends `func` makes as statements (`res.status(409).json(e);`),
379
+ * for a handler that sends through a parameter and returns nothing. Only the
380
+ * outermost call of a statement is a send, only in `func`'s own scope, and
381
+ * only where `isSend` accepts it, so a logger call or an `await save()`
382
+ * statement is never one.
383
+ */
384
+ private responseSendStatements;
385
+ /**
386
+ * True when nothing in `func` runs after `statement`: a bare `return;`
387
+ * follows it, or it is the last statement of its block and the statement
388
+ * that block belongs to (an `if` branch, a `try` or `catch` block) is itself
389
+ * last, up to `func`'s body. A statement inside a loop, a `switch` case or
390
+ * any other construct does not end the path; a send there is not read as a
391
+ * success body.
392
+ */
393
+ private statementEndsFunctionPath;
373
394
  /**
374
395
  * True when `candidate` is a response send: a call or `new` whose result is
375
396
  * transport machinery (by the extraction config's verified rule or the
@@ -1533,35 +1554,10 @@ export declare class TypeInferrer {
1533
1554
  * The npm package name that declares a type. `undefined` when the type has
1534
1555
  * no declaration to read (a top type, a primitive, an anonymous object
1535
1556
  * literal) or when nothing owns its declaration but the workspace itself.
1536
- *
1537
- * The module graph is asked first, where there is one, because it is the
1538
- * only thing that can answer for a project whose resolution does not go
1539
- * through `node_modules`. Deno resolves an npm dependency's types straight
1540
- * out of its own cache
1541
- * (`$DENO_DIR/npm/<registry-host>/<name>/<version>/…`), a path with no
1542
- * `node_modules` segment anywhere in it, so the scan below found no package
1543
- * for ANY dependency-declared type on such a repo and every receiver the
1544
- * compiler had typed correctly went unclassified (carrick#1260). It runs
1545
- * first rather than second because a mixed checkout has both, and the graph
1546
- * is what actually resolved the module.
1547
- *
1548
- * The path scan is the Node answer: the LAST `node_modules` segment wins,
1549
- * which is what a nested or content-addressed store
1550
- * (`node_modules/.store/pkg@1.0.0/node_modules/pkg`) requires. Scoped names
1551
- * keep both segments.
1557
+ * The declaration is the attributed one (carrick#2031), and the package is
1558
+ * named by the module graph first, then by the path (`installedPackageOf`).
1552
1559
  */
1553
1560
  private declaringPackageOf;
1554
- /**
1555
- * The declaration a symbol is attributed to (carrick#2031). One declaration
1556
- * is itself. Where several files merge one symbol (a global interface the
1557
- * compiler's library declares and a typings package augments), their order
1558
- * is the order the checker merged them in, and on TypeScript 6 that changes
1559
- * once the program is rebuilt, so the first one is not an answer. The
1560
- * compiler's default library wins where it declares the symbol; otherwise,
1561
- * and among the library's own files, the declaring file whose path sorts
1562
- * first, then the earliest declaration in it.
1563
- */
1564
- private attributedDeclaration;
1565
1561
  /**
1566
1562
  * The awaited return type of the member invoked on the receiver. Reported as
1567
1563
  * a fact next to the receiver; a caller that classified on it alone would be
@@ -28,6 +28,7 @@ import { externalImportsOf, isExternalOrigin } from './origin.js';
28
28
  import { reachedOnlyOnFailure } from './failure-path.js';
29
29
  import { readResponseModes } from './response-modes.js';
30
30
  import { functionAtLine } from './function-line-index.js';
31
+ import { attributedDeclaration, installedPackageOf } from './declaration-attribution.js';
31
32
  import { endLineOf, startLineOf } from './line-index.js';
32
33
  import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
33
34
  import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statesNoShape, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
@@ -1566,9 +1567,13 @@ export class TypeInferrer {
1566
1567
  * Returns `undefined` when this does not apply, which leaves the caller's
1567
1568
  * own reading of the located expression untouched:
1568
1569
  * - the row's line is neither a route registration nor a line inside the
1569
- * handler the located send sits in;
1570
+ * handler the located send sits in, unless the located send states an
1571
+ * error or redirect status and the handler keeps another send (carrick#2463);
1570
1572
  * - the located expression is neither a send nor the argument of one;
1571
- * - that send is not one the handler returns;
1573
+ * - that send is neither one the handler returns nor a statement of it;
1574
+ * - it is a success send the handler does not return (a statement send
1575
+ * joins only when the located send is dropped, and only when it ends
1576
+ * the handler's path);
1572
1577
  * - it is a success send and the only one that survives, so the join would
1573
1578
  * be the located body anyway;
1574
1579
  * - no joined body is the located one (a text body beside JSON ones).
@@ -1585,27 +1590,53 @@ export class TypeInferrer {
1585
1590
  // The row's line is a registration, or a line inside the handler the
1586
1591
  // located send sits in (carrick#2005: a file-based route's row names the
1587
1592
  // send's own line, and a send's line is no registration). A row whose
1588
- // line is outside that function says nothing about it.
1589
- if (!this.registrationAtLine(sourceFile, request.line_number) &&
1590
- !(func.getStartLineNumber() <= request.line_number &&
1591
- request.line_number <= func.getEndLineNumber())) {
1592
- return undefined;
1593
- }
1594
- const branches = this.responseReturnedExpressions(func).flatMap((expression) => this.expandResponseBranches(expression, 0));
1595
- if (branches.length === 0)
1596
- return undefined;
1597
- const serialisers = this.calleesProvenSerialiser(branches);
1593
+ // line is outside that function says nothing about the function, so it
1594
+ // is "untied" (carrick#2463): only a located error or redirect send is
1595
+ // walked then, and only when a send survives the drop.
1596
+ const tied = !!this.registrationAtLine(sourceFile, request.line_number) ||
1597
+ (func.getStartLineNumber() <= request.line_number &&
1598
+ request.line_number <= func.getEndLineNumber());
1599
+ const returnedBranches = this.responseReturnedExpressions(func).flatMap((expression) => this.expandResponseBranches(expression, 0));
1600
+ const serialisers = this.calleesProvenSerialiser(returnedBranches);
1598
1601
  const isSend = (candidate) => this.isResponseSend(candidate, extractionConfig, serialisers);
1602
+ // carrick#2427: a handler that sends through a parameter returns nothing,
1603
+ // its sends are statements, and the located one can be an error send.
1604
+ // Statement sends find and drop that branch. A call whose result is
1605
+ // response machinery is not always a body send, though (a header or
1606
+ // cookie setter returns the response too), so a statement send is a
1607
+ // success body only when the located send is dropped and the statement
1608
+ // ends the handler's path. A located success send joins the returned
1609
+ // sends alone.
1610
+ const statementSends = this.responseSendStatements(func, isSend);
1611
+ const locatingBranches = [...returnedBranches, ...statementSends.map((s) => s.send)];
1612
+ if (locatingBranches.length === 0)
1613
+ return undefined;
1599
1614
  const peeled = this.peelTransparentExpression(located);
1600
1615
  const site = isSend(peeled) ? peeled : this.receivingCallOf(located, isSend);
1601
1616
  if (!site)
1602
1617
  return undefined;
1603
1618
  const contains = (outer, inner) => outer.getStart() <= inner.getStart() && inner.getEnd() <= outer.getEnd();
1604
- const locatedBranch = branches.find((branch) => contains(branch, site));
1619
+ const locatedBranch = locatingBranches.find((branch) => contains(branch, site));
1605
1620
  if (!locatedBranch)
1606
1621
  return undefined;
1607
1622
  const dropped = (status) => status === 'error' || status === 'redirect';
1608
1623
  const siteStatus = this.responseSiteStatus(site, true);
1624
+ if (!tied && !dropped(siteStatus))
1625
+ return undefined;
1626
+ let branches;
1627
+ if (dropped(siteStatus)) {
1628
+ branches = [
1629
+ ...returnedBranches,
1630
+ ...statementSends
1631
+ .filter((s) => s.send === locatedBranch || this.statementEndsFunctionPath(s.statement, func))
1632
+ .map((s) => s.send),
1633
+ ];
1634
+ }
1635
+ else {
1636
+ if (!returnedBranches.includes(locatedBranch))
1637
+ return undefined;
1638
+ branches = returnedBranches;
1639
+ }
1609
1640
  const survivors = [];
1610
1641
  for (const branch of branches) {
1611
1642
  const status = branch === locatedBranch ? siteStatus : this.responseSiteStatus(branch, true);
@@ -1619,6 +1650,10 @@ export class TypeInferrer {
1619
1650
  continue;
1620
1651
  survivors.push(branch);
1621
1652
  }
1653
+ // carrick#2463 (D2): an untied row claims "no success payload" only when
1654
+ // the function it reads keeps a send after the error sends are dropped.
1655
+ if (!tied && survivors.length === 0)
1656
+ return undefined;
1622
1657
  const locatedDropped = dropped(siteStatus);
1623
1658
  if (!locatedDropped && survivors.length <= 1)
1624
1659
  return undefined;
@@ -1646,6 +1681,73 @@ export class TypeInferrer {
1646
1681
  }
1647
1682
  return undefined;
1648
1683
  }
1684
+ /**
1685
+ * The response sends `func` makes as statements (`res.status(409).json(e);`),
1686
+ * for a handler that sends through a parameter and returns nothing. Only the
1687
+ * outermost call of a statement is a send, only in `func`'s own scope, and
1688
+ * only where `isSend` accepts it, so a logger call or an `await save()`
1689
+ * statement is never one.
1690
+ */
1691
+ responseSendStatements(func, isSend) {
1692
+ const body = func.getBody();
1693
+ if (!body || !Node.isBlock(body))
1694
+ return [];
1695
+ const sends = [];
1696
+ for (const statement of body.getDescendantsOfKind(SyntaxKind.ExpressionStatement)) {
1697
+ if (this.findContainingFunctionForNode(statement) !== func)
1698
+ continue;
1699
+ const expression = this.peelTransparentExpression(statement.getExpression());
1700
+ if (isSend(expression))
1701
+ sends.push({ statement, send: expression });
1702
+ }
1703
+ return sends;
1704
+ }
1705
+ /**
1706
+ * True when nothing in `func` runs after `statement`: a bare `return;`
1707
+ * follows it, or it is the last statement of its block and the statement
1708
+ * that block belongs to (an `if` branch, a `try` or `catch` block) is itself
1709
+ * last, up to `func`'s body. A statement inside a loop, a `switch` case or
1710
+ * any other construct does not end the path; a send there is not read as a
1711
+ * success body.
1712
+ */
1713
+ statementEndsFunctionPath(statement, func) {
1714
+ const body = func.getBody();
1715
+ const next = statement.getNextSibling();
1716
+ if (next && Node.isReturnStatement(next) && !next.getExpression())
1717
+ return true;
1718
+ let current = statement;
1719
+ for (;;) {
1720
+ const parent = current.getParent();
1721
+ if (!parent)
1722
+ return false;
1723
+ if (Node.isIfStatement(parent)) {
1724
+ current = parent;
1725
+ continue;
1726
+ }
1727
+ if (!Node.isBlock(parent))
1728
+ return false;
1729
+ const statements = parent.getStatements();
1730
+ if (statements[statements.length - 1] !== current)
1731
+ return false;
1732
+ if (parent === body)
1733
+ return true;
1734
+ const owner = parent.getParent();
1735
+ if (!owner)
1736
+ return false;
1737
+ if (Node.isIfStatement(owner) || Node.isTryStatement(owner) || Node.isBlock(owner)) {
1738
+ current = Node.isBlock(owner) ? parent : owner;
1739
+ continue;
1740
+ }
1741
+ if (Node.isCatchClause(owner)) {
1742
+ const tryStatement = owner.getParent();
1743
+ if (!tryStatement || !Node.isTryStatement(tryStatement))
1744
+ return false;
1745
+ current = tryStatement;
1746
+ continue;
1747
+ }
1748
+ return false;
1749
+ }
1750
+ }
1649
1751
  /**
1650
1752
  * True when `candidate` is a response send: a call or `new` whose result is
1651
1753
  * transport machinery (by the extraction config's verified rule or the
@@ -5849,69 +5951,16 @@ export class TypeInferrer {
5849
5951
  * The npm package name that declares a type. `undefined` when the type has
5850
5952
  * no declaration to read (a top type, a primitive, an anonymous object
5851
5953
  * literal) or when nothing owns its declaration but the workspace itself.
5852
- *
5853
- * The module graph is asked first, where there is one, because it is the
5854
- * only thing that can answer for a project whose resolution does not go
5855
- * through `node_modules`. Deno resolves an npm dependency's types straight
5856
- * out of its own cache
5857
- * (`$DENO_DIR/npm/<registry-host>/<name>/<version>/…`), a path with no
5858
- * `node_modules` segment anywhere in it, so the scan below found no package
5859
- * for ANY dependency-declared type on such a repo and every receiver the
5860
- * compiler had typed correctly went unclassified (carrick#1260). It runs
5861
- * first rather than second because a mixed checkout has both, and the graph
5862
- * is what actually resolved the module.
5863
- *
5864
- * The path scan is the Node answer: the LAST `node_modules` segment wins,
5865
- * which is what a nested or content-addressed store
5866
- * (`node_modules/.store/pkg@1.0.0/node_modules/pkg`) requires. Scoped names
5867
- * keep both segments.
5954
+ * The declaration is the attributed one (carrick#2031), and the package is
5955
+ * named by the module graph first, then by the path (`installedPackageOf`).
5868
5956
  */
5869
5957
  declaringPackageOf(type) {
5870
5958
  const symbol = type.getSymbol() ?? type.getAliasSymbol();
5871
- const declaration = this.attributedDeclaration(symbol?.getDeclarations() ?? []);
5959
+ const declaration = attributedDeclaration(() => this.project.getProgram().compilerObject, (symbol?.getDeclarations() ?? []).map((node) => node.compilerNode));
5872
5960
  if (!declaration) {
5873
5961
  return undefined;
5874
5962
  }
5875
- const filePath = declaration.getSourceFile().getFilePath().replace(/\\/g, '/');
5876
- const named = this.packageOf?.(filePath);
5877
- if (named) {
5878
- return named;
5879
- }
5880
- const marker = '/node_modules/';
5881
- const index = filePath.lastIndexOf(marker);
5882
- if (index < 0) {
5883
- return undefined;
5884
- }
5885
- const rest = filePath.slice(index + marker.length).split('/');
5886
- if (rest.length === 0 || rest[0].length === 0) {
5887
- return undefined;
5888
- }
5889
- if (rest[0].startsWith('@')) {
5890
- return rest.length > 1 ? `${rest[0]}/${rest[1]}` : undefined;
5891
- }
5892
- return rest[0];
5893
- }
5894
- /**
5895
- * The declaration a symbol is attributed to (carrick#2031). One declaration
5896
- * is itself. Where several files merge one symbol (a global interface the
5897
- * compiler's library declares and a typings package augments), their order
5898
- * is the order the checker merged them in, and on TypeScript 6 that changes
5899
- * once the program is rebuilt, so the first one is not an answer. The
5900
- * compiler's default library wins where it declares the symbol; otherwise,
5901
- * and among the library's own files, the declaring file whose path sorts
5902
- * first, then the earliest declaration in it.
5903
- */
5904
- attributedDeclaration(declarations) {
5905
- if (declarations.length <= 1)
5906
- return declarations[0];
5907
- const program = this.project.getProgram().compilerObject;
5908
- const fromLibrary = declarations.filter((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile().compilerNode));
5909
- const candidates = fromLibrary.length > 0 ? fromLibrary : declarations;
5910
- return [...candidates].sort((a, b) => {
5911
- const pathA = a.getSourceFile().getFilePath();
5912
- const pathB = b.getSourceFile().getFilePath();
5913
- return pathA < pathB ? -1 : pathA > pathB ? 1 : a.getStart() - b.getStart();
5914
- })[0];
5963
+ return installedPackageOf(declaration.getSourceFile().fileName, this.packageOf);
5915
5964
  }
5916
5965
  /**
5917
5966
  * The awaited return type of the member invoked on the receiver. Reported as