Last 12 weeks · 27 commits
2 of 6 standards met
Fixes #319. When normalizing negative indices in , , , and , indices below previously looped with , causing multiple wraps around the string and resolving to arbitrary in-bounds positions instead of matching semantics. Changes Replaced with across index resolution paths. Added tests verifying behavior when indices are less than .
The bug collected matches by driving in a loop: A zero-length match does not move , so the loop rematches at the same index forever. Any global regexp that can match the empty string hangs and then runs the process out of memory: , , , , and are all affected, so commenting out every line — a fairly ordinary thing to reach for — cannot be done today. The same loop never reset , so a regexp that has already been used resumes from wherever it stopped and silently skips earlier matches, where always starts from the beginning of the string: The fix is used instead. It steps over an empty match, and it does so by a whole code point when the regexp is unicode-aware, which a hand-rolled would get wrong for astral characters. is reset first, since starts from the regexp's own . That leaves the empty match reaching , which rejects a zero-length range. An empty match spans no characters, so there is no range to overwrite — the substitution is an insertion at the matched position, which is exactly what does with one. It is applied with , so it belongs to the range starting at that index, the same way an overwrite does. The string overloads had the same gap and are fixed alongside: threw, and could not terminate because clamps its start index to the string length and so never reports . Every case below now agrees with the built-in method: Notes README documents only two differences from , and neither covers empty matches, so this brings the code in line with what is already documented. The one behaviour worth stating — that a zero-length match inserts — is added there. The change is additive. I diffed 2406 combinations of source string, string and regexp pattern, string and function substitution, / and against : zero differences outside the empty-match cases above, which previously hung or threw. The existing 252 tests pass unchanged; 7 tests are added. , and both projects are clean. vs for the insertion is the one judgement call here. It is invisible unless something else is inserted at the same index, and keeps the replacement part of the range it replaced, the way does. Happy to switch it if you would rather it went the other way. Closes #336.
Version / @ 1bb4cce What happens collects matches by driving in a loop: A zero-length match does not advance , so the loop rematches at the same index forever. Any global regexp that can match the empty string never terminates — it allocates matches until the process runs out of memory: , , , , and are all affected, so prefixing or suffixing every line cannot be done through today. handles all of them: Second, smaller problem in the same loop is never reset before the scan, so a global regexp that has already been used resumes from wherever it stopped and silently skips earlier matches. always starts from the beginning of the string: Nothing is thrown here, the wrong characters are simply left alone. Non-global and string overloads Once the loop terminates, an empty match reaches , which rejects a zero-length range, so the non-global path throws rather than hangs: The string overloads have the same gap. throws as above, and cannot terminate either, for a different reason — clamps its start index to the string length, so on a 3-character string returns rather than and the loop never ends: Expected An empty match spans no characters, so there is no range to overwrite — the substitution is an insertion at the matched position, which is what does with one. The README lists only two differences from ("always match against the original string", "mutates the magic string state") and empty matches are not among them. So: terminate, start global regexps from the beginning of the string, and insert at a zero-length match. I have a PR ready for this.
This PR adds the inspect.software health badge to the README. Why this PR? Your project ranks in the upper rating bands of our open-source health index (which measures maintainability, responsiveness, and security). This badge makes that standing objectively visible to your users. Our index is a free public good, and scores cannot be bought. Technical details: No tracking: Static SVG served via GitHub's CDN. No JavaScript, no third-party tracking. No maintenance: The badge updates automatically after every inspection. Transparent:** Links directly to your full report, based on our open methodology. If you prefer to keep your README minimal, feel free to close this PR without replying. Your report will remain publicly available and up-to-date either way.
and walk from one source to the next, but never look at the separator that puts between them. Two things fall out of that. The separator survives a trim that should have removed it. Once the leading source is fully whitespace, the next thing in the output is its separator, and the walk skips straight over it to the following source: And the trim keeps going past a separator it should have stopped at. With , the output is . Trimming whitespace from the start should take the three leading spaces and stop at the semicolon, but the old walk carried on into the next source and removed the spaces after the semicolon too, which are not at the start any more: Both loops now give the separator its turn in the walk, so it is trimmed when it matches and stops the trim when it does not. This is the same shape as #322, #323 and #327: a method that mirrors everywhere except the separator. Tests The block had three tests and none of them had a source that trims away completely, which is why this stayed hidden. Four tests added. Against master all four fail: The two existing cases where the first source has real content ( and ) are untouched, since the walk still stops at the first source that does not trim away. Suite goes from 248 to 252 passing. is clean and is clean.
drops state that the original carries into its sourcemap, in both and . has four fields: , , and . forwards and through the constructor and copies by hand a few lines later. is the one it never touches. It also leaves at the fresh the constructor makes, so the names that records never reach the clone. is left alone on purpose: it is a lazy cache that recomputes from , and the clone gets the same . treats a source as carrying four properties: passed on and only, so a cloned bundle loses the per source ignore-list hint and its indent exclusion ranges. Passing them explicitly does not break the existing fallback where reads a missing property off . I checked both paths: a hint set on the source descriptor and a hint set on the itself both survive the clone now, and a bundle with no hint still reports . Tests Neither block covered any of this, which is what made it worth checking. 's block already asserts , , , and are carried over, so the intent is clear; these were simply missed. Five tests added. Against master exactly four of them fail: The fifth, , passes both before and after. It guards the fix rather than the bug: has to be a copy, so writing to the clone must not show up in the original. Suite goes from 242 to 247 passing. is clean and is clean.
Repository: Rich-Harris/magic-string. Description: Manipulate strings like a wizard Stars: 2770, Forks: 125. Primary language: TypeScript. Languages: TypeScript (81.8%), JavaScript (18.1%), HTML (0%). License: MIT. Latest release: v1.2.2 (5d ago). Open PRs: 7, open issues: 19. Last activity: 5d ago. Community health: 42%. Top contributors: Rich-Harris, antfu, mourner, greenkeeperio-bot, TrySound, btea, guybedford, wahidrizka, eventualbuddha, sapphi-red and others.