From dec54fd1113d7679e66c63da9bbc3b94803aae62 Mon Sep 17 00:00:00 2001 From: Stephen Carter <123964848+stephen-carter-at-sf@users.noreply.github.com> Date: Wed, 7 Aug 2024 14:58:32 -0400 Subject: [PATCH] NEW: @W-16263070@: Exclude from the workspace any dot files/folders unless explicitly added (#63) --- packages/code-analyzer-core/test/run.test.ts | 61 ++++----- .../code-analyzer-core/test/workspace.test.ts | 10 +- .../code-analyzer-engine-api/src/workspace.ts | 121 ++++++++++++------ .../sampleWorkspace/sub1/sub3/.gitignore | 1 + .../sub1/sub3/.someDotFolder/someFile.txt | 0 .../subFolder/someOtherFile.txt | 0 .../sub1/sub3/.someOtherDotFile | 0 .../sub1/sub3/node_modules/placeholder.txt | 0 .../sub3/node_modules/subFolder/someFile.txt | 0 .../sub1/sub3/someOtherFileInSub3.txt | 0 .../test/workspace.test.ts | 96 ++++++++++++-- 11 files changed, 207 insertions(+), 82 deletions(-) create mode 100644 packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.gitignore create mode 100644 packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someDotFolder/someFile.txt create mode 100644 packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someDotFolder/subFolder/someOtherFile.txt create mode 100644 packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someOtherDotFile create mode 100644 packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/node_modules/placeholder.txt create mode 100644 packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/node_modules/subFolder/someFile.txt create mode 100644 packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/someOtherFileInSub3.txt diff --git a/packages/code-analyzer-core/test/run.test.ts b/packages/code-analyzer-core/test/run.test.ts index 8608913e..0187b4d5 100644 --- a/packages/code-analyzer-core/test/run.test.ts +++ b/packages/code-analyzer-core/test/run.test.ts @@ -127,14 +127,12 @@ describe("Tests for the run method of CodeAnalyzer", () => { const expectedEngineRunOptions: engApi.RunOptions = { workspace: new engApi.Workspace([path.resolve('src')], "FixedId") }; - expect(stubEngine1.runRulesCallHistory).toEqual([{ - ruleNames: expectedStubEngine1RuleNames, - runOptions: expectedEngineRunOptions - }]); - expect(stubEngine2.runRulesCallHistory).toEqual([{ - ruleNames: expectedStubEngine2RuleNames, - runOptions: expectedEngineRunOptions - }]); + expect(stubEngine1.runRulesCallHistory).toHaveLength(1); + expect(stubEngine1.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine1RuleNames); + expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); + expect(stubEngine2.runRulesCallHistory).toHaveLength(1); + expect(stubEngine2.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine2RuleNames); + expectEquivalentRunOptions(stubEngine2.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); }); it("When specifying path start points as files and subfolders, then they are passed to each engine successfully", async () => { @@ -150,14 +148,12 @@ describe("Tests for the run method of CodeAnalyzer", () => { { file: path.resolve("test", "run.test.ts")} ] }; - expect(stubEngine1.runRulesCallHistory).toEqual([{ - ruleNames: expectedStubEngine1RuleNames, - runOptions: expectedEngineRunOptions - }]); - expect(stubEngine2.runRulesCallHistory).toEqual([{ - ruleNames: expectedStubEngine2RuleNames, - runOptions: expectedEngineRunOptions - }]); + expect(stubEngine1.runRulesCallHistory).toHaveLength(1); + expect(stubEngine1.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine1RuleNames); + expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); + expect(stubEngine2.runRulesCallHistory).toHaveLength(1); + expect(stubEngine2.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine2RuleNames); + expectEquivalentRunOptions(stubEngine2.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); }); it("When specifying path start points individual methods, then they are passed to each engine successfully", async () => { @@ -195,14 +191,12 @@ describe("Tests for the run method of CodeAnalyzer", () => { } ] }; - expect(stubEngine1.runRulesCallHistory).toEqual([{ - ruleNames: expectedStubEngine1RuleNames, - runOptions: expectedEngineRunOptions - }]); - expect(stubEngine2.runRulesCallHistory).toEqual([{ - ruleNames: expectedStubEngine2RuleNames, - runOptions: expectedEngineRunOptions - }]); + expect(stubEngine1.runRulesCallHistory).toHaveLength(1); + expect(stubEngine1.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine1RuleNames); + expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); + expect(stubEngine2.runRulesCallHistory).toHaveLength(1); + expect(stubEngine2.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine2RuleNames); + expectEquivalentRunOptions(stubEngine2.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); }); it("When the workspace provided is one that is not constructed from CodeAnalyzer's createWorkspace method, then it should still work", async () => { @@ -226,11 +220,10 @@ describe("Tests for the run method of CodeAnalyzer", () => { const expectedEngineRunOptions: engApi.RunOptions = { workspace: new engApi.Workspace([__dirname], "FixedId") }; - expect(stubEngine1.runRulesCallHistory).toEqual([{ - ruleNames: expectedStubEngine1RuleNames, - runOptions: expectedEngineRunOptions - }]); - expect(stubEngine2.runRulesCallHistory).toEqual([]); + expect(stubEngine1.runRulesCallHistory).toHaveLength(1); + expect(stubEngine1.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine1RuleNames); + expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); + expect(stubEngine2.runRulesCallHistory).toHaveLength(0); }); it("When zero rules are selected, then all engines should be skipped and returned results contain no violations", async () => { @@ -632,4 +625,14 @@ class DummyWorkspace implements Workspace { getFilesAndFolders(): string[] { return [__dirname]; } +} + +function expectEquivalentRunOptions(actual: engApi.RunOptions, expected: engApi.RunOptions): void { + expectEquivalentWorkspaces(actual.workspace, expected.workspace); + expect(actual.pathStartPoints).toEqual(expected.pathStartPoints); +} + +function expectEquivalentWorkspaces(actual: engApi.Workspace, expected: engApi.Workspace): void { + expect(actual.getWorkspaceId()).toEqual(expected.getWorkspaceId()); + expect(actual.getFilesAndFolders()).toEqual(expected.getFilesAndFolders()); } \ No newline at end of file diff --git a/packages/code-analyzer-core/test/workspace.test.ts b/packages/code-analyzer-core/test/workspace.test.ts index bd50978f..8089cd9e 100644 --- a/packages/code-analyzer-core/test/workspace.test.ts +++ b/packages/code-analyzer-core/test/workspace.test.ts @@ -40,11 +40,15 @@ describe("Tests for the createWorkspace method of CodeAnalyzer", () => { expect(workspace.getFilesAndFolders()).toEqual([path.resolve('test')]); }); - it("When including files we care not to process like .gitignore file or a node_modules folder, then they are removed", async () => { + it("When explicitly including files that we normally would ignore, then they are still included since they were explicitely added", async () => { const workspace: Workspace = await codeAnalyzer.createWorkspace([ 'test/test-data/sampleWorkspace/node_modules/place_holder.txt', 'test/test-data/sampleWorkspace/someFile.txt', - 'test/test-data/sampleWorkspace/.gitignore',]); - expect(workspace.getFilesAndFolders()).toEqual([path.resolve('test', 'test-data', 'sampleWorkspace', 'someFile.txt')]); + 'test/test-data/sampleWorkspace/.gitignore']); + expect(workspace.getFilesAndFolders()).toEqual([ + path.resolve('test', 'test-data', 'sampleWorkspace', 'node_modules', 'place_holder.txt'), + path.resolve('test', 'test-data', 'sampleWorkspace', 'someFile.txt'), + path.resolve('test', 'test-data', 'sampleWorkspace', '.gitignore') + ].sort()); }); }); \ No newline at end of file diff --git a/packages/code-analyzer-engine-api/src/workspace.ts b/packages/code-analyzer-engine-api/src/workspace.ts index 422fc83d..b478a55d 100644 --- a/packages/code-analyzer-engine-api/src/workspace.ts +++ b/packages/code-analyzer-engine-api/src/workspace.ts @@ -1,13 +1,15 @@ import fs from "node:fs"; import path from "node:path"; -const UNWANTED_FOLDERS: string[] = ['node_modules', '.git', '.github']; -const UNWANTED_FILES: string[] = ['code_analyzer_config.yml', 'code_analyzer_config.yaml', '.gitignore']; +const NON_DOT_FOLDERS_TO_EXCLUDE: string[] = ['node_modules']; +const NON_DOT_FILES_TO_EXCLUDE: string[] = ['code_analyzer_config.yml', 'code_analyzer_config.yaml']; export class Workspace { private static nextId: number = 0; private readonly workspaceId: string; - private readonly filesAndFolders: string[]; + private readonly rawFilesAndFolders: string[]; + + private filesAndFolders?: string[]; private expandedFiles?: string[]; private workspaceRoot?: string | null; @@ -19,9 +21,7 @@ export class Workspace { */ constructor(absoluteFileAndFolderPaths: string[], workspaceId?: string) { this.workspaceId = workspaceId || `workspace${++Workspace.nextId}`; - this.filesAndFolders = removeRedundantPaths(absoluteFileAndFolderPaths) - .filter(isWantedPath) - .map(removeTrailingPathSep); + this.rawFilesAndFolders = absoluteFileAndFolderPaths; } /** @@ -33,10 +33,13 @@ export class Workspace { /** * Returns the unique list of files and folders that were used to construct the workspace. - * Any files or folders that Code Analyzer chooses to ignore (like .gitignore files, node_modules folders, etc.) - * are excluded from the list. + * Note that besides removing redundant paths, no other filtering is done. For example, if a user explicitly + * provided to the Workspace constructor a .dotFile then we will not exclude this file. */ getFilesAndFolders(): string[] { + if (!this.filesAndFolders) { + this.filesAndFolders = this.removeRedundantPaths(this.rawFilesAndFolders).map(removeTrailingPathSep); + } return this.filesAndFolders; } @@ -49,7 +52,7 @@ export class Workspace { */ getWorkspaceRoot(): string | null { if (this.workspaceRoot === undefined) { - this.workspaceRoot = calculateLongestCommonParentFolderOf(this.getFilesAndFolders()); + this.workspaceRoot = calculateLongestCommonParentFolderOf(this.rawFilesAndFolders); } return this.workspaceRoot; } @@ -59,34 +62,81 @@ export class Workspace { * This list is composed of the files that getFilesAndFolders() returns plus any files found recursively inside * any of the folders that getFilesAndFolders() returns. That is, the folders are expanded so that the resulting * list only contains file paths. - * Any files that Code Analyzer chooses to ignore (like .gitignore files, files in node_modules folders, etc.) - * are excluded from the list. + * Any files underneath the workspace root that Code Analyzer chooses to ignore (like .gitignore files, + * files in node_modules folders, etc.) are automatically excluded unless they were explicitly provided when + * constructing the workspace. */ async getExpandedFiles(): Promise { if (!this.expandedFiles) { - this.expandedFiles = (await expandToListAllFiles(this.filesAndFolders)).filter(isWantedPath); + this.expandedFiles = (await expandToListAllFiles(this.getFilesAndFolders())).filter(f => !this.shouldExclude(f)); } return this.expandedFiles as string[]; } -} -/** - * Removes redundant paths. - * If a user supplies a parent folder and subfolder of file underneath the parent folder, then we can safely - * remove that subfolder or file. Also, if we find duplicate entries, we remove those as well. - */ -function removeRedundantPaths(absolutePaths: string[]): string[] { - const pathsSortedByLength: string[] = absolutePaths.sort((a, b) => a.length - b.length); - const filteredPaths: string[] = []; - for (const currentPath of pathsSortedByLength) { - const isAlreadyContained = filteredPaths.some(existingPath => - currentPath.startsWith(existingPath + path.sep) || existingPath === currentPath - ); - if (!isAlreadyContained) { - filteredPaths.push(currentPath); + /** + * Returns whether a path should be excluded or not (like paths under a "node_modules" folder should be excluded) + * Idea: In the future, we might consider having a .code_analyzer_ignore file or something that users can create + * which could help the user have better control over what files are excluded. + * + * Note: When determining whether to exclude a file or not, we base it entirely off looking at the + * portion of the path underneath the workspace root. This allows us to not accidentally remove all files if the + * workspace happens to live under a dot folder for example. Additionally, we do not remove any files that have + * been explicitly provided to the Workspace constructor, which allows users to target .dot folders and files if + * they choose to do so. + */ + private shouldExclude(fileOrFolder: string): boolean { + return this.isExcludeCandidate(fileOrFolder) && !this.excludeCandidateWasExplicitlyProvided(fileOrFolder); + } + private isExcludeCandidate(fileOrFolder: string): boolean { + const relativeFileOrFolder: string = this.makeRelativeToWorkspaceRoot(fileOrFolder); + if (relativeFileOrFolder.length === 0) { // folder is equal to the workspace root + return false; + } + return NON_DOT_FOLDERS_TO_EXCLUDE.some(f => relativeFileOrFolder.includes(`${path.sep}${f}${path.sep}`)) || + NON_DOT_FILES_TO_EXCLUDE.includes(path.basename(relativeFileOrFolder)) || + relativeFileOrFolder.includes(`${path.sep}.`); + } + private excludeCandidateWasExplicitlyProvided(fileOrFolder: string): boolean { + if (this.rawFilesAndFolders.includes(fileOrFolder)) { + return true; } + const parentFolder: string = path.dirname(fileOrFolder); + if (this.isExcludeCandidate(parentFolder)) { + return this.excludeCandidateWasExplicitlyProvided(parentFolder); + } + return false; + } + + /** + * Returns the file or folder as a relative path, relative to the workspace root + */ + private makeRelativeToWorkspaceRoot(fileOrFolder: string): string { + if(this.getWorkspaceRoot()) { + return fileOrFolder.slice(this.getWorkspaceRoot()!.length); + } + /* istanbul ignore next */ + return fileOrFolder; + } + + /** + * Removes redundant paths. + * If a user supplies a parent folder and subfolder of file underneath the parent folder, then we can safely + * remove that subfolder or file (unless it is an excludeCandidate that has been explicitly requested to keep). + * Also, if we find duplicate entries, we remove those as well. + */ + private removeRedundantPaths(absolutePaths: string[]): string[] { + const pathsSortedByLength: string[] = [...absolutePaths].sort((a, b) => a.length - b.length); + const filteredPaths: string[] = []; + for (const currentPath of pathsSortedByLength) { + const isAlreadyContained = filteredPaths.some(existingPath => + currentPath.startsWith(existingPath + path.sep) || existingPath === currentPath + ); + if (!isAlreadyContained || this.isExcludeCandidate(currentPath)) { + filteredPaths.push(currentPath); + } + } + return filteredPaths.sort(); // sort alphabetically } - return filteredPaths.sort(); // sort alphabetically } /** @@ -97,31 +147,22 @@ function removeTrailingPathSep(absolutePath: string): string { absolutePath.slice(0, absolutePath.length - path.sep.length) : absolutePath; } -/** - * Returns whether a path should be included or not (like those that live under a "node_modules" folder should not be) - * Idea: in the future, we might consider having a .code_analyzer_ignore file or something that users can create - */ -function isWantedPath(absolutePath: string): boolean { - return !UNWANTED_FOLDERS.some(f => absolutePath.includes(`${path.sep}${f}${path.sep}`)) && - !UNWANTED_FILES.includes(path.basename(absolutePath)); -} - /** * Expands a list of files and/or folders to be a list of all contained files, including the files found in subfolders */ export async function expandToListAllFiles(absoluteFileOrFolderPaths: string[]): Promise { - const allFiles: string[] = []; + const allFiles: Set = new Set(); // Using a set to guarantee uniqueness async function processPath(currentPath: string): Promise { if ((await fs.promises.stat(currentPath)).isDirectory()) { const subPaths: string[] = await fs.promises.readdir(currentPath); const absSubPaths: string[] = subPaths.map(f => path.join(currentPath, f)); await Promise.all(absSubPaths.map(processPath)); // Process subdirectories recursively } else { - allFiles.push(currentPath); + allFiles.add(currentPath); } } await Promise.all(absoluteFileOrFolderPaths.map(processPath)); - return allFiles.sort(); + return [... allFiles].sort(); } /** diff --git a/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.gitignore b/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.gitignore new file mode 100644 index 00000000..9bfb325f --- /dev/null +++ b/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.gitignore @@ -0,0 +1 @@ +!node_modules/ \ No newline at end of file diff --git a/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someDotFolder/someFile.txt b/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someDotFolder/someFile.txt new file mode 100644 index 00000000..e69de29b diff --git a/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someDotFolder/subFolder/someOtherFile.txt b/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someDotFolder/subFolder/someOtherFile.txt new file mode 100644 index 00000000..e69de29b diff --git a/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someOtherDotFile b/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/.someOtherDotFile new file mode 100644 index 00000000..e69de29b diff --git a/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/node_modules/placeholder.txt b/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/node_modules/placeholder.txt new file mode 100644 index 00000000..e69de29b diff --git a/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/node_modules/subFolder/someFile.txt b/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/node_modules/subFolder/someFile.txt new file mode 100644 index 00000000..e69de29b diff --git a/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/someOtherFileInSub3.txt b/packages/code-analyzer-engine-api/test/test-data/sampleWorkspace/sub1/sub3/someOtherFileInSub3.txt new file mode 100644 index 00000000..e69de29b diff --git a/packages/code-analyzer-engine-api/test/workspace.test.ts b/packages/code-analyzer-engine-api/test/workspace.test.ts index 4adaa203..9b45b797 100644 --- a/packages/code-analyzer-engine-api/test/workspace.test.ts +++ b/packages/code-analyzer-engine-api/test/workspace.test.ts @@ -32,15 +32,7 @@ describe('Tests for the Workspace class', () => { expect(workspace.getFilesAndFolders()).toEqual([__dirname]); }); - it("When including files we don't care to process like .gitignore file or a node_modules folder, then they are removed", async () => { - const workspace: Workspace = new Workspace([ - path.join(SAMPLE_WORKSPACE_FOLDER ,'node_modules', 'place_holder.txt'), - path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.txt'), - path.join(SAMPLE_WORKSPACE_FOLDER ,'.gitignore')]); - expect(workspace.getFilesAndFolders()).toEqual([path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.txt')]); - }); - - it('When calling getExpandedFiles, then all files underneath all subfolders are found', async () => { + it('When calling getExpandedFiles, then all files underneath all subfolders are found while excluding dot files/folders and node_modules', async () => { const workspace: Workspace = new Workspace([path.join(__dirname, 'test-data', 'sampleWorkspace')]); expect(await workspace.getExpandedFiles()).toEqual([ path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.txt'), @@ -49,10 +41,22 @@ describe('Tests for the Workspace class', () => { path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile', 'dummy.txt'), path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile1InSub2.txt'), path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile2InSub2.txt'), - path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.txt') + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someOtherFileInSub3.txt') ].sort()); }); + it("When explicitly including files we normally don't care to process like .gitignore file or a node_modules folder, then we still include them", async () => { + const sampleFiles: string[] = [ + path.join(SAMPLE_WORKSPACE_FOLDER ,'sub1', 'sub3', 'node_modules', 'placeholder.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER ,'sub1', 'sub3', '.gitignore') + ].sort(); + const workspace: Workspace = new Workspace(sampleFiles); + expect(workspace.getFilesAndFolders()).toEqual(sampleFiles); + expect(await workspace.getExpandedFiles()).toEqual(sampleFiles); + }); + it('When workspace is empty, then getExpandedFiles is empty', async () => { const workspace: Workspace = new Workspace([]); expect(await workspace.getExpandedFiles()).toEqual([]); @@ -145,6 +149,78 @@ describe('Tests for the Workspace class', () => { const workspace: Workspace = new Workspace([__dirname + path.sep]); expect(workspace.getWorkspaceRoot()).toEqual(__dirname); }); + + it('When a workspace consists a dot file/folder as a direct child underneath the workspace root (as opposed to indirectly) in it, then it should be excluded', async () => { + // This case is really a sanity check against a specific implementation that we have. It may seem like an + // arbitrary test, but it will help catch things if our implementation does special handling around direct vs + // indirect children. + const folderDirectlyContainingDotFileAndDotFolder: string = path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3'); + const workspace: Workspace = new Workspace([folderDirectlyContainingDotFileAndDotFolder]); + // Sanity checks: + expect(workspace.getWorkspaceRoot()).toEqual(folderDirectlyContainingDotFileAndDotFolder); + expect(workspace.getFilesAndFolders()).toEqual([folderDirectlyContainingDotFileAndDotFolder]); + // Actual verification - should not include dot files or folders: + expect(await workspace.getExpandedFiles()).toEqual([ + path.join(folderDirectlyContainingDotFileAndDotFolder, 'someFileInSub3.txt'), + path.join(folderDirectlyContainingDotFileAndDotFolder, 'someOtherFileInSub3.txt') + ]); + }); + + it('When a workspace root happens to live under a dot folder, then the files are not excluded', async () => { + const dotFolder: string = path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', '.someDotFolder'); + const workspace: Workspace = new Workspace([ + path.join(dotFolder, 'subFolder'), + path.join(dotFolder, 'someFile.txt')]); + expect(workspace.getFilesAndFolders()).toEqual([ + path.join(dotFolder, 'subFolder'), + path.join(dotFolder, 'someFile.txt',) + ].sort()); + expect(await workspace.getExpandedFiles()).toEqual([ + path.join(dotFolder, 'subFolder', 'someOtherFile.txt'), + path.join(dotFolder, 'someFile.txt',) + ].sort()); + }); + + it('When a workspace root happens to live under a node_modules folder, then the files are not excluded', async () => { + const nodeModulesFolder: string = path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'node_modules'); + const workspace: Workspace = new Workspace([ + path.join(nodeModulesFolder, 'placeholder.txt'), + path.join(nodeModulesFolder, 'subFolder'), + ]); + expect(workspace.getFilesAndFolders()).toEqual([ + path.join(nodeModulesFolder, 'placeholder.txt'), + path.join(nodeModulesFolder, 'subFolder') + ].sort()); + expect(await workspace.getExpandedFiles()).toEqual([ + path.join(nodeModulesFolder, 'placeholder.txt'), + path.join(nodeModulesFolder, 'subFolder', 'someFile.txt') + ].sort()); + }); + + it('When explicitly including paths that lives in a .dotFolder or a node_modules folder then it should still be included even if the .dotFolder is underneath the workspace root', async () => { + const workspace: Workspace = new Workspace([ + path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3'), // We want everything from the parent folder and to exclude the normal candidates except for the explicitly included files: + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', '.someDotFolder', 'subFolder'), // This is not redundant since normally it would be excluded + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'node_modules', 'subFolder', 'someFile.txt'), // This is not redundant since normally it would be excluded + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.txt'), // This is redundant since we already have its parent + ]); + expect(workspace.getFilesAndFolders()).toEqual([ + path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', '.someDotFolder', 'subFolder'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'node_modules', 'subFolder', 'someFile.txt'), + ].sort()); + + // This should not contain the .someDotFolder/someFile.txt file since it wasn't explicitly listed + expect(await workspace.getExpandedFiles()).toEqual([ + path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', '.someDotFolder', 'subFolder', 'someOtherFile.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'node_modules', 'subFolder', 'someFile.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.txt'), + path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someOtherFileInSub3.txt') + ].sort()); + }); }); function getAbsoluteRootFolder(): string {