carrick 0.3.123 → 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.
@@ -107,5 +107,49 @@ export interface PositionRowsOptions {
107
107
  * a directory inside the scanned trees anywhere but beneath `.carrick`.
108
108
  */
109
109
  export declare function writePositionRows(options: PositionRowsOptions): PositionRowsCounts;
110
+ /** What a declaration's file is written as in a row's `decl_file`: `lib:<name>` for the default library, else `PathNames`'. */
111
+ export declare function declaringFileName(program: ts.Program, paths: PathNames, file: ts.SourceFile): string | null;
112
+ /**
113
+ * Names for the files rows point at, never an absolute path: relative to the
114
+ * scanned repo's root when inside it (its real path too, so a root reached
115
+ * through a link still contains its files), `lib:<name>` for the compiler's
116
+ * default library, `pkg:<name>/<path>` for an installed package outside the
117
+ * root, and nothing otherwise.
118
+ */
119
+ export declare class PathNames {
120
+ private readonly roots;
121
+ private readonly packageNames;
122
+ /**
123
+ * Answers by declaring file: a service's rows name a few thousand files
124
+ * hundreds of thousands of times, and the module graph's answer is a scan
125
+ * of its packages (on a Deno service it was 40% of the walk).
126
+ */
127
+ private readonly declaringFiles;
128
+ private readonly declaringPackages;
129
+ constructor(scanRoot: string);
130
+ /** The root `file` lies under, and its path from there; `undefined` outside every root. */
131
+ private inside;
132
+ /** A request file's name: relative to the root, which every file the scanner sends is inside. */
133
+ relative(file: string): string;
134
+ /** What a declaring file is written as, or `null` when nothing names it. */
135
+ declaringFile(fileName: string): string | null;
136
+ private nameDeclaringFile;
137
+ /**
138
+ * The package a declaring file belongs to: the module graph's or the path's
139
+ * answer (`installedPackageOf`), else, for a file inside the root, the
140
+ * `name` of the nearest package.json at or above it, up to the root.
141
+ */
142
+ declaringPackage(fileName: string, packageOf: (filePath: string) => string | undefined): string | null;
143
+ private nameDeclaringPackage;
144
+ private packageNameIn;
145
+ /**
146
+ * `import("/abs/path")` in a printed type, written with the declaring
147
+ * file's name instead. The print has no enclosing node, so it names every
148
+ * module by the file that declares it and one type reads the same from
149
+ * every file; it also skips the accessibility search an enclosing node
150
+ * costs (40% of the walk on a 600-file service).
151
+ */
152
+ scrubType(text: string): string;
153
+ }
110
154
  /** The kind of row a node gives, if any (exported for the editor-parity test, carrick#2418). */
111
155
  export declare function kindOf(node: ts.Node): NodeKind | undefined;
@@ -242,6 +242,10 @@ export function writePositionRows(options) {
242
242
  counts.load_ms = Math.round(counts.load_ms);
243
243
  return counts;
244
244
  }
245
+ /** What a declaration's file is written as in a row's `decl_file`: `lib:<name>` for the default library, else `PathNames`'. */
246
+ export function declaringFileName(program, paths, file) {
247
+ return program.isSourceFileDefaultLibrary(file) ? `lib:${path.basename(file.fileName)}` : paths.declaringFile(file.fileName);
248
+ }
245
249
  /** A file's line, with its keys in the pinned order. */
246
250
  function fileLine(line) {
247
251
  return JSON.stringify(line);
@@ -253,7 +257,7 @@ function fileLine(line) {
253
257
  * default library, `pkg:<name>/<path>` for an installed package outside the
254
258
  * root, and nothing otherwise.
255
259
  */
256
- class PathNames {
260
+ export class PathNames {
257
261
  roots;
258
262
  packageNames = new Map();
259
263
  /**
@@ -466,9 +470,7 @@ class PositionWalker {
466
470
  }
467
471
  /** How rows name a position in a program file: its file, 1-based line and package. */
468
472
  located(declaringFile, position) {
469
- const declFile = this.program.isSourceFileDefaultLibrary(declaringFile)
470
- ? `lib:${path.basename(declaringFile.fileName)}`
471
- : this.paths.declaringFile(declaringFile.fileName);
473
+ const declFile = declaringFileName(this.program, this.paths, declaringFile);
472
474
  const declLine = declaringFile.getLineAndCharacterOfPosition(position).line + 1;
473
475
  const declPackage = this.paths.declaringPackage(declaringFile.fileName, (file) => this.loader.packageOf(file));
474
476
  return { declFile, declLine, declPackage };
@@ -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).