carrick 0.3.108 → 0.3.110
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 +9 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/bundler.d.ts +10 -0
- package/sidecar/dist/src/bundler.js +42 -0
- package/sidecar/dist/src/capture/anchors.js +32 -0
- package/sidecar/dist/src/capture/api.d.ts +5 -0
- package/sidecar/dist/src/capture/check-deep.js +2 -1
- package/sidecar/dist/src/capture/check-workspace.d.ts +11 -0
- package/sidecar/dist/src/capture/check-workspace.js +81 -8
- package/sidecar/dist/src/capture/compiler-options.d.ts +39 -0
- package/sidecar/dist/src/capture/compiler-options.js +100 -0
- package/sidecar/dist/src/capture/deno-project.js +3 -2
- package/sidecar/dist/src/capture/index.d.ts +2 -0
- package/sidecar/dist/src/capture/index.js +25 -8
- package/sidecar/dist/src/capture/installed-package-directory.d.ts +22 -0
- package/sidecar/dist/src/capture/installed-package-directory.js +30 -0
- package/sidecar/dist/src/capture/installed-package.js +3 -2
- package/sidecar/dist/src/capture/resolution-edges.d.ts +36 -0
- package/sidecar/dist/src/capture/resolution-edges.js +150 -0
- package/sidecar/dist/src/capture/self-check.js +3 -2
- package/sidecar/dist/src/capture/status-table.d.ts +62 -0
- package/sidecar/dist/src/capture/status-table.js +104 -0
- package/sidecar/dist/src/definition-resolver.js +3 -2
- package/sidecar/dist/src/failure-path.d.ts +42 -2
- package/sidecar/dist/src/failure-path.js +38 -13
- package/sidecar/dist/src/index.js +91 -7
- package/sidecar/dist/src/library-claims.js +2 -0
- package/sidecar/dist/src/line-index.d.ts +9 -0
- package/sidecar/dist/src/line-index.js +24 -0
- package/sidecar/dist/src/print-type.d.ts +48 -0
- package/sidecar/dist/src/print-type.js +71 -0
- package/sidecar/dist/src/printed-names.js +2 -1
- package/sidecar/dist/src/program-files.d.ts +44 -0
- package/sidecar/dist/src/program-files.js +64 -0
- package/sidecar/dist/src/project-loader.d.ts +5 -0
- package/sidecar/dist/src/project-loader.js +14 -5
- package/sidecar/dist/src/response-modes.d.ts +77 -0
- package/sidecar/dist/src/response-modes.js +459 -0
- package/sidecar/dist/src/retype.js +7 -1
- package/sidecar/dist/src/type-inferrer.d.ts +53 -0
- package/sidecar/dist/src/type-inferrer.js +267 -79
- package/sidecar/dist/src/types.d.ts +50 -2
- package/sidecar/dist/src/validators.d.ts +159 -88
- package/sidecar/dist/src/validators.js +13 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an installed package sits on disk, by path alone, for capture's
|
|
3
|
+
* resolution walk. It mirrors `DeclarationReader.installedPackage` in
|
|
4
|
+
* library-claims.ts: the capture seam (pinned decision 11a) lets neither side
|
|
5
|
+
* import the other, so a change to what counts as installed changes both.
|
|
6
|
+
*
|
|
7
|
+
* Seam: node builtins only.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The installed package directory a real path lies in, by path alone: the
|
|
11
|
+
* directory after the path's last `node_modules` segment (two segments for a
|
|
12
|
+
* scope), taken relative to `root`, and the name it is installed as. A source
|
|
13
|
+
* file in a directory named `node_modules` above `root`, or a checkout under a
|
|
14
|
+
* `node_modules` ancestor, is not installed; an install hoisted above `root`
|
|
15
|
+
* (`../../node_modules/pkg`) and a pnpm store
|
|
16
|
+
* (`node_modules/.pnpm/pkg@1/node_modules/pkg`) are. The caller reads the
|
|
17
|
+
* package.json there: without one, nothing is installed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function installedPackageDirectory(root: string, realPath: string): {
|
|
20
|
+
directory: string;
|
|
21
|
+
name: string;
|
|
22
|
+
} | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where an installed package sits on disk, by path alone, for capture's
|
|
3
|
+
* resolution walk. It mirrors `DeclarationReader.installedPackage` in
|
|
4
|
+
* library-claims.ts: the capture seam (pinned decision 11a) lets neither side
|
|
5
|
+
* import the other, so a change to what counts as installed changes both.
|
|
6
|
+
*
|
|
7
|
+
* Seam: node builtins only.
|
|
8
|
+
*/
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
/**
|
|
11
|
+
* The installed package directory a real path lies in, by path alone: the
|
|
12
|
+
* directory after the path's last `node_modules` segment (two segments for a
|
|
13
|
+
* scope), taken relative to `root`, and the name it is installed as. A source
|
|
14
|
+
* file in a directory named `node_modules` above `root`, or a checkout under a
|
|
15
|
+
* `node_modules` ancestor, is not installed; an install hoisted above `root`
|
|
16
|
+
* (`../../node_modules/pkg`) and a pnpm store
|
|
17
|
+
* (`node_modules/.pnpm/pkg@1/node_modules/pkg`) are. The caller reads the
|
|
18
|
+
* package.json there: without one, nothing is installed.
|
|
19
|
+
*/
|
|
20
|
+
export function installedPackageDirectory(root, realPath) {
|
|
21
|
+
const segments = path.relative(root, realPath).split(path.sep);
|
|
22
|
+
const last = segments.lastIndexOf('node_modules');
|
|
23
|
+
if (last < 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
const width = segments[last + 1]?.startsWith('@') ? 2 : 1;
|
|
26
|
+
return {
|
|
27
|
+
directory: path.resolve(root, ...segments.slice(0, last + 1 + width)),
|
|
28
|
+
name: segments.slice(last + 1, last + 1 + width).join('/'),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -24,16 +24,17 @@
|
|
|
24
24
|
import ts from 'typescript';
|
|
25
25
|
import * as fs from 'node:fs';
|
|
26
26
|
import * as path from 'node:path';
|
|
27
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
27
28
|
import { isPublishedSemver } from './lockfile.js';
|
|
28
29
|
const DECLARATION_SUFFIXES = ['', '.d.ts', '.d.mts', '.d.cts', '.ts', '.mts', '.cts', '/index.d.ts'];
|
|
29
|
-
const RESOLUTION_OPTIONS = {
|
|
30
|
+
const RESOLUTION_OPTIONS = sidecarCompilerOptions({
|
|
30
31
|
module: ts.ModuleKind.ESNext,
|
|
31
32
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
32
33
|
target: ts.ScriptTarget.ESNext,
|
|
33
34
|
noEmit: true,
|
|
34
35
|
skipLibCheck: true,
|
|
35
36
|
types: [],
|
|
36
|
-
};
|
|
37
|
+
});
|
|
37
38
|
/** DefinitelyTyped packages are imported by the runtime name (TS6137). */
|
|
38
39
|
function publicNameOf(name) {
|
|
39
40
|
if (!name.startsWith('@types/'))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the scanned repo's installed tree resolved the packages a stub pins
|
|
3
|
+
* (carrick#2091).
|
|
4
|
+
*
|
|
5
|
+
* A stub pins only the externals its declarations import. The check
|
|
6
|
+
* workspace has no lockfile, so without this record every package those pins
|
|
7
|
+
* depend on resolves fresh from the registry: the check can install a version
|
|
8
|
+
* the repo never ran, or fail on one the registry lists but cannot serve.
|
|
9
|
+
*
|
|
10
|
+
* The walk reads the installed tree the way Node's lookup does, so one reader
|
|
11
|
+
* covers npm, pnpm (isolated store and hoisted), yarn's node-modules linker
|
|
12
|
+
* and bun. Starting at each pin installed at the pinned version, it follows
|
|
13
|
+
* `dependencies` and `optionalDependencies` breadth first and records
|
|
14
|
+
* `<parent>@<version> -> <child>: <version>` for every child installed as a
|
|
15
|
+
* published package of that name. Workspace members, `link:` targets and npm
|
|
16
|
+
* aliases are left out: the registry cannot serve them by that name. An edge
|
|
17
|
+
* whose parent version is installed twice with different children is left
|
|
18
|
+
* out, since no one answer is the repo's.
|
|
19
|
+
*
|
|
20
|
+
* The walk only reads. Seam: node builtins and this bundle only.
|
|
21
|
+
*/
|
|
22
|
+
/** The stub file the check reads to pin transitive edges. */
|
|
23
|
+
export declare const RESOLUTION_FILE = "carrick-resolution.json";
|
|
24
|
+
/** `<parent>@<version>` -> child name -> child version. */
|
|
25
|
+
export type ResolutionEdges = Record<string, Record<string, string>>;
|
|
26
|
+
export interface InstalledResolution {
|
|
27
|
+
/** Key-sorted at both levels; byte-stable for one tree. */
|
|
28
|
+
edges: ResolutionEdges;
|
|
29
|
+
/** Dependencies of walked packages that no edge records: the check resolves these from the registry. */
|
|
30
|
+
unrecorded: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The edges the installed tree under `serviceRoot` resolved, from the stub's
|
|
34
|
+
* pins outward. A pin not installed at its pinned version starts no walk.
|
|
35
|
+
*/
|
|
36
|
+
export declare function installedResolutionEdges(serviceRoot: string, pins: Record<string, string>): InstalledResolution;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the scanned repo's installed tree resolved the packages a stub pins
|
|
3
|
+
* (carrick#2091).
|
|
4
|
+
*
|
|
5
|
+
* A stub pins only the externals its declarations import. The check
|
|
6
|
+
* workspace has no lockfile, so without this record every package those pins
|
|
7
|
+
* depend on resolves fresh from the registry: the check can install a version
|
|
8
|
+
* the repo never ran, or fail on one the registry lists but cannot serve.
|
|
9
|
+
*
|
|
10
|
+
* The walk reads the installed tree the way Node's lookup does, so one reader
|
|
11
|
+
* covers npm, pnpm (isolated store and hoisted), yarn's node-modules linker
|
|
12
|
+
* and bun. Starting at each pin installed at the pinned version, it follows
|
|
13
|
+
* `dependencies` and `optionalDependencies` breadth first and records
|
|
14
|
+
* `<parent>@<version> -> <child>: <version>` for every child installed as a
|
|
15
|
+
* published package of that name. Workspace members, `link:` targets and npm
|
|
16
|
+
* aliases are left out: the registry cannot serve them by that name. An edge
|
|
17
|
+
* whose parent version is installed twice with different children is left
|
|
18
|
+
* out, since no one answer is the repo's.
|
|
19
|
+
*
|
|
20
|
+
* The walk only reads. Seam: node builtins and this bundle only.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import { installedPackageDirectory } from './installed-package-directory.js';
|
|
25
|
+
import { isPublishedSemver } from './lockfile.js';
|
|
26
|
+
/** The stub file the check reads to pin transitive edges. */
|
|
27
|
+
export const RESOLUTION_FILE = 'carrick-resolution.json';
|
|
28
|
+
function validName(name) {
|
|
29
|
+
return !name.split('/').some((s) => s === '' || s === '.' || s === '..');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Node's lookup of `name` from `fromDir`: `<dir>/node_modules/<name>` at each
|
|
33
|
+
* ancestor, skipping an ancestor that is itself a `node_modules` directory.
|
|
34
|
+
* That skip is how a pnpm store package finds its sibling links.
|
|
35
|
+
*/
|
|
36
|
+
function lookup(fromDir, name) {
|
|
37
|
+
let dir = fromDir;
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (path.basename(dir) !== 'node_modules') {
|
|
40
|
+
const candidate = path.join(dir, 'node_modules', ...name.split('/'));
|
|
41
|
+
if (fs.existsSync(path.join(candidate, 'package.json')))
|
|
42
|
+
return candidate;
|
|
43
|
+
}
|
|
44
|
+
const parent = path.dirname(dir);
|
|
45
|
+
if (parent === dir)
|
|
46
|
+
return undefined;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The package `name` installed as Node would find it from `fromDir`: a
|
|
52
|
+
* directory `installedPackageDirectory` accepts, holding a package.json of
|
|
53
|
+
* that name with a published version.
|
|
54
|
+
*/
|
|
55
|
+
function installedFrom(root, fromDir, name) {
|
|
56
|
+
if (!validName(name))
|
|
57
|
+
return undefined;
|
|
58
|
+
const found = lookup(fromDir, name);
|
|
59
|
+
if (found === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
try {
|
|
62
|
+
const dir = fs.realpathSync(found);
|
|
63
|
+
const installed = installedPackageDirectory(root, dir);
|
|
64
|
+
if (installed === undefined || installed.directory !== dir || installed.name !== name) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
68
|
+
if (manifest?.name !== name || typeof manifest.version !== 'string')
|
|
69
|
+
return undefined;
|
|
70
|
+
if (!isPublishedSemver(manifest.version))
|
|
71
|
+
return undefined;
|
|
72
|
+
const names = new Set();
|
|
73
|
+
for (const field of [manifest.dependencies, manifest.optionalDependencies]) {
|
|
74
|
+
if (field && typeof field === 'object')
|
|
75
|
+
for (const dep of Object.keys(field))
|
|
76
|
+
names.add(dep);
|
|
77
|
+
}
|
|
78
|
+
return { dir, name, version: manifest.version, dependencies: [...names].sort() };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Dangling link, unreadable or corrupt manifest: nothing installed here.
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function sorted(record) {
|
|
86
|
+
return Object.fromEntries(Object.keys(record)
|
|
87
|
+
.sort()
|
|
88
|
+
.map((k) => [k, record[k]]));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The edges the installed tree under `serviceRoot` resolved, from the stub's
|
|
92
|
+
* pins outward. A pin not installed at its pinned version starts no walk.
|
|
93
|
+
*/
|
|
94
|
+
export function installedResolutionEdges(serviceRoot, pins) {
|
|
95
|
+
let root;
|
|
96
|
+
try {
|
|
97
|
+
root = fs.realpathSync(path.resolve(serviceRoot));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return { edges: {}, unrecorded: 0 };
|
|
101
|
+
}
|
|
102
|
+
const queue = [];
|
|
103
|
+
const visited = new Set();
|
|
104
|
+
for (const name of Object.keys(pins).sort()) {
|
|
105
|
+
const pkg = installedFrom(root, root, name);
|
|
106
|
+
if (pkg === undefined || pkg.version !== pins[name] || visited.has(pkg.dir))
|
|
107
|
+
continue;
|
|
108
|
+
visited.add(pkg.dir);
|
|
109
|
+
queue.push(pkg);
|
|
110
|
+
}
|
|
111
|
+
// parent key -> child -> every version a copy of that parent resolved.
|
|
112
|
+
const seen = new Map();
|
|
113
|
+
// Each (parent key, child) a walked package declares, recorded or not.
|
|
114
|
+
const declared = new Set();
|
|
115
|
+
for (let i = 0; i < queue.length; i++) {
|
|
116
|
+
const parent = queue[i];
|
|
117
|
+
const key = `${parent.name}@${parent.version}`;
|
|
118
|
+
let children = seen.get(key);
|
|
119
|
+
if (children === undefined)
|
|
120
|
+
seen.set(key, (children = new Map()));
|
|
121
|
+
for (const name of parent.dependencies) {
|
|
122
|
+
declared.add(`${key}>${name}`);
|
|
123
|
+
const child = installedFrom(root, parent.dir, name);
|
|
124
|
+
if (child === undefined)
|
|
125
|
+
continue;
|
|
126
|
+
let versions = children.get(name);
|
|
127
|
+
if (versions === undefined)
|
|
128
|
+
children.set(name, (versions = new Set()));
|
|
129
|
+
versions.add(child.version);
|
|
130
|
+
if (!visited.has(child.dir)) {
|
|
131
|
+
visited.add(child.dir);
|
|
132
|
+
queue.push(child);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const edges = {};
|
|
137
|
+
let recorded = 0;
|
|
138
|
+
for (const [key, children] of seen) {
|
|
139
|
+
const kept = {};
|
|
140
|
+
for (const [name, versions] of children) {
|
|
141
|
+
if (versions.size === 1)
|
|
142
|
+
kept[name] = [...versions][0];
|
|
143
|
+
}
|
|
144
|
+
if (Object.keys(kept).length === 0)
|
|
145
|
+
continue;
|
|
146
|
+
edges[key] = sorted(kept);
|
|
147
|
+
recorded += Object.keys(kept).length;
|
|
148
|
+
}
|
|
149
|
+
return { edges: sorted(edges), unrecorded: declared.size - recorded };
|
|
150
|
+
}
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
import ts from 'typescript';
|
|
33
33
|
import * as fs from 'node:fs';
|
|
34
34
|
import * as path from 'node:path';
|
|
35
|
+
import { sidecarCompilerOptions } from './compiler-options.js';
|
|
35
36
|
import { collectSpecifiers, declarationCandidates, isDeclarationFileName, isRelative, packageNameOf, } from './specifiers.js';
|
|
36
37
|
import { repairDanglingImports } from './repair-dangling.js';
|
|
37
38
|
import { findDisqualifyingTopTypes, findUnresolvedPlaceholders, isErrorPlaceholder, provenanceOf, unemittedModuleProvenance, unresolvedInTreeProvenance, } from './deep-walk.js';
|
|
@@ -78,7 +79,7 @@ export function selfCheckStub(args) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
function runSelfCheck(args, treeFiles, repaired) {
|
|
81
|
-
const options = {
|
|
82
|
+
const options = sidecarCompilerOptions({
|
|
82
83
|
noEmit: true,
|
|
83
84
|
strict: true,
|
|
84
85
|
// MUST be false: the whole stub tree is .d.ts (see module header).
|
|
@@ -86,7 +87,7 @@ function runSelfCheck(args, treeFiles, repaired) {
|
|
|
86
87
|
module: ts.ModuleKind.ESNext,
|
|
87
88
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
88
89
|
types: [],
|
|
89
|
-
};
|
|
90
|
+
});
|
|
90
91
|
const program = ts.createProgram(treeFiles, options, args.compilerHost?.(options));
|
|
91
92
|
const checker = program.getTypeChecker();
|
|
92
93
|
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response tables keyed by HTTP status code (carrick#1841).
|
|
3
|
+
*
|
|
4
|
+
* An object type whose every key is a status code (`{ 200: Item; 404: Problem
|
|
5
|
+
* }`, or a range key such as `2XX`) is not a body: it is a table of the bodies
|
|
6
|
+
* each status carries. What a caller receives on success is the union of its
|
|
7
|
+
* 2xx values. The producer side reads the same rule off a schema object
|
|
8
|
+
* literal (`successStatusEntry` in the inferrer); this module reads it off a
|
|
9
|
+
* TYPE, through the compiler's own API, so the inferrer, the v1 bundle and
|
|
10
|
+
* the capture all decide a table the same way.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here names a library or a generator. The shape is the HTTP status
|
|
13
|
+
* vocabulary, which is the language of the wire, not of any one client.
|
|
14
|
+
*/
|
|
15
|
+
import ts from 'typescript';
|
|
16
|
+
/** `value` when it is an integer in the HTTP status range, else `undefined`. */
|
|
17
|
+
export declare function httpStatus(value: unknown): number | undefined;
|
|
18
|
+
/** One verdict for a set of status codes, or `mixed` when they disagree. */
|
|
19
|
+
export declare function classifyStatusCodes(codes: number[]): 'success' | 'error' | 'redirect' | 'mixed';
|
|
20
|
+
/**
|
|
21
|
+
* The status code a response-table KEY names: `200`, or a range key (`2XX`,
|
|
22
|
+
* `2xx`) read as the bottom of its range. Quotes around the key are ignored.
|
|
23
|
+
* `undefined` for any other key.
|
|
24
|
+
*/
|
|
25
|
+
export declare function statusKeyCode(name: string): number | undefined;
|
|
26
|
+
/** One row of a response table: the key as written and the status it names. */
|
|
27
|
+
export interface StatusTableEntry {
|
|
28
|
+
symbol: ts.Symbol;
|
|
29
|
+
/** The key as the type declares it (`200`, `2XX`). */
|
|
30
|
+
key: string;
|
|
31
|
+
code: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The rows of `type` when it is a response table: a plain object type with at
|
|
35
|
+
* least one property, every key a status code, no index signature and no call
|
|
36
|
+
* or construct signature. `undefined` for anything else, so a payload that
|
|
37
|
+
* happens to hold one numeric key among others is never read as a table.
|
|
38
|
+
*/
|
|
39
|
+
export declare function statusTableOf(checker: ts.TypeChecker, type: ts.Type): StatusTableEntry[] | undefined;
|
|
40
|
+
/** The rows of a table that answer a success: 2xx, in the order declared. */
|
|
41
|
+
export declare function successEntries(entries: StatusTableEntry[]): StatusTableEntry[];
|
|
42
|
+
/**
|
|
43
|
+
* Whether a body type states nothing: `void`, `undefined`, `never`, or a union
|
|
44
|
+
* made only of them. A `204: void` row is such a body.
|
|
45
|
+
*/
|
|
46
|
+
export declare function statesNoBody(type: ts.Type): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* What a response table's success rows carry, read for a consumer:
|
|
49
|
+
* - `body`: the 2xx rows' value types, at least one of which states a body;
|
|
50
|
+
* - `no_body`: the table has no 2xx row, or every 2xx row states nothing.
|
|
51
|
+
* `undefined` when `type` is not a response table.
|
|
52
|
+
*/
|
|
53
|
+
export declare function statusTableBody(checker: ts.TypeChecker, type: ts.Type, at: ts.Node): {
|
|
54
|
+
kind: 'body';
|
|
55
|
+
entries: StatusTableEntry[];
|
|
56
|
+
types: ts.Type[];
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'no_body';
|
|
59
|
+
entries: StatusTableEntry[];
|
|
60
|
+
} | undefined;
|
|
61
|
+
/** The members of a union, or the type itself: what a set comparison reads. */
|
|
62
|
+
export declare function constituents(type: ts.Type): ts.Type[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response tables keyed by HTTP status code (carrick#1841).
|
|
3
|
+
*
|
|
4
|
+
* An object type whose every key is a status code (`{ 200: Item; 404: Problem
|
|
5
|
+
* }`, or a range key such as `2XX`) is not a body: it is a table of the bodies
|
|
6
|
+
* each status carries. What a caller receives on success is the union of its
|
|
7
|
+
* 2xx values. The producer side reads the same rule off a schema object
|
|
8
|
+
* literal (`successStatusEntry` in the inferrer); this module reads it off a
|
|
9
|
+
* TYPE, through the compiler's own API, so the inferrer, the v1 bundle and
|
|
10
|
+
* the capture all decide a table the same way.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here names a library or a generator. The shape is the HTTP status
|
|
13
|
+
* vocabulary, which is the language of the wire, not of any one client.
|
|
14
|
+
*/
|
|
15
|
+
import ts from 'typescript';
|
|
16
|
+
/** `value` when it is an integer in the HTTP status range, else `undefined`. */
|
|
17
|
+
export function httpStatus(value) {
|
|
18
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 100 && value <= 599
|
|
19
|
+
? value
|
|
20
|
+
: undefined;
|
|
21
|
+
}
|
|
22
|
+
/** One verdict for a set of status codes, or `mixed` when they disagree. */
|
|
23
|
+
export function classifyStatusCodes(codes) {
|
|
24
|
+
if (codes.every((code) => code >= 400))
|
|
25
|
+
return 'error';
|
|
26
|
+
if (codes.every((code) => code >= 300 && code < 400))
|
|
27
|
+
return 'redirect';
|
|
28
|
+
if (codes.every((code) => code < 300))
|
|
29
|
+
return 'success';
|
|
30
|
+
return 'mixed';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The status code a response-table KEY names: `200`, or a range key (`2XX`,
|
|
34
|
+
* `2xx`) read as the bottom of its range. Quotes around the key are ignored.
|
|
35
|
+
* `undefined` for any other key.
|
|
36
|
+
*/
|
|
37
|
+
export function statusKeyCode(name) {
|
|
38
|
+
const key = name.replace(/['"`]/g, '').trim();
|
|
39
|
+
if (/^\d{3}$/.test(key))
|
|
40
|
+
return httpStatus(Number(key));
|
|
41
|
+
if (/^[1-5]xx$/i.test(key))
|
|
42
|
+
return Number(key[0]) * 100;
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The rows of `type` when it is a response table: a plain object type with at
|
|
47
|
+
* least one property, every key a status code, no index signature and no call
|
|
48
|
+
* or construct signature. `undefined` for anything else, so a payload that
|
|
49
|
+
* happens to hold one numeric key among others is never read as a table.
|
|
50
|
+
*/
|
|
51
|
+
export function statusTableOf(checker, type) {
|
|
52
|
+
if (!(type.flags & ts.TypeFlags.Object))
|
|
53
|
+
return undefined;
|
|
54
|
+
if (type.getCallSignatures().length > 0 || type.getConstructSignatures().length > 0) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (checker.getIndexInfosOfType(type).length > 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
const properties = checker.getPropertiesOfType(type);
|
|
60
|
+
if (properties.length === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const symbol of properties) {
|
|
64
|
+
const key = symbol.getName();
|
|
65
|
+
const code = statusKeyCode(key);
|
|
66
|
+
if (code === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
entries.push({ symbol, key, code });
|
|
69
|
+
}
|
|
70
|
+
return entries;
|
|
71
|
+
}
|
|
72
|
+
/** The rows of a table that answer a success: 2xx, in the order declared. */
|
|
73
|
+
export function successEntries(entries) {
|
|
74
|
+
return entries.filter((entry) => entry.code >= 200 && entry.code <= 299);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether a body type states nothing: `void`, `undefined`, `never`, or a union
|
|
78
|
+
* made only of them. A `204: void` row is such a body.
|
|
79
|
+
*/
|
|
80
|
+
export function statesNoBody(type) {
|
|
81
|
+
const members = type.isUnion() ? type.types : [type];
|
|
82
|
+
return members.every((member) => (member.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never)) !== 0);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* What a response table's success rows carry, read for a consumer:
|
|
86
|
+
* - `body`: the 2xx rows' value types, at least one of which states a body;
|
|
87
|
+
* - `no_body`: the table has no 2xx row, or every 2xx row states nothing.
|
|
88
|
+
* `undefined` when `type` is not a response table.
|
|
89
|
+
*/
|
|
90
|
+
export function statusTableBody(checker, type, at) {
|
|
91
|
+
const entries = statusTableOf(checker, type);
|
|
92
|
+
if (!entries)
|
|
93
|
+
return undefined;
|
|
94
|
+
const success = successEntries(entries);
|
|
95
|
+
const types = success.map((entry) => checker.getTypeOfSymbolAtLocation(entry.symbol, at));
|
|
96
|
+
if (success.length === 0 || types.every((body) => statesNoBody(body))) {
|
|
97
|
+
return { kind: 'no_body', entries };
|
|
98
|
+
}
|
|
99
|
+
return { kind: 'body', entries: success, types };
|
|
100
|
+
}
|
|
101
|
+
/** The members of a union, or the type itself: what a set comparison reads. */
|
|
102
|
+
export function constituents(type) {
|
|
103
|
+
return type.isUnion() ? type.types : [type];
|
|
104
|
+
}
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import * as path from 'node:path';
|
|
36
36
|
import * as fs from 'node:fs';
|
|
37
37
|
import { Project, Node } from 'ts-morph';
|
|
38
|
+
import { sidecarCompilerOptions } from './capture/index.js';
|
|
38
39
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
39
40
|
export class DefinitionResolver {
|
|
40
41
|
/**
|
|
@@ -57,13 +58,13 @@ export class DefinitionResolver {
|
|
|
57
58
|
}
|
|
58
59
|
try {
|
|
59
60
|
const stubProject = new Project({
|
|
60
|
-
compilerOptions: {
|
|
61
|
+
compilerOptions: sidecarCompilerOptions({
|
|
61
62
|
target: 99, // ESNext
|
|
62
63
|
module: 99, // ESNext
|
|
63
64
|
moduleResolution: 100, // Bundler
|
|
64
65
|
strict: true,
|
|
65
66
|
skipLibCheck: true,
|
|
66
|
-
},
|
|
67
|
+
}),
|
|
67
68
|
skipAddingFilesFromTsConfig: true,
|
|
68
69
|
});
|
|
69
70
|
for (const filePath of walkDtsFiles(typesDir)) {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* the other way round: after `if (res.status === 204) return null`, 200 still
|
|
28
28
|
* gets through, and the json read that follows is the payload.
|
|
29
29
|
*/
|
|
30
|
-
import { Node } from 'ts-morph';
|
|
30
|
+
import { Node, type CaseOrDefaultClause, type SwitchStatement } from 'ts-morph';
|
|
31
31
|
/** A set of HTTP statuses, as the predicate that admits them. */
|
|
32
32
|
export type Statuses = (status: number) => boolean;
|
|
33
33
|
/** The statuses `ok` is true for. */
|
|
@@ -60,8 +60,48 @@ export declare function reachedOnlyOnFailure(node: Node, boundary: Node, isRespo
|
|
|
60
60
|
* an `if` or a conditional expression it sits in, or an earlier `if` in an
|
|
61
61
|
* enclosing block one of whose branches cannot complete, which leaves the
|
|
62
62
|
* rest of the block to the other side. A test that does not read the
|
|
63
|
-
* response's `ok` or `status` is not listed.
|
|
63
|
+
* response's `ok` or `status` is not listed. A `switch` is not read here:
|
|
64
|
+
* `retype.ts` finds one by its own climb.
|
|
64
65
|
*/
|
|
65
66
|
export declare function testsOnPath(node: Node, boundary: Node | undefined, isResponse: (node: Node) => boolean): SideTaken[];
|
|
67
|
+
/**
|
|
68
|
+
* One test on the path to a node, as the side the node is on. `test` is the
|
|
69
|
+
* statement or expression that tests, so two nodes under the same test share
|
|
70
|
+
* it.
|
|
71
|
+
*
|
|
72
|
+
* - `branch`: an `if` or a conditional expression, and whether the node is
|
|
73
|
+
* on its true side: the branch the node sits in or, for an earlier `if` in
|
|
74
|
+
* an enclosing block one of whose branches cannot complete, the side that
|
|
75
|
+
* did not leave.
|
|
76
|
+
* - `clause`: the node sits in this clause of a `switch`.
|
|
77
|
+
* - `after-switch`: the node follows a `switch` in an enclosing block.
|
|
78
|
+
*/
|
|
79
|
+
export type PathStep = {
|
|
80
|
+
kind: 'branch';
|
|
81
|
+
test: Node;
|
|
82
|
+
condition: Node;
|
|
83
|
+
whenTrue: boolean;
|
|
84
|
+
} | {
|
|
85
|
+
kind: 'clause';
|
|
86
|
+
test: SwitchStatement;
|
|
87
|
+
clause: CaseOrDefaultClause;
|
|
88
|
+
} | {
|
|
89
|
+
kind: 'after-switch';
|
|
90
|
+
test: SwitchStatement;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Every test on the path from `node` up to `boundary` (the whole file when it
|
|
94
|
+
* is `undefined`), whatever it reads, innermost first. `testsOnPath` reads
|
|
95
|
+
* the branches as statuses; `response-modes.ts` reads branches and switches
|
|
96
|
+
* as values of a request field (carrick#2054).
|
|
97
|
+
*/
|
|
98
|
+
export declare function stepsOnPath(node: Node, boundary: Node | undefined): PathStep[];
|
|
66
99
|
/** `node` reads the response's `ok` or `status`, itself or anywhere inside it. */
|
|
67
100
|
export declare function readsResponseStatus(node: Node, isResponse: (node: Node) => boolean): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* A statement that never runs on into the statement after it: it returns,
|
|
103
|
+
* throws, breaks or continues, or every path through it does. A loop, a
|
|
104
|
+
* `switch` or a `try` is read as one that can complete, which only ever
|
|
105
|
+
* leaves a read on the path it was on.
|
|
106
|
+
*/
|
|
107
|
+
export declare function cannotComplete(statement: Node): boolean;
|
|
@@ -64,30 +64,51 @@ export function reachedOnlyOnFailure(node, boundary, isResponse) {
|
|
|
64
64
|
* an `if` or a conditional expression it sits in, or an earlier `if` in an
|
|
65
65
|
* enclosing block one of whose branches cannot complete, which leaves the
|
|
66
66
|
* rest of the block to the other side. A test that does not read the
|
|
67
|
-
* response's `ok` or `status` is not listed.
|
|
67
|
+
* response's `ok` or `status` is not listed. A `switch` is not read here:
|
|
68
|
+
* `retype.ts` finds one by its own climb.
|
|
68
69
|
*/
|
|
69
70
|
export function testsOnPath(node, boundary, isResponse) {
|
|
70
71
|
const sides = [];
|
|
71
|
-
const
|
|
72
|
+
for (const step of stepsOnPath(node, boundary)) {
|
|
73
|
+
if (step.kind !== 'branch')
|
|
74
|
+
continue;
|
|
75
|
+
const test = readTest(step.condition, isResponse);
|
|
72
76
|
if (test === UNRELATED)
|
|
73
|
-
|
|
74
|
-
sides.push({ admits: whenTrue ? test.whenTrue : test.whenFalse, inexact: test.inexact });
|
|
75
|
-
}
|
|
77
|
+
continue;
|
|
78
|
+
sides.push({ admits: step.whenTrue ? test.whenTrue : test.whenFalse, inexact: test.inexact });
|
|
79
|
+
}
|
|
80
|
+
return sides;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Every test on the path from `node` up to `boundary` (the whole file when it
|
|
84
|
+
* is `undefined`), whatever it reads, innermost first. `testsOnPath` reads
|
|
85
|
+
* the branches as statuses; `response-modes.ts` reads branches and switches
|
|
86
|
+
* as values of a request field (carrick#2054).
|
|
87
|
+
*/
|
|
88
|
+
export function stepsOnPath(node, boundary) {
|
|
89
|
+
const steps = [];
|
|
90
|
+
const branch = (test, condition, whenTrue) => steps.push({ kind: 'branch', test, condition, whenTrue });
|
|
76
91
|
for (let child = node, parent = node.getParent(); parent && parent !== boundary; child = parent, parent = parent.getParent()) {
|
|
77
92
|
if (Node.isIfStatement(parent)) {
|
|
78
93
|
if (child === parent.getThenStatement()) {
|
|
79
|
-
|
|
94
|
+
branch(parent, parent.getExpression(), true);
|
|
80
95
|
}
|
|
81
96
|
else if (child === parent.getElseStatement()) {
|
|
82
|
-
|
|
97
|
+
branch(parent, parent.getExpression(), false);
|
|
83
98
|
}
|
|
84
99
|
}
|
|
85
100
|
else if (Node.isConditionalExpression(parent)) {
|
|
86
101
|
if (child === parent.getWhenTrue()) {
|
|
87
|
-
|
|
102
|
+
branch(parent, parent.getCondition(), true);
|
|
88
103
|
}
|
|
89
104
|
else if (child === parent.getWhenFalse()) {
|
|
90
|
-
|
|
105
|
+
branch(parent, parent.getCondition(), false);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (Node.isCaseClause(parent) || Node.isDefaultClause(parent)) {
|
|
109
|
+
const statement = parent.getParent()?.getParent();
|
|
110
|
+
if (statement && Node.isSwitchStatement(statement)) {
|
|
111
|
+
steps.push({ kind: 'clause', test: statement, clause: parent });
|
|
91
112
|
}
|
|
92
113
|
}
|
|
93
114
|
if (Node.isBlock(parent) ||
|
|
@@ -97,21 +118,25 @@ export function testsOnPath(node, boundary, isResponse) {
|
|
|
97
118
|
for (const statement of parent.getStatements()) {
|
|
98
119
|
if (statement === child)
|
|
99
120
|
break;
|
|
121
|
+
if (Node.isSwitchStatement(statement)) {
|
|
122
|
+
steps.push({ kind: 'after-switch', test: statement });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
100
125
|
if (!Node.isIfStatement(statement))
|
|
101
126
|
continue;
|
|
102
127
|
const otherwise = statement.getElseStatement();
|
|
103
128
|
const thenLeaves = cannotComplete(statement.getThenStatement());
|
|
104
129
|
const elseLeaves = otherwise !== undefined && cannotComplete(otherwise);
|
|
105
130
|
if (thenLeaves && !elseLeaves) {
|
|
106
|
-
|
|
131
|
+
branch(statement, statement.getExpression(), false);
|
|
107
132
|
}
|
|
108
133
|
else if (elseLeaves && !thenLeaves) {
|
|
109
|
-
|
|
134
|
+
branch(statement, statement.getExpression(), true);
|
|
110
135
|
}
|
|
111
136
|
}
|
|
112
137
|
}
|
|
113
138
|
}
|
|
114
|
-
return
|
|
139
|
+
return steps;
|
|
115
140
|
}
|
|
116
141
|
/** The statuses `condition` lets through when it is true and when it is false. */
|
|
117
142
|
function readTest(condition, isResponse) {
|
|
@@ -216,7 +241,7 @@ export function readsResponseStatus(node, isResponse) {
|
|
|
216
241
|
* `switch` or a `try` is read as one that can complete, which only ever
|
|
217
242
|
* leaves a read on the path it was on.
|
|
218
243
|
*/
|
|
219
|
-
function cannotComplete(statement) {
|
|
244
|
+
export function cannotComplete(statement) {
|
|
220
245
|
if (Node.isReturnStatement(statement) ||
|
|
221
246
|
Node.isThrowStatement(statement) ||
|
|
222
247
|
Node.isBreakStatement(statement) ||
|