carrick 0.3.104 → 0.3.106
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 +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +13 -0
- package/sidecar/dist/src/capture/anchors.js +229 -37
- package/sidecar/dist/src/capture/api.d.ts +64 -4
- package/sidecar/dist/src/capture/check-classify.d.ts +10 -1
- package/sidecar/dist/src/capture/check-classify.js +23 -12
- package/sidecar/dist/src/capture/check-fields.js +4 -6
- package/sidecar/dist/src/capture/check-probe.js +16 -6
- package/sidecar/dist/src/capture/check-scrub.d.ts +3 -0
- package/sidecar/dist/src/capture/check-scrub.js +8 -4
- package/sidecar/dist/src/capture/check.js +75 -49
- package/sidecar/dist/src/capture/deep-walk.d.ts +20 -0
- package/sidecar/dist/src/capture/deep-walk.js +51 -11
- package/sidecar/dist/src/capture/guarded-fs.d.ts +5 -1
- package/sidecar/dist/src/capture/guarded-fs.js +23 -1
- package/sidecar/dist/src/capture/index.js +14 -2
- package/sidecar/dist/src/capture/member-name.d.ts +20 -0
- package/sidecar/dist/src/capture/member-name.js +24 -0
- package/sidecar/dist/src/capture/self-check.js +50 -9
- package/sidecar/dist/src/capture/service-config.d.ts +2 -0
- package/sidecar/dist/src/capture/service-config.js +1 -1
- package/sidecar/dist/src/failure-path.d.ts +67 -0
- package/sidecar/dist/src/failure-path.js +236 -0
- package/sidecar/dist/src/printed-names.d.ts +43 -0
- package/sidecar/dist/src/printed-names.js +186 -0
- package/sidecar/dist/src/retype.js +60 -99
- package/sidecar/dist/src/type-inferrer.d.ts +35 -3
- package/sidecar/dist/src/type-inferrer.js +208 -13
- package/sidecar/dist/src/type-structural-expander.js +10 -1
- package/sidecar/dist/src/types.d.ts +24 -0
- package/sidecar/dist/src/validators.d.ts +76 -0
- package/sidecar/dist/src/validators.js +8 -0
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* Seam: node builtins + `typescript` + this bundle only.
|
|
30
30
|
*/
|
|
31
31
|
import ts from 'typescript';
|
|
32
|
+
import { memberPath } from './member-name.js';
|
|
32
33
|
/**
|
|
33
34
|
* Cap on named fields. A mismatch with more differing members than this is
|
|
34
35
|
* better described as two unrelated shapes than as a list, and the text says
|
|
@@ -172,7 +173,7 @@ function walk(sent, expected, path, depth, ctx) {
|
|
|
172
173
|
* makes a sender-only member worth naming beside it. */
|
|
173
174
|
let absent = 0;
|
|
174
175
|
for (const [name, expectedProp] of expectedProps) {
|
|
175
|
-
const at =
|
|
176
|
+
const at = memberPath(path, expectedProp, checker);
|
|
176
177
|
const sentProp = sentProps.get(name);
|
|
177
178
|
const supplied = ctx.serverSupplied.has(name);
|
|
178
179
|
if (!sentProp) {
|
|
@@ -226,15 +227,12 @@ function walk(sent, expected, path, depth, ctx) {
|
|
|
226
227
|
// producer returns — nothing is absent and nothing is named.
|
|
227
228
|
if (absent === 0)
|
|
228
229
|
return;
|
|
229
|
-
for (const [name] of sentProps) {
|
|
230
|
+
for (const [name, sentProp] of sentProps) {
|
|
230
231
|
if (expectedProps.has(name))
|
|
231
232
|
continue;
|
|
232
|
-
ctx.found.push({ path:
|
|
233
|
+
ctx.found.push({ path: memberPath(path, sentProp, checker), nature: 'extra_in_sent' });
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
|
-
function join(path, name) {
|
|
236
|
-
return path === '' ? name : `${path}.${name}`;
|
|
237
|
-
}
|
|
238
236
|
function isOptional(symbol) {
|
|
239
237
|
return (symbol.flags & ts.SymbolFlags.Optional) !== 0;
|
|
240
238
|
}
|
|
@@ -155,8 +155,8 @@ export function buildProbe(spec, packageOf) {
|
|
|
155
155
|
// them in, so a route sending a `Uint8Array` read with `.blob()` is not a
|
|
156
156
|
// mismatch, and neither is the same route read as text. Either side, either
|
|
157
157
|
// half: a file upload states no fields a declared body could be compared
|
|
158
|
-
// with.
|
|
159
|
-
//
|
|
158
|
+
// with. Bytes that assign to bytes are not a contract either: the check
|
|
159
|
+
// cannot read a file's content (carrick#1812). `null`/`undefined` are
|
|
160
160
|
// dropped first, so `Blob | null` still gates, while a bare `null`,
|
|
161
161
|
// `undefined` or `never` leaves nothing to test.
|
|
162
162
|
if (spec.protocol === 'http') {
|
|
@@ -182,6 +182,12 @@ export function buildProbe(spec, packageOf) {
|
|
|
182
182
|
// or several (ambiguous envelope, or a bare payload whose own fields
|
|
183
183
|
// include more than one object-shaped property) keep the whole type —
|
|
184
184
|
// the unwrap never fires on anything but an unambiguous envelope;
|
|
185
|
+
// - a consumer that names a field only the outer object has (not the
|
|
186
|
+
// payload under its one object-shaped property) reads that object, so
|
|
187
|
+
// it keeps the whole type (carrick#1764): a bare payload with one
|
|
188
|
+
// object-shaped property is not an envelope around it. A field both
|
|
189
|
+
// declare says nothing either way, and neither does `__typename`, which
|
|
190
|
+
// the server puts on every object (see below);
|
|
185
191
|
// - `GqlObjLike` can never select `any`/`unknown`/`never` (top/bottom
|
|
186
192
|
// types fail both its array and its object arm), and the comparand is
|
|
187
193
|
// re-gated below anyway — the v2 port of v1's comparand re-guard, so
|
|
@@ -191,6 +197,10 @@ export function buildProbe(spec, packageOf) {
|
|
|
191
197
|
push(`type GqlPayloadKeys<T> = { [K in keyof T]-?: [GqlObjLike<T[K]>] extends [true] ? K : never }[keyof T];`);
|
|
192
198
|
push(`type GqlSingleKey<K> = [K] extends [never] ? never : ([GqlIsUnion<K>] extends [false] ? K : never);`);
|
|
193
199
|
push(`type GqlPayloadOf<S> = [S] extends [readonly unknown[]] ? never : [GqlIsUnion<S>] extends [true] ? never : [S] extends [object] ? ([GqlSingleKey<GqlPayloadKeys<S>>] extends [never] ? never : S[GqlSingleKey<GqlPayloadKeys<S>> & keyof S]) : never;`);
|
|
200
|
+
// The field names a reading declares: the keys of every object member, so
|
|
201
|
+
// a nullable or union consumer names what each member names.
|
|
202
|
+
push(`type GqlNames<T> = T extends object ? keyof T : never;`);
|
|
203
|
+
push(`type GqlReadsOuter<S, E> = [Extract<Exclude<GqlNames<S>, GqlNames<GqlPayloadOf<S>>>, Exclude<GqlNames<E>, '__typename'>>] extends [never] ? false : true;`);
|
|
194
204
|
// carrick#1759: the server supplies `__typename` on every object a
|
|
195
205
|
// selection asks for, so the consumer's `__typename` is read as OPTIONAL,
|
|
196
206
|
// at any depth. It keeps its declared type: a producer that states a
|
|
@@ -208,14 +218,14 @@ export function buildProbe(spec, packageOf) {
|
|
|
208
218
|
// nothing observable, so a consumer with no `__typename` to relax is
|
|
209
219
|
// judged, and named in the headline, exactly as before.
|
|
210
220
|
//
|
|
211
|
-
// The envelope short-circuit below tests the RELAXED consumer: a
|
|
212
|
-
//
|
|
213
|
-
// `__typename` alone and unwrap onto that
|
|
221
|
+
// The envelope short-circuit below tests the RELAXED consumer: a consumer
|
|
222
|
+
// that names only fields the payload shares would otherwise fail it on
|
|
223
|
+
// `__typename` alone and unwrap onto that payload.
|
|
214
224
|
push(`type GqlDepth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];`);
|
|
215
225
|
push(`type GqlTypenameParts<T, D extends number> = { [K in keyof T as K extends '__typename' ? never : K]: GqlTypenameOptional<T[K], GqlDepth[D]> } & { [K in keyof T as K extends '__typename' ? K : never]?: T[K] };`);
|
|
216
226
|
push(`type GqlTypenameOptional<T, D extends number = 16> = [D] extends [never] ? T : T extends (...args: any[]) => any ? T : T extends object ? ('__typename' extends keyof T ? { [K in keyof GqlTypenameParts<T, D>]: GqlTypenameParts<T, D>[K] } : { [K in keyof T]: GqlTypenameOptional<T[K], GqlDepth[D]> }) : T;`);
|
|
217
227
|
push(`type GqlExpected = [GqlTypenameOptional<Expected>] extends [Expected] ? Expected : GqlTypenameOptional<Expected>;`);
|
|
218
|
-
push(`type GqlComparand = [Sent] extends [GqlExpected] ? Sent : ([GqlPayloadOf<Sent>] extends [never] ? Sent : GqlPayloadOf<Sent>);`);
|
|
228
|
+
push(`type GqlComparand = [Sent] extends [GqlExpected] ? Sent : ([GqlPayloadOf<Sent>] extends [never] ? Sent : [GqlReadsOuter<Sent, Expected>] extends [true] ? Sent : GqlPayloadOf<Sent>);`);
|
|
219
229
|
gateLines.set(push(`type _G_comparand_any = Assert<Not<IsAny<GqlComparand>>>;`), 'sent:any');
|
|
220
230
|
gateLines.set(push(`type _G_comparand_unknown = Assert<Not<IsUnknown<GqlComparand>>>;`), 'sent:unknown');
|
|
221
231
|
gateLines.set(push(`type _G_comparand_never = Assert<Not<IsNever<GqlComparand>>>;`), 'sent:never');
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
export interface ScrubContext {
|
|
17
17
|
/** Absolute path of the scratch workspace root (fully removed from output). */
|
|
18
18
|
workspaceRoot: string;
|
|
19
|
+
/** The same root with symlinks resolved, as a tool that prints real paths
|
|
20
|
+
* names it (pnpm, under a macOS temp dir). Absent when there is no tree. */
|
|
21
|
+
workspaceRealRoot?: string;
|
|
19
22
|
/** Workspace package dir (== sanitized service) -> the @carrick/<dir> label. */
|
|
20
23
|
packageLabelOf: (packageDir: string) => string | undefined;
|
|
21
24
|
}
|
|
@@ -46,10 +46,14 @@ export function scrubPaths(text, ctx) {
|
|
|
46
46
|
let out = text.replace(IMPORT_PATH_RE, (_m, p1) => {
|
|
47
47
|
return `import("${labelForImportPath(p1, ctx)}")`;
|
|
48
48
|
});
|
|
49
|
-
// Defensive: no raw temp path may survive anywhere in the message.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// Defensive: no raw temp path may survive anywhere in the message. The real
|
|
50
|
+
// root goes first: the given root can be its suffix (`/private/var/...`
|
|
51
|
+
// against `/var/...`), and stripping that first would strand the prefix.
|
|
52
|
+
for (const root of [ctx.workspaceRealRoot, ctx.workspaceRoot]) {
|
|
53
|
+
if (!root)
|
|
54
|
+
continue;
|
|
55
|
+
out = out.split(root).join('');
|
|
56
|
+
out = out.split(root.split('/').join('\\')).join('');
|
|
53
57
|
}
|
|
54
58
|
return out;
|
|
55
59
|
}
|
|
@@ -103,15 +103,18 @@ export async function runCheck(opts, onProgress) {
|
|
|
103
103
|
// to a flat npm install that manufactures nominal false-incompatibles. Fail
|
|
104
104
|
// explicitly instead (design Check step 2).
|
|
105
105
|
if (!fs.existsSync(pnpmPath)) {
|
|
106
|
-
|
|
106
|
+
// Capture-decay verdicts stand without isolation too (carrick#1833).
|
|
107
|
+
const { preGated, probing } = preGate(planPairs(opts, (s) => `@carrick/${s}`), readStubAliasRecords(opts.stubs));
|
|
107
108
|
return {
|
|
108
109
|
success: false,
|
|
109
110
|
workspace_dir: '',
|
|
110
111
|
isolation: 'unavailable',
|
|
111
112
|
install_ok: false,
|
|
112
|
-
install_error: 'vendored pnpm not found; type isolation is unavailable',
|
|
113
113
|
ts_version: tsVersion,
|
|
114
|
-
verdicts: sortVerdicts(
|
|
114
|
+
verdicts: sortVerdicts([
|
|
115
|
+
...unverifiableAll(probing, 'isolation:unavailable', 'type isolation unavailable (pnpm missing); compatibility cannot be verified.'),
|
|
116
|
+
...preGated,
|
|
117
|
+
]),
|
|
115
118
|
degraded_services: opts.stubs.map((s) => ({
|
|
116
119
|
service_name: s.service_name,
|
|
117
120
|
reason: 'isolation unavailable',
|
|
@@ -126,6 +129,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
126
129
|
});
|
|
127
130
|
const scrubCtx = {
|
|
128
131
|
workspaceRoot: ws.workspaceDir,
|
|
132
|
+
workspaceRealRoot: fs.realpathSync(ws.workspaceDir),
|
|
129
133
|
packageLabelOf: ws.packageLabelOf,
|
|
130
134
|
};
|
|
131
135
|
// Generate probes. A pair naming a service with no stub is unverifiable.
|
|
@@ -154,61 +158,26 @@ export async function runCheck(opts, onProgress) {
|
|
|
154
158
|
const spec = opts.pairs.find((p) => p.pair_key === v.pair_key);
|
|
155
159
|
v.pair_id = fnvOfSpec(spec);
|
|
156
160
|
}
|
|
157
|
-
// Capture-time deep-decay pre-gate (adversarial-review finding 1): a
|
|
158
|
-
// member-level `any`/`unknown` recorded by the capture self-check with no
|
|
159
|
-
// failing-external explanation. The probe gates below are WHOLE-type only
|
|
160
|
-
// — `{ orderId: string; metadata: any }` sails through IsAny and the
|
|
161
|
-
// assignment compiles clean — so such pairs must never reach a probe.
|
|
162
|
-
// `any` routes to gate_caught_baked_any, `unknown` to unverifiable; both
|
|
163
|
-
// read as None downstream, never compatible.
|
|
164
161
|
const aliasRecords = readStubAliasRecords(opts.stubs);
|
|
165
|
-
const preGated =
|
|
166
|
-
const probing = [];
|
|
167
|
-
for (const plan of plans) {
|
|
168
|
-
const hit = deepDecayOf(plan, aliasRecords);
|
|
169
|
-
if (!hit) {
|
|
170
|
-
probing.push(plan);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
preGated.push({
|
|
174
|
-
pair_id: plan.pairId,
|
|
175
|
-
pair_key: plan.spec.pair_key,
|
|
176
|
-
// `any` is a confirmed baked top type -> gate_caught_baked_any; `unknown`
|
|
177
|
-
// and `budget_exhausted` (a subtree the capture walk could not finish)
|
|
178
|
-
// are "cannot verify" -> unverifiable. All read as None downstream.
|
|
179
|
-
bucket: hit.kind === 'any' ? 'gate_caught_baked_any' : 'unverifiable',
|
|
180
|
-
gate: `capture:${hit.side}:${hit.kind}`,
|
|
181
|
-
diagnostic: hit.kind === 'budget_exhausted'
|
|
182
|
-
? `the ${hit.side} type is too deep or wide to verify within the ` +
|
|
183
|
-
`capture budget (at '${hit.path}'); compatibility cannot be ` +
|
|
184
|
-
`verified — abstaining so a buried 'any' can never read compatible.`
|
|
185
|
-
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}' from ` +
|
|
186
|
-
`capture; compatibility cannot be verified (a partially-unresolved ` +
|
|
187
|
-
`type would let an arbitrary shape read compatible).`,
|
|
188
|
-
codes: [],
|
|
189
|
-
resolved: false,
|
|
190
|
-
unresolved_side: hit.side,
|
|
191
|
-
unresolved_reason: hit.kind === 'budget_exhausted'
|
|
192
|
-
? `the ${hit.side} type is too deep or wide to verify at '${hit.path}'`
|
|
193
|
-
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}'`,
|
|
194
|
-
notes: [],
|
|
195
|
-
});
|
|
196
|
-
}
|
|
162
|
+
const { preGated, probing } = preGate(plans, aliasRecords);
|
|
197
163
|
writeProbes(ws, probing);
|
|
198
164
|
const errors = [];
|
|
199
165
|
const degraded = [];
|
|
200
166
|
// ---- Install (async, off the event loop) --------------------------------
|
|
201
167
|
progress('installing', 'installing pinned dependencies');
|
|
202
168
|
const install = await runProcess(pnpmPath, ['install'], ws.workspaceDir);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
169
|
+
if (install.code !== 0) {
|
|
170
|
+
const installError = scrubPaths((install.stderr || install.stdout).trim().slice(0, 2000), scrubCtx);
|
|
171
|
+
// `errors` says why the run failed (carrick#1821); each pair the install
|
|
172
|
+
// stopped says it too, because the client reads every pair's own verdict
|
|
173
|
+
// on a failed check (carrick#1833).
|
|
174
|
+
errors.push(`workspace dependency install failed${installError ? `: ${installError}` : ''}`);
|
|
207
175
|
for (const s of opts.stubs) {
|
|
208
176
|
degraded.push({ service_name: s.service_name, reason: 'workspace install failed' });
|
|
209
177
|
}
|
|
210
178
|
const verdicts = sortVerdicts([
|
|
211
|
-
...unverifiableAll(probing, 'install:failed', 'workspace dependency install failed; compatibility cannot be verified.'
|
|
179
|
+
...unverifiableAll(probing, 'install:failed', 'workspace dependency install failed; compatibility cannot be verified.' +
|
|
180
|
+
(installError ? ` Installer output: ${installError}` : '')),
|
|
212
181
|
// Capture-decay verdicts stand regardless of the install outcome.
|
|
213
182
|
...preGated,
|
|
214
183
|
...unresolved,
|
|
@@ -220,7 +189,6 @@ export async function runCheck(opts, onProgress) {
|
|
|
220
189
|
workspace_dir: cleanup ? '' : ws.workspaceDir,
|
|
221
190
|
isolation: 'pnpm',
|
|
222
191
|
install_ok: false,
|
|
223
|
-
install_error: installError,
|
|
224
192
|
ts_version: tsVersion,
|
|
225
193
|
verdicts,
|
|
226
194
|
degraded_services: degraded,
|
|
@@ -249,7 +217,9 @@ export async function runCheck(opts, onProgress) {
|
|
|
249
217
|
});
|
|
250
218
|
}
|
|
251
219
|
const verdicts = sortVerdicts([
|
|
252
|
-
...unverifiableAll(probing, 'tsc:abnormal-termination',
|
|
220
|
+
...unverifiableAll(probing, 'tsc:abnormal-termination', `the type checker terminated abnormally (exit code ${tsc.code ?? 'null'}); ` +
|
|
221
|
+
'compatibility cannot be verified.' +
|
|
222
|
+
(excerpt ? ` Compiler output: ${excerpt}` : '')),
|
|
253
223
|
...preGated,
|
|
254
224
|
...unresolved,
|
|
255
225
|
]);
|
|
@@ -300,6 +270,7 @@ export async function runCheck(opts, onProgress) {
|
|
|
300
270
|
scrubCtx,
|
|
301
271
|
deepFindings: deepByPair.get(plan.pairId),
|
|
302
272
|
fieldReport: fieldsByPair.get(plan.pairId),
|
|
273
|
+
rawTextSide: rawTextSideOf(plan, aliasRecords),
|
|
303
274
|
})),
|
|
304
275
|
...preGated,
|
|
305
276
|
...unresolved,
|
|
@@ -341,6 +312,61 @@ function readStubAliasRecords(stubs) {
|
|
|
341
312
|
}
|
|
342
313
|
return byService;
|
|
343
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Capture-time deep-decay pre-gate (adversarial-review finding 1): a
|
|
317
|
+
* member-level `any`/`unknown` recorded by the capture self-check with no
|
|
318
|
+
* failing-external explanation. The probe gates are WHOLE-type only —
|
|
319
|
+
* `{ orderId: string; metadata: any }` sails through IsAny and the assignment
|
|
320
|
+
* compiles clean — so such pairs must never reach a probe. `any` routes to
|
|
321
|
+
* gate_caught_baked_any, `unknown` to unverifiable; both read as None
|
|
322
|
+
* downstream, never compatible. These verdicts come from the capture alone,
|
|
323
|
+
* so they stand whatever the install or the compiler then does (carrick#1833).
|
|
324
|
+
*/
|
|
325
|
+
function preGate(plans, aliasRecords) {
|
|
326
|
+
const preGated = [];
|
|
327
|
+
const probing = [];
|
|
328
|
+
for (const plan of plans) {
|
|
329
|
+
const hit = deepDecayOf(plan, aliasRecords);
|
|
330
|
+
if (!hit) {
|
|
331
|
+
probing.push(plan);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
preGated.push({
|
|
335
|
+
pair_id: plan.pairId,
|
|
336
|
+
pair_key: plan.spec.pair_key,
|
|
337
|
+
// `any` is a confirmed baked top type -> gate_caught_baked_any; `unknown`
|
|
338
|
+
// and `budget_exhausted` (a subtree the capture walk could not finish)
|
|
339
|
+
// are "cannot verify" -> unverifiable. All read as None downstream.
|
|
340
|
+
bucket: hit.kind === 'any' ? 'gate_caught_baked_any' : 'unverifiable',
|
|
341
|
+
gate: `capture:${hit.side}:${hit.kind}`,
|
|
342
|
+
diagnostic: hit.kind === 'budget_exhausted'
|
|
343
|
+
? `the ${hit.side} type is too deep or wide to verify within the ` +
|
|
344
|
+
`capture budget (at '${hit.path}'); compatibility cannot be ` +
|
|
345
|
+
`verified — abstaining so a buried 'any' can never read compatible.`
|
|
346
|
+
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}' from ` +
|
|
347
|
+
`capture; compatibility cannot be verified (a partially-unresolved ` +
|
|
348
|
+
`type would let an arbitrary shape read compatible).`,
|
|
349
|
+
codes: [],
|
|
350
|
+
resolved: false,
|
|
351
|
+
unresolved_side: hit.side,
|
|
352
|
+
unresolved_reason: hit.kind === 'budget_exhausted'
|
|
353
|
+
? `the ${hit.side} type is too deep or wide to verify at '${hit.path}'`
|
|
354
|
+
: `the ${hit.side} type carries '${hit.kind}' at '${hit.path}'`,
|
|
355
|
+
notes: [],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return { preGated, probing };
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The side whose capture record marks its body as read raw (carrick#1842):
|
|
362
|
+
* the sent side when both are, as the bytes gate names it.
|
|
363
|
+
*/
|
|
364
|
+
function rawTextSideOf(plan, aliasRecords) {
|
|
365
|
+
return [plan.direction.sent, plan.direction.expected].find((side) => {
|
|
366
|
+
const endpoint = plan.spec[side];
|
|
367
|
+
return aliasRecords.get(endpoint.service_name)?.get(endpoint.alias)?.raw_text_read === true;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
344
370
|
/** First side (producer, then consumer) whose capture recorded a deep decay. */
|
|
345
371
|
function deepDecayOf(plan, aliasRecords) {
|
|
346
372
|
for (const side of ['producer', 'consumer']) {
|
|
@@ -63,6 +63,9 @@ export declare function findDisqualifyingTopTypes(root: ts.Type, program: ts.Pro
|
|
|
63
63
|
* names. A walk that runs out of budget reports what it found before it did.
|
|
64
64
|
*/
|
|
65
65
|
export declare function findUnresolvedPlaceholders(root: ts.Type, program: ts.Program, checker: ts.TypeChecker, location: ts.Node): string[];
|
|
66
|
+
/** TypeScript's unresolved-reference placeholder: `TypeFlags.Any` with the
|
|
67
|
+
* internal `intrinsicName === 'error'` (stable since TS 1.x; see `anchors.ts`). */
|
|
68
|
+
export declare function isErrorPlaceholder(t: ts.Type): boolean;
|
|
66
69
|
/** What an anchor's SOURCE program could not resolve (carrick#1164). */
|
|
67
70
|
export interface UnresolvedAtAnchor {
|
|
68
71
|
/** Member paths holding the unresolved-reference placeholder. */
|
|
@@ -87,3 +90,20 @@ export interface UnresolvedAtAnchor {
|
|
|
87
90
|
* one other cause the walk itself can distinguish is its own budget.
|
|
88
91
|
*/
|
|
89
92
|
export declare function provenanceOf(finding: DeepTopType, unresolved?: UnresolvedAtAnchor): TypeProvenance;
|
|
93
|
+
/**
|
|
94
|
+
* The entry for one position at which the emitted tree holds the
|
|
95
|
+
* unresolved-reference placeholder (carrick#1446): `path` is `''` for the
|
|
96
|
+
* alias's own type. `names` are the identifiers the self-check could not find
|
|
97
|
+
* in this alias's statement and the files it reaches.
|
|
98
|
+
*
|
|
99
|
+
* The cause is `unresolved_import`, the word a reader already has for "a
|
|
100
|
+
* reference here did not resolve"; the detail says where it failed to.
|
|
101
|
+
*/
|
|
102
|
+
export declare function unresolvedInTreeProvenance(path: string, names: readonly string[]): TypeProvenance;
|
|
103
|
+
/**
|
|
104
|
+
* The root entry for a literal anchor demoted because its text names a module
|
|
105
|
+
* whose declaration emit was skipped (carrick#1446, carrick#1165): that text is
|
|
106
|
+
* what the index serves for it, and nothing in the emitted tree declares what
|
|
107
|
+
* it imports.
|
|
108
|
+
*/
|
|
109
|
+
export declare function unemittedModuleProvenance(): TypeProvenance;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* wherever it is reported.
|
|
15
15
|
*/
|
|
16
16
|
import ts from 'typescript';
|
|
17
|
+
import { memberPath } from './member-name.js';
|
|
17
18
|
/** Cap on findings reported per alias. The FIRST one is what the check phase
|
|
18
19
|
* pre-gates on, so verdicts never depend on this number; the rest are there to
|
|
19
20
|
* tell a reader which fields are `any` (carrick#376), and a type with more
|
|
@@ -73,7 +74,7 @@ export function findUnresolvedPlaceholders(root, program, checker, location) {
|
|
|
73
74
|
}
|
|
74
75
|
/** TypeScript's unresolved-reference placeholder: `TypeFlags.Any` with the
|
|
75
76
|
* internal `intrinsicName === 'error'` (stable since TS 1.x; see `anchors.ts`). */
|
|
76
|
-
function isErrorPlaceholder(t) {
|
|
77
|
+
export function isErrorPlaceholder(t) {
|
|
77
78
|
return ((t.flags & ts.TypeFlags.Any) !== 0 &&
|
|
78
79
|
t.intrinsicName === 'error');
|
|
79
80
|
}
|
|
@@ -230,7 +231,7 @@ function walkTopTypes(root, program, checker, location, flagOf) {
|
|
|
230
231
|
continue;
|
|
231
232
|
}
|
|
232
233
|
const propType = checker.getTypeOfSymbolAtLocation(prop, location);
|
|
233
|
-
walk(propType, path
|
|
234
|
+
walk(propType, memberPath(path, prop, checker), depth + 1);
|
|
234
235
|
if (exhausted)
|
|
235
236
|
return;
|
|
236
237
|
}
|
|
@@ -276,8 +277,8 @@ function disqualifyingFlag(t) {
|
|
|
276
277
|
}
|
|
277
278
|
return t.flags & ts.TypeFlags.Unknown ? 'unknown' : undefined;
|
|
278
279
|
}
|
|
279
|
-
/** How many
|
|
280
|
-
const
|
|
280
|
+
/** How many specifiers or names a detail lists before it counts the rest. */
|
|
281
|
+
const MAX_NAMED_IN_DETAIL = 3;
|
|
281
282
|
/**
|
|
282
283
|
* Turn a deep finding into the published provenance entry (carrick#376).
|
|
283
284
|
*
|
|
@@ -321,12 +322,51 @@ function unresolvedDetail(specifiers) {
|
|
|
321
322
|
const lead = "the type at this position did not resolve on the scanned checkout, so the compiler printed a placeholder 'any' rather than a declared type";
|
|
322
323
|
if (specifiers.length === 0)
|
|
323
324
|
return lead;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
return `${lead}; unresolved imports reachable from the anchor: ${quotedList(specifiers)}`;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The entry for one position at which the emitted tree holds the
|
|
329
|
+
* unresolved-reference placeholder (carrick#1446): `path` is `''` for the
|
|
330
|
+
* alias's own type. `names` are the identifiers the self-check could not find
|
|
331
|
+
* in this alias's statement and the files it reaches.
|
|
332
|
+
*
|
|
333
|
+
* The cause is `unresolved_import`, the word a reader already has for "a
|
|
334
|
+
* reference here did not resolve"; the detail says where it failed to.
|
|
335
|
+
*/
|
|
336
|
+
export function unresolvedInTreeProvenance(path, names) {
|
|
337
|
+
const where = path === '' ? 'this type' : 'the type at this position';
|
|
338
|
+
const lead = `${where} does not resolve in the declarations the capture emitted, so the ` +
|
|
339
|
+
"compiler reads it as 'any' although the printed text shows the name it could not follow";
|
|
340
|
+
return {
|
|
341
|
+
path,
|
|
342
|
+
kind: 'any',
|
|
343
|
+
reason: 'unresolved_import',
|
|
344
|
+
detail: names.length === 0
|
|
345
|
+
? lead
|
|
346
|
+
: `${lead}; names those declarations cannot find: ${quotedList(names)}`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* The root entry for a literal anchor demoted because its text names a module
|
|
351
|
+
* whose declaration emit was skipped (carrick#1446, carrick#1165): that text is
|
|
352
|
+
* what the index serves for it, and nothing in the emitted tree declares what
|
|
353
|
+
* it imports.
|
|
354
|
+
*/
|
|
355
|
+
export function unemittedModuleProvenance() {
|
|
356
|
+
return {
|
|
357
|
+
path: '',
|
|
358
|
+
kind: 'any',
|
|
359
|
+
reason: 'unresolved_import',
|
|
360
|
+
detail: 'this type names a module whose declarations the capture could not emit, so it ' +
|
|
361
|
+
"does not resolve in the declarations the capture emitted and the compiler reads it as 'any'",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/** `'a', 'b', 'c' and 2 more`. */
|
|
365
|
+
function quotedList(items) {
|
|
366
|
+
const named = items
|
|
367
|
+
.slice(0, MAX_NAMED_IN_DETAIL)
|
|
368
|
+
.map((item) => `'${item}'`)
|
|
327
369
|
.join(', ');
|
|
328
|
-
const more =
|
|
329
|
-
|
|
330
|
-
: '';
|
|
331
|
-
return `${lead}; unresolved imports reachable from the anchor: ${named}${more}`;
|
|
370
|
+
const more = items.length > MAX_NAMED_IN_DETAIL ? ` and ${items.length - MAX_NAMED_IN_DETAIL} more` : '';
|
|
371
|
+
return `${named}${more}`;
|
|
332
372
|
}
|
|
@@ -42,7 +42,11 @@ export declare class WriteGuard {
|
|
|
42
42
|
private readonly files;
|
|
43
43
|
private readonly protect;
|
|
44
44
|
private constructor();
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* A guard over `roots`. Throws when a root equals or contains a protected
|
|
47
|
+
* tree, or when a directory root lies inside one anywhere but beneath a
|
|
48
|
+
* `.carrick` directory (carrick#1768).
|
|
49
|
+
*/
|
|
46
50
|
static of(roots: WriteRoots): WriteGuard;
|
|
47
51
|
/**
|
|
48
52
|
* A new, empty directory under `parent` (the OS temp dir by default) and a
|
|
@@ -40,7 +40,11 @@ export class WriteGuard {
|
|
|
40
40
|
this.files = files;
|
|
41
41
|
this.protect = protect;
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* A guard over `roots`. Throws when a root equals or contains a protected
|
|
45
|
+
* tree, or when a directory root lies inside one anywhere but beneath a
|
|
46
|
+
* `.carrick` directory (carrick#1768).
|
|
47
|
+
*/
|
|
44
48
|
static of(roots) {
|
|
45
49
|
const protect = (roots.protect ?? []).map(landing);
|
|
46
50
|
const dirs = (roots.dirs ?? []).map(landing);
|
|
@@ -51,6 +55,16 @@ export class WriteGuard {
|
|
|
51
55
|
throw new WriteRefused(`refused write root ${root}: it is or contains the scanned tree ${covered}`);
|
|
52
56
|
}
|
|
53
57
|
}
|
|
58
|
+
// Inside a scanned tree, a directory root is Carrick's only beneath a
|
|
59
|
+
// `.carrick` directory. Anywhere else it is the repo's own (a sibling
|
|
60
|
+
// service, say), and a stub dir is emptied before it is written. A file
|
|
61
|
+
// root is exempt: the surface entry has to sit inside rootDir.
|
|
62
|
+
for (const root of dirs) {
|
|
63
|
+
const inside = protect.find((tree) => within(tree, root) && !carrickOwned(tree, root));
|
|
64
|
+
if (inside !== undefined) {
|
|
65
|
+
throw new WriteRefused(`refused write root ${root}: it lies inside the scanned tree ${inside}, outside a .carrick directory`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
54
68
|
return new WriteGuard(dirs, files, protect);
|
|
55
69
|
}
|
|
56
70
|
/**
|
|
@@ -133,6 +147,14 @@ function within(root, p) {
|
|
|
133
147
|
const rel = path.relative(root, p);
|
|
134
148
|
return rel === '' || (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
|
|
135
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Whether `root`, inside `tree`, lies beneath a `.carrick` directory between
|
|
152
|
+
* the two. `.carrick` itself is not enough: it holds the workspace's proposal,
|
|
153
|
+
* jobs and scan logs. Both are resolved paths.
|
|
154
|
+
*/
|
|
155
|
+
function carrickOwned(tree, root) {
|
|
156
|
+
return path.relative(tree, root).split(path.sep).slice(0, -1).includes('.carrick');
|
|
157
|
+
}
|
|
136
158
|
function isDirectory(p) {
|
|
137
159
|
try {
|
|
138
160
|
return fs.statSync(p).isDirectory();
|
|
@@ -145,10 +145,13 @@ export function captureStub(opts) {
|
|
|
145
145
|
// Everything this capture writes, it writes through `guard` (carrick#1748):
|
|
146
146
|
// the stub dir, the staging dir, and the surface entry. A stub dir is
|
|
147
147
|
// emptied before it is written, so one that is or holds the repo is refused
|
|
148
|
-
// before anything is touched.
|
|
148
|
+
// before anything is touched. The scan root is protected too: a stub dir
|
|
149
|
+
// in a sibling service of the same repo is refused unless it sits beneath
|
|
150
|
+
// a `.carrick` directory (carrick#1768).
|
|
149
151
|
let guard;
|
|
150
152
|
try {
|
|
151
|
-
|
|
153
|
+
const protect = opts.scanRoot === undefined ? [repoRoot] : [repoRoot, path.resolve(opts.scanRoot)];
|
|
154
|
+
guard = WriteGuard.of({ dirs: [stubDir], protect });
|
|
152
155
|
}
|
|
153
156
|
catch (err) {
|
|
154
157
|
return fail(stubDir, packageName, [err instanceof Error ? err.message : String(err)]);
|
|
@@ -563,6 +566,7 @@ function demoteDanglingAliases(args) {
|
|
|
563
566
|
serialization: 'structural_fallback',
|
|
564
567
|
failureReason: `declaration emit was skipped for module '${dangling}'; ` +
|
|
565
568
|
'alias demoted to keep the partially emitted tree usable',
|
|
569
|
+
namesUnemittedModule: true,
|
|
566
570
|
};
|
|
567
571
|
});
|
|
568
572
|
if (surfaceKey !== undefined && demoted.size > 0) {
|
|
@@ -667,11 +671,19 @@ function resolveAnchors(opts, parsed, ctx, deno) {
|
|
|
667
671
|
siblingSymbolSpecs.set(anchor.symbol_name, entryRelativeSpecifier(ctx.entryDir, ctx.repoRoot, anchor.source_file));
|
|
668
672
|
}
|
|
669
673
|
}
|
|
674
|
+
// A module specifier as the entry resolves it, under the program's own
|
|
675
|
+
// resolution (the Deno graph's, for a Deno service).
|
|
676
|
+
const entryMode = entrySource?.impliedNodeFormat;
|
|
677
|
+
const resolveFromEntry = (specifier) => (deno
|
|
678
|
+
? deno.resolve(specifier, ctx.entryPath, options)
|
|
679
|
+
: ts.resolveModuleName(specifier, ctx.entryPath, options, ts.sys, undefined, undefined, entryMode)
|
|
680
|
+
.resolvedModule)?.resolvedFileName;
|
|
670
681
|
return opts.anchors.map((request) => resolveAnchor(program, request, {
|
|
671
682
|
repoRoot: ctx.repoRoot,
|
|
672
683
|
entryDir: ctx.entryDir,
|
|
673
684
|
placeholder: placeholders.get(request.alias),
|
|
674
685
|
siblingSymbolSpecs,
|
|
686
|
+
resolveFromEntry,
|
|
675
687
|
}));
|
|
676
688
|
}
|
|
677
689
|
finally {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A member's place in a path, written the same way on every scan
|
|
3
|
+
* (carrick#1766).
|
|
4
|
+
*
|
|
5
|
+
* A member keyed by a unique symbol (`[Symbol.iterator]`, or a user's
|
|
6
|
+
* `const KEY: unique symbol`) has no name of its own. The checker escapes it as
|
|
7
|
+
* `__@<description>@<symbol id>`, and the id counts the symbols the process
|
|
8
|
+
* made before this one, so it differs between two scans of one tree and a
|
|
9
|
+
* stored sentence naming it changed on every scan. The checker prints such a
|
|
10
|
+
* member from its key instead, `[Symbol.iterator]`, which is how the source
|
|
11
|
+
* writes it.
|
|
12
|
+
*
|
|
13
|
+
* Every other member keeps `getName()`. A string key that starts with `__` is
|
|
14
|
+
* escaped with one more underscore, so it never reads as symbol-keyed here, and
|
|
15
|
+
* `symbolToString` would quote a key such as `content-type`, changing text that
|
|
16
|
+
* is already stable.
|
|
17
|
+
*/
|
|
18
|
+
import ts from 'typescript';
|
|
19
|
+
/** `parent.name`, or `parent[KEY]` for a member keyed by a unique symbol. */
|
|
20
|
+
export declare function memberPath(parent: string, member: ts.Symbol, checker: ts.TypeChecker): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A member's place in a path, written the same way on every scan
|
|
3
|
+
* (carrick#1766).
|
|
4
|
+
*
|
|
5
|
+
* A member keyed by a unique symbol (`[Symbol.iterator]`, or a user's
|
|
6
|
+
* `const KEY: unique symbol`) has no name of its own. The checker escapes it as
|
|
7
|
+
* `__@<description>@<symbol id>`, and the id counts the symbols the process
|
|
8
|
+
* made before this one, so it differs between two scans of one tree and a
|
|
9
|
+
* stored sentence naming it changed on every scan. The checker prints such a
|
|
10
|
+
* member from its key instead, `[Symbol.iterator]`, which is how the source
|
|
11
|
+
* writes it.
|
|
12
|
+
*
|
|
13
|
+
* Every other member keeps `getName()`. A string key that starts with `__` is
|
|
14
|
+
* escaped with one more underscore, so it never reads as symbol-keyed here, and
|
|
15
|
+
* `symbolToString` would quote a key such as `content-type`, changing text that
|
|
16
|
+
* is already stable.
|
|
17
|
+
*/
|
|
18
|
+
/** `parent.name`, or `parent[KEY]` for a member keyed by a unique symbol. */
|
|
19
|
+
export function memberPath(parent, member, checker) {
|
|
20
|
+
if (!member.escapedName.startsWith('__@')) {
|
|
21
|
+
return parent === '' ? member.getName() : `${parent}.${member.getName()}`;
|
|
22
|
+
}
|
|
23
|
+
return `${parent}${checker.symbolToString(member)}`;
|
|
24
|
+
}
|