Skip to content

Commit 2ca90b7

Browse files
Merge pull request #14999 from RyanCavanaugh/typesMap
Add advanced safelist for exclusions
2 parents 3029b8f + a874567 commit 2ca90b7

File tree

3 files changed

+167
-15
lines changed

3 files changed

+167
-15
lines changed

src/harness/unittests/tsserverProjectSystem.ts

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

20+
const customSafeList = {
21+
path: <Path>"/typeMapList.json",
22+
content: JSON.stringify({
23+
"quack": {
24+
"match": "/duckquack-(\\d+)\\.min\\.js",
25+
"types": ["duck-types"]
26+
},
27+
})
28+
};
29+
2030
export interface PostExecAction {
2131
readonly success: boolean;
2232
readonly callback: TI.RequestCompletedAction;
@@ -1445,6 +1455,28 @@ namespace ts.projectSystem {
14451455
checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]);
14461456
});
14471457

1458+
it("ignores files excluded by the safe type list", () => {
1459+
const file1 = {
1460+
path: "/a/b/f1.ts",
1461+
content: "export let x = 5"
1462+
};
1463+
const office = {
1464+
path: "/lib/duckquack-3.min.js",
1465+
content: "whoa do @@ not parse me ok thanks!!!"
1466+
};
1467+
const host = createServerHost([customSafeList, file1, office]);
1468+
const projectService = createProjectService(host);
1469+
projectService.loadSafeList(customSafeList.path);
1470+
try {
1471+
projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) });
1472+
const proj = projectService.externalProjects[0];
1473+
assert.deepEqual(proj.getFileNames(/*excludeFilesFromExternalLibraries*/ true), [file1.path]);
1474+
assert.deepEqual(proj.getTypeAcquisition().include, ["duck-types"]);
1475+
} finally {
1476+
projectService.resetSafeList();
1477+
}
1478+
});
1479+
14481480
it("open file become a part of configured project if it is referenced from root file", () => {
14491481
const file1 = {
14501482
path: "/a/b/f1.ts",
@@ -1695,7 +1727,7 @@ namespace ts.projectSystem {
16951727
checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]);
16961728
});
16971729

1698-
it ("loading files with correct priority", () => {
1730+
it("loading files with correct priority", () => {
16991731
const f1 = {
17001732
path: "/a/main.ts",
17011733
content: "let x = 1"
@@ -1720,14 +1752,14 @@ namespace ts.projectSystem {
17201752
});
17211753
projectService.openClientFile(f1.path);
17221754
projectService.checkNumberOfProjects({ configuredProjects: 1 });
1723-
checkProjectActualFiles(projectService.configuredProjects[0], [ f1.path ]);
1755+
checkProjectActualFiles(projectService.configuredProjects[0], [f1.path]);
17241756

17251757
projectService.closeClientFile(f1.path);
17261758

17271759
projectService.openClientFile(f2.path);
17281760
projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 });
1729-
checkProjectActualFiles(projectService.configuredProjects[0], [ f1.path ]);
1730-
checkProjectActualFiles(projectService.inferredProjects[0], [ f2.path ]);
1761+
checkProjectActualFiles(projectService.configuredProjects[0], [f1.path]);
1762+
checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]);
17311763
});
17321764

