From 56a70876669b14e6a48e8a4f05bdb3e908662a7b Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 22 May 2026 23:59:02 +0200 Subject: [PATCH 1/2] Add CLAUDE.md with project conventions Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..5a4dd536162 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# CLAUDE.md + +Project-specific conventions for `rector/rector-src`. See `CONTRIBUTING.md` for the human-facing version. + +## Project + +- PHP `^8.3` required. Do not use syntax that breaks on 8.3. +- The package is `rector/rector-src`; it `replace`s `rector/rector`. +- Sibling extension packages (`rector-doctrine`, `rector-symfony`, `rector-phpunit`, `rector-downgrade-php`) are pulled in as `dev-main`. + +## Layout + +- `src/` — core engine (`Rector\` namespace). +- `rules/` — built-in Rector rules, also under `Rector\` namespace (PSR-4 maps both `src/` and `rules/` to `Rector\`). +- `rules-tests/` — tests for `rules/`, namespace `Rector\Tests\`. +- `tests/` — tests for `src/`, same `Rector\Tests\` namespace. +- `utils/` + `utils-tests/` — internal dev tooling (`Rector\Utils\`). +- `config/` — config sets/presets (kept as plain class-string literals; do not let Rector rewrite them). +- `build/target-repository/docs` — documentation lives here, not in repo root. + +## Coding style + +- `declare(strict_types=1);` at the top of every PHP file. +- Classes are `final` by default; `abstract` only when explicitly intended for extension. +- Constructor property promotion with `private readonly` for dependencies. +- Run `composer fix-cs` (ECS) before committing; the ruleset is symplify + common + psr12. +- Do not add `@author`, `@since`, or change/`@var` tags that ECS would strip. +- No emojis in source. + +## Quality gates + +Match what `composer complete-check` runs: + +```bash +composer check-cs # ECS, read-only +composer phpstan # PHPStan level 8, 512M +vendor/bin/phpunit +``` + +PHPStan extras enabled in `phpstan.neon`: +- `type-perfect`: `no_mixed`, `null_over_false`, `narrow_param`, `narrow_return` — return/param types must be narrow; prefer `null` over `false` for "no result". +- `unused-public`: public methods/properties/constants must be used somewhere. If you add a new public API, expect to use it or mark it accordingly. +- `symplify/phpstan-rules` + `rector-rules`: forbids `var_dump`, `dd`, `property_exists`, `class_exists`, `@` error suppression, dynamic names, etc., outside the narrowly listed exceptions in `phpstan.neon`. Do not add new exceptions casually — fix the code. + +Rector applies to its own source: `composer rector` runs the config in `rector.php`. + +## Writing a Rector rule + +Required shape (see `rules/Php85/Rector/FuncCall/OrdSingleByteRector.php` as a canonical example): + +1. Namespace mirrors the path: `Rector\\Rector\\`. +2. `final class` extends `Rector\Rector\AbstractRector`. +3. Implement `MinPhpVersionInterface` when the rule targets a specific PHP version; return a `PhpVersionFeature::*` constant from `provideMinPhpVersion()`. +4. Implement three methods: + - `getRuleDefinition(): RuleDefinition` — one-line description + at least one `CodeSample` (before/after). + - `getNodeTypes(): array` — list of `PhpParser\Node\...` classes to subscribe to. + - `refactor(Node $node): ?Node` — return the new node, `null` for no change, or `NodeVisitor::REMOVE_NODE` to delete. Do **not** return integer values except `REMOVE_NODE` (see `rector.noIntegerRefactorReturn`). +5. Add a `@see` PHPDoc pointing to the test class: `@see \Rector\Tests\<...>\Test`. +6. Inject services via constructor promotion (`ValueResolver`, etc.); reuse what `AbstractRector` already exposes (`$this->nodeNameResolver`, `$this->nodeTypeResolver`, `$this->nodeFactory`, `$this->nodeComparator`). +7. Bail out early: check `isFirstClassCallable()`, name match, arg presence, type, **then** transform. + +## Tests for a Rector rule + +Mirror the rule path under `rules-tests/`: + +``` +rules-tests//Rector/// +├── Test.php +├── Fixture/ +│ ├── some_case.php.inc +│ └── skip_some_case.php.inc +└── config/ + └── configured_rule.php +``` + +- Test class extends `Rector\Testing\PHPUnit\AbstractRectorTestCase`, uses `#[DataProvider('provideData')]`, and returns `self::yieldFilesFromDirectory(__DIR__ . '/Fixture')`. +- `provideConfigFilePath()` returns the config file that registers the rule and pins `phpVersion(PhpVersion::PHP_XX)` when version-bound. +- Fixtures use the `.php.inc` extension. Before/after are separated by a line containing exactly `-----`. A fixture with **no** `-----` separator asserts the file is unchanged; name those `skip_*.php.inc`. +- The fixture's `namespace` must match its directory. +- `Fixture/`, `Source/`, `Expected/` directories are auto-skipped by ECS, PHPStan, and Rector — don't try to make them conformant. + +## What not to do + +- Don't introduce new abstractions, traits, or helpers beyond what the task needs — the existing `AbstractRector` already exposes most node helpers. +- Don't modify `phpstan.neon` ignore lists or `ecs.php` skip lists to silence a new warning; fix the underlying code instead. +- Don't add `class_exists`/`property_exists`/`function_exists` runtime checks — use `ReflectionProvider` (errors from `symplify/phpstan-rules` will reject the PR). +- Don't bypass `instanceof` rules by adding a new ignore; only existing `Skipper`/internal paths are allowed. +- Don't touch files under `config/` with code-style rewrites — class strings there are intentional. +- Don't push docs into repo root — they belong under `build/target-repository/docs`. + +## CI parity + +`.github/workflows/code_analysis.yaml`, `tests.yaml`, `rector.yaml`, `e2e*.yaml`, and `phpstan_printer_test.yaml` mirror the local `composer complete-check`. If it passes locally with `composer complete-check && composer rector`, CI usually agrees. From 548beabc9b7ed64c92675668ca50d65347dce741 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 23 May 2026 00:07:57 +0200 Subject: [PATCH 2/2] [ci] kick of auto fix issuer --- .github/workflows/claude_issue_fixer.yaml | 196 ++++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 .github/workflows/claude_issue_fixer.yaml diff --git a/.github/workflows/claude_issue_fixer.yaml b/.github/workflows/claude_issue_fixer.yaml new file mode 100644 index 00000000000..6212c6e7e7b --- /dev/null +++ b/.github/workflows/claude_issue_fixer.yaml @@ -0,0 +1,196 @@ +name: Auto Fix Issue + +on: + schedule: + - cron: '0 */12 * * *' # every 12 hours + workflow_dispatch: # allow manual trigger + +permissions: + contents: write + pull-requests: write + issues: read + +jobs: + pick-and-fix: + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + # 1. Checkout + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # 2. Install Claude Code + - name: Install Claude Code + run: npm install -g @anthropic-ai/claude-code + + # 3. Install PHP & Composer dependencies + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: composer:v2 + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@v3 + + # 4. Pick a random open issue from rectorphp/rector (skip any already covered by a PR) + - name: Pick a fixable issue + id: pick + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + ISSUES_REPO="rectorphp/rector" + + # Fetch open issues (exclude PRs) + issues=$(gh api "repos/$ISSUES_REPO/issues?state=open&per_page=100" \ + --jq '[.[] | select(.pull_request == null) | {number, title, body}]') + + # Collect issue numbers already covered by an open PR in this repo + covered=$(gh api "repos/${{ github.repository }}/pulls?state=open&per_page=100" \ + --jq '[.[].head.ref]' | grep -oP '(?<=issue-)\d+' | tr '\n' ' ' || true) + + echo "Issues covered by open PRs: ${covered:-none}" + + # Exclude covered issues and pick one at random + candidates=$(echo "$issues" | jq -c \ + --argjson covered "$(echo "$covered" | jq -Rsc 'split(" ") | map(select(. != ""))')" \ + '[.[] | select((.number | tostring) as $n | $covered | index($n) == null)]') + + count=$(echo "$candidates" | jq 'length') + echo "Candidates: $count" + + if [ "$count" -eq 0 ]; then + echo "no_issue=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + issue=$(echo "$candidates" | jq -c ".[$(( RANDOM % count ))]") + issue_number=$(echo "$issue" | jq -r '.number') + issue_title=$(echo "$issue" | jq -r '.title') + issue_body=$(echo "$issue" | jq -r '.body // ""') + + echo "Picked issue #$issue_number — $issue_title" + + branch="fix/issue-${issue_number}-$(echo "$issue_title" | \ + tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | cut -c1-50 | sed 's/-$//')" + + echo "issue_number=$issue_number" >> "$GITHUB_OUTPUT" + echo "issue_title=$issue_title" >> "$GITHUB_OUTPUT" + echo "issue_body<> "$GITHUB_OUTPUT" + echo "$issue_body" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + echo "branch=$branch" >> "$GITHUB_OUTPUT" + echo "no_issue=false" >> "$GITHUB_OUTPUT" + + # 5. Early exit if no suitable issue was found + - name: No suitable issue found + if: steps.pick.outputs.no_issue == 'true' + run: echo "::notice::No fixable open issues found — skipping this run." + + # 6. Configure git + - name: Configure git identity + if: steps.pick.outputs.no_issue == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # 7. Create fix branch + - name: Create fix branch + if: steps.pick.outputs.no_issue == 'false' + run: git checkout -b "${{ steps.pick.outputs.branch }}" + + # 8. Ask Claude Code to fix the issue + - name: Run Claude Code to fix the issue + if: steps.pick.outputs.no_issue == 'false' + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + CLAUDE_ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }} + CLAUDE_ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }} + CLAUDE_ISSUE_BODY: ${{ steps.pick.outputs.issue_body }} + run: | + claude --print --dangerously-skip-permissions \ + "You are an expert software engineer. Fix the following GitHub issue in this repository. + + Issue #${CLAUDE_ISSUE_NUMBER}: ${CLAUDE_ISSUE_TITLE} + + Issue description: + ${CLAUDE_ISSUE_BODY} + + Instructions: + 1. Understand the root cause of the issue deeply before making any changes. + 2. Make the minimal change that fixes the issue — no unrelated refactoring. + 3. Follow the existing code style, naming conventions, and formatting. + 4. If the project has a test suite, add or update a test that covers this fix. + 5. Run the test suite and make sure all tests pass. + 6. Stage all changed files with 'git add'. + 7. Commit with the message: fix: (fixes #${CLAUDE_ISSUE_NUMBER}) + 8. Do NOT push — only commit locally. + 9. If you cannot confidently fix this issue without breaking anything, output exactly: + CANNOT_FIX: + and make no changes." + + # 9. Verify something was committed + - name: Check for new commits + if: steps.pick.outputs.no_issue == 'false' + id: check_commit + run: | + ahead=$(git rev-list --count origin/HEAD..HEAD 2>/dev/null || echo 0) + echo "commits_ahead=$ahead" >> "$GITHUB_OUTPUT" + if [ "$ahead" -eq 0 ]; then + echo "::warning::Claude Code made no commits — issue may not be fixable automatically." + fi + + # 10. Push branch and open PR + - name: Push branch and open PR + if: > + steps.pick.outputs.no_issue == 'false' && + steps.check_commit.outputs.commits_ahead != '0' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: ${{ steps.pick.outputs.branch }} + ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }} + ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }} + run: | + git push origin "$BRANCH" + + gh pr create \ + --title "fix: $ISSUE_TITLE (fixes rectorphp/rector#$ISSUE_NUMBER)" \ + --body "## Summary + Fixes rectorphp/rector#${ISSUE_NUMBER} — ${ISSUE_TITLE} + + ## Root cause + See the commit message and diff for details of what was changed and why. + + ## Testing + - Test suite run inside the workflow — see CI logs for details. + + > Opened automatically by the [Auto Fix Issue](.github/workflows/auto-fix-issue.yml) workflow. Please review carefully before merging." \ + --head "$BRANCH" \ + --label "auto-fix" + + # 11. Summary + - name: Write job summary + if: always() + env: + NO_ISSUE: ${{ steps.pick.outputs.no_issue }} + ISSUE_NUMBER: ${{ steps.pick.outputs.issue_number }} + ISSUE_TITLE: ${{ steps.pick.outputs.issue_title }} + BRANCH: ${{ steps.pick.outputs.branch }} + COMMITS_AHEAD: ${{ steps.check_commit.outputs.commits_ahead }} + run: | + if [ "$NO_ISSUE" = "true" ]; then + echo "## ✅ No candidates found" >> "$GITHUB_STEP_SUMMARY" + echo "No open issues without an existing PR were found." >> "$GITHUB_STEP_SUMMARY" + elif [ "${COMMITS_AHEAD:-0}" = "0" ]; then + echo "## ⚠️ Could not fix issue #${ISSUE_NUMBER}" >> "$GITHUB_STEP_SUMMARY" + echo "**${ISSUE_TITLE}** — Claude Code was unable to produce a commit." >> "$GITHUB_STEP_SUMMARY" + else + echo "## 🚀 PR opened for issue #${ISSUE_NUMBER}" >> "$GITHUB_STEP_SUMMARY" + echo "**${ISSUE_TITLE}** — branch: \`${BRANCH}\`" >> "$GITHUB_STEP_SUMMARY" + fi