diff --git a/gitnexus/src/core/ingestion/languages/r/package-config.ts b/gitnexus/src/core/ingestion/languages/r/package-config.ts index e1d0e4d94..e7abcfcca 100644 --- a/gitnexus/src/core/ingestion/languages/r/package-config.ts +++ b/gitnexus/src/core/ingestion/languages/r/package-config.ts @@ -237,6 +237,16 @@ function preferredPackageDir(pkgDir: string): string { /** Global directory budget for {@link hidesUndiscoveredPackage} across all its roots. */ const HIDDEN_SCAN_BUDGET = 5000; +/** + * The `Package:` field of a `DESCRIPTION` file's text, or `undefined` when it has none. A leading + * UTF-8 byte-order mark is not part of the first field name, and a field with an empty value + * names nothing: `[ \t]*`, not `\s*`, so the value never begins on the next line. + */ +function descriptionPackageName(content: string): string | undefined { + const text = content.charCodeAt(0) === 0xfeff ? content.slice(1) : content; + return /^Package:[ \t]*(\S+)/m.exec(text)?.[1]; +} + /** * Scan the subtrees the main walk skipped for a package it never discovered. * @@ -271,8 +281,8 @@ export async function hidesUndiscoveredPackage( } else if (entry.isFile() && entry.name === 'DESCRIPTION') { try { const content = await fs.readFile(path.join(dir, entry.name), 'utf-8'); - const pkgMatch = content.match(/^Package:\s*(\S+)/m); - if (pkgMatch && !knownPackageNames.has(pkgMatch[1])) return true; + const pkgName = descriptionPackageName(content); + if (pkgName !== undefined && !knownPackageNames.has(pkgName)) return true; } catch { return true; } @@ -325,9 +335,8 @@ export async function loadRPackageConfig(repoRoot: string): Promise { + const root = pool.dir(); + fs.writeFileSync(path.join(root, 'DESCRIPTION'), description); + return root; +}; + +describe('DESCRIPTION Package: field', () => { + it('discovers a package whose DESCRIPTION starts with a UTF-8 byte-order mark', async () => { + const config = await loadRPackageConfig(repoWith('Package: bompkg\nVersion: 1.0\n')); + expect([...(config?.packages ?? [])]).toEqual([['bompkg', '']]); + }); + + it('sees a byte-order-marked package when scanning for a hidden one', async () => { + const root = repoWith('Package: bompkg\nVersion: 1.0\n'); + expect(await hidesUndiscoveredPackage([root], new Set())).toBe(true); + expect(await hidesUndiscoveredPackage([root], new Set(['bompkg']))).toBe(false); + }); + + it('does not take the next line as the value of an empty Package: field', async () => { + const config = await loadRPackageConfig(repoWith('Package:\nVersion: 1.0\n')); + expect([...(config?.packages.keys() ?? [])]).toEqual([]); + const root = repoWith('Package:\nVersion: 1.0\n'); + expect(await hidesUndiscoveredPackage([root], new Set())).toBe(false); + }); + + it('still reads an ordinary field, with CRLF line endings and extra blanks', async () => { + const config = await loadRPackageConfig(repoWith('Package: spaced\r\nVersion: 1.0\r\n')); + expect([...(config?.packages.keys() ?? [])]).toEqual(['spaced']); + }); +});