Files
vscode/build/lib/test/layersChecker.test.ts
T
dde569a9f6 Speed up valid-layers-check by ~11x (#328087)
The check took ~257s and would sometimes run out of memory. Two separate
bottlenecks were responsible.

layersChecker.ts built a full in-process ts.Program (15s, 2GB) and then
asked the type checker for a symbol at every property access in the
program, ~2.5M identifiers. Each lookup materialized symbol and type
objects, pushing the heap past 3.6GB, and the alias/containment chain was
then walked per symbol.

It now runs on the TS7 native API that the repo already vendors for
build/lib/tsgo.ts:

- updateSnapshot replaces createProgram, so the program lives in the
  compiler rather than the V8 heap
- the disallowed types are resolved to symbols once up front, turning the
  per-reference check into a local id lookup
- symbol lookups are batched instead of issued one node at a time
- property accesses are prefiltered by member name, so only those that
  could name a member of a disallowed type are resolved at all

That brings it from 48.6s and 3.8GB peak to 11.7s and 0.8GB peak.

The six tsconfig.<layer>.json projects were type checked serially with
TS6, which accounted for the remaining ~208s. They move to the native
compiler and run in parallel in the new layersTypeCheck.ts, taking ~11s.
Concurrency is derived from free memory (the largest project peaks at
~3.5GB) so a memory-constrained machine falls back towards serial
execution rather than swapping.

Verified to report byte-identical violations, including line and column,
against a seeded set covering direct references, import aliases,
inherited members, nested member access and ipcMain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-29 20:24:47 +00:00

108 lines
3.8 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { afterEach, beforeEach, suite, test } from 'node:test';
import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { dirname, join } from 'path';
import { checkLayers, getRule, type ILayerViolation, type IRule } from '../../checker/layersChecker.ts';
suite('layersChecker', () => {
let rootPath: string;
let tsconfigPath: string;
const rules: IRule[] = [
{ target: '**/test/**', skip: true },
{ target: '**/browser/**', disallowedTypes: ['ForbiddenService'] },
];
beforeEach(() => {
rootPath = realpathSync(mkdtempSync(join(tmpdir(), '.layers-checker-')));
tsconfigPath = join(rootPath, 'tsconfig.json');
writeFileSync(tsconfigPath, JSON.stringify({
compilerOptions: {
module: 'nodenext',
moduleResolution: 'nodenext',
noEmit: true,
skipLibCheck: true,
},
include: ['**/*.ts'],
}));
});
afterEach(() => {
rmSync(rootPath, { recursive: true, force: true });
});
test('matches rules relative to a hidden parent path', () => {
const fileName = join(rootPath, 'vs', 'feature', 'browser', 'feature.ts');
assert.strictEqual(getRule(fileName, rootPath, rules), rules[1]);
});
test('ignores skipped files', async () => {
writeForbiddenService();
writeSource('vs/feature/browser/test/feature.test.ts', `
import { ForbiddenService } from '../../../platform/common/service.js';
export const service: ForbiddenService | undefined = undefined;
`);
assert.deepStrictEqual(await getViolations(), []);
});
test('detects aliased forbidden types', async () => {
writeForbiddenService();
writeSource('vs/feature/browser/feature.ts', `
import { ForbiddenService as Service } from '../../platform/common/service.js';
export const service: Service | undefined = undefined;
`);
assert.deepStrictEqual(await getViolations(), [
{ type: 'ForbiddenService', fileName: 'vs/feature/browser/feature.ts', line: 2, character: 13 },
{ type: 'ForbiddenService', fileName: 'vs/feature/browser/feature.ts', line: 2, character: 33 },
{ type: 'ForbiddenService', fileName: 'vs/feature/browser/feature.ts', line: 3, character: 26 },
]);
});
test('detects members of inferred forbidden types', async () => {
writeForbiddenService();
writeSource('vs/feature/browser/feature.ts', `
import { getService } from '../../platform/common/service.js';
getService().run();
`);
assert.deepStrictEqual(await getViolations(), [
{ type: 'ForbiddenService', fileName: 'vs/feature/browser/feature.ts', line: 3, character: 17 },
]);
});
function writeForbiddenService(): void {
writeSource('vs/platform/common/service.ts', `
export interface ForbiddenService { run(): void }
export declare function getService(): ForbiddenService;
`);
}
function writeSource(relativePath: string, contents: string): string {
const fileName = join(rootPath, relativePath);
mkdirSync(dirname(fileName), { recursive: true });
writeFileSync(fileName, contents);
return fileName;
}
async function getViolations(): Promise<Pick<ILayerViolation, 'type' | 'fileName' | 'line' | 'character'>[]> {
const violations = await checkLayers(tsconfigPath, rules);
return violations
.map(violation => ({
type: violation.type,
fileName: violation.fileName.slice(rootPath.length + 1).replaceAll('\\', '/'),
line: violation.line,
character: violation.character,
}))
.sort((a, b) => a.line - b.line || a.character - b.character);
}
});