Skip to content

Commit 76b7be8

Browse files
author
Andy Hanson
committed
Add quickfix and refactoring to install @types packages
1 parent 6e5a4a9 commit 76b7be8

37 files changed

+879
-203
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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1829,7 +1829,9 @@ namespace ts {
18291829
return i < 0 ? path : path.substring(i + 1);
18301830
}
18311831

1832-
export function combinePaths(path1: string, path2: string) {
1832+
export function combinePaths(path1: Path, path2: string): Path;
1833+
export function combinePaths(path1: string, path2: string): string;
1834+
export function combinePaths(path1: string, path2: string): string {
18331835
if (!(path1 && path1.length)) return path2;
18341836
if (!(path2 && path2.length)) return path1;
18351837
if (getRootLength(path2) !== 0) return path2;

src/compiler/moduleNameResolver.ts

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,11 @@ namespace ts {
124124
}
125125
}
126126

127-
export function getEffectiveTypeRoots(options: CompilerOptions, host: { directoryExists?: (directoryName: string) => boolean, getCurrentDirectory?: () => string }): string[] | undefined {
127+
export interface GetEffectiveTypeRootsHost {
128+
directoryExists?: (directoryName: string) => boolean;
129+
getCurrentDirectory?: () => string;
130+
}
131+
export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined {
128132
if (options.typeRoots) {
129133
return options.typeRoots;
130134
}
@@ -985,7 +989,8 @@ namespace ts {
985989
return withPackageId(packageId, pathAndExtension);
986990
}
987991

988-
function getPackageName(moduleName: string): { packageName: string, rest: string } {
992+
/* @internal */
993+
export function getPackageName(moduleName: string): { packageName: string, rest: string } {
989994
let idx = moduleName.indexOf(directorySeparator);
990995
if (moduleName[0] === "@") {
991996
idx = moduleName.indexOf(directorySeparator, idx + 1);
@@ -1153,4 +1158,68 @@ namespace ts {
11531158
function toSearchResult<T>(value: T | undefined): SearchResult<T> {
11541159
return value !== undefined ? { value } : undefined;
11551160
}
1161+
1162+
/* @internal */
1163+
export const enum PackageNameValidationResult {
1164+
Ok,
1165+
ScopedPackagesNotSupported,
1166+
EmptyName,
1167+
NameTooLong,
1168+
NameStartsWithDot,
1169+
NameStartsWithUnderscore,
1170+
NameContainsNonURISafeCharacters
1171+
}
1172+
1173+
const MaxPackageNameLength = 214;
1174+
1175+
/**
1176+
* Validates package name using rules defined at https://docs.npmjs.com/files/package.json
1177+
*/
1178+
/* @internal */
1179+
export function validatePackageName(packageName: string): PackageNameValidationResult {
1180+
if (!packageName) {
1181+
return PackageNameValidationResult.EmptyName;
1182+
}
1183+
if (packageName.length > MaxPackageNameLength) {
1184+
return PackageNameValidationResult.NameTooLong;
1185+
}
1186+
if (packageName.charCodeAt(0) === CharacterCodes.dot) {
1187+
return PackageNameValidationResult.NameStartsWithDot;
1188+
}
1189+
if (packageName.charCodeAt(0) === CharacterCodes._) {
1190+
return PackageNameValidationResult.NameStartsWithUnderscore;
1191+
}
1192+
// check if name is scope package like: starts with @ and has one '/' in the middle
1193+
// scoped packages are not currently supported
1194+
// TODO: when support will be added we'll need to split and check both scope and package name
1195+
if (/^@[^/]+\/[^/]+$/.test(packageName)) {
1196+
return PackageNameValidationResult.ScopedPackagesNotSupported;
1197+
}
1198+
if (encodeURIComponent(packageName) !== packageName) {
1199+
return PackageNameValidationResult.NameContainsNonURISafeCharacters;
1200+
}
1201+
return PackageNameValidationResult.Ok;
1202+
}
1203+
1204+
/* @internal */
1205+
export function renderPackageNameValidationFailure(result: PackageNameValidationResult, typing: string): string {
1206+
switch (result) {
1207+
case PackageNameValidationResult.EmptyName:
1208+
return `Package name '${typing}' cannot be empty`;
1209+
case PackageNameValidationResult.NameTooLong:
1210+
return `Package name '${typing}' should be less than ${MaxPackageNameLength} characters`;
1211+
case PackageNameValidationResult.NameStartsWithDot:
1212+
return `Package name '${typing}' cannot start with '.'`;
1213+
case PackageNameValidationResult.NameStartsWithUnderscore:
1214+
return `Package name '${typing}' cannot start with '_'`;
1215+
case PackageNameValidationResult.ScopedPackagesNotSupported:
1216+
return `Package '${typing}' is scoped and currently is not supported`;
1217+
case PackageNameValidationResult.NameContainsNonURISafeCharacters:
1218+
return `Package name '${typing}' contains non URI safe characters`;
1219+
case PackageNameValidationResult.Ok:
1220+
throw Debug.fail(); // Shouldn't have called this.
1221+
default:
1222+
Debug.assertNever(result);
1223+
}
1224+
}
11561225
}

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();
@@ -2750,16 +2754,26 @@ Actual: ${stringify(fullActual)}`);
27502754
}
27512755
}
27522756

2753-
public verifyCodeFixAvailable(negative: boolean) {
2754-
const codeFix = this.getCodeFixActions(this.activeFile.fileName);
2757+
public verifyCodeFixAvailable(negative: boolean, info: FourSlashInterface.VerifyCodeFixAvailableOptions[] | undefined) {
2758+
const codeFixes = this.getCodeFixActions(this.activeFile.fileName);
27552759

2756-
if (negative && codeFix.length) {
2757-
this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`);
2760+
if (negative) {
2761+
if (codeFixes.length) {
2762+
this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`);
2763+
}
2764+
return;
27582765
}
27592766

2760-
if (!(negative || codeFix.length)) {
2767+
if (!codeFixes.length) {
27612768
this.raiseError(`verifyCodeFixAvailable failed - expected code fixes but none found.`);
27622769
}
2770+
if (info) {
2771+
assert.equal(info.length, codeFixes.length);
2772+
ts.zipWith(codeFixes, info, (fix, info) => {
2773+
assert.equal(fix.description, info.description);
2774+
this.assertObjectsEqual(fix.commands, info.commands);
2775+
});
2776+
}
27632777
}
27642778

27652779
public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) {
@@ -2803,6 +2817,14 @@ Actual: ${stringify(fullActual)}`);
28032817
}
28042818
}
28052819

2820+
public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) {
2821+
const selection = this.getSelection();
2822+
2823+
const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection) || ts.emptyArray)
2824+
.filter(r => r.name === name && r.actions.some(a => a.name === actionName));
2825+
this.assertObjectsEqual(actualRefactors, refactors);
2826+
}
2827+
28062828
public verifyApplicableRefactorAvailableForRange(negative: boolean) {
28072829
const ranges = this.getRanges();
28082830
if (!(ranges && ranges.length === 1)) {
@@ -3577,6 +3599,10 @@ namespace FourSlashInterface {
35773599
public symbolsInScope(range: FourSlash.Range): ts.Symbol[] {
35783600
return this.state.symbolsInScope(range);
35793601
}
3602+
3603+
public setTypesRegistry(map: ts.MapLike<void>): void {
3604+
this.state.setTypesRegistry(map);
3605+
}
35803606
}
35813607

35823608
export class GoTo {
@@ -3752,8 +3778,8 @@ namespace FourSlashInterface {
37523778
this.state.verifyCodeFix(options);
37533779
}
37543780

3755-
public codeFixAvailable() {
3756-
this.state.verifyCodeFixAvailable(this.negative);
3781+
public codeFixAvailable(options?: VerifyCodeFixAvailableOptions[]) {
3782+
this.state.verifyCodeFixAvailable(this.negative, options);
37573783
}
37583784

37593785
public applicableRefactorAvailableAtMarker(markerName: string) {
@@ -3764,6 +3790,10 @@ namespace FourSlashInterface {
37643790
this.state.verifyApplicableRefactorAvailableForRange(this.negative);
37653791
}
37663792

3793+
public refactor(options: VerifyRefactorOptions) {
3794+
this.state.verifyRefactor(options);
3795+
}
3796+
37673797
public refactorAvailable(name: string, actionName?: string) {
37683798
this.state.verifyRefactorAvailable(this.negative, name, actionName);
37693799
}
@@ -4404,4 +4434,15 @@ namespace FourSlashInterface {
44044434
errorCode?: number;
44054435
index?: number;
44064436
}
4437+
4438+
export interface VerifyCodeFixAvailableOptions {
4439+
description: string;
4440+
commands?: ts.CodeActionCommand[];
4441+
}
4442+
4443+
export interface VerifyRefactorOptions {
4444+
name: string;
4445+
actionName: string;
4446+
refactors: ts.ApplicableRefactorInfo[];
4447+
}
44074448
}

src/harness/harnessLanguageService.ts

Lines changed: 10 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,14 @@ namespace Harness.LanguageService {
182183

183184
/// Native adapter
184185
class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost {
186+
tryGetTypesRegistry(): ts.Map<void> | undefined {
187+
if (this.typesRegistry === undefined) {
188+
ts.Debug.fail("fourslash test should set types registry.");
189+
}
190+
return this.typesRegistry;
191+
}
192+
installPackage = ts.notImplemented;
193+
185194
getCompilationSettings() { return this.settings; }
186195
getCancellationToken() { return this.cancellationToken; }
187196
getDirectories(path: string): string[] {
@@ -493,6 +502,7 @@ namespace Harness.LanguageService {
493502
getCodeFixesAtPosition(): ts.CodeAction[] {
494503
throw new Error("Not supported on the shim.");
495504
}
505+
applyCodeActionCommand = ts.notImplemented;
496506
getCodeFixDiagnostics(): ts.Diagnostic[] {
497507
throw new Error("Not supported on the shim.");
498508
}

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/languageService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export function Component(x: Config): any;`
2121
// to write an alias to a module's default export was referrenced across files and had no default export
2222
it("should be able to create a language service which can respond to deinition requests without throwing", () => {
2323
const languageService = ts.createLanguageService({
24+
tryGetTypesRegistry: notImplemented,
25+
installPackage: notImplemented,
2426
getCompilationSettings() {
2527
return {};
2628
},

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

0 commit comments

Comments
 (0)