Skip to content

Commit f3c5029

Browse files
committed
Add tests and fix bugs
1 parent 9bfba73 commit f3c5029

File tree

4 files changed

+150
-12
lines changed

4 files changed

+150
-12
lines changed

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@ namespace ts.projectSystem {
1717
})
1818
};
1919

20+
const typeMapList = {
21+
path: <Path>"/typeMapList.json",
22+
content: JSON.stringify({
23+
"jquery": {
24+
// jquery files can have names like "jquery-1.10.2.min.js" (or "jquery.intellisense.js")
25+
"match": "/jquery(-(\\.\\d+)+)?(\\.intellisense)?(\\.min)?\\.js$",
26+
"types": ["jquery"]
27+
},
28+
"WinJS": {
29+
"match": "^(.*/winjs)/base\\.js$", // If the winjs/base.js file is found..
30+
"exclude": [["^", 1, "/.*"]], // ..then exclude all files under the winjs folder
31+
"types": ["winjs"] // And fetch the @types package for WinJS
32+
},
33+
"Office Nuget": {
34+
"match": "^(.*/1/office)/excel\\.debug\\.js$", // Office NuGet package is installed under a "1/office" folder
35+
"exclude": [["^", 1, "/.*"]], // Exclude that whole folder if the file indicated above is found in it
36+
"types": ["office"] // @types package to fetch instead
37+
},
38+
"Minified files": {
39+
"match": "^.*\\.min\\.js$" // Catch-all for minified files. Default exclude is the matched file.
40+
}
41+
})
42+
};
43+
2044
export interface PostExecAction {
2145
readonly success: boolean;
2246
readonly callback: TI.RequestCompletedAction;
@@ -1445,6 +1469,25 @@ namespace ts.projectSystem {
14451469
checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
14461470
});
14471471

1472+
it("ignores files excluded by the safe type list", () => {
1473+
const file1 = {
1474+
path: "/a/b/f1.ts",
1475+
content: "export let x = 5"
1476+
};
1477+
const office = {
1478+
path: "lib/1/office/excel.debug.js",
1479+
content: "whoa do @@ not parse me ok thanks!!!"
1480+
};
1481+
const host = createServerHost([typeMapList, file1, office]);
1482+
const projectService = createProjectService(host);
1483+
projectService.loadSafeList(typeMapList.path);
1484+
1485+
projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) });
1486+
const proj = projectService.externalProjects[0];
1487+
assert.deepEqual(proj.getFileNames(true), [file1.path]);
1488+
assert.deepEqual(proj.getTypeAcquisition().include, ["office"]);
1489+
});
1490+
14481491
it("open file become a part of configured project if it is referenced from root file", () => {
14491492
const file1 = {
14501493
path: "/a/b/f1.ts",
@@ -1695,7 +1738,7 @@ namespace ts.projectSystem {
16951738
checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
16961739
});
16971740

1698-
it ("loading files with correct priority", () => {
1741+
it("loading files with correct priority", () => {
16991742
const f1 = {
17001743
path: "/a/main.ts",
17011744
content: "let x = 1"
@@ -1720,14 +1763,14 @@ namespace ts.projectSystem {
17201763
});
17211764
projectService.openClientFile(f1.path);
17221765
projectService.checkNumberOfProjects({ configuredProjects: 1 });
1723-
checkProjectActualFiles(projectService.configuredProjects[0], [ f1.path ]);
1766+
checkProjectActualFiles(projectService.configuredProjects[0], [f1.path]);
17241767

17251768
projectService.closeClientFile(f1.path);
17261769

17271770
projectService.openClientFile(f2.path);
17281771
projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 });
1729-
checkProjectActualFiles(projectService.configuredProjects[0], [ f1.path ]);
1730-
checkProjectActualFiles(projectService.inferredProjects[0], [ f2.path ]);
1772+
checkProjectActualFiles(projectService.configuredProjects[0], [f1.path]);
1773+
checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]);
17311774
});
17321775

