fix(r): strip backticks from NAMESPACE export entries in linear time

The pattern /^`+|`+$/g is quadratic on an entry such as x, a long run of
backticks, then y; the entry is repository-controlled text read on every
importFrom lookup of a name the package does not export. Trim with two scans.
This commit is contained in:
Mick Cooney 2026-10-16 17:12:15 +01:00
parent b270621383
commit 40d3e6c987
2 changed files with 21 additions and 1 deletions

View file

@ -152,7 +152,17 @@ function packageDefIndex(
return byPackage.get(pkg);
}
const stripBackticks = (s: string): string => s.replace(/^`+|`+$/g, '');
/**
* `s` without its leading and trailing backticks. Two scans, not `/^`+|`+$/g`: the pattern is
* quadratic on `x` + n backticks + `y`, and a NAMESPACE entry is repository-controlled text.
*/
const stripBackticks = (s: string): string => {
let start = 0;
let end = s.length;
while (start < end && s.charCodeAt(start) === 96) start++;
while (end > start && s.charCodeAt(end - 1) === 96) end--;
return s.slice(start, end);
};
/**
* Resolve a NAMESPACE `importFrom(pkg, importedName)` to the `R/` file(s) of the

View file

@ -637,6 +637,16 @@ describe('resolveRImportTarget — named imports (NAMESPACE importFrom)', () =>
expect(resolve(t, 'scorelib', 'g')).toEqual([F]);
});
it('looks a name up among export entries that hold a long run of backticks in linear time', () => {
// `/^`+|`+$/g` took ~12 s per entry on this shape; the bound leaves a ten-fold margin.
const hostile = `x${'`'.repeat(200_000)}y`;
const s = setup({ [F]: src }, { exports: [hostile, 'f'] });
const start = performance.now();
const answers = [resolve(s, 'scorelib', 'hidden'), resolve(s, 'scorelib', 'f')];
expect(performance.now() - start).toBeLessThan(2_000);
expect(answers).toEqual([null, [F]]);
});
it('treats a package without a NAMESPACE file as exporting everything', () => {
const s = setup({ [F]: src }, undefined);
expect(resolve(s, 'scorelib', 'hidden')).toEqual([F]);