Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/common/src/compiler/DocumentRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { errors } from "../errors";
import { StandardizedFilePath, TransactionalFileSystem } from "../fileSystem";
import { ts } from "../typescript";
import { getSetExternalModuleIndicator, ts } from "../typescript";
import { createCompilerSourceFile } from "./createCompilerSourceFile";

/**
Expand Down Expand Up @@ -138,7 +138,11 @@ export class DocumentRegistry implements ts.DocumentRegistry {
version: string,
scriptKind: ts.ScriptKind | undefined,
): ts.SourceFile {
const newSourceFile = createCompilerSourceFile(fileName, scriptSnapshot, compilationSettings.target, version, true, scriptKind);
const sourceFileOptions: ts.CreateSourceFileOptions = {
languageVersion: compilationSettings.target ?? ts.ScriptTarget.Latest,
setExternalModuleIndicator: getSetExternalModuleIndicator(compilationSettings),
};
const newSourceFile = createCompilerSourceFile(fileName, scriptSnapshot, sourceFileOptions, version, true, scriptKind);
this.#sourceFileCacheByFilePath.set(fileName, newSourceFile);
return newSourceFile;
}
Expand Down
80 changes: 80 additions & 0 deletions packages/common/src/tests/compiler/documentRegistryTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect } from "chai";
import { DocumentRegistry } from "../../compiler";
import { InMemoryFileSystemHost, TransactionalFileSystem } from "../../fileSystem";
import { ts } from "../../typescript";

describe("DocumentRegistry", () => {
function setup() {
const fileSystem = new TransactionalFileSystem({
fileSystem: new InMemoryFileSystemHost(),
libFolderPath: undefined,
skipLoadingLibFiles: false,
});
return { fileSystem, registry: new DocumentRegistry(fileSystem), nativeRegistry: ts.createDocumentRegistry() };
}

const testCases = [
{ name: "forced module", fileName: "/file.ts", text: "const value = 1;", mode: ts.ModuleDetectionKind.Force, expectedModule: true },
{ name: "declaration-file global", fileName: "/file.d.ts", text: "interface Global {}", mode: ts.ModuleDetectionKind.Force, expectedModule: false },
{ name: "legacy script", fileName: "/file.ts", text: "const value = 1;", mode: ts.ModuleDetectionKind.Legacy, expectedModule: false },
{ name: "explicit legacy module", fileName: "/file.ts", text: "export const value = 1;", mode: ts.ModuleDetectionKind.Legacy, expectedModule: true },
];
for (const testCase of testCases) {
it(`should classify a ${testCase.name} like the native registry and preserve the default target`, () => {
const { fileSystem, registry, nativeRegistry } = setup();
const fileName = fileSystem.getStandardizedAbsolutePath(testCase.fileName);
const options = { moduleDetection: testCase.mode };
const snapshot = ts.ScriptSnapshot.fromString(testCase.text);
const actual = registry.createOrUpdateSourceFile(fileName, options, snapshot, ts.ScriptKind.TS);
const expected = nativeRegistry.acquireDocument(fileName, options, snapshot, "0", ts.ScriptKind.TS);

expect(ts.isExternalModule(expected)).to.equal(testCase.expectedModule);
expect(ts.isExternalModule(actual)).to.equal(ts.isExternalModule(expected));
expect(actual.languageVersion).to.equal(ts.ScriptTarget.Latest);
expect(registry.getSourceFileVersion(actual)).to.equal("0");
});
}

it("should preserve an explicit target and script kind when creating a forced module", () => {
const { fileSystem, registry } = setup();
const fileName = fileSystem.getStandardizedAbsolutePath("/file.txt");
const options = { target: ts.ScriptTarget.ES2015, moduleDetection: ts.ModuleDetectionKind.Force };
const snapshot = ts.ScriptSnapshot.fromString("const element = <div />;");
const sourceFile = registry.createOrUpdateSourceFile(fileName, options, snapshot, ts.ScriptKind.TSX);
const statement = sourceFile.statements[0] as ts.VariableStatement;

expect(ts.isExternalModule(sourceFile)).to.equal(true);
expect(sourceFile.languageVersion).to.equal(ts.ScriptTarget.ES2015);
expect(statement.declarationList.declarations[0].initializer!.kind).to.equal(ts.SyntaxKind.JsxSelfClosingElement);
});

it("should apply changed compilation settings when updating an acquired document", () => {
const { registry, nativeRegistry } = setup();
const snapshot = ts.ScriptSnapshot.fromString("const value = 1;");
const legacy = { moduleDetection: ts.ModuleDetectionKind.Legacy };
registry.acquireDocument("/file.ts", legacy, snapshot, "0", ts.ScriptKind.TS);
nativeRegistry.acquireDocument("/file.ts", legacy, snapshot, "0", ts.ScriptKind.TS);
const force = { moduleDetection: ts.ModuleDetectionKind.Force };
const actual = registry.updateDocument("/file.ts", force, snapshot, "1", ts.ScriptKind.TS);
const expected = nativeRegistry.updateDocument("/file.ts", force, snapshot, "1", ts.ScriptKind.TS);

expect(ts.isExternalModule(expected)).to.equal(true);
expect(ts.isExternalModule(actual)).to.equal(ts.isExternalModule(expected));
expect(registry.getSourceFileVersion(actual)).to.equal("1");
});

it("should preserve forced classification through keyed acquisition and text updates", () => {
const { registry } = setup();
const options = { moduleDetection: ts.ModuleDetectionKind.Force };
const fileName = "/file.ts";
const filePath = fileName as ts.Path;
const key = registry.getKeyForCompilationSettings(options);
registry.acquireDocumentWithKey(fileName, filePath, options, key, ts.ScriptSnapshot.fromString("export const value = 1;"), "0", ts.ScriptKind.TS);
const text = "const value = 2;";
const actual = registry.updateDocumentWithKey(fileName, filePath, options, key, ts.ScriptSnapshot.fromString(text), "1", ts.ScriptKind.TS);

expect(ts.isExternalModule(actual)).to.equal(true);
expect(actual.text).to.equal(text);
expect(registry.getSourceFileVersion(actual)).to.equal("1");
});
});
5 changes: 5 additions & 0 deletions packages/common/src/typescript/tsInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,8 @@ export interface FileSystemEntries {
export function getEmitModuleResolutionKind(this: any, compilerOptions: ts.CompilerOptions) {
return (ts as any).getEmitModuleResolutionKind.apply(this, arguments);
}

/** @internal */
export function getSetExternalModuleIndicator(this: any, compilerOptions: ts.CompilerOptions): (file: ts.SourceFile) => void {
return (ts as any).getSetExternalModuleIndicator.apply(this, arguments);
}
98 changes: 98 additions & 0 deletions packages/ts-morph/src/tests/projectTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,104 @@ describe("Project", () => {
});
});

