Skip to content

Binding diagnostics as a core artifact#336

Open
koriym wants to merge 4 commits into
ray-di:2.xfrom
koriym:extract-bindings-package
Open

Binding diagnostics as a core artifact#336
koriym wants to merge 4 commits into
ray-di:2.xfrom
koriym:extract-bindings-package

Conversation

@koriym

@koriym koriym commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

Make composed bindings a first-class, readable artifact of Ray.Di, and move rich
visualization out of core.

  • Move binding diagnostics out of core — the Ray\Bindings namespace (HTML
    viewer, bindings-html CLI, viewer assets) and the deprecated
    Ray\Di\BindingsMarkdown shim are removed. Rich visualization moves to the
    ray/object-visual-grapher package. Ray\Di\ModuleVisitorInterface becomes
    the stable route for tools to visit a module's composed container via
    AbstractModule::accept().
  • Write bindings.md at composition time — an Injector created with an
    explicit tmpDir writes {tmpDir}/bindings.md: a deterministic, human- and
    agent-readable snapshot of every resolved binding, the composing modules, and
    the binding provenance. The writer is best-effort and atomic (tmp file +
    rename), the file carries its own signature trailer so an unchanged binding
    surface skips rewriting, and getInstance() performs no diagnostics I/O. The
    shared sys_get_temp_dir() fallback is not application-owned, so nothing is
    written there.

Not in this PR

Runtime just-in-time (JIT) binding is intentionally left untouched here. Its
future is tracked separately as a soft-deprecation (keep resolving, emit
E_USER_DEPRECATED, no scheduled removal) in #337 (EN) / #338 (JA). Keeping it
out of this PR lets binding diagnostics land independently of that discussion —
bindings.md is a composition-time snapshot and does not depend on the JIT
outcome.

Commits

  1. Move binding diagnostics out of core
  2. Write bindings.md at composition time

Test plan

  • 264 tests / 435 assertions green
  • Coverage 100% (classes, methods, lines)
  • PHPStan / Psalm / phpcs / phpmd pass
  • PHP 8.2–8.5

Notes

Intended as a Ray.Di minor (2.23.0); the public API is unchanged. Removing the
Ray\Bindings\* classes shipped in 2.22.2 is a small BC break covered by the
migration to ray/object-visual-grapher.

Summary by CodeRabbit

  • New Features
    • Enhanced “binding diagnostics” workflow: at composition time, an atomic bindings.md snapshot is written only when an explicit temp directory is provided, and it refreshes only when composed bindings change.
    • Snapshot updates use a deterministic signature to skip unnecessary rewrites.
  • Bug Fixes
    • Improved diagnostics output for interceptors, including support for interceptors provided as instances.
  • Documentation
    • Updated README to describe the new workflow and added a pointer to Ray.ObjectGrapher.
  • Chores
    • Removed the legacy bindings HTML viewer, related assets, and the associated command tooling.
    • Updated dev scripts and narrowed static-analysis/test scopes to the DI bindings area.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Binding diagnostics are implemented directly in Ray\Di, using signature-based, atomic bindings.md snapshots generated during explicit-directory composition. Legacy bindings rendering, CLI, assets, autoload mappings, and related test scopes are removed.

Changes

Binding diagnostics

Layer / File(s) Summary
Deterministic snapshot writer and lifecycle tests
src/di/BindingsMarkdown.php, tests/di/BindingsMarkdownTest.php
BindingsMarkdown renders provenance diagnostics, embeds a signature, skips unchanged files, rewrites stale snapshots, and writes atomically. Tests cover composition-time output, modification-time stability, signature invalidation, and interceptor instances.
Composition-time integration and contracts
src/di/Injector.php, src/di/ModuleVisitorInterface.php, src/di/AbstractModule.php, src/di/SpyCompiler.php, tests/di/AbstractModuleTest.php
Injector writes diagnostics only for explicit existing directories; ModuleVisitorInterface moves to Ray\Di; module visitor handling and interceptor-name formatting support the updated namespace and object values.
Package, tooling, and documentation scope
composer.json, phpstan.neon, phpunit.xml.dist, psalm.xml, README.md
Legacy bindings autoloading, executable, renderer assets, source files, and tests are removed. Analysis targets are narrowed, ObjectGrapher is suggested, and README documents explicit-directory diagnostics.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant Injector
  participant Container
  participant BindingsMarkdown
  participant bindingsmd as bindings.md
  Application->>Injector: construct with explicit tmpDir
  Injector->>Container: compose modules
  Injector->>BindingsMarkdown: generate diagnostics
  BindingsMarkdown->>Container: read bindings and provenance
  BindingsMarkdown->>bindingsmd: atomically write changed snapshot
  Application->>Injector: call getInstance()
  Injector-->>Application: return instance without diagnostics I/O
