carrick 0.3.124 → 0.3.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carrick",
3
- "version": "0.3.124",
3
+ "version": "0.3.125",
4
4
  "carrickCache": {
5
5
  "cache_version": 22,
6
6
  "intent_cache_version": 2
@@ -113,11 +113,11 @@
113
113
  "zod": "^3.23.0"
114
114
  },
115
115
  "optionalDependencies": {
116
- "@carrick-tools/cli-darwin-arm64": "0.3.124",
117
- "@carrick-tools/cli-darwin-x64": "0.3.124",
118
- "@carrick-tools/cli-linux-arm64": "0.3.124",
119
- "@carrick-tools/cli-linux-x64": "0.3.124",
120
- "@carrick-tools/cli-win32-x64": "0.3.124"
116
+ "@carrick-tools/cli-darwin-arm64": "0.3.125",
117
+ "@carrick-tools/cli-darwin-x64": "0.3.125",
118
+ "@carrick-tools/cli-linux-arm64": "0.3.125",
119
+ "@carrick-tools/cli-linux-x64": "0.3.125",
120
+ "@carrick-tools/cli-win32-x64": "0.3.125"
121
121
  },
122
122
  "devDependencies": {
123
123
  "@types/node": "^24.13.3"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "carrick",
3
3
  "description": "Claude Code plugin for Carrick, which indexes TypeScript codebases across service and repository boundaries. After each edit it adds the file's routes, calls, and cross-service type mismatches to the session, and it registers Carrick's language server. It pairs with the Carrick MCP server, which lets agents search functions by intent rather than name.",
4
- "version": "0.3.124",
4
+ "version": "0.3.125",
5
5
  "author": {
6
6
  "name": "Carrick",
7
7
  "email": "hello@carrick.tools"
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import { decisiveAssignmentLine, unwidenedAliasOf, } from './check-probe.js';
31
31
  import { scrubDiagnostic, scrubPaths } from './check-scrub.js';
32
+ import { onlyDeclaredOpenAny } from './deep-walk.js';
32
33
  import { describeFieldReport, fieldReportNotes, } from './check-fields.js';
33
34
  const PRIMARY_RE = /^(?<file>(?:[a-zA-Z]:)?[^(]*?)\((?<line>\d+),(?<col>\d+)\): error TS(?<code>\d+): (?<msg>.*)$/;
34
35
  /** Parse `tsc --pretty false` output into structured diagnostics. */
@@ -350,9 +351,10 @@ function onUnwidenedLines(plan, diagnostic) {
350
351
  * - its own declarations typecheck (the alias is not poisoned);
351
352
  * - the member-level walk ran over it and found no `any`/`unknown`, which
352
353
  * would assign to whatever the other side declares;
353
- * - the same walk found none on the two published sides either. A mismatch
354
- * that is not a fact stays the mismatch it was: the reading would be
355
- * clearing it against a member that accepts anything.
354
+ * - the same walk found none on the two published sides either, an open
355
+ * member included (carrick#2591). A mismatch that is not a fact stays the
356
+ * mismatch it was: the reading would be clearing it against a member that
357
+ * accepts anything.
356
358
  */
357
359
  function unwidenedReadingFits(input) {
358
360
  const { plan, probeDiags, poisonReason } = input;
@@ -364,7 +366,7 @@ function unwidenedReadingFits(input) {
364
366
  if (poisonReason(sent.service_name, unwidenedAliasOf(sent.alias)))
365
367
  return false;
366
368
  const walked = input.deepFindings?.sentUnwidened;
367
- return walked !== undefined && walked.length === 0 && factness(input).resolved;
369
+ return walked !== undefined && walked.length === 0 && factness(input, true).resolved;
368
370
  }
369
371
  /**
370
372
  * The field-level sentence appended to a mismatch diagnostic
@@ -410,7 +412,7 @@ function namedFields(input, scrubCtx) {
410
412
  * empty. A walk that could not run leaves the verdict not-a-fact: absence of
411
413
  * findings is not evidence.
412
414
  */
413
- function factness(input) {
415
+ function factness(input, strict = false) {
414
416
  const findings = input.deepFindings;
415
417
  if (!findings) {
416
418
  return {
@@ -419,6 +421,10 @@ function factness(input) {
419
421
  };
420
422
  }
421
423
  for (const side of ['sent', 'expected']) {
424
+ // Open members only (carrick#2591): the closed members were judged and an
425
+ // open one is never a mismatch, so the verdict is a fact about the type.
426
+ if (!strict && onlyDeclaredOpenAny(findings[side]))
427
+ continue;
422
428
  const first = findings[side][0];
423
429
  if (!first)
424
430
  continue;
@@ -25,6 +25,7 @@ import { scrubPaths } from './check-scrub.js';
25
25
  import { assembleWorkspace, writeProbes, } from './check-workspace.js';
26
26
  import { buildPoisonIndexes } from './check-poison.js';
27
27
  import { openProbeProgram, probeDeepFindings } from './check-deep.js';
28
+ import { onlyDeclaredOpenAny } from './deep-walk.js';
28
29
  import { pairFieldReports } from './check-fields.js';
29
30
  import { pairDispatchUnions } from './check-union.js';
30
31
  function runProcess(command, args, cwd, env) {
@@ -465,7 +466,13 @@ function deepDecayOf(plan, aliasRecords) {
465
466
  const record = aliasRecords.get(endpoint.service_name)?.get(endpoint.alias);
466
467
  // The capture emits every deep finding it made; the FIRST is the one the
467
468
  // pre-gate reads, exactly as when the walk stopped at the first hit.
468
- const finding = record?.any_provenance?.[0];
469
+ const findings = record?.any_provenance ?? [];
470
+ // An `any` the source declares under a named member is an open member
471
+ // (carrick#2591): the pair is probed, and the compiler judges the closed
472
+ // members. Anything else on the record keeps the pair out of the probe.
473
+ if (onlyDeclaredOpenAny(findings))
474
+ continue;
475
+ const finding = findings[0];
469
476
  if (finding) {
470
477
  return { side, kind: finding.kind, path: finding.path };
471
478
  }
@@ -20,6 +20,14 @@ import type { TypeProvenance } from './api.js';
20
20
  * within its depth/node budget, treated as unverifiable (fail closed) rather
21
21
  * than silently clean. */
22
22
  export type DeepTopType = Pick<TypeProvenance, 'kind' | 'path'>;
23
+ /** Cap on findings reported per alias. The FIRST one is what the check phase
24
+ * pre-gates on, so verdicts never depend on this number; the rest are there to
25
+ * tell a reader which fields are `any` (carrick#376), and a type with more
26
+ * than this many is already better described by "this type is not typed".
27
+ * The scanner mirrors it (`CAPTURE_FINDINGS_CAP` in
28
+ * `src/engine/type_compat_v2.rs`): a record this long may have stopped
29
+ * listing, so it cannot vouch for every position (carrick#1752). */
30
+ export declare const MAX_DEEP_FINDINGS = 32;
23
31
  /**
24
32
  * Depth-bounded structural walk for a disqualifying top type at ANY depth:
25
33
  * a member, array element, index signature, type argument, or callable RETURN
@@ -76,6 +84,29 @@ export declare function findUncarriedPositions(root: ts.Type, program: ts.Progra
76
84
  /** TypeScript's unresolved-reference placeholder: `TypeFlags.Any` with the
77
85
  * internal `intrinsicName === 'error'` (stable since TS 1.x; see `anchors.ts`). */
78
86
  export declare function isErrorPlaceholder(t: ts.Type): boolean;
87
+ /**
88
+ * True when a deep-walk path passes through a named member (the same rule as
89
+ * `path_names_a_member` in `src/engine/type_compat_v2.rs`). The walk writes a
90
+ * member as its name, an element or type argument as `<n>`, an index signature
91
+ * as `[index]` and a callable return as `()`; a path made of the last three
92
+ * alone sits at the root of the type or inside a container, never in a field.
93
+ */
94
+ export declare function pathNamesAMember(path: string): boolean;
95
+ /**
96
+ * True when every finding is an `any` the SOURCE declares under a named
97
+ * member (carrick#2591): an OPEN member. The policy: an open member is never
98
+ * a mismatch, and the closed members are judged as for a fully typed pair.
99
+ * `any` accepts and supplies every shape in the assignment the probe makes, so
100
+ * the compiler already judges only the closed members.
101
+ *
102
+ * Nothing else qualifies. A callable's return (`getData()`) is no data
103
+ * member. An `unknown` member rejects a narrower counterparty
104
+ * under assignment, so it would read as a mismatch the policy forbids; a
105
+ * `budget_exhausted` sentinel, a root top type and an `any` an unresolved
106
+ * import left say nothing about the author's intent. A list as long as the
107
+ * walk's cap may have stopped listing, so it cannot vouch for every position.
108
+ */
109
+ export declare function onlyDeclaredOpenAny(findings: readonly TypeProvenance[]): boolean;
79
110
  /** What an anchor's SOURCE program could not resolve (carrick#1164). */
80
111
  export interface UnresolvedAtAnchor {
81
112
  /** Member paths holding the unresolved-reference placeholder. */
@@ -22,7 +22,7 @@ import { memberPath } from './member-name.js';
22
22
  * The scanner mirrors it (`CAPTURE_FINDINGS_CAP` in
23
23
  * `src/engine/type_compat_v2.rs`): a record this long may have stopped
24
24
  * listing, so it cannot vouch for every position (carrick#1752). */
25
- const MAX_DEEP_FINDINGS = 32;
25
+ export const MAX_DEEP_FINDINGS = 32;
26
26
  /**
27
27
  * Depth-bounded structural walk for a disqualifying top type at ANY depth:
28
28
  * a member, array element, index signature, type argument, or callable RETURN
@@ -269,6 +269,48 @@ function walkTopTypes(root, program, checker, location, flagOf) {
269
269
  rest.sort((a, b) => a.path.localeCompare(b.path));
270
270
  return [head, ...rest];
271
271
  }
272
+ /**
273
+ * True when a deep-walk path passes through a named member (the same rule as
274
+ * `path_names_a_member` in `src/engine/type_compat_v2.rs`). The walk writes a
275
+ * member as its name, an element or type argument as `<n>`, an index signature
276
+ * as `[index]` and a callable return as `()`; a path made of the last three
277
+ * alone sits at the root of the type or inside a container, never in a field.
278
+ */
279
+ export function pathNamesAMember(path) {
280
+ const rest = path.replaceAll('[index]', '').replaceAll('()', '');
281
+ let inArgument = false;
282
+ for (const c of rest) {
283
+ if (c === '<')
284
+ inArgument = true;
285
+ else if (c === '>')
286
+ inArgument = false;
287
+ else if (c !== '.' && !inArgument)
288
+ return true;
289
+ }
290
+ return false;
291
+ }
292
+ /**
293
+ * True when every finding is an `any` the SOURCE declares under a named
294
+ * member (carrick#2591): an OPEN member. The policy: an open member is never
295
+ * a mismatch, and the closed members are judged as for a fully typed pair.
296
+ * `any` accepts and supplies every shape in the assignment the probe makes, so
297
+ * the compiler already judges only the closed members.
298
+ *
299
+ * Nothing else qualifies. A callable's return (`getData()`) is no data
300
+ * member. An `unknown` member rejects a narrower counterparty
301
+ * under assignment, so it would read as a mismatch the policy forbids; a
302
+ * `budget_exhausted` sentinel, a root top type and an `any` an unresolved
303
+ * import left say nothing about the author's intent. A list as long as the
304
+ * walk's cap may have stopped listing, so it cannot vouch for every position.
305
+ */
306
+ export function onlyDeclaredOpenAny(findings) {
307
+ return (findings.length > 0 &&
308
+ findings.length < MAX_DEEP_FINDINGS &&
309
+ findings.every((f) => f.kind === 'any' &&
310
+ f.reason === 'declared' &&
311
+ pathNamesAMember(f.path) &&
312
+ !f.path.includes('()')));
313
+ }
272
314
  /** The disqualifier the self-check and the check phase gate on (see
273
315
  * `findDisqualifyingTopTypes`). */
274
316
  function disqualifyingFlag(t) {
@@ -34,7 +34,7 @@ export { sidecarCompilerOptions } from './compiler-options.js';
34
34
  export { isInsideCheckout, untrustedReadBound, UNTRUSTED_TREE_ENV, type ReadBound } from './checkout.js';
35
35
  export { serviceConfigPath } from './project-references.js';
36
36
  export { referenceRedirectOf } from './reference-redirect.js';
37
- export { findServiceTsconfig } from './service-config.js';
37
+ export { findServiceTsconfig, fileConfigPath } from './service-config.js';
38
38
  export { WriteGuard } from './guarded-fs.js';
39
39
  export { installedPackageDirectory } from './installed-package-directory.js';
40
40
  export { isDeclarationFileName, packageNameOf } from './specifiers.js';
@@ -52,7 +52,7 @@ export { sidecarCompilerOptions } from './compiler-options.js';
52
52
  export { isInsideCheckout, untrustedReadBound, UNTRUSTED_TREE_ENV } from './checkout.js';
53
53
  export { serviceConfigPath } from './project-references.js';
54
54
  export { referenceRedirectOf } from './reference-redirect.js';
55
- export { findServiceTsconfig } from './service-config.js';
55
+ export { findServiceTsconfig, fileConfigPath } from './service-config.js';
56
56
  // carrick#2405: the per-position rows write through the guard, and name an
57
57
  // installed declaring file the way the capture's resolution walk does.
58
58
  export { WriteGuard } from './guarded-fs.js';
@@ -19,3 +19,14 @@
19
19
  export declare function findServiceTsconfig(serviceRoot: string, scanRoot?: string): string | undefined;
20
20
  /** The path with every symlink resolved; the path as given when it does not exist. */
21
21
  export declare function realPath(p: string): string;
22
+ /**
23
+ * The config an editor types `file` under, for a service with none at its
24
+ * root (carrick#1604): the nearest `tsconfig.json`, then `jsconfig.json`, in
25
+ * each directory from the file's up to the service root, inclusive. The
26
+ * climb stops after a directory named `node_modules`, as tsserver's does. A file
27
+ * outside the service root, or with no config on its way up, has none.
28
+ *
29
+ * `known` caches the answer per directory; pass one map for one service so
30
+ * its files share the climb.
31
+ */
32
+ export declare function fileConfigPath(file: string, serviceRoot: string, known?: Map<string, string | undefined>): string | undefined;
@@ -55,3 +55,43 @@ export function realPath(p) {
55
55
  return path.resolve(p);
56
56
  }
57
57
  }
58
+ /** The names an editor looks for in a file's directory, in order. */
59
+ const FILE_CONFIG_NAMES = ['tsconfig.json', 'jsconfig.json'];
60
+ /**
61
+ * The config an editor types `file` under, for a service with none at its
62
+ * root (carrick#1604): the nearest `tsconfig.json`, then `jsconfig.json`, in
63
+ * each directory from the file's up to the service root, inclusive. The
64
+ * climb stops after a directory named `node_modules`, as tsserver's does. A file
65
+ * outside the service root, or with no config on its way up, has none.
66
+ *
67
+ * `known` caches the answer per directory; pass one map for one service so
68
+ * its files share the climb.
69
+ */
70
+ export function fileConfigPath(file, serviceRoot, known = new Map()) {
71
+ const root = path.resolve(serviceRoot);
72
+ const climbed = [];
73
+ let found;
74
+ let dir = path.dirname(path.resolve(file));
75
+ for (;;) {
76
+ const from = path.relative(root, dir);
77
+ if (from.startsWith('..') || path.isAbsolute(from))
78
+ break;
79
+ if (known.has(dir)) {
80
+ found = known.get(dir);
81
+ break;
82
+ }
83
+ climbed.push(dir);
84
+ const name = FILE_CONFIG_NAMES.find((n) => fs.existsSync(path.join(dir, n)));
85
+ if (name !== undefined) {
86
+ found = path.join(dir, name);
87
+ break;
88
+ }
89
+ // tsserver looks in a `node_modules` directory itself, then stops.
90
+ if (dir === root || path.basename(dir) === 'node_modules')
91
+ break;
92
+ dir = path.dirname(dir);
93
+ }
94
+ for (const d of climbed)
95
+ known.set(d, found);
96
+ return found;
97
+ }
@@ -80,6 +80,16 @@ export declare class ProjectLoader {
80
80
  private denoProject;
81
81
  /** The tsconfig the service names, when the project is built from one. */
82
82
  private namedTsconfigPath;
83
+ /**
84
+ * Set when the service has no config at or above its root: each file is then
85
+ * typed under the nearest `tsconfig.json`/`jsconfig.json` on its way up to
86
+ * the service root (carrick#1604), and the default project holds the rest.
87
+ */
88
+ private inferredService;
89
+ /** The per-directory answers of `fileConfigPath` for this service. */
90
+ private readonly fileConfigs;
91
+ /** The reference owner rule of each config a file was found under. */
92
+ private readonly ownerRules;
83
93
  /** Each file's owning config (carrick#1604), resolved on first use. */
84
94
  private configPathFor;
85
95
  /** Programs built for owning projects other than the named tsconfig. */
@@ -128,6 +138,12 @@ export declare class ProjectLoader {
128
138
  * as before.
129
139
  */
130
140
  projectKeyFor(file?: string): string;
141
+ /**
142
+ * `projectKeyFor` for a service with no config at its root: the owner of the
143
+ * nearest config on the file's way up to the service root, or `''` (the
144
+ * default project, under the inferred options) for a file with none.
145
+ */
146
+ private inferredKeyFor;
131
147
  /**
132
148
  * The project behind a key from `projectKeyFor`, built on first use and
133
149
  * kept: a service whose files all belong to one project builds one.
@@ -11,8 +11,9 @@
11
11
  */
12
12
  import { Project, getCompilerOptionsFromTsConfig } from 'ts-morph';
13
13
  import * as path from 'node:path';
14
+ import ts from 'typescript';
14
15
  import * as fs from 'node:fs';
15
- import { DenoProject, findDenoConfig, findServiceTsconfig, referenceRedirectOf, serviceConfigPath, sidecarCompilerOptions, untrustedReadBound, } from './capture/index.js';
16
+ import { DenoProject, findDenoConfig, fileConfigPath, findServiceTsconfig, referenceRedirectOf, serviceConfigPath, sidecarCompilerOptions, untrustedReadBound, } from './capture/index.js';
16
17
  import { moduleFormatResolutionHost } from './module-format.js';
17
18
  import { ExternalImports, registerExternalImports } from './origin.js';
18
19
  /**
@@ -92,6 +93,11 @@ const TARGET_MAP = {
92
93
  'ES2023': 10,
93
94
  'ESNext': 99,
94
95
  };
96
+ /** Whether the config's own `compilerOptions` state `maxNodeModuleJsDepth`. */
97
+ function statesJsDepth(configPath) {
98
+ const raw = ts.readConfigFile(configPath, ts.sys.readFile).config;
99
+ return raw?.compilerOptions?.maxNodeModuleJsDepth !== undefined;
100
+ }
95
101
  /**
96
102
  * ProjectLoader - Manages ts-morph Project initialization and access
97
103
  *
@@ -126,6 +132,16 @@ export class ProjectLoader {
126
132
  denoProject;
127
133
  /** The tsconfig the service names, when the project is built from one. */
128
134
  namedTsconfigPath;
135
+ /**
136
+ * Set when the service has no config at or above its root: each file is then
137
+ * typed under the nearest `tsconfig.json`/`jsconfig.json` on its way up to
138
+ * the service root (carrick#1604), and the default project holds the rest.
139
+ */
140
+ inferredService = false;
141
+ /** The per-directory answers of `fileConfigPath` for this service. */
142
+ fileConfigs = new Map();
143
+ /** The reference owner rule of each config a file was found under. */
144
+ ownerRules = new Map();
129
145
  /** Each file's owning config (carrick#1604), resolved on first use. */
130
146
  configPathFor;
131
147
  /** Programs built for owning projects other than the named tsconfig. */
@@ -231,6 +247,7 @@ export class ProjectLoader {
231
247
  }
232
248
  else {
233
249
  this.log('No tsconfig.json found, using default compiler options');
250
+ this.inferredService = true;
234
251
  this.buildProject = (kind) => {
235
252
  const project = new Project({
236
253
  ...this.fileSystemOption(),
@@ -239,7 +256,7 @@ export class ProjectLoader {
239
256
  });
240
257
  // Add source files from common locations
241
258
  if (kind === 'listed')
242
- this.addDefaultSourceFiles(project);
259
+ this.addDefaultSourceFiles(project, (file) => this.projectKeyFor(file) === '');
243
260
  return project;
244
261
  };
245
262
  }
@@ -347,11 +364,18 @@ export class ProjectLoader {
347
364
  */
348
365
  projectFromConfig(configPath, kind = 'listed') {
349
366
  const imports = new ExternalImports();
367
+ const options = getCompilerOptionsFromTsConfig(configPath).options;
368
+ // TypeScript reads a `jsconfig.json` with an implicit depth of 2, which
369
+ // loads the JavaScript sources of installed packages; every other program
370
+ // here reads 0. A depth the config states is kept (carrick#1604).
371
+ if (this.inferredService && path.basename(configPath) === 'jsconfig.json' && !statesJsDepth(configPath)) {
372
+ options.maxNodeModuleJsDepth = 0;
373
+ }
350
374
  const project = new Project({
351
375
  ...this.fileSystemOption(),
352
376
  tsConfigFilePath: configPath,
353
377
  // Over the tsconfig's own options: ts-morph lays these on top.
354
- compilerOptions: sidecarCompilerOptions(optionsFor(kind, getCompilerOptionsFromTsConfig(configPath).options)),
378
+ compilerOptions: sidecarCompilerOptions(optionsFor(kind, options)),
355
379
  skipAddingFilesFromTsConfig: kind === 'scripts',
356
380
  // Each import resolves in its file's own module format (carrick#1619),
357
381
  // a referenced project's unbuilt output reads as its source, under its
@@ -373,12 +397,33 @@ export class ProjectLoader {
373
397
  * as before.
374
398
  */
375
399
  projectKeyFor(file) {
400
+ if (this.inferredService)
401
+ return this.inferredKeyFor(file);
376
402
  if (!this.namedTsconfigPath || file === undefined)
377
403
  return '';
378
404
  this.configPathFor ??= serviceConfigPath(this.namedTsconfigPath, (d) => this.logError(d));
379
405
  const config = this.configPathFor(path.resolve(this.repoRoot, file));
380
406
  return config === this.namedTsconfigPath ? '' : config;
381
407
  }
408
+ /**
409
+ * `projectKeyFor` for a service with no config at its root: the owner of the
410
+ * nearest config on the file's way up to the service root, or `''` (the
411
+ * default project, under the inferred options) for a file with none.
412
+ */
413
+ inferredKeyFor(file) {
414
+ if (file === undefined)
415
+ return '';
416
+ const absolute = path.resolve(this.repoRoot, file);
417
+ const config = fileConfigPath(absolute, this.repoRoot, this.fileConfigs);
418
+ if (config === undefined)
419
+ return '';
420
+ let owner = this.ownerRules.get(config);
421
+ if (!owner) {
422
+ owner = serviceConfigPath(config, (d) => this.logError(d));
423
+ this.ownerRules.set(config, owner);
424
+ }
425
+ return owner(absolute);
426
+ }
382
427
  /**
383
428
  * The project behind a key from `projectKeyFor`, built on first use and
384
429
  * kept: a service whose files all belong to one project builds one.
@@ -512,7 +557,7 @@ export class ProjectLoader {
512
557
  * dependencies installed it cost 40 s and added nothing the compiler needs
513
558
  * (carrick#749).
514
559
  */
515
- addDefaultSourceFiles(project) {
560
+ addDefaultSourceFiles(project, keep = () => true) {
516
561
  const globs = DEFAULT_SOURCE_PATTERNS.map((pattern) => path.join(this.repoRoot, pattern));
517
562
  // Negated last so it applies to every pattern above it.
518
563
  globs.push(`!${path.join(this.repoRoot, '**/node_modules/**')}`);
@@ -525,6 +570,8 @@ export class ProjectLoader {
525
570
  return;
526
571
  }
527
572
  for (const filePath of filePaths) {
573
+ if (!keep(filePath))
574
+ continue;
528
575
  try {
529
576
  project.addSourceFileAtPath(filePath);
530
577
  }
@@ -1800,8 +1800,10 @@ export declare class TypeInferrer {
1800
1800
  * - a cast on it: `(await request.json()) as NewWidget`;
1801
1801
  * - the annotation of the binding it initialises:
1802
1802
  * `const input: NewWidget = await request.json()`;
1803
- * - a schema the read, or the binding that holds it, is handed to
1804
- * (`schemaConsumingRead`): the schema's input is what a caller may send.
1803
+ * - a type the handler states for the read further on
1804
+ * (`statedInFunction`): a schema it is handed to, the annotation of a
1805
+ * name it is assigned to, a pass-through function, a cast of the whole
1806
+ * value.
1805
1807
  *
1806
1808
  * The first two are `typeNodeStatedAtRead`, the reading a consumer's
1807
1809
  * response read already gets. An annotation further out is not one of them:
@@ -2030,19 +2032,58 @@ export declare class TypeInferrer {
2030
2032
  */
2031
2033
  private middlewareBodySchemaValues;
2032
2034
  /**
2033
- * The contract of the schema a handler validates an untyped body read with
2034
- * (carrick#1166), or null.
2035
- *
2036
- * `read` is the located expression: the read call, or the binding that holds
2037
- * it. Every call in the enclosing function that takes that value as an
2038
- * argument is a candidate, and the schema is the call's receiver
2039
- * (`Schema.safeParse(body)`) or one of its other arguments
2040
- * (`parse(Schema, body)`). A candidate counts only when it declares schema
2041
- * types (`schemaOutputType` / `schemaInputType`), so a logger or a service
2042
- * call the body is also handed to is never read as a contract. No method or
2043
- * library name is matched.
2044
- */
2045
- private schemaConsumingRead;
2035
+ * The type the function a value is made in states for it, where the
2036
+ * compiler types the value itself `any` or `unknown` (carrick#2594), or
2037
+ * null.
2038
+ *
2039
+ * `value` is the located expression: a body read, or a call whose result is
2040
+ * untyped. The value is followed through the names that hold it: a
2041
+ * declaration it initialises, and a name it is assigned to later
2042
+ * (`let body: unknown; body = await read()`). At each place the value or
2043
+ * one of those names is used whole, the source can state a type:
2044
+ *
2045
+ * 0. the annotation of the name that holds it (`let body: Upload`), or the
2046
+ * declared return type of the function that returns it unchanged (only
2047
+ * with `reads.returned`);
2048
+ * 1. a schema it is handed to (`Schema.safeParse(body ?? {})`,
2049
+ * `parse(Schema, body)`): the schema's input is what a sender may send
2050
+ * (carrick#1166). A candidate counts only when it declares schema types,
2051
+ * so no method or library name is matched;
2052
+ * 2. a function of the project it is handed to that returns that argument
2053
+ * unchanged (through casts) or an empty default: its declared return
2054
+ * type. A function that builds a new value from its argument is a
2055
+ * converter, and its return type describes what it built, never the
2056
+ * value it was handed, so it states nothing here;
2057
+ * 3. a cast of the whole value (`(body as { phone: string }).phone`);
2058
+ * 4. the parameter of a call it is handed to (only with
2059
+ * `reads.parameters`), where that parameter's type is a body shape once
2060
+ * any callable member is dropped (a function never crosses the wire).
2061
+ *
2062
+ * The first rank with an answer decides. A placeholder (`unknown`,
2063
+ * `Record<string, unknown>`, any type node with `any` or `unknown` written
2064
+ * in it) states nothing, and two different statements in one rank decide
2065
+ * nothing: the answer is null.
2066
+ */
2067
+ private statedInFunction;
2068
+ /**
2069
+ * What a call that takes the value at argument `index` states for it:
2070
+ * a schema among its receiver and other arguments (rank 1), a callee of the
2071
+ * project that returns that argument unchanged (rank 2), or, with
2072
+ * `reads.parameters`, the parameter's own type (rank 4).
2073
+ */
2074
+ private stateAtReceivingCall;
2075
+ /**
2076
+ * The project function `call` invokes, when every value it returns is its
2077
+ * argument at `index` as it was handed in (through parentheses, casts,
2078
+ * `satisfies` and `!`) or an empty default (`{}`, `[]`, `null`,
2079
+ * `undefined`), and at least one return is that argument. Such a function
2080
+ * narrows its argument, so its declared return type describes the value it
2081
+ * was handed. Any other function builds something new from it, and is not
2082
+ * this.
2083
+ */
2084
+ private passThroughFunction;
2085
+ /** `{}`, `[]`, `null` or `undefined`: a default that states no member. */
2086
+ private isEmptyDefault;
2046
2087
  /**
2047
2088
  * The part a validated read names when that part is not a body, or
2048
2089
  * `undefined` (carrick#1166).
@@ -1996,6 +1996,27 @@ export class TypeInferrer {
1996
1996
  });
1997
1997
  }
1998
1998
  typeString = this.unwrapPromise(typeString, returnType);
1999
+ // carrick#2594: the call's value is untyped (`Promise<any>`,
2000
+ // `Promise<unknown>`), and the function it is made in states its type:
2001
+ // it returns the value from a function with a declared return type, holds
2002
+ // it in an annotated name, validates it with a schema, narrows it with a
2003
+ // function that returns it unchanged, casts it whole, or hands it to a
2004
+ // typed parameter. A function that converts the value into something new
2005
+ // states nothing about it, and neither does a use on a failure path.
2006
+ const untypedAnswer = typeString.trim() === 'any' || typeString.trim() === 'unknown';
2007
+ if (untypedAnswer && func && !explicitType && !unwrapResult.wasUnwrapped && !carrierPayload) {
2008
+ const stated = this.statedInFunction(callExpr, func, {
2009
+ returned: true,
2010
+ parameters: true,
2011
+ skipFailurePaths: true,
2012
+ });
2013
+ if (stated) {
2014
+ this.log(`Call result at ${where} is untyped, and its function states its type ` +
2015
+ `(${stated.how}); publishing that type`);
2016
+ typeString = stated.text;
2017
+ isExplicit = true;
2018
+ }
2019
+ }
1999
2020
  // #336: consumer analogue of the #306 producer anchor. Anchor on the
2000
2021
  // CALL's own payload (the rule-extracted Type, else the awaited call
2001
2022
  // type), peeling array levels to the element symbol + depth. Without the
@@ -2659,18 +2680,22 @@ export class TypeInferrer {
2659
2680
  isExplicit = true;
2660
2681
  }
2661
2682
  // carrick#1166: the read itself is untyped (`await c.req.json()` is `any`
2662
- // or `unknown`), and the handler validates it by handing it to a schema:
2663
- // `Schema.safeParse(body)`, `Schema.parse(await c.req.json())`. The schema
2664
- // declares what a caller may send, so its INPUT is the request contract,
2665
- // read the way every other request anchor reads a schema (carrick#1101).
2683
+ // or `unknown`), and the handler states its type further on: it validates
2684
+ // it with a schema (`Schema.safeParse(body ?? {})`, whose INPUT is the
2685
+ // request contract, carrick#1101), assigns it to an annotated name
2686
+ // (`let body: T; body = await c.req.json()`), narrows it with a function
2687
+ // that returns it unchanged, or casts the whole value (carrick#2594).
2666
2688
  const readType = this.unwrapPromiseType(payloadType);
2667
2689
  if (!explicitType &&
2668
2690
  !unwrapResult.wasUnwrapped &&
2669
2691
  (readType.isAny() || readType.isUnknown())) {
2670
- const parsedWith = this.schemaConsumingRead(node);
2692
+ const func = this.findContainingFunctionForNode(node);
2693
+ const parsedWith = func
2694
+ ? this.statedInFunction(node, func, { returned: false, parameters: false, skipFailurePaths: false })
2695
+ : null;
2671
2696
  if (parsedWith) {
2672
2697
  this.log(`Request locator at ${request.file_path}:${request.line_number} is an untyped read ` +
2673
- "the handler validates with a schema; publishing the schema's input");
2698
+ `its handler states a type for (${parsedWith.how}); publishing that type`);
2674
2699
  return this.declaredRequestInferredType(request, parsedWith, this.getNodeLocation(node));
2675
2700
  }
2676
2701
  }
@@ -6606,8 +6631,10 @@ export class TypeInferrer {
6606
6631
  * - a cast on it: `(await request.json()) as NewWidget`;
6607
6632
  * - the annotation of the binding it initialises:
6608
6633
  * `const input: NewWidget = await request.json()`;
6609
- * - a schema the read, or the binding that holds it, is handed to
6610
- * (`schemaConsumingRead`): the schema's input is what a caller may send.
6634
+ * - a type the handler states for the read further on
6635
+ * (`statedInFunction`): a schema it is handed to, the annotation of a
6636
+ * name it is assigned to, a pass-through function, a cast of the whole
6637
+ * value.
6611
6638
  *
6612
6639
  * The first two are `typeNodeStatedAtRead`, the reading a consumer's
6613
6640
  * response read already gets. An annotation further out is not one of them:
@@ -6623,10 +6650,13 @@ export class TypeInferrer {
6623
6650
  'reads off the platform request, as the source types that read');
6624
6651
  return this.createInferredType(request, statedText, true, this.getNodeLocation(read));
6625
6652
  }
6626
- const validated = this.schemaConsumingRead(read);
6653
+ const func = this.findContainingFunctionForNode(read);
6654
+ const validated = func
6655
+ ? this.statedInFunction(read, func, { returned: false, parameters: false, skipFailurePaths: false })
6656
+ : null;
6627
6657
  if (validated) {
6628
6658
  this.log(`Request at ${request.file_path}:${request.line_number} is the body its handler ` +
6629
- "reads off the platform request and validates with a schema; publishing the schema's input");
6659
+ `reads off the platform request and states a type for (${validated.how}); publishing that type`);
6630
6660
  return this.declaredRequestInferredType(request, validated, this.getNodeLocation(read));
6631
6661
  }
6632
6662
  return null;
@@ -7285,70 +7315,280 @@ export class TypeInferrer {
7285
7315
  return values;
7286
7316
  }
7287
7317
  /**
7288
- * The contract of the schema a handler validates an untyped body read with
7289
- * (carrick#1166), or null.
7290
- *
7291
- * `read` is the located expression: the read call, or the binding that holds
7292
- * it. Every call in the enclosing function that takes that value as an
7293
- * argument is a candidate, and the schema is the call's receiver
7294
- * (`Schema.safeParse(body)`) or one of its other arguments
7295
- * (`parse(Schema, body)`). A candidate counts only when it declares schema
7296
- * types (`schemaOutputType` / `schemaInputType`), so a logger or a service
7297
- * call the body is also handed to is never read as a contract. No method or
7298
- * library name is matched.
7299
- */
7300
- schemaConsumingRead(read) {
7301
- const func = this.findContainingFunctionForNode(read);
7302
- if (!func)
7303
- return null;
7304
- const bindings = new Set();
7305
- const addBinding = (identifier) => {
7306
- const symbol = identifier?.getSymbol()?.compilerSymbol;
7307
- if (symbol)
7308
- bindings.add(symbol);
7318
+ * The type the function a value is made in states for it, where the
7319
+ * compiler types the value itself `any` or `unknown` (carrick#2594), or
7320
+ * null.
7321
+ *
7322
+ * `value` is the located expression: a body read, or a call whose result is
7323
+ * untyped. The value is followed through the names that hold it: a
7324
+ * declaration it initialises, and a name it is assigned to later
7325
+ * (`let body: unknown; body = await read()`). At each place the value or
7326
+ * one of those names is used whole, the source can state a type:
7327
+ *
7328
+ * 0. the annotation of the name that holds it (`let body: Upload`), or the
7329
+ * declared return type of the function that returns it unchanged (only
7330
+ * with `reads.returned`);
7331
+ * 1. a schema it is handed to (`Schema.safeParse(body ?? {})`,
7332
+ * `parse(Schema, body)`): the schema's input is what a sender may send
7333
+ * (carrick#1166). A candidate counts only when it declares schema types,
7334
+ * so no method or library name is matched;
7335
+ * 2. a function of the project it is handed to that returns that argument
7336
+ * unchanged (through casts) or an empty default: its declared return
7337
+ * type. A function that builds a new value from its argument is a
7338
+ * converter, and its return type describes what it built, never the
7339
+ * value it was handed, so it states nothing here;
7340
+ * 3. a cast of the whole value (`(body as { phone: string }).phone`);
7341
+ * 4. the parameter of a call it is handed to (only with
7342
+ * `reads.parameters`), where that parameter's type is a body shape once
7343
+ * any callable member is dropped (a function never crosses the wire).
7344
+ *
7345
+ * The first rank with an answer decides. A placeholder (`unknown`,
7346
+ * `Record<string, unknown>`, any type node with `any` or `unknown` written
7347
+ * in it) states nothing, and two different statements in one rank decide
7348
+ * nothing: the answer is null.
7349
+ */
7350
+ statedInFunction(value, func, reads) {
7351
+ const holders = new Set();
7352
+ const positions = [value];
7353
+ const holderUse = (node) => {
7354
+ if (!Node.isIdentifier(node))
7355
+ return false;
7356
+ const symbol = node.getSymbol()?.compilerSymbol;
7357
+ return !!symbol && holders.has(symbol);
7309
7358
  };
7310
- if (Node.isIdentifier(read)) {
7311
- addBinding(read);
7312
- }
7313
- else {
7314
- const declaration = read.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
7315
- const initializer = declaration?.getInitializer();
7316
- if (declaration &&
7317
- initializer &&
7318
- this.unwrapExpressionNode(initializer) === read &&
7319
- Node.isIdentifier(declaration.getNameNode())) {
7320
- addBinding(declaration.getNameNode());
7321
- }
7322
- }
7323
- const carriesRead = (argument) => argument === read ||
7324
- (Node.isIdentifier(argument) &&
7325
- bindings.has(argument.getSymbol()?.compilerSymbol));
7326
- for (const call of func.getDescendantsOfKind(SyntaxKind.CallExpression)) {
7327
- const args = call.getArguments().map((arg) => this.unwrapExpressionNode(arg));
7328
- const carrier = args.findIndex(carriesRead);
7329
- if (carrier < 0)
7359
+ const hold = (name) => {
7360
+ if (!Node.isIdentifier(name))
7361
+ return;
7362
+ const symbol = name.getSymbol()?.compilerSymbol;
7363
+ if (!symbol || holders.has(symbol))
7364
+ return;
7365
+ holders.add(symbol);
7366
+ for (const id of func.getDescendantsOfKind(SyntaxKind.Identifier)) {
7367
+ if (id === name || id.getSymbol()?.compilerSymbol !== symbol)
7368
+ continue;
7369
+ const parent = id.getParent();
7370
+ if (Node.isVariableDeclaration(parent) && parent.getNameNode() === id)
7371
+ continue;
7372
+ if (Node.isBinaryExpression(parent) &&
7373
+ parent.getLeft() === id &&
7374
+ parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
7375
+ continue;
7376
+ }
7377
+ positions.push(id);
7378
+ }
7379
+ };
7380
+ hold(value);
7381
+ const found = [];
7382
+ const stateNode = (rank, typeNode, at, how) => {
7383
+ if (!typeNode || this.leavesAPositionOpen(typeNode))
7384
+ return;
7385
+ const text = this.structuralTextFromTypeNode(typeNode);
7386
+ if (text)
7387
+ found.push({ rank, text, at, how });
7388
+ };
7389
+ for (let i = 0; i < positions.length; i++) {
7390
+ const position = positions[i];
7391
+ if (reads.skipFailurePaths &&
7392
+ position !== value &&
7393
+ reachedOnlyOnFailure(position, func, holderUse)) {
7330
7394
  continue;
7331
- const candidates = [];
7332
- const callee = call.getExpression();
7333
- if (Node.isPropertyAccessExpression(callee)) {
7334
- candidates.push(callee.getExpression());
7335
7395
  }
7336
- args.forEach((arg, index) => {
7337
- if (index !== carrier)
7338
- candidates.push(arg);
7339
- });
7340
- for (const candidate of candidates) {
7341
- const candidateType = candidate.getType();
7342
- if (!this.schemaOutputType(candidateType, candidate) &&
7343
- !this.schemaInputType(candidateType, candidate)) {
7396
+ // Climb the wrappers that leave the value what it was: parentheses,
7397
+ // `await`, `!`, `satisfies`, a placeholder cast, and an empty default
7398
+ // (`body ?? {}`).
7399
+ let outer = position;
7400
+ for (;;) {
7401
+ const parent = outer.getParent();
7402
+ if (!parent)
7403
+ break;
7404
+ if ((Node.isParenthesizedExpression(parent) ||
7405
+ Node.isAwaitExpression(parent) ||
7406
+ Node.isNonNullExpression(parent) ||
7407
+ Node.isSatisfiesExpression(parent)) &&
7408
+ parent.getExpression() === outer) {
7409
+ outer = parent;
7410
+ continue;
7411
+ }
7412
+ if ((Node.isAsExpression(parent) || Node.isTypeAssertion(parent)) &&
7413
+ parent.getExpression() === outer) {
7414
+ const typeNode = parent.getTypeNode();
7415
+ if (typeNode && !this.leavesAPositionOpen(typeNode)) {
7416
+ stateNode(3, typeNode, parent, 'a cast of the whole value');
7417
+ break;
7418
+ }
7419
+ outer = parent;
7344
7420
  continue;
7345
7421
  }
7346
- const contract = this.schemaContract(candidate, 'input');
7347
- if (contract)
7348
- return contract;
7422
+ if (Node.isBinaryExpression(parent) &&
7423
+ parent.getLeft() === outer &&
7424
+ [SyntaxKind.QuestionQuestionToken, SyntaxKind.BarBarToken].includes(parent.getOperatorToken().getKind()) &&
7425
+ this.isEmptyDefault(parent.getRight())) {
7426
+ outer = parent;
7427
+ continue;
7428
+ }
7429
+ break;
7430
+ }
7431
+ const parent = outer.getParent();
7432
+ if (!parent)
7433
+ continue;
7434
+ if (Node.isVariableDeclaration(parent) && parent.getInitializer() === outer) {
7435
+ stateNode(0, parent.getTypeNode(), parent, 'the annotation of the name that holds it');
7436
+ hold(parent.getNameNode());
7437
+ continue;
7438
+ }
7439
+ if (Node.isBinaryExpression(parent) &&
7440
+ parent.getRight() === outer &&
7441
+ parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken &&
7442
+ Node.isIdentifier(parent.getLeft())) {
7443
+ const declaration = parent.getLeft().getSymbol()?.getValueDeclaration();
7444
+ if (declaration && Node.isVariableDeclaration(declaration)) {
7445
+ stateNode(0, declaration.getTypeNode(), declaration, 'the annotation of the name it is assigned to');
7446
+ }
7447
+ hold(parent.getLeft());
7448
+ continue;
7449
+ }
7450
+ if (reads.returned) {
7451
+ const returnedHere = (Node.isReturnStatement(parent) &&
7452
+ parent.getExpression() === outer &&
7453
+ this.findContainingFunctionForNode(parent) === func) ||
7454
+ (Node.isArrowFunction(parent) && parent === func && parent.getBody() === outer);
7455
+ if (returnedHere) {
7456
+ stateNode(0, func.getReturnTypeNode(), func, 'the declared return type of the function that returns it');
7457
+ continue;
7458
+ }
7459
+ }
7460
+ if (Node.isCallExpression(parent) && parent.getArguments().includes(outer)) {
7461
+ this.stateAtReceivingCall(parent, parent.getArguments().indexOf(outer), reads, found, stateNode);
7349
7462
  }
7350
7463
  }
7351
- return null;
7464
+ if (found.length === 0)
7465
+ return null;
7466
+ const rank = Math.min(...found.map((entry) => entry.rank));
7467
+ const decided = found.filter((entry) => entry.rank === rank);
7468
+ if (new Set(decided.map((entry) => entry.text)).size !== 1) {
7469
+ this.log(`A value at ${this.getNodeLocation(value).file_path}:${this.getNodeLocation(value).start_line} ` +
7470
+ `is stated as ${decided.length} different types in its function; stating none`);
7471
+ return null;
7472
+ }
7473
+ const [first] = decided;
7474
+ return { text: first.text, provenance: first.provenance, at: first.at, how: first.how };
7475
+ }
7476
+ /**
7477
+ * What a call that takes the value at argument `index` states for it:
7478
+ * a schema among its receiver and other arguments (rank 1), a callee of the
7479
+ * project that returns that argument unchanged (rank 2), or, with
7480
+ * `reads.parameters`, the parameter's own type (rank 4).
7481
+ */
7482
+ stateAtReceivingCall(call, index, reads, found, stateNode) {
7483
+ const schemaCandidates = [];
7484
+ const callee = call.getExpression();
7485
+ if (Node.isPropertyAccessExpression(callee))
7486
+ schemaCandidates.push(callee.getExpression());
7487
+ call.getArguments().forEach((argument, at) => {
7488
+ if (at !== index)
7489
+ schemaCandidates.push(this.unwrapExpressionNode(argument));
7490
+ });
7491
+ for (const candidate of schemaCandidates) {
7492
+ const candidateType = candidate.getType();
7493
+ if (!this.schemaOutputType(candidateType, candidate) &&
7494
+ !this.schemaInputType(candidateType, candidate)) {
7495
+ continue;
7496
+ }
7497
+ const contract = this.schemaContract(candidate, 'input');
7498
+ if (contract) {
7499
+ found.push({ rank: 1, text: contract.text, provenance: contract.provenance, at: call, how: 'a schema it is handed to' });
7500
+ return;
7501
+ }
7502
+ }
7503
+ const passThrough = this.passThroughFunction(call, index);
7504
+ if (passThrough) {
7505
+ stateNode(2, passThrough.getReturnTypeNode(), call, 'a function that returns it unchanged');
7506
+ return;
7507
+ }
7508
+ if (!reads.parameters)
7509
+ return;
7510
+ const signature = this.project.getTypeChecker().getResolvedSignature(call);
7511
+ const parameter = signature?.getParameters()[index];
7512
+ const declaration = parameter?.getValueDeclaration();
7513
+ if (!parameter || (declaration && Node.isParameterDeclaration(declaration) && declaration.isRestParameter())) {
7514
+ return;
7515
+ }
7516
+ let parameterType = parameter.getTypeAtLocation(call);
7517
+ if (parameterType.isUnion()) {
7518
+ const bodies = parameterType.getUnionTypes().filter((part) => !this.isCallableType(part));
7519
+ if (bodies.length !== 1)
7520
+ return;
7521
+ parameterType = bodies[0];
7522
+ }
7523
+ if (!this.isObjectShape(parameterType))
7524
+ return;
7525
+ const text = this.structuralTextFromType(parameterType, call);
7526
+ if (!text || /\b(any|unknown)\b/.test(text))
7527
+ return;
7528
+ found.push({ rank: 4, text, at: call, how: 'the parameter of a call it is handed to' });
7529
+ }
7530
+ /**
7531
+ * The project function `call` invokes, when every value it returns is its
7532
+ * argument at `index` as it was handed in (through parentheses, casts,
7533
+ * `satisfies` and `!`) or an empty default (`{}`, `[]`, `null`,
7534
+ * `undefined`), and at least one return is that argument. Such a function
7535
+ * narrows its argument, so its declared return type describes the value it
7536
+ * was handed. Any other function builds something new from it, and is not
7537
+ * this.
7538
+ */
7539
+ passThroughFunction(call, index) {
7540
+ let symbol = call.getExpression().getSymbol();
7541
+ if (symbol?.isAlias())
7542
+ symbol = symbol.getAliasedSymbol() ?? symbol;
7543
+ for (const declaration of symbol?.getDeclarations() ?? []) {
7544
+ const fn = Node.isFunctionDeclaration(declaration) || Node.isMethodDeclaration(declaration)
7545
+ ? declaration
7546
+ : Node.isVariableDeclaration(declaration)
7547
+ ? this.unwrapExpressionNode(declaration.getInitializer())
7548
+ : undefined;
7549
+ if (!fn ||
7550
+ !(Node.isFunctionDeclaration(fn) ||
7551
+ Node.isMethodDeclaration(fn) ||
7552
+ Node.isArrowFunction(fn) ||
7553
+ Node.isFunctionExpression(fn)) ||
7554
+ !fn.getBody() ||
7555
+ fn.getSourceFile().isDeclarationFile() ||
7556
+ fn.getSourceFile().isInNodeModules()) {
7557
+ continue;
7558
+ }
7559
+ const parameter = fn.getParameters()[index];
7560
+ const parameterSymbol = parameter?.getNameNode().getSymbol()?.compilerSymbol;
7561
+ if (!parameter || parameter.isRestParameter() || !parameterSymbol)
7562
+ continue;
7563
+ const returned = this.returnedExpressions(fn);
7564
+ let returnsArgument = false;
7565
+ const everyReturnPasses = returned.every((expression) => {
7566
+ let node = expression;
7567
+ while (Node.isParenthesizedExpression(node) ||
7568
+ Node.isAsExpression(node) ||
7569
+ Node.isTypeAssertion(node) ||
7570
+ Node.isSatisfiesExpression(node) ||
7571
+ Node.isNonNullExpression(node)) {
7572
+ node = node.getExpression();
7573
+ }
7574
+ if (Node.isIdentifier(node) && node.getSymbol()?.compilerSymbol === parameterSymbol) {
7575
+ returnsArgument = true;
7576
+ return true;
7577
+ }
7578
+ return this.isEmptyDefault(node);
7579
+ });
7580
+ if (everyReturnPasses && returnsArgument)
7581
+ return fn;
7582
+ }
7583
+ return undefined;
7584
+ }
7585
+ /** `{}`, `[]`, `null` or `undefined`: a default that states no member. */
7586
+ isEmptyDefault(node) {
7587
+ const bare = this.unwrapExpressionNode(node);
7588
+ return ((Node.isObjectLiteralExpression(bare) && bare.getProperties().length === 0) ||
7589
+ (Node.isArrayLiteralExpression(bare) && bare.getElements().length === 0) ||
7590
+ Node.isNullLiteral(bare) ||
7591
+ (Node.isIdentifier(bare) && bare.getText() === 'undefined'));
7352
7592
  }
7353
7593
  /**
7354
7594
  * The part a validated read names when that part is not a body, or