carrick 0.3.85 → 0.3.87
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/dist/contract.d.ts +9 -5
- package/dist/contract.js.map +1 -1
- package/dist/init/hosted.d.ts +27 -1
- package/dist/init/hosted.js +53 -18
- package/dist/init/hosted.js.map +1 -1
- package/dist/init/output.d.ts +5 -3
- package/dist/init/output.js +12 -4
- package/dist/init/output.js.map +1 -1
- package/package.json +6 -6
- package/sidecar/dist/src/bundler.d.ts +5 -0
- package/sidecar/dist/src/bundler.js +13 -3
- package/sidecar/dist/src/capture/machinery.d.ts +1 -1
- package/sidecar/dist/src/capture/machinery.js +1 -1
- package/sidecar/dist/src/capture/repair-dangling.d.ts +46 -0
- package/sidecar/dist/src/capture/repair-dangling.js +162 -0
- package/sidecar/dist/src/capture/self-check.js +61 -5
- package/sidecar/dist/src/definition-resolver.d.ts +7 -0
- package/sidecar/dist/src/definition-resolver.js +43 -4
- package/sidecar/dist/src/origin.d.ts +45 -0
- package/sidecar/dist/src/origin.js +86 -0
- package/sidecar/dist/src/type-inferrer.d.ts +18 -31
- package/sidecar/dist/src/type-inferrer.js +31 -78
- package/sidecar/dist/src/type-structural-expander.d.ts +38 -6
- package/sidecar/dist/src/type-structural-expander.js +61 -32
- package/templates/skills/carrick-census.md +5 -0
- package/templates/skills/carrick-impact.md +11 -4
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair an emitted declaration whose own import does not resolve
|
|
3
|
+
* (carrick#1397).
|
|
4
|
+
*
|
|
5
|
+
* A `symbol` anchor prints nothing: its surface line is
|
|
6
|
+
* `import('./m').RenderRequest` and the shape lives in the `.d.ts` the
|
|
7
|
+
* compiler emitted for `m`. When that file imports a module the scanned
|
|
8
|
+
* checkout does not have — a dependency that was not installed, a generated
|
|
9
|
+
* module that was never generated — the stub reports the module missing, the
|
|
10
|
+
* self-check records a dangling internal specifier, and the scanner's publish
|
|
11
|
+
* gate refuses the alias whole. Twenty resolved members are thrown away with
|
|
12
|
+
* the one that did not resolve, which is exactly the loss carrick#1377 named
|
|
13
|
+
* for the two PRINT paths.
|
|
14
|
+
*
|
|
15
|
+
* So the declaration is repaired: the import that did not resolve is dropped
|
|
16
|
+
* and every type it bound is written `unknown` at its own position. This is
|
|
17
|
+
* file-granular — the same emitted file backs every alias whose closure
|
|
18
|
+
* reaches it — and it says nothing the source program did not already say:
|
|
19
|
+
* those names were TypeScript's unresolved-reference placeholder there too,
|
|
20
|
+
* which is why the anchor recorded their member paths as unresolved.
|
|
21
|
+
*
|
|
22
|
+
* Fail-closed in two places. A name used where `unknown` is not a type — a
|
|
23
|
+
* heritage clause, a type-parameter position — leaves the file untouched, so
|
|
24
|
+
* the alias keeps its honest refusal rather than gaining a broken declaration.
|
|
25
|
+
* And the caller re-checks the repaired tree: a name the rewrite did not reach
|
|
26
|
+
* turns into a `Cannot find name` diagnostic, which the self-check reads back
|
|
27
|
+
* as the same dangling specifier.
|
|
28
|
+
*
|
|
29
|
+
* Seam: node builtins + `typescript`, like the rest of this directory.
|
|
30
|
+
*/
|
|
31
|
+
import ts from 'typescript';
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
/**
|
|
34
|
+
* Drop every import of a failing specifier from each file and write `unknown`
|
|
35
|
+
* where its names were used.
|
|
36
|
+
*
|
|
37
|
+
* `failing` maps an absolute file path to the specifiers the stub could not
|
|
38
|
+
* resolve from it. Returns the files actually rewritten; a file whose names
|
|
39
|
+
* cannot all be replaced by `unknown` is left exactly as it was.
|
|
40
|
+
*/
|
|
41
|
+
export function repairDanglingImports(failing) {
|
|
42
|
+
const repaired = new Map();
|
|
43
|
+
for (const [file, specifiers] of failing) {
|
|
44
|
+
const result = repairFile(file, specifiers);
|
|
45
|
+
if (result)
|
|
46
|
+
repaired.set(file, result);
|
|
47
|
+
}
|
|
48
|
+
return repaired;
|
|
49
|
+
}
|
|
50
|
+
function repairFile(file, specifiers) {
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = fs.readFileSync(file, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
59
|
+
const names = new Set();
|
|
60
|
+
const edits = [];
|
|
61
|
+
const removedSpecifiers = new Set();
|
|
62
|
+
// A re-export of the failing module binds nothing in THIS file and is what
|
|
63
|
+
// another file reads through: dropping it would turn "cannot find module"
|
|
64
|
+
// into "has no exported member" over there, which is neither diagnostic the
|
|
65
|
+
// re-check reads, and the importer's member would publish as clean. There is
|
|
66
|
+
// no `unknown` to write for an export, so the file keeps its refusal.
|
|
67
|
+
let unreplaceable = false;
|
|
68
|
+
for (const statement of source.statements) {
|
|
69
|
+
const specifier = importSpecifierOf(statement);
|
|
70
|
+
if (specifier === undefined || !specifiers.has(specifier))
|
|
71
|
+
continue;
|
|
72
|
+
if (ts.isExportDeclaration(statement)) {
|
|
73
|
+
unreplaceable = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
for (const name of boundNames(statement))
|
|
77
|
+
names.add(name);
|
|
78
|
+
removedSpecifiers.add(specifier);
|
|
79
|
+
edits.push({ start: statement.getFullStart(), end: statement.getEnd(), text: '' });
|
|
80
|
+
}
|
|
81
|
+
// An `import(...)` type written inline carries its own specifier and has no
|
|
82
|
+
// statement to drop; the whole node becomes `unknown`.
|
|
83
|
+
const visit = (node) => {
|
|
84
|
+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
85
|
+
const literal = node.argument.literal;
|
|
86
|
+
if (ts.isStringLiteral(literal) && specifiers.has(literal.text)) {
|
|
87
|
+
removedSpecifiers.add(literal.text);
|
|
88
|
+
edits.push({ start: node.getStart(source), end: node.getEnd(), text: 'unknown' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (ts.isTypeReferenceNode(node) && names.has(leftmostName(node.typeName))) {
|
|
93
|
+
edits.push({ start: node.getStart(source), end: node.getEnd(), text: 'unknown' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (ts.isTypeQueryNode(node) && names.has(leftmostName(node.exprName))) {
|
|
97
|
+
edits.push({ start: node.getStart(source), end: node.getEnd(), text: 'unknown' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Positions where `unknown` is not a type: what a declaration EXTENDS or
|
|
101
|
+
// IMPLEMENTS, and a type parameter's own name. Leave the file alone rather
|
|
102
|
+
// than emit a declaration that does not parse as it used to read.
|
|
103
|
+
if (ts.isExpressionWithTypeArguments(node) &&
|
|
104
|
+
ts.isIdentifier(node.expression) &&
|
|
105
|
+
names.has(node.expression.text)) {
|
|
106
|
+
unreplaceable = true;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
node.forEachChild(visit);
|
|
110
|
+
};
|
|
111
|
+
visit(source);
|
|
112
|
+
if (unreplaceable || removedSpecifiers.size === 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
edits.sort((a, b) => b.start - a.start);
|
|
115
|
+
let out = text;
|
|
116
|
+
for (const edit of edits) {
|
|
117
|
+
out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
|
|
118
|
+
}
|
|
119
|
+
fs.writeFileSync(file, out);
|
|
120
|
+
return { specifiers: [...removedSpecifiers].sort(), names: [...names].sort() };
|
|
121
|
+
}
|
|
122
|
+
/** The module specifier an import STATEMENT names, if it names one. */
|
|
123
|
+
function importSpecifierOf(statement) {
|
|
124
|
+
let expression;
|
|
125
|
+
if (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) {
|
|
126
|
+
expression = statement.moduleSpecifier;
|
|
127
|
+
}
|
|
128
|
+
else if (ts.isImportEqualsDeclaration(statement) &&
|
|
129
|
+
ts.isExternalModuleReference(statement.moduleReference)) {
|
|
130
|
+
expression = statement.moduleReference.expression;
|
|
131
|
+
}
|
|
132
|
+
return expression && ts.isStringLiteral(expression) ? expression.text : undefined;
|
|
133
|
+
}
|
|
134
|
+
/** Every local name an import statement binds. */
|
|
135
|
+
function boundNames(statement) {
|
|
136
|
+
const out = [];
|
|
137
|
+
if (ts.isImportDeclaration(statement)) {
|
|
138
|
+
const clause = statement.importClause;
|
|
139
|
+
if (!clause)
|
|
140
|
+
return out;
|
|
141
|
+
if (clause.name)
|
|
142
|
+
out.push(clause.name.text);
|
|
143
|
+
const bindings = clause.namedBindings;
|
|
144
|
+
if (bindings && ts.isNamespaceImport(bindings))
|
|
145
|
+
out.push(bindings.name.text);
|
|
146
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
147
|
+
for (const element of bindings.elements)
|
|
148
|
+
out.push(element.name.text);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (ts.isImportEqualsDeclaration(statement)) {
|
|
152
|
+
out.push(statement.name.text);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
/** The leftmost identifier of `A`, `A.B` or `A.B.C`. */
|
|
157
|
+
function leftmostName(name) {
|
|
158
|
+
let current = name;
|
|
159
|
+
while (ts.isQualifiedName(current))
|
|
160
|
+
current = current.left;
|
|
161
|
+
return current.text;
|
|
162
|
+
}
|
|
@@ -33,6 +33,7 @@ import ts from 'typescript';
|
|
|
33
33
|
import * as fs from 'node:fs';
|
|
34
34
|
import * as path from 'node:path';
|
|
35
35
|
import { collectSpecifiers, isRelative, packageNameOf } from './specifiers.js';
|
|
36
|
+
import { repairDanglingImports } from './repair-dangling.js';
|
|
36
37
|
import { findDisqualifyingTopTypes, provenanceOf, } from './deep-walk.js';
|
|
37
38
|
export function selfCheckStub(args) {
|
|
38
39
|
const typesDir = path.join(args.stubDir, 'types');
|
|
@@ -56,7 +57,18 @@ export function selfCheckStub(args) {
|
|
|
56
57
|
linked = true;
|
|
57
58
|
}
|
|
58
59
|
try {
|
|
59
|
-
|
|
60
|
+
const first = runSelfCheck(args, treeFiles);
|
|
61
|
+
// carrick#1397: an emitted declaration that imports a module the checkout
|
|
62
|
+
// does not have makes every alias reaching that file unpublishable, however
|
|
63
|
+
// much of it resolved. Repair the file — drop the import, write `unknown`
|
|
64
|
+
// where its names were used — and check the tree again. The second pass is
|
|
65
|
+
// the verdict: it reads the repaired text, and a name the rewrite did not
|
|
66
|
+
// reach comes back as a `Cannot find name` diagnostic that
|
|
67
|
+
// `repairedNameFailures` folds back into the same dangling specifier.
|
|
68
|
+
const repaired = repairDanglingImports(first.internalFailuresByFile);
|
|
69
|
+
if (repaired.size === 0)
|
|
70
|
+
return first.records;
|
|
71
|
+
return runSelfCheck(args, treeFiles, repaired).records;
|
|
60
72
|
}
|
|
61
73
|
finally {
|
|
62
74
|
// unlinkSync, not rmSync: the link target is a directory and rmSync
|
|
@@ -65,7 +77,7 @@ export function selfCheckStub(args) {
|
|
|
65
77
|
fs.unlinkSync(linkPath);
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
|
-
function runSelfCheck(args, treeFiles) {
|
|
80
|
+
function runSelfCheck(args, treeFiles, repaired) {
|
|
69
81
|
const options = {
|
|
70
82
|
noEmit: true,
|
|
71
83
|
strict: true,
|
|
@@ -102,14 +114,25 @@ function runSelfCheck(args, treeFiles) {
|
|
|
102
114
|
return entry;
|
|
103
115
|
};
|
|
104
116
|
for (const d of diagnostics) {
|
|
105
|
-
if (
|
|
117
|
+
if (!d.file)
|
|
106
118
|
continue;
|
|
119
|
+
const abs = path.resolve(d.file.fileName);
|
|
107
120
|
const msg = ts.flattenDiagnosticMessageText(d.messageText, ' ');
|
|
121
|
+
// A name the repair did not reach: the import that bound it is gone, so
|
|
122
|
+
// the module is no longer reported missing and only this diagnostic is
|
|
123
|
+
// left to say the tree is incomplete. Blamed on the specifier that bound
|
|
124
|
+
// the name, which is the sentence a reader can act on (carrick#1397).
|
|
125
|
+
const orphaned = repairedNameFailure(repaired, abs, d, msg);
|
|
126
|
+
if (orphaned) {
|
|
127
|
+
bucketIn(failuresByFile, abs).internal.add(orphaned);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (d.code !== 2307 && d.code !== 2792)
|
|
131
|
+
continue;
|
|
108
132
|
const m = /Cannot find module '([^']+)'/.exec(msg);
|
|
109
133
|
if (!m)
|
|
110
134
|
continue;
|
|
111
135
|
const spec = m[1];
|
|
112
|
-
const abs = path.resolve(d.file.fileName);
|
|
113
136
|
// A surface diagnostic outside every alias statement (a file-level import,
|
|
114
137
|
// a reference directive) is attributable to no alias and keeps the
|
|
115
138
|
// service-wide file bucket: soundness over precision, the same fallback
|
|
@@ -158,7 +181,40 @@ function runSelfCheck(args, treeFiles) {
|
|
|
158
181
|
surfaceFailuresByAlias,
|
|
159
182
|
}));
|
|
160
183
|
}
|
|
161
|
-
|
|
184
|
+
const internalFailuresByFile = new Map();
|
|
185
|
+
for (const [file, failures] of failuresByFile) {
|
|
186
|
+
// The surface's own failures are a literal anchor's pasted text
|
|
187
|
+
// (carrick#1361), not a declaration with an import to drop.
|
|
188
|
+
if (file === surfaceAbs || failures.internal.size === 0)
|
|
189
|
+
continue;
|
|
190
|
+
// A file already repaired has had its turn: repairing it again would chase
|
|
191
|
+
// its own leftover diagnostics.
|
|
192
|
+
if (repaired?.has(file))
|
|
193
|
+
continue;
|
|
194
|
+
internalFailuresByFile.set(file, failures.internal);
|
|
195
|
+
}
|
|
196
|
+
return { records, internalFailuresByFile };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The specifier to blame for a `Cannot find name` diagnostic in a file this
|
|
200
|
+
* capture repaired, when the name is one the dropped import bound.
|
|
201
|
+
*
|
|
202
|
+
* The rewrite writes `unknown` at every type position it can reach; a use it
|
|
203
|
+
* cannot — and there should be none, since a position where `unknown` is not a
|
|
204
|
+
* type leaves the whole file untouched — would otherwise read as a clean tree,
|
|
205
|
+
* because the module that is missing is no longer imported to be reported.
|
|
206
|
+
* Fail closed: the alias keeps the refusal it had before the repair.
|
|
207
|
+
*/
|
|
208
|
+
function repairedNameFailure(repaired, file, diagnostic, message) {
|
|
209
|
+
if (!repaired || diagnostic.code !== 2304)
|
|
210
|
+
return undefined;
|
|
211
|
+
const entry = repaired.get(file);
|
|
212
|
+
if (!entry)
|
|
213
|
+
return undefined;
|
|
214
|
+
const named = /Cannot find name '([^']+)'/.exec(message);
|
|
215
|
+
if (!named || !entry.names.includes(named[1]))
|
|
216
|
+
return undefined;
|
|
217
|
+
return entry.specifiers[0];
|
|
162
218
|
}
|
|
163
219
|
/**
|
|
164
220
|
* Position -> the alias whose `export type` statement span covers it, for
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* - `expanded`: the fully *structural* form, with every named member type
|
|
13
13
|
* inlined to its member structure, recursively.
|
|
14
14
|
*
|
|
15
|
+
* An alias whose own type did not resolve produces neither: the tree is read
|
|
16
|
+
* with nothing installed, so a reference that leaves it lands on TypeScript's
|
|
17
|
+
* unresolved-reference placeholder, which the compiler prints as the reference
|
|
18
|
+
* text rather than as `any` (see `isUnresolvedReference`). Both forms are then
|
|
19
|
+
* the top type, which is what every downstream rule about an empty answer is
|
|
20
|
+
* written to refuse.
|
|
21
|
+
*
|
|
15
22
|
* `type.getText(node, NoTruncation)` does NOT inline named members — the
|
|
16
23
|
* compiler prints a referenced type by its symbol name when that symbol is in
|
|
17
24
|
* scope (`total: Money`, not `total: { amountCents: number; currency: string }`).
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* - `expanded`: the fully *structural* form, with every named member type
|
|
13
13
|
* inlined to its member structure, recursively.
|
|
14
14
|
*
|
|
15
|
+
* An alias whose own type did not resolve produces neither: the tree is read
|
|
16
|
+
* with nothing installed, so a reference that leaves it lands on TypeScript's
|
|
17
|
+
* unresolved-reference placeholder, which the compiler prints as the reference
|
|
18
|
+
* text rather than as `any` (see `isUnresolvedReference`). Both forms are then
|
|
19
|
+
* the top type, which is what every downstream rule about an empty answer is
|
|
20
|
+
* written to refuse.
|
|
21
|
+
*
|
|
15
22
|
* `type.getText(node, NoTruncation)` does NOT inline named members — the
|
|
16
23
|
* compiler prints a referenced type by its symbol name when that symbol is in
|
|
17
24
|
* scope (`total: Money`, not `total: { amountCents: number; currency: string }`).
|
|
@@ -26,7 +33,7 @@
|
|
|
26
33
|
import * as path from 'node:path';
|
|
27
34
|
import * as fs from 'node:fs';
|
|
28
35
|
import { Project, Node } from 'ts-morph';
|
|
29
|
-
import { expandTypeStructural } from './type-structural-expander.js';
|
|
36
|
+
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
30
37
|
export class DefinitionResolver {
|
|
31
38
|
project;
|
|
32
39
|
constructor(options) {
|
|
@@ -71,7 +78,10 @@ export class DefinitionResolver {
|
|
|
71
78
|
}
|
|
72
79
|
const results = [];
|
|
73
80
|
for (const alias of aliases) {
|
|
74
|
-
const result = this.resolveAlias(surface, alias
|
|
81
|
+
const result = this.resolveAlias(surface, alias, {
|
|
82
|
+
program: stubProject.getProgram().compilerObject,
|
|
83
|
+
repoRoot: stubDir,
|
|
84
|
+
});
|
|
75
85
|
if (result) {
|
|
76
86
|
results.push(result);
|
|
77
87
|
}
|
|
@@ -89,7 +99,7 @@ export class DefinitionResolver {
|
|
|
89
99
|
/**
|
|
90
100
|
* Resolve a single alias: the original text and the structural form.
|
|
91
101
|
*/
|
|
92
|
-
resolveAlias(sourceFile, alias) {
|
|
102
|
+
resolveAlias(sourceFile, alias, origin) {
|
|
93
103
|
const decl = sourceFile.getTypeAlias(alias) ??
|
|
94
104
|
sourceFile.getInterface(alias) ??
|
|
95
105
|
sourceFile.getClass(alias) ??
|
|
@@ -98,6 +108,12 @@ export class DefinitionResolver {
|
|
|
98
108
|
return null;
|
|
99
109
|
try {
|
|
100
110
|
const type = decl.getType();
|
|
111
|
+
// An alias whose own type did not resolve answers the top type it IS,
|
|
112
|
+
// not the reference the compiler echoes for it (carrick#1444).
|
|
113
|
+
if (isUnresolvedReference(type)) {
|
|
114
|
+
this.log(`${alias} names a type this tree cannot resolve; answering any`);
|
|
115
|
+
return { type_alias: alias, definition: 'any', expanded: 'any' };
|
|
116
|
+
}
|
|
101
117
|
// As-written form: prefer the alias target's own declaration (the real
|
|
102
118
|
// `interface Order {...}` in the tree) over the surface's import-type
|
|
103
119
|
// line, so named shapes read naturally. Fall back to the alias line for
|
|
@@ -118,7 +134,7 @@ export class DefinitionResolver {
|
|
|
118
134
|
}
|
|
119
135
|
}
|
|
120
136
|
// Structural form — every named member inlined to its shape.
|
|
121
|
-
const expanded = expandTypeStructural(type);
|
|
137
|
+
const expanded = expandTypeStructural(type, origin);
|
|
122
138
|
return { type_alias: alias, definition, expanded };
|
|
123
139
|
}
|
|
124
140
|
catch (err) {
|
|
@@ -133,6 +149,29 @@ export class DefinitionResolver {
|
|
|
133
149
|
console.error(`[sidecar:definition-resolver:error] ${message}`);
|
|
134
150
|
}
|
|
135
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* True when a type is TypeScript's unresolved-reference placeholder:
|
|
154
|
+
* `TypeFlags.Any` carrying the internal `intrinsicName === 'error'` (stable
|
|
155
|
+
* since TS 1.x). `capture/deep-walk.ts` tests the same two facts for the same
|
|
156
|
+
* reason; the seam forbids importing it from here, so the pin is that both read
|
|
157
|
+
* the flag and the intrinsic name and nothing else.
|
|
158
|
+
*
|
|
159
|
+
* The compiler prints this placeholder as the reference text it FAILED to
|
|
160
|
+
* resolve, never as `any`. That print is what makes an unresolvable alias read
|
|
161
|
+
* as a confident name: an instantiation over a dependency's internal generics
|
|
162
|
+
* has no members in it and resolves nowhere, and every scanner-side rule that
|
|
163
|
+
* refuses an empty answer asks the TEXT (`text_is_bare_top_type`), so nothing
|
|
164
|
+
* downstream can tell the difference (carrick#1444).
|
|
165
|
+
*
|
|
166
|
+
* Asked only of the alias's own type. At a member position the same echo is
|
|
167
|
+
* the producer's own vocabulary (`status: OrderStatus`) inside a shape that
|
|
168
|
+
* otherwise resolved, and replacing it with `any` would remove a name a reader
|
|
169
|
+
* can look up in the producing repo.
|
|
170
|
+
*/
|
|
171
|
+
function isUnresolvedReference(type) {
|
|
172
|
+
return (type.isAny() &&
|
|
173
|
+
type.compilerType.intrinsicName === 'error');
|
|
174
|
+
}
|
|
136
175
|
/** All .d.ts files under a directory, depth-first, deterministic order. */
|
|
137
176
|
function walkDtsFiles(dir) {
|
|
138
177
|
const out = [];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a declaration comes from: the user's own source, or the runtime and
|
|
3
|
+
* the packages it installed.
|
|
4
|
+
*
|
|
5
|
+
* One module because more than one layer asks the question — the inference
|
|
6
|
+
* path (`type-inferrer.ts`, deciding whether a type is framework machinery)
|
|
7
|
+
* and the structural printer (`type-structural-expander.ts`, deciding whether
|
|
8
|
+
* to inline a type's members or keep it by name). They must answer it the same
|
|
9
|
+
* way or one layer inlines what the other suppresses.
|
|
10
|
+
*
|
|
11
|
+
* The capture side keeps its own copy in `capture/machinery.ts`: that seam
|
|
12
|
+
* forbids importing anything but node builtins, `typescript` and its own
|
|
13
|
+
* bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
|
|
14
|
+
* rather than by sharing this module.
|
|
15
|
+
*/
|
|
16
|
+
import type { ts } from 'ts-morph';
|
|
17
|
+
/**
|
|
18
|
+
* True when a declaration's source file is runtime/library origin rather than
|
|
19
|
+
* user source. Four answers, in order:
|
|
20
|
+
*
|
|
21
|
+
* 1. The runtime declarations Carrick materialises for a non-Node runtime
|
|
22
|
+
* under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
|
|
23
|
+
* modules it copies beside them. Carrick's own artefact layout, not a
|
|
24
|
+
* guess about anyone else's.
|
|
25
|
+
* 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
|
|
26
|
+
* classifies it; on a bare checkout the DOM `Response` resolves from here.
|
|
27
|
+
* 3. An install under a `node_modules` segment, however it entered the
|
|
28
|
+
* program (an import, or a root the loader registered).
|
|
29
|
+
* 4. A file the PROGRAM'S RESOLVER marked as an external library import.
|
|
30
|
+
* This is the graph-backed answer for a project whose resolution does not
|
|
31
|
+
* go through `node_modules` (carrick#1264): Deno serves an npm dependency's
|
|
32
|
+
* types from its own cache, a path with no `node_modules` segment, and
|
|
33
|
+
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
34
|
+
* the graph, which the compiler records on the file. Nothing crosses the
|
|
35
|
+
* capture seam; both programs are built with that host. One exclusion: a
|
|
36
|
+
* workspace package reached through a `node_modules` symlink is also
|
|
37
|
+
* marked external by the compiler but is the user's own source, so a file
|
|
38
|
+
* inside the checkout (the nearest `.git` above the service root) that
|
|
39
|
+
* carries no `node_modules` segment stays user source.
|
|
40
|
+
*
|
|
41
|
+
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
42
|
+
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
43
|
+
* the pair on a real program.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a declaration comes from: the user's own source, or the runtime and
|
|
3
|
+
* the packages it installed.
|
|
4
|
+
*
|
|
5
|
+
* One module because more than one layer asks the question — the inference
|
|
6
|
+
* path (`type-inferrer.ts`, deciding whether a type is framework machinery)
|
|
7
|
+
* and the structural printer (`type-structural-expander.ts`, deciding whether
|
|
8
|
+
* to inline a type's members or keep it by name). They must answer it the same
|
|
9
|
+
* way or one layer inlines what the other suppresses.
|
|
10
|
+
*
|
|
11
|
+
* The capture side keeps its own copy in `capture/machinery.ts`: that seam
|
|
12
|
+
* forbids importing anything but node builtins, `typescript` and its own
|
|
13
|
+
* bundle, so the two are kept in lockstep by `machinery-indicator-mirror.test.ts`
|
|
14
|
+
* rather than by sharing this module.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
/**
|
|
19
|
+
* True when a declaration's source file is runtime/library origin rather than
|
|
20
|
+
* user source. Four answers, in order:
|
|
21
|
+
*
|
|
22
|
+
* 1. The runtime declarations Carrick materialises for a non-Node runtime
|
|
23
|
+
* under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
|
|
24
|
+
* modules it copies beside them. Carrick's own artefact layout, not a
|
|
25
|
+
* guess about anyone else's.
|
|
26
|
+
* 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
|
|
27
|
+
* classifies it; on a bare checkout the DOM `Response` resolves from here.
|
|
28
|
+
* 3. An install under a `node_modules` segment, however it entered the
|
|
29
|
+
* program (an import, or a root the loader registered).
|
|
30
|
+
* 4. A file the PROGRAM'S RESOLVER marked as an external library import.
|
|
31
|
+
* This is the graph-backed answer for a project whose resolution does not
|
|
32
|
+
* go through `node_modules` (carrick#1264): Deno serves an npm dependency's
|
|
33
|
+
* types from its own cache, a path with no `node_modules` segment, and
|
|
34
|
+
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
35
|
+
* the graph, which the compiler records on the file. Nothing crosses the
|
|
36
|
+
* capture seam; both programs are built with that host. One exclusion: a
|
|
37
|
+
* workspace package reached through a `node_modules` symlink is also
|
|
38
|
+
* marked external by the compiler but is the user's own source, so a file
|
|
39
|
+
* inside the checkout (the nearest `.git` above the service root) that
|
|
40
|
+
* carries no `node_modules` segment stays user source.
|
|
41
|
+
*
|
|
42
|
+
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
43
|
+
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
44
|
+
* the pair on a real program.
|
|
45
|
+
*/
|
|
46
|
+
export function isExternalOrigin(program, sourceFile, repoRoot) {
|
|
47
|
+
const file = sourceFile.fileName.replace(/\\/g, '/');
|
|
48
|
+
if (file.includes('/.carrick/deno/')) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (program.isSourceFileDefaultLibrary(sourceFile)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (file.includes('/node_modules/')) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return program.isSourceFileFromExternalLibrary(sourceFile) && !isInsideCheckout(file, repoRoot);
|
|
58
|
+
}
|
|
59
|
+
const checkoutRoots = new Map();
|
|
60
|
+
/** The checkout the service root sits in: the nearest ancestor holding a
|
|
61
|
+
* `.git` entry (a directory, or the file a worktree carries), else the service
|
|
62
|
+
* root itself. */
|
|
63
|
+
function checkoutRootOf(repoRoot) {
|
|
64
|
+
const key = path.resolve(repoRoot);
|
|
65
|
+
const cached = checkoutRoots.get(key);
|
|
66
|
+
if (cached)
|
|
67
|
+
return cached;
|
|
68
|
+
let dir = key;
|
|
69
|
+
let root = key;
|
|
70
|
+
for (;;) {
|
|
71
|
+
if (fs.existsSync(path.join(dir, '.git'))) {
|
|
72
|
+
root = dir;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
const parent = path.dirname(dir);
|
|
76
|
+
if (parent === dir)
|
|
77
|
+
break;
|
|
78
|
+
dir = parent;
|
|
79
|
+
}
|
|
80
|
+
checkoutRoots.set(key, root);
|
|
81
|
+
return root;
|
|
82
|
+
}
|
|
83
|
+
function isInsideCheckout(file, repoRoot) {
|
|
84
|
+
const root = checkoutRootOf(repoRoot).replace(/\\/g, '/');
|
|
85
|
+
return file === root || file.startsWith(root.endsWith('/') ? root : root + '/');
|
|
86
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* (redirects, 204s), the LLM emits null and we fall back to the containing
|
|
18
18
|
* function's return type.
|
|
19
19
|
*/
|
|
20
|
-
import { Project
|
|
20
|
+
import { Project } from 'ts-morph';
|
|
21
21
|
import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js';
|
|
22
22
|
/**
|
|
23
23
|
* Strongly-discriminating member names of HTTP transport machinery — the
|
|
@@ -38,35 +38,6 @@ import type { InferRequestItem, InferResult, ExtractionConfig } from './types.js
|
|
|
38
38
|
* test (`machinery-indicator-mirror.test.ts`) asserts the two sets stay equal.
|
|
39
39
|
*/
|
|
40
40
|
export declare const MACHINERY_MEMBER_INDICATORS: Set<string>;
|
|
41
|
-
/**
|
|
42
|
-
* True when a declaration's source file is runtime/library origin rather than
|
|
43
|
-
* user source. Four answers, in order:
|
|
44
|
-
*
|
|
45
|
-
* 1. The runtime declarations Carrick materialises for a non-Node runtime
|
|
46
|
-
* under `.carrick/deno/` (carrick#1017), and the remote (JSR, `https:`)
|
|
47
|
-
* modules it copies beside them. Carrick's own artefact layout, not a
|
|
48
|
-
* guess about anyone else's.
|
|
49
|
-
* 2. A TypeScript default library (`lib.dom.d.ts`, ...), as the program
|
|
50
|
-
* classifies it; on a bare checkout the DOM `Response` resolves from here.
|
|
51
|
-
* 3. An install under a `node_modules` segment, however it entered the
|
|
52
|
-
* program (an import, or a root the loader registered).
|
|
53
|
-
* 4. A file the PROGRAM'S RESOLVER marked as an external library import.
|
|
54
|
-
* This is the graph-backed answer for a project whose resolution does not
|
|
55
|
-
* go through `node_modules` (carrick#1264): Deno serves an npm dependency's
|
|
56
|
-
* types from its own cache, a path with no `node_modules` segment, and
|
|
57
|
-
* `DenoProject.resolve` hands the compiler `isExternalLibraryImport` from
|
|
58
|
-
* the graph, which the compiler records on the file. Nothing crosses the
|
|
59
|
-
* capture seam; both programs are built with that host. One exclusion: a
|
|
60
|
-
* workspace package reached through a `node_modules` symlink is also
|
|
61
|
-
* marked external by the compiler but is the user's own source, so a file
|
|
62
|
-
* inside the checkout (the nearest `.git` above the service root) that
|
|
63
|
-
* carries no `node_modules` segment stays user source.
|
|
64
|
-
*
|
|
65
|
-
* Lockstep mirror of `isExternalOrigin` in `capture/machinery.ts` (the capture
|
|
66
|
-
* seam forbids sharing a module); `machinery-indicator-mirror.test.ts` guards
|
|
67
|
-
* the pair on a real program.
|
|
68
|
-
*/
|
|
69
|
-
export declare function isExternalOrigin(program: ts.Program, sourceFile: ts.SourceFile, repoRoot: string): boolean;
|
|
70
41
|
/**
|
|
71
42
|
* Options for TypeInferrer construction
|
|
72
43
|
*/
|
|
@@ -99,6 +70,13 @@ export declare class TypeInferrer {
|
|
|
99
70
|
private readonly packageOf;
|
|
100
71
|
private readonly repoRoot;
|
|
101
72
|
constructor(options: TypeInferrerOptions);
|
|
73
|
+
/**
|
|
74
|
+
* What the structural printer needs to tell the user's own declarations from
|
|
75
|
+
* the runtime's and its packages' — the same program and service root this
|
|
76
|
+
* class asks `isExternalOrigin` about machinery, so the two layers cannot
|
|
77
|
+
* classify one declaration two ways.
|
|
78
|
+
*/
|
|
79
|
+
private expandOrigin;
|
|
102
80
|
/**
|
|
103
81
|
* Infer types for the given requests
|
|
104
82
|
*
|
|
@@ -708,7 +686,16 @@ export declare class TypeInferrer {
|
|
|
708
686
|
* True when an object literal argument is response INIT rather than a body:
|
|
709
687
|
* every property it declares is one the standard `ResponseInit` declares
|
|
710
688
|
* (`status`, `statusText`, `headers`), and it states at least one of them as
|
|
711
|
-
* init really does — a
|
|
689
|
+
* init really does — a status in the HTTP range, or headers.
|
|
690
|
+
*
|
|
691
|
+
* The status does NOT have to be a literal code. A route that carries its
|
|
692
|
+
* outcome in a value writes `new Response(body, { status: result.status })`,
|
|
693
|
+
* where the source fixes no code and `statedStatusCodes` answers
|
|
694
|
+
* `'variable'` — status-shaped, just not pinned. Requiring a literal there
|
|
695
|
+
* left the whole init object reading as a body, so an endpoint whose payload
|
|
696
|
+
* this layer does not publish (a string body) published `{ status: number }`
|
|
697
|
+
* as its response contract instead: a wrong contract, served, where an
|
|
698
|
+
* abstention was the honest answer.
|
|
712
699
|
*
|
|
713
700
|
* A payload that merely has a `status` member of its own (`{ status: "ok",
|
|
714
701
|
* service: "ledger" }`) declares members init does not, or states `status` as
|