17331776
it("tsconfig script block support", () => {
@@ -1845,7 +1888,7 @@ namespace ts.projectSystem {
18451888
// #3. Ensure no errors when compiler options aren't specified
18461889
const config3 = {
18471890
path: "/a/b/tsconfig.json",
1848-
content: JSON.stringify({ })
1891+
content: JSON.stringify({})
18491892
};
18501893

18511894
host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") });
@@ -3381,13 +3424,13 @@ namespace ts.projectSystem {
33813424
assert.equal((<protocol.CompileOnSaveAffectedFileListSingleProject[]>response)[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile");
33823425
}
33833426

3384-
it ("projectUsesOutFile should not be returned if not set", () => {
3427+
it("projectUsesOutFile should not be returned if not set", () => {
33853428
test({}, /*expectedUsesOutFile*/ false);
33863429
});
3387-
it ("projectUsesOutFile should be true if outFile is set", () => {
3430+
it("projectUsesOutFile should be true if outFile is set", () => {
33883431
test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true);
33893432
});
3390-
it ("projectUsesOutFile should be true if out is set", () => {
3433+
it("projectUsesOutFile should be true if out is set", () => {
33913434
test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true);
33923435
});
33933436
});
@@ -3468,7 +3511,7 @@ namespace ts.projectSystem {
34683511

34693512
const cancellationToken = new TestServerCancellationToken();
34703513
const host = createServerHost([f1, config]);
3471-
const session = createSession(host, /*typingsInstaller*/ undefined, () => {}, cancellationToken);
3514+
const session = createSession(host, /*typingsInstaller*/ undefined, () => { }, cancellationToken);
34723515
{
34733516
session.executeCommandSeq(<protocol.OpenRequest>{
34743517
command: "open",

src/server/editorServices.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ namespace ts.server {
3535
(event: ProjectServiceEvent): void;
3636
}
3737

38+
export interface SafeList {
39+
[name: string]: { match: RegExp, exclude?: Array<Array<string | number>>, types?: string[] };
40+
}
41+
3842
function prepareConvertersForEnumLikeCompilerOptions(commandLineOptions: CommandLineOption[]): Map<Map<number>> {
3943
const map: Map<Map<number>> = createMap<Map<number>>();
4044
for (const option of commandLineOptions) {
@@ -259,6 +263,7 @@ namespace ts.server {
259263
private readonly throttledOperations: ThrottledOperations;
260264

261265
private readonly hostConfiguration: HostConfiguration;
266+
private static safelist: SafeList = {};
262267

263268
private changedFiles: ScriptInfo[];
264269

@@ -284,8 +289,6 @@ namespace ts.server {
284289

285290
this.typingsCache = new TypingsCache(this.typingsInstaller);
286291

287-
// ts.disableIncrementalParsing = true;
288-
289292
this.hostConfiguration = {
290293
formatCodeOptions: getDefaultFormatCodeSettings(this.host),
291294
hostInfo: "Unknown host",
@@ -831,7 +834,7 @@ namespace ts.server {
831834
getDirectoryPath(configFilename),
832835
/*existingOptions*/ {},
833836
configFilename,
834-
/*resolutionStack*/ [],
837+
/*resolutionStack*/[],
835838
this.hostConfiguration.extraFileExtensions);
836839

837840
if (parsedCommandLine.errors.length) {
@@ -1399,13 +1402,97 @@ namespace ts.server {
13991402
this.refreshInferredProjects();
14001403
}
14011404

1405+
/** Makes a filename safe to insert in a RegExp */
1406+
private static filenameEscapeRegexp = /[-\/\\^$*+?.()|[\]{}]/g;
1407+
private static escapeFilenameForRegex(filename: string) {
1408+
return filename.replace(this.filenameEscapeRegexp, "\\$&");
1409+
}
1410+
1411+
loadSafeList(fileName: string): void {
1412+
const raw: SafeList = JSON.parse(this.host.readFile(fileName, "utf-8"));
1413+
// Parse the regexps
1414+
for (const k of Object.keys(raw)) {
1415+
raw[k].match = new RegExp(raw[k].match as {} as string, "gi");
1416+
}
1417+
// raw is now fixed and ready
1418+
ProjectService.safelist = raw;
1419+
}
1420+
1421+
applySafeList(proj: protocol.ExternalProject): void {
1422+
const { rootFiles, typeAcquisition } = proj;
1423+
const types = (typeAcquisition && typeAcquisition.include) || [];
1424+
1425+
const excludeRules: string[] = [];
1426+
1427+
for (const name of Object.keys(ProjectService.safelist)) {
1428+
const rule = ProjectService.safelist[name];
1429+
for (const root of rootFiles) {
1430+
if (rule.match.test(root.fileName)) {
1431+
this.logger.info(`Excluding files based on rule ${name}`);
1432+
1433+
// If the file matches, collect its types packages and exclude rules
1434+
if (rule.types) {
1435+
for (const type of rule.types) {
1436+
if (types.indexOf(type) < 0) {
1437+
types.push(type);
1438+
}
1439+
}
1440+
}
1441+
1442+
if (rule.exclude) {
1443+
for (const exclude of rule.exclude) {
1444+
const processedRule = root.fileName.replace(rule.match, (...groups: Array<string>) => {
1445+
return exclude.map(groupNumberOrString => {
1446+
// RegExp group numbers are 1-based, but the first element in groups
1447+
// is actually the original string, so it all works out in the end.
1448+
if (typeof groupNumberOrString === "number") {
1449+
if (typeof groups[groupNumberOrString] !== "string") {
1450+
// Specification was wrong - exclude nothing!
1451+
this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`);
1452+
// * can't appear in a filename; escape it because it's feeding into a RegExp
1453+
return "\\*";
1454+
}
1455+
return ProjectService.escapeFilenameForRegex(groups[groupNumberOrString]);
1456+
}
1457+
return groupNumberOrString;
1458+
}).join("");
1459+
});
1460+
1461+
if (excludeRules.indexOf(processedRule) == -1) {
1462+
excludeRules.push(processedRule);
1463+
}
1464+
}
1465+
}
1466+
else {
1467+
// If not rules listed, add the default rule to exclude the matched file
1468+
if (excludeRules.indexOf(root.fileName) < 0) {
1469+
excludeRules.push(root.fileName);
1470+
}
1471+
}
1472+
}
1473+
}
1474+
1475+
// Copy back this field into the project if needed
1476+
if (types.length > 0) {
1477+
proj.typeAcquisition = proj.typeAcquisition || { };
1478+
proj.typeAcquisition.include = types;
1479+
}
1480+
}
1481+
1482+
const excludeRegexes = excludeRules.map(e => new RegExp(e, "i"));
1483+
proj.rootFiles = proj.rootFiles.filter(file => !excludeRegexes.some(re => re.test(file.fileName)));
1484+
}
1485+
14021486
openExternalProject(proj: protocol.ExternalProject, suppressRefreshOfInferredProjects = false): void {
14031487
// typingOptions has been deprecated and is only supported for backward compatibility
14041488
// purposes. It should be removed in future releases - use typeAcquisition instead.
14051489
if (proj.typingOptions && !proj.typeAcquisition) {
14061490
const typeAcquisition = convertEnableAutoDiscoveryToEnable(proj.typingOptions);
14071491
proj.typeAcquisition = typeAcquisition;
14081492
}
1493+
1494+
this.applySafeList(proj);
1495+
14091496
let tsConfigFiles: NormalizedPath[];
14101497
const rootFiles: protocol.ExternalFile[] = [];
14111498
for (const file of proj.rootFiles) {

src/server/protocol.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,10 @@ namespace ts.server.protocol {
9191
/* @internal */
9292
export type BreakpointStatement = "breakpointStatement";
9393
export type CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects";
94+
export type LoadTypesMap = "loadTypesMap";
9495
export type GetCodeFixes = "getCodeFixes";
9596
/* @internal */
97+
/* @internal */
9698
export type GetCodeFixesFull = "getCodeFixes-full";
9799
export type GetSupportedCodeFixes = "getSupportedCodeFixes";
98100
}

src/server/session.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ namespace ts.server {
186186
/* @internal */
187187
export const BreakpointStatement: protocol.CommandTypes.BreakpointStatement = "breakpointStatement";
188188
export const CompilerOptionsForInferredProjects: protocol.CommandTypes.CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects";
189+
export const LoadTypesMap: protocol.CommandTypes.LoadTypesMap = "loadTypesMap";
189190
export const GetCodeFixes: protocol.CommandTypes.GetCodeFixes = "getCodeFixes";
190191
/* @internal */
191192
export const GetCodeFixesFull: protocol.CommandTypes.GetCodeFixesFull = "getCodeFixes-full";
@@ -1765,6 +1766,11 @@ namespace ts.server {
17651766
this.setCompilerOptionsForInferredProjects(request.arguments);
17661767
return this.requiredResponse(true);
17671768
},
1769+
[CommandNames.LoadTypesMap]: (request: protocol.FileRequest) => {
1770+
const loadArgs = <protocol.FileRequestArgs>request.arguments;
1771+
this.projectService.loadSafeList(loadArgs.file);
1772+
return this.notRequired();
1773+
},
17681774
[CommandNames.ProjectInfo]: (request: protocol.ProjectInfoRequest) => {
17691775
return this.requiredResponse(this.getProjectInfo(request.arguments));
17701776
},

0 commit comments

Comments
 (0)