carrick 0.3.109 → 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.
@@ -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
+ }
@@ -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 take = (test, whenTrue) => {
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
- return;
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
- take(readTest(parent.getExpression(), isResponse), true);
94
+ branch(parent, parent.getExpression(), true);
80
95
  }
81
96
  else if (child === parent.getElseStatement()) {
82
- take(readTest(parent.getExpression(), isResponse), false);
97
+ branch(parent, parent.getExpression(), false);
83
98
  }
84
99
  }
85
100
  else if (Node.isConditionalExpression(parent)) {
86
101
  if (child === parent.getWhenTrue()) {
87
- take(readTest(parent.getCondition(), isResponse), true);
102
+ branch(parent, parent.getCondition(), true);
88
103
  }
89
104
  else if (child === parent.getWhenFalse()) {
90
- take(readTest(parent.getCondition(), isResponse), false);
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
- take(readTest(statement.getExpression(), isResponse), false);
131
+ branch(statement, statement.getExpression(), false);
107
132
  }
108
133
  else if (elseLeaves && !thenLeaves) {
109
- take(readTest(statement.getExpression(), isResponse), true);
134
+ branch(statement, statement.getExpression(), true);
110
135
  }
111
136
  }
112
137
  }
113
138
  }
114
- return sides;
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) ||
@@ -2584,6 +2584,8 @@ class DeclarationReader {
2584
2584
  * repository checked out under a `node_modules` ancestor, is not installed.
2585
2585
  * An install hoisted above the service root (`../../node_modules/pkg`) and
2586
2586
  * a pnpm store (`node_modules/.pnpm/pkg@1/node_modules/pkg`) are.
2587
+ * Mirrored by `installedPackageDirectory` in capture/, which the capture
2588
+ * seam keeps from being shared.
2587
2589
  */
2588
2590
  installedPackage(fileName) {
2589
2591
  const segments = path.relative(this.root, this.realpath(fileName)).split(path.sep);
@@ -1,3 +1,4 @@
1
+ import type { Node } from 'ts-morph';
1
2
  /**
2
3
  * The 1-based line of a position in a text, counted by line feeds alone: one
3
4
  * more than the `\n`s before the position. That is the count ts-morph's
@@ -7,3 +8,11 @@
7
8
  * The text is read once; each lookup after that is a binary search.
8
9
  */
9
10
  export declare function lineIndex(text: string): (pos: number) => number;
11
+ /**
12
+ * `node.getStartLineNumber()` from a per-file index. ts-morph counts the
13
+ * file's line feeds from position 0 on every call, so a walk that asks of
14
+ * every node of a file pays the file's size once per node (carrick#1935).
15
+ */
16
+ export declare function startLineOf(node: Node): number;
17
+ /** `node.getEndLineNumber()` from the same per-file index. */
18
+ export declare function endLineOf(node: Node): number;
@@ -24,3 +24,27 @@ export function lineIndex(text) {
24
24
  return lo + 1;
25
25
  };
26
26
  }
27
+ const fileLineIndexes = new WeakMap();
28
+ function lineAtIn(node, pos) {
29
+ // Keyed on the compiler node: a manipulation (`replaceWithText`, a retype)
30
+ // gives the file a new one, so the index is built again from the new text.
31
+ const key = node.getSourceFile().compilerNode;
32
+ let lineAt = fileLineIndexes.get(key);
33
+ if (!lineAt) {
34
+ lineAt = lineIndex(key.text);
35
+ fileLineIndexes.set(key, lineAt);
36
+ }
37
+ return lineAt(pos);
38
+ }
39
+ /**
40
+ * `node.getStartLineNumber()` from a per-file index. ts-morph counts the
41
+ * file's line feeds from position 0 on every call, so a walk that asks of
42
+ * every node of a file pays the file's size once per node (carrick#1935).
43
+ */
44
+ export function startLineOf(node) {
45
+ return lineAtIn(node, node.getStart());
46
+ }
47
+ /** `node.getEndLineNumber()` from the same per-file index. */
48
+ export function endLineOf(node) {
49
+ return lineAtIn(node, node.getEnd());
50
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Which incoming-message field picks each member of a response union (carrick#2054).
3
+ *
4
+ * A handler that sends `{ x }` when `?mode=a` and `{ y }` otherwise publishes
5
+ * `{ x } | { y }`. A call that states `mode=a` receives only `{ x }`, and a
6
+ * call that states nothing may receive either. The union alone cannot say
7
+ * which, so this reads it from the handler: for each success body the union
8
+ * joined, the tests on its path that compare a value read from the incoming message
9
+ * with a string literal.
10
+ *
11
+ * The reading, in order:
12
+ *
13
+ * - A test counts only when it separates two of the joined bodies: one on
14
+ * each side of it, or, for an earlier `if` that cannot complete, one in
15
+ * its branch and one after it. A validation guard whose branch only sends
16
+ * an error separates nothing the join kept, and is ignored.
17
+ * - A counted test is a message read when a value it tests is read from a
18
+ * parameter of a function enclosing the send (`messageRead`). A test that
19
+ * reads nothing from the incoming message (server state, a local count) is ignored,
20
+ * so a body under it is under no test of the field and stays in every case.
21
+ * - `===`, `==`, `!==` and `!=` against a string literal, with `!` flipping
22
+ * the test, and `switch` labels, are read as values. Any other test of a
23
+ * message read leaves its field read with its values unread.
24
+ * - `reads` lists every message read among the counted tests. `cases` is
25
+ * read only for one placed field whose tests were all read: one case per
26
+ * literal compared, plus "any other value" (`value: null`) when a body
27
+ * admits one, each the union of the bodies it admits.
28
+ * - When every case is the same union, the route has no modes.
29
+ *
30
+ * The published union is not changed by any of this.
31
+ */
32
+ import { Node, type Symbol as TsSymbol } from 'ts-morph';
33
+ /**
34
+ * Where in the incoming message a field is read from. Only the HTTP sources
35
+ * are read so far; another protocol (a socket message, a pub/sub payload)
36
+ * adds its own source here.
37
+ */
38
+ export type MessageSource = 'query' | 'body' | 'unplaced';
39
+ /** One incoming-message field a response union depends on. */
40
+ export interface ResponseModeRead {
41
+ /**
42
+ * `query`: read with `.get(...)` on the default lib's `URLSearchParams`.
43
+ * `body`: a top-level property of the awaited result of a zero-argument
44
+ * `json()` the default lib or an installed library declares. `unplaced`:
45
+ * anything else (a header, a path parameter, the method, a framework's own
46
+ * accessor, a `json()` the repo declares).
47
+ */
48
+ location: MessageSource;
49
+ field: string;
50
+ }
51
+ /** The union a stated value of the field receives. */
52
+ export interface ResponseModeCase {
53
+ /** The value; `null` for any value no other case names. */
54
+ value: string | null;
55
+ type_string: string;
56
+ }
57
+ export interface ResponseModes {
58
+ /** Every message field the union depends on, sorted by location, then field. */
59
+ reads: ResponseModeRead[];
60
+ /**
61
+ * Present only when `reads` is one placed field and every test of it was
62
+ * read. Sorted by value, the `null` case last.
63
+ */
64
+ cases?: ResponseModeCase[];
65
+ }
66
+ /** One success body the union joined, in source order. */
67
+ export interface JoinedBody {
68
+ node: Node;
69
+ type_string: string;
70
+ }
71
+ /**
72
+ * The modes of the union joined from `bodies`, or `undefined` when no message
73
+ * read separates them. `join` builds a union text from member texts with the
74
+ * rules the published union is built with. `isLibOrExternal` says whether a
75
+ * symbol is declared by a TypeScript lib or an installed library.
76
+ */
77
+ export declare function readResponseModes(bodies: JoinedBody[], join: (texts: string[]) => string, isLibOrExternal: (symbol: TsSymbol | undefined) => boolean): ResponseModes | undefined;