mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Check only the files whose diagnostics the tsc gate keeps
The tsc gate (preflight's errorsOf, and the original program of a JSX or declaration-projection build) asked for every file's semantic diagnostics, then dropped all of those in an opted-in package's files, in unselected package JavaScript and in package shims. With --npm-static that is nearly the whole check: on three.js's nodes fixture the 82 package files took 1.17 s and reported 13,022 diagnostics, the program's own files 0.1 s. getPreEmitDiagnostics takes the files whose diagnostics its caller keeps. When some file is left out, the server checks only the kept files (getDiagnosticsOfCheckedFiles), and their semantic diagnostics come in the full check's order. When a stored answer for the supplied declaration files applies to the program, it stands in for checking those files, as in the full check; the storage rule is unchanged. A global diagnostic can come from checking any file, so when the partial check reports one, the full check answers as before. At load time, before workspace packages are known, the gate leaves out only package and shim files. The kept diagnostics are unchanged.
This commit is contained in:
@@ -665,6 +665,14 @@ function createRequireProgramRoots7(program: ts.Program): string[] {
|
||||
return [...roots].sort();
|
||||
}
|
||||
|
||||
/** Whether the tsc gate can keep a file's diagnostics when the program
|
||||
* loads: it drops all of an opted-in package's files and of the package
|
||||
* shims (keptErrors in preflight also drops unselected package JavaScript,
|
||||
* which is not known yet here, so those files are still checked). */
|
||||
function tscGateFileKept(fileName: string): boolean {
|
||||
return npmStaticPackageOfPath(fileName) === null && !isPackageShimFile(fileName);
|
||||
}
|
||||
|
||||
/** Classify the explicitly selected installed package's own sources. */
|
||||
export function entryPackageFilePredicate(entryPath: string): (file: string) => boolean {
|
||||
const entryFile = tsgoPath(entryPath);
|
||||
@@ -934,7 +942,7 @@ function loadProgram7(
|
||||
const original = program;
|
||||
// The project world must read the original text too, so it is built
|
||||
// before the rewritten files are served, and only when it is needed.
|
||||
const diagnostics = ts.getPreEmitDiagnostics(original);
|
||||
const diagnostics = ts.getPreEmitDiagnostics(original, tscGateFileKept);
|
||||
const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error);
|
||||
tscWorld = {
|
||||
program: original,
|
||||
@@ -982,7 +990,7 @@ function loadProgram7(
|
||||
} else {
|
||||
tscWorld = {
|
||||
program: original,
|
||||
diagnostics: ts.getPreEmitDiagnostics(original),
|
||||
diagnostics: ts.getPreEmitDiagnostics(original, tscGateFileKept),
|
||||
projectWorld: null,
|
||||
declWorldFallback: true,
|
||||
};
|
||||
@@ -5438,7 +5446,14 @@ function preflight7(load: LoadResult): {
|
||||
const sf = p.getSourceFile(d.fileName);
|
||||
return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs);
|
||||
};
|
||||
const errorsOf = (p: ts.Program): ts.Diagnostic[] => keptErrors(p, ts.getPreEmitDiagnostics(p));
|
||||
// keptErrors drops every diagnostic of these files: the gate does not ask
|
||||
// the checker for them.
|
||||
const diagnosticsKept = (fileName: string): boolean =>
|
||||
npmStaticPackageOfPath(fileName) === null &&
|
||||
!unselectedPackageJsFile(fileName) &&
|
||||
!isPackageShimFile(fileName);
|
||||
const errorsOf = (p: ts.Program): ts.Diagnostic[] =>
|
||||
keptErrors(p, ts.getPreEmitDiagnostics(p, diagnosticsKept));
|
||||
const keptErrors = (p: ts.Program, all: readonly ts.Diagnostic[]): ts.Diagnostic[] => {
|
||||
// First pass: every comment-side 2300's (file, name) — the partners
|
||||
// the second pass forgives.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
||||
import { afterAll, afterEach, expect, test } from "vitest";
|
||||
import type { SourceFile } from "./ast-types.js";
|
||||
import {
|
||||
reusedKeptLibDiagnostics,
|
||||
reusedLibDiagnostics,
|
||||
setLibDiagnosticsStore,
|
||||
type LibDiagnosticsStore,
|
||||
@@ -118,6 +119,45 @@ test("a stored answer stands in for checking the supplied files, merged by file
|
||||
expect(full?.semantic).toEqual(reused?.semantic);
|
||||
});
|
||||
|
||||
test("with kept files, a stored answer needs checking only the kept other files", () => {
|
||||
const store = memoryStore();
|
||||
setLibDiagnosticsStore(store, "checker");
|
||||
const pkg = join(dir, "node_modules", "pkg", "index.d.ts");
|
||||
const semantic = new Map([
|
||||
[lib, [diagnostic(lib, 1)]],
|
||||
[fallback, []],
|
||||
[main, [diagnostic(main, 5)]],
|
||||
[pkg, [diagnostic(pkg, 3)]],
|
||||
]);
|
||||
const withPackage = files({
|
||||
[main]: "export const n: number = 'x';\n",
|
||||
[pkg]: "export declare const p: number;\n",
|
||||
});
|
||||
const all = [lib, fallback, main, pkg];
|
||||
const keep = new Set([lib, fallback, main]);
|
||||
// Without a stored answer nothing is checked or stored.
|
||||
const cold = sessionProgram({ semantic });
|
||||
expect(reusedKeptLibDiagnostics(cold, all, supplied, withPackage, keep)).toBeNull();
|
||||
expect(cold.asked).toEqual([]);
|
||||
expect(store.entries.size).toBe(0);
|
||||
reusedLibDiagnostics(sessionProgram({ semantic }), all, supplied, withPackage);
|
||||
expect(store.entries.size).toBe(1);
|
||||
const warm = sessionProgram({ semantic });
|
||||
expect(reusedKeptLibDiagnostics(warm, all, supplied, withPackage, keep)).toEqual({
|
||||
semantic: [diagnostic(lib, 1), diagnostic(main, 5)],
|
||||
global: [],
|
||||
});
|
||||
expect(warm.asked).toEqual([`checked ${main}`]);
|
||||
// A left-out file that can declare something global still prevents reuse.
|
||||
const augmenting = files({
|
||||
[main]: "export const n: number = 'x';\n",
|
||||
[pkg]: "export {};\ndeclare global {\n var NaN: number;\n}\n",
|
||||
});
|
||||
const blocked = sessionProgram({ semantic });
|
||||
expect(reusedKeptLibDiagnostics(blocked, all, supplied, augmenting, keep)).toBeNull();
|
||||
expect(blocked.asked).toEqual([]);
|
||||
});
|
||||
|
||||
test("files merge in code point order, which differs from UTF-16 order", () => {
|
||||
const store = memoryStore();
|
||||
setLibDiagnosticsStore(store, "checker");
|
||||
|
||||
@@ -64,6 +64,31 @@ export function reusedLibDiagnostics(
|
||||
fileNames: readonly string[],
|
||||
supplied: (fileName: string) => boolean,
|
||||
sourceFile: (fileName: string) => SourceFile | undefined,
|
||||
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
|
||||
return reuse(program, fileNames, supplied, sourceFile, () => true, false);
|
||||
}
|
||||
|
||||
/** reusedLibDiagnostics for a caller that keeps only the diagnostics of the
|
||||
* `kept` files: a stored answer then needs checking only the kept other
|
||||
* files, and without a stored answer the result is null (nothing is checked
|
||||
* or stored). */
|
||||
export function reusedKeptLibDiagnostics(
|
||||
program: Ts7SessionProgram,
|
||||
fileNames: readonly string[],
|
||||
supplied: (fileName: string) => boolean,
|
||||
sourceFile: (fileName: string) => SourceFile | undefined,
|
||||
kept: ReadonlySet<string>,
|
||||
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
|
||||
return reuse(program, fileNames, supplied, sourceFile, (name) => kept.has(name), true);
|
||||
}
|
||||
|
||||
function reuse(
|
||||
program: Ts7SessionProgram,
|
||||
fileNames: readonly string[],
|
||||
supplied: (fileName: string) => boolean,
|
||||
sourceFile: (fileName: string) => SourceFile | undefined,
|
||||
keep: (fileName: string) => boolean,
|
||||
storedOnly: boolean,
|
||||
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
|
||||
if (configured === null || process.env["SCRIPTC_LIB_DIAGNOSTICS"] === "0") return null;
|
||||
const { store, checker } = configured;
|
||||
@@ -104,12 +129,16 @@ export function reusedLibDiagnostics(
|
||||
if (globals !== null) writeEntry(store, key, globals, Object.fromEntries(stored.files));
|
||||
}
|
||||
if (stored !== null && (scripts.length === 0 || (globals !== null && !collides(globals)))) {
|
||||
const checked = program.checkedFileDiagnosticData(own);
|
||||
const checkedNames = storedOnly ? own.filter(keep) : own;
|
||||
const checked = program.checkedFileDiagnosticData(checkedNames);
|
||||
if (checked === null) return null;
|
||||
const byFile = new Map<string, Ts7DiagnosticData[]>(stored.files);
|
||||
own.forEach((name, index) => byFile.set(name, checked.files[index] ?? []));
|
||||
const byFile = new Map<string, Ts7DiagnosticData[]>(
|
||||
storedOnly ? [...stored.files].filter(([name]) => keep(name)) : stored.files,
|
||||
);
|
||||
checkedNames.forEach((name, index) => byFile.set(name, checked.files[index] ?? []));
|
||||
return { semantic: inFileOrder(byFile), global: checked.global };
|
||||
}
|
||||
if (storedOnly) return null;
|
||||
// The full check, as before. Its answers for the supplied files are stored
|
||||
// when it reports no global diagnostics and the program's scripts declare
|
||||
// none of the supplied files' globals (so nothing of the program reached
|
||||
@@ -339,7 +368,7 @@ function readEntry(
|
||||
|
||||
/** Each file's diagnostics (already in the checker's order) in the order the
|
||||
* checker sorts files: by file name, compared by code point. */
|
||||
function inFileOrder(byFile: ReadonlyMap<string, Ts7DiagnosticData[]>): Ts7DiagnosticData[] {
|
||||
export function inFileOrder(byFile: ReadonlyMap<string, Ts7DiagnosticData[]>): Ts7DiagnosticData[] {
|
||||
const names = [...byFile.keys()].sort(compareCodePoints);
|
||||
const out: Ts7DiagnosticData[] = [];
|
||||
for (const name of names) for (const d of byFile.get(name)!) out.push(d);
|
||||
|
||||
@@ -3,7 +3,12 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { Ts7Api } from "./rpc-api.js";
|
||||
import { Ts7Host, type Ts7ApiOptions, type Ts7CompilerOptions } from "./program-host.js";
|
||||
import {
|
||||
getPreEmitDiagnostics,
|
||||
Ts7Host,
|
||||
type Ts7ApiOptions,
|
||||
type Ts7CompilerOptions,
|
||||
} from "./program-host.js";
|
||||
import { ModuleDetectionKind, ModuleKind, ModuleResolutionKind, ScriptTarget } from "./enums.js";
|
||||
import { tsgoPath } from "./session-path.js";
|
||||
|
||||
@@ -70,6 +75,54 @@ test("implementation discovery leaves declaration trees lazy without skipping th
|
||||
expect(() => program.getSourceFileNames()).toThrow("disposed");
|
||||
});
|
||||
|
||||
test("pre-emit diagnostics for kept files check only those, unless a global diagnostic appears", () => {
|
||||
const { directory, entry } = fixture();
|
||||
const other = join(directory, "other.ts");
|
||||
const later = join(directory, "later.ts");
|
||||
writeFileSync(
|
||||
entry,
|
||||
'import "./other.js";\nimport "./later.js";\nexport const n: number = "x";\n',
|
||||
);
|
||||
writeFileSync(other, "export const s: string = 1;\n");
|
||||
writeFileSync(later, "export const b: boolean = 0;\n");
|
||||
const { host } = makeHost(directory);
|
||||
const program = host.createProgram([entry], options);
|
||||
try {
|
||||
const full = getPreEmitDiagnostics(program);
|
||||
const keep = (name: string): boolean => name !== tsgoPath(other);
|
||||
const checked = vi.spyOn(program.project.program, "checkedFileDiagnosticData");
|
||||
const kept = getPreEmitDiagnostics(program, keep);
|
||||
// The full answer without the left-out file's diagnostics, in the same order.
|
||||
expect(full.some((diagnostic) => diagnostic.fileName === tsgoPath(other))).toBe(true);
|
||||
expect(kept.map((diagnostic) => [diagnostic.fileName, diagnostic.code])).toEqual(
|
||||
full
|
||||
.filter((diagnostic) => diagnostic.fileName === undefined || keep(diagnostic.fileName))
|
||||
.map((diagnostic) => [diagnostic.fileName, diagnostic.code]),
|
||||
);
|
||||
expect(kept).toHaveLength(2);
|
||||
expect(checked).toHaveBeenCalledTimes(1);
|
||||
expect(checked.mock.calls[0]![0]).not.toContain(tsgoPath(other));
|
||||
// Keeping every file is the full check.
|
||||
expect(getPreEmitDiagnostics(program, () => true)).toEqual(full);
|
||||
expect(checked).toHaveBeenCalledTimes(1);
|
||||
// A global diagnostic from the partial check, or a server without it: the full check.
|
||||
for (const answer of [
|
||||
{
|
||||
files: [],
|
||||
global: [{ fileName: undefined, pos: 0, end: 0, code: 2318, category: 1, text: "global" }],
|
||||
},
|
||||
null,
|
||||
]) {
|
||||
checked.mockReturnValueOnce(
|
||||
answer as ReturnType<typeof program.project.program.checkedFileDiagnosticData>,
|
||||
);
|
||||
expect(getPreEmitDiagnostics(program, keep)).toEqual(full);
|
||||
}
|
||||
} finally {
|
||||
program.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test("the factory receives one connection with the host's filesystem and timing", () => {
|
||||
const { directory, entry } = fixture();
|
||||
const factory = vi.fn((connection: Ts7ApiOptions) => new Ts7Api(connection));
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
import { packageShimDeclarationText } from "../package-shims.js";
|
||||
import { typePatchedText } from "../type-patches/layer.js";
|
||||
import { ambientDtsPath, fallbackDtsPath, overridesDtsPath } from "../dts-paths.js";
|
||||
import { reusedLibDiagnostics } from "./lib-diagnostics.js";
|
||||
import { inFileOrder, reusedKeptLibDiagnostics, reusedLibDiagnostics } from "./lib-diagnostics.js";
|
||||
import { ts7Diagnostic } from "./diagnostic.js";
|
||||
import type { Ts7DiagnosticData } from "./session-schema.generated.js";
|
||||
|
||||
@@ -423,17 +423,31 @@ export class Ts7Program {
|
||||
/** The semantic and global diagnostics with the stored diagnostics of the
|
||||
* declaration files scriptc supplies (lib-diagnostics.ts), or null. */
|
||||
reusedLibDiagnostics(): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
|
||||
return this.libDiagnostics(null);
|
||||
}
|
||||
|
||||
/** reusedLibDiagnostics for the `kept` files only: null without a stored
|
||||
* answer. */
|
||||
reusedKeptLibDiagnostics(
|
||||
kept: ReadonlySet<string>,
|
||||
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
|
||||
return this.libDiagnostics(kept);
|
||||
}
|
||||
|
||||
private libDiagnostics(
|
||||
kept: ReadonlySet<string> | null,
|
||||
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
|
||||
this.snapshot.ensureActive();
|
||||
const program = this.project.program;
|
||||
const declarations = new Set([ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]);
|
||||
return reusedLibDiagnostics(
|
||||
program,
|
||||
this.getSourceFileNames(),
|
||||
(name) =>
|
||||
declarations.has(tsgoPath(name)) ||
|
||||
program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true,
|
||||
(name) => this.getSourceFile(name),
|
||||
);
|
||||
const names = this.getSourceFileNames();
|
||||
const supplied = (name: string): boolean =>
|
||||
declarations.has(tsgoPath(name)) ||
|
||||
program.getSourceFileMetadataByPath(name)?.isDefaultLibrary === true;
|
||||
const sourceFile = (name: string): SourceFile | undefined => this.getSourceFile(name);
|
||||
return kept === null
|
||||
? reusedLibDiagnostics(program, names, supplied, sourceFile)
|
||||
: reusedKeptLibDiagnostics(program, names, supplied, sourceFile, kept);
|
||||
}
|
||||
|
||||
isSourceFileDefaultLibrary(file: SourceFile): boolean {
|
||||
@@ -464,10 +478,35 @@ export class Ts7Program {
|
||||
|
||||
/** 5.9.3's getPreEmitDiagnostics, composed from 7.0.2's split surface (the
|
||||
* package dropped the aggregate). Order mirrors 5.9.3: config, options/
|
||||
* program, global, syntactic, semantic. */
|
||||
export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[] {
|
||||
* program, global, syntactic, semantic.
|
||||
*
|
||||
* With `keep`, a caller that discards every diagnostic of the other files
|
||||
* asks for semantic diagnostics of the kept files only: the server checks
|
||||
* only those (getDiagnosticsOfCheckedFiles), or only those not answered by
|
||||
* stored diagnostics of the supplied declaration files, and their
|
||||
* diagnostics come in the full check's order. A global diagnostic can come
|
||||
* from checking any file, so when that partial check reports one, the full
|
||||
* check answers. */
|
||||
export function getPreEmitDiagnostics(
|
||||
program: Ts7Program,
|
||||
keep?: (fileName: string) => boolean,
|
||||
): readonly Diagnostic[] {
|
||||
const config = program.getConfigFileParsingDiagnostics();
|
||||
const options = program.getProgramDiagnostics();
|
||||
if (keep !== undefined) {
|
||||
const kept = program.getSourceFileNames().filter((name) => keep(name));
|
||||
if (kept.length < program.getSourceFileNames().length) {
|
||||
const partial =
|
||||
program.reusedKeptLibDiagnostics(new Set(kept)) ?? keptFileDiagnostics(program, kept);
|
||||
if (partial !== null && partial.global.length === 0)
|
||||
return [
|
||||
...config,
|
||||
...options,
|
||||
...program.getSyntacticDiagnostics(),
|
||||
...partial.semantic.map(ts7Diagnostic),
|
||||
];
|
||||
}
|
||||
}
|
||||
const reused = program.reusedLibDiagnostics();
|
||||
if (reused !== null)
|
||||
return [
|
||||
@@ -486,6 +525,19 @@ export function getPreEmitDiagnostics(program: Ts7Program): readonly Diagnostic[
|
||||
];
|
||||
}
|
||||
|
||||
/** The semantic diagnostics of the kept files, in the full check's order,
|
||||
* and the global diagnostics of checking them; null when the server cannot
|
||||
* check only some files. */
|
||||
function keptFileDiagnostics(
|
||||
program: Ts7Program,
|
||||
kept: readonly string[],
|
||||
): { semantic: Ts7DiagnosticData[]; global: Ts7DiagnosticData[] } | null {
|
||||
const checked = program.project.program.checkedFileDiagnosticData(kept);
|
||||
if (checked === null) return null;
|
||||
const byFile = new Map(kept.map((name, index) => [name, checked.files[index] ?? []]));
|
||||
return { semantic: inFileOrder(byFile), global: checked.global };
|
||||
}
|
||||
|
||||
/** ts.findConfigFile: nearest configName walking up from searchPath. */
|
||||
export function findConfigFile(
|
||||
searchPath: string,
|
||||
|
||||
Reference in New Issue
Block a user