| Age | Commit message (Collapse) | Author |
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The write-back probe discarded the file_put_contents result and returned
a hard-coded false, so the comparison against false always held and the
error branch was taken for every path is_writable() rejected. Compare
the write result instead, matching the PHP original.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Silencer only lowers the PHP error_reporting() level and re-throws
whatever the guarded work raises. A region that never reaches the PHP
runtime has no level to lower and emits no diagnostic on failure, so
wrapping it is indistinguishable from running it unguarded.
The pair kept in Application::hint_common_errors brackets a
getComposer() call, which loads installed plugins and dispatches
PluginEvents::INIT.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
tokio's File returns from write_all once the blocking write is queued, so
the tail of the response body was still in flight when the download future
resolved. The caller then renamed the file, stat'd it and copied it into
the cache, so a package could land in the file cache truncated. The first
run still extracted the complete file and only a later run reading that
cache entry failed with "End-of-central-directory signature not found".
Flushing also surfaces the last write's error, which used to be dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
|
|
Comments that pointed at design notes kept outside the repository are dead
ends for anyone reading only the tree, so what each of them explained now
lives in a tagged TODO at the site it applies to. Several of those sites
also stated something the implementation does not do, and the TODOs record
the actual gap instead: the two halves of the codec recognize handle
descriptors by different rules, the scripts Command path drops the exception
class and collects output in a BufferedOutput that cannot carry an
interactive command, find_shortest_path panics where PHP throws, and the
package dispatch hand-rolls the variant selection AnyPackage should own.
The classifier document likewise described rust-snapshot,
plugin-constructible and several of the open questions as designed rather
than as built.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
file_get_contents() and file_get_contents_with_max_length() return
Vec<u8> instead of a from_utf8_lossy'd String.
Call sites whose consumer takes a &str still convert lossily and are
marked TODO(bytes).
file_get_contents_with_max_length() now reads at most the requested
number of bytes instead of the whole file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`config --list` and `config <key>` rendered a value with `as_string()`,
which yields None for everything but a string, so every int-valued
setting printed as an empty string: `cache-ttl`, `cache-files-ttl` and
the resolved value of `cache-files-maxsize`. PHP builds that output by
concatenating the value into the message, which casts it, so the ports
now go through `php_to_string`.
Two more differences in the same rendering path:
The `[a, b]` branch flattened the array through `as_list()`, which sees
only `PhpMixed::List`, so a keyed array reaching it rendered as `[]`.
PHP takes that branch whenever the first key is numeric, so the values
now come from `array_values_mixed`, which covers both representations.
The raw-vs-resolved comparison deciding between `raw (value)` and
`value` compared the two as strings. PHP compares with `!==`, which
also compares the type: with a `cache-ttl` of `"15552000"` in
composer.json, the raw string and the resolved int are not identical
and Composer prints `15552000 (15552000)`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Composer shells out to `rm -rf` because PHP has no recursive directory removal.
Rust has one, and every package installed from a dist archive pays for a removal:
the extraction pipeline drops its temporary directory once per package, and
uninstalling a package removes its whole tree.
The asynchronous path goes through `tokio::fs` rather than `std::fs`, so the walk
runs on a blocking thread and the sibling installs the reactor is driving keep
making progress, the way they did while the subprocess was working.
Installing laravel/laravel (109 packages) from a warm cache drops from 3.85 to
3.19 CPU seconds. The removals run concurrently, so on an idle 16-core machine
they never reach the critical path and wall time is unchanged at 1.65 s; pinned
to two cores it falls from 2.33 s to 2.20 s. Pruning the 33 dev packages with
`install --no-dev`, where whole package trees are removed rather than empty
temporary directories, drops from 844 ms to 806 ms even on 16 cores.
Windows keeps the `rmdir /S /Q` subprocess, and both platforms keep falling back
to `remove_directory_php` when the fast path does not clear the directory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`fund_command.rs` claimed `CompleteAliasPackage` still had to be handled,
but `as_complete()` resolves through `is_complete()`, which already covers
`CompleteAliasPackage` and `RootAliasPackage` — the same set PHP's
`instanceof CompletePackageInterface` matches. The preceding alias guard
rules those out anyway, so both markers went away; the first condition is
now a single chain, matching the shape of the PHP original.
`Application::find()` skips commands it cannot borrow, where Symfony
registers the aliases of every command unconditionally. That is a real
divergence, so it gets `TODO(port)`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Composer requires Windows 7 or later with proc_open available before it
will use junctions, because a PHP bug (bugs.php.net #77552) makes
junction detection fragile and can lose the target content when a
package is removed. Junctions are created and removed here without
going through PHP, so that bug does not apply and the check always
passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Auditor.php calls ConsoleIO::sanitize() with a single argument, so
$allowNewlines is the default true. The port passed false, stripping
newlines out of advisory titles, links and ignore reasons before they
reach the table renderer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
VersionGuesser probes git, hg, fossil and svn in turn on every version
guess, and a temp directory that is no working copy fails each probe
only after paying that binary's startup cost. Dropping the three from
the dev shell takes the `command` test binary from ~140s to ~46s, and
no test that was passing starts failing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
date() renders in the system default timezone, so the mismatch the ignore
described is gone; the test passes with the local date ahead of or behind
the UTC date.
test_show_outdated_deps_sorting_by_age still fails that way, because
ShowCommand::get_relative_time formats the release date in UTC while taking
"today" from date(). Its ignore now says so on its own.
|
|
PHP resolves a link kind through `$package->{'get'.ucfirst($linkType)}()`,
so the argument is a `Link::$TYPES` value ("requires", "devRequires", ...).
The helper matched the composer.json key names ("require", "require-dev",
...) instead, so every caller passing a `Link::TYPE_*` constant got an empty
map back: `show <package>` printed none of its requires/provides/conflicts/
replaces sections, and `--format=json` carried none of those keys.
The two callers that were passing composer.json keys now pass the matching
`method`, as PHP does.
|
|
The R-table dispatchers hand-rolled argument decoding and return encoding per
method, spread over 24 helpers. `FromPluginArg` and `ToPluginValue` replace
them: blanket impls for `Option<T>`, `Vec<T>` and `IndexMap<String, T>` compose
over a handful of leaf types, so a composite like `Vec<IndexMap<String, String>>`
needs no helper of its own, and the lossy UTF-8 conversion of a PHP string lives
in one impl instead of a dozen call sites.
Argument errors now read `{method} expects {expected} at position {position}`
rather than a message written per call site. `arg_or` takes its default for an
explicit null too, not only for an omitted argument; every parameter it serves
is declared non-nullable in the proxy stubs, so PHP never passes one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`@composer <args>` and a bare `composer <args>` script both re-enter the
binary running the script, taken from COMPOSER_BINARY. That path used to
be prefixed with the PHP interpreter command, which produced
`php <shirabe path> install` and could not run: Shirabe ships as a native
executable, not a phar.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Messages that instruct the user to run a command named the Composer
binary. Shirabe ships as its own binary, so the hints now name it.
File names (composer.json, composer.lock), the "composer" repository
type and prose about Composer itself are untouched. The installer and
functional integration fixtures live in the Composer submodule and
cannot be edited, so their expected output is normalized on load.
|
|
|
|
A PHP script listener that walks the event it receives (Laravel's
Illuminate\Foundation\ComposerScripts::postAutoloadDump asks for
$event->getComposer()->getConfig()->get('vendor-dir')) aborted the run
with `unknown Rust handle 2`.
The event's getComposer/getIO answers register an entity in the R table
and hand back its rhandle, but ScriptRpcDispatcher resolved only rhandle
0 and the one event handle of the call in flight, so it could not serve a
method on a handle it had just minted itself. The R-table lookup
PluginRpcDispatcher already does is now dispatch_r_table_method, shared
by both.
The stubs that graph hands out extend and implement the real Composer
contracts (Composer\Package\PackageInterface and the rest), which live in
the Composer PHP runtime and not among the generated stubs or guards, so
execute_event_php_script loads that runtime the way the plugin and
command-class paths do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
CompilingMatcher::match formats `<operator><constraint>;<version>` into a
fresh String on every call, including the cache hits the key exists to serve.
Profiling put the formatting at 2.9 % of self time, more than the match it
guards. Build the key into a reused thread-local buffer and copy it only when
there is a miss to record.
Taking the version as `&str` rather than `String` removes the copy the callers
made for the same reason: `optimize_by_identical_dependencies` allocated one
per candidate package in its innermost loop.
laravel/framework require --no-install (warm cache, network disabled):
instructions:u 9142799608 -> 8895247586 (-2.7 %)
cycles:u 4626440169 -> 4496152041 (-2.8 %)
wall (hyperfine, 25 runs)
1.239 s +- 0.014 s -> 1.227 s +- 0.016 s (-1.0 %)
monolog/monolog is unchanged (102.1 ms -> 101.6 ms, within noise).
composer.lock is byte-identical for both packages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
isSubsetOf builds a throwaway `MultiConstraint([candidate, constraint])` and
hands it to `Intervals::get`, whose cache key is the constraint's string form.
That MultiConstraint is fresh on every call, so its memoized string form is
always cold and the whole intersection has to be stringified recursively --
profiling put `AnyConstraint: Display::fmt` at 3.6 % of self time, more than
the interval computation the cache exists to skip.
Cache the answer on the pair of operand strings instead. Both operands are
long-lived, so each one's own string memo stays warm and the throwaway
intersection is never built on a hit.
laravel/framework require --no-install (warm cache, network disabled):
instructions:u 9885142257 -> 9142874710 (-7.5 %)
cycles:u 4858265879 -> 4444135669 (-8.5 %)
wall (hyperfine, 20 runs)
1.322 s +- 0.018 s -> 1.224 s +- 0.011 s (-7.4 %)
monolog/monolog is unchanged (101.6 ms -> 100.3 ms, within noise).
composer.lock is byte-identical for both packages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
startCachedAsyncDownload's cached branch decoded the provider JSON into an
owned value and then rebuilt it entry by entry, recursively cloning every
package definition only to drop the original. Match the decoded value by
value instead. PHP hands the decoded array to the closure by COW, so no copy
happens there either.
laravel/framework require --no-install (warm cache, network disabled):
instructions:u 10276126917 -> 9884102191 (-3.8 %)
cycles:u 5048262624 -> 4777881212 (-5.4 %)
wall (hyperfine, 20 runs)
1.345 s +- 0.015 s -> 1.316 s +- 0.018 s (-2.2 %)
monolog/monolog is unchanged (100.7 ms -> 101.7 ms, within noise).
composer.lock is byte-identical for both packages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PackageInterface::getRequires() and friends return the array of Link objects;
in PHP that is a copy-on-write array of object references, so a caller pays
nothing to look at it. The port returned IndexMap<String, Link> by value, so
every call deep-cloned the whole map, keys and constraints included. Pool
building calls these accessors once per package per candidate, which put
IndexMap::clone at 13.5% of `require laravel/laravel`.
Store the maps as Rc<IndexMap<String, Link>> and return a handle. Callers that
mutate the map clone it explicitly at the point of mutation, matching where PHP
would separate the array.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Intervals::get() memoizes generateIntervals() results, but the port deep-cloned
the IntervalCollection out of the cache on every hit, where PHP hands the array
back by copy-on-write. On `require laravel/laravel` that is 282k calls, 99% of
them hits.
Store and return Arc<IntervalCollection> so a hit costs a refcount bump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
composer/semver memoizes MultiConstraint::__toString() into $this->string, but
the port recomputed it on every call. On `require laravel/laravel` that meant
837k stringifications visiting 1.7M child constraints, all of them feeding the
CompilingMatcher and Intervals cache keys.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
php_version_compare canonicalized both operands into a Vec<&str> and copied
the canonicalized bytes a second time through from_utf8_lossy().into_owned().
Walk the split iterators directly and move the Vec<u8> into the String, which
takes the per-call allocation count from six down to two. Splits are only ever
inserted at ASCII digit boundaries, so the buffer is always valid UTF-8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Options and arguments were stored and passed as PhpMixed even though
Symfony only ever puts a string, a bool, a list of strings or null in
one. get_option already narrowed to InputOptionValue at the boundary;
this widens that enum into InputValue and pushes it through
InputInterface, InputOption/InputArgument defaults, the Input storage,
ArgvInput/ArrayInput/StringInput/CompletionInput, Command::add_option
and add_argument, and the Composer-side wrappers.
Two neighbouring string|int unions get types of their own:
InputDefinition::{get_argument,has_argument} take an ArgumentName, and
ArrayInput keys its parameters by ParameterName. has_parameter_option
and get_parameter_option take the values they look for as &[&str],
which is what PHP's `(array) $values` cast produced anyway.
Two behaviours change along the way. Input::set_option on a negated
option now negates with PHP's loose bool cast rather than treating a
non-bool as false, matching `!$value`. ArrayInput::parse now resolves
an integer key to an argument position instead of looking up an
argument literally named "0".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
create_packages handed ArrayLoader::load_packages a clone of the package
array it had just built and then dropped its own copy, so every package
definition in the response was deep-copied once for nothing.
Measured on `require laravel/laravel` (77 packages) against a warm cache
with the network disabled: the create_packages span drops from 420 ms to
275 ms, and the run from 2.333 s to 2.192 s. Peak RSS is unchanged -- the
copy was transient. `composer.lock` is byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
CurlDownloader built a fresh reqwest Client, and with it a fresh
connection pool, on every construction. A `require` run constructs two:
RequireCommand::doUpdate discards the Composer instance that
BaseCommand::initialize built and rebuilds it against the rewritten
composer.json, so the second one opened a second TCP+TLS connection to
the same repository and paid another CA bundle parse.
Nothing in the constructor varies the Client -- `options` and
`disable_tls` are not applied to it -- so it can be shared. The cache is
keyed by tokio runtime rather than by process: a pooled connection is
driven by a task on the runtime that opened it and hangs if it is later
handed to another one, and `sync_executor::block_on` builds a disposable
runtime per call outside `main`.
Measured on `require monolog/monolog` in a git-managed project against a
warm cache: TLS connections to the repository drop from 2 to 1, taking
the run from 646 ms to 529 ms with the network and from 104 ms to 101 ms
offline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Profiling `create-project` showed glibc malloc/free accounting for about
41% of samples: the work is dominated by a very large number of small,
short-lived allocations, mostly from decoding repository JSON into
PhpMixed.
`create-project laravel/laravel`, offline against a warmed cache, all
three variants measured in one hyperfine run (8 runs each, identical
composer.lock):
glibc 3.633 s +- 0.023 (user 3.580 s, sys 2.255 s)
mimalloc 3.096 s +- 0.054 (user 3.004 s, sys 2.315 s) -14.8%
jemalloc 3.177 s +- 0.028 (user 3.136 s, sys 2.285 s) -12.5%
The gain is entirely in user time; system time is unchanged, so the
extraction and file-writing half of the run is unaffected. Peak RSS drops
slightly, from 267 MB to 257 MB. mimalloc wins over jemalloc and, unlike
jemalloc, needs no C build of its own at every profile.
The allocator is declared in the library rather than in `main.rs` so that
the test and benchmark binaries, which link this crate but not `main.rs`,
run against the allocator the binary ships with.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
MetadataMinifier::expand now returns ExpandedVersions, which holds the
minified input plus, for each expanded version, a table of references to
where its fields live. A version is copied only when materialize() asks
for it. ComposerRepository::load_async_packages runs its constraint and
stability filters straight off that view through the new VersionFields
trait, so the versions it rejects are never copied at all.
Benchmarks are new under crates/shirabe/benches. load_packages against
real packagist p2 metadata, before -> after:
symfony/console (768 versions)
0 accepted 9.01 ms -> 4.40 ms -51%
50 accepted 10.70 ms -> 6.30 ms -41%
147 accepted 13.17 ms -> 9.62 ms -27%
329 accepted 18.58 ms -> 16.89 ms -9%
663 accepted 27.27 ms -> 27.89 ms +2%
laravel/framework (1277 versions)
0 accepted 39.44 ms -> 11.22 ms -72%
81 accepted 44.76 ms -> 18.25 ms -59%
840 accepted 125.44 ms -> 120.12 ms -4%
1266 accepted 163.18 ms -> 182.01 ms +12%
The crossover sits near 80% acceptance. Past it the view loses, because
materialize rebuilds a map where the old code cloned one, and the
minified input stays alive alongside the copies; the 1266-of-1277 case
measured between +5% and +12% across runs. Loads with a real constraint
sit far below the crossover.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
`InputInterface::get_option` returned `PhpMixed`, so the negation branch
of `Input::get_option` reproduced PHP's `return !$value;` as
`!value.as_bool().unwrap_or(false)`, which inverts the result for a
string value instead of leaving it `false`. It now returns
`InputOptionValue`, whose `to_bool` is PHP's truthiness cast.
The narrowing also removes the `Vec<PhpMixed>` element handling the
commands carried for array options: `as_array` hands back `&[String]`,
so the `filter_map(|v| v.as_string())` chains at eight call sites
collapse. Its other accessors keep `PhpMixed`'s names and meanings
(`is_null`, `as_bool`, `as_string`, `to_bool`), and
`From<InputOptionValue> for PhpMixed` covers the callers that feed the
value back into an `IndexMap<String, PhpMixed>` or a `PhpMixed`
parameter.
`Input` keeps its parsed options and `InputOption` its defaults as
`PhpMixed`, so `Input::get_option` is where the narrowing happens and
where a value outside the domain panics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Replace the hand-built `IndexMap<String, PhpMixed>` rows behind
`check-platform-reqs --format=json` with structs deriving
`serde::Serialize`, so key order and the `null` values PHP keeps for a
missing failed requirement or provider live in the type rather than being
rebuilt at every insertion site.
Test the provider against its raw value the way PHP's `$provider === ''`
does, instead of against the `strip_tags` result.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The `check_*` methods returned `PhpMixed`, and `output_result` reproduced
PHP's truthiness over it incorrectly: only `PhpMixed::Bool(false)` took the
falsey path, so an empty string or an empty list printed a blank line
instead of reporting `FAIL` and raising the exit code. `CheckResult` and
`GithubRateLimit` make the shapes PHP returns explicit, and `check_platform`
stores the detail of an error or warning as `Option<String>` rather than
`true`-or-string.
`check_http` and `check_composer_repo` also built the exception label with
`std::any::type_name_of_val`, printing the Rust type path where PHP prints
`get_class($e)`; they now go through `AnyThrowable::php_class_name` like the
rest of the file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Replace the `IndexMap<String, PhpMixed>` maps behind `show`'s three output
shapes with structs deriving `serde::Serialize`, so field order, key omission
and PHP's rule that an empty array encodes as `[]` live in the type instead of
being rebuilt at every insertion site.
Typing the fields after PHP's own signatures corrects three divergences:
`description`, `source.url`/`source.reference` and `dist.url`/`dist.reference`
now encode as `null` rather than `""` where the getter returns null, and the
package list follows PHP's `??` chain for the homepage fallback instead of
also skipping empty strings.
Record with TODO(port) that `get_links_for_type` matches composer.json key
names while `show` passes `Link::TYPE_*`, so its link sections never print.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The stat array was modelled as IndexMap<String, PhpMixed>, forcing
callers through untyped lookups with an unwrap_or(0) fallback PHP has no
counterpart for. Only `size` and `comp_size` are kept: statIndex has a
single call site in Composer and it reads nothing else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`ProcessExecutor::execute_args` existed only to turn `execute`'s
`anyhow::Result` into an exit code of 1, so every one of its ~87 call
sites silently took the "command failed" branch on an error PHP would
have thrown. It is gone; callers use `execute` and propagate with `?`.
Where the enclosing function had no `Result` to propagate into, its
signature grew one, up to and including `Git::get_version`,
`Svn::binary_version`, `GitHub`/`GitLab`/`Bitbucket::authorize_oauth`,
`InitCommand::get_git_config` and `DiagnoseCommand::check_git`.
The VCS drivers had the same problem in the other direction: their
`get_contents` returned `Result<Response, Box<TransportException>>`, a
type too narrow for the PHP method, which lets any Throwable out of the
`catch (TransportException $e)` block. Every non-transport error was
therefore rewritten into a `TransportException` with code 0, which the
callers switch on. They now return
`anyhow::Result<Result<Response, Box<TransportException>>>`: the outer
`Result` carries what PHP does not catch, the inner one the exception
the drivers handle.
That signature also restores `GitLabDriver::getContents`: the 400/401
`TransportException`s it raises to force authentication are thrown
inside its own `try` block and handled by its own `catch`, but the port
returned them straight to the caller, so the authentication flow behind
them never ran.
`impl_php_exception!` gains `From<Box<$ty>> for anyhow::Error` so a
caught exception can be re-propagated with `?`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Commands were carried as `PhpMixed`, whose `String`/`List` variants do
not tell a shell command line apart from an argv list at the type level.
`Perforce::execute_command` and `Git::run_command` therefore funnelled
string commands through `execute_args`, spawning `p4 set` or
`git command` as a single argument instead of running it through a shell
as PHP does; their tests were written against that shape.
Introduce `CommandLine::{Shell, Args}` and use it for every
`ProcessExecutor` entry point, the mock expectation queue and the
`Git::run_command` callables. The unreachable "Invalid command type"
branches disappear with it, and the affected tests go back to the string
expectations the PHP suite uses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
function_exists() returns false in PHP when a function is blocked by
disable_functions, when its extension is not compiled in, or when the
PHP version predates it. None of those apply to a native binary.
|
|
|
|
The `single` group compares the argv form against the string form, which
goes through an extra `/bin/sh -c`; the `concurrent` group varies the job
count around `max_jobs` so the semaphore throttle is visible as a
throughput knee.
`execute_async`'s futures are `!Send`, so they run on a current-thread
runtime and overlap only through `join_all` within a single task.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The Cargo.toml section-header pattern only matched single-bracket
headers, so a `[[bench]]` / `[[bin]]` / `[[test]]` table left the
previous section active and its keys were attributed to whatever
`[dependencies]` block preceded it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Cache::read()/write() rewrite every character outside the allowlist to
"-", so a package name containing "_" was stored and looked up under a
file name Composer never writes: the repository metadata cache was never
hit for those packages, and "vendor/foo_bar" collided with
"vendor/foo-bar". With no cached copy there is no last-modified date to
send either, so a degraded repository resolved such packages as not
found instead of serving the cached metadata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PHP's PREG_PATTERN_ORDER is column-oriented, but 7 of the 10 call sites
read it row-wise, rebuilding each occurrence by indexing every column at
the same offset. Return an iterator of PregMatches instead, which is also
what the set-order and offset-capture variants were carrying, so the
three functions collapse into one and PregMatchesAll,
PregMatchesAllWithOffsets, CaptureKey and preg_match_map! all go away.
The offset-capture call sites are served by the new PregMatches
get_offset/name_offset accessors.
The search stays eager: regex::Captures borrows only the subject, so the
matches outlive the pattern resolved for the call, and PHP's
preg_match_all is eager too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The capture groups were discarded at 162 of the preg_match call sites,
which only tested the Option. They now call preg_is_match, which lets the
regex engine skip capture tracking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Every call site but one passed offset 0. The remaining one, the UTF-8
chunking loop in Application, slices the subject instead: its pattern has
no anchor or lookaround, so matching a suffix is equivalent to starting
the search at that offset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PhpFileCleaner::clean scans for the single class/interface/trait keyword
of a one-declaration file with a pattern PHP runs anchored (`A`) one char
before the keyword. The port dropped the anchor and relied on the
leftmost match of an offset search instead, on the grounds that the
keyword sits exactly one char past the offset.
That holds for the keyword but not for the guards around it: where the
preceding char makes PCRE fail (`Foo::class`, `$class`, `->class`), the
unanchored search does not fail, it matches the next declaration further
down the file and returns the cleaned prefix cut short there. Given
<?php
namespace A {
$x = Foo::class;
}
namespace B {
class Bar {}
}
PhpFileParser::findClasses reported A\Bar where PHP reports B\Bar,
because the `namespace B {` the cleaner skipped past never reached the
parser.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The shim carried the PCRE A (anchored) modifier alongside every compiled
pattern so preg_match2 could honour it by searching the sub-slice at the
offset. Only two call sites ever passed such a pattern, and each can cut
that slice itself, so the flag is gone from the cache, ResolvedPattern,
PregPattern and the php_regex! macro, and a pattern still carrying A is
now rejected rather than silently searched unanchored.
StringInput::tokenize and PhpFileCleaner::match search from their cursor
with a `^`-prefixed pattern instead. The rule is written down in
docs/dev/regex-porting.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The two preg_match_all variants took the same arguments and differed
only in what they returned: a Vec of columns, or the named-and-numbered
PregMatchesAll. The latter is the one all but two call sites already
used, so preg_match_all2 takes over the plain PHP name and the Vec
variant goes away.
Its remaining readers only ever wanted group 0's column, which they now
take through CaptureKey::ByIndex(0); in the formatter this replaces the
array_shift that popped that column off the PREG_PATTERN_ORDER array.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|