diff --git a/changelog.md b/changelog.md index 4b1586d..2455b24 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,7 @@ # Changelog ## Next -- Fixed the `.git` folder not being ignored when analysing multiple folders ([#25](https://github.com/Nixinova/LinguistJS/issues/25)). +- Fixed file paths not resolving properly when analysing multiple folders ([#25](https://github.com/Nixinova/LinguistJS/issues/25)). ## 2.6.0 *2023-06-29* @@ -21,7 +21,7 @@ *2023-01-11* - Fixed gitattributes wildcards not being applied into subfolders ([#17](https://github.com/Nixinova/LinguistJS/issues/17)). -# 2.5.3 +## 2.5.3 *2022-09-03* - Fixed a crash occurring when parsing heuristics for `.txt` files ([#16](https://github.com/Nixinova/LinguistJS/issues/16)). diff --git a/src/helpers/walk-tree.ts b/src/helpers/walk-tree.ts index 3b37198..ceb18d7 100644 --- a/src/helpers/walk-tree.ts +++ b/src/helpers/walk-tree.ts @@ -5,8 +5,26 @@ import { Ignore } from 'ignore'; let allFiles: Set; let allFolders: Set; +interface WalkInput { + /** Whether this is walking the tree from the root */ + init: boolean, + /** The common root absolute path of all folders being checked */ + commonRoot: string, + /** The absolute path that each folder is relative to */ + folderRoots: string[], + /** The absolute path of folders being checked */ + folders: string[], + gitignores: Ignore, + regexIgnores: RegExp[], +}; +interface WalkOutput { + files: string[], + folders: string[], +}; + /** Generate list of files in a directory. */ -export default function walk(init: boolean, root: string, folders: string[], gitignores: Ignore, regexIgnores: RegExp[]): { files: string[], folders: string[] } { +export default function walk(data: WalkInput): WalkOutput { + const { init, commonRoot, folderRoots, folders, gitignores, regexIgnores } = data; // Initialise files and folders lists if (init) { @@ -17,22 +35,24 @@ export default function walk(init: boolean, root: string, folders: string[], git // Walk tree of a folder if (folders.length === 1) { const folder = folders[0]; + const localRoot = folderRoots[0].replace(commonRoot, '').replace(/^\//, ''); // Get list of files and folders inside this folder const files = fs.readdirSync(folder).map(file => { // Create path relative to root - const base = paths.resolve(folder, file).replace(/\\/g, '/').replace(root, '.'); + const base = paths.resolve(folder, file).replace(/\\/g, '/').replace(commonRoot, '.'); // Add trailing slash to mark directories - const isDir = fs.lstatSync(paths.resolve(root, base)).isDirectory(); + const isDir = fs.lstatSync(paths.resolve(commonRoot, base)).isDirectory(); return isDir ? `${base}/` : base; }); // Loop through files and folders for (const file of files) { // Create absolute path for disc operations - const path = paths.resolve(root, file).replace(/\\/g, '/'); + const path = paths.resolve(commonRoot, file).replace(/\\/g, '/'); + const localPath = localRoot ? file.replace(`./${localRoot}/`, '') : file.replace('./', ''); // Skip if nonexistant or ignored const nonExistant = !fs.existsSync(path); - const isGitIgnored = gitignores.test(file.replace('./', '')).ignored; - const isRegexIgnored = regexIgnores.find(match => file.replace('./', '').match(match)); + const isGitIgnored = gitignores.test(localPath).ignored; + const isRegexIgnored = regexIgnores.find(match => localPath.match(match)); if (nonExistant || isGitIgnored || isRegexIgnored) continue; // Add absolute folder path to list allFolders.add(paths.resolve(folder).replace(/\\/g, '/')); @@ -40,23 +60,23 @@ export default function walk(init: boolean, root: string, folders: string[], git if (file.endsWith('/')) { // Recurse into subfolders allFolders.add(path); - walk(false, root, [path], gitignores, regexIgnores); + walk({ init: false, commonRoot: commonRoot, folderRoots, folders: [path], gitignores, regexIgnores }); } else { - // Add relative file path to list + // Add file path to list allFiles.add(path); } } } // Recurse into all folders else { - for (const path of folders) { - walk(false, root, [path], gitignores, regexIgnores); + for (const i in folders) { + walk({ init: false, commonRoot: commonRoot, folderRoots: [folderRoots[i]], folders: [folders[i]], gitignores, regexIgnores }); } } // Return absolute files and folders lists return { - files: [...allFiles].map(file => file.replace(/^\./, root)), + files: [...allFiles].map(file => file.replace(/^\./, commonRoot)), folders: [...allFolders], }; } diff --git a/src/index.ts b/src/index.ts index fa160a7..31f0dc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,10 +46,13 @@ async function analyse(input?: string | string[], opts: T.Options = {}): Promise if (opts.ignoredFiles) gitignores.add(opts.ignoredFiles); // Set a common root path so that vendor paths do not incorrectly match parent folders - const resolvedInput = input.map(path => paths.resolve(path).replace(/\\/g, '/')); + const normPath = (file: string) => file.replace(/\\/g, '/'); + const resolvedInput = input.map(path => normPath(paths.resolve(path))); const commonRoot = (input.length > 1 ? commonPrefix(resolvedInput) : resolvedInput[0]).replace(/\/?$/, ''); - const relPath = (file: string) => paths.relative(commonRoot, file).replace(/\\/g, '/'); - const unRelPath = (file: string) => paths.resolve(commonRoot, file).replace(/\\/g, '/'); + const localRoot = (folder: string) => folder.replace(commonRoot, '').replace(/^\//, ''); + const relPath = (file: string) => normPath(paths.relative(commonRoot, file)); + const unRelPath = (file: string) => normPath(paths.resolve(commonRoot, file)); + const localPath = (file: string) => localRoot(unRelPath(file)); // Load file paths and folders let files, folders; @@ -60,7 +63,7 @@ async function analyse(input?: string | string[], opts: T.Options = {}): Promise } else { // Uses directory on disc - const data = walk(true, commonRoot, input, gitignores, regexIgnores); + const data = walk({ init: true, commonRoot, folderRoots: resolvedInput, folders: resolvedInput, gitignores, regexIgnores }); files = data.files; folders = data.folders; } @@ -90,6 +93,8 @@ async function analyse(input?: string | string[], opts: T.Options = {}): Promise const customText = ignore(); if (!useRawContent && opts.checkAttributes) { for (const folder of folders) { + // TODO FIX: this is absolute when only 1 path given + const localFilePath = (path: string) => localRoot(folder) ? localRoot(folder) + '/' + localPath(path) : path; // Skip if folder is marked in gitattributes if (relPath(folder) && gitignores.ignores(relPath(folder))) { @@ -100,7 +105,8 @@ async function analyse(input?: string | string[], opts: T.Options = {}): Promise const ignoresFile = paths.join(folder, '.gitignore'); if (opts.checkIgnored && fs.existsSync(ignoresFile)) { const ignoresData = await readFile(ignoresFile); - gitignores.add(ignoresData); + const localIgnoresData = ignoresData.replace(/^[\/\\]/g, localRoot(folder) + '/'); + gitignores.add(localIgnoresData); } // Parse gitattributes @@ -111,16 +117,16 @@ async function analyse(input?: string | string[], opts: T.Options = {}): Promise const contentTypeMatches = attributesData.matchAll(/^(\S+).*?(-?binary|-?text)(?!=auto)/gm); for (const [_line, path, type] of contentTypeMatches) { if (['text', '-binary'].includes(type)) { - customText.add(path); + customText.add(localFilePath(path)); } if (['-text', 'binary'].includes(type)) { - customBinary.add(path); + customBinary.add(localFilePath(path)); } } // Custom vendor options const vendorMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-(vendored|generated|documentation)(?!=false)/gm); for (const [_line, path] of vendorMatches) { - gitignores.add(path); + gitignores.add(localFilePath(path)); } // Custom file associations const customLangMatches = attributesData.matchAll(/^(\S+).*[^-]linguist-language=(\S+)/gm); @@ -147,7 +153,7 @@ async function analyse(input?: string | string[], opts: T.Options = {}): Promise files = files.filter(file => !regexIgnores.find(match => match.test(file))); } else { - files = gitignores.filter(files.map(relPath)).map(unRelPath); + files = gitignores.filter(files.map(localPath)).map(unRelPath); } }