More scenarios in their own test

This commit is contained in:
Sheetal Nandi
2018-12-07 16:04:49 -08:00
parent 5c8ef3934d
commit 53e2507f60
66 changed files with 3308 additions and 3307 deletions

View File

@@ -58,7 +58,6 @@
"unittests/reuseProgramStructure.ts",
"unittests/semver.ts",
"unittests/transform.ts",
"unittests/transpile.ts",
"unittests/tsbuild.ts",
"unittests/tsbuildWatchMode.ts",
"unittests/tscWatchMode.ts",
@@ -89,25 +88,50 @@
"unittests/services/patternMatcher.ts",
"unittests/services/preProcessFile.ts",
"unittests/services/textChanges.ts",
"unittests/services/transpile.ts",
"unittests/tscWatch/emit.ts",
"unittests/tscWatch/resolutionCache.ts",
"unittests/tscWatch/watchEnvironment.ts",
"unittests/tscWatch/watchApi.ts",
"unittests/tsserver/cachingFileSystemInformation.ts",
"unittests/tsserver/cancellationToken.ts",
"unittests/tsserver/compileOnSave.ts",
"unittests/tsserver/completions.ts",
"unittests/tsserver/configFileSearch.ts",
"unittests/tsserver/declarationFileMaps.ts",
"unittests/tsserver/documentRegistry.ts",
"unittests/tsserver/duplicatePackages.ts",
"unittests/tsserver/events/largeFileReferenced.ts",
"unittests/tsserver/events/projectLoading.ts",
"unittests/tsserver/events/projectUpdatedInBackground.ts",
"unittests/tsserver/externalProjects.ts",
"unittests/tsserver/forceConsistentCasingInFileNames.ts",
"unittests/tsserver/formatSettings.ts",
"unittests/tsserver/getEditsForFileRename.ts",
"unittests/tsserver/importHelpers.ts",
"unittests/tsserver/inferredProjects.ts",
"unittests/tsserver/languageService.ts",
"unittests/tsserver/maxNodeModuleJsDepth.ts",
"unittests/tsserver/metadataInResponse.ts",
"unittests/tsserver/navTo.ts",
"unittests/tsserver/occurences.ts",
"unittests/tsserver/openFile.ts",
"unittests/tsserver/projectErrors.ts",
"unittests/tsserver/projectReferences.ts",
"unittests/tsserver/refactors.ts",
"unittests/tsserver/reload.ts",
"unittests/tsserver/rename.ts",
"unittests/tsserver/resolutionCache.ts",
"unittests/tsserver/session.ts",
"unittests/tsserver/skipLibCheck.ts",
"unittests/tsserver/symLinks.ts",
"unittests/tsserver/syntaxOperations.ts",
"unittests/tsserver/textStorage.ts",
"unittests/tsserver/telemetry.ts",
"unittests/tsserver/typeAquisition.ts",
"unittests/tsserver/typeReferenceDirectives.ts",
"unittests/tsserver/typingsInstaller.ts",
"unittests/tsserver/untitledFiles.ts",
"unittests/tsserver/versionCache.ts",
"unittests/tsserver/watchEnvironment.ts"
]

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("commandLineParsing:: parseCommandLine", () => {
describe("config:: commandLineParsing:: parseCommandLine", () => {
function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine) {
const parsed = parseCommandLine(commandLine);
@@ -367,7 +367,7 @@ namespace ts {
});
});
describe("commandLineParsing:: parseBuildOptions", () => {
describe("config:: commandLineParsing:: parseBuildOptions", () => {
function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) {
const parsed = parseBuildCommand(commandLine);
const parsedBuildOptions = JSON.stringify(parsed.buildOptions);

View File

@@ -208,7 +208,7 @@ namespace ts {
}
}
describe("configurationExtension", () => {
describe("config:: configurationExtension", () => {
forEach<[string, string, fakes.ParseConfigHost], void>([
["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost],
["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost]

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("convertCompilerOptionsFromJson", () => {
describe("config:: convertCompilerOptionsFromJson", () => {
const formatDiagnosticHost: FormatDiagnosticsHost = {
getCurrentDirectory: () => "/apath/",
getCanonicalFileName: createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ true),

View File

@@ -1,6 +1,6 @@
namespace ts {
interface ExpectedResult { typeAcquisition: TypeAcquisition; errors: Diagnostic[]; }
describe("convertTypeAcquisitionFromJson", () => {
describe("config:: convertTypeAcquisitionFromJson", () => {
function assertTypeAcquisition(json: any, configFileName: string, expectedResult: ExpectedResult) {
assertTypeAcquisitionWithJson(json, configFileName, expectedResult);
assertTypeAcquisitionWithJsonNode(json, configFileName, expectedResult);

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("initTSConfig", () => {
describe("config:: initTSConfig", () => {
function initTSConfigCorrectly(name: string, commandLinesArgs: string[]) {
describe(name, () => {
const commandLine = parseCommandLine(commandLinesArgs);
@@ -30,4 +30,4 @@ namespace ts {
initTSConfigCorrectly("Initialized TSConfig with advanced options", ["--init", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"]);
});
}
}

View File

@@ -143,7 +143,7 @@ namespace ts {
return createFileDiagnostic(file, start, length, diagnosticMessage, arg0);
}
describe("matchFiles", () => {
describe("config:: matchFiles", () => {
it("with defaults", () => {
const json = {};
const expected: ParsedCommandLine = {

View File

@@ -96,7 +96,7 @@ namespace ts {
checkResult(prog, host);
}
describe("project-references meta check", () => {
describe("config:: project-references meta check", () => {
it("default setup was created correctly", () => {
const spec: TestSpecification = {
"/primary": {
@@ -118,7 +118,7 @@ namespace ts {
/**
* Validate that we enforce the basic settings constraints for referenced projects
*/
describe("project-references constraint checking for settings", () => {
describe("config:: project-references constraint checking for settings", () => {
it("errors when declaration = false", () => {
const spec: TestSpecification = {
"/primary": {
@@ -248,7 +248,7 @@ namespace ts {
/**
* Path mapping behavior
*/
describe("project-references path mapping", () => {
describe("config:: project-references path mapping", () => {
it("redirects to the output .d.ts file", () => {
const spec: TestSpecification = {
"/alpha": {
@@ -268,7 +268,7 @@ namespace ts {
});
});
describe("project-references nice-behavior", () => {
describe("config:: project-references nice-behavior", () => {
it("issues a nice error when the input file is missing", () => {
const spec: TestSpecification = {
"/alpha": {
@@ -289,7 +289,7 @@ namespace ts {
/**
* 'composite' behavior
*/
describe("project-references behavior changes under composite: true", () => {
describe("config:: project-references behavior changes under composite: true", () => {
it("doesn't infer the rootDir from source paths", () => {
const spec: TestSpecification = {
"/alpha": {
@@ -308,7 +308,7 @@ namespace ts {
});
});
describe("project-references errors when a file in a composite project occurs outside the root", () => {
describe("config:: project-references errors when a file in a composite project occurs outside the root", () => {
it("Errors when a file is outside the rootdir", () => {
const spec: TestSpecification = {
"/alpha": {

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("showConfig", () => {
describe("config:: showConfig", () => {
function showTSConfigCorrectly(name: string, commandLinesArgs: string[], configJson?: object) {
describe(name, () => {
const outputFileName = `showConfig/${name.replace(/[^a-z0-9\-./ ]/ig, "")}/tsconfig.json`;

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("tsconfigParsing:: parseConfigFileTextToJson", () => {
describe("config:: tsconfigParsing:: parseConfigFileTextToJson", () => {
function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic[] }) {
const parsed = parseConfigFileTextToJson("/apath/tsconfig.json", jsonText);
assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject));

View File

@@ -1,4 +1,4 @@
describe("asyncArrowEvaluation", () => {
describe("evaluation:: asyncArrowEvaluation", () => {
// https://github.com/Microsoft/TypeScript/issues/24722
it("this capture (es5)", async () => {
const result = evaluator.evaluateTypeScript(`
@@ -15,4 +15,4 @@ describe("asyncArrowEvaluation", () => {
await result.main();
assert.instanceOf(result.output[0].a(), result.A);
});
});
});

View File

@@ -1,4 +1,4 @@
describe("asyncGeneratorEvaluation", () => {
describe("evaluation:: asyncGeneratorEvaluation", () => {
it("return (es5)", async () => {
const result = evaluator.evaluateTypeScript(`
async function * g() {
@@ -27,4 +27,4 @@ describe("asyncGeneratorEvaluation", () => {
{ value: 0, done: true }
]);
});
});
});

View File

@@ -1,4 +1,4 @@
describe("forAwaitOfEvaluation", () => {
describe("evaluation:: forAwaitOfEvaluation", () => {
it("sync (es5)", async () => {
const result = evaluator.evaluateTypeScript(`
let i = 0;

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("cancellableLanguageServiceOperations", () => {
describe("services:: cancellableLanguageServiceOperations", () => {
const file = `
function foo(): void;
function foo<T>(x: T): T;

View File

@@ -6,7 +6,7 @@ interface ClassificationEntry {
position?: number;
}
describe("Colorization", () => {
describe("services:: Colorization", () => {
// Use the shim adapter to ensure test coverage of the shim layer for the classifier
const languageServiceAdapter = new Harness.LanguageService.ShimLanguageServiceAdapter(/*preprocessToResolve*/ false);
const classifier = languageServiceAdapter.getClassifier();

View File

@@ -343,7 +343,7 @@ interface Array<T> {}`
}
}
describe("convertToAsyncFunctions", () => {
describe("services:: convertToAsyncFunctions", () => {
_testConvertToAsyncFunction("convertToAsyncFunction_basic", `
function [#|f|](): Promise<void>{
return fetch('https://typescriptlang.org').then(result => { console.log(result) });

View File

@@ -1,4 +1,4 @@
describe("DocumentRegistry", () => {
describe("services:: DocumentRegistry", () => {
it("documents are shared between projects", () => {
const documentRegistry = ts.createDocumentRegistry();
const defaultCompilerOptions = ts.getDefaultCompilerOptions();

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("extractConstants", () => {
describe("services:: extract:: extractConstants", () => {
testExtractConstant("extractConstant_TopLevel",
`let x = [#|1|];`);

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("extractFunctions", () => {
describe("services:: extract:: extractFunctions", () => {
testExtractFunction("extractFunction1",
`namespace A {
let x = 1;

View File

@@ -42,7 +42,7 @@ namespace ts {
}
}
describe("extractRanges", () => {
describe("services:: extract:: extractRanges", () => {
it("get extract range from selection", () => {
testExtractRange(`
[#|
@@ -418,4 +418,4 @@ switch (x) {
testExtractRangeFailed("extract-method-not-for-token-expression-statement", `[#|a|]`, [refactor.extractSymbol.Messages.cannotExtractIdentifier.message]);
});
}
}

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("Symbol Walker", () => {
describe("services:: extract:: Symbol Walker", () => {
function test(description: string, source: string, verifier: (file: SourceFile, checker: TypeChecker) => void) {
it(description, () => {
const result = Harness.Compiler.compileFiles([{

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("hostNewLineSupport", () => {
describe("services:: hostNewLineSupport", () => {
function testLSWithFiles(settings: CompilerOptions, files: Harness.Compiler.TestFile[]) {
function snapFor(path: string): IScriptSnapshot | undefined {
if (path === "lib.d.ts") {
@@ -46,4 +46,4 @@ namespace ts {
`);
});
});
}
}

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("languageService", () => {
describe("services:: languageService", () => {
const files: {[index: string]: string} = {
"foo.ts": `import Vue from "./vue";
import Component from "./vue-class-component";
@@ -43,4 +43,4 @@ export function Component(x: Config): any;`
expect(definitions).to.exist; // tslint:disable-line no-unused-expression
});
});
}
}

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("Organize imports", () => {
describe("services:: Organize imports", () => {
describe("Sort imports", () => {
it("Sort - non-relative vs non-relative", () => {
assertSortsBefore(

View File

@@ -1,4 +1,4 @@
describe("PatternMatcher", () => {
describe("services:: PatternMatcher", () => {
describe("BreakIntoCharacterSpans", () => {
it("EmptyIdentifier", () => {
verifyBreakIntoCharacterSpans("");

View File

@@ -1,4 +1,4 @@
describe("PreProcessFile:", () => {
describe("services:: PreProcessFile:", () => {
function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void {
const resultPreProcess = ts.preProcessFile(sourceText, readImportFile, detectJavaScriptImports);

View File

@@ -2,7 +2,7 @@
// tslint:disable trim-trailing-whitespace
namespace ts {
describe("textChanges", () => {
describe("services:: textChanges", () => {
function findChild(name: string, n: Node) {
return find(n)!;
@@ -753,4 +753,4 @@ let x = foo
});
}
});
}
}

View File

@@ -1,5 +1,5 @@
namespace ts {
describe("Transpile", () => {
describe("services:: Transpile", () => {
interface TranspileTestSettings {
options?: TranspileOptions;

View File

@@ -38,7 +38,7 @@ namespace ts.tscWatch {
checkOutputDoesNotContain(host, expectedNonAffectedFiles);
}
describe("tsc-watch emit with outFile or out setting", () => {
describe("tsc-watch:: emit with outFile or out setting", () => {
function createWatchForOut(out?: string, outFile?: string) {
const host = createWatchedSystem([]);
const config: FileOrFolderEmit = {
@@ -161,7 +161,7 @@ namespace ts.tscWatch {
});
});
describe("tsc-watch emit for configured projects", () => {
describe("tsc-watch:: emit for configured projects", () => {
const file1Consumer1Path = "/a/b/file1Consumer1.ts";
const moduleFile1Path = "/a/b/moduleFile1.ts";
const configFilePath = "/a/b/tsconfig.json";
@@ -495,7 +495,7 @@ namespace ts.tscWatch {
});
});
describe("tsc-watch emit file content", () => {
describe("tsc-watch:: emit file content", () => {
interface EmittedFile extends File {
shouldBeWritten: boolean;
}
@@ -676,7 +676,7 @@ namespace ts.tscWatch {
});
});
describe("tsc-watch with when module emit is specified as node", () => {
describe("tsc-watch:: emit with when module emit is specified as node", () => {
it("when instead of filechanged recursive directory watcher is invoked", () => {
const configFile: File = {
path: "/a/rootFolder/project/tsconfig.json",

View File

@@ -12,6 +12,15 @@ namespace ts.tscWatch {
export import checkOutputContains = TestFSWithWatch.checkOutputContains;
export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain;
export const commonFile1: File = {
path: "/a/b/commonFile1.ts",
content: "let x = 1"
};
export const commonFile2: File = {
path: "/a/b/commonFile2.ts",
content: "let y = 1"
};
export function checkProgramActualFiles(program: Program, expectedFiles: ReadonlyArray<string>) {
checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles);
}

View File

@@ -1,5 +1,5 @@
namespace ts.tscWatch {
describe("resolutionCache:: tsc-watch module resolution caching", () => {
describe("tsc-watch:: resolutionCache:: tsc-watch module resolution caching", () => {
it("works", () => {
const root = {
path: "/a/d/f0.ts",
@@ -404,7 +404,7 @@ declare module "fs" {
});
});
describe("resolutionCache:: tsc-watch with modules linked to sibling folder", () => {
describe("tsc-watch:: resolutionCache:: tsc-watch with modules linked to sibling folder", () => {
const projectRoot = "/user/username/projects/project";
const mainPackageRoot = `${projectRoot}/main`;
const linkedPackageRoot = `${projectRoot}/linked-package`;

View File

@@ -1,5 +1,5 @@
namespace ts.tscWatch {
describe("watchAPI:: tsc-watch with custom module resolution", () => {
describe("tsc-watch:: watchAPI:: tsc-watch with custom module resolution", () => {
const projectRoot = "/user/username/projects/project";
const configFileJson: any = {
compilerOptions: { module: "commonjs", resolveJsonModule: true },

View File

@@ -1,6 +1,6 @@
namespace ts.tscWatch {
import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory;
describe("watchEnvironment:: tsc-watch with different polling/non polling options", () => {
describe("tsc-watch:: watchEnvironment:: tsc-watch with different polling/non polling options", () => {
it("watchFile using dynamic priority polling", () => {
const projectFolder = "/a/username/project";
const file1: File = {

View File

@@ -25,15 +25,6 @@ namespace ts.tscWatch {
}
describe("tsc-watch program updates", () => {
const commonFile1: File = {
path: "/a/b/commonFile1.ts",
content: "let x = 1"
};
const commonFile2: File = {
path: "/a/b/commonFile2.ts",
content: "let y = 1"
};
it("create watch without config file", () => {
const appFile: File = {
path: "/a/b/c/app.ts",

View File

@@ -0,0 +1,271 @@
namespace ts.projectSystem {
describe("tsserver:: cancellationToken", () => {
// Disable sourcemap support for the duration of the test, as sourcemapping the errors generated during this test is slow and not something we care to test
let oldPrepare: AnyFunction;
before(() => {
oldPrepare = (Error as any).prepareStackTrace;
delete (Error as any).prepareStackTrace;
});
after(() => {
(Error as any).prepareStackTrace = oldPrepare;
});
it("is attached to request", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let xyz = 1;"
};
const host = createServerHost([f1]);
let expectedRequestId: number;
const cancellationToken: server.ServerCancellationToken = {
isCancellationRequested: () => false,
setRequest: requestId => {
if (expectedRequestId === undefined) {
assert.isTrue(false, "unexpected call");
}
assert.equal(requestId, expectedRequestId);
},
resetRequest: noop
};
const session = createSession(host, { cancellationToken });
expectedRequestId = session.getNextSeq();
session.executeCommandSeq(<server.protocol.OpenRequest>{
command: "open",
arguments: { file: f1.path }
});
expectedRequestId = session.getNextSeq();
session.executeCommandSeq(<server.protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
expectedRequestId = session.getNextSeq();
session.executeCommandSeq(<server.protocol.OccurrencesRequest>{
command: "occurrences",
arguments: { file: f1.path, line: 1, offset: 6 }
});
expectedRequestId = 2;
host.runQueuedImmediateCallbacks();
expectedRequestId = 2;
host.runQueuedImmediateCallbacks();
});
it("Geterr is cancellable", () => {
const f1 = {
path: "/a/app.ts",
content: "let x = 1"
};
const config = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: {}
})
};
const cancellationToken = new TestServerCancellationToken();
const host = createServerHost([f1, config]);
const session = createSession(host, {
canUseEvents: true,
eventHandler: noop,
cancellationToken
});
{
session.executeCommandSeq(<protocol.OpenRequest>{
command: "open",
arguments: { file: f1.path }
});
// send geterr for missing file
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: ["/a/missing"] }
});
// no files - expect 'completed' event
assert.equal(host.getOutput().length, 1, "expect 1 message");
verifyRequestCompleted(session.getSeq(), 0);
}
{
const getErrId = session.getNextSeq();
// send geterr for a valid file
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run new request
session.executeCommandSeq(<protocol.ProjectInfoRequest>{
command: "projectInfo",
arguments: { file: f1.path }
});
session.clearMessages();
// cancel previously issued Geterr
cancellationToken.setRequestToCancel(getErrId);
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
verifyRequestCompleted(getErrId, 0);
cancellationToken.resetToken();
}
{
const getErrId = session.getNextSeq();
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
session.clearMessages();
cancellationToken.setRequestToCancel(getErrId);
host.runQueuedImmediateCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
verifyRequestCompleted(getErrId, 0);
cancellationToken.resetToken();
}
{
const getErrId = session.getNextSeq();
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
session.clearMessages();
// the semanticDiag message
host.runQueuedImmediateCallbacks();
assert.equal(host.getOutput().length, 1);
const e2 = <protocol.Event>getMessage(0);
assert.equal(e2.event, "semanticDiag");
session.clearMessages();
host.runQueuedImmediateCallbacks(1);
assert.equal(host.getOutput().length, 2);
const e3 = <protocol.Event>getMessage(0);
assert.equal(e3.event, "suggestionDiag");
verifyRequestCompleted(getErrId, 1);
cancellationToken.resetToken();
}
{
const getErr1 = session.getNextSeq();
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
assert.equal(host.getOutput().length, 0, "expect 0 messages");
// run first step
host.runQueuedTimeoutCallbacks();
assert.equal(host.getOutput().length, 1, "expect 1 message");
const e1 = <protocol.Event>getMessage(0);
assert.equal(e1.event, "syntaxDiag");
session.clearMessages();
session.executeCommandSeq(<protocol.GeterrRequest>{
command: "geterr",
arguments: { files: [f1.path] }
});
// make sure that getErr1 is completed
verifyRequestCompleted(getErr1, 0);
}
function verifyRequestCompleted(expectedSeq: number, n: number) {
const event = <protocol.RequestCompletedEvent>getMessage(n);
assert.equal(event.event, "requestCompleted");
assert.equal(event.body.request_seq, expectedSeq, "expectedSeq");
session.clearMessages();
}
function getMessage(n: number) {
return JSON.parse(server.extractMessage(host.getOutput()[n]));
}
});
it("Lower priority tasks are cancellable", () => {
const f1 = {
path: "/a/app.ts",
content: `{ let x = 1; } var foo = "foo"; var bar = "bar"; var fooBar = "fooBar";`
};
const config = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: {}
})
};
const cancellationToken = new TestServerCancellationToken(/*cancelAfterRequest*/ 3);
const host = createServerHost([f1, config]);
const session = createSession(host, {
canUseEvents: true,
eventHandler: noop,
cancellationToken,
throttleWaitMilliseconds: 0
});
{
session.executeCommandSeq(<protocol.OpenRequest>{
command: "open",
arguments: { file: f1.path }
});
// send navbar request (normal priority)
session.executeCommandSeq(<protocol.NavBarRequest>{
command: "navbar",
arguments: { file: f1.path }
});
// ensure the nav bar request can be canceled
verifyExecuteCommandSeqIsCancellable(<protocol.NavBarRequest>{
command: "navbar",
arguments: { file: f1.path }
});
// send outlining spans request (normal priority)
session.executeCommandSeq(<protocol.OutliningSpansRequestFull>{
command: "outliningSpans",
arguments: { file: f1.path }
});
// ensure the outlining spans request can be canceled
verifyExecuteCommandSeqIsCancellable(<protocol.OutliningSpansRequestFull>{
command: "outliningSpans",
arguments: { file: f1.path }
});
}
function verifyExecuteCommandSeqIsCancellable<T extends server.protocol.Request>(request: Partial<T>) {
// Set the next request to be cancellable
// The cancellation token will cancel the request the third time
// isCancellationRequested() is called.
cancellationToken.setRequestToCancel(session.getNextSeq());
let operationCanceledExceptionThrown = false;
try {
session.executeCommandSeq(request);
}
catch (e) {
assert(e instanceof OperationCanceledException);
operationCanceledExceptionThrown = true;
}
assert(operationCanceledExceptionThrown, "Operation Canceled Exception not thrown for request: " + JSON.stringify(request));
}
});
});
}

View File

@@ -0,0 +1,122 @@
namespace ts.projectSystem {
describe("tsserver:: completions", () => {
it("works", () => {
const aTs: File = {
path: "/a.ts",
content: "export const foo = 0;",
};
const bTs: File = {
path: "/b.ts",
content: "foo",
};
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const session = createSession(createServerHost([aTs, bTs, tsconfig]));
openFilesForSession([aTs, bTs], session);
const requestLocation: protocol.FileLocationRequestArgs = {
file: bTs.path,
line: 1,
offset: 3,
};
const response = executeSessionRequest<protocol.CompletionsRequest, protocol.CompletionInfoResponse>(session, protocol.CommandTypes.CompletionInfo, {
...requestLocation,
includeExternalModuleExports: true,
prefix: "foo",
});
const entry: protocol.CompletionEntry = {
hasAction: true,
insertText: undefined,
isRecommended: undefined,
kind: ScriptElementKind.constElement,
kindModifiers: ScriptElementKindModifier.exportedModifier,
name: "foo",
replacementSpan: undefined,
sortText: "0",
source: "/a",
};
assert.deepEqual<protocol.CompletionInfo | undefined>(response, {
isGlobalCompletion: true,
isMemberCompletion: false,
isNewIdentifierLocation: false,
entries: [entry],
});
const detailsRequestArgs: protocol.CompletionDetailsRequestArgs = {
...requestLocation,
entryNames: [{ name: "foo", source: "/a" }],
};
const detailsResponse = executeSessionRequest<protocol.CompletionDetailsRequest, protocol.CompletionDetailsResponse>(session, protocol.CommandTypes.CompletionDetails, detailsRequestArgs);
const detailsCommon: protocol.CompletionEntryDetails & CompletionEntryDetails = {
displayParts: [
keywordPart(SyntaxKind.ConstKeyword),
spacePart(),
displayPart("foo", SymbolDisplayPartKind.localName),
punctuationPart(SyntaxKind.ColonToken),
spacePart(),
displayPart("0", SymbolDisplayPartKind.stringLiteral),
],
documentation: emptyArray,
kind: ScriptElementKind.constElement,
kindModifiers: ScriptElementKindModifier.exportedModifier,
name: "foo",
source: [{ text: "./a", kind: "text" }],
tags: undefined,
};
assert.deepEqual<ReadonlyArray<protocol.CompletionEntryDetails> | undefined>(detailsResponse, [
{
codeActions: [
{
description: `Import 'foo' from module "./a"`,
changes: [
{
fileName: "/b.ts",
textChanges: [
{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 },
newText: 'import { foo } from "./a";\n\n',
},
],
},
],
commands: undefined,
},
],
...detailsCommon,
},
]);
interface CompletionDetailsFullRequest extends protocol.FileLocationRequest {
readonly command: protocol.CommandTypes.CompletionDetailsFull;
readonly arguments: protocol.CompletionDetailsRequestArgs;
}
interface CompletionDetailsFullResponse extends protocol.Response {
readonly body?: ReadonlyArray<CompletionEntryDetails>;
}
const detailsFullResponse = executeSessionRequest<CompletionDetailsFullRequest, CompletionDetailsFullResponse>(session, protocol.CommandTypes.CompletionDetailsFull, detailsRequestArgs);
assert.deepEqual<ReadonlyArray<CompletionEntryDetails> | undefined>(detailsFullResponse, [
{
codeActions: [
{
description: `Import 'foo' from module "./a"`,
changes: [
{
fileName: "/b.ts",
textChanges: [createTextChange(createTextSpan(0, 0), 'import { foo } from "./a";\n\n')],
},
],
commands: undefined,
}
],
...detailsCommon,
}
]);
});
});
}

View File

@@ -0,0 +1,174 @@
namespace ts.projectSystem {
describe("tsserver:: searching for config file", () => {
it("should stop at projectRootPath if given", () => {
const f1 = {
path: "/a/file1.ts",
content: ""
};
const configFile = {
path: "/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, configFile]);
const service = createProjectService(host);
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a");
checkNumberOfConfiguredProjects(service, 0);
checkNumberOfInferredProjects(service, 1);
service.closeClientFile(f1.path);
service.openClientFile(f1.path);
checkNumberOfConfiguredProjects(service, 1);
checkNumberOfInferredProjects(service, 0);
});
it("should use projectRootPath when searching for inferred project again", () => {
const projectDir = "/a/b/projects/project";
const configFileLocation = `${projectDir}/src`;
const f1 = {
path: `${configFileLocation}/file1.ts`,
content: ""
};
const configFile = {
path: `${configFileLocation}/tsconfig.json`,
content: "{}"
};
const configFile2 = {
path: "/a/b/projects/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, libFile, configFile, configFile2]);
const service = createProjectService(host);
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
checkNumberOfProjects(service, { configuredProjects: 1 });
assert.isDefined(service.configuredProjects.get(configFile.path));
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
const typeRootLocations = getTypeRootsFromLocation(configFileLocation);
checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true);
// Delete config file - should create inferred project and not configured project
host.reloadFS([f1, libFile, configFile2]);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(service, { inferredProjects: 1 });
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true);
});
it("should use projectRootPath when searching for inferred project again 2", () => {
const projectDir = "/a/b/projects/project";
const configFileLocation = `${projectDir}/src`;
const f1 = {
path: `${configFileLocation}/file1.ts`,
content: ""
};
const configFile = {
path: `${configFileLocation}/tsconfig.json`,
content: "{}"
};
const configFile2 = {
path: "/a/b/projects/tsconfig.json",
content: "{}"
};
const host = createServerHost([f1, libFile, configFile, configFile2]);
const service = createProjectService(host, { useSingleInferredProject: true }, { useInferredProjectPerProjectRoot: true });
service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir);
checkNumberOfProjects(service, { configuredProjects: 1 });
assert.isDefined(service.configuredProjects.get(configFile.path));
checkWatchedFiles(host, [libFile.path, configFile.path]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true);
// Delete config file - should create inferred project with project root path set
host.reloadFS([f1, libFile, configFile2]);
host.runQueuedTimeoutCallbacks();
checkNumberOfProjects(service, { inferredProjects: 1 });
assert.equal(service.inferredProjects[0].projectRootPath, projectDir);
checkWatchedFiles(host, [libFile.path, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]);
checkWatchedDirectories(host, [], /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true);
});
describe("when the opened file is not from project root", () => {
const projectRoot = "/a/b/projects/project";
const file: File = {
path: `${projectRoot}/src/index.ts`,
content: "let y = 10"
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: "{}"
};
const files = [file, libFile];
const filesWithConfig = files.concat(tsconfig);
const dirOfFile = getDirectoryPath(file.path);
function openClientFile(files: File[]) {
const host = createServerHost(files);
const projectService = createProjectService(host);
projectService.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, "/a/b/projects/proj");
return { host, projectService };
}
function verifyConfiguredProject(host: TestServerHost, projectService: TestProjectService, orphanInferredProject?: boolean) {
projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: orphanInferredProject ? 1 : 0 });
const project = Debug.assertDefined(projectService.configuredProjects.get(tsconfig.path));
if (orphanInferredProject) {
const inferredProject = projectService.inferredProjects[0];
assert.isTrue(inferredProject.isOrphan());
}
checkProjectActualFiles(project, [file.path, libFile.path, tsconfig.path]);
checkWatchedFiles(host, [libFile.path, tsconfig.path]);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectories(host, (orphanInferredProject ? [projectRoot, `${dirOfFile}/node_modules/@types`] : [projectRoot]).concat(getTypeRootsFromLocation(projectRoot)), /*recursive*/ true);
}
function verifyInferredProject(host: TestServerHost, projectService: TestProjectService) {
projectService.checkNumberOfProjects({ inferredProjects: 1 });
const project = projectService.inferredProjects[0];
assert.isDefined(project);
const filesToWatch = [libFile.path];
forEachAncestorDirectory(dirOfFile, ancestor => {
filesToWatch.push(combinePaths(ancestor, "tsconfig.json"));
filesToWatch.push(combinePaths(ancestor, "jsconfig.json"));
});
checkProjectActualFiles(project, [file.path, libFile.path]);
checkWatchedFiles(host, filesToWatch);
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
checkWatchedDirectories(host, getTypeRootsFromLocation(dirOfFile), /*recursive*/ true);
}
it("tsconfig for the file exists", () => {
const { host, projectService } = openClientFile(filesWithConfig);
verifyConfiguredProject(host, projectService);
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyInferredProject(host, projectService);
host.reloadFS(filesWithConfig);
host.runQueuedTimeoutCallbacks();
verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true);
});
it("tsconfig for the file does not exist", () => {
const { host, projectService } = openClientFile(files);
verifyInferredProject(host, projectService);
host.reloadFS(filesWithConfig);
host.runQueuedTimeoutCallbacks();
verifyConfiguredProject(host, projectService, /*orphanInferredProject*/ true);
host.reloadFS(files);
host.runQueuedTimeoutCallbacks();
verifyInferredProject(host, projectService);
});
});
});
}

View File

@@ -0,0 +1,566 @@
namespace ts.projectSystem {
function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan {
return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) };
}
function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan {
return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) };
}
function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation {
return documentSpanFromSubstring(file, substring, options);
}
function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem {
return {
...protocolFileSpanFromSubstring(file, text, options),
isDefinition,
isWriteAccess: isDefinition,
lineText,
};
}
function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry {
return {
...documentSpanFromSubstring(file, text, options),
isDefinition,
isWriteAccess: isDefinition,
isInString: undefined,
};
}
function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray<File>): void {
openFilesForSession([file], session);
const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false));
const program = project.getCurrentProgram()!;
const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true);
closeFilesForSession([file], session);
Debug.assert(!output.emitSkipped);
assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false })));
}
describe("tsserver:: with declaration file maps:: project references", () => {
const aTs: File = {
path: "/a/a.ts",
content: "export function fnA() {}\nexport interface IfaceA {}\nexport const instanceA: IfaceA = {};",
};
const compilerOptions: CompilerOptions = {
outDir: "bin",
declaration: true,
declarationMap: true,
composite: true,
};
const configContent = JSON.stringify({ compilerOptions });
const aTsconfig: File = { path: "/a/tsconfig.json", content: configContent };
const aDtsMapContent: RawSourceMap = {
version: 3,
file: "a.d.ts",
sourceRoot: "",
sources: ["../a.ts"],
names: [],
mappings: "AAAA,wBAAgB,GAAG,SAAK;AACxB,MAAM,WAAW,MAAM;CAAG;AAC1B,eAAO,MAAM,SAAS,EAAE,MAAW,CAAC"
};
const aDtsMap: File = {
path: "/a/bin/a.d.ts.map",
content: JSON.stringify(aDtsMapContent),
};
const aDts: File = {
path: "/a/bin/a.d.ts",
// Need to mangle the sourceMappingURL part or it breaks the build
content: `export declare function fnA(): void;\nexport interface IfaceA {\n}\nexport declare const instanceA: IfaceA;\n//# source${""}MappingURL=a.d.ts.map`,
};
const bTs: File = {
path: "/b/b.ts",
content: "export function fnB() {}",
};
const bTsconfig: File = { path: "/b/tsconfig.json", content: configContent };
const bDtsMapContent: RawSourceMap = {
version: 3,
file: "b.d.ts",
sourceRoot: "",
sources: ["../b.ts"],
names: [],
mappings: "AAAA,wBAAgB,GAAG,SAAK",
};
const bDtsMap: File = {
path: "/b/bin/b.d.ts.map",
content: JSON.stringify(bDtsMapContent),
};
const bDts: File = {
// Need to mangle the sourceMappingURL part or it breaks the build
path: "/b/bin/b.d.ts",
content: `export declare function fnB(): void;\n//# source${""}MappingURL=b.d.ts.map`,
};
const dummyFile: File = {
path: "/dummy/dummy.ts",
content: "let a = 10;"
};
const userTs: File = {
path: "/user/user.ts",
content: 'import * as a from "../a/bin/a";\nimport * as b from "../b/bin/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }',
};
const userTsForConfigProject: File = {
path: "/user/user.ts",
content: 'import * as a from "../a/a";\nimport * as b from "../b/b";\nexport function fnUser() { a.fnA(); b.fnB(); a.instanceA; }',
};
const userTsconfig: File = {
path: "/user/tsconfig.json",
content: JSON.stringify({
file: ["user.ts"],
references: [{ path: "../a" }, { path: "../b" }]
})
};
function makeSampleProjects(addUserTsConfig?: boolean) {
const host = createServerHost([aTs, aTsconfig, aDtsMap, aDts, bTsconfig, bTs, bDtsMap, bDts, ...(addUserTsConfig ? [userTsForConfigProject, userTsconfig] : [userTs]), dummyFile]);
const session = createSession(host);
checkDeclarationFiles(aTs, session, [aDtsMap, aDts]);
checkDeclarationFiles(bTs, session, [bDtsMap, bDts]);
// Testing what happens if we delete the original sources.
host.deleteFile(bTs.path);
openFilesForSession([userTs], session);
const service = session.getProjectService();
checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 1 } : { inferredProjects: 1 });
return session;
}
function verifyInferredProjectUnchanged(session: TestSession) {
checkProjectActualFiles(session.getProjectService().inferredProjects[0], [userTs.path, aDts.path, bDts.path]);
}
function verifyDummyProject(session: TestSession) {
checkProjectActualFiles(session.getProjectService().inferredProjects[0], [dummyFile.path]);
}
function verifyOnlyOrphanInferredProject(session: TestSession) {
openFilesForSession([dummyFile], session);
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 });
verifyDummyProject(session);
}
function verifySingleInferredProject(session: TestSession) {
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1 });
verifyInferredProjectUnchanged(session);
// Close user file should close all the projects after opening dummy file
closeFilesForSession([userTs], session);
verifyOnlyOrphanInferredProject(session);
}
function verifyATsConfigProject(session: TestSession) {
checkProjectActualFiles(session.getProjectService().configuredProjects.get(aTsconfig.path)!, [aTs.path, aTsconfig.path]);
}
function verifyATsConfigOriginalProject(session: TestSession) {
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 });
verifyInferredProjectUnchanged(session);
verifyATsConfigProject(session);
// Close user file should close all the projects
closeFilesForSession([userTs], session);
verifyOnlyOrphanInferredProject(session);
}
function verifyATsConfigWhenOpened(session: TestSession) {
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 });
verifyInferredProjectUnchanged(session);
verifyATsConfigProject(session);
closeFilesForSession([userTs], session);
openFilesForSession([dummyFile], session);
checkNumberOfProjects(session.getProjectService(), { inferredProjects: 1, configuredProjects: 1 });
verifyDummyProject(session);
verifyATsConfigProject(session); // ATsConfig should still be alive
}
function verifyUserTsConfigProject(session: TestSession) {
checkProjectActualFiles(session.getProjectService().configuredProjects.get(userTsconfig.path)!, [userTs.path, aDts.path, userTsconfig.path]);
}
it("goToDefinition", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.DefinitionRequest, protocol.DefinitionResponse>(session, protocol.CommandTypes.Definition, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]);
verifySingleInferredProject(session);
});
it("getDefinitionAndBoundSpan", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.DefinitionAndBoundSpanRequest, protocol.DefinitionAndBoundSpanResponse>(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual(response, {
textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"),
definitions: [protocolFileSpanFromSubstring(aTs, "fnA")],
});
verifySingleInferredProject(session);
});
it("getDefinitionAndBoundSpan with file navigation", () => {
const session = makeSampleProjects(/*addUserTsConfig*/ true);
const response = executeSessionRequest<protocol.DefinitionAndBoundSpanRequest, protocol.DefinitionAndBoundSpanResponse>(session, protocol.CommandTypes.DefinitionAndBoundSpan, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual(response, {
textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"),
definitions: [protocolFileSpanFromSubstring(aTs, "fnA")],
});
checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 });
verifyUserTsConfigProject(session);
// Navigate to the definition
closeFilesForSession([userTs], session);
openFilesForSession([aTs], session);
// UserTs configured project should be alive
checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 });
verifyUserTsConfigProject(session);
verifyATsConfigProject(session);
closeFilesForSession([aTs], session);
verifyOnlyOrphanInferredProject(session);
});
it("goToType", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.TypeDefinitionRequest, protocol.TypeDefinitionResponse>(session, protocol.CommandTypes.TypeDefinition, protocolFileLocationFromSubstring(userTs, "instanceA"));
assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "IfaceA")]);
verifySingleInferredProject(session);
});
it("goToImplementation", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.ImplementationRequest, protocol.ImplementationResponse>(session, protocol.CommandTypes.Implementation, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual(response, [protocolFileSpanFromSubstring(aTs, "fnA")]);
verifySingleInferredProject(session);
});
it("goToDefinition -- target does not exist", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.DefinitionRequest, protocol.DefinitionResponse>(session, CommandNames.Definition, protocolFileLocationFromSubstring(userTs, "fnB()"));
// bTs does not exist, so stick with bDts
assert.deepEqual(response, [protocolFileSpanFromSubstring(bDts, "fnB")]);
verifySingleInferredProject(session);
});
it("navigateTo", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.NavtoRequest, protocol.NavtoResponse>(session, CommandNames.Navto, { file: userTs.path, searchValue: "fn" });
assert.deepEqual<ReadonlyArray<protocol.NavtoItem> | undefined>(response, [
{
...protocolFileSpanFromSubstring(bDts, "export declare function fnB(): void;"),
name: "fnB",
matchKind: "prefix",
isCaseSensitive: true,
kind: ScriptElementKind.functionElement,
kindModifiers: "export,declare",
},
{
...protocolFileSpanFromSubstring(userTs, "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"),
name: "fnUser",
matchKind: "prefix",
isCaseSensitive: true,
kind: ScriptElementKind.functionElement,
kindModifiers: "export",
},
{
...protocolFileSpanFromSubstring(aTs, "export function fnA() {}"),
name: "fnA",
matchKind: "prefix",
isCaseSensitive: true,
kind: ScriptElementKind.functionElement,
kindModifiers: "export",
},
]);
verifyATsConfigOriginalProject(session);
});
const referenceATs = (aTs: File): protocol.ReferencesResponseItem => makeReferenceItem(aTs, /*isDefinition*/ true, "fnA", "export function fnA() {}");
const referencesUserTs = (userTs: File): ReadonlyArray<protocol.ReferencesResponseItem> => [
makeReferenceItem(userTs, /*isDefinition*/ false, "fnA", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"),
];
it("findAllReferences", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.ReferencesRequest, protocol.ReferencesResponse>(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual<protocol.ReferencesResponseBody | undefined>(response, {
refs: [...referencesUserTs(userTs), referenceATs(aTs)],
symbolName: "fnA",
symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnA()").offset,
symbolDisplayString: "function fnA(): void",
});
verifyATsConfigOriginalProject(session);
});
it("findAllReferences -- starting at definition", () => {
const session = makeSampleProjects();
openFilesForSession([aTs], session); // If it's not opened, the reference isn't found.
const response = executeSessionRequest<protocol.ReferencesRequest, protocol.ReferencesResponse>(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(aTs, "fnA"));
assert.deepEqual<protocol.ReferencesResponseBody | undefined>(response, {
refs: [referenceATs(aTs), ...referencesUserTs(userTs)],
symbolName: "fnA",
symbolStartOffset: protocolLocationFromSubstring(aTs.content, "fnA").offset,
symbolDisplayString: "function fnA(): void",
});
verifyATsConfigWhenOpened(session);
});
interface ReferencesFullRequest extends protocol.FileLocationRequest { readonly command: protocol.CommandTypes.ReferencesFull; }
interface ReferencesFullResponse extends protocol.Response { readonly body: ReadonlyArray<ReferencedSymbol>; }
it("findAllReferencesFull", () => {
const session = makeSampleProjects();
const responseFull = executeSessionRequest<ReferencesFullRequest, ReferencesFullResponse>(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual<ReadonlyArray<ReferencedSymbol>>(responseFull, [
{
definition: {
...documentSpanFromSubstring(aTs, "fnA"),
kind: ScriptElementKind.functionElement,
name: "function fnA(): void",
containerKind: ScriptElementKind.unknown,
containerName: "",
displayParts: [
keywordPart(SyntaxKind.FunctionKeyword),
spacePart(),
displayPart("fnA", SymbolDisplayPartKind.functionName),
punctuationPart(SyntaxKind.OpenParenToken),
punctuationPart(SyntaxKind.CloseParenToken),
punctuationPart(SyntaxKind.ColonToken),
spacePart(),
keywordPart(SyntaxKind.VoidKeyword),
],
},
references: [
makeReferenceEntry(userTs, /*isDefinition*/ false, "fnA"),
makeReferenceEntry(aTs, /*isDefinition*/ true, "fnA"),
],
},
]);
verifyATsConfigOriginalProject(session);
});
it("findAllReferencesFull definition is in mapped file", () => {
const aTs: File = { path: "/a/a.ts", content: `function f() {}` };
const aTsconfig: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({ compilerOptions: { declaration: true, declarationMap: true, outFile: "../bin/a.js" } }),
};
const bTs: File = { path: "/b/b.ts", content: `f();` };
const bTsconfig: File = { path: "/b/tsconfig.json", content: JSON.stringify({ references: [{ path: "../a" }] }) };
const aDts: File = { path: "/bin/a.d.ts", content: `declare function f(): void;\n//# sourceMappingURL=a.d.ts.map` };
const aDtsMap: File = {
path: "/bin/a.d.ts.map",
content: JSON.stringify({ version: 3, file: "a.d.ts", sourceRoot: "", sources: ["../a/a.ts"], names: [], mappings: "AAAA,iBAAS,CAAC,SAAK" }),
};
const session = createSession(createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap]));
checkDeclarationFiles(aTs, session, [aDtsMap, aDts]);
openFilesForSession([bTs], session);
checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 });
const responseFull = executeSessionRequest<ReferencesFullRequest, ReferencesFullResponse>(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(bTs, "f()"));
assert.deepEqual<ReadonlyArray<ReferencedSymbol>>(responseFull, [
{
definition: {
containerKind: ScriptElementKind.unknown,
containerName: "",
displayParts: [
keywordPart(SyntaxKind.FunctionKeyword),
spacePart(),
displayPart("f", SymbolDisplayPartKind.functionName),
punctuationPart(SyntaxKind.OpenParenToken),
punctuationPart(SyntaxKind.CloseParenToken),
punctuationPart(SyntaxKind.ColonToken),
spacePart(),
keywordPart(SyntaxKind.VoidKeyword),
],
fileName: aTs.path,
kind: ScriptElementKind.functionElement,
name: "function f(): void",
textSpan: { start: 9, length: 1 },
},
references: [
{
fileName: bTs.path,
isDefinition: false,
isInString: undefined,
isWriteAccess: false,
textSpan: { start: 0, length: 1 },
},
{
fileName: aTs.path,
isDefinition: true,
isInString: undefined,
isWriteAccess: true,
textSpan: { start: 9, length: 1 },
},
],
}
]);
});
it("findAllReferences -- target does not exist", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.ReferencesRequest, protocol.ReferencesResponse>(session, protocol.CommandTypes.References, protocolFileLocationFromSubstring(userTs, "fnB()"));
assert.deepEqual<protocol.ReferencesResponseBody | undefined>(response, {
refs: [
makeReferenceItem(bDts, /*isDefinition*/ true, "fnB", "export declare function fnB(): void;"),
makeReferenceItem(userTs, /*isDefinition*/ false, "fnB", "export function fnUser() { a.fnA(); b.fnB(); a.instanceA; }"),
],
symbolName: "fnB",
symbolStartOffset: protocolLocationFromSubstring(userTs.content, "fnB()").offset,
symbolDisplayString: "function fnB(): void",
});
verifySingleInferredProject(session);
});
const renameATs = (aTs: File): protocol.SpanGroup => ({
file: aTs.path,
locs: [protocolRenameSpanFromSubstring(aTs.content, "fnA")],
});
const renameUserTs = (userTs: File): protocol.SpanGroup => ({
file: userTs.path,
locs: [protocolRenameSpanFromSubstring(userTs.content, "fnA")],
});
it("renameLocations", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
displayName: "fnA",
fileToRename: undefined,
fullDisplayName: '"/a/bin/a".fnA', // Ideally this would use the original source's path instead of the declaration file's path.
kind: ScriptElementKind.functionElement,
kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","),
triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnA"),
},
locs: [renameUserTs(userTs), renameATs(aTs)],
});
verifyATsConfigOriginalProject(session);
});
it("renameLocations -- starting at definition", () => {
const session = makeSampleProjects();
openFilesForSession([aTs], session); // If it's not opened, the reference isn't found.
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "fnA"));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
displayName: "fnA",
fileToRename: undefined,
fullDisplayName: '"/a/a".fnA',
kind: ScriptElementKind.functionElement,
kindModifiers: ScriptElementKindModifier.exportedModifier,
triggerSpan: protocolTextSpanFromSubstring(aTs.content, "fnA"),
},
locs: [renameATs(aTs), renameUserTs(userTs)],
});
verifyATsConfigWhenOpened(session);
});
it("renameLocationsFull", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.RenameFullRequest, protocol.RenameFullResponse>(session, protocol.CommandTypes.RenameLocationsFull, protocolFileLocationFromSubstring(userTs, "fnA()"));
assert.deepEqual<ReadonlyArray<RenameLocation>>(response, [
renameLocation(userTs, "fnA"),
renameLocation(aTs, "fnA"),
]);
verifyATsConfigOriginalProject(session);
});
it("renameLocations -- target does not exist", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(userTs, "fnB()"));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
displayName: "fnB",
fileToRename: undefined,
fullDisplayName: '"/b/bin/b".fnB',
kind: ScriptElementKind.functionElement,
kindModifiers: [ScriptElementKindModifier.exportedModifier, ScriptElementKindModifier.ambientModifier].join(","),
triggerSpan: protocolTextSpanFromSubstring(userTs.content, "fnB"),
},
locs: [
{
file: bDts.path,
locs: [protocolRenameSpanFromSubstring(bDts.content, "fnB")],
},
{
file: userTs.path,
locs: [protocolRenameSpanFromSubstring(userTs.content, "fnB")],
},
],
});
verifySingleInferredProject(session);
});
it("getEditsForFileRename", () => {
const session = makeSampleProjects();
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, protocol.CommandTypes.GetEditsForFileRename, {
oldFilePath: aTs.path,
newFilePath: "/a/aNew.ts",
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, [
{
fileName: userTs.path,
textChanges: [
{ ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" },
],
},
]);
verifySingleInferredProject(session);
});
it("getEditsForFileRename when referencing project doesnt include file and its renamed", () => {
const aTs: File = { path: "/a/src/a.ts", content: "" };
const aTsconfig: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions: {
composite: true,
declaration: true,
declarationMap: true,
outDir: "./build",
}
}),
};
const bTs: File = { path: "/b/src/b.ts", content: "" };
const bTsconfig: File = {
path: "/b/tsconfig.json",
content: JSON.stringify({
compilerOptions: {
composite: true,
outDir: "./build",
},
include: ["./src"],
references: [{ path: "../a" }],
}),
};
const host = createServerHost([aTs, aTsconfig, bTs, bTsconfig]);
const session = createSession(host);
openFilesForSession([aTs, bTs], session);
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, CommandNames.GetEditsForFileRename, {
oldFilePath: aTs.path,
newFilePath: "/a/src/a1.ts",
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, []); // Should not change anything
});
});
}

View File

@@ -0,0 +1,94 @@
namespace ts.projectSystem {
describe("tsserver:: document registry in project service", () => {
const projectRootPath = "/user/username/projects/project";
const importModuleContent = `import {a} from "./module1"`;
const file: File = {
path: `${projectRootPath}/index.ts`,
content: importModuleContent
};
const moduleFile: File = {
path: `${projectRootPath}/module1.d.ts`,
content: "export const a: number;"
};
const configFile: File = {
path: `${projectRootPath}/tsconfig.json`,
content: JSON.stringify({ files: ["index.ts"] })
};
function getProject(service: TestProjectService) {
return service.configuredProjects.get(configFile.path)!;
}
function checkProject(service: TestProjectService, moduleIsOrphan: boolean) {
// Update the project
const project = getProject(service);
project.getLanguageService();
checkProjectActualFiles(project, [file.path, libFile.path, configFile.path, ...(moduleIsOrphan ? [] : [moduleFile.path])]);
const moduleInfo = service.getScriptInfo(moduleFile.path)!;
assert.isDefined(moduleInfo);
assert.equal(moduleInfo.isOrphan(), moduleIsOrphan);
const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings());
assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]);
}
function createServiceAndHost() {
const host = createServerHost([file, moduleFile, libFile, configFile]);
const service = createProjectService(host);
service.openClientFile(file.path);
checkProject(service, /*moduleIsOrphan*/ false);
return { host, service };
}
function changeFileToNotImportModule(service: TestProjectService) {
const info = service.getScriptInfo(file.path)!;
service.applyChangesToFile(info, [{ span: { start: 0, length: importModuleContent.length }, newText: "" }]);
checkProject(service, /*moduleIsOrphan*/ true);
}
function changeFileToImportModule(service: TestProjectService) {
const info = service.getScriptInfo(file.path)!;
service.applyChangesToFile(info, [{ span: { start: 0, length: 0 }, newText: importModuleContent }]);
checkProject(service, /*moduleIsOrphan*/ false);
}
it("Caches the source file if script info is orphan", () => {
const { service } = createServiceAndHost();
const project = getProject(service);
const moduleInfo = service.getScriptInfo(moduleFile.path)!;
const sourceFile = moduleInfo.cacheSourceFile!.sourceFile;
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
// edit file
changeFileToNotImportModule(service);
assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile);
// write content back
changeFileToImportModule(service);
assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile);
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
});
it("Caches the source file if script info is orphan, and orphan script info changes", () => {
const { host, service } = createServiceAndHost();
const project = getProject(service);
const moduleInfo = service.getScriptInfo(moduleFile.path)!;
const sourceFile = moduleInfo.cacheSourceFile!.sourceFile;
assert.equal(project.getSourceFile(moduleInfo.path), sourceFile);
// edit file
changeFileToNotImportModule(service);
assert.equal(moduleInfo.cacheSourceFile!.sourceFile, sourceFile);
const updatedModuleContent = moduleFile.content + "\nexport const b: number;";
host.writeFile(moduleFile.path, updatedModuleContent);
// write content back
changeFileToImportModule(service);
assert.notEqual(moduleInfo.cacheSourceFile!.sourceFile, sourceFile);
assert.equal(project.getSourceFile(moduleInfo.path), moduleInfo.cacheSourceFile!.sourceFile);
assert.equal(moduleInfo.cacheSourceFile!.sourceFile.text, updatedModuleContent);
});
});
}

View File

@@ -0,0 +1,54 @@
namespace ts.projectSystem {
describe("tsserver:: duplicate packages", () => {
// Tests that 'moduleSpecifiers.ts' will import from the redirecting file, and not from the file it redirects to, if that can provide a global module specifier.
it("works with import fixes", () => {
const packageContent = "export const foo: number;";
const packageJsonContent = JSON.stringify({ name: "foo", version: "1.2.3" });
const aFooIndex: File = { path: "/a/node_modules/foo/index.d.ts", content: packageContent };
const aFooPackage: File = { path: "/a/node_modules/foo/package.json", content: packageJsonContent };
const bFooIndex: File = { path: "/b/node_modules/foo/index.d.ts", content: packageContent };
const bFooPackage: File = { path: "/b/node_modules/foo/package.json", content: packageJsonContent };
const userContent = 'import("foo");\nfoo';
const aUser: File = { path: "/a/user.ts", content: userContent };
const bUser: File = { path: "/b/user.ts", content: userContent };
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const host = createServerHost([aFooIndex, aFooPackage, bFooIndex, bFooPackage, aUser, bUser, tsconfig]);
const session = createSession(host);
openFilesForSession([aUser, bUser], session);
for (const user of [aUser, bUser]) {
const response = executeSessionRequest<protocol.CodeFixRequest, protocol.CodeFixResponse>(session, protocol.CommandTypes.GetCodeFixes, {
file: user.path,
startLine: 2,
startOffset: 1,
endLine: 2,
endOffset: 4,
errorCodes: [Diagnostics.Cannot_find_name_0.code],
});
assert.deepEqual<ReadonlyArray<protocol.CodeFixAction> | undefined>(response, [
{
description: `Import 'foo' from module "foo"`,
fixName: "import",
fixId: "fixMissingImport",
fixAllDescription: "Add all missing imports",
changes: [{
fileName: user.path,
textChanges: [{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 },
newText: 'import { foo } from "foo";\n\n',
}],
}],
commands: undefined,
},
]);
}
});
});
}

View File

@@ -0,0 +1,45 @@
namespace ts.projectSystem {
describe("tsserver:: forceConsistentCasingInFileNames", () => {
it("works when extends is specified with a case insensitive file system", () => {
const rootPath = "/Users/username/dev/project";
const file1: File = {
path: `${rootPath}/index.ts`,
content: 'import {x} from "file2";',
};
const file2: File = {
path: `${rootPath}/file2.js`,
content: "",
};
const file2Dts: File = {
path: `${rootPath}/types/file2/index.d.ts`,
content: "export declare const x: string;",
};
const tsconfigAll: File = {
path: `${rootPath}/tsconfig.all.json`,
content: JSON.stringify({
compilerOptions: {
baseUrl: ".",
paths: { file2: ["./file2.js"] },
typeRoots: ["./types"],
forceConsistentCasingInFileNames: true,
},
}),
};
const tsconfig: File = {
path: `${rootPath}/tsconfig.json`,
content: JSON.stringify({ extends: "./tsconfig.all.json" }),
};
const host = createServerHost([file1, file2, file2Dts, libFile, tsconfig, tsconfigAll], { useCaseSensitiveFileNames: false });
const session = createSession(host);
openFilesForSession([file1], session);
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics();
assert.deepEqual(diagnostics, []);
});
});
}

View File

@@ -0,0 +1,39 @@
namespace ts.projectSystem {
describe("tsserver:: format settings", () => {
it("can be set globally", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x;"
};
const host = createServerHost([f1]);
const projectService = createProjectService(host);
projectService.openClientFile(f1.path);
const defaultSettings = projectService.getFormatCodeOptions(f1.path as server.NormalizedPath);
// set global settings
const newGlobalSettings1 = { ...defaultSettings, placeOpenBraceOnNewLineForControlBlocks: !defaultSettings.placeOpenBraceOnNewLineForControlBlocks };
projectService.setHostConfiguration({ formatOptions: newGlobalSettings1 });
// get format options for file - should be equal to new global settings
const s1 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path));
assert.deepEqual(s1, newGlobalSettings1, "file settings should be the same with global settings");
// set per file format options
const newPerFileSettings = { ...defaultSettings, insertSpaceAfterCommaDelimiter: !defaultSettings.insertSpaceAfterCommaDelimiter };
projectService.setHostConfiguration({ formatOptions: newPerFileSettings, file: f1.path });
// get format options for file - should be equal to new per-file settings
const s2 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path));
assert.deepEqual(s2, newPerFileSettings, "file settings should be the same with per-file settings");
// set new global settings - they should not affect ones that were set per-file
const newGlobalSettings2 = { ...defaultSettings, insertSpaceAfterSemicolonInForStatements: !defaultSettings.insertSpaceAfterSemicolonInForStatements };
projectService.setHostConfiguration({ formatOptions: newGlobalSettings2 });
// get format options for file - should be equal to new per-file settings
const s3 = projectService.getFormatCodeOptions(server.toNormalizedPath(f1.path));
assert.deepEqual(s3, newPerFileSettings, "file settings should still be the same with per-file settings");
});
});
}

View File

@@ -0,0 +1,105 @@
namespace ts.projectSystem {
describe("tsserver:: getEditsForFileRename", () => {
it("works for host implementing 'resolveModuleNames' and 'getResolvedModuleWithFailedLookupLocationsFromCache'", () => {
const userTs: File = {
path: "/user.ts",
content: 'import { x } from "./old";',
};
const newTs: File = {
path: "/new.ts",
content: "export const x = 0;",
};
const tsconfig: File = {
path: "/tsconfig.json",
content: "{}",
};
const host = createServerHost([userTs, newTs, tsconfig]);
const projectService = createProjectService(host);
projectService.openClientFile(userTs.path);
const project = projectService.configuredProjects.get(tsconfig.path)!;
Debug.assert(!!project.resolveModuleNames);
const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatSettings, emptyOptions);
assert.deepEqual<ReadonlyArray<FileTextChanges>>(edits, [{
fileName: "/user.ts",
textChanges: [{
span: textSpanFromSubstring(userTs.content, "./old"),
newText: "./new",
}],
}]);
});
it("works with multiple projects", () => {
const aUserTs: File = {
path: "/a/user.ts",
content: 'import { x } from "./old";',
};
const aOldTs: File = {
path: "/a/old.ts",
content: "export const x = 0;",
};
const aTsconfig: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({ files: ["./old.ts", "./user.ts"] }),
};
const bUserTs: File = {
path: "/b/user.ts",
content: 'import { x } from "../a/old";',
};
const bTsconfig: File = {
path: "/b/tsconfig.json",
content: "{}",
};
const host = createServerHost([aUserTs, aOldTs, aTsconfig, bUserTs, bTsconfig]);
const session = createSession(host);
openFilesForSession([aUserTs, bUserTs], session);
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, CommandNames.GetEditsForFileRename, {
oldFilePath: aOldTs.path,
newFilePath: "/a/new.ts",
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, [
{
fileName: aTsconfig.path,
textChanges: [{ ...protocolTextSpanFromSubstring(aTsconfig.content, "./old.ts"), newText: "new.ts" }],
},
{
fileName: aUserTs.path,
textChanges: [{ ...protocolTextSpanFromSubstring(aUserTs.content, "./old"), newText: "./new" }],
},
{
fileName: bUserTs.path,
textChanges: [{ ...protocolTextSpanFromSubstring(bUserTs.content, "../a/old"), newText: "../a/new" }],
},
]);
});
it("works with file moved to inferred project", () => {
const aTs: File = { path: "/a.ts", content: 'import {} from "./b";' };
const cTs: File = { path: "/c.ts", content: "export {};" };
const tsconfig: File = { path: "/tsconfig.json", content: JSON.stringify({ files: ["./a.ts", "./b.ts"] }) };
const host = createServerHost([aTs, cTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs, cTs], session);
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, CommandNames.GetEditsForFileRename, {
oldFilePath: "/b.ts",
newFilePath: cTs.path,
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, [
{
fileName: "/tsconfig.json",
textChanges: [{ ...protocolTextSpanFromSubstring(tsconfig.content, "./b.ts"), newText: "c.ts" }],
},
{
fileName: "/a.ts",
textChanges: [{ ...protocolTextSpanFromSubstring(aTs.content, "./b"), newText: "./c" }],
},
]);
});
});
}

View File

@@ -15,6 +15,9 @@ namespace ts.projectSystem {
export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories;
export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed;
export import commonFile1 = tscWatch.commonFile1;
export import commonFile2 = tscWatch.commonFile2;
const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
export function mapOutputToJson(s: string) {
return convertToObject(
@@ -459,9 +462,13 @@ namespace ts.projectSystem {
return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes);
}
//function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) {
// checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path));
//}
export function checkOpenFiles(projectService: server.ProjectService, expectedFiles: File[]) {
checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path));
}
export function checkScriptInfos(projectService: server.ProjectService, expectedFiles: ReadonlyArray<string>) {
checkArray("ScriptInfos files", arrayFrom(projectService.filenameToScriptInfo.values(), info => info.fileName), expectedFiles);
}
export function protocolLocationFromSubstring(str: string, substring: string): protocol.Location {
const start = str.indexOf(substring);
@@ -497,18 +504,10 @@ namespace ts.projectSystem {
Debug.assert(start !== -1);
return createTextSpan(start, substring.length);
}
//function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs {
// return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) };
//}
//function protocolFileSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileSpan {
// return { file: file.path, ...protocolTextSpanFromSubstring(file.content, substring, options) };
//}
//function documentSpanFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): DocumentSpan {
// return { fileName: file.path, textSpan: textSpanFromSubstring(file.content, substring, options) };
//}
//function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation {
// return documentSpanFromSubstring(file, substring, options);
//}
export function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs {
return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) };
}
export interface SpanFromSubstringOptions {
readonly index: number;
@@ -648,33 +647,4 @@ namespace ts.projectSystem {
assert.strictEqual(outputs.length, index + 1, JSON.stringify(outputs));
}
}
//function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem {
// return {
// ...protocolFileSpanFromSubstring(file, text, options),
// isDefinition,
// isWriteAccess: isDefinition,
// lineText,
// };
//}
//function makeReferenceEntry(file: File, isDefinition: boolean, text: string, options?: SpanFromSubstringOptions): ReferenceEntry {
// return {
// ...documentSpanFromSubstring(file, text, options),
// isDefinition,
// isWriteAccess: isDefinition,
// isInString: undefined,
// };
//}
//function checkDeclarationFiles(file: File, session: TestSession, expectedFiles: ReadonlyArray<File>): void {
// openFilesForSession([file], session);
// const project = Debug.assertDefined(session.getProjectService().getDefaultProjectForFile(file.path as server.NormalizedPath, /*ensureProject*/ false));
// const program = project.getCurrentProgram()!;
// const output = getFileEmitOutput(program, Debug.assertDefined(program.getSourceFile(file.path)), /*emitOnlyDtsFiles*/ true);
// closeFilesForSession([file], session);
// Debug.assert(!output.emitSkipped);
// assert.deepEqual(output.outputFiles, expectedFiles.map((e): OutputFile => ({ name: e.path, text: e.content, writeByteOrderMark: false })));
//}
}

View File

@@ -0,0 +1,18 @@
namespace ts.projectSystem {
describe("tsserver:: import helpers", () => {
it("should not crash in tsserver", () => {
const f1 = {
path: "/a/app.ts",
content: "export async function foo() { return 100; }"
};
const tslib = {
path: "/a/node_modules/tslib/index.d.ts",
content: ""
};
const host = createServerHost([f1, tslib]);
const service = createProjectService(host);
service.openExternalProject({ projectFileName: "p", rootFiles: [toExternalFile(f1.path)], options: { importHelpers: true } });
service.checkNumberOfProjects({ externalProjects: 1 });
});
});
}

View File

@@ -0,0 +1,229 @@
namespace ts.projectSystem {
describe("tsserver:: Inferred projects", () => {
it("should support files without extensions", () => {
const f = {
path: "/a/compile",
content: "let x = 1"
};
const host = createServerHost([f]);
const session = createSession(host);
session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
seq: 1,
type: "request",
command: "compilerOptionsForInferredProjects",
arguments: {
options: {
allowJs: true
}
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 2,
type: "request",
command: "open",
arguments: {
file: f.path,
fileContent: f.content,
scriptKindName: "JS"
}
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { inferredProjects: 1 });
checkProjectActualFiles(projectService.inferredProjects[0], [f.path]);
});
it("inferred projects per project root", () => {
const file1 = { path: "/a/file1.ts", content: "let x = 1;", projectRootPath: "/a" };
const file2 = { path: "/a/file2.ts", content: "let y = 2;", projectRootPath: "/a" };
const file3 = { path: "/b/file2.ts", content: "let x = 3;", projectRootPath: "/b" };
const file4 = { path: "/c/file3.ts", content: "let z = 4;" };
const host = createServerHost([file1, file2, file3, file4]);
const session = createSession(host, {
useSingleInferredProject: true,
useInferredProjectPerProjectRoot: true
});
session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
seq: 1,
type: "request",
command: CommandNames.CompilerOptionsForInferredProjects,
arguments: {
options: {
allowJs: true,
target: ScriptTarget.ESNext
}
}
});
session.executeCommand(<server.protocol.SetCompilerOptionsForInferredProjectsRequest>{
seq: 2,
type: "request",
command: CommandNames.CompilerOptionsForInferredProjects,
arguments: {
options: {
allowJs: true,
target: ScriptTarget.ES2015
},
projectRootPath: "/b"
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 3,
type: "request",
command: CommandNames.Open,
arguments: {
file: file1.path,
fileContent: file1.content,
scriptKindName: "JS",
projectRootPath: file1.projectRootPath
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 4,
type: "request",
command: CommandNames.Open,
arguments: {
file: file2.path,
fileContent: file2.content,
scriptKindName: "JS",
projectRootPath: file2.projectRootPath
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 5,
type: "request",
command: CommandNames.Open,
arguments: {
file: file3.path,
fileContent: file3.content,
scriptKindName: "JS",
projectRootPath: file3.projectRootPath
}
});
session.executeCommand(<server.protocol.OpenRequest>{
seq: 6,
type: "request",
command: CommandNames.Open,
arguments: {
file: file4.path,
fileContent: file4.content,
scriptKindName: "JS"
}
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { inferredProjects: 3 });
checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]);
checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]);
checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]);
assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext);
assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext);
assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015);
});
function checkInferredProject(inferredProject: server.InferredProject, actualFiles: File[], target: ScriptTarget) {
checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path));
assert.equal(inferredProject.getCompilationSettings().target, target);
}
function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) {
const files: [File, File, File, File] = [
{ path: "/a/file1.ts", content: "let x = 1;" },
{ path: "/A/file2.ts", content: "let y = 2;" },
{ path: "/b/file2.ts", content: "let x = 3;" },
{ path: "/c/file3.ts", content: "let z = 4;" }
];
const host = createServerHost(files, { useCaseSensitiveFileNames });
const projectService = createProjectService(host, { useSingleInferredProject: true, }, { useInferredProjectPerProjectRoot: true });
projectService.setCompilerOptionsForInferredProjects({
allowJs: true,
target: ScriptTarget.ESNext
});
projectService.setCompilerOptionsForInferredProjects({
allowJs: true,
target: ScriptTarget.ES2015
}, "/a");
openClientFiles(["/a", "/a", "/b", undefined]);
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0], files[1]], ScriptTarget.ES2015],
[[files[2]], ScriptTarget.ESNext]
]);
closeClientFiles();
openClientFiles(["/a", "/A", "/b", undefined]);
if (useCaseSensitiveFileNames) {
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0]], ScriptTarget.ES2015],
[[files[1]], ScriptTarget.ESNext],
[[files[2]], ScriptTarget.ESNext]
]);
}
else {
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0], files[1]], ScriptTarget.ES2015],
[[files[2]], ScriptTarget.ESNext]
]);
}
closeClientFiles();
projectService.setCompilerOptionsForInferredProjects({
allowJs: true,
target: ScriptTarget.ES2017
}, "/A");
openClientFiles(["/a", "/a", "/b", undefined]);
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0], files[1]], useCaseSensitiveFileNames ? ScriptTarget.ES2015 : ScriptTarget.ES2017],
[[files[2]], ScriptTarget.ESNext]
]);
closeClientFiles();
openClientFiles(["/a", "/A", "/b", undefined]);
if (useCaseSensitiveFileNames) {
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0]], ScriptTarget.ES2015],
[[files[1]], ScriptTarget.ES2017],
[[files[2]], ScriptTarget.ESNext]
]);
}
else {
verifyInferredProjectsState([
[[files[3]], ScriptTarget.ESNext],
[[files[0], files[1]], ScriptTarget.ES2017],
[[files[2]], ScriptTarget.ESNext]
]);
}
closeClientFiles();
function openClientFiles(projectRoots: [string | undefined, string | undefined, string | undefined, string | undefined]) {
files.forEach((file, index) => {
projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRoots[index]);
});
}
function closeClientFiles() {
files.forEach(file => projectService.closeClientFile(file.path));
}
function verifyInferredProjectsState(expected: [File[], ScriptTarget][]) {
checkNumberOfProjects(projectService, { inferredProjects: expected.length });
projectService.inferredProjects.forEach((p, index) => {
const [actualFiles, target] = expected[index];
checkInferredProject(p, actualFiles, target);
});
}
}
it("inferred projects per project root with case sensitive system", () => {
verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true);
});
it("inferred projects per project root with case insensitive system", () => {
verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false);
});
});
}

View File

@@ -0,0 +1,19 @@
namespace ts.projectSystem {
describe("tsserver:: Language service", () => {
it("should work correctly on case-sensitive file systems", () => {
const lib = {
path: "/a/Lib/lib.d.ts",
content: "let x: number"
};
const f = {
path: "/a/b/app.ts",
content: "let x = 1;"
};
const host = createServerHost([lib, f], { executingFilePath: "/a/Lib/tsc.js", useCaseSensitiveFileNames: true });
const projectService = createProjectService(host);
projectService.openClientFile(f.path);
projectService.checkNumberOfProjects({ inferredProjects: 1 });
projectService.inferredProjects[0].getLanguageService().getProgram();
});
});
}

View File

@@ -0,0 +1,55 @@
namespace ts.projectSystem {
describe("tsserver:: maxNodeModuleJsDepth for inferred projects", () => {
it("should be set to 2 if the project has js root files", () => {
const file1: File = {
path: "/a/b/file1.js",
content: `var t = require("test"); t.`
};
const moduleFile: File = {
path: "/a/b/node_modules/test/index.js",
content: `var v = 10; module.exports = v;`
};
const host = createServerHost([file1, moduleFile]);
const projectService = createProjectService(host);
projectService.openClientFile(file1.path);
let project = projectService.inferredProjects[0];
let options = project.getCompilationSettings();
assert.isTrue(options.maxNodeModuleJsDepth === 2);
// Assert the option sticks
projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES2016 });
project = projectService.inferredProjects[0];
options = project.getCompilationSettings();
assert.isTrue(options.maxNodeModuleJsDepth === 2);
});
it("should return to normal state when all js root files are removed from project", () => {
const file1 = {
path: "/a/file1.ts",
content: "let x =1;"
};
const file2 = {
path: "/a/file2.js",
content: "let x =1;"
};
const host = createServerHost([file1, file2, libFile]);
const projectService = createProjectService(host, { useSingleInferredProject: true });
projectService.openClientFile(file1.path);
checkNumberOfInferredProjects(projectService, 1);
let project = projectService.inferredProjects[0];
assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth);
projectService.openClientFile(file2.path);
project = projectService.inferredProjects[0];
assert.isTrue(project.getCompilationSettings().maxNodeModuleJsDepth === 2);
projectService.closeClientFile(file2.path);
project = projectService.inferredProjects[0];
assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth);
});
});
}

View File

@@ -0,0 +1,99 @@
namespace ts.projectSystem {
describe("tsserver:: with metadata in response", () => {
const metadata = "Extra Info";
function verifyOutput(host: TestServerHost, expectedResponse: protocol.Response) {
const output = host.getOutput().map(mapOutputToJson);
assert.deepEqual(output, [expectedResponse]);
host.clearOutput();
}
function verifyCommandWithMetadata<T extends server.protocol.Request, U = undefined>(session: TestSession, host: TestServerHost, command: Partial<T>, expectedResponseBody: U) {
command.seq = session.getSeq();
command.type = "request";
session.onMessage(JSON.stringify(command));
verifyOutput(host, expectedResponseBody ?
{ seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: true, body: expectedResponseBody, metadata } :
{ seq: 0, type: "response", command: command.command!, request_seq: command.seq, success: false, message: "No content available." }
);
}
const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { return this.prop; } }` };
const tsconfig: File = {
path: "/tsconfig.json",
content: JSON.stringify({
compilerOptions: { plugins: [{ name: "myplugin" }] }
})
};
function createHostWithPlugin(files: ReadonlyArray<File>) {
const host = createServerHost(files);
host.require = (_initialPath, moduleName) => {
assert.equal(moduleName, "myplugin");
return {
module: () => ({
create(info: server.PluginCreateInfo) {
const proxy = Harness.LanguageService.makeDefaultProxy(info);
proxy.getCompletionsAtPosition = (filename, position, options) => {
const result = info.languageService.getCompletionsAtPosition(filename, position, options);
if (result) {
result.metadata = metadata;
}
return result;
};
return proxy;
}
}),
error: undefined
};
};
return host;
}
describe("With completion requests", () => {
const completionRequestArgs: protocol.CompletionsRequestArgs = {
file: aTs.path,
line: 1,
offset: aTs.content.indexOf("this.") + 1 + "this.".length
};
const expectedCompletionEntries: ReadonlyArray<protocol.CompletionEntry> = [
{ name: "foo", kind: ScriptElementKind.memberFunctionElement, kindModifiers: "", sortText: "0" },
{ name: "prop", kind: ScriptElementKind.memberVariableElement, kindModifiers: "", sortText: "0" }
];
it("can pass through metadata when the command returns array", () => {
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest, ReadonlyArray<protocol.CompletionEntry>>(session, host, {
command: protocol.CommandTypes.Completions,
arguments: completionRequestArgs
}, expectedCompletionEntries);
});
it("can pass through metadata when the command returns object", () => {
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest, protocol.CompletionInfo>(session, host, {
command: protocol.CommandTypes.CompletionInfo,
arguments: completionRequestArgs
}, {
isGlobalCompletion: false,
isMemberCompletion: true,
isNewIdentifierLocation: false,
entries: expectedCompletionEntries
});
});
it("returns undefined correctly", () => {
const aTs: File = { path: "/a.ts", content: `class c { prop = "hello"; foo() { const x = 0; } }` };
const host = createHostWithPlugin([aTs, tsconfig]);
const session = createSession(host);
openFilesForSession([aTs], session);
verifyCommandWithMetadata<protocol.CompletionsRequest>(session, host, {
command: protocol.CommandTypes.Completions,
arguments: { file: aTs.path, line: 1, offset: aTs.content.indexOf("x") + 1 }
}, /*expectedResponseBody*/ undefined);
});
});
});
}

View File

@@ -0,0 +1,30 @@
namespace ts.projectSystem {
describe("tsserver:: navigate-to for javascript project", () => {
function containsNavToItem(items: protocol.NavtoItem[], itemName: string, itemKind: string) {
return find(items, item => item.name === itemName && item.kind === itemKind) !== undefined;
}
it("should not include type symbols", () => {
const file1: File = {
path: "/a/b/file1.js",
content: "function foo() {}"
};
const configFile: File = {
path: "/a/b/jsconfig.json",
content: "{}"
};
const host = createServerHost([file1, configFile, libFile]);
const session = createSession(host);
openFilesForSession([file1], session);
// Try to find some interface type defined in lib.d.ts
const libTypeNavToRequest = makeSessionRequest<protocol.NavtoRequestArgs>(CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path });
const items = session.executeCommand(libTypeNavToRequest).response as protocol.NavtoItem[];
assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`);
const localFunctionNavToRequst = makeSessionRequest<protocol.NavtoRequestArgs>(CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path });
const items2 = session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[];
assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`);
});
});
}

View File

@@ -0,0 +1,47 @@
namespace ts.projectSystem {
describe("tsserver:: occurence highlight on string", () => {
it("should be marked if only on string values", () => {
const file1: File = {
path: "/a/b/file1.ts",
content: `let t1 = "div";\nlet t2 = "div";\nlet t3 = { "div": 123 };\nlet t4 = t3["div"];`
};
const host = createServerHost([file1]);
const session = createSession(host);
const projectService = session.getProjectService();
projectService.openClientFile(file1.path);
{
const highlightRequest = makeSessionRequest<protocol.FileLocationRequestArgs>(
CommandNames.Occurrences,
{ file: file1.path, line: 1, offset: 11 }
);
const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[];
const firstOccurence = highlightResponse[0];
assert.isTrue(firstOccurence.isInString, "Highlights should be marked with isInString");
}
{
const highlightRequest = makeSessionRequest<protocol.FileLocationRequestArgs>(
CommandNames.Occurrences,
{ file: file1.path, line: 3, offset: 13 }
);
const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[];
assert.isTrue(highlightResponse.length === 2);
const firstOccurence = highlightResponse[0];
assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on property name");
}
{
const highlightRequest = makeSessionRequest<protocol.FileLocationRequestArgs>(
CommandNames.Occurrences,
{ file: file1.path, line: 4, offset: 14 }
);
const highlightResponse = session.executeCommand(highlightRequest).response as protocol.OccurrencesResponseItem[];
assert.isTrue(highlightResponse.length === 2);
const firstOccurence = highlightResponse[0];
assert.isUndefined(firstOccurence.isInString, "Highlights should not be marked with isInString if on indexer");
}
});
});
}

View File

@@ -0,0 +1,108 @@
namespace ts.projectSystem {
describe("tsserver:: Open-file", () => {
it("can be reloaded with empty content", () => {
const f = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const projectFileName = "externalProject";
const host = createServerHost([f]);
const projectService = createProjectService(host);
// create a project
projectService.openExternalProject({ projectFileName, rootFiles: [toExternalFile(f.path)], options: {} });
projectService.checkNumberOfProjects({ externalProjects: 1 });
const p = projectService.externalProjects[0];
// force to load the content of the file
p.updateGraph();
const scriptInfo = p.getScriptInfo(f.path)!;
checkSnapLength(scriptInfo.getSnapshot(), f.content.length);
// open project and replace its content with empty string
projectService.openClientFile(f.path, "");
checkSnapLength(scriptInfo.getSnapshot(), 0);
});
function checkSnapLength(snap: IScriptSnapshot, expectedLength: number) {
assert.equal(snap.getLength(), expectedLength, "Incorrect snapshot size");
}
function verifyOpenFileWorks(useCaseSensitiveFileNames: boolean) {
const file1: File = {
path: "/a/b/src/app.ts",
content: "let x = 10;"
};
const file2: File = {
path: "/a/B/lib/module2.ts",
content: "let z = 10;"
};
const configFile: File = {
path: "/a/b/tsconfig.json",
content: ""
};
const configFile2: File = {
path: "/a/tsconfig.json",
content: ""
};
const host = createServerHost([file1, file2, configFile, configFile2], {
useCaseSensitiveFileNames
});
const service = createProjectService(host);
// Open file1 -> configFile
verifyConfigFileName(file1, "/a", configFile);
verifyConfigFileName(file1, "/a/b", configFile);
verifyConfigFileName(file1, "/a/B", configFile);
// Open file2 use root "/a/b"
verifyConfigFileName(file2, "/a", useCaseSensitiveFileNames ? configFile2 : configFile);
verifyConfigFileName(file2, "/a/b", useCaseSensitiveFileNames ? configFile2 : configFile);
verifyConfigFileName(file2, "/a/B", useCaseSensitiveFileNames ? undefined : configFile);
function verifyConfigFileName(file: File, projectRoot: string, expectedConfigFile: File | undefined) {
const { configFileName } = service.openClientFile(file.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectRoot);
assert.equal(configFileName, expectedConfigFile && expectedConfigFile.path);
service.closeClientFile(file.path);
}
}
it("works when project root is used with case-sensitive system", () => {
verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ true);
});
it("works when project root is used with case-insensitive system", () => {
verifyOpenFileWorks(/*useCaseSensitiveFileNames*/ false);
});
it("uses existing project even if project refresh is pending", () => {
const projectFolder = "/user/someuser/projects/myproject";
const aFile: File = {
path: `${projectFolder}/src/a.ts`,
content: "export const x = 0;"
};
const configFile: File = {
path: `${projectFolder}/tsconfig.json`,
content: "{}"
};
const files = [aFile, configFile, libFile];
const host = createServerHost(files);
const service = createProjectService(host);
service.openClientFile(aFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder);
verifyProject();
const bFile: File = {
path: `${projectFolder}/src/b.ts`,
content: `export {}; declare module "./a" { export const y: number; }`
};
files.push(bFile);
host.reloadFS(files);
service.openClientFile(bFile.path, /*fileContent*/ undefined, ScriptKind.TS, projectFolder);
verifyProject();
function verifyProject() {
assert.isDefined(service.configuredProjects.get(configFile.path));
const project = service.configuredProjects.get(configFile.path)!;
checkProjectActualFiles(project, files.map(f => f.path));
}
});
});
}

View File

@@ -0,0 +1,658 @@
namespace ts.projectSystem {
describe("tsserver:: with project references and tsbuild", () => {
function createHost(files: ReadonlyArray<File>, rootNames: ReadonlyArray<string>) {
const host = createServerHost(files);
// ts build should succeed
const solutionBuilder = tscWatch.createSolutionBuilder(host, rootNames, {});
solutionBuilder.buildAllProjects();
assert.equal(host.getOutput().length, 0);
return host;
}
describe("with container project", () => {
function getProjectFiles(project: string): [File, File] {
return [
TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json"),
TestFSWithWatch.getTsBuildProjectFile(project, "index.ts"),
];
}
const project = "container";
const containerLib = getProjectFiles("container/lib");
const containerExec = getProjectFiles("container/exec");
const containerCompositeExec = getProjectFiles("container/compositeExec");
const containerConfig = TestFSWithWatch.getTsBuildProjectFile(project, "tsconfig.json");
const files = [libFile, ...containerLib, ...containerExec, ...containerCompositeExec, containerConfig];
it("does not error on container only project", () => {
const host = createHost(files, [containerConfig.path]);
// Open external project for the folder
const session = createSession(host);
const service = session.getProjectService();
service.openExternalProjects([{
projectFileName: TestFSWithWatch.getTsBuildProjectFilePath(project, project),
rootFiles: files.map(f => ({ fileName: f.path })),
options: {}
}]);
checkNumberOfProjects(service, { configuredProjects: 4 });
files.forEach(f => {
const args: protocol.FileRequestArgs = {
file: f.path,
projectFileName: endsWith(f.path, "tsconfig.json") ? f.path : undefined
};
const syntaxDiagnostics = session.executeCommandSeq<protocol.SyntacticDiagnosticsSyncRequest>({
command: protocol.CommandTypes.SyntacticDiagnosticsSync,
arguments: args
}).response;
assert.deepEqual(syntaxDiagnostics, []);
const semanticDiagnostics = session.executeCommandSeq<protocol.SemanticDiagnosticsSyncRequest>({
command: protocol.CommandTypes.SemanticDiagnosticsSync,
arguments: args
}).response;
assert.deepEqual(semanticDiagnostics, []);
});
const containerProject = service.configuredProjects.get(containerConfig.path)!;
checkProjectActualFiles(containerProject, [containerConfig.path]);
const optionsDiagnostics = session.executeCommandSeq<protocol.CompilerOptionsDiagnosticsRequest>({
command: protocol.CommandTypes.CompilerOptionsDiagnosticsFull,
arguments: { projectFileName: containerProject.projectName }
}).response;
assert.deepEqual(optionsDiagnostics, []);
});
it("can successfully find references with --out options", () => {
const host = createHost(files, [containerConfig.path]);
const session = createSession(host);
openFilesForSession([containerCompositeExec[1]], session);
const service = session.getProjectService();
checkNumberOfProjects(service, { configuredProjects: 1 });
const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst");
const response = session.executeCommandSeq<protocol.RenameRequest>({
command: protocol.CommandTypes.Rename,
arguments: {
file: containerCompositeExec[1].path,
...locationOfMyConst
}
}).response as protocol.RenameResponseBody;
const myConstLen = "myConst".length;
const locationOfMyConstInLib = protocolLocationFromSubstring(containerLib[1].content, "myConst");
assert.deepEqual(response.locs, [
{ file: containerCompositeExec[1].path, locs: [{ start: locationOfMyConst, end: { line: locationOfMyConst.line, offset: locationOfMyConst.offset + myConstLen } }] },
{ file: containerLib[1].path, locs: [{ start: locationOfMyConstInLib, end: { line: locationOfMyConstInLib.line, offset: locationOfMyConstInLib.offset + myConstLen } }] }
]);
});
});
describe("with main and depedency project", () => {
const projectLocation = "/user/username/projects/myproject";
const dependecyLocation = `${projectLocation}/dependency`;
const mainLocation = `${projectLocation}/main`;
const dependencyTs: File = {
path: `${dependecyLocation}/FnS.ts`,
content: `export function fn1() { }
export function fn2() { }
export function fn3() { }
export function fn4() { }
export function fn5() { }
`
};
const dependencyConfig: File = {
path: `${dependecyLocation}/tsconfig.json`,
content: JSON.stringify({ compilerOptions: { composite: true, declarationMap: true } })
};
const mainTs: File = {
path: `${mainLocation}/main.ts`,
content: `import {
fn1,
fn2,
fn3,
fn4,
fn5
} from '../dependency/fns'
fn1();
fn2();
fn3();
fn4();
fn5();
`
};
const mainConfig: File = {
path: `${mainLocation}/tsconfig.json`,
content: JSON.stringify({
compilerOptions: { composite: true, declarationMap: true },
references: [{ path: "../dependency" }]
})
};
const randomFile: File = {
path: `${projectLocation}/random/random.ts`,
content: "let a = 10;"
};
const randomConfig: File = {
path: `${projectLocation}/random/tsconfig.json`,
content: "{}"
};
const dtsLocation = `${dependecyLocation}/FnS.d.ts`;
const dtsPath = dtsLocation.toLowerCase() as Path;
const dtsMapLocation = `${dtsLocation}.map`;
const dtsMapPath = dtsMapLocation.toLowerCase() as Path;
const files = [dependencyTs, dependencyConfig, mainTs, mainConfig, libFile, randomFile, randomConfig];
function verifyScriptInfos(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray<string>, closedInfos: ReadonlyArray<string>, otherWatchedFiles: ReadonlyArray<string>) {
checkScriptInfos(session.getProjectService(), openInfos.concat(closedInfos));
checkWatchedFiles(host, closedInfos.concat(otherWatchedFiles).map(f => f.toLowerCase()));
}
function verifyInfosWithRandom(session: TestSession, host: TestServerHost, openInfos: ReadonlyArray<string>, closedInfos: ReadonlyArray<string>, otherWatchedFiles: ReadonlyArray<string>) {
verifyScriptInfos(session, host, openInfos.concat(randomFile.path), closedInfos, otherWatchedFiles.concat(randomConfig.path));
}
function verifyOnlyRandomInfos(session: TestSession, host: TestServerHost) {
verifyScriptInfos(session, host, [randomFile.path], [libFile.path], [randomConfig.path]);
}
// Returns request and expected Response, expected response when no map file
interface SessionAction<Req = protocol.Request, Response = {}> {
reqName: string;
request: Partial<Req>;
expectedResponse: Response;
expectedResponseNoMap?: Response;
expectedResponseNoDts?: Response;
}
function gotoDefintinionFromMainTs(fn: number): SessionAction<protocol.DefinitionAndBoundSpanRequest, protocol.DefinitionInfoAndBoundSpan> {
const textSpan = usageSpan(fn);
const definition: protocol.FileSpan = { file: dependencyTs.path, ...definitionSpan(fn) };
const declareSpaceLength = "declare ".length;
return {
reqName: "goToDef",
request: {
command: protocol.CommandTypes.DefinitionAndBoundSpan,
arguments: { file: mainTs.path, ...textSpan.start }
},
expectedResponse: {
// To dependency
definitions: [definition],
textSpan
},
expectedResponseNoMap: {
// To the dts
definitions: [{ file: dtsPath, start: { line: fn, offset: definition.start.offset + declareSpaceLength }, end: { line: fn, offset: definition.end.offset + declareSpaceLength } }],
textSpan
},
expectedResponseNoDts: {
// To import declaration
definitions: [{ file: mainTs.path, ...importSpan(fn) }],
textSpan
}
};
}
function definitionSpan(fn: number): protocol.TextSpan {
return { start: { line: fn, offset: 17 }, end: { line: fn, offset: 20 } };
}
function importSpan(fn: number): protocol.TextSpan {
return { start: { line: fn + 1, offset: 5 }, end: { line: fn + 1, offset: 8 } };
}
function usageSpan(fn: number): protocol.TextSpan {
return { start: { line: fn + 8, offset: 1 }, end: { line: fn + 8, offset: 4 } };
}
function renameFromDependencyTs(fn: number): SessionAction<protocol.RenameRequest, protocol.RenameResponseBody> {
const triggerSpan = definitionSpan(fn);
return {
reqName: "rename",
request: {
command: protocol.CommandTypes.Rename,
arguments: { file: dependencyTs.path, ...triggerSpan.start }
},
expectedResponse: {
info: {
canRename: true,
fileToRename: undefined,
displayName: `fn${fn}`,
fullDisplayName: `"${dependecyLocation}/FnS".fn${fn}`,
kind: ScriptElementKind.functionElement,
kindModifiers: "export",
triggerSpan
},
locs: [
{ file: dependencyTs.path, locs: [triggerSpan] }
]
}
};
}
function renameFromDependencyTsWithBothProjectsOpen(fn: number): SessionAction<protocol.RenameRequest, protocol.RenameResponseBody> {
const { reqName, request, expectedResponse } = renameFromDependencyTs(fn);
const { info, locs } = expectedResponse;
return {
reqName,
request,
expectedResponse: {
info,
locs: [
locs[0],
{
file: mainTs.path,
locs: [
importSpan(fn),
usageSpan(fn)
]
}
]
},
// Only dependency result
expectedResponseNoMap: expectedResponse,
expectedResponseNoDts: expectedResponse
};
}
// Returns request and expected Response
type SessionActionGetter<Req = protocol.Request, Response = {}> = (fn: number) => SessionAction<Req, Response>;
// Open File, expectedProjectActualFiles, actionGetter, openFileLastLine
interface DocumentPositionMapperVerifier {
openFile: File;
expectedProjectActualFiles: ReadonlyArray<string>;
actionGetter: SessionActionGetter;
openFileLastLine: number;
}
function verifyDocumentPositionMapperUpdates(
mainScenario: string,
verifier: ReadonlyArray<DocumentPositionMapperVerifier>,
closedInfos: ReadonlyArray<string>) {
const openFiles = verifier.map(v => v.openFile);
const expectedProjectActualFiles = verifier.map(v => v.expectedProjectActualFiles);
const actionGetters = verifier.map(v => v.actionGetter);
const openFileLastLines = verifier.map(v => v.openFileLastLine);
const configFiles = openFiles.map(openFile => `${getDirectoryPath(openFile.path)}/tsconfig.json`);
const openInfos = openFiles.map(f => f.path);
// When usage and dependency are used, dependency config is part of closedInfo so ignore
const otherWatchedFiles = verifier.length > 1 ? [configFiles[0]] : configFiles;
function openTsFile(onHostCreate?: (host: TestServerHost) => void) {
const host = createHost(files, [mainConfig.path]);
if (onHostCreate) {
onHostCreate(host);
}
const session = createSession(host);
openFilesForSession([...openFiles, randomFile], session);
return { host, session };
}
function checkProject(session: TestSession, noDts?: true) {
const service = session.getProjectService();
checkNumberOfProjects(service, { configuredProjects: 1 + verifier.length });
configFiles.forEach((configFile, index) => {
checkProjectActualFiles(
service.configuredProjects.get(configFile)!,
noDts ?
expectedProjectActualFiles[index].filter(f => f.toLowerCase() !== dtsPath) :
expectedProjectActualFiles[index]
);
});
}
function verifyInfos(session: TestSession, host: TestServerHost) {
verifyInfosWithRandom(session, host, openInfos, closedInfos, otherWatchedFiles);
}
function verifyInfosWhenNoMapFile(session: TestSession, host: TestServerHost, dependencyTsOK?: true) {
const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined);
verifyInfosWithRandom(
session,
host,
openInfos,
closedInfos.filter(f => f !== dtsMapClosedInfo && (dependencyTsOK || f !== dependencyTs.path)),
dtsMapClosedInfo ? otherWatchedFiles.concat(dtsMapClosedInfo) : otherWatchedFiles
);
}
function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) {
const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined);
const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined);
verifyInfosWithRandom(
session,
host,
openInfos,
closedInfos.filter(f => (dependencyTsAndMapOk || f !== dtsMapClosedInfo) && f !== dtsClosedInfo && (dependencyTsAndMapOk || f !== dependencyTs.path)),
// When project actual file contains dts, it needs to be watched
dtsClosedInfo && expectedProjectActualFiles.some(expectedProjectActualFiles => expectedProjectActualFiles.some(f => f.toLowerCase() === dtsPath)) ?
otherWatchedFiles.concat(dtsClosedInfo) :
otherWatchedFiles
);
}
function verifyDocumentPositionMapper(session: TestSession, dependencyMap: server.ScriptInfo, documentPositionMapper: server.ScriptInfo["documentPositionMapper"], notEqual?: true) {
assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap);
if (notEqual) {
assert.notStrictEqual(dependencyMap.documentPositionMapper, documentPositionMapper);
}
else {
assert.strictEqual(dependencyMap.documentPositionMapper, documentPositionMapper);
}
}
function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) {
const { reqName, request, expectedResponse, expectedResponseNoMap, expectedResponseNoDts } = actionGetter(fn);
const { response } = session.executeCommandSeq(request);
return { reqName, response, expectedResponse, expectedResponseNoMap, expectedResponseNoDts };
}
function firstAction(session: TestSession) {
actionGetters.forEach(actionGetter => action(actionGetter, 1, session));
}
function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType<typeof action>, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) {
// action
let isFirst = true;
for (const actionGetter of actionGetters) {
for (let fn = 1; fn <= 5; fn++) {
const result = action(actionGetter, fn, session);
const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath);
if (dtsAbsent) {
assert.isUndefined(dtsInfo);
}
else {
assert.isDefined(dtsInfo);
}
verifyAction(result, dtsInfo, isFirst);
isFirst = false;
}
}
}
function verifyAllFnAction(
session: TestSession,
host: TestServerHost,
firstDocumentPositionMapperNotEquals?: true,
dependencyMap?: server.ScriptInfo,
documentPositionMapper?: server.ScriptInfo["documentPositionMapper"]
) {
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse }, dtsInfo, isFirst) => {
assert.deepEqual(response, expectedResponse, `Failed on ${reqName}`);
verifyInfos(session, host);
assert.equal(dtsInfo!.sourceMapFilePath, dtsMapPath);
if (isFirst) {
if (dependencyMap) {
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, firstDocumentPositionMapperNotEquals);
documentPositionMapper = dependencyMap.documentPositionMapper;
}
else {
dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!;
documentPositionMapper = dependencyMap.documentPositionMapper;
}
}
else {
verifyDocumentPositionMapper(session, dependencyMap!, documentPositionMapper);
}
});
return { dependencyMap: dependencyMap!, documentPositionMapper };
}
function verifyAllFnActionWithNoMap(
session: TestSession,
host: TestServerHost,
dependencyTsOK?: true
) {
let sourceMapFilePath: server.ScriptInfo["sourceMapFilePath"];
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoMap }, dtsInfo, isFirst) => {
assert.deepEqual(response, expectedResponseNoMap || expectedResponse, `Failed on ${reqName}`);
verifyInfosWhenNoMapFile(session, host, dependencyTsOK);
assert.isUndefined(session.getProjectService().filenameToScriptInfo.get(dtsMapPath));
if (isFirst) {
assert.isNotString(dtsInfo!.sourceMapFilePath);
assert.isNotFalse(dtsInfo!.sourceMapFilePath);
assert.isDefined(dtsInfo!.sourceMapFilePath);
sourceMapFilePath = dtsInfo!.sourceMapFilePath;
}
else {
assert.equal(dtsInfo!.sourceMapFilePath, sourceMapFilePath);
}
});
return sourceMapFilePath;
}
function verifyAllFnActionWithNoDts(
session: TestSession,
host: TestServerHost,
dependencyTsAndMapOk?: true
) {
// action
verifyAllFnActionWorker(session, ({ reqName, response, expectedResponse, expectedResponseNoDts }) => {
assert.deepEqual(response, expectedResponseNoDts || expectedResponse, `Failed on ${reqName}`);
verifyInfosWhenNoDtsFile(session, host, dependencyTsAndMapOk);
}, /*dtsAbsent*/ true);
}
function verifyScenarioWithChangesWorker(
change: (host: TestServerHost, session: TestSession) => void,
afterActionDocumentPositionMapperNotEquals: true | undefined,
timeoutBeforeAction: boolean
) {
const { host, session } = openTsFile();
// Create DocumentPositionMapper
firstAction(session);
const dependencyMap = session.getProjectService().filenameToScriptInfo.get(dtsMapPath)!;
const documentPositionMapper = dependencyMap.documentPositionMapper;
// change
change(host, session);
if (timeoutBeforeAction) {
host.runQueuedTimeoutCallbacks();
checkProject(session);
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper);
}
// action
verifyAllFnAction(session, host, afterActionDocumentPositionMapperNotEquals, dependencyMap, documentPositionMapper);
}
function verifyScenarioWithChanges(
scenarioName: string,
change: (host: TestServerHost, session: TestSession) => void,
afterActionDocumentPositionMapperNotEquals?: true
) {
describe(scenarioName, () => {
it("when timeout occurs before request", () => {
verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ true);
});
it("when timeout does not occur before request", () => {
verifyScenarioWithChangesWorker(change, afterActionDocumentPositionMapperNotEquals, /*timeoutBeforeAction*/ false);
});
});
}
function verifyMainScenarioAndScriptInfoCollection(session: TestSession, host: TestServerHost) {
// Main scenario action
const { dependencyMap, documentPositionMapper } = verifyAllFnAction(session, host);
checkProject(session);
verifyInfos(session, host);
// Collecting at this point retains dependency.d.ts and map
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfos(session, host);
verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyMainScenarioAndScriptInfoCollectionWithNoMap(session: TestSession, host: TestServerHost, dependencyTsOKInScenario?: true) {
// Main scenario action
verifyAllFnActionWithNoMap(session, host, dependencyTsOKInScenario);
// Collecting at this point retains dependency.d.ts and map watcher
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfosWhenNoMapFile(session, host);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyMainScenarioAndScriptInfoCollectionWithNoDts(session: TestSession, host: TestServerHost, dependencyTsAndMapOk?: true) {
// Main scenario action
verifyAllFnActionWithNoDts(session, host, dependencyTsAndMapOk);
// Collecting at this point retains dependency.d.ts and map watcher
closeFilesForSession([randomFile], session);
openFilesForSession([randomFile], session);
verifyInfosWhenNoDtsFile(session, host);
// Closing open file, removes dependencies too
closeFilesForSession([...openFiles, randomFile], session);
openFilesForSession([randomFile], session);
verifyOnlyRandomInfos(session, host);
}
function verifyScenarioWhenFileNotPresent(
scenarioName: string,
fileLocation: string,
verifyScenarioAndScriptInfoCollection: (session: TestSession, host: TestServerHost, dependencyTsOk?: true) => void,
noDts?: true
) {
describe(scenarioName, () => {
it(mainScenario, () => {
const { host, session } = openTsFile(host => host.deleteFile(fileLocation));
checkProject(session, noDts);
verifyScenarioAndScriptInfoCollection(session, host);
});
it("when file is created", () => {
let fileContents: string | undefined;
const { host, session } = openTsFile(host => {
fileContents = host.readFile(fileLocation);
host.deleteFile(fileLocation);
});
firstAction(session);
host.writeFile(fileLocation, fileContents!);
verifyMainScenarioAndScriptInfoCollection(session, host);
});
it("when file is deleted", () => {
const { host, session } = openTsFile();
firstAction(session);
// The dependency file is deleted when orphan files are collected
host.deleteFile(fileLocation);
verifyScenarioAndScriptInfoCollection(session, host, /*dependencyTsOk*/ true);
});
});
}
it(mainScenario, () => {
const { host, session } = openTsFile();
checkProject(session);
verifyMainScenarioAndScriptInfoCollection(session, host);
});
// Edit
verifyScenarioWithChanges(
"when usage file changes, document position mapper doesnt change",
(_host, session) => openFiles.forEach(
(openFile, index) => session.executeCommandSeq<protocol.ChangeRequest>({
command: protocol.CommandTypes.Change,
arguments: { file: openFile.path, line: openFileLastLines[index], offset: 1, endLine: openFileLastLines[index], endOffset: 1, insertString: "const x = 10;" }
})
)
);
// Edit dts to add new fn
verifyScenarioWithChanges(
"when dependency .d.ts changes, document position mapper doesnt change",
host => host.writeFile(
dtsLocation,
host.readFile(dtsLocation)!.replace(
"//# sourceMappingURL=FnS.d.ts.map",
`export declare function fn6(): void;
//# sourceMappingURL=FnS.d.ts.map`
)
)
);
// Edit map file to represent added new line
verifyScenarioWithChanges(
"when dependency file's map changes",
host => host.writeFile(
dtsMapLocation,
`{"version":3,"file":"FnS.d.ts","sourceRoot":"","sources":["FnS.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,wBAAgB,GAAG,SAAM;AACzB,eAAO,MAAM,CAAC,KAAK,CAAC"}`
),
/*afterActionDocumentPositionMapperNotEquals*/ true
);
verifyScenarioWhenFileNotPresent(
"when map file is not present",
dtsMapLocation,
verifyMainScenarioAndScriptInfoCollectionWithNoMap
);
verifyScenarioWhenFileNotPresent(
"when .d.ts file is not present",
dtsLocation,
verifyMainScenarioAndScriptInfoCollectionWithNoDts,
/*noDts*/ true
);
}
const usageVerifier: DocumentPositionMapperVerifier = {
openFile: mainTs,
expectedProjectActualFiles: [mainTs.path, libFile.path, mainConfig.path, dtsPath],
actionGetter: gotoDefintinionFromMainTs,
openFileLastLine: 14
};
describe("from project that uses dependency", () => {
const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"can go to definition correctly",
[usageVerifier],
closedInfos
);
});
const definingVerifier: DocumentPositionMapperVerifier = {
openFile: dependencyTs,
expectedProjectActualFiles: [dependencyTs.path, libFile.path, dependencyConfig.path],
actionGetter: renameFromDependencyTs,
openFileLastLine: 6
};
describe("from defining project", () => {
const closedInfos = [libFile.path, dtsLocation, dtsMapLocation];
verifyDocumentPositionMapperUpdates(
"rename locations from dependency",
[definingVerifier],
closedInfos
);
});
describe("when opening depedency and usage project", () => {
const closedInfos = [libFile.path, dtsPath, dtsMapLocation, dependencyConfig.path];
verifyDocumentPositionMapperUpdates(
"goto Definition in usage and rename locations from defining project",
[usageVerifier, { ...definingVerifier, actionGetter: renameFromDependencyTsWithBothProjectsOpen }],
closedInfos
);
});
});
});
}

View File

@@ -0,0 +1,120 @@
namespace ts.projectSystem {
describe("tsserver:: refactors", () => {
it("use formatting options", () => {
const file = {
path: "/a.ts",
content: "function f() {\n 1;\n}",
};
const host = createServerHost([file]);
const session = createSession(host);
openFilesForSession([file], session);
const response0 = session.executeCommandSeq<server.protocol.ConfigureRequest>({
command: server.protocol.CommandTypes.Configure,
arguments: {
formatOptions: {
indentSize: 2,
},
},
}).response;
assert.deepEqual(response0, /*expected*/ undefined);
const response1 = session.executeCommandSeq<server.protocol.GetEditsForRefactorRequest>({
command: server.protocol.CommandTypes.GetEditsForRefactor,
arguments: {
refactor: "Extract Symbol",
action: "function_scope_1",
file: "/a.ts",
startLine: 2,
startOffset: 3,
endLine: 2,
endOffset: 4,
},
}).response;
assert.deepEqual(response1, {
edits: [
{
fileName: "/a.ts",
textChanges: [
{
start: { line: 2, offset: 3 },
end: { line: 2, offset: 5 },
newText: "newFunction();",
},
{
start: { line: 3, offset: 2 },
end: { line: 3, offset: 2 },
newText: "\n\nfunction newFunction() {\n 1;\n}\n",
},
]
}
],
renameFilename: "/a.ts",
renameLocation: { line: 2, offset: 3 },
});
});
it("handles text changes in tsconfig.json", () => {
const aTs = {
path: "/a.ts",
content: "export const a = 0;",
};
const tsconfig = {
path: "/tsconfig.json",
content: '{ "files": ["./a.ts"] }',
};
const session = createSession(createServerHost([aTs, tsconfig]));
openFilesForSession([aTs], session);
const response1 = session.executeCommandSeq<server.protocol.GetEditsForRefactorRequest>({
command: server.protocol.CommandTypes.GetEditsForRefactor,
arguments: {
refactor: "Move to a new file",
action: "Move to a new file",
file: "/a.ts",
startLine: 1,
startOffset: 1,
endLine: 1,
endOffset: 20,
},
}).response;
assert.deepEqual(response1, {
edits: [
{
fileName: "/a.ts",
textChanges: [
{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 20 },
newText: "",
},
],
},
{
fileName: "/tsconfig.json",
textChanges: [
{
start: { line: 1, offset: 21 },
end: { line: 1, offset: 21 },
newText: ", \"./a.1.ts\"",
},
],
},
{
fileName: "/a.1.ts",
textChanges: [
{
start: { line: 0, offset: 0 },
end: { line: 0, offset: 0 },
newText: "export const a = 0;\n",
},
],
}
],
renameFilename: undefined,
renameLocation: undefined,
});
});
});
}

View File

@@ -0,0 +1,53 @@
namespace ts.projectSystem {
describe("tsserver:: rename", () => {
it("works with fileToRename", () => {
const aTs: File = { path: "/a.ts", content: "export const a = 0;" };
const bTs: File = { path: "/b.ts", content: 'import { a } from "./a";' };
const session = createSession(createServerHost([aTs, bTs]));
openFilesForSession([bTs], session);
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(bTs, 'a";'));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
fileToRename: aTs.path,
displayName: aTs.path,
fullDisplayName: aTs.path,
kind: ScriptElementKind.moduleElement,
kindModifiers: "",
triggerSpan: protocolTextSpanFromSubstring(bTs.content, "a", { index: 1 }),
},
locs: [{ file: bTs.path, locs: [protocolRenameSpanFromSubstring(bTs.content, "./a")] }],
});
});
it("works with prefixText and suffixText", () => {
const aTs: File = { path: "/a.ts", content: "const x = 0; const o = { x };" };
const session = createSession(createServerHost([aTs]));
openFilesForSession([aTs], session);
const response = executeSessionRequest<protocol.RenameRequest, protocol.RenameResponse>(session, protocol.CommandTypes.Rename, protocolFileLocationFromSubstring(aTs, "x"));
assert.deepEqual<protocol.RenameResponseBody | undefined>(response, {
info: {
canRename: true,
fileToRename: undefined,
displayName: "x",
fullDisplayName: "x",
kind: ScriptElementKind.constElement,
kindModifiers: ScriptElementKindModifier.none,
triggerSpan: protocolTextSpanFromSubstring(aTs.content, "x"),
},
locs: [
{
file: aTs.path,
locs: [
protocolRenameSpanFromSubstring(aTs.content, "x"),
protocolRenameSpanFromSubstring(aTs.content, "x", { index: 1 }, { prefixText: "x: " }),
],
},
],
});
});
});
}

View File

@@ -5,7 +5,7 @@ namespace ts.projectSystem {
return resolutionTrace;
}
describe("resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => {
describe("tsserver:: resolutionCache:: tsserverProjectSystem extra resolution pass in lshost", () => {
it("can load typings that are proper modules", () => {
const file1 = {
path: "/a/b/app.js",
@@ -46,7 +46,7 @@ namespace ts.projectSystem {
});
});
describe("resolutionCache:: tsserverProjectSystem watching @types", () => {
describe("tsserver:: resolutionCache:: tsserverProjectSystem watching @types", () => {
it("works correctly when typings are added or removed", () => {
const f1 = {
path: "/a/b/app.ts",
@@ -92,7 +92,7 @@ namespace ts.projectSystem {
});
});
describe("resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => {
describe("tsserver:: resolutionCache:: tsserverProjectSystem add the missing module file for inferred project", () => {
it("should remove the `module not found` error", () => {
const moduleFile = {
path: "/a/b/moduleFile.ts",
@@ -358,7 +358,7 @@ namespace ts.projectSystem {
});
});
describe("resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => {
describe("tsserver:: resolutionCache:: tsserverProjectSystem rename a module file and rename back", () => {
it("should restore the states for inferred projects", () => {
const moduleFile = {
path: "/a/b/moduleFile.ts",
@@ -493,7 +493,7 @@ namespace ts.projectSystem {
});
});
describe("resolutionCache:: tsserverProjectSystem module resolution caching", () => {
describe("tsserver:: resolutionCache:: tsserverProjectSystem module resolution caching", () => {
const projectLocation = "/user/username/projects/myproject";
const configFile: File = {
path: `${projectLocation}/tsconfig.json`,

View File

@@ -0,0 +1,98 @@
namespace ts.projectSystem {
describe("tsserver:: syntax operations", () => {
function navBarFull(session: TestSession, file: File) {
return JSON.stringify(session.executeCommandSeq<protocol.FileRequest>({
command: protocol.CommandTypes.NavBarFull,
arguments: { file: file.path }
}).response);
}
function openFile(session: TestSession, file: File) {
session.executeCommandSeq<protocol.OpenRequest>({
command: protocol.CommandTypes.Open,
arguments: { file: file.path, fileContent: file.content }
});
}
it("works when file is removed and added with different content", () => {
const projectRoot = "/user/username/projects/myproject";
const app: File = {
path: `${projectRoot}/app.ts`,
content: "console.log('Hello world');"
};
const unitTest1: File = {
path: `${projectRoot}/unitTest1.ts`,
content: `import assert = require('assert');
describe("Test Suite 1", () => {
it("Test A", () => {
assert.ok(true, "This shouldn't fail");
});
it("Test B", () => {
assert.ok(1 === 1, "This shouldn't fail");
assert.ok(false, "This should fail");
});
});`
};
const tsconfig: File = {
path: `${projectRoot}/tsconfig.json`,
content: "{}"
};
const files = [app, libFile, tsconfig];
const host = createServerHost(files);
const session = createSession(host);
const service = session.getProjectService();
openFile(session, app);
checkNumberOfProjects(service, { configuredProjects: 1 });
const project = service.configuredProjects.get(tsconfig.path)!;
const expectedFilesWithoutUnitTest1 = files.map(f => f.path);
checkProjectActualFiles(project, expectedFilesWithoutUnitTest1);
host.writeFile(unitTest1.path, unitTest1.content);
host.runQueuedTimeoutCallbacks();
const expectedFilesWithUnitTest1 = expectedFilesWithoutUnitTest1.concat(unitTest1.path);
checkProjectActualFiles(project, expectedFilesWithUnitTest1);
openFile(session, unitTest1);
checkProjectActualFiles(project, expectedFilesWithUnitTest1);
const navBarResultUnitTest1 = navBarFull(session, unitTest1);
host.deleteFile(unitTest1.path);
host.checkTimeoutQueueLengthAndRun(2);
checkProjectActualFiles(project, expectedFilesWithoutUnitTest1);
session.executeCommandSeq<protocol.CloseRequest>({
command: protocol.CommandTypes.Close,
arguments: { file: unitTest1.path }
});
checkProjectActualFiles(project, expectedFilesWithoutUnitTest1);
const unitTest1WithChangedContent: File = {
path: unitTest1.path,
content: `import assert = require('assert');
export function Test1() {
assert.ok(true, "This shouldn't fail");
};
export function Test2() {
assert.ok(1 === 1, "This shouldn't fail");
assert.ok(false, "This should fail");
};`
};
host.writeFile(unitTest1.path, unitTest1WithChangedContent.content);
host.runQueuedTimeoutCallbacks();
checkProjectActualFiles(project, expectedFilesWithUnitTest1);
openFile(session, unitTest1WithChangedContent);
checkProjectActualFiles(project, expectedFilesWithUnitTest1);
const sourceFile = project.getLanguageService().getNonBoundSourceFile(unitTest1WithChangedContent.path);
assert.strictEqual(sourceFile.text, unitTest1WithChangedContent.content);
const navBarResultUnitTest1WithChangedContent = navBarFull(session, unitTest1WithChangedContent);
assert.notStrictEqual(navBarResultUnitTest1WithChangedContent, navBarResultUnitTest1, "With changes in contents of unitTest file, we should see changed naviagation bar item result");
});
});
}

View File

@@ -1,5 +1,5 @@
namespace ts.projectSystem {
describe("project telemetry", () => {
describe("tsserver:: project telemetry", () => {
it("does nothing for inferred project", () => {
const file = makeFile("/a.js");
const et = new TestServerEventManager([file]);

View File

@@ -1,5 +1,5 @@
namespace ts.textStorage {
describe("Text storage", () => {
describe("tsserver:: Text storage", () => {
const f = {
path: "/a/app.ts",
content: `

View File

@@ -0,0 +1,52 @@
namespace ts.projectSystem {
describe("tsserver:: autoDiscovery", () => {
it("does not depend on extension", () => {
const file1 = {
path: "/a/b/app.html",
content: ""
};
const file2 = {
path: "/a/b/app.d.ts",
content: ""
};
const host = createServerHost([file1, file2]);
const projectService = createProjectService(host);
projectService.openExternalProject({
projectFileName: "/a/b/proj.csproj",
rootFiles: [toExternalFile(file2.path), { fileName: file1.path, hasMixedContent: true, scriptKind: ScriptKind.JS }],
options: {}
});
projectService.checkNumberOfProjects({ externalProjects: 1 });
const typeAcquisition = projectService.externalProjects[0].getTypeAcquisition();
assert.isTrue(typeAcquisition.enable, "Typine acquisition should be enabled");
});
});
describe("tsserver:: prefer typings to js", () => {
it("during second resolution pass", () => {
const typingsCacheLocation = "/a/typings";
const f1 = {
path: "/a/b/app.js",
content: "var x = require('bar')"
};
const barjs = {
path: "/a/b/node_modules/bar/index.js",
content: "export let x = 1"
};
const barTypings = {
path: `${typingsCacheLocation}/node_modules/@types/bar/index.d.ts`,
content: "export let y: number"
};
const config = {
path: "/a/b/jsconfig.json",
content: JSON.stringify({ compilerOptions: { allowJs: true }, exclude: ["node_modules"] })
};
const host = createServerHost([f1, barjs, barTypings, config]);
const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller(typingsCacheLocation, /*throttleLimit*/ 5, host) });
projectService.openClientFile(f1.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, barTypings.path, config.path]);
});
});
}

View File

@@ -0,0 +1,87 @@
namespace ts.projectSystem {
describe("tsserver:: typeReferenceDirectives", () => {
it("when typeReferenceDirective contains UpperCasePackage", () => {
const projectLocation = "/user/username/projects/myproject";
const libProjectLocation = `${projectLocation}/lib`;
const typeLib: File = {
path: `${libProjectLocation}/@types/UpperCasePackage/index.d.ts`,
content: `declare class BrokenTest {
constructor(name: string, width: number, height: number, onSelect: Function);
Name: string;
SelectedFile: string;
}`
};
const appLib: File = {
path: `${libProjectLocation}/@app/lib/index.d.ts`,
content: `/// <reference types="UpperCasePackage" />
declare class TestLib {
issue: BrokenTest;
constructor();
test(): void;
}`
};
const testProjectLocation = `${projectLocation}/test`;
const testFile: File = {
path: `${testProjectLocation}/test.ts`,
content: `class TestClass1 {
constructor() {
var l = new TestLib();
}
public test2() {
var x = new BrokenTest('',0,0,null);
}
}`
};
const testConfig: File = {
path: `${testProjectLocation}/tsconfig.json`,
content: JSON.stringify({
compilerOptions: {
module: "amd",
typeRoots: ["../lib/@types", "../lib/@app"]
}
})
};
const files = [typeLib, appLib, testFile, testConfig, libFile];
const host = createServerHost(files);
const service = createProjectService(host);
service.openClientFile(testFile.path);
checkNumberOfProjects(service, { configuredProjects: 1 });
const project = service.configuredProjects.get(testConfig.path)!;
checkProjectActualFiles(project, files.map(f => f.path));
host.writeFile(appLib.path, appLib.content.replace("test()", "test2()"));
host.checkTimeoutQueueLengthAndRun(2);
});
it("when typeReferenceDirective is relative path and in a sibling folder", () => {
const projectRootPath = "/user/username/projects/browser-addon";
const projectPath = `${projectRootPath}/background`;
const file: File = {
path: `${projectPath}/a.ts`,
content: "let x = 10;"
};
const tsconfig: File = {
path: `${projectPath}/tsconfig.json`,
content: JSON.stringify({
compilerOptions: {
types: [
"../typedefs/filesystem"
]
}
})
};
const filesystem: File = {
path: `${projectRootPath}/typedefs/filesystem.d.ts`,
content: `interface LocalFileSystem { someProperty: string; }`
};
const files = [file, tsconfig, filesystem, libFile];
const host = createServerHost(files);
const service = createProjectService(host);
service.openClientFile(file.path);
});
});
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
namespace ts.projectSystem {
describe("tsserver:: Untitled files", () => {
it("Can convert positions to locations", () => {
const aTs: File = { path: "/proj/a.ts", content: "" };
const tsconfig: File = { path: "/proj/tsconfig.json", content: "{}" };
const session = createSession(createServerHost([aTs, tsconfig]));
openFilesForSession([aTs], session);
const untitledFile = "untitled:^Untitled-1";
executeSessionRequestNoResponse<protocol.OpenRequest>(session, protocol.CommandTypes.Open, {
file: untitledFile,
fileContent: `/// <reference path="../../../../../../typings/@epic/Core.d.ts" />\nlet foo = 1;\nfooo/**/`,
scriptKindName: "TS",
projectRootPath: "/proj",
});
const response = executeSessionRequest<protocol.CodeFixRequest, protocol.CodeFixResponse>(session, protocol.CommandTypes.GetCodeFixes, {
file: untitledFile,
startLine: 3,
startOffset: 1,
endLine: 3,
endOffset: 5,
errorCodes: [Diagnostics.Cannot_find_name_0_Did_you_mean_1.code],
});
assert.deepEqual<ReadonlyArray<protocol.CodeFixAction> | undefined>(response, [
{
description: "Change spelling to 'foo'",
fixAllDescription: "Fix all detected spelling errors",
fixId: "fixSpelling",
fixName: "spelling",
changes: [{
fileName: untitledFile,
textChanges: [{
start: { line: 3, offset: 1 },
end: { line: 3, offset: 5 },
newText: "foo",
}],
}],
commands: undefined,
},
]);
});
});
}

View File

@@ -15,7 +15,7 @@ namespace ts {
assert.equal(editedText, checkText);
}
describe(`VersionCache TS code`, () => {
describe(`tsserver:: VersionCache TS code`, () => {
let validateEditAtLineCharIndex: (line: number, char: number, deleteLength: number, insertString: string) => void;
before(() => {
@@ -77,7 +77,7 @@ var q:Point=<Point>p;`;
});
});
describe(`VersionCache simple text`, () => {
describe(`tsserver:: VersionCache simple text`, () => {
let validateEditAtPosition: (position: number, deleteLength: number, insertString: string) => void;
let testContent: string;
let lines: string[];
@@ -181,7 +181,7 @@ and grew 1cm per day`;
});
});
describe(`VersionCache stress test`, () => {
describe(`tsserver:: VersionCache stress test`, () => {
let rsa: number[] = [];
let la: number[] = [];
let las: number[] = [];

View File

@@ -1,6 +1,6 @@
namespace ts.projectSystem {
import Tsc_WatchDirectory = TestFSWithWatch.Tsc_WatchDirectory;
describe("watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => {
describe("tsserver:: watchEnvironment:: tsserverProjectSystem watchDirectories implementation", () => {
function verifyCompletionListWithNewFileInSubFolder(tscWatchDirectory: Tsc_WatchDirectory) {
const projectFolder = "/a/username/project";
const projectSrcFolder = `${projectFolder}/src`;
@@ -83,7 +83,7 @@ namespace ts.projectSystem {
});
});
describe("watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => {
describe("tsserver:: watchEnvironment:: tsserverProjectSystem Watched recursive directories with windows style file system", () => {
function verifyWatchedDirectories(rootedPath: string, useProjectAtRoot: boolean) {
const root = useProjectAtRoot ? rootedPath : `${rootedPath}myfolder/allproject/`;
const configFile: File = {

File diff suppressed because it is too large Load Diff