chore(deps): count triple-slash type references as candidates
From Copilot's twelfth pass of #1962. A `/// <reference types="x" />` pulls in a package's declarations exactly like an import does, but TypeScript reports it in `preProcessFile`'s `typeReferenceDirectives`, not `importedFiles` — and only the latter was read. A shared source using one could therefore load the root's copy of a package alongside a client's without it ever entering the candidate set, so its skew would pass. Same silent-miss direction as the earlier syntax gaps, through a channel the parser switch didn't cover because I only read one of its two outputs. `typeReferencePackageNames` maps a directive to both candidates, since the name is the *type* name and not the package: `node` resolves to `@types/node`, while a package shipping its own declarations resolves to itself. Returning both over-approximates, the safe direction — whichever isn't installed drops out downstream. Scoped names mangle `@scope/pkg` → `@types/scope__pkg`, TypeScript's convention. `path` references name a file, not a package, and are ignored. The real candidate set is unchanged at 17: no shared source uses a triple-slash reference today. As with the other syntax fixes, this closes the channel against the one that arrives later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
This commit is contained in:
parent
af63a07817
commit
87c1e05420
2 changed files with 64 additions and 2 deletions
|
|
@ -175,7 +175,30 @@ function typescript() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every third-party package name imported by a blob of TypeScript source.
|
* The package(s) a `/// <reference types="x" />` directive can resolve to
|
||||||
|
* (Copilot, #1962). Such a directive pulls in declarations exactly like an
|
||||||
|
* import does, but TypeScript reports it separately from `importedFiles`, so
|
||||||
|
* reading only the latter would let a referenced package skew unseen.
|
||||||
|
*
|
||||||
|
* Both candidates are returned because the directive name is the *type* name,
|
||||||
|
* not the package: `node` resolves to `@types/node`, while a package shipping
|
||||||
|
* its own declarations resolves to itself. Returning both over-approximates,
|
||||||
|
* which is the safe direction — whichever isn't installed drops out. A scoped
|
||||||
|
* name mangles as `@scope/pkg` → `@types/scope__pkg`, TypeScript's convention.
|
||||||
|
*/
|
||||||
|
export function typeReferencePackageNames(directive) {
|
||||||
|
const name = packageNameOf(directive);
|
||||||
|
if (!name) return [];
|
||||||
|
const scoped = /^@([^/]+)\/(.+)$/.exec(name);
|
||||||
|
const typesName = scoped
|
||||||
|
? `@types/${scoped[1]}__${scoped[2]}`
|
||||||
|
: `@types/${name}`;
|
||||||
|
return [name, typesName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every third-party package name whose declarations a blob of TypeScript source
|
||||||
|
* pulls in — via an import of any form, or a triple-slash type reference.
|
||||||
* Over-approximating is safe (a name absent from every lockfile contributes
|
* Over-approximating is safe (a name absent from every lockfile contributes
|
||||||
* nothing downstream — `@inspector/core` is a build-time alias, not a package,
|
* nothing downstream — `@inspector/core` is a build-time alias, not a package,
|
||||||
* and drops out that way); under-approximating is not, since a missed package
|
* and drops out that way); under-approximating is not, since a missed package
|
||||||
|
|
@ -185,11 +208,14 @@ export function importedPackageNames(source) {
|
||||||
const names = new Set();
|
const names = new Set();
|
||||||
// (source, readImportFiles, detectJavaScriptImports) — the latter two make it
|
// (source, readImportFiles, detectJavaScriptImports) — the latter two make it
|
||||||
// report `require(…)` and dynamic imports as well as static ones.
|
// report `require(…)` and dynamic imports as well as static ones.
|
||||||
const { importedFiles } = typescript().preProcessFile(source, true, true);
|
const { importedFiles, typeReferenceDirectives } =
|
||||||
|
typescript().preProcessFile(source, true, true);
|
||||||
for (const { fileName } of importedFiles) {
|
for (const { fileName } of importedFiles) {
|
||||||
const name = packageNameOf(fileName);
|
const name = packageNameOf(fileName);
|
||||||
if (name) names.add(name);
|
if (name) names.add(name);
|
||||||
}
|
}
|
||||||
|
for (const { fileName } of typeReferenceDirectives ?? [])
|
||||||
|
for (const name of typeReferencePackageNames(fileName)) names.add(name);
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
partitionSkew,
|
partitionSkew,
|
||||||
sourcesWithNoFiles,
|
sourcesWithNoFiles,
|
||||||
topLevelLockVersions,
|
topLevelLockVersions,
|
||||||
|
typeReferencePackageNames,
|
||||||
} from "./verify-dep-lockstep.mjs";
|
} from "./verify-dep-lockstep.mjs";
|
||||||
|
|
||||||
test("isSharedSourceFile: all four TS extensions, not just .ts/.tsx", () => {
|
test("isSharedSourceFile: all four TS extensions, not just .ts/.tsx", () => {
|
||||||
|
|
@ -196,6 +197,41 @@ test("importedPackageNames: static, side-effect, and dynamic forms; builtins and
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("importedPackageNames: triple-slash type references count (Copilot, #1962)", () => {
|
||||||
|
// A `/// <reference types="x" />` pulls in declarations exactly like an
|
||||||
|
// import, but TypeScript reports it in `typeReferenceDirectives`, not
|
||||||
|
// `importedFiles` — so reading only the latter let a referenced package skew
|
||||||
|
// unseen. `path` references name a file, not a package, and are ignored.
|
||||||
|
const source = [
|
||||||
|
'/// <reference types="node" />',
|
||||||
|
'/// <reference types="express" />',
|
||||||
|
'/// <reference path="./local.d.ts" />',
|
||||||
|
'import { z } from "zod";',
|
||||||
|
].join("\n");
|
||||||
|
assert.deepEqual([...importedPackageNames(source)].sort(), [
|
||||||
|
"@types/express",
|
||||||
|
"@types/node",
|
||||||
|
"express",
|
||||||
|
"node",
|
||||||
|
"zod",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("typeReferencePackageNames: both the bare and the @types form (Copilot, #1962)", () => {
|
||||||
|
// The directive names a *type*, not a package: `node` resolves to
|
||||||
|
// `@types/node`, while a package shipping its own declarations resolves to
|
||||||
|
// itself. Returning both over-approximates, the safe direction — whichever
|
||||||
|
// isn't installed drops out downstream.
|
||||||
|
assert.deepEqual(typeReferencePackageNames("node"), ["node", "@types/node"]);
|
||||||
|
// Scoped names mangle with a double underscore, TypeScript's convention.
|
||||||
|
assert.deepEqual(typeReferencePackageNames("@scope/pkg"), [
|
||||||
|
"@scope/pkg",
|
||||||
|
"@types/scope__pkg",
|
||||||
|
]);
|
||||||
|
assert.deepEqual(typeReferencePackageNames("./relative"), []);
|
||||||
|
assert.deepEqual(typeReferencePackageNames(""), []);
|
||||||
|
});
|
||||||
|
|
||||||
test("importedPackageNames: prose in comments never becomes a package (Copilot, #1962)", () => {
|
test("importedPackageNames: prose in comments never becomes a package (Copilot, #1962)", () => {
|
||||||
// The regex scan this replaced could not tell code from a comment, so
|
// The regex scan this replaced could not tell code from a comment, so
|
||||||
// `// adapted from "react"` added `react` to the candidate set — and if that
|
// `// adapted from "react"` added `react` to the candidate set — and if that
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue