| Age | Commit message (Collapse) | Author |
|
|
|
Composer::getLoop() answers, so a plugin reaches the graph's own downloader
and executor through the route Composer's own docblocks point plugin authors
at, rather than through an instance of its own.
wait() drains the promises it is given and rethrows the first rejection once
the group is done, which is what React\Promise\all() hands PHP. abortJobs()
has nothing to cancel while every request settles before the call that
started it returns. A non-null $progress is an explicit error: a ProgressBar
is a symfony/console object each world runs its own implementation of, so
there is none to hand across.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
add() and addCopy() answer with a promise, and enableAsync(), wait() and
countActiveJobs() answer alongside them. The Rust future runs to completion
before the promise is handed over, so requests a plugin starts together run
one after another rather than overlapping; overlapping them needs a promise
representation that crosses the boundary unresolved.
Everything else the surface does is preserved. add() still refuses a
downloader outside a Loop, and it does so by throwing out of the call the way
PHP does, where a failed request instead arrives as a rejection the caller
handles — __shirabe_rejected_promise is the failure half of the resolved-
promise helper. wait() and countActiveJobs() answer for a downloader with no
outstanding job, which, once every request settles before its call returns,
it never has.
This is where HttpDownloader parts company with ProcessExecutor, whose async
surface stays an explicit error: executeAsync() resolves its promise with a
Symfony Process, whose state is the proc_open() resource of whichever process
called start(), where a request resolves its promise with a Response.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
A response is built for one request and the graph never retains it, so there
is no entity for a handle to point at. The child holds a real instance
instead, revived from the object record the wire carries, and collect() frees
the copy each world holds — which is what that method is for. The
value-object rule rejects the class only because collect() assigns to $this,
so the category comes from an overrides.list entry.
HttpDownloader::get() and copy() answer with one.
Two gaps stay: decodeJson() reaches Composer\Json\JsonFile, which a guard
shadows, and Composer answers a curl request with the CurlResponse subclass
where this port flattens the value into a Response.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
A downloader carries its own options, TLS defaults and request backends, and
what it shares with the graph is the IO it collects authentication into and
the config it reads. Plugin code writing `new HttpDownloader($io, $config)`
therefore allocates a Rust-side entity of its own rather than a second
downloader the graph knows nothing about, and the guard that shadowed the
class in the worker is gone.
The request surface is not served yet: get() and copy() have no wire
representation for the Response they return, and the async surface resolves
its promises with one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
A Rust-side failure reached plugin code as a RuntimeException whose message
carried the name of the call that failed, so `catch (TransportException $e)`
never matched and the status code the plugin branches on was gone.
The Throw frame now names the class the exception was thrown as and carries
the state that class declares beyond message and code.
\Shirabe\MaterializedThrowable rebuilds it in the child: `new $class($message,
$code)` for a class whose constructor has \Exception's shape, then the
properties by reflection. A class the child cannot build that way keeps the
RuntimeException shape.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Composer reaches this class two ways: the object graph hands one out through
Composer::getLoop()->getProcessExecutor(), and plugins write
`new ProcessExecutor($io)` freely. Both bind to a Rust-side entity, so the
timeout the run shares -- seeded from process-timeout and rewritten while the
run is in flight -- has one value instead of one per world, and the executor
can still be passed to the classes that take one (`new Filesystem($process)`).
Three things the stub generator was missing came with it:
- By-ref parameters. The call carries their positions and the answer carries
what each holds afterwards; a position the answer omits was never assigned
to, which is what PHP does with an untouched by-ref parameter.
ProcessExecutor::execute is the only one on a proxied class.
- Argument arity, reproduced where the real body reads func_num_args().
execute($cmd) forwards the child's output and execute($cmd, $out) captures
it, and nothing but the argument count separates the two.
- Static methods that cannot run in the worker. One that reads a static
property the Rust side owns, or that reaches a guarded class, forwards
through __shirabeCallStatic instead of being materialized. That also fixes
Filesystem::isLocalPath and getPlatformPath, whose materialized bodies
called the guarded Composer\Util\Platform.
The async surface stays an explicit error. executeAsync resolves its promise
with a Symfony Process, whose proc_open() resource and pipes belong to
whichever process called start(), so a Rust-side spawn has none to hand back;
running the real start() in the worker needs a promise representation that
crosses the boundary unresolved.
The fixture project drives the whole synchronous surface from plugin code and
compares the trace against upstream Composer byte for byte.
Co-Authored-By: Claude Opus 5 (1M context) <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>
|
|
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>
|
|
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>
|
|
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.
|
|
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>
|
|
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>
|
|
Preg had shed everything it owned: after the last few rounds its methods
were one-line forwards to the shim's preg_*(), differing only in a
default argument or a wrapper the caller unwrapped anyway. The 460 call
sites now name the shim function, and shirabe-pcre is gone from the
workspace along with its LICENSE entry.
The forwards expand as they read: isMatch becomes
preg_match2(.., 0).is_some() (is_none() where PHP negates it), isMatch3
and match3 drop the .is_some(), matchAll counts through
preg_match_all2(..).occurrence_count(), and replace4/replace5 spell out
the limit and count arguments preg_replace2 takes. Callbacks are the one
place the shapes differ: preg_replace_callback carries an error out of
the callback, so the fourteen infallible closures wrap their result in
Ok() and expect() it back.
Config::process() is the fifteenth, and it drops the `error` cell it
captured to smuggle a failure past a closure that could only return a
String. The `?` in the closure now carries it, which is what the PHP
does -- a throw from the callback leaves preg_replace_callback at the
failing match rather than running the remaining replacements and
reporting the last error.
The module doc that explained why composer/pcre's exceptions and
*StrictGroups() variants have no counterpart moves to the shim's preg
module, where the functions it describes live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PregMatches keyed both forms of a capture group through CaptureKey, so
every read built one: a usize wrapped in an enum, or worse, a String
allocated to name a group that regex::Captures can look up from a &str.
It now mirrors regex::Captures instead -- get() takes the group number,
name() the group name -- and the enum drops out of the type entirely.
That is 285 call sites across 59 files, and the named ones carry most of
the win: `matches.get(&CaptureKey::ByName("host".to_string()))` reads as
`matches.name("host")`. ProcessExecutor loses a `user_key` binding that
existed only to build the key once.
CaptureKey stays as the key type of PregMatchesAll and
PregMatchesAllWithOffsets, where numbered and named entries share one
IndexMap and a key type is the point. Five files still name it.
Also retargets the two preg_match_all comments that described the
occurrence count through `matches[&CaptureKey::ByIndex(0)].len()`, an
Index impl these types no longer carry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
is_match_named and is_match_with_indexed_captures reshaped a match into
a name-keyed map or a number-positioned vec, each allocating a String
per group up front for callers that then read one or two of them. Every
one of the eleven call sites ports a plain Preg::isMatch in PHP, so they
now call is_match3 and reach for the group they want through
get(&CaptureKey::ByIndex(N)) / get(&CaptureKey::ByName(..)), the same
way the rest of the tree already reads a match.
Falling out of that: PregNamedGroups existed only to type the first
variant; PregMatches::iter() only to build both; and PregMatches::pattern
only to give iter() the capture names. PregMatches is now a plain
wrapper over regex::Captures, so preg_replace_callback no longer clones
the resolved pattern for every match, and preg_match_map! is internal to
the shim again.
SvnDriver::get_file_content and get_change_date recover the flat
`isMatch(..) && $match[2] !== null` condition the PHP has, which the vec
shape had forced into a nested if.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Preg::match4 and Preg::replace_callback gave callers a
PregMatchedGroups: an IndexMap rebuilt from the match with an owned
String per group, plus a second String for a named group's name key.
That is the copy PregMatches shed when it started wrapping
regex::Captures, reinstated one layer up -- and nearly every regex call
in the tree goes through Preg rather than the shim's preg_* directly, so
almost nothing saw the borrow.
PregMatchedGroups existed only to drop the null (unmatched) groups the
old PregMatches held as Option<String> values. PregMatches::get reports
a non-participating group as None on its own, so the two read alike and
the type collapses into it. Call sites still reach groups through
get(&CaptureKey::ByIndex(N)); what changes is that the value arrives as
a &str borrowed from the subject, which the signatures now carry as a
lifetime.
Three places needed the borrow reckoned with rather than a mechanical
rewrite: PhpFileCleaner::clean and Problem::get_messages read their
groups out before mutating what the match borrows, and
Git::get_authentication_failure names the lifetime of its url argument,
which the result borrows instead of self.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PregMatches was an IndexMap of owned Strings copied out of the match, so
every preg_match2/preg_replace_callback call allocated a String per
capture group (twice over for a named group) whether or not the caller
read it. It now wraps the regex::Captures itself, held alongside the
pattern it came from so groups stay reachable by both their named and
their numbered form, and hands out &str borrowed from the subject. The
subject's lifetime becomes a parameter of the type.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`Composer\Pcre\Preg` fills `$matches` through a by-ref parameter, and the
port mirrored that with a `&mut` (or `Option<&mut>`) out-param plus a
bool or count return. Callers had to declare an empty map one line ahead
of the call, and the type never said the map is only meaningful when the
call matched. Return the matches instead:
- match3/match4/is_match3/is_match4 -> Option<PregMatchedGroups>
- is_match_named -> Option<PregNamedGroups>
- match_all2/is_match_all -> PregMatchesAll
- is_match_all_with_offsets3 -> PregMatchesAllWithOffsets
Nothing is lost: the bool is `Option::is_some()`, and the occurrence
count is the length of any one column of a PREG_PATTERN_ORDER map, now
spelled `PregMatchesAll::occurrence_count()`. is_match() still answers
the bool question directly for callers that want no groups.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PHP fills `$matches` through a by-ref parameter, which the port mirrored
with a `&mut` out-param plus a bool or count return. Every caller then
had to declare an empty binding one line ahead of the call, and nothing
in the type said the binding is only meaningful when the call succeeded.
Return the matches instead: preg_match() and preg_match2() hand back an
Option, and the three preg_match_all* functions hand back the collection
they used to fill.
The occurrence count the two map-shaped preg_match_all* functions used
to return is the length of any one of the map's columns, so it is not
lost -- Preg::match_all() and friends derive it via occurrence_count().
preg_replace2() keeps its `count: Option<&mut usize>`: that one is not
derivable from the replaced string, and callers that do not want it pay
nothing for passing None.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|