Skip to content

Commit d05443b

Browse files
author
Andy
authored
Add quickfix and refactoring to install @types packages (microsoft#19130)
* Add quickfix and refactoring to install @types packages * Move `validatePackageName` to `jsTyping.ts` * Remove combinePaths overloads * Respond to code review * Update api baselines * Use native PromiseConstructor * Return false instead of undefined * Remove getProjectRootPath * Update api
1 parent 314172a commit d05443b

36 files changed

+773
-209
lines changed

scripts/buildProtocol.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,25 @@ class DeclarationsWalker {
5151
return this.processType((<any>type).typeArguments[0]);
5252
}
5353
else {
54-
for (const decl of s.getDeclarations()) {
55-
const sourceFile = decl.getSourceFile();
56-
if (sourceFile === this.protocolFile || path.basename(sourceFile.fileName) === "lib.d.ts") {
57-
return;
58-
}
59-
if (decl.kind === ts.SyntaxKind.EnumDeclaration && !isStringEnum(decl as ts.EnumDeclaration)) {
60-
this.removedTypes.push(type);
61-
return;
62-
}
63-
else {
64-
// splice declaration in final d.ts file
65-
let text = decl.getFullText();
66-
this.text += `${text}\n`;
67-
// recursively pull all dependencies into result dts file
54+
const declarations = s.getDeclarations();
55+
if (declarations) {
56+
for (const decl of declarations) {
57+
const sourceFile = decl.getSourceFile();
58+
if (sourceFile === this.protocolFile || path.basename(sourceFile.fileName) === "lib.d.ts") {
59+
return;
60+
}
61+
if (decl.kind === ts.SyntaxKind.EnumDeclaration && !isStringEnum(decl as ts.EnumDeclaration)) {
62+
this.removedTypes.push(type);
63+
return;
64+
}
65+
else {
66+
// splice declaration in final d.ts file
67+
let text = decl.getFullText();
68+
this.text += `${text}\n`;
69+
// recursively pull all dependencies into result dts file
6870

69-
this.visitTypeNodes(decl);
71+
this.visitTypeNodes(decl);
72+
}
7073
}
7174
}
7275
}

src/compiler/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1861,7 +1861,7 @@ namespace ts {
18611861
return i < 0 ? path : path.substring(i + 1);
18621862
}
18631863

1864-
export function combinePaths(path1: string, path2: string) {
1864+
export function combinePaths(path1: string, path2: string): string {
18651865
if (!(path1 && path1.length)) return path2;
18661866
if (!(path2 && path2.length)) return path1;
18671867
if (getRootLength(path2) !== 0) return path2;

src/compiler/moduleNameResolver.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,8 @@ namespace ts {
10051005
return withPackageId(packageId, pathAndExtension);
10061006
}
10071007

1008-
function getPackageName(moduleName: string): { packageName: string, rest: string } {
1008+
/* @internal */
1009+
export function getPackageName(moduleName: string): { packageName: string, rest: string } {
10091010
let idx = moduleName.indexOf(directorySeparator);
10101011
if (moduleName[0] === "@") {
10111012
idx = moduleName.indexOf(directorySeparator, idx + 1);
@@ -1063,18 +1064,27 @@ namespace ts {
10631064
const mangledScopedPackageSeparator = "__";
10641065

10651066
/** For a scoped package, we must look in `@types/foo__bar` instead of `@types/@foo/bar`. */
1066-
function mangleScopedPackage(moduleName: string, state: ModuleResolutionState): string {
1067-
if (startsWith(moduleName, "@")) {
1068-
const replaceSlash = moduleName.replace(ts.directorySeparator, mangledScopedPackageSeparator);
1069-
if (replaceSlash !== moduleName) {
1070-
const mangled = replaceSlash.slice(1); // Take off the "@"
1071-
if (state.traceEnabled) {
1072-
trace(state.host, Diagnostics.Scoped_package_detected_looking_in_0, mangled);
1073-
}
1074-
return mangled;
1067+
function mangleScopedPackage(packageName: string, state: ModuleResolutionState): string {
1068+
const mangled = getMangledNameForScopedPackage(packageName);
1069+
if (state.traceEnabled && mangled !== packageName) {
1070+
trace(state.host, Diagnostics.Scoped_package_detected_looking_in_0, mangled);
1071+
}
1072+
return mangled;
1073+
}
1074+
1075+
/* @internal */
1076+
export function getTypesPackageName(packageName: string): string {
1077+
return `@types/${getMangledNameForScopedPackage(packageName)}`;
1078+
}
1079+
1080+
function getMangledNameForScopedPackage(packageName: string): string {
1081+
if (startsWith(packageName, "@")) {
1082+
const replaceSlash = packageName.replace(ts.directorySeparator, mangledScopedPackageSeparator);
1083+
if (replaceSlash !== packageName) {
1084+
return replaceSlash.slice(1); // Take off the "@"
10751085
}
10761086
}
1077-
return moduleName;
1087+
return packageName;
10781088
}
10791089

10801090
/* @internal */

src/harness/fourslash.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,10 @@ namespace FourSlash {
953953
return this.getChecker().getSymbolsInScope(node, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace);
954954
}
955955

956+
public setTypesRegistry(map: ts.MapLike<void>): void {
957+
this.languageServiceAdapterHost.typesRegistry = ts.createMapFromTemplate(map);
958+
}
959+
956960
public verifyTypeOfSymbolAtLocation(range: Range, symbol: ts.Symbol, expected: string): void {
957961
const node = this.goToAndGetNode(range);
958962
const checker = this.getChecker();
@@ -2777,16 +2781,26 @@ Actual: ${stringify(fullActual)}`);
27772781
}
27782782
}
27792783

2780-
public verifyCodeFixAvailable(negative: boolean) {
2781-
const codeFix = this.getCodeFixActions(this.activeFile.fileName);
2784+
public verifyCodeFixAvailable(negative: boolean, info: FourSlashInterface.VerifyCodeFixAvailableOptions[] | undefined) {
2785+
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
27822786

2783-
if (negative && codeFix.length) {
2784-
this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`);
2787+
if (negative) {
2788+
if (codeFixes.length) {
2789+
this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`);
2790+
}
2791+
return;
27852792
}
27862793

2787-
if (!(negative || codeFix.length)) {
2794+
if (!codeFixes.length) {
27882795
this.raiseError(`verifyCodeFixAvailable failed - expected code fixes but none found.`);
27892796
}
2797+
if (info) {
2798+
assert.equal(info.length, codeFixes.length);
2799+
ts.zipWith(codeFixes, info, (fix, info) => {
2800+
assert.equal(fix.description, info.description);
2801+
this.assertObjectsEqual(fix.commands, info.commands);
2802+
});
2803+
}
27902804
}
27912805

27922806
public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) {
@@ -2830,6 +2844,14 @@ Actual: ${stringify(fullActual)}`);
28302844
}
28312845
}
28322846

2847+
public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) {
2848+
const selection = this.getSelection();
2849+
2850+
const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection) || ts.emptyArray)
2851+
.filter(r => r.name === name && r.actions.some(a => a.name === actionName));
2852+
this.assertObjectsEqual(actualRefactors, refactors);
2853+
}
2854+
28332855
public verifyApplicableRefactorAvailableForRange(negative: boolean) {
28342856
const ranges = this.getRanges();
28352857
if (!(ranges && ranges.length === 1)) {
@@ -3614,6 +3636,10 @@ namespace FourSlashInterface {
36143636
public symbolsInScope(range: FourSlash.Range): ts.Symbol[] {
36153637
return this.state.symbolsInScope(range);
36163638
}
3639+
3640+
public setTypesRegistry(map: ts.MapLike<void>): void {
3641+
this.state.setTypesRegistry(map);
3642+
}
36173643
}
36183644

36193645
export class GoTo {
@@ -3789,8 +3815,8 @@ namespace FourSlashInterface {
37893815
this.state.verifyCodeFix(options);
37903816
}
37913817

3792-
public codeFixAvailable() {
3793-
this.state.verifyCodeFixAvailable(this.negative);
3818+
public codeFixAvailable(options?: VerifyCodeFixAvailableOptions[]) {
3819+
this.state.verifyCodeFixAvailable(this.negative, options);
37943820
}
37953821

37963822
public applicableRefactorAvailableAtMarker(markerName: string) {
@@ -3801,6 +3827,10 @@ namespace FourSlashInterface {
38013827
this.state.verifyApplicableRefactorAvailableForRange(this.negative);
38023828
}
38033829

3830+
public refactor(options: VerifyRefactorOptions) {
3831+
this.state.verifyRefactor(options);
3832+
}
3833+
38043834
public refactorAvailable(name: string, actionName?: string) {
38053835
this.state.verifyRefactorAvailable(this.negative, name, actionName);
38063836
}
@@ -4449,6 +4479,17 @@ namespace FourSlashInterface {
44494479
index?: number;
44504480
}
44514481

4482+
export interface VerifyCodeFixAvailableOptions {
4483+
description: string;
4484+
commands?: ts.CodeActionCommand[];
4485+
}
4486+
4487+
export interface VerifyRefactorOptions {
4488+
name: string;
4489+
actionName: string;
4490+
refactors: ts.ApplicableRefactorInfo[];
4491+
}
4492+
44524493
export interface VerifyCompletionActionOptions extends NewContentOptions {
44534494
name: string;
44544495
description: string;

src/harness/harnessLanguageService.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ namespace Harness.LanguageService {
123123
}
124124

125125
export class LanguageServiceAdapterHost {
126+
public typesRegistry: ts.Map<void> | undefined;
126127
protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/false);
127128

128129
constructor(protected cancellationToken = DefaultHostCancellationToken.Instance,
@@ -182,6 +183,11 @@ namespace Harness.LanguageService {
182183

183184
/// Native adapter
184185
class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost {
186+
isKnownTypesPackageName(name: string): boolean {
187+
return this.typesRegistry && this.typesRegistry.has(name);
188+
}
189+
installPackage = ts.notImplemented;
190+
185191
getCompilationSettings() { return this.settings; }
186192
getCancellationToken() { return this.cancellationToken; }
187193
getDirectories(path: string): string[] {
@@ -493,6 +499,7 @@ namespace Harness.LanguageService {
493499
getCodeFixesAtPosition(): ts.CodeAction[] {
494500
throw new Error("Not supported on the shim.");
495501
}
502+
applyCodeActionCommand = ts.notImplemented;
496503
getCodeFixDiagnostics(): ts.Diagnostic[] {
497504
throw new Error("Not supported on the shim.");
498505
}

src/harness/unittests/compileOnSave.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace ts.projectSystem {
1212

1313
describe("CompileOnSave affected list", () => {
1414
function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) {
15-
const response: server.protocol.CompileOnSaveAffectedFileListSingleProject[] = session.executeCommand(request).response;
15+
const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[];
1616
const actualResult = response.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName));
1717
expectedFileList = expectedFileList.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName));
1818

src/harness/unittests/extractTestHelpers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ namespace ts {
9797
return rulesProvider;
9898
}
9999

100+
const notImplementedHost: LanguageServiceHost = {
101+
getCompilationSettings: notImplemented,
102+
getScriptFileNames: notImplemented,
103+
getScriptVersion: notImplemented,
104+
getScriptSnapshot: notImplemented,
105+
getDefaultLibFileName: notImplemented,
106+
};
107+
100108
export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage) {
101109
const t = extractTest(text);
102110
const selectionRange = t.ranges.get("selection");
@@ -125,6 +133,7 @@ namespace ts {
125133
file: sourceFile,
126134
startPosition: selectionRange.start,
127135
endPosition: selectionRange.end,
136+
host: notImplementedHost,
128137
rulesProvider: getRuleProvider()
129138
};
130139
const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end));
@@ -188,6 +197,7 @@ namespace ts {
188197
file: sourceFile,
189198
startPosition: selectionRange.start,
190199
endPosition: selectionRange.end,
200+
host: notImplementedHost,
191201
rulesProvider: getRuleProvider()
192202
};
193203
const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end));

src/harness/unittests/projectErrors.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,23 @@ namespace ts.projectSystem {
5757
});
5858

5959
checkNumberOfProjects(projectService, { externalProjects: 1 });
60-
const diags = session.executeCommand(compilerOptionsRequest).response;
60+
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
6161
// only file1 exists - expect error
6262
checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]);
6363
}
6464
host.reloadFS([file2, libFile]);
6565
{
6666
// only file2 exists - expect error
6767
checkNumberOfProjects(projectService, { externalProjects: 1 });
68-
const diags = session.executeCommand(compilerOptionsRequest).response;
68+
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
6969
checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]);
7070
}
7171

7272
host.reloadFS([file1, file2, libFile]);
7373
{
7474
// both files exist - expect no errors
7575
checkNumberOfProjects(projectService, { externalProjects: 1 });
76-
const diags = session.executeCommand(compilerOptionsRequest).response;
76+
const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
7777
checkDiagnosticsWithLinePos(diags, []);
7878
}
7979
});
@@ -103,13 +103,13 @@ namespace ts.projectSystem {
103103
seq: 2,
104104
arguments: { projectFileName: project.getProjectName() }
105105
};
106-
let diags = session.executeCommand(compilerOptionsRequest).response;
106+
let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
107107
checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]);
108108

109109
host.reloadFS([file1, file2, config, libFile]);
110110

111111
checkNumberOfProjects(projectService, { configuredProjects: 1 });
112-
diags = session.executeCommand(compilerOptionsRequest).response;
112+
diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[];
113113
checkDiagnosticsWithLinePos(diags, []);
114114
});
115115

src/harness/unittests/session.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ namespace ts.server {
315315
item: false
316316
};
317317
const command = "newhandle";
318-
const result = {
318+
const result: ts.server.HandlerResponse = {
319319
response: respBody,
320320
responseRequired: true
321321
};
@@ -332,7 +332,7 @@ namespace ts.server {
332332
const respBody = {
333333
item: false
334334
};
335-
const resp = {
335+
const resp: ts.server.HandlerResponse = {
336336
response: respBody,
337337
responseRequired: true
338338
};
@@ -372,7 +372,7 @@ namespace ts.server {
372372
};
373373
const command = "test";
374374

375-
session.output(body, command);
375+
session.output(body, command, /*reqSeq*/ 0);
376376

377377
expect(lastSent).to.deep.equal({
378378
seq: 0,
@@ -475,7 +475,7 @@ namespace ts.server {
475475
};
476476
const command = "test";
477477

478-
session.output(body, command);
478+
session.output(body, command, /*reqSeq*/ 0);
479479

480480
expect(session.lastSent).to.deep.equal({
481481
seq: 0,
@@ -542,7 +542,7 @@ namespace ts.server {
542542
handleRequest(msg: protocol.Request) {
543543
let response: protocol.Response;
544544
try {
545-
({ response } = this.executeCommand(msg));
545+
response = this.executeCommand(msg).response as protocol.Response;
546546
}
547547
catch (e) {
548548
this.output(undefined, msg.command, msg.seq, e.toString());

0 commit comments

Comments
 (0)