From 6a7bc17bfed09c04791d39a84c2321a8241533d9 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Mon, 3 Apr 2017 23:03:14 -0700 Subject: [PATCH] Add tests and fix bugs --- .../unittests/tsserverProjectSystem.ts | 61 ++++++++++-- src/server/editorServices.ts | 93 ++++++++++++++++++- src/server/protocol.ts | 2 + src/server/session.ts | 6 ++ 4 files changed, 150 insertions(+), 12 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index b2fc4322242..8ef8e71b37f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -17,6 +17,30 @@ namespace ts.projectSystem { }) }; + const typeMapList = { + path: "/typeMapList.json", + content: JSON.stringify({ + "jquery": { + // jquery files can have names like "jquery-1.10.2.min.js" (or "jquery.intellisense.js") + "match": "/jquery(-(\\.\\d+)+)?(\\.intellisense)?(\\.min)?\\.js$", + "types": ["jquery"] + }, + "WinJS": { + "match": "^(.*/winjs)/base\\.js$", // If the winjs/base.js file is found.. + "exclude": [["^", 1, "/.*"]], // ..then exclude all files under the winjs folder + "types": ["winjs"] // And fetch the @types package for WinJS + }, + "Office Nuget": { + "match": "^(.*/1/office)/excel\\.debug\\.js$", // Office NuGet package is installed under a "1/office" folder + "exclude": [["^", 1, "/.*"]], // Exclude that whole folder if the file indicated above is found in it + "types": ["office"] // @types package to fetch instead + }, + "Minified files": { + "match": "^.*\\.min\\.js$" // Catch-all for minified files. Default exclude is the matched file. + } + }) + }; + export interface PostExecAction { readonly success: boolean; readonly callback: TI.RequestCompletedAction; @@ -1445,6 +1469,25 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); }); + it("ignores files excluded by the safe type list", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export let x = 5" + }; + const office = { + path: "lib/1/office/excel.debug.js", + content: "whoa do @@ not parse me ok thanks!!!" + }; + const host = createServerHost([typeMapList, file1, office]); + const projectService = createProjectService(host); + projectService.loadSafeList(typeMapList.path); + + projectService.openExternalProject({ projectFileName: "project", options: {}, rootFiles: toExternalFiles([file1.path, office.path]) }); + const proj = projectService.externalProjects[0]; + assert.deepEqual(proj.getFileNames(true), [file1.path]); + assert.deepEqual(proj.getTypeAcquisition().include, ["office"]); + }); + it("open file become a part of configured project if it is referenced from root file", () => { const file1 = { path: "/a/b/f1.ts", @@ -1695,7 +1738,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); }); - it ("loading files with correct priority", () => { + it("loading files with correct priority", () => { const f1 = { path: "/a/main.ts", content: "let x = 1" @@ -1720,14 +1763,14 @@ namespace ts.projectSystem { }); projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [ f1.path ]); + checkProjectActualFiles(projectService.configuredProjects[0], [f1.path]); projectService.closeClientFile(f1.path); projectService.openClientFile(f2.path); projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [ f1.path ]); - checkProjectActualFiles(projectService.inferredProjects[0], [ f2.path ]); + checkProjectActualFiles(projectService.configuredProjects[0], [f1.path]); + checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); }); it("tsconfig script block support", () => { @@ -1845,7 +1888,7 @@ namespace ts.projectSystem { // #3. Ensure no errors when compiler options aren't specified const config3 = { path: "/a/b/tsconfig.json", - content: JSON.stringify({ }) + content: JSON.stringify({}) }; host = createServerHost([file1, file2, config3, libFile], { executingFilePath: combinePaths(getDirectoryPath(libFile.path), "tsc.js") }); @@ -3381,13 +3424,13 @@ namespace ts.projectSystem { assert.equal((response)[0].projectUsesOutFile, expectedUsesOutFile, "usesOutFile"); } - it ("projectUsesOutFile should not be returned if not set", () => { + it("projectUsesOutFile should not be returned if not set", () => { test({}, /*expectedUsesOutFile*/ false); }); - it ("projectUsesOutFile should be true if outFile is set", () => { + it("projectUsesOutFile should be true if outFile is set", () => { test({ outFile: "/a/out.js" }, /*expectedUsesOutFile*/ true); }); - it ("projectUsesOutFile should be true if out is set", () => { + it("projectUsesOutFile should be true if out is set", () => { test({ out: "/a/out.js" }, /*expectedUsesOutFile*/ true); }); }); @@ -3468,7 +3511,7 @@ namespace ts.projectSystem { const cancellationToken = new TestServerCancellationToken(); const host = createServerHost([f1, config]); - const session = createSession(host, /*typingsInstaller*/ undefined, () => {}, cancellationToken); + const session = createSession(host, /*typingsInstaller*/ undefined, () => { }, cancellationToken); { session.executeCommandSeq({ command: "open", diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 30c7a87dc74..78108debef7 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -35,6 +35,10 @@ namespace ts.server { (event: ProjectServiceEvent): void; } + export interface SafeList { + [name: string]: { match: RegExp, exclude?: Array>, types?: string[] }; + } + function prepareConvertersForEnumLikeCompilerOptions(commandLineOptions: CommandLineOption[]): Map> { const map: Map> = createMap>(); for (const option of commandLineOptions) { @@ -259,6 +263,7 @@ namespace ts.server { private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; + private static safelist: SafeList = {}; private changedFiles: ScriptInfo[]; @@ -284,8 +289,6 @@ namespace ts.server { this.typingsCache = new TypingsCache(this.typingsInstaller); - // ts.disableIncrementalParsing = true; - this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host), hostInfo: "Unknown host", @@ -831,7 +834,7 @@ namespace ts.server { getDirectoryPath(configFilename), /*existingOptions*/ {}, configFilename, - /*resolutionStack*/ [], + /*resolutionStack*/[], this.hostConfiguration.extraFileExtensions); if (parsedCommandLine.errors.length) { @@ -1399,6 +1402,87 @@ namespace ts.server { this.refreshInferredProjects(); } + /** Makes a filename safe to insert in a RegExp */ + private static filenameEscapeRegexp = /[-\/\\^$*+?.()|[\]{}]/g; + private static escapeFilenameForRegex(filename: string) { + return filename.replace(this.filenameEscapeRegexp, "\\$&"); + } + + loadSafeList(fileName: string): void { + const raw: SafeList = JSON.parse(this.host.readFile(fileName, "utf-8")); + // Parse the regexps + for (const k of Object.keys(raw)) { + raw[k].match = new RegExp(raw[k].match as {} as string, "gi"); + } + // raw is now fixed and ready + ProjectService.safelist = raw; + } + + applySafeList(proj: protocol.ExternalProject): void { + const { rootFiles, typeAcquisition } = proj; + const types = (typeAcquisition && typeAcquisition.include) || []; + + const excludeRules: string[] = []; + + for (const name of Object.keys(ProjectService.safelist)) { + const rule = ProjectService.safelist[name]; + for (const root of rootFiles) { + if (rule.match.test(root.fileName)) { + this.logger.info(`Excluding files based on rule ${name}`); + + // If the file matches, collect its types packages and exclude rules + if (rule.types) { + for (const type of rule.types) { + if (types.indexOf(type) < 0) { + types.push(type); + } + } + } + + if (rule.exclude) { + for (const exclude of rule.exclude) { + const processedRule = root.fileName.replace(rule.match, (...groups: Array) => { + return exclude.map(groupNumberOrString => { + // RegExp group numbers are 1-based, but the first element in groups + // is actually the original string, so it all works out in the end. + if (typeof groupNumberOrString === "number") { + if (typeof groups[groupNumberOrString] !== "string") { + // Specification was wrong - exclude nothing! + this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`); + // * can't appear in a filename; escape it because it's feeding into a RegExp + return "\\*"; + } + return ProjectService.escapeFilenameForRegex(groups[groupNumberOrString]); + } + return groupNumberOrString; + }).join(""); + }); + + if (excludeRules.indexOf(processedRule) == -1) { + excludeRules.push(processedRule); + } + } + } + else { + // If not rules listed, add the default rule to exclude the matched file + if (excludeRules.indexOf(root.fileName) < 0) { + excludeRules.push(root.fileName); + } + } + } + } + + // Copy back this field into the project if needed + if (types.length > 0) { + proj.typeAcquisition = proj.typeAcquisition || { }; + proj.typeAcquisition.include = types; + } + } + + const excludeRegexes = excludeRules.map(e => new RegExp(e, "i")); + proj.rootFiles = proj.rootFiles.filter(file => !excludeRegexes.some(re => re.test(file.fileName))); + } + openExternalProject(proj: protocol.ExternalProject, suppressRefreshOfInferredProjects = false): void { // typingOptions has been deprecated and is only supported for backward compatibility // purposes. It should be removed in future releases - use typeAcquisition instead. @@ -1406,6 +1490,9 @@ namespace ts.server { const typeAcquisition = convertEnableAutoDiscoveryToEnable(proj.typingOptions); proj.typeAcquisition = typeAcquisition; } + + this.applySafeList(proj); + let tsConfigFiles: NormalizedPath[]; const rootFiles: protocol.ExternalFile[] = []; for (const file of proj.rootFiles) { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 0bf13df7f4b..bc96099f5e1 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -91,8 +91,10 @@ namespace ts.server.protocol { /* @internal */ export type BreakpointStatement = "breakpointStatement"; export type CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects"; + export type LoadTypesMap = "loadTypesMap"; export type GetCodeFixes = "getCodeFixes"; /* @internal */ + /* @internal */ export type GetCodeFixesFull = "getCodeFixes-full"; export type GetSupportedCodeFixes = "getSupportedCodeFixes"; } diff --git a/src/server/session.ts b/src/server/session.ts index 06b46d7d1bf..341ee3be950 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -186,6 +186,7 @@ namespace ts.server { /* @internal */ export const BreakpointStatement: protocol.CommandTypes.BreakpointStatement = "breakpointStatement"; export const CompilerOptionsForInferredProjects: protocol.CommandTypes.CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects"; + export const LoadTypesMap: protocol.CommandTypes.LoadTypesMap = "loadTypesMap"; export const GetCodeFixes: protocol.CommandTypes.GetCodeFixes = "getCodeFixes"; /* @internal */ export const GetCodeFixesFull: protocol.CommandTypes.GetCodeFixesFull = "getCodeFixes-full"; @@ -1765,6 +1766,11 @@ namespace ts.server { this.setCompilerOptionsForInferredProjects(request.arguments); return this.requiredResponse(true); }, + [CommandNames.LoadTypesMap]: (request: protocol.FileRequest) => { + const loadArgs = request.arguments; + this.projectService.loadSafeList(loadArgs.file); + return this.notRequired(); + }, [CommandNames.ProjectInfo]: (request: protocol.ProjectInfoRequest) => { return this.requiredResponse(this.getProjectInfo(request.arguments)); },