Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 0 additions & 137 deletions .github/workflows/claude-regression-tests.yml

This file was deleted.

92 changes: 91 additions & 1 deletion .github/workflows/issue-bot.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions

name: "Issue bot"

Check notice on line 3 in .github/workflows/issue-bot.yml

View workflow job for this annotation

GitHub Actions / Evaluate results

Issue bot detected open issues which are affected by this pull request - see https://github.com/phpstan/phpstan-src/actions/runs/21949873129

on:
workflow_dispatch:
Expand Down Expand Up @@ -192,16 +192,25 @@
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
run: |
set +e
./console.php evaluate >> $GITHUB_STEP_SUMMARY
./console.php evaluate > tmp/step-summary.md
exit_code="$?"

cat tmp/step-summary.md >> $GITHUB_STEP_SUMMARY

if [[ "$exit_code" == "2" ]]; then
echo "::notice file=.github/workflows/issue-bot.yml,line=3 ::Issue bot detected open issues which are affected by this pull request - see https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
exit 0
fi

exit $exit_code

- name: "Upload step summary"
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: step-summary
path: issue-bot/tmp/step-summary.md

- name: "Evaluate results - push"
working-directory: "issue-bot"
if: "github.repository_owner == 'phpstan' && github.ref == 'refs/heads/2.1.x'"
Expand All @@ -220,3 +229,84 @@
fi

exit $exit_code

regression-tests:
name: "Generate regression tests"
needs: evaluate
if: github.event_name == 'pull_request' && github.event.pull_request.user.login == 'phpstan-bot'
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 60

steps:
- name: "Check for feedback loop"
id: check
env:
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }}
run: |
COMMIT_MSG=$(gh api "repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}" --jq '.commit.message' 2>/dev/null || true)
if [[ "$COMMIT_MSG" == "Add regression test for #"* ]]; then
echo "Last commit was regression test addition, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: "Checkout"
if: steps.check.outputs.skip != 'true'
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0
token: ${{ secrets.PHPSTAN_BOT_TOKEN }}

- name: "Download step summary"
if: steps.check.outputs.skip != 'true'
uses: actions/download-artifact@v4
with:
name: step-summary
path: ./tmp

- name: "Install PHP"
if: steps.check.outputs.skip != 'true'
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.4"
ini-file: development
extensions: mbstring

- name: "Install dependencies"
if: steps.check.outputs.skip != 'true'
uses: "ramsey/composer-install@v3"

- name: "Install Claude Code"
if: steps.check.outputs.skip != 'true'
run: npm install -g @anthropic-ai/claude-code

- name: "Generate regression tests"
if: steps.check.outputs.skip != 'true'
env:
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }}
run: |
git config user.name "phpstan-bot"
git config user.email "ondrej+phpstanbot@mirtes.cz"

claude -p \
--model claude-opus-4-6 \
--output-format stream-json \
--verbose \
--dangerously-skip-permissions \
"Read the file ./tmp/step-summary.md which contains the Issue Bot step summary from CI in Markdown format. Interpret this Markdown to figure out which GitHub issues need regression tests added. For each affected issue (where behavior changed), run /regression-test with the issue number.

Do not create a branch or push - this will be handled automatically."

- name: "Push"
if: steps.check.outputs.skip != 'true'
run: |
if [ "$(git rev-parse HEAD)" = "$(git rev-parse @{u})" ]; then
echo "No new commits to push"
exit 0
fi

git push
21 changes: 20 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3818,7 +3818,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
$scope->getNamespace(),
$scope->expressionTypes,
$scope->nativeExpressionTypes,
array_merge($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions),
$this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions),
$scope->inClosureBindScopeClasses,
$scope->anonymousFunctionReflection,
$scope->inFirstLevelStatement,
Expand Down Expand Up @@ -4007,6 +4007,25 @@ private function intersectConditionalExpressions(array $otherConditionalExpressi
return $newConditionalExpressions;
}

/**
* @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressions
* @param array<string, ConditionalExpressionHolder[]> $existingConditionalExpressions
* @return array<string, ConditionalExpressionHolder[]>
*/
private function mergeConditionalExpressions(array $newConditionalExpressions, array $existingConditionalExpressions): array
{
$result = $existingConditionalExpressions;
foreach ($newConditionalExpressions as $exprString => $holders) {
if (!array_key_exists($exprString, $result)) {
$result[$exprString] = $holders;
} else {
$result[$exprString] = array_merge($result[$exprString], $holders);
}
}

return $result;
}

/**
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
* @param array<string, ExpressionTypeHolder> $ourExpressionTypes
Expand Down
61 changes: 61 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4364,6 +4364,13 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
$matchScope = $matchScope->filterByFalseyValue($filteringExpr);
}

if (!$hasDefaultCond && !$hasAlwaysTrueCond && $condType->isBoolean()->yes() && $condType->isConstantScalarValue()->yes()) {
if ($this->isScopeConditionallyImpossible($matchScope)) {
$hasAlwaysTrueCond = true;
$matchScope = $matchScope->addTypeToExpression($expr->cond, new NeverType());
}
}

$isExhaustive = $hasDefaultCond || $hasAlwaysTrueCond;
if (!$isExhaustive) {
$remainingType = $matchScope->getType($expr->cond);
Expand Down Expand Up @@ -7598,6 +7605,60 @@ private function getFilteringExprForMatchArm(Expr\Match_ $expr, array $condition
);
}

/**
* Checks if a scope's conditional expressions form a contradiction,
* meaning no combination of variable values is possible.
* Used for match(true) exhaustiveness detection.
*/
private function isScopeConditionallyImpossible(MutatingScope $scope): bool
{
$boolVars = [];
foreach ($scope->getDefinedVariables() as $varName) {
$varType = $scope->getVariableType($varName);
if ($varType->isBoolean()->yes() && !$varType->isConstantScalarValue()->yes()) {
$boolVars[] = $varName;
}
}

if ($boolVars === []) {
return false;
}

// Check if any boolean variable's both truth values lead to contradictions
foreach ($boolVars as $varName) {
$varExpr = new Variable($varName);

$truthyScope = $scope->filterByTruthyValue($varExpr);
$truthyContradiction = $this->scopeHasNeverVariable($truthyScope, $boolVars);
if (!$truthyContradiction) {
continue;
}

$falseyScope = $scope->filterByFalseyValue($varExpr);
$falseyContradiction = $this->scopeHasNeverVariable($falseyScope, $boolVars);
if ($falseyContradiction) {
return true;
}
}

return false;
}

/**
* @param string[] $varNames
*/
private function scopeHasNeverVariable(MutatingScope $scope, array $varNames): bool
{
foreach ($varNames as $varName) {
$type = $scope->getVariableType($varName);
if ($type instanceof NeverType) {
return true;
}
}

return false;
}

private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, MutatingScope $bodyScope): MutatingScope
{
// infer $items[$i] type from for ($i = 0; $i < count($items); $i++) {...}
Expand Down
10 changes: 10 additions & 0 deletions src/Analyser/SpecifiedTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,16 @@ public function unionWith(SpecifiedTypes $other): self
$result = $result->setAlwaysOverwriteTypes();
}

$conditionalExpressionHolders = $this->newConditionalExpressionHolders;
foreach ($other->newConditionalExpressionHolders as $exprString => $holders) {
if (!array_key_exists($exprString, $conditionalExpressionHolders)) {
$conditionalExpressionHolders[$exprString] = $holders;
} else {
$conditionalExpressionHolders[$exprString] = array_merge($conditionalExpressionHolders[$exprString], $holders);
}
}
$result->newConditionalExpressionHolders = $conditionalExpressionHolders;

return $result->setRootExpr($rootExpr);
}

Expand Down
Loading
Loading