describe("module detection", () => {
const duplicateDeclarations = { "/a.ts": "const value = 1;", "/b.ts": "const value = 2;" };

function setup(files: Record<string, string>, mode: ts.ModuleDetectionKind) {
const options = { module: ts.ModuleKind.ESNext, moduleDetection: mode, types: [], skipLibCheck: true };
const project = new Project({ useInMemoryFileSystem: true, compilerOptions: options });
for (const [fileName, text] of Object.entries(files))
project.createSourceFile(fileName, text);
return { project, options };
}

function getNativeDiagnosticCodes(files: Record<string, string>, options: ts.CompilerOptions) {
const libFolder = "/node_modules/typescript/lib/";
const texts = new Map<string, string>([
...getLibFiles().map(file => [libFolder + file.fileName, file.text] as const),
...Object.entries(files),
]);
const host = ts.createCompilerHost(options);
host.getCurrentDirectory = () => "/";
host.getDefaultLibFileName = () => libFolder + ts.getDefaultLibFileName(options);
host.getDefaultLibLocation = () => libFolder;
host.fileExists = fileName => texts.has(fileName);
host.readFile = fileName => texts.get(fileName);
host.getSourceFile = (fileName, languageVersionOrOptions) => {
const text = texts.get(fileName);
return text == null ? undefined : ts.createSourceFile(fileName, text, languageVersionOrOptions, true);
};
const program = ts.createProgram(Object.keys(files), options, host);
return ts.getPreEmitDiagnostics(program).map(d => d.code);
}

for (const mode of [ts.ModuleDetectionKind.Force, ts.ModuleDetectionKind.Legacy]) {
it(`should report duplicate declarations like native TypeScript in ${ts.ModuleDetectionKind[mode]} mode`, () => {
const { project, options } = setup(duplicateDeclarations, mode);
const expected = getNativeDiagnosticCodes(duplicateDeclarations, options);

expect(expected).to.deep.equal(mode === ts.ModuleDetectionKind.Force ? [] : [2451, 2451]);
expect(project.getPreEmitDiagnostics().map(d => d.getCode())).to.deep.equal(expected);
});
}

it("should diagnose an unimported type from another forced module like native TypeScript", () => {
const files = { "/a.ts": "interface Shared { value: number }", "/b.ts": "const item: Shared = { value: 1 };" };
const { project, options } = setup(files, ts.ModuleDetectionKind.Force);
const expected = getNativeDiagnosticCodes(files, options);

expect(expected).to.deep.equal([2304]);
expect(project.getPreEmitDiagnostics().map(d => d.getCode())).to.deep.equal(expected);
});

it("should preserve declaration-file globals in Force mode", () => {
const files = { "/global.d.ts": "interface Global { value: number }", "/a.ts": "const item: Global = { value: 1 };" };
const { project, options } = setup(files, ts.ModuleDetectionKind.Force);
const expected = getNativeDiagnosticCodes(files, options);

expect(expected).to.deep.equal([]);
expect(project.getPreEmitDiagnostics().map(d => d.getCode())).to.deep.equal(expected);
expect(ts.isExternalModule(project.getSourceFileOrThrow("global.d.ts").compilerNode)).to.equal(false);
});

it("should preserve forced module isolation after manipulating an already checked source file", () => {
const { project, options } = setup(duplicateDeclarations, ts.ModuleDetectionKind.Force);
project.getPreEmitDiagnostics(); // warm the program before reparsing
const sourceFile = project.getSourceFileOrThrow("a.ts");
const declaration = sourceFile.getVariableDeclarationOrThrow("value");
declaration.setInitializer("3");
const files = { ...duplicateDeclarations, "/a.ts": sourceFile.getFullText() };

expect(getNativeDiagnosticCodes(files, options)).to.deep.equal([]);
expect(project.getPreEmitDiagnostics().map(d => d.getCode())).to.deep.equal([]);
expect(ts.isExternalModule(sourceFile.compilerNode)).to.equal(true);
expect(declaration).to.equal(sourceFile.getVariableDeclarationOrThrow("value"));
});

const transitions = [
[ts.ModuleDetectionKind.Legacy, ts.ModuleDetectionKind.Force],
[ts.ModuleDetectionKind.Force, ts.ModuleDetectionKind.Legacy],
];
for (const [from, to] of transitions) {
it(`should reclassify warmed source files when changing from ${ts.ModuleDetectionKind[from]} to ${ts.ModuleDetectionKind[to]}`, () => {
const { project, options } = setup(duplicateDeclarations, from);
project.getPreEmitDiagnostics();
project.getLanguageService().compilerObject.getSemanticDiagnostics("/a.ts");
const sourceFile = project.getSourceFileOrThrow("a.ts");
const declaration = sourceFile.getVariableDeclarationOrThrow("value");
project.compilerOptions.set({ moduleDetection: to });
const expected = getNativeDiagnosticCodes(duplicateDeclarations, { ...options, moduleDetection: to });

expect(expected).to.deep.equal(to === ts.ModuleDetectionKind.Force ? [] : [2451, 2451]);
expect(project.getPreEmitDiagnostics().map(d => d.getCode())).to.deep.equal(expected);
expect(ts.isExternalModule(sourceFile.compilerNode)).to.equal(to === ts.ModuleDetectionKind.Force);
expect(project.getLanguageService().compilerObject.getSemanticDiagnostics("/a.ts").map(d => d.code))
.to.deep.equal(to === ts.ModuleDetectionKind.Force ? [] : [2451]);
expect(declaration).to.equal(sourceFile.getVariableDeclarationOrThrow("value"));
});
}
});

describe(nameof<Project>("resolveSourceFileDependencies"), () => {
it("should resolve file dependencies once specified", () => {
const { project, initialFiles, resolvedFiles } = fileDependencyResolutionSetup({ skipFileDependencyResolution: true });
Expand Down