-
Notifications
You must be signed in to change notification settings - Fork 554
Fix phpstan/phpstan#13526: False positive with array_key_exists and union types #5103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
30c231a
fe8c9bb
57f27b6
313ebe5
1718ecc
0006215
1272139
5add2a9
e4e435b
bd31d25
19447f1
0d1f531
b7d3172
25b268b
547f128
080bafa
2a5eaba
3bb4431
afa5966
97f58a0
da9ed28
5c6e070
5abeb3c
a712fce
f9efbdc
34a77ac
8263f6a
4512e57
450cf52
5ae8cfd
b43a04b
cbba989
411c411
8b39a71
d145799
7c6309c
c476b60
f554b84
37c4f55
394461f
1297915
0c27812
0998ee8
72f877f
c2da2e8
a7a7abc
8e131a9
20243ca
925c25a
9712da0
2e145a5
e04947f
0e8466f
b7b2ba4
e94c5c8
18c1ef7
2fb5636
a10a9a2
0e54233
e696ed0
74d0ddf
fd4c63b
94a0731
394e477
0bfbaf9
8882d85
dc7d857
95584e1
95d2866
0807b1b
5a5255e
5dac347
fc0e899
6b26f28
1c5bad8
ad296c2
b4735ef
b15ff63
45dea8d
ceec281
c537128
bb5fd87
1cd9ef9
90089b5
ef25c91
7dfcbf1
8c2b2b4
19b0f0b
edb776f
463007a
79e5543
9ef8c48
4e9dd5a
7eb282d
69fa23c
becebaa
f9d0efe
f3b36e6
fe05703
842777b
d1a2026
af2bc4a
2246d72
14ee1c0
86353a4
d74052a
a47ba97
87b9010
4b00cbf
b2d447a
12b6f66
d7e9f7a
58ad8e1
e5e64c7
fa86019
31ab876
5719ba9
1b1873c
7aca84b
e7d321d
0324196
df51ee0
7a43104
4781276
d56346a
95070d4
33eba6f
79eb04f
6803e54
ed40e65
786479f
a7b310d
0591587
b054374
76da17b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| name: "Claude Fix Issue" | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| issue-number: | ||
| description: "Issue number from phpstan/phpstan repository" | ||
| required: true | ||
| type: string | ||
| workflow_call: | ||
| inputs: | ||
| issue-number: | ||
| description: "Issue number from phpstan/phpstan repository" | ||
| required: true | ||
| type: string | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| fix: | ||
| name: "Fix #${{ inputs.issue-number }}" | ||
| runs-on: "ubuntu-latest" | ||
| timeout-minutes: 60 | ||
| permissions: | ||
| contents: read | ||
| issues: read | ||
| pull-requests: write | ||
|
|
||
| steps: | ||
| - name: Harden the runner (Audit all outbound calls) | ||
| uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: "Checkout" | ||
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | ||
| with: | ||
| ref: 2.1.x | ||
Check failureCode scanning / octoscan Use of 'actions/checkout' with a custom ref. Error
Use of 'actions/checkout' with a custom ref.
|
||
| repository: phpstan/phpstan-src | ||
| fetch-depth: 0 | ||
|
Comment on lines
+36
to
+41
Check warningCode scanning / zizmor credential persistence through GitHub Actions artifacts Warning
credential persistence through GitHub Actions artifacts
|
||
|
|
||
| - name: "Install PHP" | ||
| uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 | ||
| with: | ||
| coverage: "none" | ||
| php-version: "8.4" | ||
| ini-file: development | ||
| extensions: mbstring | ||
|
|
||
| - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 | ||
|
|
||
| - name: "Install Claude Code" | ||
| run: npm install -g @anthropic-ai/claude-code | ||
|
|
||
| - name: "Fetch issue details" | ||
| id: issue | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| ISSUE_NUMBER: ${{ inputs.issue-number }} | ||
| run: | | ||
| ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" \ | ||
| --repo phpstan/phpstan \ | ||
| --json title,body,url) | ||
|
|
||
Check failureCode scanning / octoscan Write to "$GITHUB_OUTPUT" in a bash script. Error
Write to "$GITHUB_OUTPUT" in a bash script.
|
||
| TITLE=$(echo "$ISSUE_JSON" | jq -r '.title') | ||
Check failureCode scanning / octoscan Write to "$GITHUB_OUTPUT" in a bash script. Error
Write to "$GITHUB_OUTPUT" in a bash script.
|
||
| URL=$(echo "$ISSUE_JSON" | jq -r '.url') | ||
| echo "title=$TITLE" >> "$GITHUB_OUTPUT" | ||
| echo "url=$URL" >> "$GITHUB_OUTPUT" | ||
| echo "$ISSUE_JSON" | jq -r '.body' > /tmp/issue-body.txt | ||
|
|
||
| - name: "Run Claude Code" | ||
| env: | ||
| CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | ||
| GH_TOKEN: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }} | ||
| run: | | ||
| git config user.name "phpstan-bot" | ||
Check failureCode scanning / octoscan Expression injection, "steps..outputs." is potentially untrusted. Error
Expression injection, "steps.**.outputs.**" is potentially untrusted.
|
||
| git config user.email "ondrej+phpstanbot@mirtes.cz" | ||
|
|
||
| claude --model claude-opus-4-6 \ | ||
| --dangerously-skip-permissions \ | ||
| -p "$(cat << 'PROMPT_EOF' | ||
| You are working on phpstan/phpstan-src, the source code of PHPStan - a PHP static analysis tool. | ||
|
|
||
| Your task is to fix the following GitHub issue from the phpstan/phpstan repository: | ||
| Issue phpstan/phpstan#${{ inputs.issue-number }}: ${{ steps.issue.outputs.title }} | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
Check warningCode scanning / zizmor code injection via template expansion Warning
code injection via template expansion
Check warningCode scanning / zizmor code injection via template expansion Warning
code injection via template expansion
Check noticeCode scanning / zizmor code injection via template expansion Note
code injection via template expansion
|
||
| URL: ${{ steps.issue.outputs.url }} | ||
Check warningCode scanning / zizmor code injection via template expansion Warning
code injection via template expansion
Check noticeCode scanning / zizmor code injection via template expansion Note
code injection via template expansion
|
||
|
|
||
| Issue body is in the file /tmp/issue-body.txt — read it before proceeding. | ||
|
|
||
| ## Step 1: Write a regression test | ||
|
|
||
| Read .claude/skills/regression-test/SKILL.md for detailed guidance on writing regression tests for PHPStan bugs. | ||
|
|
||
| The issue body is already provided above — start from Step 2 of the skill (deciding test type). For Step 1 (gathering context), you only need to fetch the playground samples from any playground links found in the issue body. | ||
|
|
||
| Skip Steps 5-6 of the skill (reverting fix and committing) — those are not needed here. | ||
|
|
||
| The regression test should fail without the fix — verify this by running it before implementing the fix. | ||
|
|
||
| ## Step 2: Fix the bug | ||
|
|
||
| Implement the fix in the source code under src/. Common areas to look: | ||
| - src/Analyser/NodeScopeResolver.php - AST traversal and scope management | ||
| - src/Analyser/MutatingScope.php - Type tracking | ||
| - src/Analyser/TypeSpecifier.php - Type narrowing from conditions | ||
| - src/Type/ - Type system implementations | ||
| - src/Rules/ - Rule implementations | ||
| - src/Reflection/ - Reflection layer | ||
|
|
||
| Read CLAUDE.md for important guidelines about the codebase architecture and common patterns. | ||
|
|
||
| ## Step 3: Verify the fix | ||
|
|
||
| 1. Run the regression test to confirm it passes now | ||
| 2. Run the full test suite: make tests | ||
| 3. Run PHPStan self-analysis: make phpstan | ||
| 4. Fix any failures that come up | ||
| 5. Run make cs-fix to fix any coding standard violations | ||
| 6. Run make name-collision and fix violations - add different tests in unique namespaces. If the function and class declarations are exactly the same, you can reuse them across files instead of duplicating them. | ||
|
|
||
| Do not create a branch, push, or create a PR - this will be handled automatically. | ||
|
|
||
| ## Step 4: Write a summary | ||
|
|
||
| After completing the fix, write two files: | ||
|
|
||
| 1. /tmp/commit-message.txt - A concise commit message (first line: short summary under 72 chars, then a blank line, then a few bullet points describing key changes). Example: | ||
| Fix array_key_exists narrowing for template types | ||
|
|
||
| - Added handling for TemplateType in TypeSpecifier when processing array_key_exists | ||
| - New regression test in tests/PHPStan/Analyser/nsrt/bug-12345.php | ||
| - The root cause was that TypeSpecifier did not unwrap template bounds before narrowing | ||
|
|
||
| 2. /tmp/pr-description.md - A pull request description in this format: | ||
| ## Summary | ||
| Brief description of what the issue was about and what the fix does. | ||
|
|
||
| ## Changes | ||
| - Bullet points of specific code changes made | ||
| - Reference file paths where changes were made | ||
|
|
||
| ## Root cause | ||
| Explain why the bug happened and how the fix addresses it. | ||
|
|
||
| ## Test | ||
| Describe the regression test that was added. | ||
|
|
||
| Fixes phpstan/phpstan#${{ inputs.issue-number }} | ||
Check failureCode scanning / zizmor code injection via template expansion Error
code injection via template expansion
Check warningCode scanning / zizmor code injection via template expansion Warning
code injection via template expansion
|
||
|
|
||
| These files are critical - they will be used for the commit message and PR description. | ||
| PROMPT_EOF | ||
| )" | ||
|
|
||
| - name: "Read Claude's summary" | ||
Check failureCode scanning / octoscan Write to "$GITHUB_OUTPUT" in a bash script. Error
Write to "$GITHUB_OUTPUT" in a bash script.
|
||
| id: claude-summary | ||
| env: | ||
| ISSUE_NUMBER: ${{ inputs.issue-number }} | ||
| run: | | ||
| if [ -f /tmp/commit-message.txt ]; then | ||
| delimiter="EOF_$(openssl rand -hex 16)" | ||
| { | ||
| echo "commit_message<<${delimiter}" | ||
| cat /tmp/commit-message.txt | ||
| echo "${delimiter}" | ||
Check failureCode scanning / octoscan Write to "$GITHUB_OUTPUT" in a bash script. Error
Write to "$GITHUB_OUTPUT" in a bash script.
|
||
| } >> "$GITHUB_OUTPUT" | ||
| else | ||
Check failureCode scanning / octoscan Write to "$GITHUB_OUTPUT" in a bash script. Error
Write to "$GITHUB_OUTPUT" in a bash script.
|
||
| echo "commit_message=Fix #$ISSUE_NUMBER" >> "$GITHUB_OUTPUT" | ||
Check failureCode scanning / octoscan Write to "$GITHUB_OUTPUT" in a bash script. Error
Write to "$GITHUB_OUTPUT" in a bash script.
|
||
| fi | ||
|
|
||
| if [ -f /tmp/pr-description.md ]; then | ||
| delimiter="EOF_$(openssl rand -hex 16)" | ||
| { | ||
| echo "pr_body<<${delimiter}" | ||
| cat /tmp/pr-description.md | ||
| echo "${delimiter}" | ||
| } >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "pr_body=Fixes phpstan/phpstan#$ISSUE_NUMBER" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
Comment on lines
+1
to
+180
Check warningCode scanning / zizmor insufficient job-level concurrency limits Warning
insufficient job-level concurrency limits
|
||
|
|
||
| - name: "Create Pull Request" | ||
| id: create-pr | ||
| uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 | ||
| with: | ||
| branch-token: ${{ secrets.PHPSTAN_BOT_FORK_TOKEN }} | ||
| token: ${{ secrets.PHPSTAN_BOT_PR_TOKEN }} | ||
| push-to-fork: phpstan-bot/phpstan-src | ||
| branch-suffix: random | ||
| delete-branch: true | ||
| title: "Fix #${{ inputs.issue-number }}: ${{ steps.issue.outputs.title }}" | ||
| body: ${{ steps.claude-summary.outputs.pr_body }} | ||
| committer: "phpstan-bot <ondrej+phpstanbot@mirtes.cz>" | ||
| commit-message: ${{ steps.claude-summary.outputs.commit_message }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| name: "Claude Random Easy Fixes (Scheduled)" | ||
|
|
||
| on: | ||
| schedule: | ||
| # Run every day, 4 times, once an hour at :15, from 2pm CET (13:00 UTC) to 5pm CET (16:00 UTC) | ||
| - cron: '15 13-16 * * *' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| trigger: | ||
Check noticeCode scanning / zizmor workflow or action definition without a name Note
workflow or action definition without a name
|
||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| actions: write | ||
Check warningCode scanning / zizmor permissions without explanatory comments Warning
permissions without explanatory comments
|
||
| steps: | ||
| - name: Harden the runner (Audit all outbound calls) | ||
| uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: Trigger Claude Random Easy Fixes | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: gh workflow run claude-random-easy-fixes.yml -f issue_count=5 --repo ${{ github.repository }} | ||
Check warningCode scanning / zizmor code injection via template expansion Warning
code injection via template expansion
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| name: "Claude Random Easy Fixes" | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| issue_count: | ||
| description: "Number of issues to pick and fix in parallel" | ||
| required: false | ||
| default: "1" | ||
| type: string | ||
|
|
||
| jobs: | ||
| pick-issues: | ||
| name: "Pick easy fix issues" | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
|
|
||
| outputs: | ||
| matrix: ${{ steps.pick-issues.outputs.matrix }} | ||
|
|
||
| permissions: | ||
| contents: read | ||
| issues: read | ||
Check warningCode scanning / zizmor permissions without explanatory comments Warning
permissions without explanatory comments
|
||
|
|
||
| steps: | ||
| - name: Harden the runner (Audit all outbound calls) | ||
| uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 | ||
| with: | ||
| egress-policy: audit | ||
|
|
||
| - name: "Pick random Easy fix issues" | ||
| id: pick-issues | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| ISSUE_COUNT: ${{ inputs.issue_count || '1' }} | ||
| run: | | ||
| # Look up milestone number for "Easy fixes" | ||
| MILESTONE_NUMBER=$(gh api "repos/phpstan/phpstan/milestones?per_page=100" \ | ||
| --jq '.[] | select(.title == "Easy fixes") | .number') | ||
| if [ -z "$MILESTONE_NUMBER" ]; then | ||
| echo "Could not find 'Easy fixes' milestone" | ||
| exit 1 | ||
| fi | ||
| # Fetch all open issues in the milestone using pagination | ||
| ISSUE_JSON=$(gh api --paginate \ | ||
| "repos/phpstan/phpstan/issues?state=open&milestone=${MILESTONE_NUMBER}&per_page=100" \ | ||
| --jq '[.[] | {number: .number, title: .title}]' \ | ||
| | jq -s 'add // []') | ||
| TOTAL=$(echo "$ISSUE_JSON" | jq 'length') | ||
| if [ "$TOTAL" -eq 0 ]; then | ||
| echo "No issues found in Easy fixes milestone" | ||
| exit 1 | ||
| fi | ||
| COUNT=$ISSUE_COUNT | ||
| if [ "$COUNT" -gt "$TOTAL" ]; then | ||
| COUNT=$TOTAL | ||
| fi | ||
| # Pick COUNT random unique issues | ||
| SELECTED=$(echo "$ISSUE_JSON" | python3 -c " | ||
| import json, sys, random | ||
| issues = json.load(sys.stdin) | ||
| random.shuffle(issues) | ||
| count = min(int('$COUNT'), len(issues)) | ||
| print(json.dumps(issues[:count])) | ||
| ") | ||
| echo "Selected $COUNT issue(s) for fixing" | ||
| for NUMBER in $(echo "$SELECTED" | jq -r '.[].number'); do | ||
| TITLE=$(echo "$SELECTED" | jq -r --argjson n "$NUMBER" '.[] | select(.number == $n) | .title') | ||
| echo "### Selected issue: #$NUMBER - $TITLE" >> "$GITHUB_STEP_SUMMARY" | ||
| done | ||
| echo "matrix=$(echo "$SELECTED" | jq -c '.')" >> "$GITHUB_OUTPUT" | ||
Check failureCode scanning / octoscan Write to "$GITHUB_OUTPUT" in a bash script. Error
Write to "$GITHUB_OUTPUT" in a bash script.
|
||
| easy-fix: | ||
| name: "Fix #${{ matrix.issue.number }}: ${{ matrix.issue.title }}" | ||
| needs: pick-issues | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| issue: ${{ fromJson(needs.pick-issues.outputs.matrix) }} | ||
| uses: ./.github/workflows/claude-fix-issue.yml | ||
Check warningCode scanning / zizmor secrets unconditionally inherited by called workflow Warning
secrets unconditionally inherited by called workflow
Check failureCode scanning / octoscan Use of local workflow "./.github/workflows/claude-fix-issue.yml" Error
Use of local workflow "./.github/workflows/claude-fix-issue.yml"
|
||
| with: | ||
| issue-number: ${{ matrix.issue.number }} | ||
| secrets: inherit | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ on: | |
| pull_request: | ||
| push: | ||
| branches: | ||
| - "2.1.x" | ||
| - "2.2.x" | ||
|
|
||
| permissions: {} | ||
|
|
||
|
|
||
Check warning
Code scanning / zizmor
permissions without explanatory comments Warning