Loading

Possibly related PRs

  • ray-di/Ray.Di#323: Adds the binding provenance and collision data consumed by the new markdown diagnostics.
  • ray-di/Ray.Di#327: Also modifies signature-based bindings.md caching and rewrite behavior.
  • ray-di/Ray.Di#331: Relates to the explicit visitor workflow and composition-time snapshot generation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title captures the main change: binding diagnostics were elevated into a core Ray.Di artifact.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.90%. Comparing base (236ac26) to head (ce8fca3).

Files with missing lines Patch % Lines
src/di/BindingsMarkdown.php 98.59% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##                 2.x     #336      +/-   ##
=============================================
- Coverage     100.00%   99.90%   -0.10%     
+ Complexity       460      415      -45     
=============================================
  Files             53       50       -3     
  Lines           1184     1074     -110     
=============================================
- Hits            1184     1073     -111     
- Misses             0        1       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@koriym
koriym force-pushed the extract-bindings-package branch 2 times, most recently from 1daea41 to ccefb7b Compare July 22, 2026 09:14
@koriym koriym changed the title Binding diagnostics as a core artifact, and remove runtime JIT binding Binding diagnostics as a core artifact Jul 22, 2026
@koriym
koriym marked this pull request as ready for review July 23, 2026 18:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plan.md`:
- Around line 94-108: Update the caching contract around BindingsMarkdown to
state that reuse is signature-based, using only resolved bindings and pointcuts,
rather than content-based. Explicitly note that provenance is intentionally
excluded from the signature even though it appears in rendered Markdown, so
matching signatures may reuse existing content; preserve the existing
deterministic-output and best-effort runtime-path requirements.
- Around line 162-164: Update the namespace count in the sentence beginning “1
package 2 namespace” to reflect the three configured prefixes:
Ray\ObjectGrapher, Ray\Bindings, and Ray\Di. Preserve the existing explanation
about the three output formats.

In `@README.md`:
- Around line 17-23: Update the README Injector example and surrounding
description to state that the explicit $tmpDir must already exist and be
writable; clarify that nonexistent paths fall back to sys_get_temp_dir() and do
not receive bindings.md, or create the directory before constructing Injector.

In `@tests/di/BindingsMarkdownTest.php`:
- Around line 146-149: Update the temporary-file setup in the bindings markdown
test to stop unlinking the shared /tmp/bindings.md path. Assert its expected
absent or unchanged state without deleting an existing file, preserving the
test’s cleanup and validation behavior otherwise.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6338e97-efa1-41ee-8799-ee93dbdf23bb

📥 Commits

Reviewing files that changed from the base of the PR and between 236ac26 and ccefb7b.

📒 Files selected for processing (22)
  • README.md
  • bin/bindings-html
  • composer.json
  • docs/bindings/bindings.css
  • docs/bindings/bindings.js
  • phpstan.neon
  • phpunit.xml.dist
  • plan.md
  • psalm.xml
  • src-bindings/Bindings.php
  • src-bindings/BindingsHtml.php
  • src-bindings/BindingsMarkdown.php
  • src-bindings/Exception/BindingsNotCollected.php
  • src/di/AbstractModule.php
  • src/di/BindingsMarkdown.php
  • src/di/Injector.php
  • src/di/ModuleVisitorInterface.php
  • src/di/SpyCompiler.php
  • tests-bindings/BindingsHtmlTest.php
  • tests-bindings/BindingsTest.php
  • tests/di/AbstractModuleTest.php
  • tests/di/BindingsMarkdownTest.php
💤 Files with no reviewable changes (13)
  • docs/bindings/bindings.css
  • psalm.xml
  • src-bindings/Bindings.php
  • src-bindings/Exception/BindingsNotCollected.php
  • bin/bindings-html
  • phpstan.neon
  • docs/bindings/bindings.js
  • src-bindings/BindingsMarkdown.php
  • src-bindings/BindingsHtml.php
  • tests-bindings/BindingsTest.php
  • src/di/AbstractModule.php
  • phpunit.xml.dist
  • tests-bindings/BindingsHtmlTest.php

Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread README.md Outdated
Comment thread tests/di/BindingsMarkdownTest.php Outdated
Drop the Ray\Bindings namespace (Bindings, BindingsMarkdown, BindingsHtml,
the bindings-html CLI and its viewer assets) and the deprecated
Ray\Di\BindingsMarkdown shim. Rich visualization moves to the
ray/object-visual-grapher package; composition provenance (BindingLog)
stays in core.

Ray\Di\ModuleVisitorInterface replaces Ray\Bindings\ModuleVisitorInterface
as the stable route for tools to visit a module's composed container via
AbstractModule::accept().
@koriym
koriym force-pushed the extract-bindings-package branch from ccefb7b to 13d4ccb Compare July 24, 2026 07:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/di/BindingsMarkdown.php`:
- Around line 72-73: Update the cache signature used by
BindingsMarkdown::render() in src/di/BindingsMarkdown.php lines 72-73 to include
all module and provenance inputs that affect the rendered markdown, so
provenance-only composition changes invalidate the existing diagnostics file. In
tests/di/BindingsMarkdownTest.php lines 215-231, update the second-composition
expectation to assert that the snapshot reflects SecondModule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 37574515-81f5-4416-a4ab-3e2da13db5dc

📥 Commits

Reviewing files that changed from the base of the PR and between ccefb7b and 13d4ccb.

📒 Files selected for processing (21)
  • README.md
  • bin/bindings-html
  • composer.json
  • docs/bindings/bindings.css
  • docs/bindings/bindings.js
  • phpstan.neon
  • phpunit.xml.dist
  • psalm.xml
  • src-bindings/Bindings.php
  • src-bindings/BindingsHtml.php
  • src-bindings/BindingsMarkdown.php
  • src-bindings/Exception/BindingsNotCollected.php
  • src/di/AbstractModule.php
  • src/di/BindingsMarkdown.php
  • src/di/Injector.php
  • src/di/ModuleVisitorInterface.php
  • src/di/SpyCompiler.php
  • tests-bindings/BindingsHtmlTest.php
  • tests-bindings/BindingsTest.php
  • tests/di/AbstractModuleTest.php
  • tests/di/BindingsMarkdownTest.php
💤 Files with no reviewable changes (14)
  • docs/bindings/bindings.js
  • psalm.xml
  • phpunit.xml.dist
  • bin/bindings-html
  • src-bindings/BindingsHtml.php
  • docs/bindings/bindings.css
  • src-bindings/Bindings.php
  • README.md
  • src-bindings/Exception/BindingsNotCollected.php
  • tests-bindings/BindingsTest.php
  • phpstan.neon
  • src/di/AbstractModule.php
  • tests-bindings/BindingsHtmlTest.php
  • src-bindings/BindingsMarkdown.php
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/di/SpyCompiler.php
  • composer.json
  • src/di/Injector.php

Comment thread src/di/BindingsMarkdown.php
An Injector created with an explicit tmpDir writes the composed bindings
to {tmpDir}/bindings.md: a deterministic, human- and agent-readable
snapshot of every resolved binding, the modules that composed them, and
the binding provenance. The shared sys_get_temp_dir() fallback is not
application-owned, so no file is written there.

Ray\Di\BindingsMarkdown keeps the 2.22 API (__invoke(Container, string)
and render(Container)) as the real implementation instead of a shim.
The writer is best-effort and atomic (tmp file + rename), and the file
carries its own signature trailer: an unchanged binding surface skips
rendering and writing, resolution performs no diagnostics I/O, and a
container revived with an empty provenance log keeps the richer
snapshot written at composition time.

SpyCompiler now names interceptor instances by their class so a
pointcut bound to an interceptor object renders instead of silently
skipping the write.
@koriym
koriym force-pushed the extract-bindings-package branch from 13d4ccb to ab660d7 Compare July 24, 2026 08:20
@koriym

koriym commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai No outstanding blockers. Please resolve all open conversations and approve this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/di/BindingsMarkdownTest.php (1)

60-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a helper to remove the repeated markdown-read boilerplate.

The $markdown = file_get_contents(...); assert(is_string($markdown)); pair is duplicated across essentially every test method. A small private helper would cut the repetition and keep assertions focused on the interesting part.