17331765
it("tsconfig script block support", () => {
@@ -1845,7 +1877,7 @@ namespace ts.projectSystem {
18451877
// #3. Ensure no errors when compiler options aren't specified
18461878
const config3 = {
18471879
path: "/a/b/tsconfig.json",
1848-
content: JSON.stringify({ })
1880+
content: JSON.stringify({})
18491881
};
18501882

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

3384-
it ("projectUsesOutFile should not be returned if not set", () => {
3416+
it("projectUsesOutFile should not be returned if not set", () => {
33853417
test({}, /*expectedUsesOutFile*/ false);
33863418
});
3387-
it ("projectUsesOutFile should be true if outFile is set", () => {
3419+
it("projectUsesOutFile should be true if outFile is set", () => {
33883420
test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true);
33893421
});
3390-
it ("projectUsesOutFile should be true if out is set", () => {
3422+
it("projectUsesOutFile should be true if out is set", () => {
33913423
test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true);
33923424
});
33933425
});
@@ -3468,7 +3500,7 @@ namespace ts.projectSystem {
34683500

34693501
const cancellationToken = new TestServerCancellationToken();
34703502
const host = createServerHost([f1, config]);
3471-
const session = createSession(host, /*typingsInstaller*/ undefined, () => {}, cancellationToken);
3503+
const session = createSession(host, /*typingsInstaller*/ undefined, () => { }, cancellationToken);
34723504
{
34733505
session.executeCommandSeq(<protocol.OpenRequest>{
34743506
command: "open",
@@ -3750,4 +3782,4 @@ namespace ts.projectSystem {
37503782
assert.isUndefined(project.getCompilerOptions().maxNodeModuleJsDepth);
37513783
});
37523784
});
3753-
}
3785+
}

src/harness/unittests/typingsInstaller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,12 +381,12 @@ namespace ts.projectSystem {
381381

382382
const p = projectService.externalProjects[0];
383383
projectService.checkNumberOfProjects({ externalProjects: 1 });
384-
checkProjectActualFiles(p, [file1.path, file2.path]);
384+
checkProjectActualFiles(p, [file2.path]);
385385

386386
installer.checkPendingCommands(/*expectedCount*/ 0);
387387

388388
checkNumberOfProjects(projectService, { externalProjects: 1 });
389-
checkProjectActualFiles(p, [file1.path, file2.path]);
389+
checkProjectActualFiles(p, [file2.path]);
390390
});
391391

392392
it("external project - with type acquisition, with only js, d.ts files", () => {

src/server/editorServices.ts

Lines changed: 123 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) {
@@ -57,6 +61,32 @@ namespace ts.server {
5761
"smart": IndentStyle.Smart
5862
});
5963

64+
const defaultTypeSafeList: SafeList = {
65+
"jquery": {
66+
// jquery files can have names like "jquery-1.10.2.min.js" (or "jquery.intellisense.js")
67+
"match": /jquery(-(\.?\d+)+)?(\.intellisense)?(\.min)?\.js$/i,
68+
"types": ["jquery"]
69+
},
70+
"WinJS": {
71+
// e.g. c:/temp/UWApp1/lib/winjs-4.0.1/js/base.js
72+
"match": /^(.*\/winjs-[.\d]+)\/js\/base\.js$/i, // If the winjs/base.js file is found..
73+
"exclude": [["^", 1, "/.*"]], // ..then exclude all files under the winjs folder
74+
"types": ["winjs"] // And fetch the @types package for WinJS
75+
},
76+
"Kendo": {
77+
// e.g. /Kendo3/wwwroot/lib/kendo/kendo.all.min.js
78+
"match": /^(.*\/kendo)\/kendo\.all\.min\.js$/i,
79+
"exclude": [["^", 1, "/.*"]],
80+
"types": ["kendo-ui"]
81+
},
82+
"Office Nuget": {
83+
// e.g. /scripts/Office/1/excel-15.debug.js
84+
"match": /^(.*\/office\/1)\/excel-\d+\.debug\.js$/i, // Office NuGet package is installed under a "1/office" folder
85+
"exclude": [["^", 1, "/.*"]], // Exclude that whole folder if the file indicated above is found in it
86+
"types": ["office"] // @types package to fetch instead
87+
}
88+
};
89+
6090
export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings {
6191
if (typeof protocolOptions.indentStyle === "string") {
6292
protocolOptions.indentStyle = indentStyle.get(protocolOptions.indentStyle.toLowerCase());
@@ -259,6 +289,7 @@ namespace ts.server {
259289
private readonly throttledOperations: ThrottledOperations;
260290

261291
private readonly hostConfiguration: HostConfiguration;
292+
private static safelist: SafeList = defaultTypeSafeList;
262293

263294
private changedFiles: ScriptInfo[];
264295

@@ -284,8 +315,6 @@ namespace ts.server {
284315

285316
this.typingsCache = new TypingsCache(this.typingsInstaller);
286317

287-
// ts.disableIncrementalParsing = true;
288-
289318
this.hostConfiguration = {
290319
formatCodeOptions: getDefaultFormatCodeSettings(this.host),
291320
hostInfo: "Unknown host",
@@ -831,7 +860,7 @@ namespace ts.server {
831860
getDirectoryPath(configFilename),
832861
/*existingOptions*/ {},
833862
configFilename,
834-
/*resolutionStack*/ [],
863+
/*resolutionStack*/[],
835864
this.hostConfiguration.extraFileExtensions);
836865

837866
if (parsedCommandLine.errors.length) {
@@ -1399,13 +1428,104 @@ namespace ts.server {
13991428
this.refreshInferredProjects();
14001429
}
14011430

1431+
/** Makes a filename safe to insert in a RegExp */
1432+
private static filenameEscapeRegexp = /[-\/\\^$*+?.()|[\]{}]/g;
1433+
private static escapeFilenameForRegex(filename: string) {
1434+
return filename.replace(this.filenameEscapeRegexp, "\\$&");
1435+
}
1436+
1437+
resetSafeList(): void {
1438+
ProjectService.safelist = defaultTypeSafeList;
1439+
}
1440+
1441+
loadSafeList(fileName: string): void {
1442+
const raw: SafeList = JSON.parse(this.host.readFile(fileName, "utf-8"));
1443+
// Parse the regexps
1444+
for (const k of Object.keys(raw)) {
1445+
raw[k].match = new RegExp(raw[k].match as {} as string, "i");
1446+
}
1447+
// raw is now fixed and ready
1448+
ProjectService.safelist = raw;
1449+
}
1450+
1451+
applySafeList(proj: protocol.ExternalProject): void {
1452+
const { rootFiles, typeAcquisition } = proj;
1453+
const types = (typeAcquisition && typeAcquisition.include) || [];
1454+
1455+
const excludeRules: string[] = [];
1456+
1457+
const normalizedNames = rootFiles.map(f => normalizeSlashes(f.fileName));
1458+
1459+
for (const name of Object.keys(ProjectService.safelist)) {
1460+
const rule = ProjectService.safelist[name];
1461+
for (const root of normalizedNames) {
1462+
if (rule.match.test(root)) {
1463+
this.logger.info(`Excluding files based on rule ${name}`);
1464+
1465+
// If the file matches, collect its types packages and exclude rules
1466+
if (rule.types) {
1467+
for (const type of rule.types) {
1468+
if (types.indexOf(type) < 0) {
1469+
types.push(type);
1470+
}
1471+
}
1472+
}
1473+
1474+
if (rule.exclude) {
1475+
for (const exclude of rule.exclude) {
1476+
const processedRule = root.replace(rule.match, (...groups: Array<string>) => {
1477+
return exclude.map(groupNumberOrString => {
1478+
// RegExp group numbers are 1-based, but the first element in groups
1479+
// is actually the original string, so it all works out in the end.
1480+
if (typeof groupNumberOrString === "number") {
1481+
if (typeof groups[groupNumberOrString] !== "string") {
1482+
// Specification was wrong - exclude nothing!
1483+
this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`);
1484+
// * can't appear in a filename; escape it because it's feeding into a RegExp
1485+
return "\\*";
1486+
}
1487+
return ProjectService.escapeFilenameForRegex(groups[groupNumberOrString]);
1488+
}
1489+
return groupNumberOrString;
1490+
}).join("");
1491+
});
1492+
1493+
if (excludeRules.indexOf(processedRule) === -1) {
1494+
excludeRules.push(processedRule);
1495+
}
1496+
}
1497+
}
1498+
else {
1499+
// If not rules listed, add the default rule to exclude the matched file
1500+
const escaped = ProjectService.escapeFilenameForRegex(root);
1501+
if (excludeRules.indexOf(escaped) < 0) {
1502+
excludeRules.push(escaped);
1503+
}
1504+
}
1505+
}
1506+
}
1507+
1508+
// Copy back this field into the project if needed
1509+
if (types.length > 0) {
1510+
proj.typeAcquisition = proj.typeAcquisition || { };
1511+
proj.typeAcquisition.include = types;
1512+
}
1513+
}
1514+
1515+
const excludeRegexes = excludeRules.map(e => new RegExp(e, "i"));
1516+
proj.rootFiles = proj.rootFiles.filter((_file, index) => !excludeRegexes.some(re => re.test(normalizedNames[index])));
1517+
}
1518+
14021519
openExternalProject(proj: protocol.ExternalProject, suppressRefreshOfInferredProjects = false): void {
14031520
// typingOptions has been deprecated and is only supported for backward compatibility
14041521
// purposes. It should be removed in future releases - use typeAcquisition instead.
14051522
if (proj.typingOptions && !proj.typeAcquisition) {
14061523
const typeAcquisition = convertEnableAutoDiscoveryToEnable(proj.typingOptions);
14071524
proj.typeAcquisition = typeAcquisition;
14081525
}
1526+
1527+
this.applySafeList(proj);
1528+
14091529
let tsConfigFiles: NormalizedPath[];
14101530
const rootFiles: protocol.ExternalFile[] = [];
14111531
for (const file of proj.rootFiles) {

0 commit comments

Comments
 (0)