♻️ Proposed helper
 class BindingsMarkdownTest extends TestCase
 {
     private string $classDir;
+
+    private function readMarkdown(): string
+    {
+        $markdown = file_get_contents($this->classDir . '/bindings.md');
+        assert(is_string($markdown));
+
+        return $markdown;
+    }

Then, e.g.:

-        $markdown = file_get_contents($this->classDir . '/bindings.md');
-        assert(is_string($markdown));
+        $markdown = $this->readMarkdown();

applied at each of the flagged call sites.

Also applies to: 91-92, 105-106, 125-126, 136-137, 179-182, 211-213, 228-231, 262-264, 276-278

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/di/BindingsMarkdownTest.php` around lines 60 - 61, Extract a private
helper in the test class that reads bindings.md from classDir and asserts the
result is a string, then replace the duplicated file_get_contents/assert pairs
in the flagged test methods with calls to that helper. Keep each test’s existing
assertions and behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/di/BindingsMarkdownTest.php`:
- Around line 60-61: Extract a private helper in the test class that reads
bindings.md from classDir and asserts the result is a string, then replace the
duplicated file_get_contents/assert pairs in the flagged test methods with calls
to that helper. Keep each test’s existing assertions and behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c72134d-846c-48f1-a04f-3e5ea6a3aafd

📥 Commits

Reviewing files that changed from the base of the PR and between 13d4ccb and ab660d7.

📒 Files selected for processing (4)
  • src/di/BindingsMarkdown.php
  • src/di/Injector.php
  • src/di/SpyCompiler.php
  • tests/di/BindingsMarkdownTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/di/Injector.php
  • src/di/SpyCompiler.php

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

@koriym Resolved the open conversations and approved PR #336. 🐇

✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

koriym added 2 commits July 25, 2026 22:44
Refactor the bindings.md writer's temp file to tempnam() (eliminates the
concat mutators and cleans up on write failure) and add focused tests for
the signature contract: binding-order independence, alphabetical module
sort, end-of-file signature anchor, pointcut interceptor inclusion, and
binding-index inclusion. Restores Covered Code MSI to 91% (>= 90%).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/di/BindingsMarkdownTest.php (1)

148-154: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Assert that an existing shared file is unchanged, not just present.

If /tmp/bindings.md already exists, a regression that overwrites it via rename() still leaves file_exists($file) === true, so this test passes despite violating the no-modification contract. Capture and compare its contents and mtime when it exists.

Proposed test fix
 $existedBefore = file_exists($file);
+$contentsBefore = $existedBefore ? file_get_contents($file) : null;
+$mtimeBefore = $existedBefore ? filemtime($file) : null;

 new Injector(new FakeToBindModule());

 clearstatcache();
 $this->assertSame($existedBefore, file_exists($file));
+if ($existedBefore) {
+    $this->assertSame($contentsBefore, file_get_contents($file));
+    $this->assertSame($mtimeBefore, filemtime($file));
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/di/BindingsMarkdownTest.php` around lines 148 - 154, Update the test
around FakeToBindModule to capture the existing bindings.md contents and
modification time when the file already exists, then after Injector construction
assert both remain unchanged; preserve the current existence assertion for the
case where the file did not exist beforehand.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/di/BindingsMarkdown.php`:
- Around line 61-66: Update the temporary-file handling in the bindings
generation flow to check the result of rename() after a successful
file_put_contents(). If the rename fails, unlink the temporary path just as in
the write-failure branch, while preserving the existing atomic move behavior;
add coverage for the failed-rename cleanup path.

---

Outside diff comments:
In `@tests/di/BindingsMarkdownTest.php`:
- Around line 148-154: Update the test around FakeToBindModule to capture the
existing bindings.md contents and modification time when the file already
exists, then after Injector construction assert both remain unchanged; preserve
the current existence assertion for the case where the file did not exist
beforehand.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e3d22a21-1cb5-4f8e-87d5-b8a866c8468c

📥 Commits

Reviewing files that changed from the base of the PR and between ec52487 and ce8fca3.

📒 Files selected for processing (2)
  • src/di/BindingsMarkdown.php
  • tests/di/BindingsMarkdownTest.php

Comment on lines +61 to +66
$tmpFile = @tempnam($classDir, 'bindings');
if ($tmpFile !== false && file_put_contents($tmpFile, $markdown) !== false) {
@rename($tmpFile, $file);
} elseif ($tmpFile !== false) {
@unlink($tmpFile);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 6 'tempnam|rename|unlink|bindings\.md' tests/di/BindingsMarkdownTest.php

Repository: ray-di/Ray.Di

Length of output: 12208


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== BindingsMarkdown writer section =="
sed -n '1,110p' src/di/BindingsMarkdown.php | cat -n

echo
echo "== References to BindingsMarkdown/write/cache behavior =="
rg -n "class BindingsMarkdown|function \\(|tempnam|rename\\(|unlink\\(|function \"__invoke\"|function __invoke" src/di/BindingsMarkdown.php src tests

echo
echo "== PHP rename semantics probe =="
tmpdir="$(mktemp -d)"
file="$tmpdir/source.txt"
target="$tmpdir/moved.txt"
touch "$file"
php -r 'var_dump(rename("$0", "$1"), fclose(fopen("$0", "w")), file_exists("$0"), file_exists("$1"));', "$file" "$target" || true
rm -rf "$tmpdir"

Repository: ray-di/Ray.Di

Length of output: 10343


🌐 Web query:

PHP rename documentation returns FALSE error reason documentation

💡 Result:

The PHP rename function returns false when it fails to rename a file or directory [1][2]. Because this function interacts directly with the operating system's filesystem, the reasons for failure are typically external to PHP itself, often resulting from OS-level restrictions, permission issues, or filesystem limitations [1][3][4]. Common reasons for rename returning false include: 1. Permission Denied: The process running PHP lacks the necessary permissions to read the source file, write to the destination directory, or modify the filesystem at the specified paths [3][4]. 2. Destination Issues: On Windows, if the destination file already exists, it must be writable; otherwise, the operation will fail [5][6]. Additionally, files that are currently in use or locked by another process (or even by PHP itself in some cases) often cannot be renamed [4]. 3. Filesystem Constraints: Renaming across different filesystems or partitions can sometimes cause errors if the underlying system calls (such as chown or chmod) required to move the file properties are not supported by the destination filesystem [1][7]. For example, moving a file to a filesystem that does not support POSIX permissions can cause warnings or failures [1]. 4. Wrapper Mismatches: The stream wrapper used for the source path must match the wrapper used for the destination path [6][8]. To debug specific failures, check for E_WARNING messages emitted by PHP, which often provide additional context regarding the underlying OS error (e.g., "Permission denied" or "Invalid argument") [3][5][4]. In scenarios where rename is known to be unreliable—such as moving files across certain network mounts or dealing with specific Windows locking behaviors—a common workaround is to manually copy the file using copy and then remove the original with unlink [1][3][4].

Citations:


Clean up the temporary file when rename() fails.

The path file_put_contents($tmpFile, $markdown) !== false && rename($tmpFile, $file) ignores a failed atomic move, leaving the bindings* temp file in $classDir. Add cleanup on failed rename() and coverage for that failure path.

Proposed fix
             if ($tmpFile !== false && file_put_contents($tmpFile, $markdown) !== false) {
-                `@rename`($tmpFile, $file);
+                if (! `@rename`($tmpFile, $file)) {
+                    `@unlink`($tmpFile);
+                }
             } elseif ($tmpFile !== false) {
                 `@unlink`($tmpFile);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$tmpFile = @tempnam($classDir, 'bindings');
if ($tmpFile !== false && file_put_contents($tmpFile, $markdown) !== false) {
@rename($tmpFile, $file);
} elseif ($tmpFile !== false) {
@unlink($tmpFile);
}
$tmpFile = `@tempnam`($classDir, 'bindings');
if ($tmpFile !== false && file_put_contents($tmpFile, $markdown) !== false) {
if (! `@rename`($tmpFile, $file)) {
`@unlink`($tmpFile);
}
} elseif ($tmpFile !== false) {
`@unlink`($tmpFile);
}
🧰 Tools
🪛 ast-grep (0.44.1)

[info] 64-64: Avoid unsafe call to unlink
Context: unlink($tmpFile)
Note: [CWE-73] External Control of File Name or Path.

(avoid-unlink)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/di/BindingsMarkdown.php` around lines 61 - 66, Update the temporary-file
handling in the bindings generation flow to check the result of rename() after a
successful file_put_contents(). If the rename fails, unlink the temporary path
just as in the write-failure branch, while preserving the existing atomic move
behavior; add coverage for the failed-rename cleanup path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant