From 30c231a9eaab0c6b291b45d885b8897f978925dc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 13 Feb 2026 10:49:03 +0100 Subject: [PATCH 01/70] Open 2.2.x --- .github/workflows/backward-compatibility.yml | 2 +- .github/workflows/build-issue-bot.yml | 2 +- .github/workflows/changelog-generator.yml | 2 +- .github/workflows/e2e-tests.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/phar.yml | 24 ++++++++++---------- .github/workflows/reflection-golden-test.yml | 2 +- .github/workflows/spelling.yml | 2 +- .github/workflows/static-analysis.yml | 2 +- .github/workflows/tests.yml | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index 53f74a49962..3e1c4662279 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" paths: - 'src/**' - '.github/workflows/backward-compatibility.yml' diff --git a/.github/workflows/build-issue-bot.yml b/.github/workflows/build-issue-bot.yml index 6bb4e62e30d..6debccc9350 100644 --- a/.github/workflows/build-issue-bot.yml +++ b/.github/workflows/build-issue-bot.yml @@ -9,7 +9,7 @@ on: - '.github/workflows/build-issue-bot.yml' push: branches: - - "2.1.x" + - "2.2.x" paths: - 'issue-bot/**' - '.github/workflows/build-issue-bot.yml' diff --git a/.github/workflows/changelog-generator.yml b/.github/workflows/changelog-generator.yml index 1dfc0d775cb..cd76fb3188d 100644 --- a/.github/workflows/changelog-generator.yml +++ b/.github/workflows/changelog-generator.yml @@ -9,7 +9,7 @@ on: - '.github/workflows/changelog-generator.yml' push: branches: - - "2.1.x" + - "2.2.x" paths: - 'changelog-generator/**' - '.github/workflows/changelog-generator.yml' diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index dbd205476cb..e57002cd237 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1cd3a2c44a7..43c28049931 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" concurrency: group: lint-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index 79ba4438682..be5ef62818b 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -6,9 +6,9 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" tags: - - '2.1.*' + - '2.2.*' concurrency: group: phar-${{ github.ref }} # will be canceled on subsequent pushes in both branches and pull requests @@ -90,14 +90,14 @@ jobs: - uses: "ramsey/composer-install@v3" env: - COMPOSER_ROOT_VERSION: "2.1.x-dev" + COMPOSER_ROOT_VERSION: "2.2.x-dev" - name: "Compile PHAR for checksum" working-directory: "compiler/build" run: "php ../box/vendor/bin/box compile --no-parallel --sort-compiled-files" env: PHAR_CHECKSUM: "1" - COMPOSER_ROOT_VERSION: "2.1.x-dev" + COMPOSER_ROOT_VERSION: "2.2.x-dev" - name: "Re-sign PHAR" run: "php compiler/build/resign.php tmp/phpstan.phar" @@ -129,25 +129,25 @@ jobs: integration-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/integration-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} extension-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/extension-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} other-tests: if: github.event_name == 'pull_request' needs: compiler-tests - uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.1.x + uses: phpstan/phpstan/.github/workflows/other-tests.yml@2.2.x with: - ref: 2.1.x + ref: 2.2.x phar-checksum: ${{needs.compiler-tests.outputs.checksum}} download-base-sha-phar: @@ -278,7 +278,7 @@ jobs: commit: name: "Commit PHAR" - if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.1.x' || startsWith(github.ref, 'refs/tags/'))" + if: "github.repository_owner == 'phpstan' && (github.ref == 'refs/heads/2.2.x' || startsWith(github.ref, 'refs/tags/'))" needs: compiler-tests runs-on: "ubuntu-latest" timeout-minutes: 60 @@ -300,7 +300,7 @@ jobs: repository: phpstan/phpstan path: phpstan-dist token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - ref: 2.1.x + ref: 2.2.x - name: "Get previous pushed dist commit" id: previous-commit diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml index 7f962ce2c0f..f57fa94f9b6 100644 --- a/.github/workflows/reflection-golden-test.yml +++ b/.github/workflows/reflection-golden-test.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index b2f810732c2..24f48d2bb76 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" jobs: typos: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index f61dcac3aa1..681d948eb38 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -9,7 +9,7 @@ on: - 'apigen/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0fdceac7231..cfd78291360 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' From 25b268bd168d028c8a88ba540facdb99b75c4e63 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 10:44:49 +0100 Subject: [PATCH 02/70] Add agentic workflow to document new config parameters Automatically detects undocumented parameters in conf/parametersSchema.neon and creates a draft PR on phpstan/phpstan with documentation updates. Triggers on push to 2.2.x when the schema changes, or manually. Co-Authored-By: Claude Opus 4.6 --- .gitattributes | 2 + .../workflows/document-config-params.lock.yml | 1152 +++++++++++++++++ .github/workflows/document-config-params.md | 132 ++ 3 files changed, 1286 insertions(+) create mode 100644 .github/workflows/document-config-params.lock.yml create mode 100644 .github/workflows/document-config-params.md diff --git a/.gitattributes b/.gitattributes index ed98b8a4c8a..ba520f3418f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,5 @@ *.stub linguist-language=PHP tests/PHPStan/Command/ErrorFormatter/data/WindowsNewlines.php eol=crlf + +.github/workflows/*.lock.yml linguist-generated=true merge=ours \ No newline at end of file diff --git a/.github/workflows/document-config-params.lock.yml b/.github/workflows/document-config-params.lock.yml new file mode 100644 index 00000000000..bf8a0262011 --- /dev/null +++ b/.github/workflows/document-config-params.lock.yml @@ -0,0 +1,1152 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.43.23). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan +# +# frontmatter-hash: 69e695fc26ed030a830663b4989c2181e28836aebc20d9269f866543d9f36bab + +name: "Document Config Parameters" +"on": + push: + branches: + - 2.2.x + paths: + - conf/parametersSchema.neon + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.ref }}" + +run-name: "Document Config Parameters" + +jobs: + activation: + needs: pre_activation + if: needs.pre_activation.outputs.activated == 'true' + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + with: + destination: /opt/gh-aw/actions + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "document-config-params.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GH_AW_ASSETS_ALLOWED_EXTS: "" + GH_AW_ASSETS_BRANCH: "" + GH_AW_ASSETS_MAX_SIZE_KB: 0 + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_WORKFLOW_ID_SANITIZED: documentconfigparams + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + has_patch: ${{ steps.collect_output.outputs.has_patch }} + model: ${{ steps.generate_aw_info.outputs.model }} + output: ${{ steps.collect_output.outputs.output }} + output_types: ${{ steps.collect_output.outputs.output_types }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + with: + destination: /opt/gh-aw/actions + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + with: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "claude", + engine_name: "Claude Code", + model: "claude-opus-4-6", + version: "", + agent_version: "2.1.39", + cli_version: "v0.43.23", + workflow_name: "Document Config Parameters", + experimental: false, + supports_tools_allowlist: true, + supports_http_transport: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults"], + firewall_enabled: true, + awf_version: "v0.17.0", + awmg_version: "", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.17.0 + - name: Install Claude Code CLI + run: npm install -g --silent @anthropic-ai/claude-code@2.1.39 + - name: Determine automatic lockdown mode for GitHub MCP server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.17.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.17.0 ghcr.io/github/gh-aw-firewall/squid:0.17.0 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine + - name: Write Safe Outputs Config + run: | + mkdir -p /opt/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/safeoutputs + mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs + cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' + {"create_pull_request":{},"missing_data":{},"missing_tool":{},"noop":{"max":1}} + GH_AW_SAFE_OUTPUTS_CONFIG_EOF + cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' + [ + { + "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Docs] \". PRs will be created as drafts.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "body": { + "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", + "type": "string" + }, + "branch": { + "description": "Source branch name containing the changes. If omitted, uses the current working branch.", + "type": "string" + }, + "labels": { + "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", + "items": { + "type": "string" + }, + "type": "array" + }, + "title": { + "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "reason": { + "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", + "type": "string" + }, + "tool": { + "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", + "type": "string" + } + }, + "required": [ + "reason" + ], + "type": "object" + }, + "name": "missing_tool" + }, + { + "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "message": { + "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "name": "noop" + }, + { + "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", + "inputSchema": { + "additionalProperties": false, + "properties": { + "alternatives": { + "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", + "type": "string" + }, + "context": { + "description": "Additional context about the missing data or where it should come from (max 256 characters).", + "type": "string" + }, + "data_type": { + "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", + "type": "string" + }, + "reason": { + "description": "Explanation of why this data is needed to complete the task (max 256 characters).", + "type": "string" + } + }, + "required": [], + "type": "object" + }, + "name": "missing_data" + } + ] + GH_AW_SAFE_OUTPUTS_TOOLS_EOF + cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' + { + "create_pull_request": { + "defaultMax": 1, + "fields": { + "body": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + }, + "branch": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "labels": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "title": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "missing_tool": { + "defaultMax": 20, + "fields": { + "alternatives": { + "type": "string", + "sanitize": true, + "maxLength": 512 + }, + "reason": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "tool": { + "type": "string", + "sanitize": true, + "maxLength": 128 + } + } + }, + "noop": { + "defaultMax": 1, + "fields": { + "message": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 65000 + } + } + } + } + GH_AW_SAFE_OUTPUTS_VALIDATION_EOF + - name: Generate Safe Outputs MCP Server Config + id: safe-outputs-config + run: | + # Generate a secure random API key (360 bits of entropy, 40+ chars) + # Mask immediately to prevent timing vulnerabilities + API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${API_KEY}" + + PORT=3001 + + # Set outputs for next steps + { + echo "safe_outputs_api_key=${API_KEY}" + echo "safe_outputs_port=${PORT}" + } >> "$GITHUB_OUTPUT" + + echo "Safe Outputs MCP server will run on port ${PORT}" + + - name: Start Safe Outputs MCP HTTP Server + id: safe-outputs-start + env: + DEBUG: '*' + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} + GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json + GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json + GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs + run: | + # Environment variables are set above to prevent template injection + export DEBUG + export GH_AW_SAFE_OUTPUTS_PORT + export GH_AW_SAFE_OUTPUTS_API_KEY + export GH_AW_SAFE_OUTPUTS_TOOLS_PATH + export GH_AW_SAFE_OUTPUTS_CONFIG_PATH + export GH_AW_MCP_LOG_DIR + + bash /opt/gh-aw/actions/start_safe_outputs_server.sh + + - name: Start MCP gateway + id: start-mcp-gateway + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} + GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + export GH_AW_ENGINE="claude" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' + + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + }, + "safeoutputs": { + "type": "http", + "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", + "headers": { + "Authorization": "$GH_AW_SAFE_OUTPUTS_API_KEY" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GitHub API Access Instructions + + The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. + + + To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. + + Temporary IDs: Some safe output tools support a temporary ID field (usually named temporary_id) so you can reference newly-created items elsewhere in the SAME agent output (for example, using #aw_abc1 in a later body). + + **IMPORTANT - temporary_id format rules:** + - If you DON'T need to reference the item later, OMIT the temporary_id field entirely (it will be auto-generated if needed) + - If you DO need cross-references/chaining, you MUST match this EXACT validation regex: /^aw_[A-Za-z0-9]{3,8}$/i + - Format: aw_ prefix followed by 3 to 8 alphanumeric characters (A-Z, a-z, 0-9, case-insensitive) + - Valid alphanumeric characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 + - INVALID examples: aw_ab (too short), aw_123456789 (too long), aw_test-id (contains hyphen), aw_id_123 (contains underscore) + - VALID examples: aw_abc, aw_abc1, aw_Test123, aw_A1B2C3D4, aw_12345678 + - To generate valid IDs: use 3-8 random alphanumeric characters or omit the field to let the system auto-generate + + Do NOT invent other aw_* formats — downstream steps will reject them with validation errors matching against /^aw_[A-Za-z0-9]{3,8}$/i. + + Discover available tools from the safeoutputs MCP server. + + **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. + + **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. + + + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/document-config-params.md}} + GH_AW_PROMPT_EOF + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + with: + script: | + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE + } + }); + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh + - name: Execute Claude Code CLI + id: agentic_execution + # Allowed tools (sorted): + # - Bash + # - BashOutput + # - Edit + # - ExitPlanMode + # - Glob + # - Grep + # - KillBash + # - LS + # - MultiEdit + # - NotebookEdit + # - NotebookRead + # - Read + # - Task + # - TodoWrite + # - Write + # - mcp__github__download_workflow_run_artifact + # - mcp__github__get_code_scanning_alert + # - mcp__github__get_commit + # - mcp__github__get_dependabot_alert + # - mcp__github__get_discussion + # - mcp__github__get_discussion_comments + # - mcp__github__get_file_contents + # - mcp__github__get_job_logs + # - mcp__github__get_label + # - mcp__github__get_latest_release + # - mcp__github__get_me + # - mcp__github__get_notification_details + # - mcp__github__get_pull_request + # - mcp__github__get_pull_request_comments + # - mcp__github__get_pull_request_diff + # - mcp__github__get_pull_request_files + # - mcp__github__get_pull_request_review_comments + # - mcp__github__get_pull_request_reviews + # - mcp__github__get_pull_request_status + # - mcp__github__get_release_by_tag + # - mcp__github__get_secret_scanning_alert + # - mcp__github__get_tag + # - mcp__github__get_workflow_run + # - mcp__github__get_workflow_run_logs + # - mcp__github__get_workflow_run_usage + # - mcp__github__issue_read + # - mcp__github__list_branches + # - mcp__github__list_code_scanning_alerts + # - mcp__github__list_commits + # - mcp__github__list_dependabot_alerts + # - mcp__github__list_discussion_categories + # - mcp__github__list_discussions + # - mcp__github__list_issue_types + # - mcp__github__list_issues + # - mcp__github__list_label + # - mcp__github__list_notifications + # - mcp__github__list_pull_requests + # - mcp__github__list_releases + # - mcp__github__list_secret_scanning_alerts + # - mcp__github__list_starred_repositories + # - mcp__github__list_tags + # - mcp__github__list_workflow_jobs + # - mcp__github__list_workflow_run_artifacts + # - mcp__github__list_workflow_runs + # - mcp__github__list_workflows + # - mcp__github__pull_request_read + # - mcp__github__search_code + # - mcp__github__search_issues + # - mcp__github__search_orgs + # - mcp__github__search_pull_requests + # - mcp__github__search_repositories + # - mcp__github__search_users + timeout-minutes: 30 + run: | + set -o pipefail + sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.17.0 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + BASH_DEFAULT_TIMEOUT_MS: 60000 + BASH_MAX_TIMEOUT_MS: 60000 + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + DISABLE_BUG_COMMAND: 1 + DISABLE_ERROR_REPORTING: 1 + DISABLE_TELEMETRY: 1 + GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GITHUB_WORKSPACE: ${{ github.workspace }} + MCP_TIMEOUT: 120000 + MCP_TOOL_TIMEOUT: 60000 + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Stop MCP gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'ANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,PHPSTAN_BOT_TOKEN' + SECRET_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + SECRET_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SECRET_PHPSTAN_BOT_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + - name: Upload Safe Outputs + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: safe-output + path: ${{ env.GH_AW_SAFE_OUTPUTS }} + if-no-files-found: warn + - name: Ingest agent output + id: collect_output + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" + GITHUB_SERVER_URL: ${{ github.server_url }} + GITHUB_API_URL: ${{ github.api_url }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); + await main(); + - name: Upload sanitized agent output + if: always() && env.GH_AW_AGENT_OUTPUT + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-output + path: ${{ env.GH_AW_AGENT_OUTPUT }} + if-no-files-found: warn + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_claude_log.cjs'); + await main(); + - name: Parse MCP gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + /tmp/gh-aw/aw.patch + if-no-files-found: ignore + + conclusion: + needs: + - activation + - agent + - detection + - safe_outputs + if: (always()) && (needs.agent.result != 'skipped') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + outputs: + noop_message: ${{ steps.noop.outputs.noop_message }} + tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} + total_count: ${{ steps.missing_tool.outputs.total_count }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Process No-Op Messages + id: noop + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_NOOP_MAX: 1 + GH_AW_WORKFLOW_NAME: "Document Config Parameters" + with: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/noop.cjs'); + await main(); + - name: Record Missing Tool + id: missing_tool + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Document Config Parameters" + with: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); + await main(); + - name: Handle Agent Failure + id: handle_agent_failure + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Document Config Parameters" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_WORKFLOW_ID: "document-config-params" + GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} + GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} + with: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); + await main(); + - name: Handle No-Op Message + id: handle_noop_message + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Document Config Parameters" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} + GH_AW_NOOP_REPORT_AS_ISSUE: "true" + with: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); + await main(); + - name: Handle Create Pull Request Error + id: handle_create_pr_error + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_WORKFLOW_NAME: "Document Config Parameters" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs'); + await main(); + - name: Update reaction comment with completion status + id: conclusion + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} + GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_WORKFLOW_NAME: "Document Config Parameters" + GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} + GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} + with: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); + await main(); + + detection: + needs: agent + if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' + runs-on: ubuntu-latest + permissions: {} + timeout-minutes: 10 + outputs: + success: ${{ steps.parse_results.outputs.success }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + with: + destination: /opt/gh-aw/actions + - name: Download agent artifacts + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/threat-detection/ + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/threat-detection/ + - name: Echo agent output types + env: + AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} + run: | + echo "Agent output-types: $AGENT_OUTPUT_TYPES" + - name: Setup threat detection + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + WORKFLOW_NAME: "Document Config Parameters" + WORKFLOW_DESCRIPTION: "Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); + await main(); + - name: Ensure threat-detection directory and log + run: | + mkdir -p /tmp/gh-aw/threat-detection + touch /tmp/gh-aw/threat-detection/detection.log + - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install Claude Code CLI + run: npm install -g --silent @anthropic-ai/claude-code@2.1.39 + - name: Execute Claude Code CLI + id: agentic_execution + # Allowed tools (sorted): + # - Bash(cat) + # - Bash(grep) + # - Bash(head) + # - Bash(jq) + # - Bash(ls) + # - Bash(tail) + # - Bash(wc) + # - BashOutput + # - ExitPlanMode + # - Glob + # - Grep + # - KillBash + # - LS + # - NotebookRead + # - Read + # - Task + # - TodoWrite + timeout-minutes: 20 + run: | + set -o pipefail + # Execute Claude Code CLI with prompt from file + claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --allowed-tools 'Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite' --debug-file /tmp/gh-aw/threat-detection/detection.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + BASH_DEFAULT_TIMEOUT_MS: 60000 + BASH_MAX_TIMEOUT_MS: 60000 + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + DISABLE_BUG_COMMAND: 1 + DISABLE_ERROR_REPORTING: 1 + DISABLE_TELEMETRY: 1 + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_WORKSPACE: ${{ github.workspace }} + MCP_TIMEOUT: 120000 + MCP_TOOL_TIMEOUT: 60000 + - name: Parse threat detection results + id: parse_results + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); + await main(); + - name: Upload threat detection log + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: threat-detection.log + path: /tmp/gh-aw/threat-detection/detection.log + if-no-files-found: ignore + + pre_activation: + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + with: + destination: /opt/gh-aw/actions + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: admin,maintainer,write + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); + await main(); + + safe_outputs: + needs: + - activation + - agent + - detection + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') + runs-on: ubuntu-slim + permissions: + contents: write + issues: write + pull-requests: write + timeout-minutes: 15 + env: + GH_AW_ENGINE_ID: "claude" + GH_AW_ENGINE_MODEL: "claude-opus-4-6" + GH_AW_WORKFLOW_ID: "document-config-params" + GH_AW_WORKFLOW_NAME: "Document Config Parameters" + outputs: + create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} + create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + with: + destination: /opt/gh-aw/actions + - name: Download agent output artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-output + path: /tmp/gh-aw/safeoutputs/ + - name: Setup agent output environment variable + run: | + mkdir -p /tmp/gh-aw/safeoutputs/ + find "/tmp/gh-aw/safeoutputs/" -type f -print + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" + - name: Download patch artifact + continue-on-error: true + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: agent-artifacts + path: /tmp/gh-aw/ + - name: Checkout repository + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: phpstan/phpstan + token: ${{ github.token }} + persist-credentials: false + fetch-depth: 1 + - name: Configure Git credentials + if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) + env: + REPO_NAME: "phpstan/phpstan" + SERVER_URL: ${{ github.server_url }} + GIT_TOKEN: ${{ github.token }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Process Safe Outputs + id: process_safe_outputs + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":true,\"fallback_as_issue\":true,\"max\":1,\"max_patch_size\":1024,\"target-repo\":\"phpstan/phpstan\",\"title_prefix\":\"[Docs] \"},\"missing_data\":{},\"missing_tool\":{}}" + with: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); + await main(); + diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md new file mode 100644 index 00000000000..a232aed9b53 --- /dev/null +++ b/.github/workflows/document-config-params.md @@ -0,0 +1,132 @@ +--- +name: Document Config Parameters +description: Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan +on: + push: + branches: [2.2.x] + paths: [conf/parametersSchema.neon] + workflow_dispatch: +engine: + id: claude + model: claude-opus-4-6 + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} +permissions: + contents: read + issues: read + pull-requests: read +tools: + bash: ["*"] + github: + toolsets: [default, repos] +safe-outputs: + github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + create-pull-request: + target-repo: phpstan/phpstan + title-prefix: "[Docs] " + draft: true + fallback-as-issue: true +timeout-minutes: 30 +--- + +# Document Undocumented Config Parameters + +You are a documentation agent for PHPStan. Your job is to find configuration parameters that exist in the schema but lack user-facing documentation, and to write documentation for them. + +## Source files + +- **Parameter schema**: `conf/parametersSchema.neon` in this workspace (phpstan-src repo) +- **Config reference docs**: `website/src/config-reference.md` in the `phpstan/phpstan` repository — fetch this file using the GitHub `get_file_contents` tool from the `phpstan/phpstan` repo + +## Task + +### Step 1: Read both files + +1. Read `conf/parametersSchema.neon` from the workspace +2. Fetch `website/src/config-reference.md` from the `phpstan/phpstan` repo using the GitHub tools + +### Step 2: Identify user-facing parameters from the schema + +Extract all parameter names from `parametersSchema.neon`. **Skip these entirely:** + +- The entire `featureToggles` section and all its sub-parameters +- Everything after the `# playground mode` comment — these are internal/irrelevant: + - `sourceLocatorPlaygroundMode` + - Nette parameters: `debugMode`, `productionMode`, `tempDir`, `__validate` + - DerivativeContainerFactory internals: `additionalConfigFiles`, `generateBaselineFile`, `analysedPaths`, `allConfigFiles`, `composerAutoloaderProjectPaths`, `analysedPathsFromConfig`, `usedLevel`, `cliAutoloadFile` + - Editor mode internals: `singleReflectionFile`, `singleReflectionInsteadOfFile` + +Also skip these internal parameters that users should not configure directly: +- `strictRulesInstalled`, `deprecationRulesInstalled` (set by installing packages, not by users) +- `cliArgumentsVariablesRegistered` (internal CLI flag) +- `rootDir`, `currentWorkingDirectory` (auto-detected, not user-configurable) +- `sysGetTempDir` (internal) +- `parametersNotInvalidatingCache` (internal) +- `env` (internal environment variable mapping) + +### Step 3: Determine which parameters are undocumented + +Check which parameter names from the schema do NOT appear as documented parameters in `config-reference.md`. A parameter counts as "documented" if it appears as a heading (`###`), in a config key listing, or is explained in a section body. + +{{#if github.event_name == 'push'}} +Focus on parameters that were added or changed in the push. Run `git diff HEAD~1 -- conf/parametersSchema.neon` to see what changed. Only document newly added parameters. +{{/if}} + +If there are no undocumented parameters, stop and report that all parameters are documented. Do not create a PR. + +### Step 4: Research each undocumented parameter + +For each undocumented parameter, investigate what it does: + +1. **Search the source code** in `src/` for where the parameter is used. Look for the parameter name in PHP files — it will typically appear in a service constructor or be read from the DI container. +2. **Check level configs** in `conf/config.level*.neon` to see which level enables the parameter and what its default value is. +3. **Check `conf/config.neon`** for the parameter's default value. +4. **Look at related rules and tests** to understand the behavior. Check `tests/` for test data files that exercise the parameter. +5. **Check if phpstan-strict-rules sets it** by searching for the parameter name in the codebase and noting if strict-rules is mentioned. + +### Step 5: Write documentation + +Write the file `website/src/config-reference.md` to the workspace with the complete updated content. The file path must exactly match the target repo's structure. + +First, write the original content fetched from phpstan/phpstan to `website/src/config-reference.md` in the workspace. Then edit it to add the new documentation. + +**Place each parameter in the correct existing section:** +- Boolean flags that enable stricter checks → "Stricter analysis" section (as `###` sub-headings) +- Parameters related to parallel processing → "Parallel processing" section +- Parameters related to caching → "Caching" section +- Other general settings → "Miscellaneous parameters" section +- Parameters related to exceptions → "Exceptions" section + +**Follow the existing documentation conventions exactly:** + +For parameters in "Stricter analysis", use this format: + +``` +### `parameterName` + +**default**: `value` ([strict-rules](https://github.com/phpstan/phpstan-strict-rules) sets it to `otherValue`) + +When set to `true/false`, it [concise description of what changes]. +``` + +Include a short PHP code example only if it helps illustrate the behavior clearly. Keep descriptions concise — one or two sentences is ideal. + +If the parameter was introduced in a specific PHPStan version (not 1.0), add a version badge: + +```html +
Available in PHPStan X.Y
+``` + +For parameters in "Miscellaneous parameters", use: + +``` +### `parameterName` + +**default**: `value` + +Description of what the parameter does. +``` + +### Step 6: Create a pull request + +After editing the documentation file, create a pull request. The PR description should list which parameters were newly documented with a one-line summary of each. From 547f1281d85464bc01e69620bfd7414854abdf3a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 10:46:15 +0100 Subject: [PATCH 03/70] Add gh-aw infrastructure files Co-Authored-By: Claude Opus 4.6 --- .github/agents/agentic-workflows.agent.md | 167 ++++++++++++++++++++++ .github/aw/actions-lock.json | 34 +++++ 2 files changed, 201 insertions(+) create mode 100644 .github/agents/agentic-workflows.agent.md create mode 100644 .github/aw/actions-lock.json diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md new file mode 100644 index 00000000000..dea035c3519 --- /dev/null +++ b/.github/agents/agentic-workflows.agent.md @@ -0,0 +1,167 @@ +--- +description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing +infer: false +--- + +# GitHub Agentic Workflows Agent + +This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files. + +## What This Agent Does + +This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task: + +- **Creating new workflows**: Routes to `create` prompt +- **Updating existing workflows**: Routes to `update` prompt +- **Debugging workflows**: Routes to `debug` prompt +- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt +- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt + +Workflows may optionally include: + +- **Project tracking / monitoring** (GitHub Projects updates, status reporting) +- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows) + +## Files This Applies To + +- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` +- Workflow lock files: `.github/workflows/*.lock.yml` +- Shared components: `.github/workflows/shared/*.md` +- Configuration: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/github-agentic-workflows.md + +## Problems This Solves + +- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions +- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues +- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes +- **Component Design**: Create reusable shared workflow components that wrap MCP servers + +## How to Use + +When you interact with this agent, it will: + +1. **Understand your intent** - Determine what kind of task you're trying to accomplish +2. **Route to the right prompt** - Load the specialized prompt file for your task +3. **Execute the task** - Follow the detailed instructions in the loaded prompt + +## Available Prompts + +### Create New Workflow +**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/create-agentic-workflow.md + +**Use cases**: +- "Create a workflow that triages issues" +- "I need a workflow to label pull requests" +- "Design a weekly research automation" + +### Update Existing Workflow +**Load when**: User wants to modify, improve, or refactor an existing workflow + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/update-agentic-workflow.md + +**Use cases**: +- "Add web-fetch tool to the issue-classifier workflow" +- "Update the PR reviewer to use discussions instead of issues" +- "Improve the prompt for the weekly-research workflow" + +### Debug Workflow +**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/debug-agentic-workflow.md + +**Use cases**: +- "Why is this workflow failing?" +- "Analyze the logs for workflow X" +- "Investigate missing tool calls in run #12345" + +### Upgrade Agentic Workflows +**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/upgrade-agentic-workflows.md + +**Use cases**: +- "Upgrade all workflows to the latest version" +- "Fix deprecated fields in workflows" +- "Apply breaking changes from the new release" + +### Create Shared Agentic Workflow +**Load when**: User wants to create a reusable workflow component or wrap an MCP server + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/create-shared-agentic-workflow.md + +**Use cases**: +- "Create a shared component for Notion integration" +- "Wrap the Slack MCP server as a reusable component" +- "Design a shared workflow for database queries" + +### Orchestration and Delegation + +**Load when**: Creating or updating workflows that coordinate multiple agents or dispatch work to other workflows + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/orchestration.md + +**Use cases**: +- Assigning work to AI coding agents +- Dispatching specialized worker workflows +- Using correlation IDs for tracking +- Orchestration design patterns + +### GitHub Projects Integration + +**Load when**: Creating or updating workflows that manage GitHub Projects v2 + +**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/projects.md + +**Use cases**: +- Tracking items and fields with update-project +- Posting periodic run summaries +- Creating new projects +- Projects v2 authentication and configuration + +## Instructions + +When a user interacts with you: + +1. **Identify the task type** from the user's request +2. **Load the appropriate prompt** from the GitHub repository URLs listed above +3. **Follow the loaded prompt's instructions** exactly +4. **If uncertain**, ask clarifying questions to determine the right prompt + +## Quick Reference + +```bash +# Initialize repository for agentic workflows +gh aw init + +# Generate the lock file for a workflow +gh aw compile [workflow-name] + +# Debug workflow runs +gh aw logs [workflow-name] +gh aw audit + +# Upgrade workflows +gh aw fix --write +gh aw compile --validate +``` + +## Key Features of gh-aw + +- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter +- **AI Engine Support**: Copilot, Claude, Codex, or custom engines +- **MCP Server Integration**: Connect to Model Context Protocol servers for tools +- **Safe Outputs**: Structured communication between AI and GitHub API +- **Strict Mode**: Security-first validation and sandboxing +- **Shared Components**: Reusable workflow building blocks +- **Repo Memory**: Persistent git-backed storage for agents +- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default + +## Important Notes + +- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/github-agentic-workflows.md for complete documentation +- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud +- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions +- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF +- Follow security best practices: minimal permissions, explicit network access, no template injection diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json new file mode 100644 index 00000000000..c0fa40a836b --- /dev/null +++ b/.github/aw/actions-lock.json @@ -0,0 +1,34 @@ +{ + "entries": { + "actions/checkout@v6.0.2": { + "repo": "actions/checkout", + "version": "v6.0.2", + "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" + }, + "actions/download-artifact@v6.0.0": { + "repo": "actions/download-artifact", + "version": "v6.0.0", + "sha": "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" + }, + "actions/github-script@v8": { + "repo": "actions/github-script", + "version": "v8", + "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + }, + "actions/setup-node@v6.2.0": { + "repo": "actions/setup-node", + "version": "v6.2.0", + "sha": "6044e13b5dc448c55e2357c09f80417699197238" + }, + "actions/upload-artifact@v6.0.0": { + "repo": "actions/upload-artifact", + "version": "v6.0.0", + "sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f" + }, + "github/gh-aw/actions/setup@v0.43.23": { + "repo": "github/gh-aw/actions/setup", + "version": "v0.43.23", + "sha": "9382be3ca9ac18917e111a99d4e6bbff58d0dccc" + } + } +} From 080bafad9c050f0aadca5143df68f55974ba8167 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 10:57:17 +0100 Subject: [PATCH 04/70] Fix manual dispatch to check all parameters, not just diff Use github.event.before for push diffs to handle multi-commit pushes. Add explicit else branch for manual dispatch to check entire schema. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/document-config-params.lock.yml | 4 ++++ .github/workflows/document-config-params.md | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/document-config-params.lock.yml b/.github/workflows/document-config-params.lock.yml index bf8a0262011..52c49749d47 100644 --- a/.github/workflows/document-config-params.lock.yml +++ b/.github/workflows/document-config-params.lock.yml @@ -480,6 +480,7 @@ jobs: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} @@ -564,6 +565,7 @@ jobs: env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} @@ -580,6 +582,7 @@ jobs: file: process.env.GH_AW_PROMPT, substitutions: { GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_BEFORE: process.env.GH_AW_GITHUB_EVENT_BEFORE, GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, @@ -593,6 +596,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} with: script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md index a232aed9b53..2315a09f579 100644 --- a/.github/workflows/document-config-params.md +++ b/.github/workflows/document-config-params.md @@ -69,7 +69,9 @@ Also skip these internal parameters that users should not configure directly: Check which parameter names from the schema do NOT appear as documented parameters in `config-reference.md`. A parameter counts as "documented" if it appears as a heading (`###`), in a config key listing, or is explained in a section body. {{#if github.event_name == 'push'}} -Focus on parameters that were added or changed in the push. Run `git diff HEAD~1 -- conf/parametersSchema.neon` to see what changed. Only document newly added parameters. +Focus only on parameters that were added or changed in this push. Run `git diff ${{ github.event.before }} -- conf/parametersSchema.neon` to see what changed across all commits in the push. Only document newly added parameters. +{{#else}} +Check ALL non-skipped parameters from the schema against the documentation. Do not look at git history or diffs — compare the entire `parametersSchema.neon` against `config-reference.md` and document every undocumented parameter you find. {{/if}} If there are no undocumented parameters, stop and report that all parameters are documented. Do not create a PR. From 2a5eaba0eb9a004433742d4f613ef7b8941ab6c3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 11:16:38 +0100 Subject: [PATCH 05/70] Skip level-only parameters in config docs workflow These parameters exist purely to be toggled by rule levels and are not configured by users directly. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/document-config-params.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md index 2315a09f579..8aed94f6071 100644 --- a/.github/workflows/document-config-params.md +++ b/.github/workflows/document-config-params.md @@ -64,6 +64,26 @@ Also skip these internal parameters that users should not configure directly: - `parametersNotInvalidatingCache` (internal) - `env` (internal environment variable mapping) +Also skip these level-only parameters — they exist purely to be toggled by rule levels in `conf/config.level*.neon` and are not configured by users directly: +- `checkThisOnly` (level 2) +- `checkMaybeUndefinedVariables` (level 1) +- `checkExtraArguments` (level 1) +- `reportMagicMethods` (level 1) +- `reportMagicProperties` (level 1) +- `checkClassCaseSensitivity` (level 2) +- `checkPhpDocMissingReturn` (level 2) +- `checkPhpDocMethodSignatures` (level 3) +- `checkAdvancedIsset` (level 4) +- `checkFunctionArgumentTypes` (level 5) +- `checkArgumentsPassedByReference` (level 5) +- `checkMissingVarTagTypehint` (level 6) +- `checkMissingTypehints` (level 6) +- `checkUnionTypes` (level 7) +- `reportMaybes` (level 7) +- `checkNullables` (level 8) +- `checkExplicitMixed` (level 9) +- `checkImplicitMixed` (level 10) + ### Step 3: Determine which parameters are undocumented Check which parameter names from the schema do NOT appear as documented parameters in `config-reference.md`. A parameter counts as "documented" if it appears as a heading (`###`), in a config key listing, or is explained in a section body. From 3bb4431d691fd74aa23a4b91d201aab5ad20658c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 11:43:33 +0100 Subject: [PATCH 06/70] Fix cross-repo patch by pre-fetching config-reference.md The safe-output patch was trying to create the file as new, but it already exists in phpstan/phpstan. Add a pre-step that fetches and commits the file so the agent's edits produce a modification patch. Co-Authored-By: Claude Opus 4.6 --- .../workflows/document-config-params.lock.yml | 7 ++++++- .github/workflows/document-config-params.md | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/document-config-params.lock.yml b/.github/workflows/document-config-params.lock.yml index 52c49749d47..14d3a14fcfc 100644 --- a/.github/workflows/document-config-params.lock.yml +++ b/.github/workflows/document-config-params.lock.yml @@ -23,7 +23,7 @@ # # Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan # -# frontmatter-hash: 69e695fc26ed030a830663b4989c2181e28836aebc20d9269f866543d9f36bab +# frontmatter-hash: 1a9f53816b136dca6a59bce24fbe857946c572cd0ca2df9f462956e9caf1a30d name: "Document Config Parameters" "on": @@ -102,6 +102,11 @@ jobs: persist-credentials: false - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + name: Fetch config-reference.md from phpstan/phpstan + run: "mkdir -p website/src\ngh api repos/phpstan/phpstan/contents/website/src/config-reference.md -H \"Accept: application/vnd.github.raw+json\" > website/src/config-reference.md\ngit add website/src/config-reference.md\ngit commit -m \"Seed config-reference.md from phpstan/phpstan\"" + - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md index 8aed94f6071..5cec3711e63 100644 --- a/.github/workflows/document-config-params.md +++ b/.github/workflows/document-config-params.md @@ -27,6 +27,15 @@ safe-outputs: draft: true fallback-as-issue: true timeout-minutes: 30 +steps: + - name: Fetch config-reference.md from phpstan/phpstan + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: | + mkdir -p website/src + gh api repos/phpstan/phpstan/contents/website/src/config-reference.md -H "Accept: application/vnd.github.raw+json" > website/src/config-reference.md + git add website/src/config-reference.md + git commit -m "Seed config-reference.md from phpstan/phpstan" --- # Document Undocumented Config Parameters @@ -36,14 +45,14 @@ You are a documentation agent for PHPStan. Your job is to find configuration par ## Source files - **Parameter schema**: `conf/parametersSchema.neon` in this workspace (phpstan-src repo) -- **Config reference docs**: `website/src/config-reference.md` in the `phpstan/phpstan` repository — fetch this file using the GitHub `get_file_contents` tool from the `phpstan/phpstan` repo +- **Config reference docs**: `website/src/config-reference.md` — already fetched from `phpstan/phpstan` into the workspace by a pre-step ## Task ### Step 1: Read both files 1. Read `conf/parametersSchema.neon` from the workspace -2. Fetch `website/src/config-reference.md` from the `phpstan/phpstan` repo using the GitHub tools +2. Read `website/src/config-reference.md` from the workspace (it was pre-fetched from the `phpstan/phpstan` repo) ### Step 2: Identify user-facing parameters from the schema @@ -108,9 +117,7 @@ For each undocumented parameter, investigate what it does: ### Step 5: Write documentation -Write the file `website/src/config-reference.md` to the workspace with the complete updated content. The file path must exactly match the target repo's structure. - -First, write the original content fetched from phpstan/phpstan to `website/src/config-reference.md` in the workspace. Then edit it to add the new documentation. +Edit the existing `website/src/config-reference.md` file in the workspace to add the new documentation. Do NOT overwrite the file — use targeted edits to insert new parameter sections in the correct locations. **Place each parameter in the correct existing section:** - Boolean flags that enable stricter checks → "Stricter analysis" section (as `###` sub-headings) From afa59666adb209fbf1184bbcf94d925473fb1d0a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 11:59:52 +0100 Subject: [PATCH 07/70] Fix git identity for pre-step commit on CI runner Co-Authored-By: Claude Opus 4.6 --- .github/workflows/document-config-params.lock.yml | 4 ++-- .github/workflows/document-config-params.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/document-config-params.lock.yml b/.github/workflows/document-config-params.lock.yml index 14d3a14fcfc..e89f06232fc 100644 --- a/.github/workflows/document-config-params.lock.yml +++ b/.github/workflows/document-config-params.lock.yml @@ -23,7 +23,7 @@ # # Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan # -# frontmatter-hash: 1a9f53816b136dca6a59bce24fbe857946c572cd0ca2df9f462956e9caf1a30d +# frontmatter-hash: d760e657da671c4981c44c5653b1d742bd302e5bb6d2d59c20b32d62d08ad4df name: "Document Config Parameters" "on": @@ -105,7 +105,7 @@ jobs: - env: GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} name: Fetch config-reference.md from phpstan/phpstan - run: "mkdir -p website/src\ngh api repos/phpstan/phpstan/contents/website/src/config-reference.md -H \"Accept: application/vnd.github.raw+json\" > website/src/config-reference.md\ngit add website/src/config-reference.md\ngit commit -m \"Seed config-reference.md from phpstan/phpstan\"" + run: "mkdir -p website/src\ngh api repos/phpstan/phpstan/contents/website/src/config-reference.md -H \"Accept: application/vnd.github.raw+json\" > website/src/config-reference.md\ngit config user.name \"github-actions[bot]\"\ngit config user.email \"github-actions[bot]@users.noreply.github.com\"\ngit add website/src/config-reference.md\ngit commit -m \"Seed config-reference.md from phpstan/phpstan\"" - name: Configure Git credentials env: diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md index 5cec3711e63..69812f25ad9 100644 --- a/.github/workflows/document-config-params.md +++ b/.github/workflows/document-config-params.md @@ -34,6 +34,8 @@ steps: run: | mkdir -p website/src gh api repos/phpstan/phpstan/contents/website/src/config-reference.md -H "Accept: application/vnd.github.raw+json" > website/src/config-reference.md + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" git add website/src/config-reference.md git commit -m "Seed config-reference.md from phpstan/phpstan" --- From 97f58a0b7bfa9805ee6299bd9a20f55afd5f4f22 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 12:35:22 +0100 Subject: [PATCH 08/70] Use direct checkout and bash PR creation instead of safe-outputs Instead of using safe-outputs create-pull-request (which generates patches that fail on cross-repo applies), checkout phpstan/phpstan to __phpstan-website/ subdirectory, edit config-reference.md in place, and push branch + create PR via bash/gh CLI. Co-Authored-By: Claude Opus 4.6 --- .github/aw/actions-lock.json | 5 + .../workflows/document-config-params.lock.yml | 640 +----------------- .github/workflows/document-config-params.md | 47 +- 3 files changed, 41 insertions(+), 651 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index c0fa40a836b..fd519aa91be 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,5 +1,10 @@ { "entries": { + "actions/checkout@v4": { + "repo": "actions/checkout", + "version": "v4", + "sha": "34e114876b0b11c390a56381ad16ebd13914f8d5" + }, "actions/checkout@v6.0.2": { "repo": "actions/checkout", "version": "v6.0.2", diff --git a/.github/workflows/document-config-params.lock.yml b/.github/workflows/document-config-params.lock.yml index e89f06232fc..f6959636751 100644 --- a/.github/workflows/document-config-params.lock.yml +++ b/.github/workflows/document-config-params.lock.yml @@ -23,7 +23,7 @@ # # Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan # -# frontmatter-hash: d760e657da671c4981c44c5653b1d742bd302e5bb6d2d59c20b32d62d08ad4df +# frontmatter-hash: 8fc29ae470188f0f52dc79af2c2bba18a872a57ddfa4479eb52b07a7b642c978 name: "Document Config Parameters" "on": @@ -75,37 +75,24 @@ jobs: issues: read pull-requests: read env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - GH_AW_ASSETS_ALLOWED_EXTS: "" - GH_AW_ASSETS_BRANCH: "" - GH_AW_ASSETS_MAX_SIZE_KB: 0 - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json GH_AW_WORKFLOW_ID_SANITIZED: documentconfigparams outputs: checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - has_patch: ${{ steps.collect_output.outputs.has_patch }} model: ${{ steps.generate_aw_info.outputs.model }} - output: ${{ steps.collect_output.outputs.output }} - output_types: ${{ steps.collect_output.outputs.output_types }} secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 with: destination: /opt/gh-aw/actions - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - - env: - GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: Fetch config-reference.md from phpstan/phpstan - run: "mkdir -p website/src\ngh api repos/phpstan/phpstan/contents/website/src/config-reference.md -H \"Accept: application/vnd.github.raw+json\" > website/src/config-reference.md\ngit config user.name \"github-actions[bot]\"\ngit config user.email \"github-actions[bot]@users.noreply.github.com\"\ngit add website/src/config-reference.md\ngit commit -m \"Seed config-reference.md from phpstan/phpstan\"" + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: __phpstan-website + ref: 2.2.x + repository: phpstan/phpstan + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: Configure Git credentials env: @@ -124,9 +111,9 @@ jobs: github.event.pull_request uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: - GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); @@ -203,230 +190,10 @@ jobs: const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.17.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.17.0 ghcr.io/github/gh-aw-firewall/squid:0.17.0 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 node:lts-alpine - - name: Write Safe Outputs Config - run: | - mkdir -p /opt/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/safeoutputs - mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF' - {"create_pull_request":{},"missing_data":{},"missing_tool":{},"noop":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_EOF - cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF' - [ - { - "description": "Create a new GitHub pull request to propose code changes. Use this after making file edits to submit them for review and merging. The PR will be created from the current branch with your committed changes. For code review comments on an existing PR, use create_pull_request_review_comment instead. CONSTRAINTS: Maximum 1 pull request(s) can be created. Title will be prefixed with \"[Docs] \". PRs will be created as drafts.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "body": { - "description": "Detailed PR description in Markdown. Include what changes were made, why, testing notes, and any breaking changes. Do NOT repeat the title as a heading.", - "type": "string" - }, - "branch": { - "description": "Source branch name containing the changes. If omitted, uses the current working branch.", - "type": "string" - }, - "labels": { - "description": "Labels to categorize the PR (e.g., 'enhancement', 'bugfix'). Labels must exist in the repository.", - "items": { - "type": "string" - }, - "type": "array" - }, - "title": { - "description": "Concise PR title describing the changes. Follow repository conventions (e.g., conventional commits). The title appears as the main heading.", - "type": "string" - } - }, - "required": [ - "title", - "body" - ], - "type": "object" - }, - "name": "create_pull_request" - }, - { - "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "reason": { - "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).", - "type": "string" - }, - "tool": { - "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.", - "type": "string" - } - }, - "required": [ - "reason" - ], - "type": "object" - }, - "name": "missing_tool" - }, - { - "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "message": { - "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').", - "type": "string" - } - }, - "required": [ - "message" - ], - "type": "object" - }, - "name": "noop" - }, - { - "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.", - "inputSchema": { - "additionalProperties": false, - "properties": { - "alternatives": { - "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).", - "type": "string" - }, - "context": { - "description": "Additional context about the missing data or where it should come from (max 256 characters).", - "type": "string" - }, - "data_type": { - "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.", - "type": "string" - }, - "reason": { - "description": "Explanation of why this data is needed to complete the task (max 256 characters).", - "type": "string" - } - }, - "required": [], - "type": "object" - }, - "name": "missing_data" - } - ] - GH_AW_SAFE_OUTPUTS_TOOLS_EOF - cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF' - { - "create_pull_request": { - "defaultMax": 1, - "fields": { - "body": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - }, - "branch": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "labels": { - "type": "array", - "itemType": "string", - "itemSanitize": true, - "itemMaxLength": 128 - }, - "title": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "missing_tool": { - "defaultMax": 20, - "fields": { - "alternatives": { - "type": "string", - "sanitize": true, - "maxLength": 512 - }, - "reason": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 256 - }, - "tool": { - "type": "string", - "sanitize": true, - "maxLength": 128 - } - } - }, - "noop": { - "defaultMax": 1, - "fields": { - "message": { - "required": true, - "type": "string", - "sanitize": true, - "maxLength": 65000 - } - } - } - } - GH_AW_SAFE_OUTPUTS_VALIDATION_EOF - - name: Generate Safe Outputs MCP Server Config - id: safe-outputs-config - run: | - # Generate a secure random API key (360 bits of entropy, 40+ chars) - # Mask immediately to prevent timing vulnerabilities - API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${API_KEY}" - - PORT=3001 - - # Set outputs for next steps - { - echo "safe_outputs_api_key=${API_KEY}" - echo "safe_outputs_port=${PORT}" - } >> "$GITHUB_OUTPUT" - - echo "Safe Outputs MCP server will run on port ${PORT}" - - - name: Start Safe Outputs MCP HTTP Server - id: safe-outputs-start - env: - DEBUG: '*' - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }} - GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json - GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json - GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs - run: | - # Environment variables are set above to prevent template injection - export DEBUG - export GH_AW_SAFE_OUTPUTS_PORT - export GH_AW_SAFE_OUTPUTS_API_KEY - export GH_AW_SAFE_OUTPUTS_TOOLS_PATH - export GH_AW_SAFE_OUTPUTS_CONFIG_PATH - export GH_AW_MCP_LOG_DIR - - bash /opt/gh-aw/actions/start_safe_outputs_server.sh - + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.17.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.17.0 ghcr.io/github/gh-aw-firewall/squid:0.17.0 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 - name: Start MCP gateway id: start-mcp-gateway env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }} - GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }} GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: | @@ -444,7 +211,7 @@ jobs: export DEBUG="*" export GH_AW_ENGINE="claude" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh { @@ -457,13 +224,6 @@ jobs: "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" } - }, - "safeoutputs": { - "type": "http", - "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", - "headers": { - "Authorization": "$GH_AW_SAFE_OUTPUTS_API_KEY" - } } }, "gateway": { @@ -483,7 +243,6 @@ jobs: - name: Create prompt with built-in context env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} @@ -502,34 +261,6 @@ jobs: cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - GitHub API Access Instructions - - The gh CLI is NOT authenticated. Do NOT use gh commands for GitHub operations. - - - To create or modify GitHub resources (issues, discussions, pull requests, etc.), you MUST call the appropriate safe output tool. Simply writing content will NOT work - the workflow requires actual tool calls. - - Temporary IDs: Some safe output tools support a temporary ID field (usually named temporary_id) so you can reference newly-created items elsewhere in the SAME agent output (for example, using #aw_abc1 in a later body). - - **IMPORTANT - temporary_id format rules:** - - If you DON'T need to reference the item later, OMIT the temporary_id field entirely (it will be auto-generated if needed) - - If you DO need cross-references/chaining, you MUST match this EXACT validation regex: /^aw_[A-Za-z0-9]{3,8}$/i - - Format: aw_ prefix followed by 3 to 8 alphanumeric characters (A-Z, a-z, 0-9, case-insensitive) - - Valid alphanumeric characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 - - INVALID examples: aw_ab (too short), aw_123456789 (too long), aw_test-id (contains hyphen), aw_id_123 (contains underscore) - - VALID examples: aw_abc, aw_abc1, aw_Test123, aw_A1B2C3D4, aw_12345678 - - To generate valid IDs: use 3-8 random alphanumeric characters or omit the field to let the system auto-generate - - Do NOT invent other aw_* formats — downstream steps will reject them with validation errors matching against /^aw_[A-Za-z0-9]{3,8}$/i. - - Discover available tools from the safeoutputs MCP server. - - **Critical**: Tool calls write structured data that downstream jobs process. Without tool calls, follow-up actions will be skipped. - - **Note**: If you made no other safe output tool calls during this workflow execution, call the "noop" tool to provide a status message indicating completion or that no actions were needed. - - The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -703,7 +434,6 @@ jobs: DISABLE_TELEMETRY: 1 GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} GITHUB_WORKSPACE: ${{ github.workspace }} MCP_TIMEOUT: 120000 MCP_TOOL_TIMEOUT: 60000 @@ -744,34 +474,6 @@ jobs: SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SECRET_PHPSTAN_BOT_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} - - name: Upload Safe Outputs - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: safe-output - path: ${{ env.GH_AW_SAFE_OUTPUTS }} - if-no-files-found: warn - - name: Ingest agent output - id: collect_output - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com" - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_API_URL: ${{ github.api_url }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs'); - await main(); - - name: Upload sanitized agent output - if: always() && env.GH_AW_AGENT_OUTPUT - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: agent-output - path: ${{ env.GH_AW_AGENT_OUTPUT }} - if-no-files-found: warn - name: Parse agent logs for step summary if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 @@ -815,247 +517,6 @@ jobs: /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/agent/ - /tmp/gh-aw/aw.patch - if-no-files-found: ignore - - conclusion: - needs: - - activation - - agent - - detection - - safe_outputs - if: (always()) && (needs.agent.result != 'skipped') - runs-on: ubuntu-slim - permissions: - contents: write - issues: write - pull-requests: write - outputs: - noop_message: ${{ steps.noop.outputs.noop_message }} - tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} - total_count: ${{ steps.missing_tool.outputs.total_count }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 - with: - destination: /opt/gh-aw/actions - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Process No-Op Messages - id: noop - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_NOOP_MAX: 1 - GH_AW_WORKFLOW_NAME: "Document Config Parameters" - with: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/noop.cjs'); - await main(); - - name: Record Missing Tool - id: missing_tool - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Document Config Parameters" - with: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/missing_tool.cjs'); - await main(); - - name: Handle Agent Failure - id: handle_agent_failure - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Document Config Parameters" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_WORKFLOW_ID: "document-config-params" - GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.agent.outputs.secret_verification_result }} - GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} - with: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs'); - await main(); - - name: Handle No-Op Message - id: handle_noop_message - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Document Config Parameters" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }} - GH_AW_NOOP_REPORT_AS_ISSUE: "true" - with: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs'); - await main(); - - name: Handle Create Pull Request Error - id: handle_create_pr_error - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Document Config Parameters" - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - with: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/handle_create_pr_error.cjs'); - await main(); - - name: Update reaction comment with completion status - id: conclusion - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }} - GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - GH_AW_WORKFLOW_NAME: "Document Config Parameters" - GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} - GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.result }} - with: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/notify_comment_error.cjs'); - await main(); - - detection: - needs: agent - if: needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true' - runs-on: ubuntu-latest - permissions: {} - timeout-minutes: 10 - outputs: - success: ${{ steps.parse_results.outputs.success }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 - with: - destination: /opt/gh-aw/actions - - name: Download agent artifacts - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-artifacts - path: /tmp/gh-aw/threat-detection/ - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/threat-detection/ - - name: Echo agent output types - env: - AGENT_OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} - run: | - echo "Agent output-types: $AGENT_OUTPUT_TYPES" - - name: Setup threat detection - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - WORKFLOW_NAME: "Document Config Parameters" - WORKFLOW_DESCRIPTION: "Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan" - HAS_PATCH: ${{ needs.agent.outputs.has_patch }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs'); - await main(); - - name: Ensure threat-detection directory and log - run: | - mkdir -p /tmp/gh-aw/threat-detection - touch /tmp/gh-aw/threat-detection/detection.log - - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install Claude Code CLI - run: npm install -g --silent @anthropic-ai/claude-code@2.1.39 - - name: Execute Claude Code CLI - id: agentic_execution - # Allowed tools (sorted): - # - Bash(cat) - # - Bash(grep) - # - Bash(head) - # - Bash(jq) - # - Bash(ls) - # - Bash(tail) - # - Bash(wc) - # - BashOutput - # - ExitPlanMode - # - Glob - # - Grep - # - KillBash - # - LS - # - NotebookRead - # - Read - # - Task - # - TodoWrite - timeout-minutes: 20 - run: | - set -o pipefail - # Execute Claude Code CLI with prompt from file - claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --allowed-tools 'Bash(cat),Bash(grep),Bash(head),Bash(jq),Bash(ls),Bash(tail),Bash(wc),BashOutput,ExitPlanMode,Glob,Grep,KillBash,LS,NotebookRead,Read,Task,TodoWrite' --debug-file /tmp/gh-aw/threat-detection/detection.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - BASH_DEFAULT_TIMEOUT_MS: 60000 - BASH_MAX_TIMEOUT_MS: 60000 - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - DISABLE_BUG_COMMAND: 1 - DISABLE_ERROR_REPORTING: 1 - DISABLE_TELEMETRY: 1 - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_WORKSPACE: ${{ github.workspace }} - MCP_TIMEOUT: 120000 - MCP_TOOL_TIMEOUT: 60000 - - name: Parse threat detection results - id: parse_results - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs'); - await main(); - - name: Upload threat detection log - if: always() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: threat-detection.log - path: /tmp/gh-aw/threat-detection/detection.log if-no-files-found: ignore pre_activation: @@ -1080,82 +541,3 @@ jobs: const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); await main(); - safe_outputs: - needs: - - activation - - agent - - detection - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.detection.outputs.success == 'true') - runs-on: ubuntu-slim - permissions: - contents: write - issues: write - pull-requests: write - timeout-minutes: 15 - env: - GH_AW_ENGINE_ID: "claude" - GH_AW_ENGINE_MODEL: "claude-opus-4-6" - GH_AW_WORKFLOW_ID: "document-config-params" - GH_AW_WORKFLOW_NAME: "Document Config Parameters" - outputs: - create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} - create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} - process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} - process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 - with: - destination: /opt/gh-aw/actions - - name: Download agent output artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-output - path: /tmp/gh-aw/safeoutputs/ - - name: Setup agent output environment variable - run: | - mkdir -p /tmp/gh-aw/safeoutputs/ - find "/tmp/gh-aw/safeoutputs/" -type f -print - echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/safeoutputs/agent_output.json" >> "$GITHUB_ENV" - - name: Download patch artifact - continue-on-error: true - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 - with: - name: agent-artifacts - path: /tmp/gh-aw/ - - name: Checkout repository - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - repository: phpstan/phpstan - token: ${{ github.token }} - persist-credentials: false - fetch-depth: 1 - - name: Configure Git credentials - if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (contains(needs.agent.outputs.output_types, 'create_pull_request')) - env: - REPO_NAME: "phpstan/phpstan" - SERVER_URL: ${{ github.server_url }} - GIT_TOKEN: ${{ github.token }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${GIT_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Process Safe Outputs - id: process_safe_outputs - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"base_branch\":\"${{ github.ref_name }}\",\"draft\":true,\"fallback_as_issue\":true,\"max\":1,\"max_patch_size\":1024,\"target-repo\":\"phpstan/phpstan\",\"title_prefix\":\"[Docs] \"},\"missing_data\":{},\"missing_tool\":{}}" - with: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs'); - await main(); - diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md index 69812f25ad9..56ba12ba6f4 100644 --- a/.github/workflows/document-config-params.md +++ b/.github/workflows/document-config-params.md @@ -19,25 +19,14 @@ tools: bash: ["*"] github: toolsets: [default, repos] -safe-outputs: - github-token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - create-pull-request: - target-repo: phpstan/phpstan - title-prefix: "[Docs] " - draft: true - fallback-as-issue: true timeout-minutes: 30 steps: - - name: Fetch config-reference.md from phpstan/phpstan - env: - GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} - run: | - mkdir -p website/src - gh api repos/phpstan/phpstan/contents/website/src/config-reference.md -H "Accept: application/vnd.github.raw+json" > website/src/config-reference.md - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add website/src/config-reference.md - git commit -m "Seed config-reference.md from phpstan/phpstan" + - uses: actions/checkout@v4 + with: + repository: phpstan/phpstan + ref: 2.2.x + path: __phpstan-website + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} --- # Document Undocumented Config Parameters @@ -47,14 +36,15 @@ You are a documentation agent for PHPStan. Your job is to find configuration par ## Source files - **Parameter schema**: `conf/parametersSchema.neon` in this workspace (phpstan-src repo) -- **Config reference docs**: `website/src/config-reference.md` — already fetched from `phpstan/phpstan` into the workspace by a pre-step +- **Config reference docs**: `__phpstan-website/website/src/config-reference.md` (checked out from `phpstan/phpstan`) +- **Source code for research**: `src/`, `conf/`, and `tests/` directories in this workspace (phpstan-src repo) ## Task ### Step 1: Read both files 1. Read `conf/parametersSchema.neon` from the workspace -2. Read `website/src/config-reference.md` from the workspace (it was pre-fetched from the `phpstan/phpstan` repo) +2. Read `__phpstan-website/website/src/config-reference.md` from the workspace ### Step 2: Identify user-facing parameters from the schema @@ -109,7 +99,7 @@ If there are no undocumented parameters, stop and report that all parameters are ### Step 4: Research each undocumented parameter -For each undocumented parameter, investigate what it does: +For each undocumented parameter, investigate what it does by reading files from the workspace (phpstan-src): 1. **Search the source code** in `src/` for where the parameter is used. Look for the parameter name in PHP files — it will typically appear in a service constructor or be read from the DI container. 2. **Check level configs** in `conf/config.level*.neon` to see which level enables the parameter and what its default value is. @@ -119,7 +109,7 @@ For each undocumented parameter, investigate what it does: ### Step 5: Write documentation -Edit the existing `website/src/config-reference.md` file in the workspace to add the new documentation. Do NOT overwrite the file — use targeted edits to insert new parameter sections in the correct locations. +Edit the existing `__phpstan-website/website/src/config-reference.md` file to add the new documentation. Do NOT overwrite the file — use targeted edits to insert new parameter sections in the correct locations. **Place each parameter in the correct existing section:** - Boolean flags that enable stricter checks → "Stricter analysis" section (as `###` sub-headings) @@ -160,4 +150,17 @@ Description of what the parameter does. ### Step 6: Create a pull request -After editing the documentation file, create a pull request. The PR description should list which parameters were newly documented with a one-line summary of each. +After editing the documentation file, push the changes and create a PR on `phpstan/phpstan`: + +```bash +cd __phpstan-website +git config user.name "github-actions[bot]" +git config user.email "github-actions[bot]@users.noreply.github.com" +git checkout -b docs/undocumented-config-params +git add website/src/config-reference.md +git commit -m "Document undocumented configuration parameters" +git push origin docs/undocumented-config-params +gh pr create --repo phpstan/phpstan --base 2.2.x --draft --title "[Docs] Document undocumented config parameters" --body "PR DESCRIPTION HERE" +``` + +Replace `PR DESCRIPTION HERE` with a description listing which parameters were newly documented with a one-line summary of each. From da9ed281ec1dcd99a7ea95ff4a4a9944fd9f33e8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 12:46:46 +0100 Subject: [PATCH 09/70] Add main repo checkout so gh-aw git config step works The gh-aw framework runs git commands in the workspace root expecting a git repository. Without checking out phpstan-src first, the "Configure Git credentials" step fails with "not a git repository". Co-Authored-By: Claude Opus 4.6 --- .github/workflows/document-config-params.lock.yml | 3 ++- .github/workflows/document-config-params.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/document-config-params.lock.yml b/.github/workflows/document-config-params.lock.yml index f6959636751..0a6a7edbf21 100644 --- a/.github/workflows/document-config-params.lock.yml +++ b/.github/workflows/document-config-params.lock.yml @@ -23,7 +23,7 @@ # # Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan # -# frontmatter-hash: 8fc29ae470188f0f52dc79af2c2bba18a872a57ddfa4479eb52b07a7b642c978 +# frontmatter-hash: 31bb738b106c65eb8c5258fe6d0d61365db8ea6b0f389fc75193988252680777 name: "Document Config Parameters" "on": @@ -87,6 +87,7 @@ jobs: destination: /opt/gh-aw/actions - name: Create gh-aw temp directory run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: path: __phpstan-website diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md index 56ba12ba6f4..4d300da6246 100644 --- a/.github/workflows/document-config-params.md +++ b/.github/workflows/document-config-params.md @@ -21,6 +21,7 @@ tools: toolsets: [default, repos] timeout-minutes: 30 steps: + - uses: actions/checkout@v4 - uses: actions/checkout@v4 with: repository: phpstan/phpstan From 5c6e070cc640224d43571bcc60da30f77e6bd805 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 12:49:22 +0100 Subject: [PATCH 10/70] Correct username --- .github/workflows/document-config-params.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md index 4d300da6246..36c626b1371 100644 --- a/.github/workflows/document-config-params.md +++ b/.github/workflows/document-config-params.md @@ -155,8 +155,8 @@ After editing the documentation file, push the changes and create a PR on `phpst ```bash cd __phpstan-website -git config user.name "github-actions[bot]" -git config user.email "github-actions[bot]@users.noreply.github.com" +git config user.name "phpstan-bot" +git config user.email "ondrej+phpstanbot@mirtes.cz" git checkout -b docs/undocumented-config-params git add website/src/config-reference.md git commit -m "Document undocumented configuration parameters" From 5abeb3cceedf31dc51dc49a3fd7851dbf36de957 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 14 Feb 2026 13:43:37 +0100 Subject: [PATCH 11/70] Instruct agent to extract nested parameters from schema The parametersSchema.neon has nested structure() blocks like exceptions.check.* and cache.*. The agent was only looking at top-level parameters and missing nested ones like throwTypeCovariance and tooWideImplicitThrowType. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/document-config-params.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md index 36c626b1371..d1ad6b64f88 100644 --- a/.github/workflows/document-config-params.md +++ b/.github/workflows/document-config-params.md @@ -49,7 +49,23 @@ You are a documentation agent for PHPStan. Your job is to find configuration par ### Step 2: Identify user-facing parameters from the schema -Extract all parameter names from `parametersSchema.neon`. **Skip these entirely:** +Extract all parameter names from `parametersSchema.neon`. Note that some parameters are nested inside `structure()` blocks — these use dotted paths in the user's `phpstan.neon`. For example, the schema has: + +```neon +exceptions: structure([ + implicitThrows: bool(), + check: structure([ + missingCheckedExceptionInThrows: bool(), + tooWideThrowType: bool(), + throwTypeCovariance: bool(), + tooWideImplicitThrowType: bool() + ]) +]) +``` + +This means the user-facing parameters are `exceptions.implicitThrows`, `exceptions.check.missingCheckedExceptionInThrows`, `exceptions.check.tooWideThrowType`, etc. Similarly, `cache` has sub-keys like `cache.nodesByStringCountMax`. Make sure to extract ALL nested parameters, not just top-level ones. + +**Skip these entirely:** - The entire `featureToggles` section and all its sub-parameters - Everything after the `# playground mode` comment — these are internal/irrelevant: From b43a04be4732e2d63e2603ca3dbb1df98f1b8a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Mon, 16 Feb 2026 08:13:17 +0100 Subject: [PATCH 12/70] Fix Claude PR reactions workflow: don't cancel in-progress runs Co-authored-by: Claude --- .github/workflows/claude-pr-reactions.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/claude-pr-reactions.yml b/.github/workflows/claude-pr-reactions.yml index 5e687aefeb8..3550dbb5a87 100644 --- a/.github/workflows/claude-pr-reactions.yml +++ b/.github/workflows/claude-pr-reactions.yml @@ -16,7 +16,7 @@ permissions: concurrency: group: claude-pr-reactions-${{ github.event.pull_request.number || github.event.issue.number }} - cancel-in-progress: true + cancel-in-progress: false jobs: react: @@ -48,3 +48,5 @@ jobs: github_token: ${{ secrets.PHPSTAN_BOT_TOKEN }} trigger_phrase: "@phpstan-bot" claude_args: "--model claude-opus-4-6 --max-turns 50" + additional_permissions: | + actions: read From d145799599842924b57468660e9225b27db3ce70 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 16 Feb 2026 09:29:24 +0100 Subject: [PATCH 13/70] Improve Claude react workflow --- .../{claude-pr-reactions.yml => claude-react-on-comment.yml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/workflows/{claude-pr-reactions.yml => claude-react-on-comment.yml} (95%) diff --git a/.github/workflows/claude-pr-reactions.yml b/.github/workflows/claude-react-on-comment.yml similarity index 95% rename from .github/workflows/claude-pr-reactions.yml rename to .github/workflows/claude-react-on-comment.yml index f0b86326377..d25beb45486 100644 --- a/.github/workflows/claude-pr-reactions.yml +++ b/.github/workflows/claude-react-on-comment.yml @@ -1,4 +1,4 @@ -name: "Claude PR Reactions" +name: "Claude React on comment" on: issue_comment: @@ -20,7 +20,7 @@ concurrency: jobs: react: - name: "React to PR feedback" + name: "React on comment" runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 60 From 7c6309cc113339081e3694f7c5a37e99ec0e8781 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 16 Feb 2026 09:35:17 +0100 Subject: [PATCH 14/70] Configure phpstan-bot git identity for Claude PR reactions workflow Co-Authored-By: Claude Opus 4.6 --- .github/workflows/claude-react-on-comment.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/claude-react-on-comment.yml b/.github/workflows/claude-react-on-comment.yml index d25beb45486..bf5a66674cc 100644 --- a/.github/workflows/claude-react-on-comment.yml +++ b/.github/workflows/claude-react-on-comment.yml @@ -41,6 +41,11 @@ jobs: - uses: "ramsey/composer-install@v3" + - name: "Configure git identity" + run: | + git config user.name "phpstan-bot" + git config user.email "ondrej+phpstanbot@mirtes.cz" + - name: "React to feedback" uses: anthropics/claude-code-action@v1 with: From c476b6006b1bc171be2ec396d1c659a04b208408 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 09:06:18 +0000 Subject: [PATCH 15/70] Split claude react-on-comment into two jobs to avoid expensive setup Add a lightweight check-trigger job on ubuntu-latest that checks if the comment contains the @phpstan-bot trigger phrase. The expensive react job (setup-php, composer install on blacksmith runner) only runs when the trigger phrase is actually present. https://claude.ai/code/session_01T86RqyQoZyKWyG4MUnf7FC --- .github/workflows/claude-react-on-comment.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/claude-react-on-comment.yml b/.github/workflows/claude-react-on-comment.yml index bf5a66674cc..4de52b149e9 100644 --- a/.github/workflows/claude-react-on-comment.yml +++ b/.github/workflows/claude-react-on-comment.yml @@ -19,8 +19,28 @@ concurrency: cancel-in-progress: false jobs: + check-trigger: + name: "Check trigger phrase" + runs-on: ubuntu-latest + timeout-minutes: 1 + outputs: + triggered: ${{ steps.check.outputs.triggered }} + steps: + - name: "Check for trigger phrase" + id: check + env: + COMMENT_BODY: ${{ github.event.comment.body || github.event.review.body || '' }} + run: | + if echo "$COMMENT_BODY" | grep -qF "@phpstan-bot"; then + echo "triggered=true" >> "$GITHUB_OUTPUT" + else + echo "triggered=false" >> "$GITHUB_OUTPUT" + fi + react: name: "React on comment" + needs: check-trigger + if: needs.check-trigger.outputs.triggered == 'true' runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 60 From 394461fd540c6542574116fc8e0734af71c001af Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 16 Feb 2026 10:55:01 +0100 Subject: [PATCH 16/70] Fix Claude credentials to phpstan-bot --- .github/workflows/claude-react-on-comment.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/claude-react-on-comment.yml b/.github/workflows/claude-react-on-comment.yml index 4de52b149e9..09be31cd16f 100644 --- a/.github/workflows/claude-react-on-comment.yml +++ b/.github/workflows/claude-react-on-comment.yml @@ -61,11 +61,6 @@ jobs: - uses: "ramsey/composer-install@v3" - - name: "Configure git identity" - run: | - git config user.name "phpstan-bot" - git config user.email "ondrej+phpstanbot@mirtes.cz" - - name: "React to feedback" uses: anthropics/claude-code-action@v1 with: @@ -73,5 +68,7 @@ jobs: github_token: ${{ secrets.PHPSTAN_BOT_TOKEN }} trigger_phrase: "@phpstan-bot" claude_args: "--model claude-opus-4-6" + bot_name: "phpstan-bot" + bot_id: "79867460" additional_permissions: | actions: read From 72f877f55fd94907da984d4cd3605abbfc7b2b0d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 16 Feb 2026 13:07:46 +0100 Subject: [PATCH 17/70] Add scheduled workflow to trigger Claude easy fixes nightly Runs claude-random-easy-fixes.yml with issue_count=5 every hour at :15 from 10pm to 7am CEST (20:15-05:15 UTC). Co-Authored-By: Claude Opus 4.6 --- .../claude-random-easy-fixes-scheduled.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/claude-random-easy-fixes-scheduled.yml diff --git a/.github/workflows/claude-random-easy-fixes-scheduled.yml b/.github/workflows/claude-random-easy-fixes-scheduled.yml new file mode 100644 index 00000000000..22650fb78d8 --- /dev/null +++ b/.github/workflows/claude-random-easy-fixes-scheduled.yml @@ -0,0 +1,15 @@ +name: "Claude Random Easy Fixes (Scheduled)" + +on: + schedule: + # Run 10 times, once an hour at :15, from 10pm CEST (20:00 UTC) to 7am CEST (05:00 UTC) + - cron: '15 20-23,0-5 * * *' + +jobs: + trigger: + runs-on: ubuntu-latest + steps: + - name: Trigger Claude Random Easy Fixes + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: gh workflow run claude-random-easy-fixes.yml -f issue_count=5 --repo ${{ github.repository }} From b4735ef309a1b29d80596e30f8443cc1fa1125ef Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 21 Feb 2026 06:20:39 +0000 Subject: [PATCH 18/70] Update aw --- .github/aw/actions-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index fd519aa91be..3fb57cfc84b 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -1,8 +1,8 @@ { "entries": { - "actions/checkout@v4": { + "actions/checkout@v4.3.1": { "repo": "actions/checkout", - "version": "v4", + "version": "v4.3.1", "sha": "34e114876b0b11c390a56381ad16ebd13914f8d5" }, "actions/checkout@v6.0.2": { @@ -30,10 +30,10 @@ "version": "v6.0.0", "sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f" }, - "github/gh-aw/actions/setup@v0.43.23": { + "github/gh-aw/actions/setup@v0.48.1": { "repo": "github/gh-aw/actions/setup", - "version": "v0.43.23", - "sha": "9382be3ca9ac18917e111a99d4e6bbff58d0dccc" + "version": "v0.48.1", + "sha": "26b6572ae210580303087bc3142fe58d140bf65c" } } } From b15ff63d28b6e5803de83b21ba2bda54ad88061d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 21 Feb 2026 06:23:43 +0000 Subject: [PATCH 19/70] Add document-phpdoc-tags workflow Adds a gh-aw workflow that compares PhpDocNodeResolver with phpdocs-basics.md to find undocumented PHPDoc tags and creates PRs on phpstan/phpstan. Co-Authored-By: Claude Opus 4.6 --- .github/aw/actions-lock.json | 5 + .../workflows/document-config-params.lock.yml | 281 +++++---- .../workflows/document-phpdoc-tags.lock.yml | 575 ++++++++++++++++++ .github/workflows/document-phpdoc-tags.md | 125 ++++ 4 files changed, 861 insertions(+), 125 deletions(-) create mode 100644 .github/workflows/document-phpdoc-tags.lock.yml create mode 100644 .github/workflows/document-phpdoc-tags.md diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 3fb57cfc84b..5e9220a26b1 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -30,6 +30,11 @@ "version": "v6.0.0", "sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f" }, + "github/gh-aw/actions/setup@v0.45.4": { + "repo": "github/gh-aw/actions/setup", + "version": "v0.45.4", + "sha": "ac090214a48a1938f7abafe132460b66752261af" + }, "github/gh-aw/actions/setup@v0.48.1": { "repo": "github/gh-aw/actions/setup", "version": "v0.48.1", diff --git a/.github/workflows/document-config-params.lock.yml b/.github/workflows/document-config-params.lock.yml index 0a6a7edbf21..b5be8ab9735 100644 --- a/.github/workflows/document-config-params.lock.yml +++ b/.github/workflows/document-config-params.lock.yml @@ -13,7 +13,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by gh-aw (v0.43.23). DO NOT EDIT. +# This file was automatically generated by gh-aw (v0.45.4). DO NOT EDIT. # # To update this file, edit the corresponding .md file and run: # gh aw compile @@ -53,9 +53,17 @@ jobs: comment_repo: "" steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 with: destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + .agents + fetch-depth: 1 + persist-credentials: false - name: Check workflow file timestamps uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: @@ -66,6 +74,127 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/document-config-params.md}} + GH_AW_PROMPT_EOF + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_BEFORE: process.env.GH_AW_GITHUB_EVENT_BEFORE, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Upload prompt artifact + if: success() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: prompt + path: /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 agent: needs: activation @@ -82,7 +211,7 @@ jobs: secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 with: destination: /opt/gh-aw/actions - name: Create gh-aw temp directory @@ -132,12 +261,11 @@ jobs: engine_name: "Claude Code", model: "claude-opus-4-6", version: "", - agent_version: "2.1.39", - cli_version: "v0.43.23", + agent_version: "2.1.42", + cli_version: "v0.45.4", workflow_name: "Document Config Parameters", experimental: false, supports_tools_allowlist: true, - supports_http_transport: true, run_id: context.runId, run_number: context.runNumber, run_attempt: process.env.GITHUB_RUN_ATTEMPT, @@ -149,8 +277,8 @@ jobs: staged: false, allowed_domains: ["defaults"], firewall_enabled: true, - awf_version: "v0.17.0", - awmg_version: "", + awf_version: "v0.19.1", + awmg_version: "v0.1.4", steps: { firewall: "squid" }, @@ -177,10 +305,10 @@ jobs: node-version: '24' package-manager-cache: false - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.17.0 + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.19.1 - name: Install Claude Code CLI - run: npm install -g --silent @anthropic-ai/claude-code@2.1.39 - - name: Determine automatic lockdown mode for GitHub MCP server + run: npm install -g --silent @anthropic-ai/claude-code@2.1.42 + - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 env: @@ -191,8 +319,8 @@ jobs: const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.17.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.17.0 ghcr.io/github/gh-aw-firewall/squid:0.17.0 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 - - name: Start MCP gateway + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.19.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.19.1 ghcr.io/github/gh-aw-firewall/squid:0.19.1 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 + - name: Start MCP Gateway id: start-mcp-gateway env: GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} @@ -241,113 +369,11 @@ jobs: script: | const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); await generateWorkflowOverview(core); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - run: | - bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" - - GH_AW_PROMPT_EOF - cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/document-config-params.md}} - GH_AW_PROMPT_EOF - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - with: - script: | - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_BEFORE: process.env.GH_AW_GITHUB_EVENT_BEFORE, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE - } - }); - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + - name: Download prompt artifact + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh + name: prompt + path: /tmp/gh-aw/aw-prompts - name: Clean git credentials run: bash /opt/gh-aw/actions/clean_git_credentials.sh - name: Execute Claude Code CLI @@ -423,7 +449,7 @@ jobs: timeout-minutes: 30 run: | set -o pipefail - sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.17.0 --skip-pull --enable-api-proxy \ + sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.19.1 --skip-pull --enable-api-proxy \ -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} @@ -449,7 +475,7 @@ jobs: SERVER_URL_STRIPPED="${SERVER_URL#https://}" git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" echo "Git configured with standard GitHub Actions identity" - - name: Stop MCP gateway + - name: Stop MCP Gateway if: always() continue-on-error: true env: @@ -486,7 +512,7 @@ jobs: setupGlobals(core, github, context, exec, io); const { main } = require('/opt/gh-aw/actions/parse_claude_log.cjs'); await main(); - - name: Parse MCP gateway logs for step summary + - name: Parse MCP Gateway logs for step summary if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 with: @@ -504,11 +530,16 @@ jobs: # Fix permissions on firewall logs so they can be uploaded as artifacts # AWF runs with sudo, creating files owned by root sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi - name: Upload agent artifacts if: always() continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: name: agent-artifacts path: | @@ -526,7 +557,7 @@ jobs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@9382be3ca9ac18917e111a99d4e6bbff58d0dccc # v0.43.23 + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 with: destination: /opt/gh-aw/actions - name: Check team membership for workflow diff --git a/.github/workflows/document-phpdoc-tags.lock.yml b/.github/workflows/document-phpdoc-tags.lock.yml new file mode 100644 index 00000000000..f3a6263460e --- /dev/null +++ b/.github/workflows/document-phpdoc-tags.lock.yml @@ -0,0 +1,575 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.45.4). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Finds undocumented PHPDoc tags supported by PHPStan and creates documentation PRs on phpstan/phpstan +# +# frontmatter-hash: 4b00de08f40349ad6705433b25b1eda1b6286fca149203f6d4ddf87f4279b376 + +name: "Document PHPDoc Tags" +"on": + push: + branches: + - 2.2.x + paths: + - src/PhpDoc/PhpDocNodeResolver.php + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.ref }}" + +run-name: "Document PHPDoc Tags" + +jobs: + activation: + needs: pre_activation + if: needs.pre_activation.outputs.activated == 'true' + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 + with: + destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + .agents + fetch-depth: 1 + persist-credentials: false + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "document-phpdoc-tags.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/document-phpdoc-tags.md}} + GH_AW_PROMPT_EOF + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_BEFORE: process.env.GH_AW_GITHUB_EVENT_BEFORE, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Upload prompt artifact + if: success() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: prompt + path: /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + env: + GH_AW_WORKFLOW_ID_SANITIZED: documentphpdoctags + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 + with: + destination: /opt/gh-aw/actions + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: __phpstan-website + ref: 2.2.x + repository: phpstan/phpstan + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "claude", + engine_name: "Claude Code", + model: "claude-opus-4-6", + version: "", + agent_version: "2.1.42", + cli_version: "v0.45.4", + workflow_name: "Document PHPDoc Tags", + experimental: false, + supports_tools_allowlist: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults"], + firewall_enabled: true, + awf_version: "v0.19.1", + awmg_version: "v0.1.4", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.19.1 + - name: Install Claude Code CLI + run: npm install -g --silent @anthropic-ai/claude-code@2.1.42 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.19.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.19.1 ghcr.io/github/gh-aw-firewall/squid:0.19.1 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + export GH_AW_ENGINE="claude" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' + + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Download prompt artifact + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: prompt + path: /tmp/gh-aw/aw-prompts + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh + - name: Execute Claude Code CLI + id: agentic_execution + # Allowed tools (sorted): + # - Bash + # - BashOutput + # - Edit + # - ExitPlanMode + # - Glob + # - Grep + # - KillBash + # - LS + # - MultiEdit + # - NotebookEdit + # - NotebookRead + # - Read + # - Task + # - TodoWrite + # - Write + # - mcp__github__download_workflow_run_artifact + # - mcp__github__get_code_scanning_alert + # - mcp__github__get_commit + # - mcp__github__get_dependabot_alert + # - mcp__github__get_discussion + # - mcp__github__get_discussion_comments + # - mcp__github__get_file_contents + # - mcp__github__get_job_logs + # - mcp__github__get_label + # - mcp__github__get_latest_release + # - mcp__github__get_me + # - mcp__github__get_notification_details + # - mcp__github__get_pull_request + # - mcp__github__get_pull_request_comments + # - mcp__github__get_pull_request_diff + # - mcp__github__get_pull_request_files + # - mcp__github__get_pull_request_review_comments + # - mcp__github__get_pull_request_reviews + # - mcp__github__get_pull_request_status + # - mcp__github__get_release_by_tag + # - mcp__github__get_secret_scanning_alert + # - mcp__github__get_tag + # - mcp__github__get_workflow_run + # - mcp__github__get_workflow_run_logs + # - mcp__github__get_workflow_run_usage + # - mcp__github__issue_read + # - mcp__github__list_branches + # - mcp__github__list_code_scanning_alerts + # - mcp__github__list_commits + # - mcp__github__list_dependabot_alerts + # - mcp__github__list_discussion_categories + # - mcp__github__list_discussions + # - mcp__github__list_issue_types + # - mcp__github__list_issues + # - mcp__github__list_label + # - mcp__github__list_notifications + # - mcp__github__list_pull_requests + # - mcp__github__list_releases + # - mcp__github__list_secret_scanning_alerts + # - mcp__github__list_starred_repositories + # - mcp__github__list_tags + # - mcp__github__list_workflow_jobs + # - mcp__github__list_workflow_run_artifacts + # - mcp__github__list_workflow_runs + # - mcp__github__list_workflows + # - mcp__github__pull_request_read + # - mcp__github__search_code + # - mcp__github__search_issues + # - mcp__github__search_orgs + # - mcp__github__search_pull_requests + # - mcp__github__search_repositories + # - mcp__github__search_users + timeout-minutes: 30 + run: | + set -o pipefail + sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.19.1 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + BASH_DEFAULT_TIMEOUT_MS: 60000 + BASH_MAX_TIMEOUT_MS: 60000 + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + DISABLE_BUG_COMMAND: 1 + DISABLE_ERROR_REPORTING: 1 + DISABLE_TELEMETRY: 1 + GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_WORKSPACE: ${{ github.workspace }} + MCP_TIMEOUT: 120000 + MCP_TOOL_TIMEOUT: 60000 + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'ANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,PHPSTAN_BOT_TOKEN' + SECRET_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + SECRET_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SECRET_PHPSTAN_BOT_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_claude_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + + pre_activation: + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 + with: + destination: /opt/gh-aw/actions + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: admin,maintainer,write + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); + await main(); + diff --git a/.github/workflows/document-phpdoc-tags.md b/.github/workflows/document-phpdoc-tags.md new file mode 100644 index 00000000000..cab8a61f3f2 --- /dev/null +++ b/.github/workflows/document-phpdoc-tags.md @@ -0,0 +1,125 @@ +--- +name: Document PHPDoc Tags +description: Finds undocumented PHPDoc tags supported by PHPStan and creates documentation PRs on phpstan/phpstan +on: + push: + branches: [2.2.x] + paths: [src/PhpDoc/PhpDocNodeResolver.php] + workflow_dispatch: +engine: + id: claude + model: claude-opus-4-6 + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} +permissions: + contents: read + issues: read + pull-requests: read +tools: + bash: ["*"] + github: + toolsets: [default, repos] +timeout-minutes: 30 +steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: phpstan/phpstan + ref: 2.2.x + path: __phpstan-website + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} +--- + +# Document Undocumented PHPDoc Tags + +You are a documentation agent for PHPStan. Your job is to find PHPDoc tags that PHPStan supports but are not documented on the website, and to write documentation for them. + +## Source files + +- **Tag handling code**: `src/PhpDoc/PhpDocNodeResolver.php` in this workspace (phpstan-src repo) +- **Valid tag list**: `src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php` in this workspace — contains `POSSIBLE_PHPSTAN_TAGS` listing all recognized `@phpstan-*` tags +- **PHPDocs basics page**: `__phpstan-website/website/src/writing-php-code/phpdocs-basics.md` (checked out from `phpstan/phpstan`) +- **PHPDoc types page**: `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` (checked out from `phpstan/phpstan`) +- **All website docs**: `__phpstan-website/website/src/` directory — search here for tags that may be documented on other pages +- **Source code for research**: `src/`, `conf/`, and `tests/` directories in this workspace (phpstan-src repo) + +## Task + +### Step 1: Extract all supported tags from source code + +1. Read `src/PhpDoc/PhpDocNodeResolver.php` and extract every PHPDoc tag name it processes. Tags appear as string literals in arrays like `['@var', '@phan-var', '@psalm-var', '@phpstan-var']` and in `getTagsByName()` calls. +2. Read `src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php` and extract the list of recognized `@phpstan-*` tags. +3. Build a complete list of **base tags** that PHPStan supports. For tags that have `@phpstan-`/`@psalm-`/`@phan-` prefix variants, the base tag is the unprefixed form (e.g., `@param` is the base for `@phpstan-param`). For tags that only exist with a `@phpstan-` prefix (e.g., `@phpstan-type`, `@phpstan-assert`), keep the prefixed form. + +### Step 2: Check which tags are documented on the website + +1. Read `__phpstan-website/website/src/writing-php-code/phpdocs-basics.md` +2. Read `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` +3. Search the entire `__phpstan-website/website/src/` directory for each tag name to check if it's documented on any page + +A tag counts as "documented" if it appears on any website page with an explanation of what it does. A tag does NOT count as documented if it only appears in passing examples without explanation, or only in the "Prefixed tags" section. + +### Step 3: Determine which tags need documentation + +**Important — prefix variants are already handled:** + +The "Prefixed tags" section of `phpdocs-basics.md` already explains that tags like `@param`, `@return`, `@var`, and generics-related tags can be prefixed with `@phpstan-` (and `@psalm-`, `@phan-`). Do NOT create separate documentation for prefix variants. Only document the base tag (e.g., `@param`, not `@phpstan-param`). Exception: tags that ONLY exist with a prefix (like `@phpstan-type`, `@phpstan-assert`) need to be documented with their prefix. + +**Important — verify tag name accuracy:** + +When checking whether a tag is documented, verify the exact tag name matches between the source code and the documentation. Flag and fix any mismatches (e.g., if docs use a slightly different tag name than the code). + +{{#if github.event_name == 'push'}} +Focus primarily on tags that were added or changed in this push. Run `git diff ${{ github.event.before }} -- src/PhpDoc/PhpDocNodeResolver.php` to see what changed. Document newly added or changed tags, but also briefly check if any other tags remain undocumented and include those too. +{{#else}} +Check ALL tags from the source code against the documentation. Do not look at git history or diffs — compare the full tag list against all website documentation and document every undocumented tag you find. +{{/if}} + +If there are no undocumented tags (and no mismatched tag names), stop and report that all tags are documented. Do not create a PR. + +### Step 4: Research each undocumented tag + +For each undocumented tag, investigate what it does: + +1. **Read the resolver method** in `PhpDocNodeResolver.php` to understand how the tag is parsed. +2. **Search the source code** in `src/` for where the resolved tag data is used. For example, search for related method names in `ResolvedPhpDocBlock.php` and in rules under `src/Rules/`. +3. **Look at related rules** in `src/Rules/` that enforce or check the tag's semantics. +4. **Check tests** in `tests/` for test data files that exercise the tag — these show exactly what behavior the tag enables. + +### Step 5: Write documentation + +Edit `__phpstan-website/website/src/writing-php-code/phpdocs-basics.md` to add documentation for missing tags. Do NOT overwrite the file — use targeted edits to insert new sections. + +**Follow the existing writing style exactly:** + +- Use section headings at the same level as similar existing sections +- Provide a concise description (one or two sentences) +- Include a short PHP code example showing the tag in use +- If the tag interacts with specific rules or features, mention that briefly +- Use fenced code blocks with `php` language annotation +- If the tag was introduced in a specific PHPStan version, add a version badge: + +```html +
Available in PHPStan X.Y
+``` + +**Placement:** Insert new sections near related existing content. For example, property-related tags go near `@readonly`, class-level tags go near other class-level tags, etc. + +**Also fix any tag name mismatches** between documentation and source code to ensure the documented tag names match what the code actually accepts. + +### Step 6: Create a pull request + +After editing the documentation file, push the changes and create a PR on `phpstan/phpstan`: + +```bash +cd __phpstan-website +git config user.name "phpstan-bot" +git config user.email "ondrej+phpstanbot@mirtes.cz" +git checkout -b docs/undocumented-phpdoc-tags +git add website/src/writing-php-code/phpdocs-basics.md +git commit -m "Document undocumented PHPDoc tags" +git push origin docs/undocumented-phpdoc-tags +gh pr create --repo phpstan/phpstan --base 2.2.x --draft --title "[Docs] Document undocumented PHPDoc tags" --body "PR DESCRIPTION HERE" +``` + +Replace `PR DESCRIPTION HERE` with a description listing which tags were newly documented with a one-line summary of each, any tag name mismatches that were fixed, and a note that prefix variants are already covered by the "Prefixed tags" section. From 45dea8d751b679ffe3e5d82c3fd86cbe8e1f1ab1 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sat, 21 Feb 2026 06:28:24 +0000 Subject: [PATCH 20/70] Add document-phpdoc-types workflow Co-Authored-By: Claude Opus 4.6 --- .../workflows/document-phpdoc-types.lock.yml | 575 ++++++++++++++++++ .github/workflows/document-phpdoc-types.md | 113 ++++ 2 files changed, 688 insertions(+) create mode 100644 .github/workflows/document-phpdoc-types.lock.yml create mode 100644 .github/workflows/document-phpdoc-types.md diff --git a/.github/workflows/document-phpdoc-types.lock.yml b/.github/workflows/document-phpdoc-types.lock.yml new file mode 100644 index 00000000000..a2441229f19 --- /dev/null +++ b/.github/workflows/document-phpdoc-types.lock.yml @@ -0,0 +1,575 @@ +# +# ___ _ _ +# / _ \ | | (_) +# | |_| | __ _ ___ _ __ | |_ _ ___ +# | _ |/ _` |/ _ \ '_ \| __| |/ __| +# | | | | (_| | __/ | | | |_| | (__ +# \_| |_/\__, |\___|_| |_|\__|_|\___| +# __/ | +# _ _ |___/ +# | | | | / _| | +# | | | | ___ _ __ _ __| |_| | _____ ____ +# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| +# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ +# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ +# +# This file was automatically generated by gh-aw (v0.45.4). DO NOT EDIT. +# +# To update this file, edit the corresponding .md file and run: +# gh aw compile +# Not all edits will cause changes to this file. +# +# For more information: https://github.github.com/gh-aw/introduction/overview/ +# +# Finds undocumented PHPDoc types in TypeNodeResolver and creates documentation PRs on phpstan/phpstan +# +# frontmatter-hash: f03c46de2b23185fffeb696c2bd043a4f156e078ffffc538664f89847bc94706 + +name: "Document PHPDoc Types" +"on": + push: + branches: + - 2.2.x + paths: + - src/PhpDoc/TypeNodeResolver.php + workflow_dispatch: + +permissions: {} + +concurrency: + group: "gh-aw-${{ github.workflow }}-${{ github.ref }}" + +run-name: "Document PHPDoc Types" + +jobs: + activation: + needs: pre_activation + if: needs.pre_activation.outputs.activated == 'true' + runs-on: ubuntu-slim + permissions: + contents: read + outputs: + comment_id: "" + comment_repo: "" + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 + with: + destination: /opt/gh-aw/actions + - name: Checkout .github and .agents folders + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: | + .github + .agents + fetch-depth: 1 + persist-credentials: false + - name: Check workflow file timestamps + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_WORKFLOW_FILE: "document-phpdoc-types.lock.yml" + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); + await main(); + - name: Create prompt with built-in context + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + run: | + bash /opt/gh-aw/actions/create_prompt_first.sh + cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" + cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + The following GitHub context information is available for this workflow: + {{#if __GH_AW_GITHUB_ACTOR__ }} + - **actor**: __GH_AW_GITHUB_ACTOR__ + {{/if}} + {{#if __GH_AW_GITHUB_REPOSITORY__ }} + - **repository**: __GH_AW_GITHUB_REPOSITORY__ + {{/if}} + {{#if __GH_AW_GITHUB_WORKSPACE__ }} + - **workspace**: __GH_AW_GITHUB_WORKSPACE__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} + - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} + - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} + - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ + {{/if}} + {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} + - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ + {{/if}} + {{#if __GH_AW_GITHUB_RUN_ID__ }} + - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ + {{/if}} + + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + + GH_AW_PROMPT_EOF + cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" + {{#runtime-import .github/workflows/document-phpdoc-types.md}} + GH_AW_PROMPT_EOF + - name: Interpolate variables and render templates + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); + await main(); + - name: Substitute placeholders + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_GITHUB_ACTOR: ${{ github.actor }} + GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} + GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} + GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} + GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + + const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); + + // Call the substitution function + return await substitutePlaceholders({ + file: process.env.GH_AW_PROMPT, + substitutions: { + GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, + GH_AW_GITHUB_EVENT_BEFORE: process.env.GH_AW_GITHUB_EVENT_BEFORE, + GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, + GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, + GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, + GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, + GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, + GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, + GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND + } + }); + - name: Validate prompt placeholders + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh + - name: Print prompt + env: + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + run: bash /opt/gh-aw/actions/print_prompt_summary.sh + - name: Upload prompt artifact + if: success() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: prompt + path: /tmp/gh-aw/aw-prompts/prompt.txt + retention-days: 1 + + agent: + needs: activation + runs-on: ubuntu-latest + permissions: + contents: read + issues: read + pull-requests: read + env: + GH_AW_WORKFLOW_ID_SANITIZED: documentphpdoctypes + outputs: + checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} + model: ${{ steps.generate_aw_info.outputs.model }} + secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 + with: + destination: /opt/gh-aw/actions + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: __phpstan-website + ref: 2.2.x + repository: phpstan/phpstan + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} + + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Checkout PR branch + id: checkout-pr + if: | + github.event.pull_request + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + with: + github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); + await main(); + - name: Generate agentic run info + id: generate_aw_info + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const fs = require('fs'); + + const awInfo = { + engine_id: "claude", + engine_name: "Claude Code", + model: "claude-opus-4-6", + version: "", + agent_version: "2.1.42", + cli_version: "v0.45.4", + workflow_name: "Document PHPDoc Types", + experimental: false, + supports_tools_allowlist: true, + run_id: context.runId, + run_number: context.runNumber, + run_attempt: process.env.GITHUB_RUN_ATTEMPT, + repository: context.repo.owner + '/' + context.repo.repo, + ref: context.ref, + sha: context.sha, + actor: context.actor, + event_name: context.eventName, + staged: false, + allowed_domains: ["defaults"], + firewall_enabled: true, + awf_version: "v0.19.1", + awmg_version: "v0.1.4", + steps: { + firewall: "squid" + }, + created_at: new Date().toISOString() + }; + + // Write to /tmp/gh-aw directory to avoid inclusion in PR + const tmpPath = '/tmp/gh-aw/aw_info.json'; + fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); + console.log('Generated aw_info.json at:', tmpPath); + console.log(JSON.stringify(awInfo, null, 2)); + + // Set model as output for reuse in other steps/jobs + core.setOutput('model', awInfo.model); + - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret + id: validate-secret + run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + with: + node-version: '24' + package-manager-cache: false + - name: Install awf binary + run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.19.1 + - name: Install Claude Code CLI + run: npm install -g --silent @anthropic-ai/claude-code@2.1.42 + - name: Determine automatic lockdown mode for GitHub MCP Server + id: determine-automatic-lockdown + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + with: + script: | + const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); + await determineAutomaticLockdown(github, context, core); + - name: Download container images + run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.19.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.19.1 ghcr.io/github/gh-aw-firewall/squid:0.19.1 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 + - name: Start MCP Gateway + id: start-mcp-gateway + env: + GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} + GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: | + set -eo pipefail + mkdir -p /tmp/gh-aw/mcp-config + + # Export gateway environment variables for MCP config and gateway script + export MCP_GATEWAY_PORT="80" + export MCP_GATEWAY_DOMAIN="host.docker.internal" + MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_API_KEY}" + export MCP_GATEWAY_API_KEY + export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" + mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" + export DEBUG="*" + + export GH_AW_ENGINE="claude" + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' + + cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh + { + "mcpServers": { + "github": { + "container": "ghcr.io/github/github-mcp-server:v0.30.3", + "env": { + "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", + "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", + "GITHUB_READ_ONLY": "1", + "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" + } + } + }, + "gateway": { + "port": $MCP_GATEWAY_PORT, + "domain": "${MCP_GATEWAY_DOMAIN}", + "apiKey": "${MCP_GATEWAY_API_KEY}", + "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" + } + } + GH_AW_MCP_CONFIG_EOF + - name: Generate workflow overview + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); + await generateWorkflowOverview(core); + - name: Download prompt artifact + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 + with: + name: prompt + path: /tmp/gh-aw/aw-prompts + - name: Clean git credentials + run: bash /opt/gh-aw/actions/clean_git_credentials.sh + - name: Execute Claude Code CLI + id: agentic_execution + # Allowed tools (sorted): + # - Bash + # - BashOutput + # - Edit + # - ExitPlanMode + # - Glob + # - Grep + # - KillBash + # - LS + # - MultiEdit + # - NotebookEdit + # - NotebookRead + # - Read + # - Task + # - TodoWrite + # - Write + # - mcp__github__download_workflow_run_artifact + # - mcp__github__get_code_scanning_alert + # - mcp__github__get_commit + # - mcp__github__get_dependabot_alert + # - mcp__github__get_discussion + # - mcp__github__get_discussion_comments + # - mcp__github__get_file_contents + # - mcp__github__get_job_logs + # - mcp__github__get_label + # - mcp__github__get_latest_release + # - mcp__github__get_me + # - mcp__github__get_notification_details + # - mcp__github__get_pull_request + # - mcp__github__get_pull_request_comments + # - mcp__github__get_pull_request_diff + # - mcp__github__get_pull_request_files + # - mcp__github__get_pull_request_review_comments + # - mcp__github__get_pull_request_reviews + # - mcp__github__get_pull_request_status + # - mcp__github__get_release_by_tag + # - mcp__github__get_secret_scanning_alert + # - mcp__github__get_tag + # - mcp__github__get_workflow_run + # - mcp__github__get_workflow_run_logs + # - mcp__github__get_workflow_run_usage + # - mcp__github__issue_read + # - mcp__github__list_branches + # - mcp__github__list_code_scanning_alerts + # - mcp__github__list_commits + # - mcp__github__list_dependabot_alerts + # - mcp__github__list_discussion_categories + # - mcp__github__list_discussions + # - mcp__github__list_issue_types + # - mcp__github__list_issues + # - mcp__github__list_label + # - mcp__github__list_notifications + # - mcp__github__list_pull_requests + # - mcp__github__list_releases + # - mcp__github__list_secret_scanning_alerts + # - mcp__github__list_starred_repositories + # - mcp__github__list_tags + # - mcp__github__list_workflow_jobs + # - mcp__github__list_workflow_run_artifacts + # - mcp__github__list_workflow_runs + # - mcp__github__list_workflows + # - mcp__github__pull_request_read + # - mcp__github__search_code + # - mcp__github__search_issues + # - mcp__github__search_orgs + # - mcp__github__search_pull_requests + # - mcp__github__search_repositories + # - mcp__github__search_users + timeout-minutes: 30 + run: | + set -o pipefail + sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.19.1 --skip-pull --enable-api-proxy \ + -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + BASH_DEFAULT_TIMEOUT_MS: 60000 + BASH_MAX_TIMEOUT_MS: 60000 + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + DISABLE_BUG_COMMAND: 1 + DISABLE_ERROR_REPORTING: 1 + DISABLE_TELEMETRY: 1 + GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json + GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GITHUB_WORKSPACE: ${{ github.workspace }} + MCP_TIMEOUT: 120000 + MCP_TOOL_TIMEOUT: 60000 + - name: Configure Git credentials + env: + REPO_NAME: ${{ github.repository }} + SERVER_URL: ${{ github.server_url }} + run: | + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + # Re-authenticate git with GitHub token + SERVER_URL_STRIPPED="${SERVER_URL#https://}" + git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" + echo "Git configured with standard GitHub Actions identity" + - name: Stop MCP Gateway + if: always() + continue-on-error: true + env: + MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} + MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} + GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} + run: | + bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" + - name: Redact secrets in logs + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); + await main(); + env: + GH_AW_SECRET_NAMES: 'ANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,PHPSTAN_BOT_TOKEN' + SECRET_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + SECRET_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} + SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} + SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SECRET_PHPSTAN_BOT_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + - name: Parse agent logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_claude_log.cjs'); + await main(); + - name: Parse MCP Gateway logs for step summary + if: always() + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); + await main(); + - name: Print firewall logs + if: always() + continue-on-error: true + env: + AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs + run: | + # Fix permissions on firewall logs so they can be uploaded as artifacts + # AWF runs with sudo, creating files owned by root + sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true + # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) + if command -v awf &> /dev/null; then + awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" + else + echo 'AWF binary not installed, skipping firewall log summary' + fi + - name: Upload agent artifacts + if: always() + continue-on-error: true + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: agent-artifacts + path: | + /tmp/gh-aw/aw-prompts/prompt.txt + /tmp/gh-aw/aw_info.json + /tmp/gh-aw/mcp-logs/ + /tmp/gh-aw/sandbox/firewall/logs/ + /tmp/gh-aw/agent-stdio.log + /tmp/gh-aw/agent/ + if-no-files-found: ignore + + pre_activation: + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + steps: + - name: Setup Scripts + uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 + with: + destination: /opt/gh-aw/actions + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + env: + GH_AW_REQUIRED_ROLES: admin,maintainer,write + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io); + const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); + await main(); + diff --git a/.github/workflows/document-phpdoc-types.md b/.github/workflows/document-phpdoc-types.md new file mode 100644 index 00000000000..61258a431e5 --- /dev/null +++ b/.github/workflows/document-phpdoc-types.md @@ -0,0 +1,113 @@ +--- +name: Document PHPDoc Types +description: Finds undocumented PHPDoc types in TypeNodeResolver and creates documentation PRs on phpstan/phpstan +on: + push: + branches: [2.2.x] + paths: [src/PhpDoc/TypeNodeResolver.php] + workflow_dispatch: +engine: + id: claude + model: claude-opus-4-6 + env: + CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} +permissions: + contents: read + issues: read + pull-requests: read +tools: + bash: ["*"] + github: + toolsets: [default, repos] +timeout-minutes: 30 +steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: phpstan/phpstan + ref: 2.2.x + path: __phpstan-website + token: ${{ secrets.PHPSTAN_BOT_TOKEN }} +--- + +# Document Undocumented PHPDoc Types + +You are a documentation agent for PHPStan. Your job is to find PHPDoc types supported by `TypeNodeResolver` that are not yet documented in the user-facing PHPDoc types reference, and to add documentation for them. + +## Source files + +- **Type resolver**: `src/PhpDoc/TypeNodeResolver.php` in this workspace (phpstan-src repo) +- **PHPDoc types docs**: `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` (checked out from `phpstan/phpstan`) + +## Task + +### Step 1: Extract supported types from TypeNodeResolver + +Read `src/PhpDoc/TypeNodeResolver.php` and extract every type name that it resolves. The types come from two places: + +1. **`resolveIdentifierTypeNode()`** — contains a `switch (strtolower($typeNode->name))` with `case` entries for each identifier type (e.g. `int`, `non-empty-string`, `callable-object`, etc.). + +2. **`resolveGenericTypeNode()`** — contains `if`/`elseif` checks on `$mainTypeName` for generic type forms (e.g. `array`, `class-string`, `key-of`, `int-mask`, etc.). + +**Skip** any type names that begin with `__` (double underscore) — these are internal. + +### Step 2: Extract documented types from phpdoc-types.md + +Read `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` and extract all type names that are already documented. Types appear as: +- Bullet list items with inline code (e.g. `* \`int\`, \`integer\``) +- In code block examples +- In prose descriptions (e.g. "`non-falsy-string` (also known as `truthy-string`)") + +Be thorough — a type counts as "documented" even if it only appears as a secondary mention, alias, or in a code example. + +### Step 3: Compare and identify undocumented types + +{{#if github.event_name == 'push'}} +Focus only on types that were added or changed in this push. Run `git diff ${{ github.event.before }} -- src/PhpDoc/TypeNodeResolver.php` to see what changed. Only document newly added types. +{{#else}} +Compare ALL non-skipped types from TypeNodeResolver against the documentation. Document every supported type that is not yet mentioned anywhere in phpdoc-types.md. +{{/if}} + +If there are no undocumented types, stop and report that all types are documented. Do not create a PR. + +### Step 4: Add documentation for undocumented types + +Edit `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` to add the missing types. Use **targeted edits** — do not overwrite the file. + +**Placement rules** — add each type to the correct existing section: + +- Integer types/ranges → "Integer ranges" section +- String types → "Other advanced string types" section +- Array types → "General arrays" section +- Class/interface/trait/enum string types → "class-string" section +- Callable types → "Callables" or "Basic types" section as appropriate +- Bottom type synonyms → "Bottom type" section +- Mixed variants → "Mixed" section +- Scalar variants → "Basic types" section +- Object variants → "Basic types" section + +**Follow the existing writing style exactly.** The documentation is concise: + +- For types added to a bullet list, just add a new `* \`type-name\`` entry or append to an existing line (e.g. adding `noreturn` to the bottom type synonyms list). +- For types that need a brief explanation, write one or two sentences in the same style as existing entries. For example, the string types section uses patterns like: + - `` `non-empty-string` is any string except `''`. `` + - `` `lowercase-string` accepts strings where `strtolower($string) === $string` is true. `` +- Only add code examples if the type's behavior is non-obvious. +- If the new type is an alias or synonym of an already-documented type, mention it alongside the existing type (e.g. add `noreturn` to the bottom type list, add `interface-string` next to `class-string`). + +### Step 5: Create a pull request + +After editing the documentation file, push the changes and create a PR on `phpstan/phpstan`: + +```bash +cd __phpstan-website +git config user.name "phpstan-bot" +git config user.email "ondrej+phpstanbot@mirtes.cz" +git checkout -b docs/undocumented-phpdoc-types +git add website/src/writing-php-code/phpdoc-types.md +git commit -m "Document undocumented PHPDoc types" +git push origin docs/undocumented-phpdoc-types +gh pr create --repo phpstan/phpstan --base 2.2.x --draft --title "[Docs] Document undocumented PHPDoc types" --body "PR DESCRIPTION HERE" +``` + +Replace `PR DESCRIPTION HERE` with a description listing which types were newly documented, grouped by section. From 2246d72d9429d7687bd29984563ee316fcc0fee8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Feb 2026 20:12:19 +0100 Subject: [PATCH 21/70] claude-random-easy-fixes-scheduled.yml - generate fixes for 20 issues only --- .github/workflows/claude-random-easy-fixes-scheduled.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude-random-easy-fixes-scheduled.yml b/.github/workflows/claude-random-easy-fixes-scheduled.yml index 22650fb78d8..1ddb9c24f53 100644 --- a/.github/workflows/claude-random-easy-fixes-scheduled.yml +++ b/.github/workflows/claude-random-easy-fixes-scheduled.yml @@ -2,8 +2,8 @@ name: "Claude Random Easy Fixes (Scheduled)" on: schedule: - # Run 10 times, once an hour at :15, from 10pm CEST (20:00 UTC) to 7am CEST (05:00 UTC) - - cron: '15 20-23,0-5 * * *' + # Run 4 times, once an hour at :15, from 9pm CET (20:00 UTC) to 12am CET (23:00 UTC) + - cron: '15 20-23 * * *' jobs: trigger: From d74052a2457e9235e1d0245835ad816d57ad8392 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Feb 2026 09:47:51 +0100 Subject: [PATCH 22/70] Remove agentic workflows --- .github/agents/agentic-workflows.agent.md | 167 ----- .github/aw/actions-lock.json | 44 -- .../workflows/document-config-params.lock.yml | 575 ------------------ .github/workflows/document-config-params.md | 183 ------ .../workflows/document-phpdoc-tags.lock.yml | 575 ------------------ .github/workflows/document-phpdoc-tags.md | 125 ---- .../workflows/document-phpdoc-types.lock.yml | 575 ------------------ .github/workflows/document-phpdoc-types.md | 113 ---- 8 files changed, 2357 deletions(-) delete mode 100644 .github/agents/agentic-workflows.agent.md delete mode 100644 .github/aw/actions-lock.json delete mode 100644 .github/workflows/document-config-params.lock.yml delete mode 100644 .github/workflows/document-config-params.md delete mode 100644 .github/workflows/document-phpdoc-tags.lock.yml delete mode 100644 .github/workflows/document-phpdoc-tags.md delete mode 100644 .github/workflows/document-phpdoc-types.lock.yml delete mode 100644 .github/workflows/document-phpdoc-types.md diff --git a/.github/agents/agentic-workflows.agent.md b/.github/agents/agentic-workflows.agent.md deleted file mode 100644 index dea035c3519..00000000000 --- a/.github/agents/agentic-workflows.agent.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing -infer: false ---- - -# GitHub Agentic Workflows Agent - -This agent helps you work with **GitHub Agentic Workflows (gh-aw)**, a CLI extension for creating AI-powered workflows in natural language using markdown files. - -## What This Agent Does - -This is a **dispatcher agent** that routes your request to the appropriate specialized prompt based on your task: - -- **Creating new workflows**: Routes to `create` prompt -- **Updating existing workflows**: Routes to `update` prompt -- **Debugging workflows**: Routes to `debug` prompt -- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt -- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt - -Workflows may optionally include: - -- **Project tracking / monitoring** (GitHub Projects updates, status reporting) -- **Orchestration / coordination** (one workflow assigning agents or dispatching and coordinating other workflows) - -## Files This Applies To - -- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md` -- Workflow lock files: `.github/workflows/*.lock.yml` -- Shared components: `.github/workflows/shared/*.md` -- Configuration: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/github-agentic-workflows.md - -## Problems This Solves - -- **Workflow Creation**: Design secure, validated agentic workflows with proper triggers, tools, and permissions -- **Workflow Debugging**: Analyze logs, identify missing tools, investigate failures, and fix configuration issues -- **Version Upgrades**: Migrate workflows to new gh-aw versions, apply codemods, fix breaking changes -- **Component Design**: Create reusable shared workflow components that wrap MCP servers - -## How to Use - -When you interact with this agent, it will: - -1. **Understand your intent** - Determine what kind of task you're trying to accomplish -2. **Route to the right prompt** - Load the specialized prompt file for your task -3. **Execute the task** - Follow the detailed instructions in the loaded prompt - -## Available Prompts - -### Create New Workflow -**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet - -**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/create-agentic-workflow.md - -**Use cases**: -- "Create a workflow that triages issues" -- "I need a workflow to label pull requests" -- "Design a weekly research automation" - -### Update Existing Workflow -**Load when**: User wants to modify, improve, or refactor an existing workflow - -**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/update-agentic-workflow.md - -**Use cases**: -- "Add web-fetch tool to the issue-classifier workflow" -- "Update the PR reviewer to use discussions instead of issues" -- "Improve the prompt for the weekly-research workflow" - -### Debug Workflow -**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors - -**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/debug-agentic-workflow.md - -**Use cases**: -- "Why is this workflow failing?" -- "Analyze the logs for workflow X" -- "Investigate missing tool calls in run #12345" - -### Upgrade Agentic Workflows -**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations - -**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/upgrade-agentic-workflows.md - -**Use cases**: -- "Upgrade all workflows to the latest version" -- "Fix deprecated fields in workflows" -- "Apply breaking changes from the new release" - -### Create Shared Agentic Workflow -**Load when**: User wants to create a reusable workflow component or wrap an MCP server - -**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/create-shared-agentic-workflow.md - -**Use cases**: -- "Create a shared component for Notion integration" -- "Wrap the Slack MCP server as a reusable component" -- "Design a shared workflow for database queries" - -### Orchestration and Delegation - -**Load when**: Creating or updating workflows that coordinate multiple agents or dispatch work to other workflows - -**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/orchestration.md - -**Use cases**: -- Assigning work to AI coding agents -- Dispatching specialized worker workflows -- Using correlation IDs for tracking -- Orchestration design patterns - -### GitHub Projects Integration - -**Load when**: Creating or updating workflows that manage GitHub Projects v2 - -**Prompt file**: https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/projects.md - -**Use cases**: -- Tracking items and fields with update-project -- Posting periodic run summaries -- Creating new projects -- Projects v2 authentication and configuration - -## Instructions - -When a user interacts with you: - -1. **Identify the task type** from the user's request -2. **Load the appropriate prompt** from the GitHub repository URLs listed above -3. **Follow the loaded prompt's instructions** exactly -4. **If uncertain**, ask clarifying questions to determine the right prompt - -## Quick Reference - -```bash -# Initialize repository for agentic workflows -gh aw init - -# Generate the lock file for a workflow -gh aw compile [workflow-name] - -# Debug workflow runs -gh aw logs [workflow-name] -gh aw audit - -# Upgrade workflows -gh aw fix --write -gh aw compile --validate -``` - -## Key Features of gh-aw - -- **Natural Language Workflows**: Write workflows in markdown with YAML frontmatter -- **AI Engine Support**: Copilot, Claude, Codex, or custom engines -- **MCP Server Integration**: Connect to Model Context Protocol servers for tools -- **Safe Outputs**: Structured communication between AI and GitHub API -- **Strict Mode**: Security-first validation and sandboxing -- **Shared Components**: Reusable workflow building blocks -- **Repo Memory**: Persistent git-backed storage for agents -- **Sandboxed Execution**: All workflows run in the Agent Workflow Firewall (AWF) sandbox, enabling full `bash` and `edit` tools by default - -## Important Notes - -- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.43.23/.github/aw/github-agentic-workflows.md for complete documentation -- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud -- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions -- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF -- Follow security best practices: minimal permissions, explicit network access, no template injection diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json deleted file mode 100644 index 5e9220a26b1..00000000000 --- a/.github/aw/actions-lock.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "entries": { - "actions/checkout@v4.3.1": { - "repo": "actions/checkout", - "version": "v4.3.1", - "sha": "34e114876b0b11c390a56381ad16ebd13914f8d5" - }, - "actions/checkout@v6.0.2": { - "repo": "actions/checkout", - "version": "v6.0.2", - "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd" - }, - "actions/download-artifact@v6.0.0": { - "repo": "actions/download-artifact", - "version": "v6.0.0", - "sha": "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" - }, - "actions/github-script@v8": { - "repo": "actions/github-script", - "version": "v8", - "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" - }, - "actions/setup-node@v6.2.0": { - "repo": "actions/setup-node", - "version": "v6.2.0", - "sha": "6044e13b5dc448c55e2357c09f80417699197238" - }, - "actions/upload-artifact@v6.0.0": { - "repo": "actions/upload-artifact", - "version": "v6.0.0", - "sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f" - }, - "github/gh-aw/actions/setup@v0.45.4": { - "repo": "github/gh-aw/actions/setup", - "version": "v0.45.4", - "sha": "ac090214a48a1938f7abafe132460b66752261af" - }, - "github/gh-aw/actions/setup@v0.48.1": { - "repo": "github/gh-aw/actions/setup", - "version": "v0.48.1", - "sha": "26b6572ae210580303087bc3142fe58d140bf65c" - } - } -} diff --git a/.github/workflows/document-config-params.lock.yml b/.github/workflows/document-config-params.lock.yml deleted file mode 100644 index b5be8ab9735..00000000000 --- a/.github/workflows/document-config-params.lock.yml +++ /dev/null @@ -1,575 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.45.4). DO NOT EDIT. -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan -# -# frontmatter-hash: 31bb738b106c65eb8c5258fe6d0d61365db8ea6b0f389fc75193988252680777 - -name: "Document Config Parameters" -"on": - push: - branches: - - 2.2.x - paths: - - conf/parametersSchema.neon - workflow_dispatch: - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.ref }}" - -run-name: "Document Config Parameters" - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: | - .github - .agents - fetch-depth: 1 - persist-credentials: false - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "document-config-params.lock.yml" - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - run: | - bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" - - GH_AW_PROMPT_EOF - cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/document-config-params.md}} - GH_AW_PROMPT_EOF - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_BEFORE: process.env.GH_AW_GITHUB_EVENT_BEFORE, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND - } - }); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh - - name: Upload prompt artifact - if: success() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: prompt - path: /tmp/gh-aw/aw-prompts/prompt.txt - retention-days: 1 - - agent: - needs: activation - runs-on: ubuntu-latest - permissions: - contents: read - issues: read - pull-requests: read - env: - GH_AW_WORKFLOW_ID_SANITIZED: documentconfigparams - outputs: - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: __phpstan-website - ref: 2.2.x - repository: phpstan/phpstan - token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "claude", - engine_name: "Claude Code", - model: "claude-opus-4-6", - version: "", - agent_version: "2.1.42", - cli_version: "v0.45.4", - workflow_name: "Document Config Parameters", - experimental: false, - supports_tools_allowlist: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults"], - firewall_enabled: true, - awf_version: "v0.19.1", - awmg_version: "v0.1.4", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.19.1 - - name: Install Claude Code CLI - run: npm install -g --silent @anthropic-ai/claude-code@2.1.42 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); - - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.19.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.19.1 ghcr.io/github/gh-aw-firewall/squid:0.19.1 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 - - name: Start MCP Gateway - id: start-mcp-gateway - env: - GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export DEBUG="*" - - export GH_AW_ENGINE="claude" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' - - cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh - { - "mcpServers": { - "github": { - "container": "ghcr.io/github/github-mcp-server:v0.30.3", - "env": { - "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", - "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - GH_AW_MCP_CONFIG_EOF - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); - await generateWorkflowOverview(core); - - name: Download prompt artifact - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 - with: - name: prompt - path: /tmp/gh-aw/aw-prompts - - name: Clean git credentials - run: bash /opt/gh-aw/actions/clean_git_credentials.sh - - name: Execute Claude Code CLI - id: agentic_execution - # Allowed tools (sorted): - # - Bash - # - BashOutput - # - Edit - # - ExitPlanMode - # - Glob - # - Grep - # - KillBash - # - LS - # - MultiEdit - # - NotebookEdit - # - NotebookRead - # - Read - # - Task - # - TodoWrite - # - Write - # - mcp__github__download_workflow_run_artifact - # - mcp__github__get_code_scanning_alert - # - mcp__github__get_commit - # - mcp__github__get_dependabot_alert - # - mcp__github__get_discussion - # - mcp__github__get_discussion_comments - # - mcp__github__get_file_contents - # - mcp__github__get_job_logs - # - mcp__github__get_label - # - mcp__github__get_latest_release - # - mcp__github__get_me - # - mcp__github__get_notification_details - # - mcp__github__get_pull_request - # - mcp__github__get_pull_request_comments - # - mcp__github__get_pull_request_diff - # - mcp__github__get_pull_request_files - # - mcp__github__get_pull_request_review_comments - # - mcp__github__get_pull_request_reviews - # - mcp__github__get_pull_request_status - # - mcp__github__get_release_by_tag - # - mcp__github__get_secret_scanning_alert - # - mcp__github__get_tag - # - mcp__github__get_workflow_run - # - mcp__github__get_workflow_run_logs - # - mcp__github__get_workflow_run_usage - # - mcp__github__issue_read - # - mcp__github__list_branches - # - mcp__github__list_code_scanning_alerts - # - mcp__github__list_commits - # - mcp__github__list_dependabot_alerts - # - mcp__github__list_discussion_categories - # - mcp__github__list_discussions - # - mcp__github__list_issue_types - # - mcp__github__list_issues - # - mcp__github__list_label - # - mcp__github__list_notifications - # - mcp__github__list_pull_requests - # - mcp__github__list_releases - # - mcp__github__list_secret_scanning_alerts - # - mcp__github__list_starred_repositories - # - mcp__github__list_tags - # - mcp__github__list_workflow_jobs - # - mcp__github__list_workflow_run_artifacts - # - mcp__github__list_workflow_runs - # - mcp__github__list_workflows - # - mcp__github__pull_request_read - # - mcp__github__search_code - # - mcp__github__search_issues - # - mcp__github__search_orgs - # - mcp__github__search_pull_requests - # - mcp__github__search_repositories - # - mcp__github__search_users - timeout-minutes: 30 - run: | - set -o pipefail - sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.19.1 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - BASH_DEFAULT_TIMEOUT_MS: 60000 - BASH_MAX_TIMEOUT_MS: 60000 - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - DISABLE_BUG_COMMAND: 1 - DISABLE_ERROR_REPORTING: 1 - DISABLE_TELEMETRY: 1 - GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_WORKSPACE: ${{ github.workspace }} - MCP_TIMEOUT: 120000 - MCP_TOOL_TIMEOUT: 60000 - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Stop MCP Gateway - if: always() - continue-on-error: true - env: - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} - GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} - run: | - bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'ANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,PHPSTAN_BOT_TOKEN' - SECRET_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - SECRET_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SECRET_PHPSTAN_BOT_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_claude_log.cjs'); - await main(); - - name: Parse MCP Gateway logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: agent-artifacts - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/agent/ - if-no-files-found: ignore - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: admin,maintainer,write - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); - await main(); - diff --git a/.github/workflows/document-config-params.md b/.github/workflows/document-config-params.md deleted file mode 100644 index d1ad6b64f88..00000000000 --- a/.github/workflows/document-config-params.md +++ /dev/null @@ -1,183 +0,0 @@ ---- -name: Document Config Parameters -description: Finds undocumented PHPStan config parameters and creates documentation PRs on phpstan/phpstan -on: - push: - branches: [2.2.x] - paths: [conf/parametersSchema.neon] - workflow_dispatch: -engine: - id: claude - model: claude-opus-4-6 - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} -permissions: - contents: read - issues: read - pull-requests: read -tools: - bash: ["*"] - github: - toolsets: [default, repos] -timeout-minutes: 30 -steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 - with: - repository: phpstan/phpstan - ref: 2.2.x - path: __phpstan-website - token: ${{ secrets.PHPSTAN_BOT_TOKEN }} ---- - -# Document Undocumented Config Parameters - -You are a documentation agent for PHPStan. Your job is to find configuration parameters that exist in the schema but lack user-facing documentation, and to write documentation for them. - -## Source files - -- **Parameter schema**: `conf/parametersSchema.neon` in this workspace (phpstan-src repo) -- **Config reference docs**: `__phpstan-website/website/src/config-reference.md` (checked out from `phpstan/phpstan`) -- **Source code for research**: `src/`, `conf/`, and `tests/` directories in this workspace (phpstan-src repo) - -## Task - -### Step 1: Read both files - -1. Read `conf/parametersSchema.neon` from the workspace -2. Read `__phpstan-website/website/src/config-reference.md` from the workspace - -### Step 2: Identify user-facing parameters from the schema - -Extract all parameter names from `parametersSchema.neon`. Note that some parameters are nested inside `structure()` blocks — these use dotted paths in the user's `phpstan.neon`. For example, the schema has: - -```neon -exceptions: structure([ - implicitThrows: bool(), - check: structure([ - missingCheckedExceptionInThrows: bool(), - tooWideThrowType: bool(), - throwTypeCovariance: bool(), - tooWideImplicitThrowType: bool() - ]) -]) -``` - -This means the user-facing parameters are `exceptions.implicitThrows`, `exceptions.check.missingCheckedExceptionInThrows`, `exceptions.check.tooWideThrowType`, etc. Similarly, `cache` has sub-keys like `cache.nodesByStringCountMax`. Make sure to extract ALL nested parameters, not just top-level ones. - -**Skip these entirely:** - -- The entire `featureToggles` section and all its sub-parameters -- Everything after the `# playground mode` comment — these are internal/irrelevant: - - `sourceLocatorPlaygroundMode` - - Nette parameters: `debugMode`, `productionMode`, `tempDir`, `__validate` - - DerivativeContainerFactory internals: `additionalConfigFiles`, `generateBaselineFile`, `analysedPaths`, `allConfigFiles`, `composerAutoloaderProjectPaths`, `analysedPathsFromConfig`, `usedLevel`, `cliAutoloadFile` - - Editor mode internals: `singleReflectionFile`, `singleReflectionInsteadOfFile` - -Also skip these internal parameters that users should not configure directly: -- `strictRulesInstalled`, `deprecationRulesInstalled` (set by installing packages, not by users) -- `cliArgumentsVariablesRegistered` (internal CLI flag) -- `rootDir`, `currentWorkingDirectory` (auto-detected, not user-configurable) -- `sysGetTempDir` (internal) -- `parametersNotInvalidatingCache` (internal) -- `env` (internal environment variable mapping) - -Also skip these level-only parameters — they exist purely to be toggled by rule levels in `conf/config.level*.neon` and are not configured by users directly: -- `checkThisOnly` (level 2) -- `checkMaybeUndefinedVariables` (level 1) -- `checkExtraArguments` (level 1) -- `reportMagicMethods` (level 1) -- `reportMagicProperties` (level 1) -- `checkClassCaseSensitivity` (level 2) -- `checkPhpDocMissingReturn` (level 2) -- `checkPhpDocMethodSignatures` (level 3) -- `checkAdvancedIsset` (level 4) -- `checkFunctionArgumentTypes` (level 5) -- `checkArgumentsPassedByReference` (level 5) -- `checkMissingVarTagTypehint` (level 6) -- `checkMissingTypehints` (level 6) -- `checkUnionTypes` (level 7) -- `reportMaybes` (level 7) -- `checkNullables` (level 8) -- `checkExplicitMixed` (level 9) -- `checkImplicitMixed` (level 10) - -### Step 3: Determine which parameters are undocumented - -Check which parameter names from the schema do NOT appear as documented parameters in `config-reference.md`. A parameter counts as "documented" if it appears as a heading (`###`), in a config key listing, or is explained in a section body. - -{{#if github.event_name == 'push'}} -Focus only on parameters that were added or changed in this push. Run `git diff ${{ github.event.before }} -- conf/parametersSchema.neon` to see what changed across all commits in the push. Only document newly added parameters. -{{#else}} -Check ALL non-skipped parameters from the schema against the documentation. Do not look at git history or diffs — compare the entire `parametersSchema.neon` against `config-reference.md` and document every undocumented parameter you find. -{{/if}} - -If there are no undocumented parameters, stop and report that all parameters are documented. Do not create a PR. - -### Step 4: Research each undocumented parameter - -For each undocumented parameter, investigate what it does by reading files from the workspace (phpstan-src): - -1. **Search the source code** in `src/` for where the parameter is used. Look for the parameter name in PHP files — it will typically appear in a service constructor or be read from the DI container. -2. **Check level configs** in `conf/config.level*.neon` to see which level enables the parameter and what its default value is. -3. **Check `conf/config.neon`** for the parameter's default value. -4. **Look at related rules and tests** to understand the behavior. Check `tests/` for test data files that exercise the parameter. -5. **Check if phpstan-strict-rules sets it** by searching for the parameter name in the codebase and noting if strict-rules is mentioned. - -### Step 5: Write documentation - -Edit the existing `__phpstan-website/website/src/config-reference.md` file to add the new documentation. Do NOT overwrite the file — use targeted edits to insert new parameter sections in the correct locations. - -**Place each parameter in the correct existing section:** -- Boolean flags that enable stricter checks → "Stricter analysis" section (as `###` sub-headings) -- Parameters related to parallel processing → "Parallel processing" section -- Parameters related to caching → "Caching" section -- Other general settings → "Miscellaneous parameters" section -- Parameters related to exceptions → "Exceptions" section - -**Follow the existing documentation conventions exactly:** - -For parameters in "Stricter analysis", use this format: - -``` -### `parameterName` - -**default**: `value` ([strict-rules](https://github.com/phpstan/phpstan-strict-rules) sets it to `otherValue`) - -When set to `true/false`, it [concise description of what changes]. -``` - -Include a short PHP code example only if it helps illustrate the behavior clearly. Keep descriptions concise — one or two sentences is ideal. - -If the parameter was introduced in a specific PHPStan version (not 1.0), add a version badge: - -```html -
Available in PHPStan X.Y
-``` - -For parameters in "Miscellaneous parameters", use: - -``` -### `parameterName` - -**default**: `value` - -Description of what the parameter does. -``` - -### Step 6: Create a pull request - -After editing the documentation file, push the changes and create a PR on `phpstan/phpstan`: - -```bash -cd __phpstan-website -git config user.name "phpstan-bot" -git config user.email "ondrej+phpstanbot@mirtes.cz" -git checkout -b docs/undocumented-config-params -git add website/src/config-reference.md -git commit -m "Document undocumented configuration parameters" -git push origin docs/undocumented-config-params -gh pr create --repo phpstan/phpstan --base 2.2.x --draft --title "[Docs] Document undocumented config parameters" --body "PR DESCRIPTION HERE" -``` - -Replace `PR DESCRIPTION HERE` with a description listing which parameters were newly documented with a one-line summary of each. diff --git a/.github/workflows/document-phpdoc-tags.lock.yml b/.github/workflows/document-phpdoc-tags.lock.yml deleted file mode 100644 index f3a6263460e..00000000000 --- a/.github/workflows/document-phpdoc-tags.lock.yml +++ /dev/null @@ -1,575 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.45.4). DO NOT EDIT. -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# Finds undocumented PHPDoc tags supported by PHPStan and creates documentation PRs on phpstan/phpstan -# -# frontmatter-hash: 4b00de08f40349ad6705433b25b1eda1b6286fca149203f6d4ddf87f4279b376 - -name: "Document PHPDoc Tags" -"on": - push: - branches: - - 2.2.x - paths: - - src/PhpDoc/PhpDocNodeResolver.php - workflow_dispatch: - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.ref }}" - -run-name: "Document PHPDoc Tags" - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: | - .github - .agents - fetch-depth: 1 - persist-credentials: false - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "document-phpdoc-tags.lock.yml" - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - run: | - bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" - - GH_AW_PROMPT_EOF - cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/document-phpdoc-tags.md}} - GH_AW_PROMPT_EOF - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_BEFORE: process.env.GH_AW_GITHUB_EVENT_BEFORE, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND - } - }); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh - - name: Upload prompt artifact - if: success() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: prompt - path: /tmp/gh-aw/aw-prompts/prompt.txt - retention-days: 1 - - agent: - needs: activation - runs-on: ubuntu-latest - permissions: - contents: read - issues: read - pull-requests: read - env: - GH_AW_WORKFLOW_ID_SANITIZED: documentphpdoctags - outputs: - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: __phpstan-website - ref: 2.2.x - repository: phpstan/phpstan - token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "claude", - engine_name: "Claude Code", - model: "claude-opus-4-6", - version: "", - agent_version: "2.1.42", - cli_version: "v0.45.4", - workflow_name: "Document PHPDoc Tags", - experimental: false, - supports_tools_allowlist: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults"], - firewall_enabled: true, - awf_version: "v0.19.1", - awmg_version: "v0.1.4", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.19.1 - - name: Install Claude Code CLI - run: npm install -g --silent @anthropic-ai/claude-code@2.1.42 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); - - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.19.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.19.1 ghcr.io/github/gh-aw-firewall/squid:0.19.1 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 - - name: Start MCP Gateway - id: start-mcp-gateway - env: - GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export DEBUG="*" - - export GH_AW_ENGINE="claude" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' - - cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh - { - "mcpServers": { - "github": { - "container": "ghcr.io/github/github-mcp-server:v0.30.3", - "env": { - "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", - "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - GH_AW_MCP_CONFIG_EOF - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); - await generateWorkflowOverview(core); - - name: Download prompt artifact - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 - with: - name: prompt - path: /tmp/gh-aw/aw-prompts - - name: Clean git credentials - run: bash /opt/gh-aw/actions/clean_git_credentials.sh - - name: Execute Claude Code CLI - id: agentic_execution - # Allowed tools (sorted): - # - Bash - # - BashOutput - # - Edit - # - ExitPlanMode - # - Glob - # - Grep - # - KillBash - # - LS - # - MultiEdit - # - NotebookEdit - # - NotebookRead - # - Read - # - Task - # - TodoWrite - # - Write - # - mcp__github__download_workflow_run_artifact - # - mcp__github__get_code_scanning_alert - # - mcp__github__get_commit - # - mcp__github__get_dependabot_alert - # - mcp__github__get_discussion - # - mcp__github__get_discussion_comments - # - mcp__github__get_file_contents - # - mcp__github__get_job_logs - # - mcp__github__get_label - # - mcp__github__get_latest_release - # - mcp__github__get_me - # - mcp__github__get_notification_details - # - mcp__github__get_pull_request - # - mcp__github__get_pull_request_comments - # - mcp__github__get_pull_request_diff - # - mcp__github__get_pull_request_files - # - mcp__github__get_pull_request_review_comments - # - mcp__github__get_pull_request_reviews - # - mcp__github__get_pull_request_status - # - mcp__github__get_release_by_tag - # - mcp__github__get_secret_scanning_alert - # - mcp__github__get_tag - # - mcp__github__get_workflow_run - # - mcp__github__get_workflow_run_logs - # - mcp__github__get_workflow_run_usage - # - mcp__github__issue_read - # - mcp__github__list_branches - # - mcp__github__list_code_scanning_alerts - # - mcp__github__list_commits - # - mcp__github__list_dependabot_alerts - # - mcp__github__list_discussion_categories - # - mcp__github__list_discussions - # - mcp__github__list_issue_types - # - mcp__github__list_issues - # - mcp__github__list_label - # - mcp__github__list_notifications - # - mcp__github__list_pull_requests - # - mcp__github__list_releases - # - mcp__github__list_secret_scanning_alerts - # - mcp__github__list_starred_repositories - # - mcp__github__list_tags - # - mcp__github__list_workflow_jobs - # - mcp__github__list_workflow_run_artifacts - # - mcp__github__list_workflow_runs - # - mcp__github__list_workflows - # - mcp__github__pull_request_read - # - mcp__github__search_code - # - mcp__github__search_issues - # - mcp__github__search_orgs - # - mcp__github__search_pull_requests - # - mcp__github__search_repositories - # - mcp__github__search_users - timeout-minutes: 30 - run: | - set -o pipefail - sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.19.1 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - BASH_DEFAULT_TIMEOUT_MS: 60000 - BASH_MAX_TIMEOUT_MS: 60000 - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - DISABLE_BUG_COMMAND: 1 - DISABLE_ERROR_REPORTING: 1 - DISABLE_TELEMETRY: 1 - GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_WORKSPACE: ${{ github.workspace }} - MCP_TIMEOUT: 120000 - MCP_TOOL_TIMEOUT: 60000 - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Stop MCP Gateway - if: always() - continue-on-error: true - env: - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} - GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} - run: | - bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'ANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,PHPSTAN_BOT_TOKEN' - SECRET_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - SECRET_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SECRET_PHPSTAN_BOT_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_claude_log.cjs'); - await main(); - - name: Parse MCP Gateway logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: agent-artifacts - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/agent/ - if-no-files-found: ignore - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: admin,maintainer,write - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); - await main(); - diff --git a/.github/workflows/document-phpdoc-tags.md b/.github/workflows/document-phpdoc-tags.md deleted file mode 100644 index cab8a61f3f2..00000000000 --- a/.github/workflows/document-phpdoc-tags.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -name: Document PHPDoc Tags -description: Finds undocumented PHPDoc tags supported by PHPStan and creates documentation PRs on phpstan/phpstan -on: - push: - branches: [2.2.x] - paths: [src/PhpDoc/PhpDocNodeResolver.php] - workflow_dispatch: -engine: - id: claude - model: claude-opus-4-6 - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} -permissions: - contents: read - issues: read - pull-requests: read -tools: - bash: ["*"] - github: - toolsets: [default, repos] -timeout-minutes: 30 -steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 - with: - repository: phpstan/phpstan - ref: 2.2.x - path: __phpstan-website - token: ${{ secrets.PHPSTAN_BOT_TOKEN }} ---- - -# Document Undocumented PHPDoc Tags - -You are a documentation agent for PHPStan. Your job is to find PHPDoc tags that PHPStan supports but are not documented on the website, and to write documentation for them. - -## Source files - -- **Tag handling code**: `src/PhpDoc/PhpDocNodeResolver.php` in this workspace (phpstan-src repo) -- **Valid tag list**: `src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php` in this workspace — contains `POSSIBLE_PHPSTAN_TAGS` listing all recognized `@phpstan-*` tags -- **PHPDocs basics page**: `__phpstan-website/website/src/writing-php-code/phpdocs-basics.md` (checked out from `phpstan/phpstan`) -- **PHPDoc types page**: `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` (checked out from `phpstan/phpstan`) -- **All website docs**: `__phpstan-website/website/src/` directory — search here for tags that may be documented on other pages -- **Source code for research**: `src/`, `conf/`, and `tests/` directories in this workspace (phpstan-src repo) - -## Task - -### Step 1: Extract all supported tags from source code - -1. Read `src/PhpDoc/PhpDocNodeResolver.php` and extract every PHPDoc tag name it processes. Tags appear as string literals in arrays like `['@var', '@phan-var', '@psalm-var', '@phpstan-var']` and in `getTagsByName()` calls. -2. Read `src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php` and extract the list of recognized `@phpstan-*` tags. -3. Build a complete list of **base tags** that PHPStan supports. For tags that have `@phpstan-`/`@psalm-`/`@phan-` prefix variants, the base tag is the unprefixed form (e.g., `@param` is the base for `@phpstan-param`). For tags that only exist with a `@phpstan-` prefix (e.g., `@phpstan-type`, `@phpstan-assert`), keep the prefixed form. - -### Step 2: Check which tags are documented on the website - -1. Read `__phpstan-website/website/src/writing-php-code/phpdocs-basics.md` -2. Read `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` -3. Search the entire `__phpstan-website/website/src/` directory for each tag name to check if it's documented on any page - -A tag counts as "documented" if it appears on any website page with an explanation of what it does. A tag does NOT count as documented if it only appears in passing examples without explanation, or only in the "Prefixed tags" section. - -### Step 3: Determine which tags need documentation - -**Important — prefix variants are already handled:** - -The "Prefixed tags" section of `phpdocs-basics.md` already explains that tags like `@param`, `@return`, `@var`, and generics-related tags can be prefixed with `@phpstan-` (and `@psalm-`, `@phan-`). Do NOT create separate documentation for prefix variants. Only document the base tag (e.g., `@param`, not `@phpstan-param`). Exception: tags that ONLY exist with a prefix (like `@phpstan-type`, `@phpstan-assert`) need to be documented with their prefix. - -**Important — verify tag name accuracy:** - -When checking whether a tag is documented, verify the exact tag name matches between the source code and the documentation. Flag and fix any mismatches (e.g., if docs use a slightly different tag name than the code). - -{{#if github.event_name == 'push'}} -Focus primarily on tags that were added or changed in this push. Run `git diff ${{ github.event.before }} -- src/PhpDoc/PhpDocNodeResolver.php` to see what changed. Document newly added or changed tags, but also briefly check if any other tags remain undocumented and include those too. -{{#else}} -Check ALL tags from the source code against the documentation. Do not look at git history or diffs — compare the full tag list against all website documentation and document every undocumented tag you find. -{{/if}} - -If there are no undocumented tags (and no mismatched tag names), stop and report that all tags are documented. Do not create a PR. - -### Step 4: Research each undocumented tag - -For each undocumented tag, investigate what it does: - -1. **Read the resolver method** in `PhpDocNodeResolver.php` to understand how the tag is parsed. -2. **Search the source code** in `src/` for where the resolved tag data is used. For example, search for related method names in `ResolvedPhpDocBlock.php` and in rules under `src/Rules/`. -3. **Look at related rules** in `src/Rules/` that enforce or check the tag's semantics. -4. **Check tests** in `tests/` for test data files that exercise the tag — these show exactly what behavior the tag enables. - -### Step 5: Write documentation - -Edit `__phpstan-website/website/src/writing-php-code/phpdocs-basics.md` to add documentation for missing tags. Do NOT overwrite the file — use targeted edits to insert new sections. - -**Follow the existing writing style exactly:** - -- Use section headings at the same level as similar existing sections -- Provide a concise description (one or two sentences) -- Include a short PHP code example showing the tag in use -- If the tag interacts with specific rules or features, mention that briefly -- Use fenced code blocks with `php` language annotation -- If the tag was introduced in a specific PHPStan version, add a version badge: - -```html -
Available in PHPStan X.Y
-``` - -**Placement:** Insert new sections near related existing content. For example, property-related tags go near `@readonly`, class-level tags go near other class-level tags, etc. - -**Also fix any tag name mismatches** between documentation and source code to ensure the documented tag names match what the code actually accepts. - -### Step 6: Create a pull request - -After editing the documentation file, push the changes and create a PR on `phpstan/phpstan`: - -```bash -cd __phpstan-website -git config user.name "phpstan-bot" -git config user.email "ondrej+phpstanbot@mirtes.cz" -git checkout -b docs/undocumented-phpdoc-tags -git add website/src/writing-php-code/phpdocs-basics.md -git commit -m "Document undocumented PHPDoc tags" -git push origin docs/undocumented-phpdoc-tags -gh pr create --repo phpstan/phpstan --base 2.2.x --draft --title "[Docs] Document undocumented PHPDoc tags" --body "PR DESCRIPTION HERE" -``` - -Replace `PR DESCRIPTION HERE` with a description listing which tags were newly documented with a one-line summary of each, any tag name mismatches that were fixed, and a note that prefix variants are already covered by the "Prefixed tags" section. diff --git a/.github/workflows/document-phpdoc-types.lock.yml b/.github/workflows/document-phpdoc-types.lock.yml deleted file mode 100644 index a2441229f19..00000000000 --- a/.github/workflows/document-phpdoc-types.lock.yml +++ /dev/null @@ -1,575 +0,0 @@ -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.45.4). DO NOT EDIT. -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# Finds undocumented PHPDoc types in TypeNodeResolver and creates documentation PRs on phpstan/phpstan -# -# frontmatter-hash: f03c46de2b23185fffeb696c2bd043a4f156e078ffffc538664f89847bc94706 - -name: "Document PHPDoc Types" -"on": - push: - branches: - - 2.2.x - paths: - - src/PhpDoc/TypeNodeResolver.php - workflow_dispatch: - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.ref }}" - -run-name: "Document PHPDoc Types" - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - sparse-checkout: | - .github - .agents - fetch-depth: 1 - persist-credentials: false - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "document-phpdoc-types.lock.yml" - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - run: | - bash /opt/gh-aw/actions/create_prompt_first.sh - cat << 'GH_AW_PROMPT_EOF' > "$GH_AW_PROMPT" - - GH_AW_PROMPT_EOF - cat "/opt/gh-aw/prompts/xpia.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/temp_folder_prompt.md" >> "$GH_AW_PROMPT" - cat "/opt/gh-aw/prompts/markdown.md" >> "$GH_AW_PROMPT" - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - - GH_AW_PROMPT_EOF - cat << 'GH_AW_PROMPT_EOF' >> "$GH_AW_PROMPT" - {{#runtime-import .github/workflows/document-phpdoc-types.md}} - GH_AW_PROMPT_EOF - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_BEFORE: ${{ github.event.before }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: ${{ needs.pre_activation.outputs.matched_command }} - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - - const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_BEFORE: process.env.GH_AW_GITHUB_EVENT_BEFORE, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_MATCHED_COMMAND - } - }); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - run: bash /opt/gh-aw/actions/print_prompt_summary.sh - - name: Upload prompt artifact - if: success() - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: prompt - path: /tmp/gh-aw/aw-prompts/prompt.txt - retention-days: 1 - - agent: - needs: activation - runs-on: ubuntu-latest - permissions: - contents: read - issues: read - pull-requests: read - env: - GH_AW_WORKFLOW_ID_SANITIZED: documentphpdoctypes - outputs: - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - path: __phpstan-website - ref: 2.2.x - repository: phpstan/phpstan - token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Generate agentic run info - id: generate_aw_info - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const fs = require('fs'); - - const awInfo = { - engine_id: "claude", - engine_name: "Claude Code", - model: "claude-opus-4-6", - version: "", - agent_version: "2.1.42", - cli_version: "v0.45.4", - workflow_name: "Document PHPDoc Types", - experimental: false, - supports_tools_allowlist: true, - run_id: context.runId, - run_number: context.runNumber, - run_attempt: process.env.GITHUB_RUN_ATTEMPT, - repository: context.repo.owner + '/' + context.repo.repo, - ref: context.ref, - sha: context.sha, - actor: context.actor, - event_name: context.eventName, - staged: false, - allowed_domains: ["defaults"], - firewall_enabled: true, - awf_version: "v0.19.1", - awmg_version: "v0.1.4", - steps: { - firewall: "squid" - }, - created_at: new Date().toISOString() - }; - - // Write to /tmp/gh-aw directory to avoid inclusion in PR - const tmpPath = '/tmp/gh-aw/aw_info.json'; - fs.writeFileSync(tmpPath, JSON.stringify(awInfo, null, 2)); - console.log('Generated aw_info.json at:', tmpPath); - console.log(JSON.stringify(awInfo, null, 2)); - - // Set model as output for reuse in other steps/jobs - core.setOutput('model', awInfo.model); - - name: Validate CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY secret - id: validate-secret - run: /opt/gh-aw/actions/validate_multi_secret.sh CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_API_KEY 'Claude Code' https://github.github.com/gh-aw/reference/engines/#anthropic-claude-code - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - - name: Setup Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 - with: - node-version: '24' - package-manager-cache: false - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.19.1 - - name: Install Claude Code CLI - run: npm install -g --silent @anthropic-ai/claude-code@2.1.42 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); - - name: Download container images - run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.19.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.19.1 ghcr.io/github/gh-aw-firewall/squid:0.19.1 ghcr.io/github/gh-aw-mcpg:v0.1.4 ghcr.io/github/github-mcp-server:v0.30.3 - - name: Start MCP Gateway - id: start-mcp-gateway - env: - GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export DEBUG="*" - - export GH_AW_ENGINE="claude" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_LOCKDOWN -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.1.4' - - cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh - { - "mcpServers": { - "github": { - "container": "ghcr.io/github/github-mcp-server:v0.30.3", - "env": { - "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN", - "GITHUB_PERSONAL_ACCESS_TOKEN": "$GITHUB_MCP_SERVER_TOKEN", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "context,repos,issues,pull_requests" - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - GH_AW_MCP_CONFIG_EOF - - name: Generate workflow overview - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { generateWorkflowOverview } = require('/opt/gh-aw/actions/generate_workflow_overview.cjs'); - await generateWorkflowOverview(core); - - name: Download prompt artifact - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6 - with: - name: prompt - path: /tmp/gh-aw/aw-prompts - - name: Clean git credentials - run: bash /opt/gh-aw/actions/clean_git_credentials.sh - - name: Execute Claude Code CLI - id: agentic_execution - # Allowed tools (sorted): - # - Bash - # - BashOutput - # - Edit - # - ExitPlanMode - # - Glob - # - Grep - # - KillBash - # - LS - # - MultiEdit - # - NotebookEdit - # - NotebookRead - # - Read - # - Task - # - TodoWrite - # - Write - # - mcp__github__download_workflow_run_artifact - # - mcp__github__get_code_scanning_alert - # - mcp__github__get_commit - # - mcp__github__get_dependabot_alert - # - mcp__github__get_discussion - # - mcp__github__get_discussion_comments - # - mcp__github__get_file_contents - # - mcp__github__get_job_logs - # - mcp__github__get_label - # - mcp__github__get_latest_release - # - mcp__github__get_me - # - mcp__github__get_notification_details - # - mcp__github__get_pull_request - # - mcp__github__get_pull_request_comments - # - mcp__github__get_pull_request_diff - # - mcp__github__get_pull_request_files - # - mcp__github__get_pull_request_review_comments - # - mcp__github__get_pull_request_reviews - # - mcp__github__get_pull_request_status - # - mcp__github__get_release_by_tag - # - mcp__github__get_secret_scanning_alert - # - mcp__github__get_tag - # - mcp__github__get_workflow_run - # - mcp__github__get_workflow_run_logs - # - mcp__github__get_workflow_run_usage - # - mcp__github__issue_read - # - mcp__github__list_branches - # - mcp__github__list_code_scanning_alerts - # - mcp__github__list_commits - # - mcp__github__list_dependabot_alerts - # - mcp__github__list_discussion_categories - # - mcp__github__list_discussions - # - mcp__github__list_issue_types - # - mcp__github__list_issues - # - mcp__github__list_label - # - mcp__github__list_notifications - # - mcp__github__list_pull_requests - # - mcp__github__list_releases - # - mcp__github__list_secret_scanning_alerts - # - mcp__github__list_starred_repositories - # - mcp__github__list_tags - # - mcp__github__list_workflow_jobs - # - mcp__github__list_workflow_run_artifacts - # - mcp__github__list_workflow_runs - # - mcp__github__list_workflows - # - mcp__github__pull_request_read - # - mcp__github__search_code - # - mcp__github__search_issues - # - mcp__github__search_orgs - # - mcp__github__search_pull_requests - # - mcp__github__search_repositories - # - mcp__github__search_users - timeout-minutes: 30 - run: | - set -o pipefail - sudo -E awf --tty --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.githubusercontent.com,anthropic.com,api.anthropic.com,api.github.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,cdn.playwright.dev,codeload.github.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,files.pythonhosted.org,ghcr.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,sentry.io,statsig.anthropic.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.19.1 --skip-pull --enable-api-proxy \ - -- /bin/bash -c 'export PATH="$(find /opt/hostedtoolcache -maxdepth 4 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && claude --print --disable-slash-commands --no-chrome --model claude-opus-4-6 --mcp-config /tmp/gh-aw/mcp-config/mcp-servers.json --allowed-tools Bash,BashOutput,Edit,ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,NotebookEdit,NotebookRead,Read,Task,TodoWrite,Write,mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - BASH_DEFAULT_TIMEOUT_MS: 60000 - BASH_MAX_TIMEOUT_MS: 60000 - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - DISABLE_BUG_COMMAND: 1 - DISABLE_ERROR_REPORTING: 1 - DISABLE_TELEMETRY: 1 - GH_AW_MCP_CONFIG: /tmp/gh-aw/mcp-config/mcp-servers.json - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GITHUB_WORKSPACE: ${{ github.workspace }} - MCP_TIMEOUT: 120000 - MCP_TOOL_TIMEOUT: 60000 - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Stop MCP Gateway - if: always() - continue-on-error: true - env: - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} - GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} - run: | - bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'ANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,PHPSTAN_BOT_TOKEN' - SECRET_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - SECRET_CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SECRET_PHPSTAN_BOT_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/agent-stdio.log - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_claude_log.cjs'); - await main(); - - name: Parse MCP Gateway logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 - with: - name: agent-artifacts - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/agent/ - if-no-files-found: ignore - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - steps: - - name: Setup Scripts - uses: github/gh-aw/actions/setup@ac090214a48a1938f7abafe132460b66752261af # v0.45.4 - with: - destination: /opt/gh-aw/actions - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: admin,maintainer,write - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('/opt/gh-aw/actions/check_membership.cjs'); - await main(); - diff --git a/.github/workflows/document-phpdoc-types.md b/.github/workflows/document-phpdoc-types.md deleted file mode 100644 index 61258a431e5..00000000000 --- a/.github/workflows/document-phpdoc-types.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -name: Document PHPDoc Types -description: Finds undocumented PHPDoc types in TypeNodeResolver and creates documentation PRs on phpstan/phpstan -on: - push: - branches: [2.2.x] - paths: [src/PhpDoc/TypeNodeResolver.php] - workflow_dispatch: -engine: - id: claude - model: claude-opus-4-6 - env: - CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} -permissions: - contents: read - issues: read - pull-requests: read -tools: - bash: ["*"] - github: - toolsets: [default, repos] -timeout-minutes: 30 -steps: - - uses: actions/checkout@v4 - - uses: actions/checkout@v4 - with: - repository: phpstan/phpstan - ref: 2.2.x - path: __phpstan-website - token: ${{ secrets.PHPSTAN_BOT_TOKEN }} ---- - -# Document Undocumented PHPDoc Types - -You are a documentation agent for PHPStan. Your job is to find PHPDoc types supported by `TypeNodeResolver` that are not yet documented in the user-facing PHPDoc types reference, and to add documentation for them. - -## Source files - -- **Type resolver**: `src/PhpDoc/TypeNodeResolver.php` in this workspace (phpstan-src repo) -- **PHPDoc types docs**: `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` (checked out from `phpstan/phpstan`) - -## Task - -### Step 1: Extract supported types from TypeNodeResolver - -Read `src/PhpDoc/TypeNodeResolver.php` and extract every type name that it resolves. The types come from two places: - -1. **`resolveIdentifierTypeNode()`** — contains a `switch (strtolower($typeNode->name))` with `case` entries for each identifier type (e.g. `int`, `non-empty-string`, `callable-object`, etc.). - -2. **`resolveGenericTypeNode()`** — contains `if`/`elseif` checks on `$mainTypeName` for generic type forms (e.g. `array`, `class-string`, `key-of`, `int-mask`, etc.). - -**Skip** any type names that begin with `__` (double underscore) — these are internal. - -### Step 2: Extract documented types from phpdoc-types.md - -Read `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` and extract all type names that are already documented. Types appear as: -- Bullet list items with inline code (e.g. `* \`int\`, \`integer\``) -- In code block examples -- In prose descriptions (e.g. "`non-falsy-string` (also known as `truthy-string`)") - -Be thorough — a type counts as "documented" even if it only appears as a secondary mention, alias, or in a code example. - -### Step 3: Compare and identify undocumented types - -{{#if github.event_name == 'push'}} -Focus only on types that were added or changed in this push. Run `git diff ${{ github.event.before }} -- src/PhpDoc/TypeNodeResolver.php` to see what changed. Only document newly added types. -{{#else}} -Compare ALL non-skipped types from TypeNodeResolver against the documentation. Document every supported type that is not yet mentioned anywhere in phpdoc-types.md. -{{/if}} - -If there are no undocumented types, stop and report that all types are documented. Do not create a PR. - -### Step 4: Add documentation for undocumented types - -Edit `__phpstan-website/website/src/writing-php-code/phpdoc-types.md` to add the missing types. Use **targeted edits** — do not overwrite the file. - -**Placement rules** — add each type to the correct existing section: - -- Integer types/ranges → "Integer ranges" section -- String types → "Other advanced string types" section -- Array types → "General arrays" section -- Class/interface/trait/enum string types → "class-string" section -- Callable types → "Callables" or "Basic types" section as appropriate -- Bottom type synonyms → "Bottom type" section -- Mixed variants → "Mixed" section -- Scalar variants → "Basic types" section -- Object variants → "Basic types" section - -**Follow the existing writing style exactly.** The documentation is concise: - -- For types added to a bullet list, just add a new `* \`type-name\`` entry or append to an existing line (e.g. adding `noreturn` to the bottom type synonyms list). -- For types that need a brief explanation, write one or two sentences in the same style as existing entries. For example, the string types section uses patterns like: - - `` `non-empty-string` is any string except `''`. `` - - `` `lowercase-string` accepts strings where `strtolower($string) === $string` is true. `` -- Only add code examples if the type's behavior is non-obvious. -- If the new type is an alias or synonym of an already-documented type, mention it alongside the existing type (e.g. add `noreturn` to the bottom type list, add `interface-string` next to `class-string`). - -### Step 5: Create a pull request - -After editing the documentation file, push the changes and create a PR on `phpstan/phpstan`: - -```bash -cd __phpstan-website -git config user.name "phpstan-bot" -git config user.email "ondrej+phpstanbot@mirtes.cz" -git checkout -b docs/undocumented-phpdoc-types -git add website/src/writing-php-code/phpdoc-types.md -git commit -m "Document undocumented PHPDoc types" -git push origin docs/undocumented-phpdoc-types -gh pr create --repo phpstan/phpstan --base 2.2.x --draft --title "[Docs] Document undocumented PHPDoc types" --body "PR DESCRIPTION HERE" -``` - -Replace `PR DESCRIPTION HERE` with a description listing which types were newly documented, grouped by section. From 87b901089f1fe6910fa93681f0118e79f6818454 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Feb 2026 09:51:49 +0100 Subject: [PATCH 23/70] One less need for a token --- .github/workflows/claude-random-easy-fixes-scheduled.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/claude-random-easy-fixes-scheduled.yml b/.github/workflows/claude-random-easy-fixes-scheduled.yml index 1ddb9c24f53..fabbfb41536 100644 --- a/.github/workflows/claude-random-easy-fixes-scheduled.yml +++ b/.github/workflows/claude-random-easy-fixes-scheduled.yml @@ -8,8 +8,11 @@ on: jobs: trigger: runs-on: ubuntu-latest + permissions: + contents: read + actions: write steps: - name: Trigger Claude Random Easy Fixes env: - GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh workflow run claude-random-easy-fixes.yml -f issue_count=5 --repo ${{ github.repository }} From 5719ba9b15f83c325ef6ed294722ac426bf3d97b Mon Sep 17 00:00:00 2001 From: StepSecurity Bot Date: Tue, 24 Feb 2026 17:02:45 +0000 Subject: [PATCH 24/70] [StepSecurity] ci: Harden GitHub Actions Signed-off-by: StepSecurity Bot --- .github/actions/downgrade-code/action.yml | 4 +- .github/workflows/apiref.yml | 32 ++++--- .github/workflows/backward-compatibility.yml | 14 +++- .github/workflows/block-merge-commits.yml | 7 +- .github/workflows/build-issue-bot.yml | 16 +++- .github/workflows/changelog-generator.yml | 16 +++- .github/workflows/claude-fix-issue.yml | 18 ++-- .github/workflows/claude-fix-pr-ci.yml | 20 +++-- .../claude-random-easy-fixes-scheduled.yml | 8 ++ .../workflows/claude-random-easy-fixes.yml | 5 ++ .github/workflows/claude-react-on-comment.yml | 21 ++++- .github/workflows/close-issues-on-merge.yml | 8 ++ .github/workflows/create-tag.yml | 15 ++-- .github/workflows/e2e-tests.yml | 25 ++++-- .github/workflows/issue-bot.yml | 61 +++++++++----- .github/workflows/lint.yml | 48 +++++++---- .github/workflows/merge-bot-pr.yml | 9 +- .github/workflows/merge-maintained-branch.yml | 9 +- .github/workflows/phar.yml | 79 ++++++++++++------ .../workflows/pr-base-on-previous-branch.yml | 10 ++- .github/workflows/pr-marked-as-ready.yml | 10 ++- .github/workflows/reflection-golden-test.yml | 32 ++++--- .github/workflows/spelling.yml | 12 ++- .github/workflows/static-analysis.yml | 48 +++++++---- .github/workflows/tests.yml | 83 +++++++++++++------ .github/workflows/update-phpstorm-stubs.yml | 18 ++-- 26 files changed, 452 insertions(+), 176 deletions(-) diff --git a/.github/actions/downgrade-code/action.yml b/.github/actions/downgrade-code/action.yml index e09c61b2320..cf9440632bf 100644 --- a/.github/actions/downgrade-code/action.yml +++ b/.github/actions/downgrade-code/action.yml @@ -10,7 +10,7 @@ runs: - name: "Change to simple-downgrade PHP version" if: inputs.php-version == '7.4' || inputs.php-version == '8.0' || inputs.php-version == '8.1' - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.4" @@ -25,7 +25,7 @@ runs: - name: "Re-store PHP version" if: inputs.php-version == '7.4' || inputs.php-version == '8.0' || inputs.php-version == '8.1' - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "${{ inputs.php-version }}" diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index d22eecb2abd..889ccbf5443 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -28,19 +28,24 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Install ApiGen dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "apigen" @@ -48,7 +53,7 @@ jobs: run: "apigen/vendor/bin/apigen -c apigen/apigen.neon --output docs -- src vendor/nikic/php-parser vendor/ondrejmirtes/better-reflection vendor/phpstan/phpdoc-parser" - name: "Upload docs" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: docs path: docs @@ -60,19 +65,24 @@ jobs: if: github.repository_owner == 'phpstan' runs-on: "ubuntu-latest" steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: "Install Node" - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: "16" - name: "Download docs" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: docs path: docs - name: "Sync with S3" - uses: jakejarvis/s3-sync-action@v0.5.1 + uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 # v0.5.1 with: args: --exclude '.git*/*' --follow-symlinks env: @@ -84,7 +94,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.APIREF_AWS_SECRET_ACCESS_KEY }} - name: "Invalidate CloudFront" - uses: chetan/invalidate-cloudfront-action@v2 + uses: chetan/invalidate-cloudfront-action@12d242edc7752fca9140c2034be28792ad22c5a8 # v2.4.1 env: DISTRIBUTION: "E37G1C2KWNAPBD" PATHS: '/${{ github.ref_name }}/*' @@ -92,14 +102,14 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.APIREF_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.APIREF_AWS_SECRET_ACCESS_KEY }} - - uses: peter-evans/repository-dispatch@v3 + - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} repository: "phpstan/phpstan" event-type: check_website_links - name: "Check for broken links" - uses: ScholliYT/Broken-Links-Crawler-Action@v3 + uses: ScholliYT/Broken-Links-Crawler-Action@21eab52f98097989d343116dbbd46dc4541b849b # v3.3.2 with: website_url: 'https://apiref.phpstan.org/${{ github.ref_name }}/index.html' resolve_before_filtering: 'true' diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index 3e1c4662279..57efc92341a 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -15,6 +15,9 @@ concurrency: group: bc-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches cancel-in-progress: true +permissions: + contents: read + jobs: backward-compatibility: name: "Backward Compatibility" @@ -23,18 +26,23 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Install BackwardCompatibilityCheck" run: | diff --git a/.github/workflows/block-merge-commits.yml b/.github/workflows/block-merge-commits.yml index 2399d07570f..f94d10b54e9 100644 --- a/.github/workflows/block-merge-commits.yml +++ b/.github/workflows/block-merge-commits.yml @@ -9,7 +9,12 @@ jobs: runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: Block Merge Commits - uses: Morishiri/block-merge-commits-action@v1.0.1 + uses: Morishiri/block-merge-commits-action@a4554c78def8d874966a8d1e20e2971121443755 # v1.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-issue-bot.yml b/.github/workflows/build-issue-bot.yml index 6debccc9350..f634d4e9752 100644 --- a/.github/workflows/build-issue-bot.yml +++ b/.github/workflows/build-issue-bot.yml @@ -18,6 +18,9 @@ concurrency: group: build-issue-bot-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches cancel-in-progress: true +permissions: + contents: read + jobs: build-issue-bot: name: "Build Issue Bot" @@ -33,19 +36,24 @@ jobs: - "vendor/bin/phpunit" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.5" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Install issue-bot dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "issue-bot" diff --git a/.github/workflows/changelog-generator.yml b/.github/workflows/changelog-generator.yml index cd76fb3188d..2dd24c145b5 100644 --- a/.github/workflows/changelog-generator.yml +++ b/.github/workflows/changelog-generator.yml @@ -18,6 +18,9 @@ concurrency: group: changelog-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches cancel-in-progress: true +permissions: + contents: read + jobs: changelog-generator: name: "Build Changelog Generator" @@ -26,19 +29,24 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Install Changelog Generator dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "changelog-generator" diff --git a/.github/workflows/claude-fix-issue.yml b/.github/workflows/claude-fix-issue.yml index 84e7338bc6c..198b6012b1b 100644 --- a/.github/workflows/claude-fix-issue.yml +++ b/.github/workflows/claude-fix-issue.yml @@ -14,6 +14,9 @@ on: required: true type: string +permissions: + contents: read + jobs: fix: name: "Fix #${{ inputs.issue-number }}" @@ -25,22 +28,27 @@ jobs: 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: repository: phpstan/phpstan-src ref: "2.1.x" fetch-depth: 0 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.4" ini-file: development extensions: mbstring - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Fetch issue details" id: issue @@ -59,7 +67,7 @@ jobs: echo "$ISSUE_JSON" | jq -r '.body' > /tmp/issue-body.txt - name: "Run Claude Code" - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@35a9e0292d36f1186f5d842b14eb575074e8b450 # v1.0.57 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_args: "--model claude-opus-4-6" @@ -161,7 +169,7 @@ jobs: - name: "Create Pull Request" id: create-pr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: branch-suffix: random delete-branch: true diff --git a/.github/workflows/claude-fix-pr-ci.yml b/.github/workflows/claude-fix-pr-ci.yml index 912681e59d0..154ff465764 100644 --- a/.github/workflows/claude-fix-pr-ci.yml +++ b/.github/workflows/claude-fix-pr-ci.yml @@ -19,9 +19,14 @@ jobs: outputs: status: ${{ steps.waitforstatuschecks.outputs.status }} steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: "Wait for status checks" id: waitforstatuschecks - uses: "WyriHaximus/github-action-wait-for-status@v1" + uses: "WyriHaximus/github-action-wait-for-status@b809158b20d3e32350fe2d868a124f7f2e0e4253" # v1 with: ignoreActions: "Wait for CI checks,Fix CI failure,Automerge PRs" checkInterval: 13 @@ -40,6 +45,11 @@ jobs: 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: "Check fix attempt count" id: check-attempts env: @@ -114,14 +124,14 @@ jobs: - name: "Checkout PR branch" if: steps.check-attempts.outputs.skip != 'true' && steps.failures.outputs.skip != 'true' - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ github.head_ref }} fetch-depth: 0 - name: "Install PHP" if: steps.check-attempts.outputs.skip != 'true' && steps.failures.outputs.skip != 'true' - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.4" @@ -130,11 +140,11 @@ jobs: - name: "Install dependencies" if: steps.check-attempts.outputs.skip != 'true' && steps.failures.outputs.skip != 'true' - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Run Claude Code" if: steps.check-attempts.outputs.skip != 'true' && steps.failures.outputs.skip != 'true' - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@35a9e0292d36f1186f5d842b14eb575074e8b450 # v1.0.57 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_args: "--model claude-opus-4-6" diff --git a/.github/workflows/claude-random-easy-fixes-scheduled.yml b/.github/workflows/claude-random-easy-fixes-scheduled.yml index fabbfb41536..f9a9e032eb7 100644 --- a/.github/workflows/claude-random-easy-fixes-scheduled.yml +++ b/.github/workflows/claude-random-easy-fixes-scheduled.yml @@ -5,6 +5,9 @@ on: # Run 4 times, once an hour at :15, from 9pm CET (20:00 UTC) to 12am CET (23:00 UTC) - cron: '15 20-23 * * *' +permissions: + contents: read + jobs: trigger: runs-on: ubuntu-latest @@ -12,6 +15,11 @@ jobs: contents: read actions: write 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 }} diff --git a/.github/workflows/claude-random-easy-fixes.yml b/.github/workflows/claude-random-easy-fixes.yml index 3e84d8e2428..7ab81867350 100644 --- a/.github/workflows/claude-random-easy-fixes.yml +++ b/.github/workflows/claude-random-easy-fixes.yml @@ -23,6 +23,11 @@ jobs: issues: read 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: diff --git a/.github/workflows/claude-react-on-comment.yml b/.github/workflows/claude-react-on-comment.yml index 1e4dd5d17b1..ec3276d5034 100644 --- a/.github/workflows/claude-react-on-comment.yml +++ b/.github/workflows/claude-react-on-comment.yml @@ -12,6 +12,9 @@ concurrency: group: claude-pr-reactions-${{ github.event.pull_request.number || github.event.issue.number }} cancel-in-progress: false +permissions: + contents: read + jobs: check-trigger: name: "Check trigger phrase" @@ -20,6 +23,11 @@ jobs: outputs: triggered: ${{ steps.check.outputs.triggered }} steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: "Check for trigger phrase" id: check env: @@ -47,23 +55,28 @@ jobs: id-token: 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.4" ini-file: development extensions: mbstring - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "React to feedback" - uses: anthropics/claude-code-action@v1 + uses: anthropics/claude-code-action@35a9e0292d36f1186f5d842b14eb575074e8b450 # v1.0.57 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} trigger_phrase: "@phpstan-bot" diff --git a/.github/workflows/close-issues-on-merge.yml b/.github/workflows/close-issues-on-merge.yml index e157df37c5e..4c411c33656 100644 --- a/.github/workflows/close-issues-on-merge.yml +++ b/.github/workflows/close-issues-on-merge.yml @@ -9,12 +9,20 @@ on: types: - closed +permissions: + contents: read + jobs: close-issues: name: Close linked issues if: github.repository_owner == 'phpstan' && github.event.pull_request.merged == true runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: "Find and close linked issues" env: GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index a853501487e..493a742a6a7 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -20,33 +20,38 @@ jobs: name: "Create tag" runs-on: "ubuntu-latest" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: 'Get Previous tag' id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + uses: "WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce" # v1.4.0 env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - name: 'Get next versions' id: semvers - uses: "WyriHaximus/github-action-next-semvers@v1" + uses: "WyriHaximus/github-action-next-semvers@d079934efaf011a4cf8912d4637097fe35d32b93" # v1 with: version: ${{ steps.previoustag.outputs.tag }} - name: "Create new minor tag" - uses: rickstaa/action-create-tag@v1 + uses: rickstaa/action-create-tag@a1c7777fcb2fee4f19b0f283ba888afa11678b72 # v1.7.2 if: inputs.version == 'minor' with: tag: ${{ steps.semvers.outputs.minor }} message: ${{ steps.semvers.outputs.minor }} - name: "Create new patch tag" - uses: rickstaa/action-create-tag@v1 + uses: rickstaa/action-create-tag@a1c7777fcb2fee4f19b0f283ba888afa11678b72 # v1.7.2 if: inputs.version == 'patch' with: tag: ${{ steps.semvers.outputs.patch }} diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index de913177244..7a378cbb307 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -22,6 +22,9 @@ concurrency: group: e2e-${{ github.head_ref || github.run_id }} # will be canceled on subsequent pushes in pull requests but not branches cancel-in-progress: true +permissions: + contents: read + jobs: result-cache-e2e-tests: name: "Result cache E2E tests" @@ -304,18 +307,23 @@ jobs: ../bashunit -a not_contains 'test.php:7' "$OUTPUT" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" extensions: mbstring ini-values: memory_limit=256M - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Patch PHPStan" run: "patch src/Analyser/Error.php < e2e/PHPStanErrorPatch.patch" @@ -422,18 +430,23 @@ jobs: ../../bin/phpstan analyze 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" tools: ${{ matrix.tools }} extensions: ${{ matrix.extensions }} - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Install bashunit" run: "curl -s https://bashunit.typeddevs.com/install.sh | bash -s e2e/ 0.22.0" diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index bb3d3cda927..ea4e4fd42a6 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -31,22 +31,27 @@ jobs: matrix: ${{ steps.shards.outputs.shards }} 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.5" - name: "Install issue-bot dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "issue-bot" - name: "Cache downloads" - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ./issue-bot/tmp key: "issue-bot-download-v7-${{ github.run_id }}" @@ -65,17 +70,17 @@ jobs: run: | echo "shards=$(jq -c '{include: [range(length) | {shard: .}]}' matrix.json)" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: playground-cache path: issue-bot/tmp/playgroundCache.tmp - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: issue-cache path: issue-bot/tmp/issueCache.tmp - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: matrix path: issue-bot/matrix.json @@ -91,36 +96,41 @@ jobs: matrix: ${{ fromJSON(needs.download.outputs.matrix) }} 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.5" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: composer-options: "--no-dev" - name: "Install issue-bot dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "issue-bot" - - uses: Wandalen/wretry.action@v3.8.0 + - uses: Wandalen/wretry.action@e68c23e6309f2871ca8ae4763e7629b9c258e1ea # v3.8.0 with: - action: actions/download-artifact@v4 + action: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: | name: playground-cache path: issue-bot/tmp attempt_limit: 5 attempt_delay: 1000 - - uses: Wandalen/wretry.action@v3.8.0 + - uses: Wandalen/wretry.action@e68c23e6309f2871ca8ae4763e7629b9c258e1ea # v3.8.0 with: - action: actions/download-artifact@v4 + action: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: | name: matrix path: issue-bot @@ -140,7 +150,7 @@ jobs: timeout-minutes: 5 run: ./console.php run ${{ steps.chunk.outputs.phpVersion }} ${{ steps.chunk.outputs.playgroundExamples }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: results-${{ steps.chunk.outputs.phpVersion }}-${{ steps.chunk.outputs.chunkNumber }} path: issue-bot/tmp/results-${{ steps.chunk.outputs.phpVersion }}-*.tmp @@ -152,31 +162,36 @@ jobs: runs-on: "ubuntu-latest" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.5" - name: "Install issue-bot dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "issue-bot" - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: playground-cache path: issue-bot/tmp - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: issue-cache path: issue-bot/tmp - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: results-* merge-multiple: true @@ -206,7 +221,7 @@ jobs: - name: "Upload step summary" if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: step-summary path: issue-bot/tmp/step-summary.md diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 43c28049931..a48cc8087d4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,11 +31,16 @@ jobs: - "8.5" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "${{ matrix.php-version }}" @@ -44,7 +49,7 @@ jobs: if: matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1' run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 doctrine/instantiator:^1.0 --update-with-dependencies --ignore-platform-reqs" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - uses: ./.github/actions/downgrade-code with: @@ -63,18 +68,23 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Checkout build-cs" - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: repository: "phpstan/build-cs" path: "build-cs" ref: "2.x" - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.5" @@ -82,10 +92,10 @@ jobs: - name: "Validate Composer" run: "composer validate" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Install build-cs dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "build-cs" @@ -102,16 +112,21 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.5" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Composer Dependency Analyser" run: "make composer-dependency-analyser" @@ -123,16 +138,21 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.5" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Name Collision Detector" run: "make name-collision" diff --git a/.github/workflows/merge-bot-pr.yml b/.github/workflows/merge-bot-pr.yml index 6d34bb3d80d..58a9ddb5570 100644 --- a/.github/workflows/merge-bot-pr.yml +++ b/.github/workflows/merge-bot-pr.yml @@ -11,17 +11,22 @@ jobs: name: Automerge PRs runs-on: ubuntu-latest steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: 'Wait for status checks' if: github.event.pull_request.user.login == 'phpstan-bot' id: waitforstatuschecks - uses: "WyriHaximus/github-action-wait-for-status@v1" + uses: "WyriHaximus/github-action-wait-for-status@b809158b20d3e32350fe2d868a124f7f2e0e4253" # v1 with: ignoreActions: "automerge,Automerge PRs" checkInterval: 13 env: GITHUB_TOKEN: "${{ secrets.PHPSTAN_BOT_TOKEN }}" - name: Merge Pull Request - uses: juliangruber/merge-pull-request-action@v1 + uses: juliangruber/merge-pull-request-action@d4773803fdc1d1fd46801ab0c56c135df9075de8 # v1.1.1 if: steps.waitforstatuschecks.outputs.status == 'success' with: github-token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" diff --git a/.github/workflows/merge-maintained-branch.yml b/.github/workflows/merge-maintained-branch.yml index 0910e6db4a5..8b5aa99578a 100644 --- a/.github/workflows/merge-maintained-branch.yml +++ b/.github/workflows/merge-maintained-branch.yml @@ -13,10 +13,15 @@ jobs: if: github.repository_owner == 'phpstan' runs-on: ubuntu-latest 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Merge branch" - uses: everlytic/branch-merge@1.1.5 + uses: everlytic/branch-merge@c4a244dc23143f824ae6c022a10732566cb8e973 # 1.1.5 with: github_token: "${{ secrets.PHPSTAN_BOT_TOKEN }}" source_ref: ${{ github.ref }} diff --git a/.github/workflows/phar.yml b/.github/workflows/phar.yml index be5ef62818b..ed356e6197e 100644 --- a/.github/workflows/phar.yml +++ b/.github/workflows/phar.yml @@ -26,26 +26,31 @@ jobs: compiler_changed: ${{ steps.changes.outputs.compiler }} 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" extensions: mbstring, intl - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 # only sebastian/diff ^4 supports PHP 7.4 so we need that in the PHAR - name: "Downgrade PHPUnit" run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 doctrine/instantiator:^1.0 --update-with-dependencies --ignore-platform-reqs" - name: "Install compiler dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "compiler" @@ -65,7 +70,7 @@ jobs: run: "composer dump" - name: "Install Box dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "compiler/box" @@ -73,7 +78,7 @@ jobs: working-directory: "compiler/build" run: "php ../box/vendor/bin/box compile --no-parallel --sort-compiled-files" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: phar-file path: tmp/phpstan.phar @@ -88,7 +93,7 @@ jobs: - name: "Set autoloader suffix" run: "composer config autoloader-suffix PHPStanChecksum" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 env: COMPOSER_ROOT_VERSION: "2.2.x-dev" @@ -109,7 +114,7 @@ jobs: id: "checksum" run: echo "md5=$(md5sum tmp/phpstan.phar | cut -d' ' -f1)" >> $GITHUB_OUTPUT - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: phar-file-checksum path: tmp/phpstan.phar @@ -117,7 +122,7 @@ jobs: - name: "Delete checksum PHAR" run: "rm tmp/phpstan.phar" - - uses: dorny/paths-filter@v3 + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 id: changes with: filters: | @@ -156,14 +161,19 @@ jobs: if: github.event_name == 'pull_request' && needs.compiler-tests.outputs.compiler_changed == 'true' runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v4 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Get base commit SHA id: base run: echo "base_sha=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT" - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 20 @@ -177,7 +187,7 @@ jobs: - name: Find phar-file-checksum from base commit id: find-artifact - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: BASE_SHA: ${{ steps.base.outputs.base_sha }} ARTIFACT_NAME: phar-file-checksum @@ -189,14 +199,14 @@ jobs: # saved to phar-file-checksum/phpstan.phar - name: Download old artifact by ID - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: artifact-ids: ${{ steps.find-artifact.outputs.artifact_id }} run-id: ${{ steps.find-artifact.outputs.run_id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: "Upload old artifact" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: phar-file-checksum-base path: phar-file-checksum/phpstan.phar @@ -209,8 +219,13 @@ jobs: runs-on: "ubuntu-latest" steps: # saved to phpstan.phar + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: "Download base phpstan.phar" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: phar-file-checksum-base @@ -229,32 +244,37 @@ jobs: needs: download-base-sha-phar runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v4 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 # saved to phar-file-checksum/phpstan.phar - name: "Download phpstan.phar" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: phar-file-checksum path: phar-file-checksum # saved to phar-file-checksum-base/phpstan.phar - name: "Download base phpstan.phar" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: phar-file-checksum-base path: phar-file-checksum-base - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Install Box dependencies" - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "compiler/box" @@ -283,10 +303,15 @@ jobs: runs-on: "ubuntu-latest" timeout-minutes: 60 steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: Import GPG key id: import-gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6.3.0 with: gpg_private_key: ${{ secrets.GPG_PHPSTANBOT_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PHPSTANBOT_KEY_PASSPHRASE }} @@ -295,7 +320,7 @@ jobs: git_commit_gpgsign: true - name: "Checkout phpstan-dist" - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: repository: phpstan/phpstan path: phpstan-dist @@ -308,7 +333,7 @@ jobs: run: echo "sha=$(sed -n '2p' .phar-checksum)" >> $GITHUB_OUTPUT - name: "Checkout phpstan-src" - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 path: phpstan-src @@ -338,7 +363,7 @@ jobs: fi - name: "Download phpstan.phar" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: phar-file @@ -364,7 +389,7 @@ jobs: run: "gpg --verify phpstan.phar.asc" - name: "Install lucky_commit" - uses: baptiste0928/cargo-install@v3 + uses: baptiste0928/cargo-install@f204293d9709061b7bc1756fec3ec4e2cd57dec0 # v3.4.0 with: crate: lucky_commit args: --no-default-features @@ -384,7 +409,7 @@ jobs: - name: "Commit PHAR - tag" if: "startsWith(github.ref, 'refs/tags/')" - uses: stefanzweifel/git-auto-commit-action@v5 + uses: stefanzweifel/git-auto-commit-action@b863ae1933cb653a53c021fe36dbb774e1fb9403 # v5.2.0 with: commit_user_name: "phpstan-bot" commit_user_email: "ondrej+phpstanbot@mirtes.cz" diff --git a/.github/workflows/pr-base-on-previous-branch.yml b/.github/workflows/pr-base-on-previous-branch.yml index 34ef71bb837..27762473433 100644 --- a/.github/workflows/pr-base-on-previous-branch.yml +++ b/.github/workflows/pr-base-on-previous-branch.yml @@ -10,14 +10,22 @@ on: - '2.2.x' +permissions: + contents: read + jobs: comment: name: "Comment on pull request" runs-on: 'ubuntu-latest' steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: Comment PR - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: body: "You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x." token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/pr-marked-as-ready.yml b/.github/workflows/pr-marked-as-ready.yml index b9785a2a3c5..03182b1e413 100644 --- a/.github/workflows/pr-marked-as-ready.yml +++ b/.github/workflows/pr-marked-as-ready.yml @@ -7,14 +7,22 @@ on: types: - ready_for_review +permissions: + contents: read + jobs: comment: name: "Comment on pull request" runs-on: 'ubuntu-latest' steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + - name: Comment PR - uses: peter-evans/create-or-update-comment@v4 + uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 with: body: "This pull request has been marked as ready for review." token: ${{ secrets.PHPSTAN_BOT_TOKEN }} diff --git a/.github/workflows/reflection-golden-test.yml b/.github/workflows/reflection-golden-test.yml index f57fa94f9b6..4ca96ccde2f 100644 --- a/.github/workflows/reflection-golden-test.yml +++ b/.github/workflows/reflection-golden-test.yml @@ -32,23 +32,28 @@ jobs: runs-on: "ubuntu-latest" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.3" # Include exotic extensions to discover more symbols extensions: ds,mbstring,runkit7,scoutapm,seaslog,simdjson,var_representation,yac - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Dump phpSymbols.txt" run: "php tests/dump-reflection-test-symbols.php" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: phpSymbols path: ${{ env.REFLECTION_GOLDEN_SYMBOLS_FILE }} @@ -69,7 +74,12 @@ jobs: - "8.5" steps: - - uses: Wandalen/wretry.action@v3.8.0 + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - uses: Wandalen/wretry.action@e68c23e6309f2871ca8ae4763e7629b9c258e1ea # v3.8.0 with: action: actions/download-artifact@v4 with: | @@ -79,12 +89,12 @@ jobs: attempt_delay: 1000 - name: "Checkout base commit" - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ github.event.pull_request.base.sha || github.event.before }} - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "${{ matrix.php-version }}" @@ -93,20 +103,20 @@ jobs: ini-file: development ini-values: memory_limit=2G - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Dump previous reflection data" run: "php tests/generate-reflection-test.php" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: reflection-${{ matrix.php-version }}.test path: ${{ env.REFLECTION_GOLDEN_TEST_FILE }} - name: "Checkout" - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Reflection golden test" run: "make tests-golden-reflection || true" diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 24f48d2bb76..442cd7347e5 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -8,16 +8,24 @@ on: branches: - "2.2.x" +permissions: + contents: read + jobs: typos: name: "Check for typos" runs-on: "ubuntu-latest" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Check for typos" - uses: "crate-ci/typos@v1" + uses: "crate-ci/typos@57b11c6b7e54c402ccd9cda953f1072ec4f78e33" # v1.43.5 with: files: "README.md src/" diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 681d948eb38..c16667fdd4a 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -38,11 +38,16 @@ jobs: operating-system: [ubuntu-latest, windows-latest] 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "${{ matrix.php-version }}" @@ -54,7 +59,7 @@ jobs: shell: bash run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 doctrine/instantiator:^1.0 --update-with-dependencies --ignore-platform-reqs" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - uses: ./.github/actions/downgrade-code with: @@ -67,7 +72,7 @@ jobs: if: failure() && (matrix.php-version == '7.4' || matrix.php-version == '8.0' || matrix.php-version == '8.1') run: "php -d memory_limit=599M bin/phpstan analyse --generate-baseline baseline-php-${{ matrix.php-version }}.neon" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: ${{ failure() }} with: name: baseline-${{ matrix.php-version }} @@ -89,21 +94,26 @@ jobs: - "8.5" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "${{ matrix.php-version }}" ini-file: development extensions: mbstring - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Cache Result cache" - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ./tmp key: "result-cache-v15-${{ matrix.php-version }}-${{ github.run_id }}" @@ -126,17 +136,22 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" ini-file: development - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Generate baseline" run: | @@ -151,17 +166,22 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" ini-file: development - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Generate baseline" run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cfd78291360..56461bf177f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,11 +38,16 @@ jobs: operating-system: [ ubuntu-latest, windows-latest ] 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "${{ matrix.php-version }}" @@ -76,11 +81,16 @@ jobs: operating-system: [ ubuntu-latest, windows-latest ] 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "${{ matrix.php-version }}" @@ -89,7 +99,7 @@ jobs: ini-file: development ini-values: memory_limit=-1 - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Check PHP configuration" run: "vendor/bin/phpunit --check-php-configuration" @@ -108,11 +118,16 @@ jobs: operating-system: [ ubuntu-latest, windows-latest ] 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" @@ -121,7 +136,7 @@ jobs: ini-file: development ini-values: memory_limit=-1 - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Check PHP configuration" run: "vendor/bin/phpunit --check-php-configuration" @@ -135,11 +150,16 @@ jobs: timeout-minutes: 60 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.3" @@ -148,7 +168,7 @@ jobs: ini-file: development ini-values: memory_limit=1G - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - id: set-matrix run: echo "matrix=$(php .github/workflows/tests-levels-matrix.php)" >> $GITHUB_OUTPUT @@ -169,11 +189,16 @@ jobs: script: "${{fromJson(needs.tests-levels-matrix.outputs.matrix)}}" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.3" @@ -182,7 +207,7 @@ jobs: ini-file: development ini-values: memory_limit=-1 - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Check PHP configuration" run: "vendor/bin/phpunit --check-php-configuration" @@ -208,11 +233,16 @@ jobs: operating-system: ubuntu-latest 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "${{ matrix.php-version }}" @@ -225,7 +255,7 @@ jobs: shell: bash run: "composer require --dev phpunit/phpunit:^9.6 sebastian/diff:^4.0 doctrine/instantiator:^1.0 --update-with-dependencies --ignore-platform-reqs" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Downgrade PHPUnit with Paratest" shell: bash @@ -253,11 +283,16 @@ jobs: operating-system: [ubuntu-latest] 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@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - name: "Checkout build-infection" - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: repository: "phpstan/build-infection" path: "build-infection" @@ -268,9 +303,9 @@ jobs: php-version: "${{ matrix.php-version }}" php-extensions: ds,mbstring - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "tests/" @@ -280,7 +315,7 @@ jobs: composer require --dev phpunit/phpunit:^12 brianium/paratest:^7.16 symfony/process:^7 symfony/string:^7 symfony/console:^7 --update-with-dependencies --ignore-platform-reqs --working-dir=tests composer require --dev phpunit/phpunit:^12 sebastian/diff --update-with-dependencies --ignore-platform-reqs - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 with: working-directory: "build-infection/" @@ -298,7 +333,7 @@ jobs: echo "name=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p')" >> $GITHUB_OUTPUT - name: "Restore result cache" - uses: actions/cache/restore@v4 + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ./tmp key: "result-cache-v1-${{ matrix.php-version }}-${{ github.run_id }}" @@ -329,7 +364,7 @@ jobs: --logger-text=php://stdout - name: "Save result cache" - uses: actions/cache/save@v4 + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 if: ${{ !cancelled() }} with: path: ./tmp diff --git a/.github/workflows/update-phpstorm-stubs.yml b/.github/workflows/update-phpstorm-stubs.yml index ad4ae46d607..bbc3f68ee55 100644 --- a/.github/workflows/update-phpstorm-stubs.yml +++ b/.github/workflows/update-phpstorm-stubs.yml @@ -7,26 +7,34 @@ on: # * is a special character in YAML so you have to quote this string - cron: '0 0 * * 2' +permissions: + contents: read + jobs: update-phpstorm-stubs: name: "Update PhpStorm stubs" if: ${{ github.repository == 'phpstan/phpstan-src' }} runs-on: "ubuntu-latest" 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@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: 2.1.x fetch-depth: '0' token: ${{ secrets.PHPSTAN_BOT_TOKEN }} - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + uses: "shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1" # v2 with: coverage: "none" php-version: "8.2" - - uses: "ramsey/composer-install@v3" + - uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # v3 - name: "Checkout stubs" - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: path: "phpstorm-stubs" repository: "jetbrains/phpstorm-stubs" @@ -38,7 +46,7 @@ jobs: run: "./bin/generate-function-metadata.php" - name: "Create Pull Request" id: create-pr - uses: peter-evans/create-pull-request@v6 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: token: ${{ secrets.PHPSTAN_BOT_TOKEN }} branch-suffix: random From e7d321d2e6d8a397a4e6fbbfcfe66a9c0a46362a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Feb 2026 22:05:49 +0100 Subject: [PATCH 25/70] Update branch --- .github/workflows/lint-workflows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-workflows.yml b/.github/workflows/lint-workflows.yml index 5dd4964d5ee..94b083a9da5 100644 --- a/.github/workflows/lint-workflows.yml +++ b/.github/workflows/lint-workflows.yml @@ -6,7 +6,7 @@ on: pull_request: push: branches: - - "2.1.x" + - "2.2.x" permissions: {} From 0324196af94edb714b3cf4763828172d21fba41c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Feb 2026 22:10:07 +0100 Subject: [PATCH 26/70] Fix --- .github/workflows/claude-react-on-comment.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/claude-react-on-comment.yml b/.github/workflows/claude-react-on-comment.yml index 7e9f11cd865..ec3276d5034 100644 --- a/.github/workflows/claude-react-on-comment.yml +++ b/.github/workflows/claude-react-on-comment.yml @@ -15,9 +15,6 @@ concurrency: permissions: contents: read -permissions: - contents: read - jobs: check-trigger: name: "Check trigger phrase" From df51ee055b9f05e31bd5b5b41cd4cfa4af6a8942 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 24 Feb 2026 22:12:31 +0100 Subject: [PATCH 27/70] Fix --- .github/workflows/spelling.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index 33ed1d13006..442cd7347e5 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -11,9 +11,6 @@ on: permissions: contents: read -permissions: - contents: read - jobs: typos: name: "Check for typos" From b054374e70fce2f9895b1fbcfec69964c3402134 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 26 Feb 2026 13:20:28 +0100 Subject: [PATCH 28/70] Remove Claude workflow --- .../claude-random-easy-fixes-scheduled.yml | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 .github/workflows/claude-random-easy-fixes-scheduled.yml diff --git a/.github/workflows/claude-random-easy-fixes-scheduled.yml b/.github/workflows/claude-random-easy-fixes-scheduled.yml deleted file mode 100644 index f9a9e032eb7..00000000000 --- a/.github/workflows/claude-random-easy-fixes-scheduled.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Claude Random Easy Fixes (Scheduled)" - -on: - schedule: - # Run 4 times, once an hour at :15, from 9pm CET (20:00 UTC) to 12am CET (23:00 UTC) - - cron: '15 20-23 * * *' - -permissions: - contents: read - -jobs: - trigger: - runs-on: ubuntu-latest - permissions: - contents: read - actions: write - 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 }} From 688fbc0c64ec243a8b8c8d3903ea0834375b9c3d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 11 Mar 2026 09:51:11 +0100 Subject: [PATCH 29/70] Update workflow --- .github/workflows/bench.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 8f359728b50..50c25ac6e53 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -9,7 +9,7 @@ on: - 'issue-bot/**' push: branches: - - "2.1.x" + - "2.2.x" paths-ignore: - 'compiler/**' - 'apigen/**' From de7c6040f7da36fb35c58b927ae482f01a92455e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 11 Mar 2026 12:40:40 +0100 Subject: [PATCH 30/70] Trigger Claude workflows in the fork --- ...pdate-config-parameters-docs-on-change.yml | 26 +++++++++++++++++++ ...aude-update-phpdoc-tags-docs-on-change.yml | 26 +++++++++++++++++++ ...ude-update-phpdoc-types-docs-on-change.yml | 26 +++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 .github/workflows/claude-update-config-parameters-docs-on-change.yml create mode 100644 .github/workflows/claude-update-phpdoc-tags-docs-on-change.yml create mode 100644 .github/workflows/claude-update-phpdoc-types-docs-on-change.yml diff --git a/.github/workflows/claude-update-config-parameters-docs-on-change.yml b/.github/workflows/claude-update-config-parameters-docs-on-change.yml new file mode 100644 index 00000000000..3ecf3081468 --- /dev/null +++ b/.github/workflows/claude-update-config-parameters-docs-on-change.yml @@ -0,0 +1,26 @@ +name: "Claude Update Config Parameters Docs On Change" + +on: + push: + branches: + - "2.2.x" + paths: + - '.github/workflows/claude-update-config-parameters-docs.yml' + - 'conf/parametersSchema.neon' + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + 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 Update Config Parameters Docs + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: gh workflow run claude-update-config-parameters-docs.yml --repo phpstan-bot/phpstan-src diff --git a/.github/workflows/claude-update-phpdoc-tags-docs-on-change.yml b/.github/workflows/claude-update-phpdoc-tags-docs-on-change.yml new file mode 100644 index 00000000000..63d7f87e088 --- /dev/null +++ b/.github/workflows/claude-update-phpdoc-tags-docs-on-change.yml @@ -0,0 +1,26 @@ +name: "Claude Update PHPDoc Tags Docs On Change" + +on: + push: + branches: + - "2.2.x" + paths: + - '.github/workflows/claude-update-phpdoc-tags-docs-on-change.yml' + - 'src/PhpDoc/PhpDocNodeResolver.php' + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + 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 Update PHPDoc Tags Docs + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: gh workflow run claude-update-phpdoc-tags-docs.yml --repo phpstan-bot/phpstan-src diff --git a/.github/workflows/claude-update-phpdoc-types-docs-on-change.yml b/.github/workflows/claude-update-phpdoc-types-docs-on-change.yml new file mode 100644 index 00000000000..f7f623896de --- /dev/null +++ b/.github/workflows/claude-update-phpdoc-types-docs-on-change.yml @@ -0,0 +1,26 @@ +name: "Claude Update PHPDoc Types Docs On Change" + +on: + push: + branches: + - "2.2.x" + paths: + - '.github/workflows/claude-update-phpdoc-types-docs-on-change.yml' + - 'src/PhpDoc/TypeNodeResolver.php' + +jobs: + fix: + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + 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 Update PHPDoc Types Docs + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: gh workflow run claude-update-phpdoc-types-docs.yml --repo phpstan-bot/phpstan-src From 53c9da669b9cb966af1ad626c307f562af588465 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 11 Mar 2026 12:41:51 +0100 Subject: [PATCH 31/70] Fix --- .../claude-update-config-parameters-docs-on-change.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude-update-config-parameters-docs-on-change.yml b/.github/workflows/claude-update-config-parameters-docs-on-change.yml index 3ecf3081468..1d1ec046e39 100644 --- a/.github/workflows/claude-update-config-parameters-docs-on-change.yml +++ b/.github/workflows/claude-update-config-parameters-docs-on-change.yml @@ -5,7 +5,7 @@ on: branches: - "2.2.x" paths: - - '.github/workflows/claude-update-config-parameters-docs.yml' + - '.github/workflows/claude-update-config-parameters-docs-on-change.yml' - 'conf/parametersSchema.neon' jobs: From e2cfb7c18018d3e337f1666f40f07b8966a53b1c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 11 Mar 2026 13:27:13 +0100 Subject: [PATCH 32/70] Claude - react on review workflow --- .github/workflows/claude-react-on-review.yml | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/claude-react-on-review.yml diff --git a/.github/workflows/claude-react-on-review.yml b/.github/workflows/claude-react-on-review.yml new file mode 100644 index 00000000000..199ac6e7e1e --- /dev/null +++ b/.github/workflows/claude-react-on-review.yml @@ -0,0 +1,23 @@ +name: "Claude React On Review" + +on: + pull_request_review: + types: [submitted] + +jobs: + claude: + runs-on: ubuntu-latest + permissions: + contents: read + actions: write + if: github.event.pull_request.user.login == 'phpstan-bot' + 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 Review PR + env: + GH_TOKEN: ${{ secrets.PHPSTAN_BOT_TOKEN }} + run: gh workflow run claude-pr-review.yml -f pr_number=${{ github.event.pull_request.number }} -f review_id=${{ github.event.review.id }} --repo phpstan-bot/phpstan-src From cf0db03aa1e9a483fe1a0c682cbcac1a0441f3bf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 19 Mar 2026 11:24:32 +0100 Subject: [PATCH 33/70] Playground - LiteralArrayKeyCastRule --- issue-bot/playground.neon | 1 + .../Playground/LiteralArrayKeyCastRule.php | 60 +++++++++++++++++++ .../LiteralArrayKeyCastRuleTest.php | 49 +++++++++++++++ .../data/literal-array-key-cast.php | 25 ++++++++ 4 files changed, 135 insertions(+) create mode 100644 src/Rules/Playground/LiteralArrayKeyCastRule.php create mode 100644 tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php create mode 100644 tests/PHPStan/Rules/Playground/data/literal-array-key-cast.php diff --git a/issue-bot/playground.neon b/issue-bot/playground.neon index a252e3bac87..70e71310d67 100644 --- a/issue-bot/playground.neon +++ b/issue-bot/playground.neon @@ -1,5 +1,6 @@ rules: - PHPStan\Rules\Playground\FunctionNeverRule + - PHPStan\Rules\Playground\LiteralArrayKeyCastRule - PHPStan\Rules\Playground\MethodNeverRule - PHPStan\Rules\Playground\NotAnalysedTraitRule - PHPStan\Rules\Playground\NoPhpCodeRule diff --git a/src/Rules/Playground/LiteralArrayKeyCastRule.php b/src/Rules/Playground/LiteralArrayKeyCastRule.php new file mode 100644 index 00000000000..47f036081bc --- /dev/null +++ b/src/Rules/Playground/LiteralArrayKeyCastRule.php @@ -0,0 +1,60 @@ + + */ +final class LiteralArrayKeyCastRule implements Rule +{ + + public function getNodeType(): string + { + return Array_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + foreach ($node->items as $item) { + if ($item->key === null) { + continue; + } + + $keyType = $scope->getType($item->key); + if (!$keyType->isConstantScalarValue()->yes()) { + continue; + } + + $constantScalars = $keyType->getConstantScalarTypes(); + foreach ($constantScalars as $constantScalar) { + $arrayKeyType = $constantScalar->toArrayKey(); + if ($arrayKeyType->equals($constantScalar)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Key %s (%s) will be cast to %s (%s) in the array.', + $constantScalar->describe(VerbosityLevel::value()), + $constantScalar->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::typeOnly()), + $arrayKeyType->describe(VerbosityLevel::value()), + $arrayKeyType->describe(VerbosityLevel::typeOnly()), + ))->identifier('phpstanPlayground.arrayKeyCast') + ->line($item->getStartLine()) + ->build(); + } + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php b/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php new file mode 100644 index 00000000000..d1562d5bdbb --- /dev/null +++ b/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php @@ -0,0 +1,49 @@ + + */ +final class LiteralArrayKeyCastRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new LiteralArrayKeyCastRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/literal-array-key-cast.php'], [ + [ + "Key '1' (string) will be cast to 1 (int) in the array.", + 14, + ], + [ + "Key null (null) will be cast to '' (string) in the array.", + 15, + ], + [ + 'Key 2.5 (float) will be cast to 2 (int) in the array.', + 16, + ], + [ + 'Key true (bool) will be cast to 1 (int) in the array.', + 18, + ], + [ + 'Key false (bool) will be cast to 0 (int) in the array.', + 19, + ], + [ + "Key '10' (string) will be cast to 10 (int) in the array.", + 21, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/data/literal-array-key-cast.php b/tests/PHPStan/Rules/Playground/data/literal-array-key-cast.php new file mode 100644 index 00000000000..6ebcd206252 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/literal-array-key-cast.php @@ -0,0 +1,25 @@ + 1, + '+1' => 2, + '1' => 3, // cast to 1 + null => 4, // cast to '' + 2.5 => 5, // cast to 2 + '1.2' => 6, + true => 7, // cast to 1 + false => 8, // cast to 0 + '08' => 9, + $partiallyCast => 10, // one part of the union is cast to 10 + ]; + } + +} From cc855b252189ba1dee6ddb2b487cf762fcc80e56 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 19 Mar 2026 12:01:09 +0100 Subject: [PATCH 34/70] Playground - ArrayDimCastRule --- issue-bot/playground.neon | 1 + src/Rules/Playground/ArrayDimCastRule.php | 62 +++++++++++++++++++ .../Rules/Playground/ArrayDimCastRuleTest.php | 53 ++++++++++++++++ .../Playground/data/array-dim-fetch-cast.php | 29 +++++++++ 4 files changed, 145 insertions(+) create mode 100644 src/Rules/Playground/ArrayDimCastRule.php create mode 100644 tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php create mode 100644 tests/PHPStan/Rules/Playground/data/array-dim-fetch-cast.php diff --git a/issue-bot/playground.neon b/issue-bot/playground.neon index 70e71310d67..9fc2864ff3e 100644 --- a/issue-bot/playground.neon +++ b/issue-bot/playground.neon @@ -1,4 +1,5 @@ rules: + - PHPStan\Rules\Playground\ArrayDimCastRule - PHPStan\Rules\Playground\FunctionNeverRule - PHPStan\Rules\Playground\LiteralArrayKeyCastRule - PHPStan\Rules\Playground\MethodNeverRule diff --git a/src/Rules/Playground/ArrayDimCastRule.php b/src/Rules/Playground/ArrayDimCastRule.php new file mode 100644 index 00000000000..de4a6bf11cc --- /dev/null +++ b/src/Rules/Playground/ArrayDimCastRule.php @@ -0,0 +1,62 @@ + + */ +final class ArrayDimCastRule implements Rule +{ + + public function getNodeType(): string + { + return ArrayDimFetch::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->dim === null) { + return []; + } + + $varType = $scope->getType($node->var); + if ($varType->isArray()->no()) { + return []; + } + + $dimType = $scope->getType($node->dim); + if (!$dimType->isConstantScalarValue()->yes()) { + return []; + } + + $constantScalars = $dimType->getConstantScalarTypes(); + $errors = []; + foreach ($constantScalars as $constantScalar) { + $arrayKeyType = $constantScalar->toArrayKey(); + if ($arrayKeyType->equals($constantScalar)) { + continue; + } + + $errors[] = RuleErrorBuilder::message(sprintf( + 'Key %s (%s) will be cast to %s (%s) in the array access.', + $constantScalar->describe(VerbosityLevel::value()), + $constantScalar->generalize(GeneralizePrecision::lessSpecific())->describe(VerbosityLevel::typeOnly()), + $arrayKeyType->describe(VerbosityLevel::value()), + $arrayKeyType->describe(VerbosityLevel::typeOnly()), + ))->identifier('phpstanPlayground.arrayDimFetchCast') + ->build(); + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php b/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php new file mode 100644 index 00000000000..789ae0f6a40 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php @@ -0,0 +1,53 @@ + + */ +final class ArrayDimCastRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ArrayDimCastRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/array-dim-fetch-cast.php'], [ + [ + "Key '1' (string) will be cast to 1 (int) in the array access.", + 13, + ], + [ + "Key null (null) will be cast to '' (string) in the array access.", + 14, + ], + [ + 'Key 2.5 (float) will be cast to 2 (int) in the array access.', + 15, + ], + [ + 'Key true (bool) will be cast to 1 (int) in the array access.', + 17, + ], + [ + 'Key false (bool) will be cast to 0 (int) in the array access.', + 18, + ], + [ + "Key '10' (string) will be cast to 10 (int) in the array access.", + 20, + ], + [ + "Key '1' (string) will be cast to 1 (int) in the array access.", + 26, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/data/array-dim-fetch-cast.php b/tests/PHPStan/Rules/Playground/data/array-dim-fetch-cast.php new file mode 100644 index 00000000000..20dfa5a7021 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/array-dim-fetch-cast.php @@ -0,0 +1,29 @@ + Date: Thu, 19 Mar 2026 13:38:14 +0100 Subject: [PATCH 35/70] Tips linking to https://phpstan.org/blog/why-array-string-keys-are-not-type-safe --- src/Rules/Playground/ArrayDimCastRule.php | 1 + src/Rules/Playground/LiteralArrayKeyCastRule.php | 1 + tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php | 8 ++++++++ .../Rules/Playground/LiteralArrayKeyCastRuleTest.php | 7 +++++++ 4 files changed, 17 insertions(+) diff --git a/src/Rules/Playground/ArrayDimCastRule.php b/src/Rules/Playground/ArrayDimCastRule.php index de4a6bf11cc..0f16a2d4616 100644 --- a/src/Rules/Playground/ArrayDimCastRule.php +++ b/src/Rules/Playground/ArrayDimCastRule.php @@ -53,6 +53,7 @@ public function processNode(Node $node, Scope $scope): array $arrayKeyType->describe(VerbosityLevel::value()), $arrayKeyType->describe(VerbosityLevel::typeOnly()), ))->identifier('phpstanPlayground.arrayDimFetchCast') + ->tip('Learn more: https://phpstan.org/blog/why-array-string-keys-are-not-type-safe') ->build(); } diff --git a/src/Rules/Playground/LiteralArrayKeyCastRule.php b/src/Rules/Playground/LiteralArrayKeyCastRule.php index 47f036081bc..807754d35cd 100644 --- a/src/Rules/Playground/LiteralArrayKeyCastRule.php +++ b/src/Rules/Playground/LiteralArrayKeyCastRule.php @@ -49,6 +49,7 @@ public function processNode(Node $node, Scope $scope): array $arrayKeyType->describe(VerbosityLevel::value()), $arrayKeyType->describe(VerbosityLevel::typeOnly()), ))->identifier('phpstanPlayground.arrayKeyCast') + ->tip('Learn more: https://phpstan.org/blog/why-array-string-keys-are-not-type-safe') ->line($item->getStartLine()) ->build(); } diff --git a/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php b/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php index 789ae0f6a40..f0ca0570744 100644 --- a/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php +++ b/tests/PHPStan/Rules/Playground/ArrayDimCastRuleTest.php @@ -18,34 +18,42 @@ protected function getRule(): Rule public function testRule(): void { + $tip = 'Learn more: https://phpstan.org/blog/why-array-string-keys-are-not-type-safe'; $this->analyse([__DIR__ . '/data/array-dim-fetch-cast.php'], [ [ "Key '1' (string) will be cast to 1 (int) in the array access.", 13, + $tip, ], [ "Key null (null) will be cast to '' (string) in the array access.", 14, + $tip, ], [ 'Key 2.5 (float) will be cast to 2 (int) in the array access.', 15, + $tip, ], [ 'Key true (bool) will be cast to 1 (int) in the array access.', 17, + $tip, ], [ 'Key false (bool) will be cast to 0 (int) in the array access.', 18, + $tip, ], [ "Key '10' (string) will be cast to 10 (int) in the array access.", 20, + $tip, ], [ "Key '1' (string) will be cast to 1 (int) in the array access.", 26, + $tip, ], ]); } diff --git a/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php b/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php index d1562d5bdbb..898e12a7321 100644 --- a/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php +++ b/tests/PHPStan/Rules/Playground/LiteralArrayKeyCastRuleTest.php @@ -18,30 +18,37 @@ protected function getRule(): Rule public function testRule(): void { + $tip = 'Learn more: https://phpstan.org/blog/why-array-string-keys-are-not-type-safe'; $this->analyse([__DIR__ . '/data/literal-array-key-cast.php'], [ [ "Key '1' (string) will be cast to 1 (int) in the array.", 14, + $tip, ], [ "Key null (null) will be cast to '' (string) in the array.", 15, + $tip, ], [ 'Key 2.5 (float) will be cast to 2 (int) in the array.', 16, + $tip, ], [ 'Key true (bool) will be cast to 1 (int) in the array.', 18, + $tip, ], [ 'Key false (bool) will be cast to 0 (int) in the array.', 19, + $tip, ], [ "Key '10' (string) will be cast to 10 (int) in the array.", 21, + $tip, ], ]); } From c1a266ece0e9bc859e6a0b5a0c93b25b3338e22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Fri, 20 Mar 2026 14:55:34 +0100 Subject: [PATCH 36/70] Check constants in parameters --- resources/constantToFunctionParameterMap.php | 2451 +++++++++++++++++ src/Reflection/AllowedConstantsResult.php | 55 + .../AnnotationsMethodParameterReflection.php | 12 + .../ExtendedParameterReflection.php | 7 + .../GenericParametersAcceptorResolver.php | 1 + .../ExtendedNativeParameterReflection.php | 17 + src/Reflection/ParameterAllowedConstants.php | 103 + .../ParameterAllowedConstantsMapProvider.php | 50 + src/Reflection/ParametersAcceptorSelector.php | 3 + .../Php/ClosureCallMethodReflection.php | 1 + src/Reflection/Php/ExitFunctionReflection.php | 1 + src/Reflection/Php/ExtendedDummyParameter.php | 17 + .../Php/PhpClassReflectionExtension.php | 7 +- src/Reflection/Php/PhpFunctionReflection.php | 3 + src/Reflection/Php/PhpMethodReflection.php | 5 + .../PhpParameterFromParserNodeReflection.php | 12 + src/Reflection/Php/PhpParameterReflection.php | 17 + .../ResolvedFunctionVariantWithOriginal.php | 1 + .../NativeFunctionReflectionProvider.php | 6 +- ...ackUnresolvedMethodPrototypeReflection.php | 1 + ...ypeUnresolvedMethodPrototypeReflection.php | 1 + .../WrappedExtendedMethodReflection.php | 1 + src/Rules/AttributesCheck.php | 3 + src/Rules/Classes/InstantiationRule.php | 3 + src/Rules/FunctionCallParametersCheck.php | 92 + src/Rules/Functions/CallCallablesRule.php | 3 + .../CallToFunctionParametersRule.php | 3 + src/Rules/Functions/CallUserFuncRule.php | 3 + src/Rules/Methods/CallMethodsRule.php | 3 + src/Rules/Methods/CallStaticMethodsRule.php | 3 + .../ConstantToFunctionParameterMapTest.php | 162 ++ .../ParameterAllowedConstantsTest.php | 285 ++ .../ParametersAcceptorSelectorTest.php | 2 + .../constantToFunctionParameterMap.neon | 2 + .../Rules/Classes/InstantiationRuleTest.php | 15 + ...constant-parameter-check-instantiation.php | 18 + .../Rules/Functions/CallCallablesRuleTest.php | 12 + .../CallToFunctionParametersRuleTest.php | 80 + .../Rules/Functions/CallUserFuncRuleTest.php | 11 + .../Rules/Functions/data/bug-12850.php | 18 + ...onstant-parameter-check-call-user-func.php | 9 + .../constant-parameter-check-callables.php | 10 + .../data/constant-parameter-check.php | 82 + .../Rules/Methods/CallMethodsRuleTest.php | 22 + .../Methods/CallStaticMethodsRuleTest.php | 16 + .../data/constant-parameter-check-methods.php | 25 + .../data/constant-parameter-check-static.php | 15 + 47 files changed, 3667 insertions(+), 2 deletions(-) create mode 100644 resources/constantToFunctionParameterMap.php create mode 100644 src/Reflection/AllowedConstantsResult.php create mode 100644 src/Reflection/ParameterAllowedConstants.php create mode 100644 src/Reflection/ParameterAllowedConstantsMapProvider.php create mode 100644 tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php create mode 100644 tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php create mode 100644 tests/PHPStan/Reflection/constantToFunctionParameterMap.neon create mode 100644 tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-12850.php create mode 100644 tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php create mode 100644 tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php create mode 100644 tests/PHPStan/Rules/Functions/data/constant-parameter-check.php create mode 100644 tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php create mode 100644 tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php diff --git a/resources/constantToFunctionParameterMap.php b/resources/constantToFunctionParameterMap.php new file mode 100644 index 00000000000..db150a3ad73 --- /dev/null +++ b/resources/constantToFunctionParameterMap.php @@ -0,0 +1,2451 @@ + 'single' | 'bitmask' + * 'constants' => list of constant names valid for this parameter + * 'exclusiveGroups' => (optional, bitmask only) groups of constants that are mutually exclusive + */ +return [ + + // ———————————————————————————————————————————— + // JSON + // ———————————————————————————————————————————— + + 'json_encode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_HEX_QUOT', + 'JSON_HEX_TAG', + 'JSON_HEX_AMP', + 'JSON_HEX_APOS', + 'JSON_NUMERIC_CHECK', + 'JSON_PRETTY_PRINT', + 'JSON_UNESCAPED_SLASHES', + 'JSON_FORCE_OBJECT', + 'JSON_PRESERVE_ZERO_FRACTION', + 'JSON_UNESCAPED_UNICODE', + 'JSON_PARTIAL_OUTPUT_ON_ERROR', + 'JSON_UNESCAPED_LINE_TERMINATORS', + 'JSON_THROW_ON_ERROR', + 'JSON_INVALID_UTF8_IGNORE', + 'JSON_INVALID_UTF8_SUBSTITUTE', + ], + ], + ], + + 'json_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_BIGINT_AS_STRING', + 'JSON_OBJECT_AS_ARRAY', + 'JSON_THROW_ON_ERROR', + 'JSON_INVALID_UTF8_IGNORE', + 'JSON_INVALID_UTF8_SUBSTITUTE', + ], + ], + ], + + 'json_validate' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'JSON_INVALID_UTF8_IGNORE', + ], + ], + ], + + // ———————————————————————————————————————————— + // PCRE + // ———————————————————————————————————————————— + + 'preg_match' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_OFFSET_CAPTURE', + 'PREG_UNMATCHED_AS_NULL', + ], + ], + ], + + 'preg_match_all' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_PATTERN_ORDER', + 'PREG_SET_ORDER', + 'PREG_OFFSET_CAPTURE', + 'PREG_UNMATCHED_AS_NULL', + ], + 'exclusiveGroups' => [ + ['PREG_PATTERN_ORDER', 'PREG_SET_ORDER'], + ], + ], + ], + + 'preg_split' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PREG_SPLIT_NO_EMPTY', + 'PREG_SPLIT_DELIM_CAPTURE', + 'PREG_SPLIT_OFFSET_CAPTURE', + ], + ], + ], + + 'preg_grep' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'PREG_GREP_INVERT', + ], + ], + ], + + // ———————————————————————————————————————————— + // Sorting + // ———————————————————————————————————————————— + + 'sort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'rsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'asort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'arsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'ksort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'krsort' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + 'SORT_NATURAL', + 'SORT_FLAG_CASE', + ], + 'exclusiveGroups' => [ + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + ], + ], + ], + + 'array_unique' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'SORT_REGULAR', + 'SORT_NUMERIC', + 'SORT_STRING', + 'SORT_LOCALE_STRING', + ], + ], + ], + + // ———————————————————————————————————————————— + // Array functions + // ———————————————————————————————————————————— + + 'array_change_key_case' => [ + 'case' => [ + 'type' => 'single', + 'constants' => [ + 'CASE_LOWER', + 'CASE_UPPER', + ], + ], + ], + + 'array_filter' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'ARRAY_FILTER_USE_KEY', + 'ARRAY_FILTER_USE_BOTH', + ], + ], + ], + + 'count' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'COUNT_NORMAL', + 'COUNT_RECURSIVE', + ], + ], + ], + + 'extract' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'EXTR_OVERWRITE', + 'EXTR_SKIP', + 'EXTR_PREFIX_SAME', + 'EXTR_PREFIX_ALL', + 'EXTR_PREFIX_INVALID', + 'EXTR_IF_EXISTS', + 'EXTR_PREFIX_IF_EXISTS', + 'EXTR_REFS', + ], + 'exclusiveGroups' => [ + ['EXTR_OVERWRITE', 'EXTR_SKIP', 'EXTR_PREFIX_SAME', 'EXTR_PREFIX_ALL', 'EXTR_PREFIX_INVALID', 'EXTR_IF_EXISTS', 'EXTR_PREFIX_IF_EXISTS'], + ], + ], + ], + + // ———————————————————————————————————————————— + // HTML entities + // ———————————————————————————————————————————— + + 'htmlspecialchars' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'htmlentities' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'html_entity_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + 'htmlspecialchars_decode' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ENT_COMPAT', + 'ENT_QUOTES', + 'ENT_NOQUOTES', + 'ENT_IGNORE', + 'ENT_SUBSTITUTE', + 'ENT_DISALLOWED', + 'ENT_HTML401', + 'ENT_XML1', + 'ENT_XHTML', + 'ENT_HTML5', + ], + 'exclusiveGroups' => [ + ['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], + ['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], + ], + ], + ], + + // ———————————————————————————————————————————— + // URL / Path + // ———————————————————————————————————————————— + + 'parse_url' => [ + 'component' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_URL_SCHEME', + 'PHP_URL_HOST', + 'PHP_URL_PORT', + 'PHP_URL_USER', + 'PHP_URL_PASS', + 'PHP_URL_PATH', + 'PHP_URL_QUERY', + 'PHP_URL_FRAGMENT', + ], + ], + ], + + 'pathinfo' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PATHINFO_DIRNAME', + 'PATHINFO_BASENAME', + 'PATHINFO_EXTENSION', + 'PATHINFO_FILENAME', + 'PATHINFO_ALL', + ], + ], + ], + + 'http_build_query' => [ + 'encoding_type' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_QUERY_RFC1738', + 'PHP_QUERY_RFC3986', + ], + ], + ], + + // ———————————————————————————————————————————— + // File operations + // ———————————————————————————————————————————— + + 'file_put_contents' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILE_USE_INCLUDE_PATH', + 'FILE_APPEND', + 'LOCK_EX', + ], + ], + ], + + 'file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILE_USE_INCLUDE_PATH', + 'FILE_IGNORE_NEW_LINES', + 'FILE_SKIP_EMPTY_LINES', + 'FILE_NO_DEFAULT_CONTEXT', + ], + ], + ], + + 'flock' => [ + 'operation' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOCK_SH', + 'LOCK_EX', + 'LOCK_UN', + 'LOCK_NB', + ], + 'exclusiveGroups' => [ + ['LOCK_SH', 'LOCK_EX', 'LOCK_UN'], + ], + ], + ], + + 'glob' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'GLOB_MARK', + 'GLOB_NOSORT', + 'GLOB_NOCHECK', + 'GLOB_NOESCAPE', + 'GLOB_BRACE', + 'GLOB_ONLYDIR', + 'GLOB_ERR', + ], + ], + ], + + 'fnmatch' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FNM_NOESCAPE', + 'FNM_PATHNAME', + 'FNM_PERIOD', + 'FNM_CASEFOLD', + ], + ], + ], + + 'scandir' => [ + 'sorting_order' => [ + 'type' => 'single', + 'constants' => [ + 'SCANDIR_SORT_ASCENDING', + 'SCANDIR_SORT_DESCENDING', + 'SCANDIR_SORT_NONE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Math + // ———————————————————————————————————————————— + + 'round' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PHP_ROUND_HALF_UP', + 'PHP_ROUND_HALF_DOWN', + 'PHP_ROUND_HALF_EVEN', + 'PHP_ROUND_HALF_ODD', + ], + ], + ], + + // ———————————————————————————————————————————— + // Random + // ———————————————————————————————————————————— + + 'srand' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MT_RAND_MT19937', + 'MT_RAND_PHP', + ], + ], + ], + + 'mt_srand' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MT_RAND_MT19937', + 'MT_RAND_PHP', + ], + ], + ], + + // ———————————————————————————————————————————— + // Filter + // ———————————————————————————————————————————— + + 'filter_var' => [ + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'FILTER_VALIDATE_INT', + 'FILTER_VALIDATE_BOOLEAN', + 'FILTER_VALIDATE_FLOAT', + 'FILTER_VALIDATE_REGEXP', + 'FILTER_VALIDATE_DOMAIN', + 'FILTER_VALIDATE_URL', + 'FILTER_VALIDATE_EMAIL', + 'FILTER_VALIDATE_IP', + 'FILTER_VALIDATE_MAC', + 'FILTER_SANITIZE_STRING', + 'FILTER_SANITIZE_STRIPPED', + 'FILTER_SANITIZE_ENCODED', + 'FILTER_SANITIZE_SPECIAL_CHARS', + 'FILTER_SANITIZE_FULL_SPECIAL_CHARS', + 'FILTER_SANITIZE_EMAIL', + 'FILTER_SANITIZE_URL', + 'FILTER_SANITIZE_NUMBER_INT', + 'FILTER_SANITIZE_NUMBER_FLOAT', + 'FILTER_SANITIZE_ADD_SLASHES', + 'FILTER_UNSAFE_RAW', + 'FILTER_DEFAULT', + 'FILTER_CALLBACK', + ], + ], + ], + + 'filter_input' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'INPUT_POST', + 'INPUT_GET', + 'INPUT_COOKIE', + 'INPUT_ENV', + 'INPUT_SERVER', + ], + ], + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'FILTER_VALIDATE_INT', + 'FILTER_VALIDATE_BOOLEAN', + 'FILTER_VALIDATE_FLOAT', + 'FILTER_VALIDATE_REGEXP', + 'FILTER_VALIDATE_DOMAIN', + 'FILTER_VALIDATE_URL', + 'FILTER_VALIDATE_EMAIL', + 'FILTER_VALIDATE_IP', + 'FILTER_VALIDATE_MAC', + 'FILTER_SANITIZE_STRING', + 'FILTER_SANITIZE_STRIPPED', + 'FILTER_SANITIZE_ENCODED', + 'FILTER_SANITIZE_SPECIAL_CHARS', + 'FILTER_SANITIZE_FULL_SPECIAL_CHARS', + 'FILTER_SANITIZE_EMAIL', + 'FILTER_SANITIZE_URL', + 'FILTER_SANITIZE_NUMBER_INT', + 'FILTER_SANITIZE_NUMBER_FLOAT', + 'FILTER_SANITIZE_ADD_SLASHES', + 'FILTER_UNSAFE_RAW', + 'FILTER_DEFAULT', + 'FILTER_CALLBACK', + ], + ], + ], + + 'filter_input_array' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'INPUT_POST', + 'INPUT_GET', + 'INPUT_COOKIE', + 'INPUT_ENV', + 'INPUT_SERVER', + ], + ], + ], + + // ———————————————————————————————————————————— + // Password hashing + // ———————————————————————————————————————————— + + 'password_hash' => [ + 'algo' => [ + 'type' => 'single', + 'constants' => [ + 'PASSWORD_DEFAULT', + 'PASSWORD_BCRYPT', + 'PASSWORD_ARGON2I', + 'PASSWORD_ARGON2ID', + ], + ], + ], + + 'password_needs_rehash' => [ + 'algo' => [ + 'type' => 'single', + 'constants' => [ + 'PASSWORD_DEFAULT', + 'PASSWORD_BCRYPT', + 'PASSWORD_ARGON2I', + 'PASSWORD_ARGON2ID', + ], + ], + ], + + // ———————————————————————————————————————————— + // Error handling + // ———————————————————————————————————————————— + + 'error_reporting' => [ + 'error_level' => [ + 'type' => 'bitmask', + 'constants' => [ + 'E_ALL', + 'E_ERROR', + 'E_WARNING', + 'E_PARSE', + 'E_NOTICE', + 'E_STRICT', + 'E_RECOVERABLE_ERROR', + 'E_DEPRECATED', + 'E_CORE_ERROR', + 'E_CORE_WARNING', + 'E_COMPILE_ERROR', + 'E_COMPILE_WARNING', + 'E_USER_ERROR', + 'E_USER_WARNING', + 'E_USER_NOTICE', + 'E_USER_DEPRECATED', + ], + ], + ], + + 'trigger_error' => [ + 'error_level' => [ + 'type' => 'single', + 'constants' => [ + 'E_USER_NOTICE', + 'E_USER_WARNING', + 'E_USER_ERROR', + 'E_USER_DEPRECATED', + ], + ], + ], + + 'user_error' => [ + 'error_level' => [ + 'type' => 'single', + 'constants' => [ + 'E_USER_NOTICE', + 'E_USER_WARNING', + 'E_USER_ERROR', + 'E_USER_DEPRECATED', + ], + ], + ], + + // ———————————————————————————————————————————— + // Multibyte string + // ———————————————————————————————————————————— + + 'mb_convert_case' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'MB_CASE_UPPER', + 'MB_CASE_LOWER', + 'MB_CASE_TITLE', + 'MB_CASE_FOLD', + 'MB_CASE_UPPER_SIMPLE', + 'MB_CASE_LOWER_SIMPLE', + 'MB_CASE_TITLE_SIMPLE', + 'MB_CASE_FOLD_SIMPLE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Fileinfo + // ———————————————————————————————————————————— + + 'finfo_file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + // ———————————————————————————————————————————— + // Debug + // ———————————————————————————————————————————— + + 'debug_backtrace' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'DEBUG_BACKTRACE_PROVIDE_OBJECT', + 'DEBUG_BACKTRACE_IGNORE_ARGS', + ], + ], + ], + + 'debug_print_backtrace' => [ + 'options' => [ + 'type' => 'single', + 'constants' => [ + 'DEBUG_BACKTRACE_IGNORE_ARGS', + ], + ], + ], + + // ———————————————————————————————————————————— + // Tokenizer + // ———————————————————————————————————————————— + + 'token_get_all' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'TOKEN_PARSE', + ], + ], + ], + + // cURL constants are excluded from this map because the constant lists + // are large and grow with each PHP/libcurl release, making them impractical + // to maintain without false positives. + + + 'image_type_to_extension' => [ + 'image_type' => [ + 'type' => 'single', + 'constants' => [ + 'IMAGETYPE_GIF', + 'IMAGETYPE_JPEG', + 'IMAGETYPE_PNG', + 'IMAGETYPE_SWF', + 'IMAGETYPE_PSD', + 'IMAGETYPE_BMP', + 'IMAGETYPE_WBMP', + 'IMAGETYPE_XBM', + 'IMAGETYPE_TIFF_II', + 'IMAGETYPE_TIFF_MM', + 'IMAGETYPE_ICO', + 'IMAGETYPE_WEBP', + 'IMAGETYPE_AVIF', + 'IMAGETYPE_JPC', + 'IMAGETYPE_JP2', + 'IMAGETYPE_JPX', + 'IMAGETYPE_JB2', + 'IMAGETYPE_SWC', + 'IMAGETYPE_IFF', + ], + ], + ], + + 'image_type_to_mime_type' => [ + 'image_type' => [ + 'type' => 'single', + 'constants' => [ + 'IMAGETYPE_GIF', + 'IMAGETYPE_JPEG', + 'IMAGETYPE_PNG', + 'IMAGETYPE_SWF', + 'IMAGETYPE_PSD', + 'IMAGETYPE_BMP', + 'IMAGETYPE_WBMP', + 'IMAGETYPE_XBM', + 'IMAGETYPE_TIFF_II', + 'IMAGETYPE_TIFF_MM', + 'IMAGETYPE_ICO', + 'IMAGETYPE_WEBP', + 'IMAGETYPE_AVIF', + 'IMAGETYPE_JPC', + 'IMAGETYPE_JP2', + 'IMAGETYPE_JPX', + 'IMAGETYPE_JB2', + 'IMAGETYPE_SWC', + 'IMAGETYPE_IFF', + ], + ], + ], + + // ———————————————————————————————————————————— + // GD image functions + // ———————————————————————————————————————————— + + 'imagecropauto' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_CROP_DEFAULT', + 'IMG_CROP_TRANSPARENT', + 'IMG_CROP_BLACK', + 'IMG_CROP_WHITE', + 'IMG_CROP_SIDES', + 'IMG_CROP_THRESHOLD', + ], + ], + ], + + 'imagelayereffect' => [ + 'effect' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_EFFECT_REPLACE', + 'IMG_EFFECT_ALPHABLEND', + 'IMG_EFFECT_NORMAL', + 'IMG_EFFECT_OVERLAY', + 'IMG_EFFECT_MULTIPLY', + ], + ], + ], + + 'imageflip' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_FLIP_HORIZONTAL', + 'IMG_FLIP_VERTICAL', + 'IMG_FLIP_BOTH', + ], + ], + ], + + 'imagefilter' => [ + 'filter' => [ + 'type' => 'single', + 'constants' => [ + 'IMG_FILTER_NEGATE', + 'IMG_FILTER_GRAYSCALE', + 'IMG_FILTER_BRIGHTNESS', + 'IMG_FILTER_CONTRAST', + 'IMG_FILTER_COLORIZE', + 'IMG_FILTER_EDGEDETECT', + 'IMG_FILTER_GAUSSIAN_BLUR', + 'IMG_FILTER_SELECTIVE_BLUR', + 'IMG_FILTER_EMBOSS', + 'IMG_FILTER_MEAN_REMOVAL', + 'IMG_FILTER_SMOOTH', + 'IMG_FILTER_PIXELATE', + 'IMG_FILTER_SCATTER', + ], + ], + ], + + // ———————————————————————————————————————————— + // Iconv + // ———————————————————————————————————————————— + + 'iconv_mime_decode' => [ + 'mode' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ICONV_MIME_DECODE_STRICT', + 'ICONV_MIME_DECODE_CONTINUE_ON_ERROR', + ], + ], + ], + + 'iconv_mime_decode_headers' => [ + 'mode' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ICONV_MIME_DECODE_STRICT', + 'ICONV_MIME_DECODE_CONTINUE_ON_ERROR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Output buffering + // ———————————————————————————————————————————— + + 'ob_start' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PHP_OUTPUT_HANDLER_CLEANABLE', + 'PHP_OUTPUT_HANDLER_FLUSHABLE', + 'PHP_OUTPUT_HANDLER_REMOVABLE', + 'PHP_OUTPUT_HANDLER_STDFLAGS', + ], + ], + ], + + // ———————————————————————————————————————————— + // Streams + // ———————————————————————————————————————————— + + 'stream_socket_client' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_CLIENT_CONNECT', + 'STREAM_CLIENT_ASYNC_CONNECT', + 'STREAM_CLIENT_PERSISTENT', + ], + ], + ], + + 'stream_socket_server' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_SERVER_BIND', + 'STREAM_SERVER_LISTEN', + ], + ], + ], + + 'stream_socket_recvfrom' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_OOB', + 'STREAM_PEEK', + ], + ], + ], + + 'stream_socket_sendto' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'STREAM_OOB', + ], + ], + ], + + 'stream_wrapper_register' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'STREAM_IS_URL', + ], + ], + ], + + 'stream_socket_shutdown' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'STREAM_SHUT_RD', + 'STREAM_SHUT_WR', + 'STREAM_SHUT_RDWR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Syslog + // ———————————————————————————————————————————— + + 'openlog' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOG_CONS', + 'LOG_NDELAY', + 'LOG_ODELAY', + 'LOG_NOWAIT', + 'LOG_PERROR', + 'LOG_PID', + ], + ], + 'facility' => [ + 'type' => 'single', + 'constants' => [ + 'LOG_AUTH', + 'LOG_AUTHPRIV', + 'LOG_CRON', + 'LOG_DAEMON', + 'LOG_KERN', + 'LOG_LOCAL0', + 'LOG_LOCAL1', + 'LOG_LOCAL2', + 'LOG_LOCAL3', + 'LOG_LOCAL4', + 'LOG_LOCAL5', + 'LOG_LOCAL6', + 'LOG_LOCAL7', + 'LOG_LPR', + 'LOG_MAIL', + 'LOG_NEWS', + 'LOG_SYSLOG', + 'LOG_USER', + 'LOG_UUCP', + ], + ], + ], + + 'syslog' => [ + 'priority' => [ + 'type' => 'single', + 'constants' => [ + 'LOG_EMERG', + 'LOG_ALERT', + 'LOG_CRIT', + 'LOG_ERR', + 'LOG_WARNING', + 'LOG_NOTICE', + 'LOG_INFO', + 'LOG_DEBUG', + ], + ], + ], + + // ———————————————————————————————————————————— + // Sockets + // ———————————————————————————————————————————— + + 'socket_recv' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_PEEK', + 'MSG_WAITALL', + 'MSG_DONTWAIT', + ], + ], + ], + + 'socket_recvfrom' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_PEEK', + 'MSG_WAITALL', + 'MSG_DONTWAIT', + ], + ], + ], + + 'socket_send' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_EOR', + 'MSG_EOF', + 'MSG_DONTROUTE', + ], + ], + ], + + 'socket_sendto' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_OOB', + 'MSG_EOR', + 'MSG_EOF', + 'MSG_DONTROUTE', + ], + ], + ], + + // ———————————————————————————————————————————— + // DNS + // ———————————————————————————————————————————— + + 'dns_get_record' => [ + 'type' => [ + 'type' => 'bitmask', + 'constants' => [ + 'DNS_ANY', + 'DNS_ALL', + 'DNS_A', + 'DNS_AAAA', + 'DNS_CNAME', + 'DNS_HINFO', + 'DNS_MX', + 'DNS_NS', + 'DNS_PTR', + 'DNS_SOA', + 'DNS_SRV', + 'DNS_TXT', + 'DNS_NAPTR', + 'DNS_A6', + 'DNS_CAA', + ], + ], + ], + + // ———————————————————————————————————————————— + // FTP + // ———————————————————————————————————————————— + + 'ftp_get' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_fget' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_put' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + 'ftp_fput' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'FTP_ASCII', + 'FTP_BINARY', + ], + ], + ], + + // ———————————————————————————————————————————— + // IMAP + // ———————————————————————————————————————————— + + 'imap_close' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'CL_EXPUNGE', + ], + ], + ], + + // ———————————————————————————————————————————— + // OpenSSL + // ———————————————————————————————————————————— + + 'openssl_pkcs7_verify' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + ], + + 'openssl_pkcs7_sign' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + ], + + 'openssl_pkcs7_encrypt' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'PKCS7_TEXT', + 'PKCS7_BINARY', + 'PKCS7_NOINTERN', + 'PKCS7_NOVERIFY', + 'PKCS7_NOCHAIN', + 'PKCS7_NOCERTS', + 'PKCS7_NOATTR', + 'PKCS7_DETACHED', + 'PKCS7_NOSIGS', + ], + ], + 'cipher_algo' => [ + 'type' => 'single', + 'constants' => [ + 'OPENSSL_CIPHER_RC2_40', + 'OPENSSL_CIPHER_RC2_128', + 'OPENSSL_CIPHER_RC2_64', + 'OPENSSL_CIPHER_DES', + 'OPENSSL_CIPHER_3DES', + 'OPENSSL_CIPHER_AES_128_CBC', + 'OPENSSL_CIPHER_AES_192_CBC', + 'OPENSSL_CIPHER_AES_256_CBC', + ], + ], + ], + + // ———————————————————————————————————————————— + // IDN + // ———————————————————————————————————————————— + + 'idn_to_ascii' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'IDNA_DEFAULT', + 'IDNA_ALLOW_UNASSIGNED', + 'IDNA_CHECK_BIDI', + 'IDNA_CHECK_CONTEXTJ', + 'IDNA_NONTRANSITIONAL_TO_ASCII', + 'IDNA_NONTRANSITIONAL_TO_UNICODE', + 'IDNA_USE_STD3_RULES', + ], + ], + 'variant' => [ + 'type' => 'single', + 'constants' => [ + 'INTL_IDNA_VARIANT_UTS46', + 'INTL_IDNA_VARIANT_2003', + ], + ], + ], + + 'idn_to_utf8' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'IDNA_DEFAULT', + 'IDNA_ALLOW_UNASSIGNED', + 'IDNA_CHECK_BIDI', + 'IDNA_CHECK_CONTEXTJ', + 'IDNA_NONTRANSITIONAL_TO_ASCII', + 'IDNA_NONTRANSITIONAL_TO_UNICODE', + 'IDNA_USE_STD3_RULES', + ], + ], + 'variant' => [ + 'type' => 'single', + 'constants' => [ + 'INTL_IDNA_VARIANT_UTS46', + 'INTL_IDNA_VARIANT_2003', + ], + ], + ], + + // ———————————————————————————————————————————— + // String functions + // ———————————————————————————————————————————— + + 'str_pad' => [ + 'pad_type' => [ + 'type' => 'single', + 'constants' => [ + 'STR_PAD_RIGHT', + 'STR_PAD_LEFT', + 'STR_PAD_BOTH', + ], + ], + ], + + // ———————————————————————————————————————————— + // File seeking + // ———————————————————————————————————————————— + + 'fseek' => [ + 'whence' => [ + 'type' => 'single', + 'constants' => [ + 'SEEK_SET', + 'SEEK_CUR', + 'SEEK_END', + ], + ], + ], + + // ———————————————————————————————————————————— + // INI parsing + // ———————————————————————————————————————————— + + 'parse_ini_file' => [ + 'scanner_mode' => [ + 'type' => 'single', + 'constants' => [ + 'INI_SCANNER_NORMAL', + 'INI_SCANNER_RAW', + 'INI_SCANNER_TYPED', + ], + ], + ], + + 'parse_ini_string' => [ + 'scanner_mode' => [ + 'type' => 'single', + 'constants' => [ + 'INI_SCANNER_NORMAL', + 'INI_SCANNER_RAW', + 'INI_SCANNER_TYPED', + ], + ], + ], + + // ———————————————————————————————————————————— + // Message queues + // ———————————————————————————————————————————— + + 'msg_receive' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MSG_IPC_NOWAIT', + 'MSG_EXCEPT', + 'MSG_NOERROR', + ], + ], + ], + + // ———————————————————————————————————————————— + // Locale + // ———————————————————————————————————————————— + + 'setlocale' => [ + 'category' => [ + 'type' => 'single', + 'constants' => [ + 'LC_CTYPE', + 'LC_NUMERIC', + 'LC_TIME', + 'LC_COLLATE', + 'LC_MONETARY', + 'LC_MESSAGES', + 'LC_ALL', + ], + ], + ], + + // ———————————————————————————————————————————— + // libxml (functions) + // ———————————————————————————————————————————— + + 'simplexml_load_file' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'simplexml_load_string' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + // ———————————————————————————————————————————— + // mysqli (functions) + // ———————————————————————————————————————————— + + 'mysqli_begin_transaction' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_START_READ_ONLY', + 'MYSQLI_TRANS_START_READ_WRITE', + 'MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_START_READ_ONLY', 'MYSQLI_TRANS_START_READ_WRITE'], + ], + ], + ], + + 'mysqli_commit' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + 'mysqli_rollback' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + // ———————————————————————————————————————————— + // Methods with global constants + // ———————————————————————————————————————————— + + // finfo methods (FILEINFO_* global constants) + + 'finfo::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::file' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::buffer' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + 'finfo::set_flags' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILEINFO_NONE', + 'FILEINFO_SYMLINK', + 'FILEINFO_MIME', + 'FILEINFO_MIME_TYPE', + 'FILEINFO_MIME_ENCODING', + 'FILEINFO_DEVICES', + 'FILEINFO_CONTINUE', + 'FILEINFO_PRESERVE_ATIME', + 'FILEINFO_RAW', + 'FILEINFO_EXTENSION', + 'FILEINFO_APPLE', + ], + ], + ], + + // SplFileObject methods (global constants) + + 'SplFileObject::flock' => [ + 'operation' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LOCK_SH', + 'LOCK_EX', + 'LOCK_UN', + 'LOCK_NB', + ], + 'exclusiveGroups' => [ + ['LOCK_SH', 'LOCK_EX', 'LOCK_UN'], + ], + ], + ], + + 'SplFileObject::fseek' => [ + 'whence' => [ + 'type' => 'single', + 'constants' => [ + 'SEEK_SET', + 'SEEK_CUR', + 'SEEK_END', + ], + ], + ], + + // DOMDocument methods (LIBXML_* global constants) + + 'DOMDocument::load' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'DOMDocument::loadXML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'DOMDocument::loadHTML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + 'LIBXML_HTML_NOIMPLIED', + 'LIBXML_HTML_NODEFDTD', + ], + ], + ], + + 'DOMDocument::loadHTMLFile' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + 'LIBXML_HTML_NOIMPLIED', + 'LIBXML_HTML_NODEFDTD', + ], + ], + ], + + 'DOMDocument::save' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOEMPTYTAG', + ], + ], + ], + + 'DOMDocument::saveXML' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOEMPTYTAG', + ], + ], + ], + + 'DOMDocument::schemaValidate' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_SCHEMA_CREATE', + ], + ], + ], + + 'DOMDocument::schemaValidateSource' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_SCHEMA_CREATE', + ], + ], + ], + + // XMLReader methods (LIBXML_* global constants) + + 'XMLReader::open' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + 'XMLReader::XML' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'LIBXML_NOENT', + 'LIBXML_DTDLOAD', + 'LIBXML_DTDATTR', + 'LIBXML_DTDVALID', + 'LIBXML_NOERROR', + 'LIBXML_NOWARNING', + 'LIBXML_NOBLANKS', + 'LIBXML_XINCLUDE', + 'LIBXML_NSCLEAN', + 'LIBXML_NOCDATA', + 'LIBXML_NONET', + 'LIBXML_PEDANTIC', + 'LIBXML_COMPACT', + 'LIBXML_PARSEHUGE', + 'LIBXML_BIGLINES', + ], + ], + ], + + // mysqli methods (global constants) + + 'mysqli::begin_transaction' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_START_READ_ONLY', + 'MYSQLI_TRANS_START_READ_WRITE', + 'MYSQLI_TRANS_START_WITH_CONSISTENT_SNAPSHOT', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_START_READ_ONLY', 'MYSQLI_TRANS_START_READ_WRITE'], + ], + ], + ], + + 'mysqli::commit' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + 'mysqli::rollback' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'MYSQLI_TRANS_COR_AND_CHAIN', + 'MYSQLI_TRANS_COR_AND_NO_CHAIN', + 'MYSQLI_TRANS_COR_RELEASE', + 'MYSQLI_TRANS_COR_NO_RELEASE', + ], + 'exclusiveGroups' => [ + ['MYSQLI_TRANS_COR_AND_CHAIN', 'MYSQLI_TRANS_COR_AND_NO_CHAIN'], + ['MYSQLI_TRANS_COR_RELEASE', 'MYSQLI_TRANS_COR_NO_RELEASE'], + ], + ], + ], + + // Collator methods (class constants) + + 'Collator::sort' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::SORT_REGULAR', + 'Collator::SORT_STRING', + 'Collator::SORT_NUMERIC', + ], + ], + ], + + 'Collator::asort' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::SORT_REGULAR', + 'Collator::SORT_STRING', + 'Collator::SORT_NUMERIC', + ], + ], + ], + + 'Collator::setAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::FRENCH_COLLATION', + 'Collator::ALTERNATE_HANDLING', + 'Collator::CASE_FIRST', + 'Collator::CASE_LEVEL', + 'Collator::NORMALIZATION_MODE', + 'Collator::STRENGTH', + 'Collator::HIRAGANA_QUATERNARY_MODE', + 'Collator::NUMERIC_COLLATION', + ], + ], + ], + + 'Collator::getAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'Collator::FRENCH_COLLATION', + 'Collator::ALTERNATE_HANDLING', + 'Collator::CASE_FIRST', + 'Collator::CASE_LEVEL', + 'Collator::NORMALIZATION_MODE', + 'Collator::STRENGTH', + 'Collator::HIRAGANA_QUATERNARY_MODE', + 'Collator::NUMERIC_COLLATION', + ], + ], + ], + + // ———————————————————————————————————————————— + // Methods with class constants + // ———————————————————————————————————————————— + + // PDO::setAttribute/getAttribute are excluded because PDO drivers add + // their own attribute constants (PGSQL_ATTR_*, MYSQL_ATTR_*, etc.) + + // PDOStatement + + 'PDOStatement::fetch' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + 'cursorOrientation' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_ORI_NEXT', + 'PDO::FETCH_ORI_PRIOR', + 'PDO::FETCH_ORI_FIRST', + 'PDO::FETCH_ORI_LAST', + 'PDO::FETCH_ORI_ABS', + 'PDO::FETCH_ORI_REL', + ], + ], + ], + + 'PDOStatement::fetchAll' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + ], + + 'PDOStatement::setFetchMode' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::FETCH_DEFAULT', + 'PDO::FETCH_LAZY', + 'PDO::FETCH_ASSOC', + 'PDO::FETCH_NUM', + 'PDO::FETCH_BOTH', + 'PDO::FETCH_OBJ', + 'PDO::FETCH_BOUND', + 'PDO::FETCH_COLUMN', + 'PDO::FETCH_CLASS', + 'PDO::FETCH_INTO', + 'PDO::FETCH_FUNC', + 'PDO::FETCH_GROUP', + 'PDO::FETCH_UNIQUE', + 'PDO::FETCH_KEY_PAIR', + 'PDO::FETCH_CLASSTYPE', + 'PDO::FETCH_SERIALIZE', + 'PDO::FETCH_PROPS_LATE', + 'PDO::FETCH_NAMED', + ], + ], + ], + + 'PDOStatement::bindColumn' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + 'PDOStatement::bindParam' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + 'PDOStatement::bindValue' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'PDO::PARAM_NULL', + 'PDO::PARAM_BOOL', + 'PDO::PARAM_INT', + 'PDO::PARAM_STR', + 'PDO::PARAM_LOB', + 'PDO::PARAM_STMT', + 'PDO::PARAM_INPUT_OUTPUT', + 'PDO::PARAM_STR_NATL', + 'PDO::PARAM_STR_CHAR', + ], + ], + ], + + // ZipArchive + + 'ZipArchive::open' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'ZipArchive::CREATE', + 'ZipArchive::EXCL', + 'ZipArchive::CHECKCONS', + 'ZipArchive::OVERWRITE', + 'ZipArchive::RDONLY', + ], + ], + ], + + 'ZipArchive::setCompressionName' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::CM_DEFAULT', + 'ZipArchive::CM_STORE', + 'ZipArchive::CM_SHRINK', + 'ZipArchive::CM_REDUCE_1', + 'ZipArchive::CM_REDUCE_2', + 'ZipArchive::CM_REDUCE_3', + 'ZipArchive::CM_REDUCE_4', + 'ZipArchive::CM_IMPLODE', + 'ZipArchive::CM_DEFLATE', + 'ZipArchive::CM_DEFLATE64', + 'ZipArchive::CM_PKWARE_IMPLODE', + 'ZipArchive::CM_BZIP2', + 'ZipArchive::CM_LZMA', + 'ZipArchive::CM_LZMA2', + 'ZipArchive::CM_ZSTD', + 'ZipArchive::CM_XZ', + ], + ], + ], + + 'ZipArchive::setCompressionIndex' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::CM_DEFAULT', + 'ZipArchive::CM_STORE', + 'ZipArchive::CM_SHRINK', + 'ZipArchive::CM_REDUCE_1', + 'ZipArchive::CM_REDUCE_2', + 'ZipArchive::CM_REDUCE_3', + 'ZipArchive::CM_REDUCE_4', + 'ZipArchive::CM_IMPLODE', + 'ZipArchive::CM_DEFLATE', + 'ZipArchive::CM_DEFLATE64', + 'ZipArchive::CM_PKWARE_IMPLODE', + 'ZipArchive::CM_BZIP2', + 'ZipArchive::CM_LZMA', + 'ZipArchive::CM_LZMA2', + 'ZipArchive::CM_ZSTD', + 'ZipArchive::CM_XZ', + ], + ], + ], + + 'ZipArchive::setEncryptionName' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::EM_NONE', + 'ZipArchive::EM_TRAD_PKWARE', + 'ZipArchive::EM_AES_128', + 'ZipArchive::EM_AES_192', + 'ZipArchive::EM_AES_256', + ], + ], + ], + + 'ZipArchive::setEncryptionIndex' => [ + 'method' => [ + 'type' => 'single', + 'constants' => [ + 'ZipArchive::EM_NONE', + 'ZipArchive::EM_TRAD_PKWARE', + 'ZipArchive::EM_AES_128', + 'ZipArchive::EM_AES_192', + 'ZipArchive::EM_AES_256', + ], + ], + ], + + // IntlDateFormatter + + 'IntlDateFormatter::__construct' => [ + 'dateType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + 'timeType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + ], + + 'IntlDateFormatter::create' => [ + 'dateType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + 'timeType' => [ + 'type' => 'single', + 'constants' => [ + 'IntlDateFormatter::FULL', + 'IntlDateFormatter::LONG', + 'IntlDateFormatter::MEDIUM', + 'IntlDateFormatter::SHORT', + 'IntlDateFormatter::NONE', + 'IntlDateFormatter::RELATIVE_FULL', + 'IntlDateFormatter::RELATIVE_LONG', + 'IntlDateFormatter::RELATIVE_MEDIUM', + 'IntlDateFormatter::RELATIVE_SHORT', + ], + ], + ], + + // NumberFormatter + + 'NumberFormatter::__construct' => [ + 'style' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PATTERN_DECIMAL', + 'NumberFormatter::DECIMAL', + 'NumberFormatter::CURRENCY', + 'NumberFormatter::PERCENT', + 'NumberFormatter::SCIENTIFIC', + 'NumberFormatter::SPELLOUT', + 'NumberFormatter::ORDINAL', + 'NumberFormatter::DURATION', + 'NumberFormatter::PATTERN_RULEBASED', + 'NumberFormatter::IGNORE', + 'NumberFormatter::CURRENCY_ACCOUNTING', + 'NumberFormatter::DEFAULT_STYLE', + ], + ], + ], + + 'NumberFormatter::create' => [ + 'style' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PATTERN_DECIMAL', + 'NumberFormatter::DECIMAL', + 'NumberFormatter::CURRENCY', + 'NumberFormatter::PERCENT', + 'NumberFormatter::SCIENTIFIC', + 'NumberFormatter::SPELLOUT', + 'NumberFormatter::ORDINAL', + 'NumberFormatter::DURATION', + 'NumberFormatter::PATTERN_RULEBASED', + 'NumberFormatter::IGNORE', + 'NumberFormatter::CURRENCY_ACCOUNTING', + 'NumberFormatter::DEFAULT_STYLE', + ], + ], + ], + + 'NumberFormatter::format' => [ + 'type' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::TYPE_DEFAULT', + 'NumberFormatter::TYPE_INT32', + 'NumberFormatter::TYPE_INT64', + 'NumberFormatter::TYPE_DOUBLE', + 'NumberFormatter::TYPE_CURRENCY', + ], + ], + ], + + 'NumberFormatter::setAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PARSE_INT_ONLY', + 'NumberFormatter::GROUPING_USED', + 'NumberFormatter::DECIMAL_ALWAYS_SHOWN', + 'NumberFormatter::MAX_INTEGER_DIGITS', + 'NumberFormatter::MIN_INTEGER_DIGITS', + 'NumberFormatter::INTEGER_DIGITS', + 'NumberFormatter::MAX_FRACTION_DIGITS', + 'NumberFormatter::MIN_FRACTION_DIGITS', + 'NumberFormatter::FRACTION_DIGITS', + 'NumberFormatter::MULTIPLIER', + 'NumberFormatter::GROUPING_SIZE', + 'NumberFormatter::ROUNDING_MODE', + 'NumberFormatter::ROUNDING_INCREMENT', + 'NumberFormatter::FORMAT_WIDTH', + 'NumberFormatter::PADDING_POSITION', + 'NumberFormatter::SECONDARY_GROUPING_SIZE', + 'NumberFormatter::SIGNIFICANT_DIGITS_USED', + 'NumberFormatter::MIN_SIGNIFICANT_DIGITS', + 'NumberFormatter::MAX_SIGNIFICANT_DIGITS', + 'NumberFormatter::LENIENT_PARSE', + ], + ], + ], + + 'NumberFormatter::getAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::PARSE_INT_ONLY', + 'NumberFormatter::GROUPING_USED', + 'NumberFormatter::DECIMAL_ALWAYS_SHOWN', + 'NumberFormatter::MAX_INTEGER_DIGITS', + 'NumberFormatter::MIN_INTEGER_DIGITS', + 'NumberFormatter::INTEGER_DIGITS', + 'NumberFormatter::MAX_FRACTION_DIGITS', + 'NumberFormatter::MIN_FRACTION_DIGITS', + 'NumberFormatter::FRACTION_DIGITS', + 'NumberFormatter::MULTIPLIER', + 'NumberFormatter::GROUPING_SIZE', + 'NumberFormatter::ROUNDING_MODE', + 'NumberFormatter::ROUNDING_INCREMENT', + 'NumberFormatter::FORMAT_WIDTH', + 'NumberFormatter::PADDING_POSITION', + 'NumberFormatter::SECONDARY_GROUPING_SIZE', + 'NumberFormatter::SIGNIFICANT_DIGITS_USED', + 'NumberFormatter::MIN_SIGNIFICANT_DIGITS', + 'NumberFormatter::MAX_SIGNIFICANT_DIGITS', + 'NumberFormatter::LENIENT_PARSE', + ], + ], + ], + + 'NumberFormatter::setTextAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::POSITIVE_PREFIX', + 'NumberFormatter::POSITIVE_SUFFIX', + 'NumberFormatter::NEGATIVE_PREFIX', + 'NumberFormatter::NEGATIVE_SUFFIX', + 'NumberFormatter::PADDING_CHARACTER', + 'NumberFormatter::CURRENCY_CODE', + 'NumberFormatter::DEFAULT_RULESET', + 'NumberFormatter::PUBLIC_RULESETS', + ], + ], + ], + + 'NumberFormatter::getTextAttribute' => [ + 'attribute' => [ + 'type' => 'single', + 'constants' => [ + 'NumberFormatter::POSITIVE_PREFIX', + 'NumberFormatter::POSITIVE_SUFFIX', + 'NumberFormatter::NEGATIVE_PREFIX', + 'NumberFormatter::NEGATIVE_SUFFIX', + 'NumberFormatter::PADDING_CHARACTER', + 'NumberFormatter::CURRENCY_CODE', + 'NumberFormatter::DEFAULT_RULESET', + 'NumberFormatter::PUBLIC_RULESETS', + ], + ], + ], + + // SplPriorityQueue + + 'SplPriorityQueue::setExtractFlags' => [ + 'flags' => [ + 'type' => 'single', + 'constants' => [ + 'SplPriorityQueue::EXTR_BOTH', + 'SplPriorityQueue::EXTR_PRIORITY', + 'SplPriorityQueue::EXTR_DATA', + ], + ], + ], + + // FilesystemIterator / GlobIterator / RecursiveDirectoryIterator + + 'FilesystemIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'FilesystemIterator::setFlags' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'GlobIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], + + 'RecursiveDirectoryIterator::__construct' => [ + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FilesystemIterator::CURRENT_AS_PATHNAME', + 'FilesystemIterator::CURRENT_AS_FILEINFO', + 'FilesystemIterator::CURRENT_AS_SELF', + 'FilesystemIterator::KEY_AS_PATHNAME', + 'FilesystemIterator::KEY_AS_FILENAME', + 'FilesystemIterator::FOLLOW_SYMLINKS', + 'FilesystemIterator::NEW_CURRENT_AND_KEY', + 'FilesystemIterator::SKIP_DOTS', + 'FilesystemIterator::UNIX_PATHS', + ], + 'exclusiveGroups' => [ + ['FilesystemIterator::CURRENT_AS_PATHNAME', 'FilesystemIterator::CURRENT_AS_FILEINFO', 'FilesystemIterator::CURRENT_AS_SELF'], + ['FilesystemIterator::KEY_AS_PATHNAME', 'FilesystemIterator::KEY_AS_FILENAME'], + ], + ], + ], +]; diff --git a/src/Reflection/AllowedConstantsResult.php b/src/Reflection/AllowedConstantsResult.php new file mode 100644 index 00000000000..8dd9f315a4d --- /dev/null +++ b/src/Reflection/AllowedConstantsResult.php @@ -0,0 +1,55 @@ + $disallowedConstants + * @param list> $violatedExclusiveGroups + */ + public function __construct( + private array $disallowedConstants, + private array $violatedExclusiveGroups, + private bool $bitmaskNotAllowed, + ) + { + } + + public function isOk(): bool + { + return $this->disallowedConstants === [] && $this->violatedExclusiveGroups === [] && !$this->bitmaskNotAllowed; + } + + public function isBitmaskNotAllowed(): bool + { + return $this->bitmaskNotAllowed; + } + + /** + * @return list + */ + public function getDisallowedConstants(): array + { + return $this->disallowedConstants; + } + + /** + * @return list> + */ + public function getViolatedExclusiveGroups(): array + { + return $this->violatedExclusiveGroups; + } + +} diff --git a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php index b01a6db6ff8..93941cf0698 100644 --- a/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php +++ b/src/Reflection/Annotations/AnnotationsMethodParameterReflection.php @@ -2,7 +2,9 @@ namespace PHPStan\Reflection\Annotations; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -80,4 +82,14 @@ public function getAttributes(): array return []; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return null; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + return new AllowedConstantsResult([], [], false); + } + } diff --git a/src/Reflection/ExtendedParameterReflection.php b/src/Reflection/ExtendedParameterReflection.php index 890b0493469..1ccd1d4b935 100644 --- a/src/Reflection/ExtendedParameterReflection.php +++ b/src/Reflection/ExtendedParameterReflection.php @@ -29,4 +29,11 @@ public function getClosureThisType(): ?Type; */ public function getAttributes(): array; + public function getAllowedConstants(): ?ParameterAllowedConstants; + + /** + * @param list $constants Global and/or class constant reflections + */ + public function checkAllowedConstants(array $constants): AllowedConstantsResult; + } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index d9b75bf3e0e..9698b42ea8f 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -105,6 +105,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc TrinaryLogic::createMaybe(), null, [], + null, ), $parameters), $parametersAcceptor->isVariadic(), $returnType, diff --git a/src/Reflection/Native/ExtendedNativeParameterReflection.php b/src/Reflection/Native/ExtendedNativeParameterReflection.php index 00e2ea1a99e..5539d9132a1 100644 --- a/src/Reflection/Native/ExtendedNativeParameterReflection.php +++ b/src/Reflection/Native/ExtendedNativeParameterReflection.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Native; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -28,6 +30,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { } @@ -97,4 +100,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/ParameterAllowedConstants.php b/src/Reflection/ParameterAllowedConstants.php new file mode 100644 index 00000000000..2f844e28a82 --- /dev/null +++ b/src/Reflection/ParameterAllowedConstants.php @@ -0,0 +1,103 @@ + $constants + * @param list> $exclusiveGroups + */ + public function __construct( + private string $type, + private array $constants, + private array $exclusiveGroups, + ) + { + } + + public function isBitmask(): bool + { + return $this->type === 'bitmask'; + } + + /** + * @return list> + */ + public function getExclusiveGroups(): array + { + return $this->exclusiveGroups; + } + + private function resolveConstantName(ConstantReflection $constant): string + { + if ($constant instanceof ClassConstantReflection) { + return $constant->getDeclaringClass()->getName() . '::' . $constant->getName(); + } + + return $constant->getName(); + } + + /** + * @param list $constants + */ + public function check(array $constants): AllowedConstantsResult + { + $bitmaskNotAllowed = !$this->isBitmask() && count($constants) > 1; + + $disallowed = []; + $names = []; + + foreach ($constants as $constant) { + $name = $this->resolveConstantName($constant); + $names[] = $name; + + if (in_array($name, $this->constants, true)) { + continue; + } + + $disallowed[] = $constant; + } + + $violated = []; + if ($this->isBitmask()) { + foreach ($this->exclusiveGroups as $group) { + $matched = []; + foreach ($names as $name) { + if (!in_array($name, $group, true)) { + continue; + } + + $matched[] = $name; + } + + if (count($matched) < 2) { + continue; + } + + $violated[] = $matched; + } + } + + return new AllowedConstantsResult($disallowed, $violated, $bitmaskNotAllowed); + } + +} diff --git a/src/Reflection/ParameterAllowedConstantsMapProvider.php b/src/Reflection/ParameterAllowedConstantsMapProvider.php new file mode 100644 index 00000000000..22c0a9fd31e --- /dev/null +++ b/src/Reflection/ParameterAllowedConstantsMapProvider.php @@ -0,0 +1,50 @@ +, exclusiveGroups?: list>}>>|null */ + private ?array $map = null; + + public function getForFunctionParameter(string $functionName, string $parameterName): ?ParameterAllowedConstants + { + return $this->get($functionName, $parameterName); + } + + public function getForMethodParameter(string $className, string $methodName, string $parameterName): ?ParameterAllowedConstants + { + return $this->get($className . '::' . $methodName, $parameterName); + } + + private function get(string $key, string $parameterName): ?ParameterAllowedConstants + { + $map = $this->getMap(); + + if (!isset($map[$key][$parameterName])) { + return null; + } + + /** @var array{type: 'single'|'bitmask', constants: list, exclusiveGroups?: list>} $config */ + $config = $map[$key][$parameterName]; + + return new ParameterAllowedConstants( + $config['type'], + $config['constants'], + $config['exclusiveGroups'] ?? [], + ); + } + + /** + * @return array, exclusiveGroups?: list>}>> + */ + private function getMap(): array + { + return $this->map ??= require __DIR__ . '/../../resources/constantToFunctionParameterMap.php'; + } + +} diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index b4c9b3a3821..608f3fb93d1 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -771,6 +771,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $parameter instanceof ExtendedParameterReflection ? $parameter->isImmediatelyInvokedCallable() : TrinaryLogic::createMaybe(), $parameter instanceof ExtendedParameterReflection ? $parameter->getClosureThisType() : null, $parameter instanceof ExtendedParameterReflection ? $parameter->getAttributes() : [], + $parameter instanceof ExtendedParameterReflection ? $parameter->getAllowedConstants() : null, ); continue; } @@ -830,6 +831,7 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc $immediatelyInvokedCallable, $closureThisType, $attributes, + null, ); if ($isVariadic) { @@ -928,6 +930,7 @@ private static function wrapParameter(ParameterReflection $parameter): ExtendedP TrinaryLogic::createMaybe(), null, [], + null, ); } diff --git a/src/Reflection/Php/ClosureCallMethodReflection.php b/src/Reflection/Php/ClosureCallMethodReflection.php index b15ec9401b9..41c278f08dd 100644 --- a/src/Reflection/Php/ClosureCallMethodReflection.php +++ b/src/Reflection/Php/ClosureCallMethodReflection.php @@ -98,6 +98,7 @@ public function getVariants(): array TrinaryLogic::createMaybe(), null, [], + null, ), $parameters), $this->closureType->isVariadic(), $this->closureType->getReturnType(), diff --git a/src/Reflection/Php/ExitFunctionReflection.php b/src/Reflection/Php/ExitFunctionReflection.php index c4ab5219df2..79c3f6034c8 100644 --- a/src/Reflection/Php/ExitFunctionReflection.php +++ b/src/Reflection/Php/ExitFunctionReflection.php @@ -59,6 +59,7 @@ public function getVariants(): array TrinaryLogic::createNo(), null, [], + null, ), ], false, diff --git a/src/Reflection/Php/ExtendedDummyParameter.php b/src/Reflection/Php/ExtendedDummyParameter.php index 19a917e0a17..69a19ccbf3a 100644 --- a/src/Reflection/Php/ExtendedDummyParameter.php +++ b/src/Reflection/Php/ExtendedDummyParameter.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -28,6 +30,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { parent::__construct($name, $type, $optional, $passedByReference, $variadic, $defaultValue); @@ -68,4 +71,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index e471943e87c..7dd05c682c6 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -31,6 +31,7 @@ use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\SignatureMap\FunctionSignature; use PHPStan\Reflection\SignatureMap\ParameterSignature; @@ -100,6 +101,7 @@ public function __construct( private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private FileTypeMapper $fileTypeMapper, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private bool $inferPrivatePropertyTypeFromConstructor, ) { @@ -723,7 +725,7 @@ private function createMethod( } } } - $variantsByType[$signatureType][] = $this->createNativeMethodVariant($methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named'); + $variantsByType[$signatureType][] = $this->createNativeMethodVariant($declaringClassName, $methodReflection->getName(), $methodSignature, $phpDocParameterTypes, $phpDocReturnType, $phpDocParameterNameMapping, $phpDocParameterOutTypes, $immediatelyInvokedCallableParameters, $closureThisParameters, $phpDocFromStubs, $signatureType !== 'named'); } } @@ -971,6 +973,8 @@ public function createUserlandMethodReflection(ClassReflection $fileDeclaringCla * @param array $closureThisParameters */ private function createNativeMethodVariant( + string $declaringClassName, + string $methodName, FunctionSignature $methodSignature, array $phpDocParameterTypes, ?Type $phpDocReturnType, @@ -1025,6 +1029,7 @@ private function createNativeMethodVariant( $immediatelyInvoked, $closureThisType, [], + $this->allowedConstantsMapProvider->getForMethodParameter($declaringClassName, $methodName, $parameterSignature->getName()), ); } diff --git a/src/Reflection/Php/PhpFunctionReflection.php b/src/Reflection/Php/PhpFunctionReflection.php index 7dbd7ca3c47..2dcb0c7b870 100644 --- a/src/Reflection/Php/PhpFunctionReflection.php +++ b/src/Reflection/Php/PhpFunctionReflection.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\FunctionReflectionFactory; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\MixedType; @@ -44,6 +45,7 @@ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ReflectionFunction $reflection, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -127,6 +129,7 @@ private function getParameters(): array $immediatelyInvokedCallable, $this->phpDocParameterClosureThisTypes[$reflection->getName()] ?? null, $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), + $this->allowedConstantsMapProvider->getForFunctionParameter(strtolower($this->reflection->getName()), $reflection->getName()), ); }, $this->reflection->getParameters()); } diff --git a/src/Reflection/Php/PhpMethodReflection.php b/src/Reflection/Php/PhpMethodReflection.php index 34f28635007..d24532340d4 100644 --- a/src/Reflection/Php/PhpMethodReflection.php +++ b/src/Reflection/Php/PhpMethodReflection.php @@ -19,6 +19,7 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodPrototypeReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\Reflection\ReflectionProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; @@ -70,6 +71,7 @@ public function __construct( private ReflectionMethod $reflection, private ReflectionProvider $reflectionProvider, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private TemplateTypeMap $templateTypeMap, private array $phpDocParameterTypes, private ?Type $phpDocReturnType, @@ -226,6 +228,7 @@ private function getParameters(): array $this->immediatelyInvokedCallableParameters[$reflection->getName()] ?? TrinaryLogic::createMaybe(), $this->phpDocClosureThisTypeParameters[$reflection->getName()] ?? null, $this->attributeReflectionFactory->fromNativeReflection($reflection->getAttributes(), InitializerExprContext::fromReflectionParameter($reflection)), + $this->allowedConstantsMapProvider->getForMethodParameter($this->declaringClass->getName(), $this->reflection->getName(), $reflection->getName()), ), $this->reflection->getParameters()); } @@ -411,6 +414,7 @@ public function changePropertyGetHookPhpDocType(Type $phpDocType): self $this->reflection, $this->reflectionProvider, $this->attributeReflectionFactory, + $this->allowedConstantsMapProvider, $this->templateTypeMap, $this->phpDocParameterTypes, $phpDocType, @@ -444,6 +448,7 @@ public function changePropertySetHookPhpDocType(string $parameterName, Type $php $this->reflection, $this->reflectionProvider, $this->attributeReflectionFactory, + $this->allowedConstantsMapProvider, $this->templateTypeMap, $phpDocParameterTypes, $this->phpDocReturnType, diff --git a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php index f048ea71006..7061d7f63e9 100644 --- a/src/Reflection/Php/PhpParameterFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpParameterFromParserNodeReflection.php @@ -2,8 +2,10 @@ namespace PHPStan\Reflection\Php; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ExtendedParameterReflection; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -113,4 +115,14 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return null; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + return new AllowedConstantsResult([], [], false); + } + } diff --git a/src/Reflection/Php/PhpParameterReflection.php b/src/Reflection/Php/PhpParameterReflection.php index 8469f7bef44..17b55295159 100644 --- a/src/Reflection/Php/PhpParameterReflection.php +++ b/src/Reflection/Php/PhpParameterReflection.php @@ -3,11 +3,13 @@ namespace PHPStan\Reflection\Php; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; +use PHPStan\Reflection\AllowedConstantsResult; use PHPStan\Reflection\AttributeReflection; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; +use PHPStan\Reflection\ParameterAllowedConstants; use PHPStan\Reflection\PassedByReference; use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; @@ -34,6 +36,7 @@ public function __construct( private TrinaryLogic $immediatelyInvokedCallable, private ?Type $closureThisType, private array $attributes, + private ?ParameterAllowedConstants $allowedConstants, ) { } @@ -143,4 +146,18 @@ public function getAttributes(): array return $this->attributes; } + public function getAllowedConstants(): ?ParameterAllowedConstants + { + return $this->allowedConstants; + } + + public function checkAllowedConstants(array $constants): AllowedConstantsResult + { + if ($this->allowedConstants === null) { + return new AllowedConstantsResult([], [], false); + } + + return $this->allowedConstants->check($constants); + } + } diff --git a/src/Reflection/ResolvedFunctionVariantWithOriginal.php b/src/Reflection/ResolvedFunctionVariantWithOriginal.php index 21108d658ef..cd57c8db9d0 100644 --- a/src/Reflection/ResolvedFunctionVariantWithOriginal.php +++ b/src/Reflection/ResolvedFunctionVariantWithOriginal.php @@ -121,6 +121,7 @@ function (ExtendedParameterReflection $param): ExtendedParameterReflection { $param->isImmediatelyInvokedCallable(), $closureThisType, $param->getAttributes(), + $param->getAllowedConstants(), ); }, $this->parametersAcceptor->getParameters(), diff --git a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php index a2f12f6daad..efb3b508db3 100644 --- a/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php +++ b/src/Reflection/SignatureMap/NativeFunctionReflectionProvider.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\Native\ExtendedNativeParameterReflection; use PHPStan\Reflection\Native\NativeFunctionReflection; +use PHPStan\Reflection\ParameterAllowedConstantsMapProvider; use PHPStan\TrinaryLogic; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -41,6 +42,7 @@ public function __construct( private FileTypeMapper $fileTypeMapper, private StubPhpDocProvider $stubPhpDocProvider, private AttributeReflectionFactory $attributeReflectionFactory, + private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, ) { } @@ -107,13 +109,14 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $acceptsNamedArguments = $phpDoc->acceptsNamedArguments(); } + $allowedConstantsMapProvider = $this->allowedConstantsMapProvider; $variantsByType = ['positional' => []]; foreach ($functionSignaturesResult as $signatureType => $functionSignatures) { foreach ($functionSignatures ?? [] as $functionSignature) { $variantsByType[$signatureType][] = new ExtendedFunctionVariant( TemplateTypeMap::createEmpty(), null, - array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc): ExtendedNativeParameterReflection { + array_map(static function (ParameterSignature $parameterSignature) use ($phpDoc, $lowerCasedFunctionName, $allowedConstantsMapProvider): ExtendedNativeParameterReflection { $type = $parameterSignature->getType(); $phpDocType = null; @@ -144,6 +147,7 @@ public function findFunctionReflection(string $functionName): ?NativeFunctionRef $immediatelyInvokedCallable, $closureThisType, [], + $allowedConstantsMapProvider->getForFunctionParameter($lowerCasedFunctionName, $parameterSignature->getName()), ); }, $functionSignature->getParameters()), $functionSignature->isVariadic(), diff --git a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php index 1f183844d53..026d3c36fb2 100644 --- a/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CallbackUnresolvedMethodPrototypeReflection.php @@ -118,6 +118,7 @@ function (ExtendedParameterReflection $parameter): ExtendedParameterReflection { $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, $parameter->getAttributes(), + $parameter->getAllowedConstants(), ); }, $acceptor->getParameters(), diff --git a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php index 68d3200bec0..8198ea1f954 100644 --- a/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php +++ b/src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php @@ -105,6 +105,7 @@ private function transformMethodWithStaticType(ClassReflection $declaringClass, $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType() !== null ? $this->transformStaticType($parameter->getClosureThisType()) : null, $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $acceptor->getParameters(), ), diff --git a/src/Reflection/WrappedExtendedMethodReflection.php b/src/Reflection/WrappedExtendedMethodReflection.php index d028d80d04b..7711a799580 100644 --- a/src/Reflection/WrappedExtendedMethodReflection.php +++ b/src/Reflection/WrappedExtendedMethodReflection.php @@ -77,6 +77,7 @@ public function getVariants(): array TrinaryLogic::createMaybe(), null, [], + null, ), $variant->getParameters()), $variant->isVariadic(), $variant->getReturnType(), diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 36b3c6faa61..3d8d8edf038 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -157,6 +157,9 @@ public function check( 'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.', '%s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.', 'Attribute class ' . $attributeClassName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', + 'Constants %s cannot be combined for %s of attribute class ' . $attributeClassName . ' constructor.', + 'Combining constants with | is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index e92e18a03d4..fc4d2e13fc5 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -269,6 +269,9 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ 'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.', '%s of class ' . $classDisplayName . ' constructor contains unresolvable type.', 'Class ' . $classDisplayName . ' constructor invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of class ' . $classDisplayName . ' constructor.', + 'Constants %s cannot be combined for %s of class ' . $classDisplayName . ' constructor.', + 'Combining constants with | is not allowed for %s of class ' . $classDisplayName . ' constructor.', )); } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 16c0938b069..a0dea2a1e0e 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -8,6 +8,7 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\ConstantReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; @@ -36,6 +37,7 @@ use function in_array; use function is_int; use function is_string; +use function lcfirst; use function max; use function sprintf; @@ -87,6 +89,9 @@ public function check( string $unresolvableReturnTypeMessage, string $unresolvableParameterTypeMessage, string $namedArgumentMessage, + string $invalidConstantMessage, + string $exclusiveConstantsMessage, + string $bitmaskNotAllowedMessage, ): array { if ($funcCall instanceof Node\Expr\MethodCall || $funcCall instanceof Node\Expr\StaticCall || $funcCall instanceof Node\Expr\FuncCall) { @@ -407,6 +412,46 @@ public function check( ->line($argumentLine) ->build(); } + + if ( + $parameter instanceof ExtendedParameterReflection + && $parameter->getAllowedConstants() !== null + && $scope->getPhpVersion()->supportsNamedArguments()->yes() + ) { + $constantReflections = $this->resolveConstantReflections($argumentValue, $scope); + if ($constantReflections !== null) { + $result = $parameter->checkAllowedConstants($constantReflections); + foreach ($result->getDisallowedConstants() as $disallowedConstant) { + $errors[] = RuleErrorBuilder::message(sprintf( + $invalidConstantMessage, + $disallowedConstant->getName(), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.invalidConstant') + ->line($argumentLine) + ->build(); + } + foreach ($result->getViolatedExclusiveGroups() as $group) { + $errors[] = RuleErrorBuilder::message(sprintf( + $exclusiveConstantsMessage, + implode(', ', $group), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.exclusiveConstants') + ->line($argumentLine) + ->build(); + } + if ($result->isBitmaskNotAllowed()) { + $errors[] = RuleErrorBuilder::message(sprintf( + $bitmaskNotAllowedMessage, + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.bitmaskNotAllowed') + ->line($argumentLine) + ->build(); + } + } + } } if ( @@ -702,6 +747,53 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu return implode(' ', $parts); } + /** + * @return list|null Null when the expression is not a constant or bitmask of constants + */ + private function resolveConstantReflections(Expr $expr, Scope $scope): ?array + { + if ($expr instanceof Expr\ConstFetch) { + if (!$this->reflectionProvider->hasConstant($expr->name, $scope)) { + return null; + } + + return [$this->reflectionProvider->getConstant($expr->name, $scope)]; + } + + if ($expr instanceof Expr\ClassConstFetch) { + if (!$expr->class instanceof Node\Name) { + return null; + } + if (!$expr->name instanceof Node\Identifier) { + return null; + } + + $className = $scope->resolveName($expr->class); + if (!$this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + if (!$classReflection->hasConstant($expr->name->name)) { + return null; + } + + return [$classReflection->getConstant($expr->name->name)]; + } + + if ($expr instanceof Expr\BinaryOp\BitwiseOr) { + $left = $this->resolveConstantReflections($expr->left, $scope); + $right = $this->resolveConstantReflections($expr->right, $scope); + if ($left === null || $right === null) { + return null; + } + + return [...$left, ...$right]; + } + + return null; + } + private function callReturnsByReference(Expr $expr, Scope $scope): bool { if ($expr instanceof Node\Expr\MethodCall) { diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 4ab0af4b014..342498bacd6 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -139,6 +139,9 @@ public function processNode( 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', '%s of ' . $callableDescription . ' contains unresolvable type.', ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $callableDescription . '.', + 'Constants %s cannot be combined for %s of ' . $callableDescription . '.', + 'Combining constants with | is not allowed for %s of ' . $callableDescription . '.', ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 39f6f7cfeac..515ea46fed8 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -68,6 +68,9 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to function ' . $functionName . ' contains unresolvable type.', '%s of function ' . $functionName . ' contains unresolvable type.', 'Function ' . $functionName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of function ' . $functionName . '.', + 'Constants %s cannot be combined for %s of function ' . $functionName . '.', + 'Combining constants with | is not allowed for %s of function ' . $functionName . '.', ); } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 3dae092b529..da834fa226e 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -84,6 +84,9 @@ public function processNode(Node $node, Scope $scope): array 'Return type of call to ' . $callableDescription . ' contains unresolvable type.', '%s of ' . $callableDescription . ' contains unresolvable type.', ucfirst($callableDescription) . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $callableDescription . '.', + 'Constants %s cannot be combined for %s of ' . $callableDescription . '.', + 'Combining constants with | is not allowed for %s of ' . $callableDescription . '.', ); } diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 1f042288f0e..e2d8f9e4a3d 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -99,6 +99,9 @@ private function processSingleMethodCall(Scope $scope, MethodCall $node, string 'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.', '%s of method ' . $messagesMethodName . ' contains unresolvable type.', 'Method ' . $messagesMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of method ' . $messagesMethodName . '.', + 'Constants %s cannot be combined for %s of method ' . $messagesMethodName . '.', + 'Combining constants with | is not allowed for %s of method ' . $messagesMethodName . '.', )); } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 166ad74aced..c19aaff2b72 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -108,6 +108,9 @@ private function processSingleMethodCall(Scope $scope, StaticCall $node, string 'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.', '%s of ' . $lowercasedMethodName . ' contains unresolvable type.', $displayMethodName . ' invoked with %s, but it\'s not allowed because of @no-named-arguments.', + 'Constant %s is not allowed for %s of ' . $lowercasedMethodName . '.', + 'Constants %s cannot be combined for %s of ' . $lowercasedMethodName . '.', + 'Combining constants with | is not allowed for %s of ' . $lowercasedMethodName . '.', )); return $errors; diff --git a/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php b/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php new file mode 100644 index 00000000000..99a4041d6c7 --- /dev/null +++ b/tests/PHPStan/Reflection/ConstantToFunctionParameterMapTest.php @@ -0,0 +1,162 @@ += 8.0')] +class ConstantToFunctionParameterMapTest extends PHPStanTestCase +{ + + public function testMapIsValid(): void + { + $map = require __DIR__ . '/../../../resources/constantToFunctionParameterMap.php'; + $this->assertIsArray($map); + + $reflectionProvider = self::createReflectionProvider(); + + foreach ($map as $entry => $parameters) { + $this->assertIsString($entry, 'Entry key must be a string.'); + $this->assertIsArray($parameters, sprintf('Parameters for %s must be an array.', $entry)); + + if (str_contains($entry, '::')) { + // Method entry: Class::method + [$className, $methodName] = explode('::', $entry, 2); + + $this->assertTrue( + $reflectionProvider->hasClass($className), + sprintf('Class %s not found in reflection (from %s).', $className, $entry), + ); + + $classReflection = $reflectionProvider->getClass($className); + $this->assertTrue( + $classReflection->hasMethod($methodName), + sprintf('Method %s not found in reflection.', $entry), + ); + + $methodReflection = $classReflection->getNativeMethod($methodName); + $variants = $methodReflection->getVariants(); + $this->assertNotEmpty($variants, sprintf('Method %s has no variants.', $entry)); + + $reflectionParameters = $variants[0]->getParameters(); + } else { + $this->assertNotSame('', $entry); + // Function entry + $nameNode = new Name($entry); + $this->assertTrue( + $reflectionProvider->hasFunction($nameNode, null), + sprintf('Function %s() not found in reflection.', $entry), + ); + + $functionReflection = $reflectionProvider->getFunction($nameNode, null); + $variants = $functionReflection->getVariants(); + $this->assertNotEmpty($variants, sprintf('Function %s() has no variants.', $entry)); + + $reflectionParameters = $variants[0]->getParameters(); + } + + $reflectionParameterNames = []; + foreach ($reflectionParameters as $reflectionParameter) { + $reflectionParameterNames[] = $reflectionParameter->getName(); + } + + foreach ($parameters as $parameterName => $config) { + $this->assertIsString($parameterName, sprintf('Parameter name for %s must be a string.', $entry)); + $this->assertContains( + $parameterName, + $reflectionParameterNames, + sprintf( + 'Parameter $%s not found in %s. Available parameters: $%s', + $parameterName, + $entry, + implode(', $', $reflectionParameterNames), + ), + ); + + $this->assertIsArray($config, sprintf('Config for %s($%s) must be an array.', $entry, $parameterName)); + $this->assertArrayHasKey('type', $config, sprintf('Missing "type" key for %s($%s).', $entry, $parameterName)); + $this->assertContains($config['type'], ['single', 'bitmask'], sprintf('Invalid type "%s" for %s($%s).', $config['type'], $entry, $parameterName)); + $this->assertArrayHasKey('constants', $config, sprintf('Missing "constants" key for %s($%s).', $entry, $parameterName)); + $this->assertIsArray($config['constants'], sprintf('Constants for %s($%s) must be an array.', $entry, $parameterName)); + $this->assertNotEmpty($config['constants'], sprintf('Constants for %s($%s) must not be empty.', $entry, $parameterName)); + + foreach ($config['constants'] as $constantName) { + $this->assertIsString($constantName, sprintf('Constant name for %s($%s) must be a string.', $entry, $parameterName)); + + if (str_contains($constantName, '::')) { + // Class constant: Class::CONSTANT + [$constClassName, $constName] = explode('::', $constantName, 2); + $this->assertTrue( + $reflectionProvider->hasClass($constClassName), + sprintf('Class %s not found in reflection (constant %s used in %s($%s)).', $constClassName, $constantName, $entry, $parameterName), + ); + $constClassReflection = $reflectionProvider->getClass($constClassName); + $this->assertTrue( + $constClassReflection->hasConstant($constName), + sprintf('Constant %s not found in reflection (used in %s($%s)).', $constantName, $entry, $parameterName), + ); + } else { + $this->assertNotSame('', $constantName); + // Global constant + $constantNameNode = new Name($constantName); + $this->assertTrue( + $reflectionProvider->hasConstant($constantNameNode, null), + sprintf('Constant %s (used in %s($%s)) not found in reflection.', $constantName, $entry, $parameterName), + ); + } + } + + $allowedKeys = ['type', 'constants', 'exclusiveGroups']; + foreach (array_keys($config) as $key) { + $this->assertContains($key, $allowedKeys, sprintf('Unknown key "%s" in config for %s($%s).', $key, $entry, $parameterName)); + } + + if (!isset($config['exclusiveGroups'])) { + continue; + } + + $this->assertSame('bitmask', $config['type'], sprintf('exclusiveGroups only makes sense for bitmask type in %s($%s).', $entry, $parameterName)); + $this->assertIsArray($config['exclusiveGroups']); + + foreach ($config['exclusiveGroups'] as $groupIndex => $group) { + $this->assertIsArray($group, sprintf('Exclusive group #%d for %s($%s) must be an array.', $groupIndex, $entry, $parameterName)); + $this->assertGreaterThanOrEqual(2, count($group), sprintf('Exclusive group #%d for %s($%s) must have at least 2 constants.', $groupIndex, $entry, $parameterName)); + + foreach ($group as $constantName) { + $this->assertContains( + $constantName, + $config['constants'], + sprintf( + 'Constant %s in exclusive group #%d for %s($%s) is not in the constants list.', + $constantName, + $groupIndex, + $entry, + $parameterName, + ), + ); + } + } + } + } + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/constantToFunctionParameterMap.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php b/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php new file mode 100644 index 00000000000..89f135dc78d --- /dev/null +++ b/tests/PHPStan/Reflection/ParameterAllowedConstantsTest.php @@ -0,0 +1,285 @@ += 8.0')] +class ParameterAllowedConstantsTest extends PHPStanTestCase +{ + + public function testJsonEncodeFlagsAllowsJsonConstant(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $flagsParam->checkAllowedConstants([$jsonThrowOnError]); + $this->assertTrue($result->isOk()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + $this->assertSame('SORT_REGULAR', $result->getDisallowedConstants()[0]->getName()); + } + + public function testJsonDecodeDoesNotAllowEncodeOnlyConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_decode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[3]; + + $this->assertSame('flags', $flagsParam->getName()); + + $jsonPrettyPrint = $reflectionProvider->getConstant(new Name('JSON_PRETTY_PRINT'), null); + $result = $flagsParam->checkAllowedConstants([$jsonPrettyPrint]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $flagsParam->checkAllowedConstants([$jsonThrowOnError]); + $this->assertTrue($result->isOk()); + } + + public function testSortFlagsExclusiveGroups(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + + $config = $flagsParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertTrue($config->isBitmask()); + $this->assertCount(1, $config->getExclusiveGroups()); + $this->assertSame( + ['SORT_REGULAR', 'SORT_NUMERIC', 'SORT_STRING', 'SORT_LOCALE_STRING', 'SORT_NATURAL'], + $config->getExclusiveGroups()[0], + ); + + $sortFlagCase = $reflectionProvider->getConstant(new Name('SORT_FLAG_CASE'), null); + $result = $flagsParam->checkAllowedConstants([$sortFlagCase]); + $this->assertTrue($result->isOk()); + } + + public function testHtmlspecialcharsMultipleExclusiveGroups(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('htmlspecialchars'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + + $config = $flagsParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertCount(2, $config->getExclusiveGroups()); + $this->assertSame(['ENT_COMPAT', 'ENT_QUOTES', 'ENT_NOQUOTES'], $config->getExclusiveGroups()[0]); + $this->assertSame(['ENT_HTML401', 'ENT_XML1', 'ENT_XHTML', 'ENT_HTML5'], $config->getExclusiveGroups()[1]); + } + + public function testSingleTypeParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('round'), null); + $modeParam = $function->getVariants()[0]->getParameters()[2]; + + $this->assertSame('mode', $modeParam->getName()); + + $config = $modeParam->getAllowedConstants(); + $this->assertNotNull($config); + $this->assertFalse($config->isBitmask()); + $this->assertSame([], $config->getExclusiveGroups()); + + $halfUp = $reflectionProvider->getConstant(new Name('PHP_ROUND_HALF_UP'), null); + $result = $modeParam->checkAllowedConstants([$halfUp]); + $this->assertTrue($result->isOk()); + } + + public function testUnmappedParameterReturnsOk(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('strlen'), null); + $param = $function->getVariants()[0]->getParameters()[0]; + + $this->assertNull($param->getAllowedConstants()); + + $anyConstant = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + $result = $param->checkAllowedConstants([$anyConstant]); + $this->assertTrue($result->isOk()); + } + + public function testMethodWithGlobalConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $class = $reflectionProvider->getClass('finfo'); + $method = $class->getNativeMethod('file'); + $flagsParam = $method->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $fileinfoMime = $reflectionProvider->getConstant(new Name('FILEINFO_MIME'), null); + $result = $flagsParam->checkAllowedConstants([$fileinfoMime]); + $this->assertTrue($result->isOk()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testMethodWithClassConstants(): void + { + $reflectionProvider = self::createReflectionProvider(); + $class = $reflectionProvider->getClass('PDOStatement'); + $method = $class->getNativeMethod('fetch'); + $modeParam = $method->getVariants()[0]->getParameters()[0]; + + $this->assertSame('mode', $modeParam->getName()); + $this->assertNotNull($modeParam->getAllowedConstants()); + $this->assertFalse($modeParam->getAllowedConstants()->isBitmask()); + + $pdoClass = $reflectionProvider->getClass('PDO'); + + $fetchAssoc = $pdoClass->getConstant('FETCH_ASSOC'); + $result = $modeParam->checkAllowedConstants([$fetchAssoc]); + $this->assertTrue($result->isOk()); + + $attrErrmode = $pdoClass->getConstant('ATTR_ERRMODE'); + $result = $modeParam->checkAllowedConstants([$attrErrmode]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testClassConstantNotAllowedWhenGlobalConstantsExpected(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $pdoClass = $reflectionProvider->getClass('PDO'); + $fetchAssoc = $pdoClass->getConstant('FETCH_ASSOC'); + + $result = $flagsParam->checkAllowedConstants([$fetchAssoc]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + } + + public function testViolatedExclusiveGroupsSortFlags(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + $sortString = $reflectionProvider->getConstant(new Name('SORT_STRING'), null); + $sortFlagCase = $reflectionProvider->getConstant(new Name('SORT_FLAG_CASE'), null); + + // Two mutually exclusive sort types + $result = $flagsParam->checkAllowedConstants([$sortNumeric, $sortString]); + $this->assertFalse($result->isOk()); + $this->assertSame([], $result->getDisallowedConstants()); + $this->assertCount(1, $result->getViolatedExclusiveGroups()); + $this->assertSame(['SORT_NUMERIC', 'SORT_STRING'], $result->getViolatedExclusiveGroups()[0]); + + // Sort type + modifier is fine + $result = $flagsParam->checkAllowedConstants([$sortString, $sortFlagCase]); + $this->assertTrue($result->isOk()); + } + + public function testViolatedExclusiveGroupsHtmlEntities(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('htmlspecialchars'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $entQuotes = $reflectionProvider->getConstant(new Name('ENT_QUOTES'), null); + $entNoquotes = $reflectionProvider->getConstant(new Name('ENT_NOQUOTES'), null); + $entHtml401 = $reflectionProvider->getConstant(new Name('ENT_HTML401'), null); + $entHtml5 = $reflectionProvider->getConstant(new Name('ENT_HTML5'), null); + $entSubstitute = $reflectionProvider->getConstant(new Name('ENT_SUBSTITUTE'), null); + + // Violates both exclusive groups + $result = $flagsParam->checkAllowedConstants([$entQuotes, $entNoquotes, $entHtml401, $entHtml5]); + $this->assertFalse($result->isOk()); + $this->assertSame([], $result->getDisallowedConstants()); + $this->assertCount(2, $result->getViolatedExclusiveGroups()); + $this->assertSame(['ENT_QUOTES', 'ENT_NOQUOTES'], $result->getViolatedExclusiveGroups()[0]); + $this->assertSame(['ENT_HTML401', 'ENT_HTML5'], $result->getViolatedExclusiveGroups()[1]); + + // One from each group is fine + $result = $flagsParam->checkAllowedConstants([$entQuotes, $entHtml5, $entSubstitute]); + $this->assertTrue($result->isOk()); + } + + public function testBitmaskNotAllowedOnSingleParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('array_unique'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertSame('flags', $flagsParam->getName()); + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertFalse($flagsParam->getAllowedConstants()->isBitmask()); + + $sortRegular = $reflectionProvider->getConstant(new Name('SORT_REGULAR'), null); + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + + // Single constant is fine + $result = $flagsParam->checkAllowedConstants([$sortRegular]); + $this->assertTrue($result->isOk()); + $this->assertFalse($result->isBitmaskNotAllowed()); + + // Bitmask on single-value parameter is not allowed + $result = $flagsParam->checkAllowedConstants([$sortRegular, $sortNumeric]); + $this->assertFalse($result->isOk()); + $this->assertTrue($result->isBitmaskNotAllowed()); + } + + public function testBitmaskAllowedOnBitmaskParameter(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('json_encode'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $this->assertNotNull($flagsParam->getAllowedConstants()); + $this->assertTrue($flagsParam->getAllowedConstants()->isBitmask()); + + $prettyPrint = $reflectionProvider->getConstant(new Name('JSON_PRETTY_PRINT'), null); + $unescaped = $reflectionProvider->getConstant(new Name('JSON_UNESCAPED_SLASHES'), null); + + $result = $flagsParam->checkAllowedConstants([$prettyPrint, $unescaped]); + $this->assertTrue($result->isOk()); + $this->assertFalse($result->isBitmaskNotAllowed()); + } + + public function testBothDisallowedAndExclusiveViolation(): void + { + $reflectionProvider = self::createReflectionProvider(); + $function = $reflectionProvider->getFunction(new Name('sort'), null); + $flagsParam = $function->getVariants()[0]->getParameters()[1]; + + $sortNumeric = $reflectionProvider->getConstant(new Name('SORT_NUMERIC'), null); + $sortString = $reflectionProvider->getConstant(new Name('SORT_STRING'), null); + $jsonThrowOnError = $reflectionProvider->getConstant(new Name('JSON_THROW_ON_ERROR'), null); + + // Wrong constant AND exclusive group violation + $result = $flagsParam->checkAllowedConstants([$sortNumeric, $sortString, $jsonThrowOnError]); + $this->assertFalse($result->isOk()); + $this->assertCount(1, $result->getDisallowedConstants()); + $this->assertSame('JSON_THROW_ON_ERROR', $result->getDisallowedConstants()[0]->getName()); + $this->assertCount(1, $result->getViolatedExclusiveGroups()); + $this->assertSame(['SORT_NUMERIC', 'SORT_STRING'], $result->getViolatedExclusiveGroups()[0]); + } + +} diff --git a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php index 1926cb56e53..b00993697a6 100644 --- a/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php +++ b/tests/PHPStan/Reflection/ParametersAcceptorSelectorTest.php @@ -92,6 +92,7 @@ public static function dataSelectFromTypes(): Generator $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType(), $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $datePeriodConstructorVariants[0]->getParameters()), false, new VoidType(), @@ -123,6 +124,7 @@ public static function dataSelectFromTypes(): Generator $parameter->isImmediatelyInvokedCallable(), $parameter->getClosureThisType(), $parameter->getAttributes(), + $parameter->getAllowedConstants(), ), $datePeriodConstructorVariants[1]->getParameters()), false, new VoidType(), diff --git a/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon b/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon new file mode 100644 index 00000000000..72ae924610a --- /dev/null +++ b/tests/PHPStan/Reflection/constantToFunctionParameterMap.neon @@ -0,0 +1,2 @@ +parameters: + phpVersion: 80500 diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 8558296e149..413aba97670 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -615,4 +615,19 @@ public function testBug11006(): void $this->analyse([__DIR__ . '/data/bug-11006.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckInstantiation(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check-instantiation.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #1 $flags of class finfo constructor.', + 12, + ], + [ + 'Constant GREGORIAN is not allowed for parameter #2 $dateType of class IntlDateFormatter constructor.', + 18, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php new file mode 100644 index 00000000000..256c7639938 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php @@ -0,0 +1,18 @@ += 8.0')] + public function testConstantParameterCheckCallables(): void + { + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/constant-parameter-check-callables.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of closure.', + 10, + ], + ]); + } + public function testMaybeNotCallable(): void { $errors = []; diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index f78f5918f77..9a539b30a89 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1586,9 +1586,14 @@ public function testBenevolentSuperglobalKeys(): void $this->analyse([__DIR__ . '/data/benevolent-superglobal-keys.php'], []); } + #[RequiresPhp('>= 8.0')] public function testFileParams(): void { $this->analyse([__DIR__ . '/data/file.php'], [ + [ + 'Constant FILE_APPEND is not allowed for parameter #2 $flags of function file.', + 16, + ], [ 'Parameter #2 $flags of function file expects 0|1|2|3|4|5|6|7|16|17|18|19|20|21|22|23, 8 given.', 16, @@ -1596,9 +1601,14 @@ public function testFileParams(): void ]); } + #[RequiresPhp('>= 8.0')] public function testFlockParams(): void { $this->analyse([__DIR__ . '/data/flock.php'], [ + [ + 'Constant FILE_APPEND is not allowed for parameter #2 $operation of function flock.', + 45, + ], [ 'Parameter #2 $operation of function flock expects int<0, 7>, 8 given.', 45, @@ -1614,6 +1624,10 @@ public function testJsonValidate(): void 'Parameter #2 $depth of function json_validate expects int<1, max>, 0 given.', 6, ], + [ + 'Constant JSON_BIGINT_AS_STRING is not allowed for parameter #3 $flags of function json_validate.', + 7, + ], [ 'Parameter #3 $flags of function json_validate expects 0|1048576, 2 given.', 7, @@ -2758,4 +2772,70 @@ public function testBug14312b(): void $this->analyse([__DIR__ . '/data/bug-14312b.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheck(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of function json_encode.', + 12, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of function json_encode.', + 21, + ], + [ + 'Constants SORT_NUMERIC, SORT_STRING cannot be combined for parameter #2 $flags of function sort.', + 27, + ], + [ + 'Constants SORT_NUMERIC, SORT_STRING cannot be combined for parameter #2 $flags of function sort.', + 30, + ], + [ + 'Constants ENT_QUOTES, ENT_NOQUOTES cannot be combined for parameter #2 $flags of function htmlspecialchars.', + 33, + ], + [ + 'Constants ENT_HTML401, ENT_HTML5 cannot be combined for parameter #2 $flags of function htmlspecialchars.', + 33, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $filter of function filter_var.', + 39, + ], + [ + 'Constant JSON_PRETTY_PRINT is not allowed for parameter #4 $flags of function json_decode.', + 51, + ], + [ + 'Constants LOCK_SH, LOCK_EX cannot be combined for parameter #2 $operation of function flock.', + 54, + ], + [ + 'Constant SORT_REGULAR is not allowed for parameter $flags of function json_encode.', + 70, + ], + [ + 'Combining constants with | is not allowed for parameter #2 $flags of function array_unique.', + 76, + ], + [ + 'Combining constants with | is not allowed for parameter #2 $filter of function filter_var.', + 79, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12850(): void + { + $this->analyse([__DIR__ . '/data/bug-12850.php'], [ + [ + 'Constants LOCK_EX, LOCK_SH cannot be combined for parameter #2 $operation of function flock.', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index dd4b3259a83..d695ffa2ad7 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -102,4 +102,15 @@ public function testNoNamedArguments(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckCallUserFunc(): void + { + $this->analyse([__DIR__ . '/data/constant-parameter-check-call-user-func.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of callable passed to call_user_func().', + 9, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12850.php b/tests/PHPStan/Rules/Functions/data/bug-12850.php new file mode 100644 index 00000000000..fa8a41ab5f0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12850.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ConstantParameterCheckCallUserFunc; + +// call_user_func with correct constant +call_user_func('json_encode', [], JSON_PRETTY_PRINT); + +// call_user_func with wrong constant +call_user_func('json_encode', [], SORT_REGULAR); diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php new file mode 100644 index 00000000000..6acb0ba1876 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php @@ -0,0 +1,10 @@ += 8.0')] + public function testConstantParameterCheckMethods(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/constant-parameter-check-methods.php'], [ + [ + 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of method finfo::file().', + 10, + ], + [ + 'Constant ATTR_ERRMODE is not allowed for parameter #1 $mode of method PDOStatement::fetch().', + 17, + ], + [ + 'Constant FRENCH_COLLATION is not allowed for parameter #2 $flags of method Collator::sort().', + 25, + ], + ]); + } + public function testBug11463(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 82ee7e15669..9345fce6418 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -997,4 +997,20 @@ public function testPipeOperator(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testConstantParameterCheckStatic(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/constant-parameter-check-static.php'], [ + [ + 'Constant GREGORIAN is not allowed for parameter #2 $dateType of static method IntlDateFormatter::create().', + 9, + ], + [ + 'Constant TYPE_INT32 is not allowed for parameter #2 $style of static method NumberFormatter::create().', + 15, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php new file mode 100644 index 00000000000..7ecae9f3f89 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php @@ -0,0 +1,25 @@ +file('test.txt', FILEINFO_MIME_TYPE); + +// finfo::file - wrong constant +$finfo->file('test.txt', SORT_REGULAR); + +// PDOStatement::fetch - correct class constant +/** @var \PDOStatement $stmt */ +$stmt->fetch(\PDO::FETCH_ASSOC); + +// PDOStatement::fetch - wrong class constant +$stmt->fetch(\PDO::ATTR_ERRMODE); + +// Collator::sort - correct class constant +/** @var \Collator $collator */ +$arr = []; +$collator->sort($arr, \Collator::SORT_STRING); + +// Collator::sort - wrong class constant +$collator->sort($arr, \Collator::FRENCH_COLLATION); diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php new file mode 100644 index 00000000000..16b7298f55c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php @@ -0,0 +1,15 @@ + Date: Fri, 20 Mar 2026 16:49:19 +0100 Subject: [PATCH 37/70] Fix false positive abount allowed constants in parameters --- .../BetterReflection/BetterReflectionProvider.php | 1 + .../Constant/RuntimeConstantReflection.php | 11 +++++++++++ src/Reflection/ConstantReflection.php | 4 ++++ .../Dummy/DummyClassConstantReflection.php | 11 +++++++++++ src/Reflection/ParameterAllowedConstants.php | 15 +++++---------- .../RealClassClassConstantReflection.php | 11 +++++++++++ src/Rules/FunctionCallParametersCheck.php | 2 +- ...ittenDeclaringClassClassConstantReflection.php | 11 +++++++++++ .../Rules/Classes/InstantiationRuleTest.php | 2 +- .../Functions/data/constant-parameter-check.php | 15 +++++++++++++++ .../PHPStan/Rules/Methods/CallMethodsRuleTest.php | 4 ++-- .../Rules/Methods/CallStaticMethodsRuleTest.php | 4 ++-- 12 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index b514bfa1a93..c7d4bac0df4 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -444,6 +444,7 @@ public function getConstant(Node\Name $nameNode, ?NamespaceAnswerer $namespaceAn array_map(static fn (BetterReflectionAttribute $betterReflectionAttribute) => ReflectionAttributeFactory::create($betterReflectionAttribute), $constantReflection->getAttributes()), InitializerExprContext::fromGlobalConstant($constantReflection), ), + $constantReflection->isInternal(), ); } diff --git a/src/Reflection/Constant/RuntimeConstantReflection.php b/src/Reflection/Constant/RuntimeConstantReflection.php index 0cbe1eb1db2..1f643c28a33 100644 --- a/src/Reflection/Constant/RuntimeConstantReflection.php +++ b/src/Reflection/Constant/RuntimeConstantReflection.php @@ -20,6 +20,7 @@ public function __construct( private TrinaryLogic $isDeprecated, private ?string $deprecatedDescription, private array $attributes, + private bool $internal, ) { } @@ -29,6 +30,16 @@ public function getName(): string return $this->name; } + public function describe(): string + { + return $this->name; + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->internal); + } + public function getValueType(): Type { return $this->valueType; diff --git a/src/Reflection/ConstantReflection.php b/src/Reflection/ConstantReflection.php index 01aea117ea2..34fceef8467 100644 --- a/src/Reflection/ConstantReflection.php +++ b/src/Reflection/ConstantReflection.php @@ -20,6 +20,10 @@ interface ConstantReflection public function getName(): string; + public function describe(): string; + + public function isBuiltin(): TrinaryLogic; + public function getValueType(): Type; public function isDeprecated(): TrinaryLogic; diff --git a/src/Reflection/Dummy/DummyClassConstantReflection.php b/src/Reflection/Dummy/DummyClassConstantReflection.php index 768c5bdf275..8436abf76c0 100644 --- a/src/Reflection/Dummy/DummyClassConstantReflection.php +++ b/src/Reflection/Dummy/DummyClassConstantReflection.php @@ -12,6 +12,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\Type; use stdClass; +use function sprintf; final class DummyClassConstantReflection implements ClassConstantReflection { @@ -62,6 +63,16 @@ public function getName(): string return $this->name; } + public function describe(): string + { + return sprintf('%s::%s', $this->getDeclaringClass()->getDisplayName(), $this->name); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getDeclaringClass()->isBuiltin()); + } + public function getValueType(): Type { return new MixedType(); diff --git a/src/Reflection/ParameterAllowedConstants.php b/src/Reflection/ParameterAllowedConstants.php index 2f844e28a82..c784e3c29af 100644 --- a/src/Reflection/ParameterAllowedConstants.php +++ b/src/Reflection/ParameterAllowedConstants.php @@ -47,15 +47,6 @@ public function getExclusiveGroups(): array return $this->exclusiveGroups; } - private function resolveConstantName(ConstantReflection $constant): string - { - if ($constant instanceof ClassConstantReflection) { - return $constant->getDeclaringClass()->getName() . '::' . $constant->getName(); - } - - return $constant->getName(); - } - /** * @param list $constants */ @@ -67,7 +58,11 @@ public function check(array $constants): AllowedConstantsResult $names = []; foreach ($constants as $constant) { - $name = $this->resolveConstantName($constant); + if ($constant->isBuiltin()->no()) { + continue; + } + + $name = $constant->describe(); $names[] = $name; if (in_array($name, $this->constants, true)) { diff --git a/src/Reflection/RealClassClassConstantReflection.php b/src/Reflection/RealClassClassConstantReflection.php index d0b69f5eedc..c565feb0811 100644 --- a/src/Reflection/RealClassClassConstantReflection.php +++ b/src/Reflection/RealClassClassConstantReflection.php @@ -9,6 +9,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Type; use PHPStan\Type\TypehintHelper; +use function sprintf; final class RealClassClassConstantReflection implements ClassConstantReflection { @@ -39,6 +40,16 @@ public function getName(): string return $this->reflection->getName(); } + public function describe(): string + { + return sprintf('%s::%s', $this->declaringClass->getDisplayName(), $this->getName()); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->declaringClass->isBuiltin()); + } + public function getFileName(): ?string { return $this->declaringClass->getFileName(); diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index a0dea2a1e0e..10a5c6ab226 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -424,7 +424,7 @@ public function check( foreach ($result->getDisallowedConstants() as $disallowedConstant) { $errors[] = RuleErrorBuilder::message(sprintf( $invalidConstantMessage, - $disallowedConstant->getName(), + $disallowedConstant->describe(), lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), )) ->identifier('argument.invalidConstant') diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php index a92a6172737..02f45dbbb69 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php @@ -8,6 +8,7 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Type; +use function sprintf; final class RewrittenDeclaringClassClassConstantReflection implements ClassConstantReflection { @@ -89,6 +90,16 @@ public function getName(): string return $this->constantReflection->getName(); } + public function describe(): string + { + return sprintf('%s::%s', $this->getDeclaringClass()->getDisplayName(), $this->getName()); + } + + public function isBuiltin(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean($this->getDeclaringClass()->isBuiltin()); + } + public function getValueType(): Type { return $this->constantReflection->getValueType(); diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 413aba97670..fdeb2a27900 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -624,7 +624,7 @@ public function testConstantParameterCheckInstantiation(): void 12, ], [ - 'Constant GREGORIAN is not allowed for parameter #2 $dateType of class IntlDateFormatter constructor.', + 'Constant IntlDateFormatter::GREGORIAN is not allowed for parameter #2 $dateType of class IntlDateFormatter constructor.', 18, ], ]); diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php index 066ddca480d..43e4b0b3dca 100644 --- a/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php @@ -80,3 +80,18 @@ // round: single-value parameter - correct round(1.5, 0, PHP_ROUND_HALF_UP); + +class Foo +{ + private const PASSWORD_ALGORITHM = PASSWORD_ARGON2ID; + + // user-defined class constant wrapping a valid constant - should not report + public function hashPassword(string $password): string + { + return password_hash($password, self::PASSWORD_ALGORITHM); + } +} + +// user-defined global constant wrapping a valid constant - should not report +define('MY_JSON_FLAGS', JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); +json_encode([], MY_JSON_FLAGS); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 4e385c4612c..4ce54156970 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3933,11 +3933,11 @@ public function testConstantParameterCheckMethods(): void 10, ], [ - 'Constant ATTR_ERRMODE is not allowed for parameter #1 $mode of method PDOStatement::fetch().', + 'Constant PDO::ATTR_ERRMODE is not allowed for parameter #1 $mode of method PDOStatement::fetch().', 17, ], [ - 'Constant FRENCH_COLLATION is not allowed for parameter #2 $flags of method Collator::sort().', + 'Constant Collator::FRENCH_COLLATION is not allowed for parameter #2 $flags of method Collator::sort().', 25, ], ]); diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 9345fce6418..9f368c7507a 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1003,11 +1003,11 @@ public function testConstantParameterCheckStatic(): void $this->checkThisOnly = false; $this->analyse([__DIR__ . '/data/constant-parameter-check-static.php'], [ [ - 'Constant GREGORIAN is not allowed for parameter #2 $dateType of static method IntlDateFormatter::create().', + 'Constant IntlDateFormatter::GREGORIAN is not allowed for parameter #2 $dateType of static method IntlDateFormatter::create().', 9, ], [ - 'Constant TYPE_INT32 is not allowed for parameter #2 $style of static method NumberFormatter::create().', + 'Constant NumberFormatter::TYPE_INT32 is not allowed for parameter #2 $style of static method NumberFormatter::create().', 15, ], ]); From 261cf495ff479369cb97db0dc519fd69256d42ee Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 20 Mar 2026 17:18:10 +0100 Subject: [PATCH 38/70] Allow custom rules to emit collector data for CollectedDataNode --- src/Analyser/CollectedDataEmitter.php | 42 +++++++++++++ src/Analyser/FileAnalyserCallback.php | 8 ++- src/Analyser/MutatingScope.php | 20 ++++++- src/Node/EmitCollectedDataNode.php | 60 +++++++++++++++++++ src/Rules/Methods/OverridingMethodRule.php | 5 +- src/Rules/Playground/PromoteParameterRule.php | 3 +- src/Rules/Rule.php | 3 +- src/Testing/CompositeRule.php | 3 +- src/Testing/DelayedRule.php | 3 +- .../Rules/CollectedDataEmitterRule.php | 33 ++++++++++ .../Rules/CollectedDataEmitterTest.php | 33 ++++++++++ 11 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 src/Analyser/CollectedDataEmitter.php create mode 100644 src/Node/EmitCollectedDataNode.php create mode 100644 tests/PHPStan/Rules/CollectedDataEmitterRule.php create mode 100644 tests/PHPStan/Rules/CollectedDataEmitterTest.php diff --git a/src/Analyser/CollectedDataEmitter.php b/src/Analyser/CollectedDataEmitter.php new file mode 100644 index 00000000000..1f5be5f4def --- /dev/null +++ b/src/Analyser/CollectedDataEmitter.php @@ -0,0 +1,42 @@ +emitCollectedData(MyCollector::class, ['some', 'data']); + * ``` + * + * @api + */ +interface CollectedDataEmitter +{ + + /** + * @template TCollector of Collector + * @param class-string $collectorType + * @param template-type $data + */ + public function emitCollectedData(string $collectorType, mixed $data): void; + +} diff --git a/src/Analyser/FileAnalyserCallback.php b/src/Analyser/FileAnalyserCallback.php index 304513f7f4e..07e5371a04e 100644 --- a/src/Analyser/FileAnalyserCallback.php +++ b/src/Analyser/FileAnalyserCallback.php @@ -11,6 +11,7 @@ use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\RootExportedNode; +use PHPStan\Node\EmitCollectedDataNode; use PHPStan\Node\InClassNode; use PHPStan\Node\InTraitNode; use PHPStan\Parser\Parser; @@ -77,9 +78,14 @@ public function __construct( public function __invoke(Node $node, Scope $scope): void { + if ($node instanceof EmitCollectedDataNode) { + $this->fileCollectedData[$scope->getFile()][$node->getCollectorType()][] = $node->getData(); + return; + } + $parserNodes = $this->parserNodes; - /** @var Scope&NodeCallbackInvoker $scope */ + /** @var Scope&NodeCallbackInvoker&CollectedDataEmitter $scope */ if ($node instanceof Node\Stmt\Trait_) { foreach (array_keys($this->linesToIgnore[$this->file] ?? []) as $lineToIgnore) { if ($lineToIgnore < $node->getStartLine() || $lineToIgnore > $node->getEndLine()) { diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 42fe96957d5..8e598d6d7bc 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -23,7 +23,9 @@ use PhpParser\Node\Stmt\Function_; use PhpParser\NodeFinder; use PHPStan\Analyser\Traverser\TransformStaticTypeTraverser; +use PHPStan\Collectors\Collector; use PHPStan\DependencyInjection\Container; +use PHPStan\Node\EmitCollectedDataNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Expr\GetIterableKeyTypeExpr; use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr; @@ -133,7 +135,7 @@ use const PHP_INT_MIN; use const PHP_VERSION_ID; -class MutatingScope implements Scope, NodeCallbackInvoker +class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter { public const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid'; @@ -4624,4 +4626,20 @@ public function invokeNodeCallback(Node $node): void $nodeCallback($node, $this); } + /** + * @template TNodeType of Node + * @template TValue + * @param class-string> $collectorType + * @param TValue $data + */ + public function emitCollectedData(string $collectorType, mixed $data): void + { + $nodeCallback = $this->nodeCallback; + if ($nodeCallback === null) { + throw new ShouldNotHappenException('Node callback is not present in this scope'); + } + + $nodeCallback(new EmitCollectedDataNode($collectorType, $data), $this); + } + } diff --git a/src/Node/EmitCollectedDataNode.php b/src/Node/EmitCollectedDataNode.php new file mode 100644 index 00000000000..b8b235adefd --- /dev/null +++ b/src/Node/EmitCollectedDataNode.php @@ -0,0 +1,60 @@ +> $collectorType + * @param TValue $data + */ + public function __construct( + private string $collectorType, + private mixed $data, + ) + { + parent::__construct([]); + } + + /** + * @return class-string> + */ + public function getCollectorType(): string + { + return $this->collectorType; + } + + /** + * @return TValue + */ + public function getData(): mixed + { + return $this->data; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_EmitCollectedDataNode'; + } + + /** + * @return list + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index f9023059fdc..40f458953b0 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Attribute; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -49,7 +50,7 @@ public function getNodeType(): string return InClassMethodNode::class; } - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $method = $node->getMethodReflection(); $prototypeData = $this->methodPrototypeFinder->findPrototype($node->getClassReflection(), $method->getName()); @@ -329,7 +330,7 @@ private function filterOverrideAttribute(array $attrGroups): array private function addErrors( array $errors, InClassMethodNode $classMethod, - Scope&NodeCallbackInvoker $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { if (count($errors) > 0) { diff --git a/src/Rules/Playground/PromoteParameterRule.php b/src/Rules/Playground/PromoteParameterRule.php index 8dc1d329165..d01b77c530e 100644 --- a/src/Rules/Playground/PromoteParameterRule.php +++ b/src/Rules/Playground/PromoteParameterRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Playground; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\Container; @@ -88,7 +89,7 @@ private function getOriginalRule(): ?Rule return $this->originalRule = $originalRule; } - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if ($this->parameterValue) { return []; diff --git a/src/Rules/Rule.php b/src/Rules/Rule.php index 03a5a047b03..fd1d3333e50 100644 --- a/src/Rules/Rule.php +++ b/src/Rules/Rule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; @@ -35,6 +36,6 @@ public function getNodeType(): string; * @param TNodeType $node * @return list */ - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array; + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array; } diff --git a/src/Testing/CompositeRule.php b/src/Testing/CompositeRule.php index c83fb047b52..269ed259cc4 100644 --- a/src/Testing/CompositeRule.php +++ b/src/Testing/CompositeRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Testing; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\Rules\DirectRegistry; @@ -37,7 +38,7 @@ public function getNodeType(): string return Node::class; } - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; diff --git a/src/Testing/DelayedRule.php b/src/Testing/DelayedRule.php index 27b35cb2f40..17909e3721b 100644 --- a/src/Testing/DelayedRule.php +++ b/src/Testing/DelayedRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Testing; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\Rules\DirectRegistry; @@ -43,7 +44,7 @@ public function getDelayedErrors(): array return $this->errors; } - public function processNode(Node $node, Scope&NodeCallbackInvoker $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $nodeType = get_class($node); foreach ($this->registry->getRules($nodeType) as $rule) { diff --git a/tests/PHPStan/Rules/CollectedDataEmitterRule.php b/tests/PHPStan/Rules/CollectedDataEmitterRule.php new file mode 100644 index 00000000000..f8a08ca1b6e --- /dev/null +++ b/tests/PHPStan/Rules/CollectedDataEmitterRule.php @@ -0,0 +1,33 @@ + + */ +final class CollectedDataEmitterRule implements Rule +{ + + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + public function processNode(Node $node, NodeCallbackInvoker&Scope&CollectedDataEmitter $scope): array + { + // same implementation as DummyCollector, but is actually a rule! + if (!$node->name instanceof Node\Identifier) { + return []; + } + + $scope->emitCollectedData(DummyCollector::class, $node->name->toString()); + + return []; + } + +} diff --git a/tests/PHPStan/Rules/CollectedDataEmitterTest.php b/tests/PHPStan/Rules/CollectedDataEmitterTest.php new file mode 100644 index 00000000000..99c642e4579 --- /dev/null +++ b/tests/PHPStan/Rules/CollectedDataEmitterTest.php @@ -0,0 +1,33 @@ + + */ +class CollectedDataEmitterTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + // @phpstan-ignore argument.type + return new CompositeRule([ + new CollectedDataEmitterRule(), + new DummyCollectorRule(), + ]); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/dummy-collector.php'], [ + [ + '2× doFoo, 2× doBar', + 5, + ], + ]); + } + +} From 4c1a2b9883287bfca99ebc32a8b3eefccae03b50 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Mar 2026 09:32:21 +0100 Subject: [PATCH 39/70] Detect named arguments whose parameters are renamed in subtypes --- src/Analyser/MutatingScope.php | 8 +- src/Analyser/Scope.php | 4 + src/Rules/AttributesCheck.php | 5 +- src/Rules/Classes/ClassAttributesRule.php | 4 +- .../Classes/ClassConstantAttributesRule.php | 4 +- src/Rules/Classes/InstantiationRule.php | 7 +- .../Constants/ConstantAttributesRule.php | 4 +- .../EnumCases/EnumCaseAttributesRule.php | 4 +- src/Rules/FunctionCallParametersCheck.php | 13 +++- .../Functions/ArrowFunctionAttributesRule.php | 4 +- src/Rules/Functions/CallCallablesRule.php | 5 +- .../CallToFunctionParametersRule.php | 5 +- src/Rules/Functions/CallUserFuncRule.php | 5 +- src/Rules/Functions/ClosureAttributesRule.php | 4 +- .../Functions/FunctionAttributesRule.php | 4 +- src/Rules/Functions/ParamAttributesRule.php | 4 +- src/Rules/Methods/CallMethodsRule.php | 10 ++- src/Rules/Methods/CallStaticMethodsRule.php | 7 +- .../Methods/ConsistentConstructorRule.php | 6 +- src/Rules/Methods/MethodAttributesRule.php | 4 +- .../MethodParameterComparisonHelper.php | 22 +++++- ...dArgumentParameterMethodCallsCollector.php | 26 +++++++ ...rridingMethodRenamesParameterCollector.php | 26 +++++++ src/Rules/Methods/OverridingMethodRule.php | 2 +- .../Properties/PropertyAttributesRule.php | 4 +- .../Properties/PropertyHookAttributesRule.php | 4 +- src/Rules/Traits/TraitAttributesRule.php | 4 +- ...sRuleNamedArgumentRenamedParameterTest.php | 60 ++++++++++++++ ...llWithPossiblyRenamedNamedArgumentRule.php | 78 +++++++++++++++++++ .../data/named-argument-renamed-parameter.php | 26 +++++++ 30 files changed, 334 insertions(+), 29 deletions(-) create mode 100644 src/Rules/Methods/NamedArgumentParameterMethodCallsCollector.php create mode 100644 src/Rules/Methods/OverridingMethodRenamesParameterCollector.php create mode 100644 tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php create mode 100644 tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php create mode 100644 tests/PHPStan/Rules/Methods/data/named-argument-renamed-parameter.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8e598d6d7bc..5fe47361448 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -146,10 +146,10 @@ class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter /** @var Type[] */ private array $resolvedTypes = []; - /** @var array */ + /** @var array */ private array $truthyScopes = []; - /** @var array */ + /** @var array */ private array $falseyScopes = []; private ?self $fiberScope = null; @@ -3115,6 +3115,9 @@ public function filterByFalseyValue(Expr $expr): self return $scope; } + /** + * @return static + */ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self { $typeSpecifications = []; @@ -3224,6 +3227,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } } + /** @var static */ return $scope->scopeFactory->create( $scope->context, $scope->isDeclareStrictTypes(), diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 6f616acd802..8e78a30d4c2 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -307,6 +307,8 @@ public function isUndefinedExpressionAllowed(Expr $expr): bool; * if-branch of `if ($x instanceof Foo)`. * * Uses the TypeSpecifier internally to determine type narrowing. + * + * @return static */ public function filterByTruthyValue(Expr $expr): self; @@ -316,6 +318,8 @@ public function filterByTruthyValue(Expr $expr): self; * The opposite of filterByTruthyValue(). Given `$x instanceof Foo`, returns * a scope where $x is known NOT to be of type Foo. This is the scope used * in the else-branch of `if ($x instanceof Foo)`. + * + * @return static */ public function filterByFalseyValue(Expr $expr): self; diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 3d8d8edf038..ee47292ed5c 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -5,6 +5,8 @@ use Attribute; use PhpParser\Node\AttributeGroup; use PhpParser\Node\Expr\New_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; @@ -36,7 +38,7 @@ public function __construct( * @return list */ public function check( - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, array $attrGroups, int $requiredTarget, string $targetName, @@ -160,6 +162,7 @@ public function check( 'Constant %s is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', 'Constants %s cannot be combined for %s of attribute class ' . $attributeClassName . ' constructor.', 'Combining constants with | is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', + null, ); foreach ($parameterErrors as $error) { diff --git a/src/Rules/Classes/ClassAttributesRule.php b/src/Rules/Classes/ClassAttributesRule.php index 197987ddf1c..9ceea3ce4f1 100644 --- a/src/Rules/Classes/ClassAttributesRule.php +++ b/src/Rules/Classes/ClassAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassNode; @@ -30,7 +32,7 @@ public function getNodeType(): string return InClassNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $classReflection = $node->getClassReflection(); diff --git a/src/Rules/Classes/ClassConstantAttributesRule.php b/src/Rules/Classes/ClassConstantAttributesRule.php index 3beaf3d0ea3..a6c0506443f 100644 --- a/src/Rules/Classes/ClassConstantAttributesRule.php +++ b/src/Rules/Classes/ClassConstantAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; @@ -25,7 +27,7 @@ public function getNodeType(): string return Node\Stmt\ClassConst::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index fc4d2e13fc5..296bb78954a 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\New_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\Container; @@ -58,7 +60,7 @@ public function getNodeType(): string return New_::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; foreach ($this->getClassNames($node, $scope) as [$class, $isName]) { @@ -71,7 +73,7 @@ public function processNode(Node $node, Scope $scope): array * @param Node\Expr\New_ $node * @return list */ - private function checkClassName(string $class, bool $isName, Node $node, Scope $scope): array + private function checkClassName(string $class, bool $isName, Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $lowercasedClass = strtolower($class); $messages = []; @@ -272,6 +274,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $ 'Constant %s is not allowed for %s of class ' . $classDisplayName . ' constructor.', 'Constants %s cannot be combined for %s of class ' . $classDisplayName . ' constructor.', 'Combining constants with | is not allowed for %s of class ' . $classDisplayName . ' constructor.', + null, )); } diff --git a/src/Rules/Constants/ConstantAttributesRule.php b/src/Rules/Constants/ConstantAttributesRule.php index f944a7981ee..f076769bf9b 100644 --- a/src/Rules/Constants/ConstantAttributesRule.php +++ b/src/Rules/Constants/ConstantAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Php\PhpVersion; @@ -31,7 +33,7 @@ public function getNodeType(): string return Node\Stmt\Const_::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if ($node->attrGroups === []) { return []; diff --git a/src/Rules/EnumCases/EnumCaseAttributesRule.php b/src/Rules/EnumCases/EnumCaseAttributesRule.php index f6489f2e871..b0e670af317 100644 --- a/src/Rules/EnumCases/EnumCaseAttributesRule.php +++ b/src/Rules/EnumCases/EnumCaseAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; @@ -25,7 +27,7 @@ public function getNodeType(): string return Node\Stmt\EnumCase::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 10a5c6ab226..66f7390a8d7 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -4,7 +4,9 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; @@ -14,6 +16,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Reflection\ResolvedFunctionVariant; +use PHPStan\Rules\Methods\NamedArgumentParameterMethodCallsCollector; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; @@ -32,6 +35,7 @@ use function array_fill; use function array_key_exists; use function array_last; +use function array_merge; use function count; use function implode; use function in_array; @@ -65,11 +69,12 @@ public function __construct( /** * @param 'attribute'|'callable'|'method'|'staticMethod'|'function'|'new' $nodeType + * @param array{class-string, string}|null $renamedNamedArgumentParameterData * @return list */ public function check( ParametersAcceptor $parametersAcceptor, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, bool $isBuiltin, Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall, string $nodeType, @@ -92,6 +97,7 @@ public function check( string $invalidConstantMessage, string $exclusiveConstantsMessage, string $bitmaskNotAllowedMessage, + ?array $renamedNamedArgumentParameterData, ): array { if ($funcCall instanceof Node\Expr\MethodCall || $funcCall instanceof Node\Expr\StaticCall || $funcCall instanceof Node\Expr\FuncCall) { @@ -359,6 +365,11 @@ public function check( ->build(); } } + } elseif ($argumentName !== null && $renamedNamedArgumentParameterData !== null) { + $scope->emitCollectedData(NamedArgumentParameterMethodCallsCollector::class, array_merge( + $renamedNamedArgumentParameterData, + [$parameter->getName(), $argumentLine], + )); } if ($this->checkArgumentTypes) { diff --git a/src/Rules/Functions/ArrowFunctionAttributesRule.php b/src/Rules/Functions/ArrowFunctionAttributesRule.php index 092758eaad9..862583c6c55 100644 --- a/src/Rules/Functions/ArrowFunctionAttributesRule.php +++ b/src/Rules/Functions/ArrowFunctionAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InArrowFunctionNode; @@ -26,7 +28,7 @@ public function getNodeType(): string return InArrowFunctionNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 342498bacd6..85c33490b1b 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Functions; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -47,7 +49,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { if (!$node->name instanceof Node\Expr) { @@ -142,6 +144,7 @@ public function processNode( 'Constant %s is not allowed for %s of ' . $callableDescription . '.', 'Constants %s cannot be combined for %s of ' . $callableDescription . '.', 'Combining constants with | is not allowed for %s of ' . $callableDescription . '.', + null, ), ); } diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 515ea46fed8..f01a081fdae 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; @@ -28,7 +30,7 @@ public function getNodeType(): string return FuncCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!($node->name instanceof Node\Name)) { return []; @@ -71,6 +73,7 @@ public function processNode(Node $node, Scope $scope): array 'Constant %s is not allowed for %s of function ' . $functionName . '.', 'Constants %s cannot be combined for %s of function ' . $functionName . '.', 'Combining constants with | is not allowed for %s of function ' . $functionName . '.', + null, ); } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index da834fa226e..0cb9d144b7d 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -5,6 +5,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Reflection\ReflectionProvider; @@ -32,7 +34,7 @@ public function getNodeType(): string return FuncCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node->name instanceof Node\Name) { return []; @@ -87,6 +89,7 @@ public function processNode(Node $node, Scope $scope): array 'Constant %s is not allowed for %s of ' . $callableDescription . '.', 'Constants %s cannot be combined for %s of ' . $callableDescription . '.', 'Combining constants with | is not allowed for %s of ' . $callableDescription . '.', + null, ); } diff --git a/src/Rules/Functions/ClosureAttributesRule.php b/src/Rules/Functions/ClosureAttributesRule.php index d9dd348f9c3..54ee5218644 100644 --- a/src/Rules/Functions/ClosureAttributesRule.php +++ b/src/Rules/Functions/ClosureAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClosureNode; @@ -26,7 +28,7 @@ public function getNodeType(): string return InClosureNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Functions/FunctionAttributesRule.php b/src/Rules/Functions/FunctionAttributesRule.php index a7b6547cb01..605982c5865 100644 --- a/src/Rules/Functions/FunctionAttributesRule.php +++ b/src/Rules/Functions/FunctionAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InFunctionNode; @@ -26,7 +28,7 @@ public function getNodeType(): string return InFunctionNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Functions/ParamAttributesRule.php b/src/Rules/Functions/ParamAttributesRule.php index ad67abb22b7..c04f2452bbb 100644 --- a/src/Rules/Functions/ParamAttributesRule.php +++ b/src/Rules/Functions/ParamAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\AttributesCheck; @@ -25,7 +27,7 @@ public function getNodeType(): string return Node\Param::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $targetName = 'parameter'; $targetType = Attribute::TARGET_PARAMETER; diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index e2d8f9e4a3d..51881bfbea2 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Scalar\String_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; @@ -34,7 +36,7 @@ public function getNodeType(): string return MethodCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; if ($node->name instanceof Node\Identifier) { @@ -62,7 +64,7 @@ public function processNode(Node $node, Scope $scope): array /** * @return list */ - private function processSingleMethodCall(Scope $scope, MethodCall $node, string $methodName): array + private function processSingleMethodCall(Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, MethodCall $node, string $methodName): array { [$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var, $node->name); if ($methodReflection === null) { @@ -102,6 +104,10 @@ private function processSingleMethodCall(Scope $scope, MethodCall $node, string 'Constant %s is not allowed for %s of method ' . $messagesMethodName . '.', 'Constants %s cannot be combined for %s of method ' . $messagesMethodName . '.', 'Combining constants with | is not allowed for %s of method ' . $messagesMethodName . '.', + !$methodReflection->isPrivate() && !$declaringClass->isFinal() ? [ + $declaringClass->getName(), + $methodReflection->getName(), + ] : null, )); } diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index c19aaff2b72..a275d3fb731 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Scalar\String_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; @@ -35,7 +37,7 @@ public function getNodeType(): string return StaticCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; if ($node->name instanceof Node\Identifier) { @@ -63,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array /** * @return list */ - private function processSingleMethodCall(Scope $scope, StaticCall $node, string $methodName): array + private function processSingleMethodCall(Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, StaticCall $node, string $methodName): array { [$errors, $method] = $this->methodCallCheck->check($scope, $methodName, $node->class, $node->name); if ($method === null) { @@ -111,6 +113,7 @@ private function processSingleMethodCall(Scope $scope, StaticCall $node, string 'Constant %s is not allowed for %s of ' . $lowercasedMethodName . '.', 'Constants %s cannot be combined for %s of ' . $lowercasedMethodName . '.', 'Combining constants with | is not allowed for %s of ' . $lowercasedMethodName . '.', + null, )); return $errors; diff --git a/src/Rules/Methods/ConsistentConstructorRule.php b/src/Rules/Methods/ConsistentConstructorRule.php index 5eace25f3a6..e3eb6d1513d 100644 --- a/src/Rules/Methods/ConsistentConstructorRule.php +++ b/src/Rules/Methods/ConsistentConstructorRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Methods; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; @@ -29,7 +31,7 @@ public function getNodeType(): string return InClassMethodNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $method = $node->getMethodReflection(); if (strtolower($method->getName()) !== '__construct') { @@ -47,7 +49,7 @@ public function processNode(Node $node, Scope $scope): array } return array_merge( - $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true), + $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, $scope, true), $this->methodVisibilityComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method), ); } diff --git a/src/Rules/Methods/MethodAttributesRule.php b/src/Rules/Methods/MethodAttributesRule.php index 56bb6016a1b..ecec4569c95 100644 --- a/src/Rules/Methods/MethodAttributesRule.php +++ b/src/Rules/Methods/MethodAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InClassMethodNode; @@ -26,7 +28,7 @@ public function getNodeType(): string return InClassMethodNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { return $this->attributesCheck->check( $scope, diff --git a/src/Rules/Methods/MethodParameterComparisonHelper.php b/src/Rules/Methods/MethodParameterComparisonHelper.php index 41a9c985cfb..c86a76cacfc 100644 --- a/src/Rules/Methods/MethodParameterComparisonHelper.php +++ b/src/Rules/Methods/MethodParameterComparisonHelper.php @@ -2,6 +2,9 @@ namespace PHPStan\Rules\Methods; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; +use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ClassReflection; @@ -33,7 +36,13 @@ public function __construct(private PhpVersion $phpVersion) /** * @return list */ - public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method, bool $ignorable): array + public function compare( + ExtendedMethodReflection $prototype, + ClassReflection $prototypeDeclaringClass, + PhpMethodFromParserNodeReflection $method, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + bool $ignorable, + ): array { /** @var list $messages */ $messages = []; @@ -64,6 +73,17 @@ public function compare(ExtendedMethodReflection $prototype, ClassReflection $pr } $methodParameter = $methodParameters[$i]; + if ($prototype->acceptsNamedArguments()->yes()) { + if ($prototypeParameter->getName() !== $methodParameter->getName()) { + $scope->emitCollectedData(OverridingMethodRenamesParameterCollector::class, [ + $prototypeDeclaringClass->getName(), + $prototype->getName(), + $method->getDeclaringClass()->getName(), + $prototypeParameter->getName(), + $methodParameter->getName(), + ]); + } + } if ($prototypeParameter->passedByReference()->no()) { if (!$methodParameter->passedByReference()->no()) { $error = RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Methods/NamedArgumentParameterMethodCallsCollector.php b/src/Rules/Methods/NamedArgumentParameterMethodCallsCollector.php new file mode 100644 index 00000000000..6da034b7617 --- /dev/null +++ b/src/Rules/Methods/NamedArgumentParameterMethodCallsCollector.php @@ -0,0 +1,26 @@ + + */ +final class NamedArgumentParameterMethodCallsCollector implements Collector +{ + + public function getNodeType(): string + { + throw new ShouldNotHappenException(); + } + + public function processNode(Node $node, Scope $scope) + { + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Methods/OverridingMethodRenamesParameterCollector.php b/src/Rules/Methods/OverridingMethodRenamesParameterCollector.php new file mode 100644 index 00000000000..d076548882b --- /dev/null +++ b/src/Rules/Methods/OverridingMethodRenamesParameterCollector.php @@ -0,0 +1,26 @@ + + */ +final class OverridingMethodRenamesParameterCollector implements Collector +{ + + public function getNodeType(): string + { + throw new ShouldNotHappenException(); + } + + public function processNode(Node $node, Scope $scope) + { + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 40f458953b0..e1916a59f46 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -226,7 +226,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE } } - $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method, false)); + $messages = array_merge($messages, $this->methodParameterComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method, $scope, false)); if (!$prototypeVariant instanceof ExtendedFunctionVariant) { return $this->addErrors($messages, $node, $scope); diff --git a/src/Rules/Properties/PropertyAttributesRule.php b/src/Rules/Properties/PropertyAttributesRule.php index 4ce2080d863..c6dbc29b452 100644 --- a/src/Rules/Properties/PropertyAttributesRule.php +++ b/src/Rules/Properties/PropertyAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassPropertyNode; @@ -32,7 +34,7 @@ public function getNodeType(): string return ClassPropertyNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$this->phpVersion->supportsOverrideAttributeOnProperty()) { $propertyReflection = $node->getClassReflection()->getNativeProperty($node->getName()); diff --git a/src/Rules/Properties/PropertyHookAttributesRule.php b/src/Rules/Properties/PropertyHookAttributesRule.php index 2eb1c11f604..79e48aa03fa 100644 --- a/src/Rules/Properties/PropertyHookAttributesRule.php +++ b/src/Rules/Properties/PropertyHookAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InPropertyHookNode; @@ -29,7 +31,7 @@ public function getNodeType(): string return InPropertyHookNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $attrGroups = $node->getOriginalNode()->attrGroups; $errors = $this->attributesCheck->check( diff --git a/src/Rules/Traits/TraitAttributesRule.php b/src/Rules/Traits/TraitAttributesRule.php index 8006203180d..7d6c6fd6e75 100644 --- a/src/Rules/Traits/TraitAttributesRule.php +++ b/src/Rules/Traits/TraitAttributesRule.php @@ -4,6 +4,8 @@ use Attribute; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\InTraitNode; @@ -32,7 +34,7 @@ public function getNodeType(): string return InTraitNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$this->phpVersion->supportsDeprecatedTraits()) { if (count($node->getTraitReflection()->getNativeReflection()->getAttributes('Deprecated')) > 0) { diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php new file mode 100644 index 00000000000..3a7773abbc9 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php @@ -0,0 +1,60 @@ + + */ +#[RequiresPhp('>= 8.0')] +class CallMethodsRuleNamedArgumentRenamedParameterTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper($reflectionProvider, checkNullables: true, checkThisOnly: false, checkUnionTypes: true, checkExplicitMixed: true, checkImplicitMixed: false, checkBenevolentUnionTypes: false, discoveringSymbolsTip: true); + $phpVersion = self::getContainer()->getByType(PhpVersion::class); + $phpClassReflectionExtension = self::getContainer()->getByType(PhpClassReflectionExtension::class); + + // @phpstan-ignore argument.type + return new CompositeRule([ + new CallMethodsRule( + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, true, true, true, true), + ), + new OverridingMethodRule( + $phpVersion, + new MethodSignatureRule(new ParentMethodHelper($phpClassReflectionExtension), true, true), + false, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + new MethodPrototypeFinder($phpVersion, $phpClassReflectionExtension), + false, + ), + new MethodCallWithPossiblyRenamedNamedArgumentRule(), + ]); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/named-argument-renamed-parameter.php'], [ + [ + 'Call to NamedArgumentRenamedParameter\Foo::doFoo() uses named argument for parameter $a, but NamedArgumentRenamedParameter\Bar renames it to $b.', + 25, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php new file mode 100644 index 00000000000..55d7e9d8ad1 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php @@ -0,0 +1,78 @@ + + */ +#[RegisteredRule(level: 0)] +final class MethodCallWithPossiblyRenamedNamedArgumentRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, NodeCallbackInvoker&Scope&CollectedDataEmitter $scope): array + { + $calls = []; + foreach ($node->get(NamedArgumentParameterMethodCallsCollector::class) as $file => $data) { + foreach ($data as [$declaringClassName, $methodName, $parameterName, $callLine]) { + $calls[$declaringClassName][$methodName][$parameterName][] = [$file, $callLine]; + } + } + + $errors = []; + foreach ($node->get(OverridingMethodRenamesParameterCollector::class) as $data) { + foreach ($data as [$prototypeDeclaringClassName, $methodName, $methodDeclaringClassName, $prototypeParameterName, $methodParameterName]) { + if (!array_key_exists($prototypeDeclaringClassName, $calls)) { + continue; + } + + $prototypeClassCalls = $calls[$prototypeDeclaringClassName]; + if (!array_key_exists($methodName, $prototypeClassCalls)) { + continue; + } + + $prototypeMethodCalls = $prototypeClassCalls[$methodName]; + if (!array_key_exists($prototypeParameterName, $prototypeMethodCalls)) { + continue; + } + + if (!array_key_exists($prototypeParameterName, $prototypeMethodCalls)) { + continue; + } + + $callsWithParameter = $prototypeMethodCalls[$prototypeParameterName]; + foreach ($callsWithParameter as [$file, $line]) { + $errors[] = RuleErrorBuilder::message(sprintf( + 'Call to %s::%s() uses named argument for parameter $%s, but %s renames it to $%s.', + $prototypeDeclaringClassName, + $methodName, + $prototypeParameterName, + $methodDeclaringClassName, + $methodParameterName, + ))->identifier('argument.parameterRenamedInSubtype') + ->file($file) + ->line($line) + ->build(); + } + } + } + + return $errors; + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/named-argument-renamed-parameter.php b/tests/PHPStan/Rules/Methods/data/named-argument-renamed-parameter.php new file mode 100644 index 00000000000..72a5b96c2c8 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/named-argument-renamed-parameter.php @@ -0,0 +1,26 @@ += 8.0 + +declare(strict_types = 1); + +namespace NamedArgumentRenamedParameter; + +interface Foo +{ + + public function doFoo(string $a): void; + +} + +class Bar implements Foo +{ + + public function doFoo(string $b): void + { + + } + +} + +function (Foo $foo): void { + $foo->doFoo(a: 'a'); +}; From b16af79131c02329e75e15f23975ca0705243b5b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Mar 2026 12:35:48 +0100 Subject: [PATCH 40/70] Regression test Closes https://github.com/phpstan/phpstan/issues/7434 --- ...sRuleNamedArgumentRenamedParameterTest.php | 10 +++++++ tests/PHPStan/Rules/Methods/data/bug-7434.php | 29 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-7434.php diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php index 3a7773abbc9..e2a51f4e2fd 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php @@ -57,4 +57,14 @@ public function testRule(): void ]); } + public function testBug7434(): void + { + $this->analyse([__DIR__ . '/data/bug-7434.php'], [ + [ + 'Call to Bug7434\Contract::method() uses named argument for parameter $val, but Bug7434\ImplementationWithDifferentName renames it to $wrong.', + 28, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-7434.php b/tests/PHPStan/Rules/Methods/data/bug-7434.php new file mode 100644 index 00000000000..be1750fdfbc --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7434.php @@ -0,0 +1,29 @@ +method(val: 'string'); +} From 0fadf7539f875aceec934c1c948bc48aed0a2813 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Mar 2026 12:46:14 +0100 Subject: [PATCH 41/70] Fix --- .../Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tests/PHPStan => src}/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php (100%) diff --git a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php b/src/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php similarity index 100% rename from tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php rename to src/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRule.php From 4f8c37f9a6f516c74eb307d0776c64bb2f98a505 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Mar 2026 14:15:17 +0100 Subject: [PATCH 42/70] Fix false negative - json_decode with JSON_THROW_ON_ERROR being passed to depth should report argument.invalidConstant --- src/Rules/FunctionCallParametersCheck.php | 88 ++++++++++++------- .../CallToFunctionParametersRuleTest.php | 4 + .../data/constant-parameter-check.php | 7 ++ 3 files changed, 69 insertions(+), 30 deletions(-) diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 66f7390a8d7..b4c866e419c 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -108,7 +108,15 @@ public function check( $functionParametersMinCount = 0; $functionParametersMaxCount = 0; + $hasAllowedConstants = false; foreach ($parametersAcceptor->getParameters() as $parameter) { + if ( + $parameter instanceof ExtendedParameterReflection + && !$hasAllowedConstants + && $parameter->getAllowedConstants() !== null + ) { + $hasAllowedConstants = true; + } if (!$parameter->isOptional()) { $functionParametersMinCount++; } @@ -426,40 +434,55 @@ public function check( if ( $parameter instanceof ExtendedParameterReflection - && $parameter->getAllowedConstants() !== null && $scope->getPhpVersion()->supportsNamedArguments()->yes() ) { $constantReflections = $this->resolveConstantReflections($argumentValue, $scope); if ($constantReflections !== null) { - $result = $parameter->checkAllowedConstants($constantReflections); - foreach ($result->getDisallowedConstants() as $disallowedConstant) { - $errors[] = RuleErrorBuilder::message(sprintf( - $invalidConstantMessage, - $disallowedConstant->describe(), - lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), - )) - ->identifier('argument.invalidConstant') - ->line($argumentLine) - ->build(); - } - foreach ($result->getViolatedExclusiveGroups() as $group) { - $errors[] = RuleErrorBuilder::message(sprintf( - $exclusiveConstantsMessage, - implode(', ', $group), - lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), - )) - ->identifier('argument.exclusiveConstants') - ->line($argumentLine) - ->build(); - } - if ($result->isBitmaskNotAllowed()) { - $errors[] = RuleErrorBuilder::message(sprintf( - $bitmaskNotAllowedMessage, - lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), - )) - ->identifier('argument.bitmaskNotAllowed') - ->line($argumentLine) - ->build(); + if ($parameter->getAllowedConstants() !== null) { + $result = $parameter->checkAllowedConstants($constantReflections); + foreach ($result->getDisallowedConstants() as $disallowedConstant) { + $errors[] = RuleErrorBuilder::message(sprintf( + $invalidConstantMessage, + $disallowedConstant->describe(), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.invalidConstant') + ->line($argumentLine) + ->build(); + } + foreach ($result->getViolatedExclusiveGroups() as $group) { + $errors[] = RuleErrorBuilder::message(sprintf( + $exclusiveConstantsMessage, + implode(', ', $group), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.exclusiveConstants') + ->line($argumentLine) + ->build(); + } + if ($result->isBitmaskNotAllowed()) { + $errors[] = RuleErrorBuilder::message(sprintf( + $bitmaskNotAllowedMessage, + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.bitmaskNotAllowed') + ->line($argumentLine) + ->build(); + } + } elseif ($isBuiltin && $hasAllowedConstants) { + foreach ($constantReflections as $constantReflection) { + if ($constantReflection->isBuiltin()->no()) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf( + $invalidConstantMessage, + $constantReflection->describe(), + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.invalidConstant') + ->line($argumentLine) + ->build(); + } } } } @@ -764,6 +787,11 @@ private function describeParameter(ParameterReflection $parameter, int|string|nu private function resolveConstantReflections(Expr $expr, Scope $scope): ?array { if ($expr instanceof Expr\ConstFetch) { + $lowerName = $expr->name->toLowerString(); + if (in_array($lowerName, ['null', 'true', 'false'], true)) { + return null; + } + if (!$this->reflectionProvider->hasConstant($expr->name, $scope)) { return null; } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 9a539b30a89..20c6a565c72 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2824,6 +2824,10 @@ public function testConstantParameterCheck(): void 'Combining constants with | is not allowed for parameter #2 $filter of function filter_var.', 79, ], + [ + 'Constant JSON_THROW_ON_ERROR is not allowed for parameter #3 $depth of function json_decode.', + 99, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php index 43e4b0b3dca..c7609983552 100644 --- a/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php @@ -95,3 +95,10 @@ public function hashPassword(string $password): string // user-defined global constant wrapping a valid constant - should not report define('MY_JSON_FLAGS', JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); json_encode([], MY_JSON_FLAGS); + +json_decode('{}', null, JSON_THROW_ON_ERROR); + +// passing true/false/null should not report +json_decode($json, true); +json_decode($json, null); +json_decode($json, false); From 74913dadbf5f2cbef1033a0a168146f2f458d41e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Mar 2026 17:35:09 +0100 Subject: [PATCH 43/70] Update map --- resources/constantToFunctionParameterMap.php | 64 ++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/resources/constantToFunctionParameterMap.php b/resources/constantToFunctionParameterMap.php index db150a3ad73..2712df60579 100644 --- a/resources/constantToFunctionParameterMap.php +++ b/resources/constantToFunctionParameterMap.php @@ -569,6 +569,38 @@ 'FILTER_CALLBACK', ], ], + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILTER_REQUIRE_SCALAR', + 'FILTER_REQUIRE_ARRAY', + 'FILTER_FORCE_ARRAY', + 'FILTER_NULL_ON_FAILURE', + 'FILTER_FLAG_NONE', + 'FILTER_FLAG_ALLOW_OCTAL', + 'FILTER_FLAG_ALLOW_HEX', + 'FILTER_FLAG_STRIP_LOW', + 'FILTER_FLAG_STRIP_HIGH', + 'FILTER_FLAG_STRIP_BACKTICK', + 'FILTER_FLAG_ENCODE_LOW', + 'FILTER_FLAG_ENCODE_HIGH', + 'FILTER_FLAG_ENCODE_AMP', + 'FILTER_FLAG_NO_ENCODE_QUOTES', + 'FILTER_FLAG_EMPTY_STRING_NULL', + 'FILTER_FLAG_ALLOW_FRACTION', + 'FILTER_FLAG_ALLOW_THOUSAND', + 'FILTER_FLAG_ALLOW_SCIENTIFIC', + 'FILTER_FLAG_PATH_REQUIRED', + 'FILTER_FLAG_QUERY_REQUIRED', + 'FILTER_FLAG_IPV4', + 'FILTER_FLAG_IPV6', + 'FILTER_FLAG_NO_RES_RANGE', + 'FILTER_FLAG_NO_PRIV_RANGE', + 'FILTER_FLAG_GLOBAL_RANGE', + 'FILTER_FLAG_HOSTNAME', + 'FILTER_FLAG_EMAIL_UNICODE', + ], + ], ], 'filter_input' => [ @@ -609,6 +641,38 @@ 'FILTER_CALLBACK', ], ], + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'FILTER_REQUIRE_SCALAR', + 'FILTER_REQUIRE_ARRAY', + 'FILTER_FORCE_ARRAY', + 'FILTER_NULL_ON_FAILURE', + 'FILTER_FLAG_NONE', + 'FILTER_FLAG_ALLOW_OCTAL', + 'FILTER_FLAG_ALLOW_HEX', + 'FILTER_FLAG_STRIP_LOW', + 'FILTER_FLAG_STRIP_HIGH', + 'FILTER_FLAG_STRIP_BACKTICK', + 'FILTER_FLAG_ENCODE_LOW', + 'FILTER_FLAG_ENCODE_HIGH', + 'FILTER_FLAG_ENCODE_AMP', + 'FILTER_FLAG_NO_ENCODE_QUOTES', + 'FILTER_FLAG_EMPTY_STRING_NULL', + 'FILTER_FLAG_ALLOW_FRACTION', + 'FILTER_FLAG_ALLOW_THOUSAND', + 'FILTER_FLAG_ALLOW_SCIENTIFIC', + 'FILTER_FLAG_PATH_REQUIRED', + 'FILTER_FLAG_QUERY_REQUIRED', + 'FILTER_FLAG_IPV4', + 'FILTER_FLAG_IPV6', + 'FILTER_FLAG_NO_RES_RANGE', + 'FILTER_FLAG_NO_PRIV_RANGE', + 'FILTER_FLAG_GLOBAL_RANGE', + 'FILTER_FLAG_HOSTNAME', + 'FILTER_FLAG_EMAIL_UNICODE', + ], + ], ], 'filter_input_array' => [ From 69e056c13fcd25f116d41f8f7cf84aa02f0ef3e3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Mar 2026 17:47:36 +0100 Subject: [PATCH 44/70] Improve map --- resources/constantToFunctionParameterMap.php | 31 +++++++++++++++++++ .../Rules/Classes/InstantiationRuleTest.php | 4 +++ 2 files changed, 35 insertions(+) diff --git a/resources/constantToFunctionParameterMap.php b/resources/constantToFunctionParameterMap.php index 2712df60579..6766e6be3f7 100644 --- a/resources/constantToFunctionParameterMap.php +++ b/resources/constantToFunctionParameterMap.php @@ -2512,4 +2512,35 @@ ], ], ], + + // RecursiveIteratorIterator + + 'RecursiveIteratorIterator::__construct' => [ + 'mode' => [ + 'type' => 'single', + 'constants' => [ + 'RecursiveIteratorIterator::LEAVES_ONLY', + 'RecursiveIteratorIterator::SELF_FIRST', + 'RecursiveIteratorIterator::CHILD_FIRST', + ], + ], + 'flags' => [ + 'type' => 'bitmask', + 'constants' => [ + 'RecursiveIteratorIterator::CATCH_GET_CHILD', + ], + ], + ], + + // DatePeriod + + 'DatePeriod::__construct' => [ + 'options' => [ + 'type' => 'bitmask', + 'constants' => [ + 'DatePeriod::EXCLUDE_START_DATE', + 'DatePeriod::INCLUDE_END_DATE', + ], + ], + ], ]; diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index fdeb2a27900..7517916e09d 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -456,6 +456,10 @@ public function testBug9946(): void public function testBug10324(): void { $this->analyse([__DIR__ . '/data/bug-10324.php'], [ + [ + 'Constant RecursiveIteratorIterator::CHILD_FIRST is not allowed for parameter #3 $flags of class RecursiveIteratorIterator constructor.', + 23, + ], [ 'Parameter #3 $flags of class RecursiveIteratorIterator constructor expects 0|16, 2 given.', 23, From ea62732f7a885af6f4673b2cf1135defa22f6ad5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Mar 2026 17:52:11 +0100 Subject: [PATCH 45/70] Parameter type has to have the same type as parameters with described allowed constants to report passed constant as an error --- src/Rules/FunctionCallParametersCheck.php | 12 ++++++++---- .../Functions/data/constant-parameter-check.php | 3 +++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index b4c866e419c..80c3f72c454 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -108,14 +108,13 @@ public function check( $functionParametersMinCount = 0; $functionParametersMaxCount = 0; - $hasAllowedConstants = false; + $allowedConstantsTypes = []; foreach ($parametersAcceptor->getParameters() as $parameter) { if ( $parameter instanceof ExtendedParameterReflection - && !$hasAllowedConstants && $parameter->getAllowedConstants() !== null ) { - $hasAllowedConstants = true; + $allowedConstantsTypes[] = $parameter->getType(); } if (!$parameter->isOptional()) { $functionParametersMinCount++; @@ -124,6 +123,11 @@ public function check( $functionParametersMaxCount++; } + $allowedConstantsType = null; + if (count($allowedConstantsTypes) > 0) { + $allowedConstantsType = TypeCombinator::union(...$allowedConstantsTypes); + } + if ($parametersAcceptor->isVariadic()) { $functionParametersMaxCount = -1; } @@ -469,7 +473,7 @@ public function check( ->line($argumentLine) ->build(); } - } elseif ($isBuiltin && $hasAllowedConstants) { + } elseif ($isBuiltin && $allowedConstantsType !== null && $allowedConstantsType->isSuperTypeOf($parameterType)->yes()) { foreach ($constantReflections as $constantReflection) { if ($constantReflection->isBuiltin()->no()) { continue; diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php index c7609983552..70878d4eee7 100644 --- a/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check.php @@ -102,3 +102,6 @@ public function hashPassword(string $password): string json_decode($json, true); json_decode($json, null); json_decode($json, false); + +// PHP_OS passed to $subject of preg_match - should not report +preg_match('/foo/', PHP_OS); From 9531b297801d2100af701991a6414c7313c7d5bc Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 22 Mar 2026 18:05:35 +0100 Subject: [PATCH 46/70] Fix test --- .../PHPStan/Rules/Classes/InstantiationRuleTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 7517916e09d..43e79cf13f6 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -453,7 +453,19 @@ public function testBug9946(): void $this->analyse([__DIR__ . '/data/bug-9946.php'], []); } + #[RequiresPhp('< 8.0')] public function testBug10324(): void + { + $this->analyse([__DIR__ . '/data/bug-10324.php'], [ + [ + 'Parameter #3 $flags of class RecursiveIteratorIterator constructor expects 0|16, 2 given.', + 23, + ], + ]); + } + + #[RequiresPhp('>= 8.0')] + public function testBug10324On80(): void { $this->analyse([__DIR__ . '/data/bug-10324.php'], [ [ From c8a72b2f62947b9df82ef34d2c3905ba35363d31 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 23 Mar 2026 09:32:15 +0100 Subject: [PATCH 47/70] Rename test --- ...p => MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/PHPStan/Rules/Methods/{CallMethodsRuleNamedArgumentRenamedParameterTest.php => MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php} (96%) diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php similarity index 96% rename from tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php rename to tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php index e2a51f4e2fd..b5ecc3f37c4 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleNamedArgumentRenamedParameterTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php @@ -18,7 +18,7 @@ * @extends RuleTestCase */ #[RequiresPhp('>= 8.0')] -class CallMethodsRuleNamedArgumentRenamedParameterTest extends RuleTestCase +class MethodCallWithPossiblyRenamedNamedArgumentRuleTest extends RuleTestCase { protected function getRule(): Rule From 9c3b6d53e640363c198761cdfbf271c2d2c022f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Tue, 24 Mar 2026 11:24:26 +0100 Subject: [PATCH 48/70] New types: decimal-int-string and non-decimal-int-string --- phpstan-baseline.neon | 8 +- src/PhpDoc/TypeNodeResolver.php | 10 + src/Type/Accessory/AccessoryArrayListType.php | 5 + .../AccessoryDecimalIntegerStringType.php | 450 ++++++++++++++++++ .../Accessory/AccessoryLiteralStringType.php | 5 + .../AccessoryLowercaseStringType.php | 5 + .../Accessory/AccessoryNonEmptyStringType.php | 5 + .../Accessory/AccessoryNonFalsyStringType.php | 5 + .../Accessory/AccessoryNumericStringType.php | 5 + .../AccessoryUppercaseStringType.php | 5 + src/Type/Accessory/HasOffsetType.php | 5 + src/Type/Accessory/HasOffsetValueType.php | 5 + src/Type/Accessory/NonEmptyArrayType.php | 5 + src/Type/Accessory/OversizedArrayType.php | 5 + src/Type/CallableType.php | 5 + src/Type/ClassStringType.php | 5 + src/Type/ClosureType.php | 5 + src/Type/Constant/ConstantStringType.php | 5 + src/Type/FloatType.php | 5 + src/Type/IntersectionType.php | 12 + src/Type/IterableType.php | 5 + src/Type/JustNullableTypeTrait.php | 5 + src/Type/MixedType.php | 17 + src/Type/NeverType.php | 5 + src/Type/NullType.php | 5 + src/Type/ObjectType.php | 5 + src/Type/StaticType.php | 5 + src/Type/StrictMixedType.php | 5 + src/Type/StringType.php | 5 + src/Type/Traits/ArrayTypeTrait.php | 5 + src/Type/Traits/LateResolvableTypeTrait.php | 5 + src/Type/Traits/ObjectTypeTrait.php | 5 + src/Type/Type.php | 11 + src/Type/TypeCombinator.php | 32 ++ src/Type/UnionType.php | 5 + src/Type/VerbosityLevel.php | 2 + src/Type/VoidType.php | 5 + .../Analyser/nsrt/decimal-int-string.php | 48 ++ .../Type/Constant/ConstantStringTypeTest.php | 59 +++ tests/PHPStan/Type/IntersectionTypeTest.php | 25 + tests/PHPStan/Type/StringTypeTest.php | 78 ++- tests/PHPStan/Type/TypeCombinatorTest.php | 91 ++++ tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 11 + 43 files changed, 997 insertions(+), 2 deletions(-) create mode 100644 src/Type/Accessory/AccessoryDecimalIntegerStringType.php create mode 100644 tests/PHPStan/Analyser/nsrt/decimal-int-string.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6906e22da9d..1847333fb30 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -777,6 +777,12 @@ parameters: count: 1 path: src/Type/Accessory/AccessoryArrayListType.php + - + rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. + identifier: phpstanApi.instanceofType + count: 1 + path: src/Type/Accessory/AccessoryDecimalIntegerStringType.php + - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType @@ -1716,7 +1722,7 @@ parameters: - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType - count: 3 + count: 4 path: src/Type/TypeCombinator.php - diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 8af74bf9264..815789377a7 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -47,6 +47,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -235,6 +236,15 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco case 'string': return new StringType(); + case 'decimal-int-string': + return new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]); + + case 'non-decimal-int-string': + return new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ]); + case 'lowercase-string': return new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]); diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index ff3536f4735..9e2bd72230b 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -385,6 +385,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryDecimalIntegerStringType.php b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php new file mode 100644 index 00000000000..b04cbc13efe --- /dev/null +++ b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php @@ -0,0 +1,450 @@ +isDecimalIntegerString(); + + if ( + $type->isString()->yes() + && ($this->inverse ? $isDecimalIntegerString->no() : $isDecimalIntegerString->yes()) + ) { + return AcceptsResult::createYes(); + } + + if ($type instanceof CompoundType) { + return $type->isAcceptedBy($this, $strictTypes); + } + + $result = $type->isString()->and($this->inverse ? $isDecimalIntegerString->negate() : $isDecimalIntegerString); + + return new AcceptsResult($result, []); + } + + public function isSuperTypeOf(Type $type): IsSuperTypeOfResult + { + if ($type instanceof CompoundType) { + return $type->isSubTypeOf($this); + } + + if ($this->equals($type)) { + return IsSuperTypeOfResult::createYes(); + } + + $isDecimalIntegerString = $type->isDecimalIntegerString(); + $result = $type->isString()->and($this->inverse ? $isDecimalIntegerString->negate() : $isDecimalIntegerString); + + return new IsSuperTypeOfResult($result, []); + } + + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult + { + if ($otherType instanceof UnionType || $otherType instanceof IntersectionType) { + return $otherType->isSuperTypeOf($this); + } + + if ($otherType instanceof AccessoryNumericStringType && !$this->inverse) { + return IsSuperTypeOfResult::createYes(); + } + + $otherTypeResult = $otherType->isString()->and($this->inverse ? $otherType->isDecimalIntegerString()->negate() : $otherType->isDecimalIntegerString()); + + return new IsSuperTypeOfResult( + $otherTypeResult->and($otherType->equals($this) ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe()), + [], + ); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); + } + + public function equals(Type $type): bool + { + return $type instanceof self && $this->inverse === $type->inverse; + } + + public function describe(VerbosityLevel $level): string + { + return $this->inverse ? 'non-decimal-int-string' : 'decimal-int-string'; + } + + public function isOffsetAccessible(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isOffsetAccessLegal(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function hasOffsetValueType(Type $offsetType): TrinaryLogic + { + return $offsetType->isInteger()->and(TrinaryLogic::createMaybe()); + } + + public function getOffsetValueType(Type $offsetType): Type + { + if ($this->hasOffsetValueType($offsetType)->no()) { + return new ErrorType(); + } + + return new StringType(); + } + + public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type + { + $stringOffset = (new StringType())->setOffsetValueType($offsetType, $valueType, $unionValues); + + if ($stringOffset instanceof ErrorType) { + return $stringOffset; + } + + return $this; + } + + public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type + { + return $this; + } + + public function unsetOffset(Type $offsetType): Type + { + return new ErrorType(); + } + + public function toNumber(): Type + { + if ($this->inverse) { + return new UnionType([ + $this->toInteger(), + $this->toFloat(), + ]); + } + + return $this->toInteger(); + } + + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + + public function toBoolean(): BooleanType + { + return $this->isNonFalsyString()->negate()->toBooleanType(); + } + + public function toInteger(): Type + { + return new IntegerType(); + } + + public function toFloat(): Type + { + return new FloatType(); + } + + public function toString(): Type + { + return $this; + } + + public function toArray(): Type + { + return new ConstantArrayType( + [new ConstantIntegerType(0)], + [$this], + [1], + isList: TrinaryLogic::createYes(), + ); + } + + public function toArrayKey(): Type + { + if ($this->inverse) { + return new StringType(); + } + + return new IntegerType(); + } + + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); + } + + return $this; + } + + public function isNull(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isConstantValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isConstantScalarValue(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function getConstantScalarTypes(): array + { + return []; + } + + public function getConstantScalarValues(): array + { + return []; + } + + public function isCallable(): TrinaryLogic + { + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createNo(); + } + + public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array + { + if ($this->inverse) { + return [new TrivialParametersAcceptor()]; + } + + throw new ShouldNotHappenException(); + } + + public function isTrue(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFalse(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isBoolean(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isFloat(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isInteger(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isString(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function isNumericString(): TrinaryLogic + { + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(); + } + + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean(!$this->inverse); + } + + public function isNonEmptyString(): TrinaryLogic + { + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(); + } + + public function isNonFalsyString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLiteralString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isLowercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isUppercaseString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + + public function isClassString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function getClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function getObjectTypeOrClassStringObjectType(): Type + { + return new ErrorType(); + } + + public function isVoid(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + public function isScalar(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } + + public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType + { + if ($type->isNull()->yes()) { + return new ConstantBooleanType(false); + } + + if ($type->isString()->yes()) { + if ($this->inverse) { + if ($type->isDecimalIntegerString()->yes()) { + return new ConstantBooleanType(false); + } + } elseif ($type->isDecimalIntegerString()->no()) { + return new ConstantBooleanType(false); + } + } + + return new BooleanType(); + } + + public function traverse(callable $cb): Type + { + return $this; + } + + public function traverseSimultaneously(Type $right, callable $cb): Type + { + return $this; + } + + public function generalize(GeneralizePrecision $precision): Type + { + return new StringType(); + } + + public function exponentiate(Type $exponent): Type + { + return new BenevolentUnionType([ + new FloatType(), + new IntegerType(), + ]); + } + + public function getFiniteTypes(): array + { + return []; + } + + public function toPhpDocNode(): TypeNode + { + return new IdentifierTypeNode($this->inverse ? 'non-decimal-int-string' : 'decimal-int-string'); + } + + public function hasTemplateOrLateResolvableType(): bool + { + return false; + } + +} diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index da1abf5e370..f0e9b7c033b 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -290,6 +290,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 2e5ca831461..826b6f706b5 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -287,6 +287,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 2499084ba4c..1238abc7dcb 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -287,6 +287,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 9f2eebdbd2d..ffc8f2b9b4b 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -290,6 +290,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 88a811bc1be..62e320e5de5 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -287,6 +287,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createYes(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index a85c74745be..683b6dec981 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -287,6 +287,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index b6757fefb62..20256f2731f 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -295,6 +295,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 66872dc2f3e..0a16fb5801f 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -383,6 +383,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 688da67695a..fa3980c769b 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -372,6 +372,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index 4956e879926..847bddeccfc 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -349,6 +349,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 380c87982a6..9c654578884 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -688,6 +688,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index c5dae4f1958..2c22f319db0 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -49,6 +49,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 0a380dff93e..884256d1d29 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -790,6 +790,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 40d0773c48d..a3b06036c7f 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -329,6 +329,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createFromBoolean(is_numeric($this->getValue())); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createFromBoolean((string) (int) $this->value === $this->value); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->getValue() !== ''); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 5df57fe7955..b792753083e 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -216,6 +216,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 05b6a14753f..c11768bf4c7 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -24,6 +24,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -403,6 +404,7 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes) || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType + || $type instanceof AccessoryDecimalIntegerStringType ) { if ( ($type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType) @@ -802,6 +804,11 @@ public function isNumericString(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); } + public function isDecimalIntegerString(): TrinaryLogic + { + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString()); + } + public function isNonEmptyString(): TrinaryLogic { if ($this->isCallable()->yes() && $this->isString()->yes()) { @@ -1295,6 +1302,10 @@ public function toArray(): Type public function toArrayKey(): Type { + if ($this->isDecimalIntegerString()->yes()) { + return new IntegerType(); + } + if ($this->isNumericString()->yes()) { return TypeCombinator::union( new IntegerType(), @@ -1478,6 +1489,7 @@ public function toPhpDocNode(): TypeNode || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLowercaseStringType || $type instanceof AccessoryUppercaseStringType + || $type instanceof AccessoryDecimalIntegerStringType ) { if ($type instanceof AccessoryNonFalsyStringType) { $nonFalsyStr = true; diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 2cf46b754e9..8911cbdf49f 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -367,6 +367,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/JustNullableTypeTrait.php b/src/Type/JustNullableTypeTrait.php index 5435c540ff0..9697954fae3 100644 --- a/src/Type/JustNullableTypeTrait.php +++ b/src/Type/JustNullableTypeTrait.php @@ -124,6 +124,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 0c1892e01eb..ce5634548af 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -20,6 +20,7 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -938,6 +939,22 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + if ($this->subtractedType !== null) { + $decimalIntegerString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]); + + if ($this->subtractedType->isSuperTypeOf($decimalIntegerString)->yes()) { + return TrinaryLogic::createNo(); + } + } + + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index 2da6f5e9fa2..40676536a7b 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -504,6 +504,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 5c7730ee9f7..42915cb2025 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -285,6 +285,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index f90a09d6908..9097a8943e5 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1303,6 +1303,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 682bc77d300..c9b2fb225b3 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -653,6 +653,11 @@ public function isNumericString(): TrinaryLogic return $this->getStaticObjectType()->isNumericString(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return $this->getStaticObjectType()->isDecimalIntegerString(); + } + public function isNonEmptyString(): TrinaryLogic { return $this->getStaticObjectType()->isNonEmptyString(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index af20367941f..f85dcba9c0e 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -286,6 +286,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 730869022fc..03361300331 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -232,6 +232,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createMaybe(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Traits/ArrayTypeTrait.php b/src/Type/Traits/ArrayTypeTrait.php index a019125c3f0..0a73a101626 100644 --- a/src/Type/Traits/ArrayTypeTrait.php +++ b/src/Type/Traits/ArrayTypeTrait.php @@ -140,6 +140,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 4b0dacddd72..5d171b751b4 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -494,6 +494,11 @@ public function isNumericString(): TrinaryLogic return $this->resolve()->isNumericString(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return $this->resolve()->isDecimalIntegerString(); + } + public function isNonEmptyString(): TrinaryLogic { return $this->resolve()->isNonEmptyString(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 51a4922f43f..3dc499e2f31 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -225,6 +225,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 9af6fcf203c..0f759655af6 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -392,6 +392,17 @@ public function isString(): TrinaryLogic; public function isNumericString(): TrinaryLogic; + /** + * When isDecimalIntegerString() returns yes(), the type + * is guaranteed to be cast to an integer in an array key. + * Examples of constant values covered by this type: "0", "1", "1234", "-1" + * + * When isDecimalIntegerString() returns no(), the type represents strings containing non-decimal integers and other text. + * These are guaranteed to stay as string in an array key. + * Examples of constant values covered by this type: "+1", "00", "18E+3", "1.2", "1,3", "foo" + */ + public function isDecimalIntegerString(): TrinaryLogic; + public function isNonEmptyString(): TrinaryLogic; /** diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index e7a0d6bdf52..443e8f203b2 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -4,6 +4,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryType; @@ -26,6 +27,7 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateUnionType; +use function array_filter; use function array_key_exists; use function array_key_first; use function array_merge; @@ -562,6 +564,24 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array } } + // numeric-string | non-decimal-int-string → string (preserving common accessories) + // Works because decimal-int-string ⊂ numeric-string, so together they cover all strings + if ($a->isString()->yes() && $b->isString()->yes()) { + $decimalIntString = new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]); + if ($b->isDecimalIntegerString()->no()) { + $bBase = self::removeDecimalIntStringAccessory($b); + if ($bBase->isSuperTypeOf($a)->yes() && $a->isSuperTypeOf($decimalIntString)->yes()) { + return [null, $bBase]; + } + } + if ($a->isDecimalIntegerString()->no()) { + $aBase = self::removeDecimalIntStringAccessory($a); + if ($aBase->isSuperTypeOf($b)->yes() && $b->isSuperTypeOf($decimalIntString)->yes()) { + return [$aBase, null]; + } + } + } + return null; } @@ -581,6 +601,18 @@ private static function getAccessoryCaseStringTypes(Type $type): array return $accessory; } + private static function removeDecimalIntStringAccessory(Type $type): Type + { + if (!$type instanceof IntersectionType) { + return $type; + } + + return self::intersect(...array_filter( + $type->getTypes(), + static fn (Type $t): bool => !$t instanceof AccessoryDecimalIntegerStringType, + )); + } + private static function unionWithSubtractedType( Type $type, ?Type $subtractedType, diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 1348e27dc5f..9a210eab14d 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -701,6 +701,11 @@ public function isNumericString(): TrinaryLogic return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString()); } + public function isDecimalIntegerString(): TrinaryLogic + { + return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString()); + } + public function isNonEmptyString(): TrinaryLogic { return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString()); diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index 32be9683a81..73513641eef 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -156,6 +157,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc || $type instanceof AccessoryNonFalsyStringType || $type instanceof AccessoryLiteralStringType || $type instanceof AccessoryNumericStringType + || $type instanceof AccessoryDecimalIntegerStringType || $type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType ) { diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index ca864245e67..f84c733abc5 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -189,6 +189,11 @@ public function isNumericString(): TrinaryLogic return TrinaryLogic::createNo(); } + public function isDecimalIntegerString(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + public function isNonEmptyString(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/tests/PHPStan/Analyser/nsrt/decimal-int-string.php b/tests/PHPStan/Analyser/nsrt/decimal-int-string.php new file mode 100644 index 00000000000..63747525028 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/decimal-int-string.php @@ -0,0 +1,48 @@ + 1]; + assertType('non-empty-array', $a); + + assertType('bool', (bool) $s); + + assertType('int', $s + $s); + } + + /** + * @param non-decimal-int-string $s + */ + public function doBar(string $s): void + { + assertType('non-decimal-int-string' ,$s); + $a = [$s => 1]; + assertType('non-empty-array', $a); + + assertType('bool', (bool) $s); + + assertType('float|int', $s + $s); + } + + /** + * @param non-decimal-int-string $s + */ + public function emptyStringIsNonDecimal(string $s): void + { + if ($s === '') { + assertType("''", $s); // '' is a valid non-decimal-int-string + } + } + +} diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index d07d72d48af..801b2d1772e 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -185,4 +185,63 @@ public function testSetInvalidValue(): void $this->assertInstanceOf(ErrorType::class, $result); } + public static function dataIsDecimalIntegerString(): iterable + { + yield [ + '0', + TrinaryLogic::createYes(), + ]; + yield [ + '1', + TrinaryLogic::createYes(), + ]; + yield [ + '1234', + TrinaryLogic::createYes(), + ]; + yield [ + '-1', + TrinaryLogic::createYes(), + ]; + yield [ + '+1', + TrinaryLogic::createNo(), + ]; + yield [ + '00', + TrinaryLogic::createNo(), + ]; + yield [ + '01', + TrinaryLogic::createNo(), + ]; + yield [ + '18E+3', + TrinaryLogic::createNo(), + ]; + yield [ + '1.2', + TrinaryLogic::createNo(), + ]; + yield [ + '1,3', + TrinaryLogic::createNo(), + ]; + yield [ + 'foo', + TrinaryLogic::createNo(), + ]; + yield [ + '1foo', + TrinaryLogic::createNo(), + ]; + } + + #[DataProvider('dataIsDecimalIntegerString')] + public function testIsDecimalIntegerString(string $value, TrinaryLogic $expected): void + { + $type = new ConstantStringType($value); + $this->assertSame($expected->describe(), $type->isDecimalIntegerString()->describe()); + } + } diff --git a/tests/PHPStan/Type/IntersectionTypeTest.php b/tests/PHPStan/Type/IntersectionTypeTest.php index e3ed23eb46b..a10aa80ee49 100644 --- a/tests/PHPStan/Type/IntersectionTypeTest.php +++ b/tests/PHPStan/Type/IntersectionTypeTest.php @@ -8,6 +8,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Accessory\HasOffsetType; @@ -747,6 +748,30 @@ public static function dataDescribe(): iterable VerbosityLevel::precise(), 'uppercase-string', ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]), + VerbosityLevel::typeOnly(), + 'string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]), + VerbosityLevel::typeOnly(), + 'string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]), + VerbosityLevel::value(), + 'decimal-int-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]), + VerbosityLevel::value(), + 'non-decimal-int-string', + ]; } #[DataProvider('dataDescribe')] diff --git a/tests/PHPStan/Type/StringTypeTest.php b/tests/PHPStan/Type/StringTypeTest.php index 3be9e03240a..205f6a81c0b 100644 --- a/tests/PHPStan/Type/StringTypeTest.php +++ b/tests/PHPStan/Type/StringTypeTest.php @@ -5,6 +5,7 @@ use Exception; use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; @@ -173,12 +174,87 @@ public static function dataAccepts(): iterable )->toArgument(), TrinaryLogic::createYes(), ]; + + $decimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]); + $nonDecimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ]); + + yield [ + $decimalIntString, + new ConstantStringType('1'), + TrinaryLogic::createYes(), + ]; + yield [ + $decimalIntString, + $decimalIntString, + TrinaryLogic::createYes(), + ]; + yield [ + $decimalIntString, + $nonDecimalIntString, + TrinaryLogic::createNo(), + ]; + yield [ + $nonDecimalIntString, + $decimalIntString, + TrinaryLogic::createNo(), + ]; + yield [ + $decimalIntString, + new StringType(), + TrinaryLogic::createMaybe(), + ]; + yield [ + $decimalIntString, + new ConstantStringType('10'), + TrinaryLogic::createYes(), + ]; + yield [ + $nonDecimalIntString, + new ConstantStringType('10'), + TrinaryLogic::createNo(), + ]; + yield [ + $decimalIntString, + new ConstantStringType('foo'), + TrinaryLogic::createNo(), + ]; + yield [ + $nonDecimalIntString, + new ConstantStringType('foo'), + TrinaryLogic::createYes(), + ]; + yield [ + $nonDecimalIntString, + new StringType(), + TrinaryLogic::createMaybe(), + ]; + yield [ + $nonDecimalIntString, + new ConstantStringType('1'), + TrinaryLogic::createNo(), + ]; + yield [ + $decimalIntString, + new ConstantStringType('+1'), + TrinaryLogic::createNo(), + ]; + yield [ + $nonDecimalIntString, + new ConstantStringType('+1'), + TrinaryLogic::createYes(), + ]; } #[DataProvider('dataAccepts')] public function testAccepts(Type $stringType, Type $otherType, TrinaryLogic $expectedResult): void { - $this->assertInstanceOf(StringType::class, $stringType); + $this->assertSame('Yes', $stringType->isString()->describe()); $actualResult = $stringType->accepts($otherType, true)->result; $this->assertSame( diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 6c51dbcd9b1..9ea18aac9b5 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -20,6 +20,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -2821,6 +2822,58 @@ public static function dataUnion(): iterable ObjectType::class, $nonFinalClass->getDisplayName(), ]; + + $decimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]); + $nonDecimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ]); + + yield [ + [ + $decimalIntString, + new StringType(), + ], + StringType::class, + 'string', + ]; + yield [ + [ + $nonDecimalIntString, + new StringType(), + ], + StringType::class, + 'string', + ]; + yield [ + [ + $decimalIntString, + $nonDecimalIntString, + ], + StringType::class, + 'string', + ]; + + yield [ + [ + $decimalIntString, + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + ], + IntersectionType::class, + 'numeric-string', + ]; + + yield [ + [ + $nonDecimalIntString, + new IntersectionType([new StringType(), new AccessoryNumericStringType()]), + ], + StringType::class, + 'string', + ]; } /** @@ -4933,6 +4986,44 @@ public static function dataIntersect(): iterable TemplateIntersectionType::class, 'T of Countable&Iterator (function a(), parameter)', ]; + + yield [ + [ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryDecimalIntegerStringType(), + ], + IntersectionType::class, + 'decimal-int-string', + ]; + + yield [ + [ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ], + IntersectionType::class, + 'non-decimal-int-string&numeric-string', + ]; + + $decimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(), + ]); + $nonDecimalIntString = new IntersectionType([ + new StringType(), + new AccessoryDecimalIntegerStringType(inverse: true), + ]); + + yield [ + [ + $decimalIntString, + $nonDecimalIntString, + ], + NeverType::class, + '*NEVER*=implicit', + ]; } /** diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 3cf9e5f3fa6..fcc6c6d0cf9 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -6,6 +6,7 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -522,6 +523,16 @@ public static function dataToPhpDocNodeWithoutCheckingEquals(): iterable new ConstantFloatType(-0.0), '-0.0', ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]), + 'decimal-int-string', + ]; + + yield [ + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]), + 'non-decimal-int-string', + ]; } #[DataProvider('dataToPhpDocNodeWithoutCheckingEquals')] From c4d39c9d36e29ad518ab45550b99ba13a4b17562 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 25 Mar 2026 10:44:24 +0100 Subject: [PATCH 49/70] issue-bot - support more options --- issue-bot/src/Console/RunCommand.php | 34 +++++++++++++++---- issue-bot/src/Playground/PlaygroundClient.php | 7 ++++ issue-bot/src/Playground/PlaygroundResult.php | 10 ++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index fb8ad112c02..2fe5a216d74 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -181,15 +181,35 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ } $tmpDir = sys_get_temp_dir() . '/phpstan-issue-bot-' . $result->getHash(); @mkdir($tmpDir, 0777, true); + + $options = $result->getOptions(); + $parameters = [ + 'level' => $result->getLevel(), + 'inferPrivatePropertyTypeFromConstructor' => $options['inferPrivatePropertyTypeFromConstructor'] ?? true, + 'treatPhpDocTypesAsCertain' => $result->isTreatPhpDocTypesAsCertain(), + 'phpVersion' => $phpVersion, + 'tmpDir' => $tmpDir, + 'rememberPossiblyImpureFunctionValues' => $options['rememberPossiblyImpureFunctionValues'] ?? true, + 'checkBenevolentUnionTypes' => $options['checkBenevolentUnionTypes'] ?? false, + 'checkTooWideReturnTypesInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, + 'checkTooWideParameterOutInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, + 'checkTooWideThrowTypesInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, + ]; + $parameters['exceptions'] = [ + 'implicitThrows' => $options['implicitThrows'] ?? true, + 'reportUncheckedExceptionDeadCatch' => $options['reportUncheckedExceptionDeadCatch'] ?? false, + 'uncheckedExceptionClasses' => $options['uncheckedExceptionClasses'] ?? [], + 'checkedExceptionClasses' => $options['checkedExceptionClasses'] ?? [], + 'check' => [ + 'missingCheckedExceptionInThrows' => $options['missingCheckedExceptionInThrows'] ?? false, + 'tooWideThrowType' => $options['tooWideThrowType'] ?? false, + 'tooWideImplicitThrowType' => $options['tooWideImplicitThrowType'] ?? false, + ], + ]; + $neon = Neon::encode([ 'includes' => $configFiles, - 'parameters' => [ - 'level' => $result->getLevel(), - 'inferPrivatePropertyTypeFromConstructor' => true, - 'treatPhpDocTypesAsCertain' => $result->isTreatPhpDocTypesAsCertain(), - 'phpVersion' => $phpVersion, - 'tmpDir' => $tmpDir, - ], + 'parameters' => $parameters, ]); $hash = $result->getHash(); diff --git a/issue-bot/src/Playground/PlaygroundClient.php b/issue-bot/src/Playground/PlaygroundClient.php index 43cd6ea9d37..6c09c8b1561 100644 --- a/issue-bot/src/Playground/PlaygroundClient.php +++ b/issue-bot/src/Playground/PlaygroundClient.php @@ -6,6 +6,7 @@ use Nette\Utils\Json; use function array_map; use function array_values; +use function is_array; use function sprintf; class PlaygroundClient @@ -27,6 +28,11 @@ public function getResult(string $hash): PlaygroundResult $versionedErrors[(int) $phpVersion] = array_map(static fn (array $error) => new PlaygroundError($error['line'] ?? -1, $error['message'], $error['identifier'] ?? null), array_values($errors)); } + $options = []; + if (isset($json['config']['options']) && is_array($json['config']['options'])) { + $options = $json['config']['options']; + } + return new PlaygroundResult( sprintf('https://phpstan.org/r/%s', $hash), $hash, @@ -35,6 +41,7 @@ public function getResult(string $hash): PlaygroundResult $json['config']['strictRules'], $json['config']['bleedingEdge'], $json['config']['treatPhpDocTypesAsCertain'], + $options, $versionedErrors, ); } diff --git a/issue-bot/src/Playground/PlaygroundResult.php b/issue-bot/src/Playground/PlaygroundResult.php index faddc6c078a..6a3b8cef092 100644 --- a/issue-bot/src/Playground/PlaygroundResult.php +++ b/issue-bot/src/Playground/PlaygroundResult.php @@ -6,6 +6,7 @@ class PlaygroundResult { /** + * @param array $options * @param array> $versionedErrors */ public function __construct( @@ -16,6 +17,7 @@ public function __construct( private bool $strictRules, private bool $bleedingEdge, private bool $treatPhpDocTypesAsCertain, + private array $options, private array $versionedErrors, ) { @@ -56,6 +58,14 @@ public function isTreatPhpDocTypesAsCertain(): bool return $this->treatPhpDocTypesAsCertain; } + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + /** * @return array> */ From c3d31d9dca6c042b2e8432caa5d8fc030a369f53 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 25 Mar 2026 10:56:31 +0100 Subject: [PATCH 50/70] Fix issue-bot --- .github/workflows/issue-bot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-bot.yml b/.github/workflows/issue-bot.yml index b0886ca8076..6c5a0518028 100644 --- a/.github/workflows/issue-bot.yml +++ b/.github/workflows/issue-bot.yml @@ -54,9 +54,9 @@ jobs: uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ./issue-bot/tmp - key: "issue-bot-download-v7-${{ github.run_id }}" + key: "issue-bot-download-v8-${{ github.run_id }}" restore-keys: | - issue-bot-download-v7- + issue-bot-download-v8- - name: "Download data" working-directory: "issue-bot" From 20830ac5a254601c1492f649bbf1c1cee9e91155 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 25 Mar 2026 18:10:03 +0100 Subject: [PATCH 51/70] tooWideThrowType is true by default --- issue-bot/src/Console/RunCommand.php | 1 - 1 file changed, 1 deletion(-) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 2fe5a216d74..8c7f0a4300e 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -202,7 +202,6 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ 'checkedExceptionClasses' => $options['checkedExceptionClasses'] ?? [], 'check' => [ 'missingCheckedExceptionInThrows' => $options['missingCheckedExceptionInThrows'] ?? false, - 'tooWideThrowType' => $options['tooWideThrowType'] ?? false, 'tooWideImplicitThrowType' => $options['tooWideImplicitThrowType'] ?? false, ], ]; From d4015ebbd114afefed634b7bfd017a525157cfdd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 12:27:41 +0100 Subject: [PATCH 52/70] Plumbing for reducing false positives about constant conditions in traits --- src/Analyser/RuleErrorTransformer.php | 5 + .../ConstantConditionInTraitCollector.php | 28 ++++++ .../ConstantConditionInTraitHelper.php | 81 ++++++++++++++++ .../ConstantConditionInTraitRule.php | 95 +++++++++++++++++++ src/Rules/RuleErrors/TransformedRuleError.php | 39 ++++++++ 5 files changed, 248 insertions(+) create mode 100644 src/Rules/Comparison/ConstantConditionInTraitCollector.php create mode 100644 src/Rules/Comparison/ConstantConditionInTraitHelper.php create mode 100644 src/Rules/Comparison/ConstantConditionInTraitRule.php create mode 100644 src/Rules/RuleErrors/TransformedRuleError.php diff --git a/src/Analyser/RuleErrorTransformer.php b/src/Analyser/RuleErrorTransformer.php index 12ad5043201..4213d4c6314 100644 --- a/src/Analyser/RuleErrorTransformer.php +++ b/src/Analyser/RuleErrorTransformer.php @@ -22,6 +22,7 @@ use PHPStan\Rules\MetadataRuleError; use PHPStan\Rules\NonIgnorableRuleError; use PHPStan\Rules\RuleError; +use PHPStan\Rules\RuleErrors\TransformedRuleError; use PHPStan\Rules\TipRuleError; use PHPStan\ShouldNotHappenException; use SebastianBergmann\Diff\Differ; @@ -55,6 +56,10 @@ public function transform( Node $node, ): Error { + if ($ruleError instanceof TransformedRuleError) { + return $ruleError->getError(); + } + $line = $node->getStartLine(); $canBeIgnored = true; $fileName = $scope->getFileDescription(); diff --git a/src/Rules/Comparison/ConstantConditionInTraitCollector.php b/src/Rules/Comparison/ConstantConditionInTraitCollector.php new file mode 100644 index 00000000000..7e2f75990d6 --- /dev/null +++ b/src/Rules/Comparison/ConstantConditionInTraitCollector.php @@ -0,0 +1,28 @@ +>, trait-string, string, null}|array{class-string>, trait-string, string, bool, Error|array}> + */ +final class ConstantConditionInTraitCollector implements Collector +{ + + public function getNodeType(): string + { + throw new ShouldNotHappenException(); + } + + public function processNode(Node $node, Scope $scope) + { + throw new ShouldNotHappenException(); + } + +} diff --git a/src/Rules/Comparison/ConstantConditionInTraitHelper.php b/src/Rules/Comparison/ConstantConditionInTraitHelper.php new file mode 100644 index 00000000000..31f70c70cc6 --- /dev/null +++ b/src/Rules/Comparison/ConstantConditionInTraitHelper.php @@ -0,0 +1,81 @@ +> $ruleName + */ + public function emitNoError( + string $ruleName, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + ): void + { + if (!$scope->isInTrait()) { + return; + } + + $exprString = sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine()); + $scope->emitCollectedData(ConstantConditionInTraitCollector::class, [ + $ruleName, + $scope->getTraitReflection()->getName(), + $exprString, + null, + ]); + } + + /** + * @param class-string> $ruleName + */ + public function emitError( + string $ruleName, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, + Expr $expr, + bool $value, + RuleError $ruleError, + ): void + { + if ($ruleError instanceof FixableNodeRuleError) { + throw new ShouldNotHappenException('Fixable errors are not supported by ConstantConditionInTraitHelper.'); + } + + if (!$scope->isInTrait()) { + return; + } + + $exprString = sprintf('%s:%d', $this->exprPrinter->printExpr($expr), $expr->getStartLine()); + $scope->emitCollectedData(ConstantConditionInTraitCollector::class, [ + $ruleName, + $scope->getTraitReflection()->getName(), + $exprString, + $value, + $this->ruleErrorTransformer->transform($ruleError, $scope, [], $expr), + ]); + } + +} diff --git a/src/Rules/Comparison/ConstantConditionInTraitRule.php b/src/Rules/Comparison/ConstantConditionInTraitRule.php new file mode 100644 index 00000000000..83ca84f220e --- /dev/null +++ b/src/Rules/Comparison/ConstantConditionInTraitRule.php @@ -0,0 +1,95 @@ + + */ +#[RegisteredRule(level: 0)] +final class ConstantConditionInTraitRule implements Rule +{ + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errorsByRuleTraitExprValue = []; + foreach ($node->get(ConstantConditionInTraitCollector::class) as $fileData) { + foreach ($fileData as $data) { + $ruleName = $data[0]; + $traitName = $data[1]; + $exprString = $data[2]; + $value = $data[3]; + $valueKey = var_export($value, true); + if ($data[3] === null) { + $errorsByRuleTraitExprValue[$ruleName][$traitName][$exprString][$valueKey][] = null; + // no error reported + continue; + } + + $error = $data[4]; + $errorsByRuleTraitExprValue[$ruleName][$traitName][$exprString][$valueKey][] = $error; + } + } + + $transformedErrors = []; + foreach ($errorsByRuleTraitExprValue as $ruleData) { + foreach ($ruleData as $traitData) { + foreach ($traitData as $valueData) { + if (count($valueData) > 1) { + continue; + } + + $uniquedErrors = []; + foreach ($valueData as $errors) { + foreach ($errors as $errorObject) { + if ($errorObject === null) { + continue; + } + if (is_array($errorObject)) { + $errorObject = Error::decode($errorObject); + } + + $message = $errorObject->getMessage(); + $uniquedErrors[$message] = $errorObject; + } + } + + $uniquedErrors = array_values($uniquedErrors); + if (count($uniquedErrors) === 0) { + continue; + } + + if (count($uniquedErrors) === 1) { + // report directly in trait, no "in context of" + $transformedErrors[] = new TransformedRuleError($uniquedErrors[0]); + continue; + } + + // report each error in its context + foreach ($uniquedErrors as $uniquedError) { + $transformedErrors[] = new TransformedRuleError($uniquedError); + } + } + } + } + + return $transformedErrors; + } + +} diff --git a/src/Rules/RuleErrors/TransformedRuleError.php b/src/Rules/RuleErrors/TransformedRuleError.php new file mode 100644 index 00000000000..0a69d7387fc --- /dev/null +++ b/src/Rules/RuleErrors/TransformedRuleError.php @@ -0,0 +1,39 @@ +error; + } + + public function getIdentifier(): string + { + $identifier = $this->error->getIdentifier(); + if ($identifier === null) { + throw new ShouldNotHappenException(); + } + + return $identifier; + } + + public function getMessage(): string + { + return $this->error->getMessage(); + } + +} From 7e4ade3e76a48ae6060b33f2b788432e225f8982 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 12:28:11 +0100 Subject: [PATCH 53/70] Use ConstantConditionInTraitHelper in StrictComparisonOfDifferentTypesRule --- build/baseline-8.0.neon | 12 ---- .../StrictComparisonOfDifferentTypesRule.php | 39 +++++++++---- ...rictComparisonOfDifferentTypesRuleTest.php | 32 +++++++--- .../data/strict-comparison-in-trait.php | 58 +++++++++++++++++++ 4 files changed, 109 insertions(+), 32 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/strict-comparison-in-trait.php diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon index 59bcf6e7156..e249b3ce3e6 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -12,24 +12,12 @@ parameters: count: 1 path: ../src/Type/ClosureTypeFactory.php - - - message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php - - message: '#^Strict comparison using \=\=\= between int\<0, max\> and false will always evaluate to false\.$#' identifier: identical.alwaysFalse count: 1 path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - - message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php - - message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' identifier: identical.alwaysFalse diff --git a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php index b8b0ca09db2..dc2889aa170 100644 --- a/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php +++ b/src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php @@ -3,7 +3,9 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; use PHPStan\Analyser\MutatingScope; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -28,6 +30,7 @@ final class StrictComparisonOfDifferentTypesRule implements Rule public function __construct( private RicherScopeGetTypeHelper $richerScopeGetTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -43,7 +46,7 @@ public function getNodeType(): string return Node\Expr\BinaryOp::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$scope instanceof MutatingScope) { throw new ShouldNotHappenException(); @@ -59,6 +62,7 @@ public function processNode(Node $node, Scope $scope): array $nodeType = $nodeTypeResult->type; if (!$nodeType instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -116,18 +120,26 @@ public function processNode(Node $node, Scope $scope): array } if (!$nodeType->getValue()) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Strict comparison using %s between %s and %s will always evaluate to false.', - $node->getOperatorSigil(), - $leftType->describe($verbosity), - $rightType->describe($verbosity), - )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Strict comparison using %s between %s and %s will always evaluate to false.', + $node->getOperatorSigil(), + $leftType->describe($verbosity), + $rightType->describe($verbosity), + )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical'))->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); + return []; + } return []; } @@ -150,10 +162,13 @@ public function processNode(Node $node, Scope $scope): array } $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Identical ? 'identical' : 'notIdentical')); + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } - return [ - $errorBuilder->build(), - ]; + return [$ruleError]; } } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index d5e7f59fd81..a62b33caa59 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Analyser\RicherScopeGetTypeHelper; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -11,7 +12,7 @@ use const PHP_VERSION_ID; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase { @@ -22,13 +23,18 @@ class StrictComparisonOfDifferentTypesRuleTest extends RuleTestCase protected function getRule(): Rule { - return new StrictComparisonOfDifferentTypesRule( - self::getContainer()->getByType(RicherScopeGetTypeHelper::class), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + // @phpstan-ignore argument.type + return new CompositeRule([ + new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -1156,4 +1162,14 @@ public function testPossiblyImpureTip(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/strict-comparison-in-trait.php'], [ + [ + 'Strict comparison using !== between string and null will always evaluate to true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/strict-comparison-in-trait.php b/tests/PHPStan/Rules/Comparison/data/strict-comparison-in-trait.php new file mode 100644 index 00000000000..ee3b08ecd11 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/strict-comparison-in-trait.php @@ -0,0 +1,58 @@ +doBar() !== null) { + + } + } + + public function doFoo2() + { + // always not nullable + if ($this->doBar2() !== null) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): string + { + + } + + public function doBar2(): string + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?string + { + + } + + public function doBar2(): string + { + + } + +} From 6b3f2d1097c3be2e39804ae8dcf17631f50aa49b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 13:46:13 +0100 Subject: [PATCH 54/70] Added ConstantConditionInTraitHelper to remaining rules --- .../BooleanAndConstantConditionRule.php | 44 ++++++++++-- .../BooleanNotConstantConditionRule.php | 16 +++-- .../BooleanOrConstantConditionRule.php | 44 ++++++++++-- .../ConstantLooseComparisonRule.php | 35 +++++++--- .../DoWhileLoopConstantConditionRule.php | 32 ++++++--- .../ElseIfConstantConditionRule.php | 14 +++- .../Comparison/IfConstantConditionRule.php | 26 ++++--- .../ImpossibleCheckTypeFunctionCallRule.php | 33 ++++++--- .../ImpossibleCheckTypeMethodCallRule.php | 35 +++++++--- ...mpossibleCheckTypeStaticMethodCallRule.php | 35 +++++++--- .../LogicalXorConstantConditionRule.php | 28 +++++++- src/Rules/Comparison/MatchExpressionRule.php | 23 +++++- ...mparisonOperatorsConstantConditionRule.php | 28 +++++--- .../TernaryOperatorConstantConditionRule.php | 22 ++++-- .../WhileLoopAlwaysFalseConditionRule.php | 20 ++++-- .../WhileLoopAlwaysTrueConditionRule.php | 22 ++++-- .../BooleanAndConstantConditionRuleTest.php | 42 +++++++---- .../BooleanNotConstantConditionRuleTest.php | 41 +++++++---- .../BooleanOrConstantConditionRuleTest.php | 42 +++++++---- .../ConstantLooseComparisonRuleTest.php | 31 ++++++-- .../DoWhileLoopConstantConditionRuleTest.php | 38 +++++++--- .../ElseIfConstantConditionRuleTest.php | 41 +++++++---- .../IfConstantConditionRuleTest.php | 39 ++++++++--- ...mpossibleCheckTypeFunctionCallRuleTest.php | 39 ++++++++--- ...sibleCheckTypeGenericOverwriteRuleTest.php | 1 + ...sibleCheckTypeMethodCallRuleEqualsTest.php | 1 + .../ImpossibleCheckTypeMethodCallRuleTest.php | 39 ++++++++--- ...sibleCheckTypeStaticMethodCallRuleTest.php | 39 ++++++++--- .../LogicalXorConstantConditionRuleTest.php | 40 +++++++---- .../Comparison/MatchExpressionRuleTest.php | 38 +++++++--- ...isonOperatorsConstantConditionRuleTest.php | 29 ++++++-- ...rnaryOperatorConstantConditionRuleTest.php | 39 ++++++++--- .../WhileLoopAlwaysFalseConditionRuleTest.php | 38 +++++++--- .../WhileLoopAlwaysTrueConditionRuleTest.php | 38 +++++++--- .../Comparison/data/boolean-and-in-trait.php | 58 +++++++++++++++ .../Comparison/data/boolean-not-in-trait.php | 58 +++++++++++++++ .../Comparison/data/boolean-or-in-trait.php | 58 +++++++++++++++ .../Comparison/data/do-while-in-trait.php | 56 +++++++++++++++ .../data/elseif-condition-in-trait.php | 62 ++++++++++++++++ .../Comparison/data/if-condition-in-trait.php | 58 +++++++++++++++ .../impossible-function-call-in-trait.php | 59 ++++++++++++++++ .../data/impossible-method-call-in-trait.php | 70 +++++++++++++++++++ ...impossible-static-method-call-in-trait.php | 68 ++++++++++++++++++ .../Comparison/data/logical-xor-in-trait.php | 58 +++++++++++++++ .../data/loose-comparison-in-trait.php | 61 ++++++++++++++++ .../Rules/Comparison/data/match-in-trait.php | 60 ++++++++++++++++ .../data/number-comparison-in-trait.php | 61 ++++++++++++++++ .../Comparison/data/ternary-in-trait.php | 54 ++++++++++++++ .../Comparison/data/while-false-in-trait.php | 58 +++++++++++++++ .../Comparison/data/while-true-in-trait.php | 58 +++++++++++++++ 50 files changed, 1749 insertions(+), 280 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/boolean-and-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/boolean-not-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/boolean-or-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/if-condition-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/impossible-function-call-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/impossible-method-call-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/impossible-static-method-call-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/loose-comparison-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/match-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/number-comparison-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/ternary-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php create mode 100644 tests/PHPStan/Rules/Comparison/data/while-true-in-trait.php diff --git a/src/Rules/Comparison/BooleanAndConstantConditionRule.php b/src/Rules/Comparison/BooleanAndConstantConditionRule.php index 15546825909..60e88caa527 100644 --- a/src/Rules/Comparison/BooleanAndConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanAndConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class BooleanAndConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -41,7 +44,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $errors = []; @@ -49,6 +52,8 @@ public function processNode( $nodeText = $originalNode->getOperatorSigil(); $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd'; + $isInTrait = $scope->isInTrait(); + $hasLeftOrRightError = false; if ($leftType instanceof ConstantBooleanType) { $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { @@ -80,8 +85,18 @@ public function processNode( if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + $hasLeftOrRightError = true; + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); } $rightScope = $node->getRightScope(); @@ -123,11 +138,21 @@ public function processNode( if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + $hasLeftOrRightError = true; + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); } - if (count($errors) === 0 && !$scope->isInFirstLevelStatement()) { + if (count($errors) === 0 && !$hasLeftOrRightError && !$scope->isInFirstLevelStatement()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { @@ -161,8 +186,17 @@ public function processNode( $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode, $nodeType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode); } } diff --git a/src/Rules/Comparison/BooleanNotConstantConditionRule.php b/src/Rules/Comparison/BooleanNotConstantConditionRule.php index 0f62704ebdb..fe786dd3c18 100644 --- a/src/Rules/Comparison/BooleanNotConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanNotConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -22,6 +24,7 @@ final class BooleanNotConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -39,7 +42,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->expr); @@ -74,12 +77,17 @@ public function processNode( $errorBuilder->identifier(sprintf('booleanNot.always%s', $exprType->getValue() ? 'False' : 'True')); - return [ - $errorBuilder->build(), - ]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->expr, !$exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->expr); return []; } diff --git a/src/Rules/Comparison/BooleanOrConstantConditionRule.php b/src/Rules/Comparison/BooleanOrConstantConditionRule.php index 8d2e1b86107..cc9fc93efa1 100644 --- a/src/Rules/Comparison/BooleanOrConstantConditionRule.php +++ b/src/Rules/Comparison/BooleanOrConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class BooleanOrConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -41,7 +44,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $originalNode = $node->getOriginalNode(); @@ -49,6 +52,8 @@ public function processNode( $messages = []; $leftType = $this->helper->getBooleanType($scope, $originalNode->left); $identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanOr ? 'booleanOr' : 'logicalOr'; + $isInTrait = $scope->isInTrait(); + $hasLeftOrRightError = false; if ($leftType instanceof ConstantBooleanType) { $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { @@ -80,8 +85,18 @@ public function processNode( if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $messages[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + $hasLeftOrRightError = true; + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError); + } else { + $messages[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left); } $rightScope = $node->getRightScope(); @@ -123,11 +138,21 @@ public function processNode( if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $messages[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + $hasLeftOrRightError = true; + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError); + } else { + $messages[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right); } - if (count($messages) === 0 && !$scope->isInFirstLevelStatement()) { + if (count($messages) === 0 && !$hasLeftOrRightError && !$scope->isInFirstLevelStatement()) { $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode); if ($nodeType instanceof ConstantBooleanType) { $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder { @@ -161,8 +186,17 @@ public function processNode( $errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False')); - $messages[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode, $nodeType->getValue(), $ruleError); + } else { + $messages[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode); } } diff --git a/src/Rules/Comparison/ConstantLooseComparisonRule.php b/src/Rules/Comparison/ConstantLooseComparisonRule.php index dde16af74d6..d872f60a016 100644 --- a/src/Rules/Comparison/ConstantLooseComparisonRule.php +++ b/src/Rules/Comparison/ConstantLooseComparisonRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class ConstantLooseComparisonRule implements Rule public function __construct( private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -36,7 +39,7 @@ public function getNodeType(): string return Node\Expr\BinaryOp::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node instanceof Node\Expr\BinaryOp\Equal && !$node instanceof Node\Expr\BinaryOp\NotEqual) { return []; @@ -44,6 +47,7 @@ public function processNode(Node $node, Scope $scope): array $nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if (!$nodeType->isTrue()->yes() && !$nodeType->isFalse()->yes()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -66,18 +70,23 @@ public function processNode(Node $node, Scope $scope): array }; if ($nodeType->isFalse()->yes()) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Loose comparison using %s between %s and %s will always evaluate to false.', - $node->getOperatorSigil(), - $scope->getType($node->left)->describe(VerbosityLevel::value()), - $scope->getType($node->right)->describe(VerbosityLevel::value()), - )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual'))->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Loose comparison using %s between %s and %s will always evaluate to false.', + $node->getOperatorSigil(), + $scope->getType($node->left)->describe(VerbosityLevel::value()), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + )))->identifier(sprintf('%s.alwaysFalse', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual'))->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -93,7 +102,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier(sprintf('%s.alwaysTrue', $node instanceof Node\Expr\BinaryOp\Equal ? 'equal' : 'notEqual')); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } } diff --git a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php index 973d49295eb..bff27fcfe20 100644 --- a/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php +++ b/src/Rules/Comparison/DoWhileLoopConstantConditionRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -25,6 +27,7 @@ final class DoWhileLoopConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -38,23 +41,26 @@ public function getNodeType(): string return DoWhileLoopConditionNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $exprType = $this->helper->getBooleanType($scope, $node->getCond()); if ($exprType instanceof ConstantBooleanType) { if ($exprType->getValue()) { if ($node->hasYield()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } foreach ($node->getExitPoints() as $exitPoint) { $statement = $exitPoint->getStatement(); if (!$statement instanceof Continue_) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } if (!$statement->num instanceof Int_) { continue; } if ($statement->num->value > 1) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } } @@ -62,6 +68,7 @@ public function processNode(Node $node, Scope $scope): array foreach ($node->getExitPoints() as $exitPoint) { $statement = $exitPoint->getStatement(); if ($statement instanceof Break_) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } } @@ -85,17 +92,22 @@ public function processNode(Node $node, Scope $scope): array return $this->possiblyImpureTipHelper->addTip($scope, $node->getCond(), $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Do-while loop condition is always %s.', - $exprType->getValue() ? 'true' : 'false', - ))) - ->line($node->getCond()->getStartLine()) - ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) - ->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Do-while loop condition is always %s.', + $exprType->getValue() ? 'true' : 'false', + ))) + ->line($node->getCond()->getStartLine()) + ->identifier(sprintf('doWhile.always%s', $exprType->getValue() ? 'True' : 'False')) + ->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->getCond(), $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->getCond()); return []; } diff --git a/src/Rules/Comparison/ElseIfConstantConditionRule.php b/src/Rules/Comparison/ElseIfConstantConditionRule.php index 10df54ef12c..22c19dd1ec0 100644 --- a/src/Rules/Comparison/ElseIfConstantConditionRule.php +++ b/src/Rules/Comparison/ElseIfConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -22,6 +24,7 @@ final class ElseIfConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -39,7 +42,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -75,10 +78,17 @@ public function processNode( $errorBuilder->identifier(sprintf('elseif.always%s', $exprType->getValue() ? 'True' : 'False')); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); return []; } diff --git a/src/Rules/Comparison/IfConstantConditionRule.php b/src/Rules/Comparison/IfConstantConditionRule.php index 19ee79df134..a1eb712e653 100644 --- a/src/Rules/Comparison/IfConstantConditionRule.php +++ b/src/Rules/Comparison/IfConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class IfConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -36,7 +39,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -59,16 +62,21 @@ public function processNode( return $this->possiblyImpureTipHelper->addTip($scope, $node->cond, $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'If condition is always %s.', - $exprType->getValue() ? 'true' : 'false', - ))) - ->identifier(sprintf('if.always%s', $exprType->getValue() ? 'True' : 'False')) - ->line($node->cond->getStartLine())->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'If condition is always %s.', + $exprType->getValue() ? 'true' : 'false', + ))) + ->identifier(sprintf('if.always%s', $exprType->getValue() ? 'True' : 'False')) + ->line($node->cond->getStartLine())->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); return []; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index 61e3b57c303..8b3d6919c4c 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class ImpossibleCheckTypeFunctionCallRule implements Rule public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -36,7 +39,7 @@ public function getNodeType(): string return Node\Expr\FuncCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node->name instanceof Node\Name) { return []; @@ -45,6 +48,7 @@ public function processNode(Node $node, Scope $scope): array $functionName = (string) $node->name; $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); if ($isAlways === null) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -67,17 +71,22 @@ public function processNode(Node $node, Scope $scope): array }; if (!$isAlways) { - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to function %s()%s will always evaluate to false.', - $functionName, - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->identifier('function.impossibleType')->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to function %s()%s will always evaluate to false.', + $functionName, + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->identifier('function.impossibleType')->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -92,7 +101,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier('function.alreadyNarrowedType'); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index bc8284d1111..650f15d5a69 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class ImpossibleCheckTypeMethodCallRule implements Rule public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -39,7 +42,7 @@ public function getNodeType(): string return Node\Expr\MethodCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node->name instanceof Node\Identifier) { return []; @@ -47,6 +50,7 @@ public function processNode(Node $node, Scope $scope): array $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); if ($isAlways === null) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -70,18 +74,23 @@ public function processNode(Node $node, Scope $scope): array if (!$isAlways) { $method = $this->getMethod($node->var, $node->name->name, $scope); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to method %s::%s()%s will always evaluate to false.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->identifier('method.impossibleType')->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to method %s::%s()%s will always evaluate to false.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->identifier('method.impossibleType')->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -98,7 +107,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier('method.alreadyNarrowedType'); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } private function getMethod( diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index 3c24b381762..3b41e6221a0 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class ImpossibleCheckTypeStaticMethodCallRule implements Rule public function __construct( private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -39,7 +42,7 @@ public function getNodeType(): string return Node\Expr\StaticCall::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if (!$node->name instanceof Node\Identifier) { return []; @@ -47,6 +50,7 @@ public function processNode(Node $node, Scope $scope): array $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); if ($isAlways === null) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -71,18 +75,23 @@ public function processNode(Node $node, Scope $scope): array if (!$isAlways) { $method = $this->getMethod($node->class, $node->name->name, $scope); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Call to static method %s::%s()%s will always evaluate to false.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), - )))->identifier('staticMethod.impossibleType')->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Call to static method %s::%s()%s will always evaluate to false.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $this->impossibleCheckTypeHelper->getArgumentsDescription($scope, $node->getArgs()), + )))->identifier('staticMethod.impossibleType')->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -99,7 +108,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier('staticMethod.alreadyNarrowedType'); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } /** diff --git a/src/Rules/Comparison/LogicalXorConstantConditionRule.php b/src/Rules/Comparison/LogicalXorConstantConditionRule.php index 3618a9f9643..16589f28886 100644 --- a/src/Rules/Comparison/LogicalXorConstantConditionRule.php +++ b/src/Rules/Comparison/LogicalXorConstantConditionRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp\LogicalXor; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -23,6 +25,7 @@ final class LogicalXorConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -38,9 +41,10 @@ public function getNodeType(): string return LogicalXor::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $errors = []; + $isInTrait = $scope->isInTrait(); $leftType = $this->helper->getBooleanType($scope, $node->left); if ($leftType instanceof ConstantBooleanType) { $addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { @@ -72,8 +76,17 @@ public function processNode(Node $node, Scope $scope): array if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->left, $leftType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->left); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->left); } $rightType = $this->helper->getBooleanType($scope, $node->right); @@ -110,8 +123,17 @@ public function processNode(Node $node, Scope $scope): array if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) { $errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.'); } - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($isInTrait) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->right, $rightType->getValue(), $ruleError); + } else { + $errors[] = $ruleError; + } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->right); } + } else { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->right); } return $errors; diff --git a/src/Rules/Comparison/MatchExpressionRule.php b/src/Rules/Comparison/MatchExpressionRule.php index 665b7af1e34..d89fe2dd87c 100644 --- a/src/Rules/Comparison/MatchExpressionRule.php +++ b/src/Rules/Comparison/MatchExpressionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -31,6 +33,7 @@ final class MatchExpressionRule implements Rule public function __construct( private ConstantConditionRuleHelper $constantConditionRuleHelper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, ) @@ -42,7 +45,7 @@ public function getNodeType(): string return MatchExpressionNode::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { $matchCondition = $node->getCondition(); $matchConditionType = $scope->getType($matchCondition); @@ -71,6 +74,7 @@ public function processNode(Node $node, Scope $scope): array $armConditionResult = $armConditionScope->getType($armConditionExpr); if (!$armConditionResult instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); continue; } if ($armConditionResult->getValue()) { @@ -80,6 +84,7 @@ public function processNode(Node $node, Scope $scope): array if (!$this->treatPhpDocTypesAsCertain) { $armConditionNativeResult = $armConditionScope->getNativeType($armConditionExpr); if (!$armConditionNativeResult instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); continue; } if ($armConditionNativeResult->getValue()) { @@ -90,6 +95,7 @@ public function processNode(Node $node, Scope $scope): array if ($matchConditionType instanceof ConstantBooleanType) { $armConditionStandaloneResult = $this->constantConditionRuleHelper->getBooleanType($armConditionScope, $armCondition->getCondition()); if (!$armConditionStandaloneResult instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); continue; } } @@ -102,11 +108,17 @@ public function processNode(Node $node, Scope $scope): array $armConditionScope->getType($armCondition->getCondition())->describe(VerbosityLevel::value()), ))->line($armLine)->identifier('match.alwaysFalse'); $this->possiblyImpureTipHelper->addTip($armConditionScope, $armConditionExpr, $errorBuilder); - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $armConditionExpr, false, $ruleError); + } else { + $errors[] = $ruleError; + } continue; } if ($i === $armsCount - 1) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $armConditionExpr); continue; } @@ -120,7 +132,12 @@ public function processNode(Node $node, Scope $scope): array ->identifier('match.alwaysTrue') ->tip('Remove remaining cases below this one and this error will disappear too.'); $this->possiblyImpureTipHelper->addTip($armConditionScope, $armConditionExpr, $errorBuilder); - $errors[] = $errorBuilder->build(); + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $armConditionExpr, true, $ruleError); + } else { + $errors[] = $ruleError; + } } } diff --git a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php index 5cd0ab417e3..392e702f497 100644 --- a/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php +++ b/src/Rules/Comparison/NumberComparisonOperatorsConstantConditionRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -24,6 +26,7 @@ final class NumberComparisonOperatorsConstantConditionRule implements Rule public function __construct( private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -39,7 +42,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { if ( @@ -88,17 +91,22 @@ public function processNode( throw new ShouldNotHappenException(); } - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Comparison operation "%s" between %s and %s is always %s.', - $node->getOperatorSigil(), - $scope->getType($node->left)->describe(VerbosityLevel::value()), - $scope->getType($node->right)->describe(VerbosityLevel::value()), - $exprType->getValue() ? 'true' : 'false', - )))->identifier(sprintf('%s.always%s', $nodeType, $exprType->getValue() ? 'True' : 'False'))->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Comparison operation "%s" between %s and %s is always %s.', + $node->getOperatorSigil(), + $scope->getType($node->left)->describe(VerbosityLevel::value()), + $scope->getType($node->right)->describe(VerbosityLevel::value()), + $exprType->getValue() ? 'true' : 'false', + )))->identifier(sprintf('%s.always%s', $nodeType, $exprType->getValue() ? 'True' : 'False'))->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } diff --git a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php index 273405ee36b..ddb606965c9 100644 --- a/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php +++ b/src/Rules/Comparison/TernaryOperatorConstantConditionRule.php @@ -3,6 +3,8 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class TernaryOperatorConstantConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -36,7 +39,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -58,14 +61,19 @@ public function processNode( return $this->possiblyImpureTipHelper->addTip($scope, $node->cond, $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Ternary operator condition is always %s.', - $exprType->getValue() ? 'true' : 'false', - )))->identifier(sprintf('ternary.always%s', $exprType->getValue() ? 'True' : 'False'))->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Ternary operator condition is always %s.', + $exprType->getValue() ? 'true' : 'false', + )))->identifier(sprintf('ternary.always%s', $exprType->getValue() ? 'True' : 'False'))->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, $exprType->getValue(), $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); return []; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php index 77b9b7543b5..d6bd7479dc8 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysFalseConditionRule.php @@ -4,6 +4,8 @@ use PhpParser\Node; use PhpParser\Node\Stmt\While_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -21,6 +23,7 @@ final class WhileLoopAlwaysFalseConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -36,7 +39,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { $exprType = $this->helper->getBooleanType($scope, $node->cond); @@ -59,13 +62,18 @@ public function processNode( return $this->possiblyImpureTipHelper->addTip($scope, $node->cond, $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) - ->identifier('while.alwaysFalse') - ->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message('While loop condition is always false.'))->line($node->cond->getStartLine()) + ->identifier('while.alwaysFalse') + ->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->cond, false, $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->cond); return []; } diff --git a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php index bfc50e3e48a..ef942cfe0cf 100644 --- a/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php +++ b/src/Rules/Comparison/WhileLoopAlwaysTrueConditionRule.php @@ -6,6 +6,8 @@ use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Stmt\Break_; use PhpParser\Node\Stmt\Continue_; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; @@ -25,6 +27,7 @@ final class WhileLoopAlwaysTrueConditionRule implements Rule public function __construct( private ConstantConditionRuleHelper $helper, private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter(ref: '%tips.treatPhpDocTypesAsCertain%')] @@ -40,7 +43,7 @@ public function getNodeType(): string public function processNode( Node $node, - Scope $scope, + Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, ): array { foreach ($node->getExitPoints() as $exitPoint) { @@ -70,12 +73,14 @@ public function processNode( $exprType = $this->helper->getBooleanType($scope, $originalNode->cond); if ($exprType->isTrue()->yes()) { if ($node->hasYield()) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); return []; } $ref = $scope->getFunction() ?? $scope->getAnonymousFunctionReflection(); if ($ref !== null && $ref->getReturnType() instanceof NeverType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); return []; } @@ -97,13 +102,18 @@ public function processNode( return $this->possiblyImpureTipHelper->addTip($scope, $originalNode->cond, $ruleErrorBuilder); }; - return [ - $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) - ->identifier('while.alwaysTrue') - ->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message('While loop condition is always true.'))->line($originalNode->cond->getStartLine()) + ->identifier('while.alwaysTrue') + ->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->cond, true, $ruleError); + return []; + } + + return [$ruleError]; } + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->cond); return []; } diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index b332d01adb8..f235ec0c7e4 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class BooleanAndConstantConditionRuleTest extends RuleTestCase { @@ -18,21 +19,26 @@ class BooleanAndConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new BooleanAndConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new BooleanAndConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -446,4 +452,16 @@ public function testBug8555(): void $this->analyse([__DIR__ . '/data/bug-8555.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/boolean-and-in-trait.php'], [ + [ + 'Left side of && is always true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index 3cf3f2a8bbc..404b6ebe989 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class BooleanNotConstantConditionRuleTest extends RuleTestCase { @@ -18,21 +19,26 @@ class BooleanNotConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new BooleanNotConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new BooleanNotConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -236,4 +242,15 @@ public function testBug6702(): void $this->analyse([__DIR__ . '/data/bug-6702.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/boolean-not-in-trait.php'], [ + [ + 'Negated boolean expression is always false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index cc628575f79..241e9be4540 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class BooleanOrConstantConditionRuleTest extends RuleTestCase { @@ -19,21 +20,26 @@ class BooleanOrConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new BooleanOrConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new BooleanOrConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -384,4 +390,16 @@ public function testBug10305(): void ]); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->reportAlwaysTrueInLastCondition = true; + $this->analyse([__DIR__ . '/data/boolean-or-in-trait.php'], [ + [ + 'Left side of || is always true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php index bb6aa370ac4..9d69da03b4c 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -10,7 +11,7 @@ use const PHP_VERSION_ID; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ConstantLooseComparisonRuleTest extends RuleTestCase { @@ -21,12 +22,17 @@ class ConstantLooseComparisonRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ConstantLooseComparisonRule( - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + // @phpstan-ignore argument.type + return new CompositeRule([ + new ConstantLooseComparisonRule( + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, + ), + new ConstantConditionInTraitRule(), + ]); } public function testRule(): void @@ -248,4 +254,15 @@ public function testBug13098(): void $this->analyse([__DIR__ . '/data/bug-13098.php'], []); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/loose-comparison-in-trait.php'], [ + [ + 'Loose comparison using == between 1 and null will always evaluate to false.', + 19, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index 97b3d705baf..f0045408012 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -3,30 +3,36 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class DoWhileLoopConstantConditionRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new DoWhileLoopConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new DoWhileLoopConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), $this->shouldTreatPhpDocTypesAsCertain(), ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), + true, ), - new PossiblyImpureTipHelper(true), - $this->shouldTreatPhpDocTypesAsCertain(), - true, - ); + new ConstantConditionInTraitRule(), + ]); } public function testBug6189(): void @@ -76,4 +82,14 @@ public function testRule(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/do-while-in-trait.php'], [ + [ + 'Do-while loop condition is always false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 594de979a59..342be2c84b1 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ElseIfConstantConditionRuleTest extends RuleTestCase { @@ -19,21 +20,26 @@ class ElseIfConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ElseIfConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new ElseIfConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -152,4 +158,15 @@ public function testBug6947(): void ]); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/elseif-condition-in-trait.php'], [ + [ + 'Elseif condition is always false.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 79b7b484e13..dd7ddf9df6a 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class IfConstantConditionRuleTest extends RuleTestCase { @@ -16,20 +17,25 @@ class IfConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new IfConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new IfConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -213,4 +219,15 @@ public function testBug4284(): void ]); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/if-condition-in-trait.php'], [ + [ + 'If condition is always true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 69e992139d8..80a3c72c215 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -13,7 +14,7 @@ use function count; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase { @@ -24,18 +25,23 @@ class ImpossibleCheckTypeFunctionCallRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ImpossibleCheckTypeFunctionCallRule( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [stdClass::class], + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleCheckTypeFunctionCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [stdClass::class], + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -1207,4 +1213,15 @@ public function testBug13799(): void ]); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impossible-function-call-in-trait.php'], [ + [ + 'Call to function is_string() with int will always evaluate to false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php index 0190415a989..3871e50298b 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeGenericOverwriteRuleTest.php @@ -21,6 +21,7 @@ public function getRule(): Rule true, ), new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true, false, true, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php index 178b4148958..8066975bf92 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -21,6 +21,7 @@ public function getRule(): Rule true, ), new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), true, false, true, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php index 382dbc2f28a..4e9a371d730 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase { @@ -19,18 +20,23 @@ class ImpossibleCheckTypeMethodCallRuleTest extends RuleTestCase public function getRule(): Rule { - return new ImpossibleCheckTypeMethodCallRule( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleCheckTypeMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -303,6 +309,17 @@ public function testBug10337(): void $this->analyse([__DIR__ . '/data/bug-10337.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impossible-method-call-in-trait.php'], [ + [ + 'Call to method ImpossibleMethodCallInTrait\TypeChecker::isString() with int will always evaluate to false.', + 30, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php index f53b87d0077..075b14d4ae6 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase { @@ -19,18 +20,23 @@ class ImpossibleCheckTypeStaticMethodCallRuleTest extends RuleTestCase public function getRule(): Rule { - return new ImpossibleCheckTypeStaticMethodCallRule( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleCheckTypeStaticMethodCallRule( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + $this->reportAlwaysTrueInLastCondition, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - $this->reportAlwaysTrueInLastCondition, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -168,6 +174,17 @@ public function testBug13566(): void $this->analyse([__DIR__ . '/data/bug-13566.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/impossible-static-method-call-in-trait.php'], [ + [ + 'Call to static method ImpossibleStaticMethodCallInTrait\TypeChecker::isString() with int will always evaluate to false.', + 28, + ], + ]); + } + public static function getAdditionalConfigFiles(): array { return [ diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php index 14b97daacaa..bea49f94bc1 100644 --- a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -3,31 +3,37 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule as TRule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class LogicalXorConstantConditionRuleTest extends RuleTestCase { protected function getRule(): TRule { - return new LogicalXorConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new LogicalXorConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), $this->shouldTreatPhpDocTypesAsCertain(), ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), + false, + true, ), - new PossiblyImpureTipHelper(true), - $this->shouldTreatPhpDocTypesAsCertain(), - false, - true, - ); + new ConstantConditionInTraitRule(), + ]); } public function testRule(): void @@ -71,4 +77,14 @@ public function testRule(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/logical-xor-in-trait.php'], [ + [ + 'Left side of xor is always false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 09330a874be..99f7e166414 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -3,11 +3,12 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class MatchExpressionRuleTest extends RuleTestCase { @@ -16,19 +17,24 @@ class MatchExpressionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new MatchExpressionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new MatchExpressionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -478,4 +484,16 @@ public function testBug11310(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/match-in-trait.php'], [ + [ + 'Match arm comparison between true and false is always false.', + 21, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 5400e3edee6..3822a2df11c 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -3,12 +3,13 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase { @@ -19,11 +20,16 @@ class NumberComparisonOperatorsConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new NumberComparisonOperatorsConstantConditionRule( - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - true, - ); + // @phpstan-ignore argument.type + return new CompositeRule([ + new NumberComparisonOperatorsConstantConditionRule( + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + $this->treatPhpDocTypesAsCertain, + true, + ), + new ConstantConditionInTraitRule(), + ]); } protected function shouldPolluteScopeWithAlwaysIterableForeach(): bool @@ -298,4 +304,15 @@ public function testBug12163(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/number-comparison-in-trait.php'], [ + [ + 'Comparison operation ">" between 1 and 0 is always true.', + 19, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index 300484a89b7..f6df113491b 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -3,10 +3,11 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class TernaryOperatorConstantConditionRuleTest extends RuleTestCase { @@ -15,20 +16,25 @@ class TernaryOperatorConstantConditionRuleTest extends RuleTestCase protected function getRule(): Rule { - return new TernaryOperatorConstantConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new TernaryOperatorConstantConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->treatPhpDocTypesAsCertain, + ), $this->treatPhpDocTypesAsCertain, ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->treatPhpDocTypesAsCertain, + true, ), - new PossiblyImpureTipHelper(true), - $this->treatPhpDocTypesAsCertain, - true, - ); + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -106,4 +112,15 @@ public function testBug3370(): void $this->analyse([__DIR__ . '/data/bug-3370.php'], []); } + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/ternary-in-trait.php'], [ + [ + 'Ternary operator condition is always true.', + 17, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php index e84fdb4d566..caf89ddfe30 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -3,30 +3,36 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class WhileLoopAlwaysFalseConditionRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new WhileLoopAlwaysFalseConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new WhileLoopAlwaysFalseConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), $this->shouldTreatPhpDocTypesAsCertain(), ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), + true, ), - new PossiblyImpureTipHelper(true), - $this->shouldTreatPhpDocTypesAsCertain(), - true, - ); + new ConstantConditionInTraitRule(), + ]); } public function testRule(): void @@ -44,4 +50,14 @@ public function testRule(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/while-false-in-trait.php'], [ + [ + 'While loop condition is always false.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php index 31f3abbf5e5..ce2f3549cc3 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysTrueConditionRuleTest.php @@ -3,31 +3,37 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Rules\Rule; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class WhileLoopAlwaysTrueConditionRuleTest extends RuleTestCase { protected function getRule(): Rule { - return new WhileLoopAlwaysTrueConditionRule( - new ConstantConditionRuleHelper( - new ImpossibleCheckTypeHelper( - self::createReflectionProvider(), - $this->getTypeSpecifier(), - [], + // @phpstan-ignore argument.type + return new CompositeRule([ + new WhileLoopAlwaysTrueConditionRule( + new ConstantConditionRuleHelper( + new ImpossibleCheckTypeHelper( + self::createReflectionProvider(), + $this->getTypeSpecifier(), + [], + $this->shouldTreatPhpDocTypesAsCertain(), + ), $this->shouldTreatPhpDocTypesAsCertain(), ), + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), + true, ), - new PossiblyImpureTipHelper(true), - $this->shouldTreatPhpDocTypesAsCertain(), - true, - ); + new ConstantConditionInTraitRule(), + ]); } public function testRule(): void @@ -74,4 +80,14 @@ public function testBug6189(): void ]); } + public function testInTrait(): void + { + $this->analyse([__DIR__ . '/data/while-true-in-trait.php'], [ + [ + 'While loop condition is always true.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-and-in-trait.php b/tests/PHPStan/Rules/Comparison/data/boolean-and-in-trait.php new file mode 100644 index 00000000000..a82755c5b3d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-and-in-trait.php @@ -0,0 +1,58 @@ +doBar() && rand(0, 1)) { + + } + } + + public function doFoo2() + { + // left side: always constant + if ($this->doBar2() && rand(0, 1)) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-not-in-trait.php b/tests/PHPStan/Rules/Comparison/data/boolean-not-in-trait.php new file mode 100644 index 00000000000..a51454f5cc9 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-not-in-trait.php @@ -0,0 +1,58 @@ +doBar()) { + + } + } + + public function doFoo2() + { + // always constant (negation of always-truthy is always false) + if (!$this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/boolean-or-in-trait.php b/tests/PHPStan/Rules/Comparison/data/boolean-or-in-trait.php new file mode 100644 index 00000000000..acb20bfb2c1 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/boolean-or-in-trait.php @@ -0,0 +1,58 @@ +doBar() || rand(0, 1)) { + + } + } + + public function doFoo2() + { + // left side: always constant + if ($this->doBar2() || rand(0, 1)) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php b/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php new file mode 100644 index 00000000000..1e5e9c279f6 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php @@ -0,0 +1,56 @@ +doBar()); + } + + public function doFoo2() + { + // always falsy + do { + } while ($this->doBar2()); + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): null + { + + } + + public function doBar2(): null + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): null + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php b/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php new file mode 100644 index 00000000000..9815ff99550 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php @@ -0,0 +1,62 @@ +doBar()) { + + } + } + + public function doFoo2() + { + $x = rand(0, 1); + // always falsy + if ($x) { + } elseif ($this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): null + { + + } + + public function doBar2(): null + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): null + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/if-condition-in-trait.php b/tests/PHPStan/Rules/Comparison/data/if-condition-in-trait.php new file mode 100644 index 00000000000..9065569d00e --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/if-condition-in-trait.php @@ -0,0 +1,58 @@ +doBar()) { + + } + } + + public function doFoo2() + { + // always truthy + if ($this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-function-call-in-trait.php b/tests/PHPStan/Rules/Comparison/data/impossible-function-call-in-trait.php new file mode 100644 index 00000000000..9b1123c1181 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-function-call-in-trait.php @@ -0,0 +1,59 @@ +doBar())) { + + } + } + + public function doFoo2() + { + // always false + if (is_string($this->doBar2())) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): int + { + + } + + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + /** @return int|string */ + public function doBar() + { + + } + + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-method-call-in-trait.php b/tests/PHPStan/Rules/Comparison/data/impossible-method-call-in-trait.php new file mode 100644 index 00000000000..86c0de8769a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-method-call-in-trait.php @@ -0,0 +1,70 @@ +isString($this->doBar())) { + + } + } + + public function doFoo2() + { + $checker = new TypeChecker(); + // always false + if ($checker->isString($this->doBar2())) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): int + { + + } + + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + /** @return int|string */ + public function doBar() + { + + } + + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call-in-trait.php b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call-in-trait.php new file mode 100644 index 00000000000..b11bf5ba709 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/impossible-static-method-call-in-trait.php @@ -0,0 +1,68 @@ +doBar())) { + + } + } + + public function doFoo2() + { + // always false + if (TypeChecker::isString($this->doBar2())) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): int + { + + } + + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + /** @return int|string */ + public function doBar() + { + + } + + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php b/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php new file mode 100644 index 00000000000..969f7807fd2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php @@ -0,0 +1,58 @@ +doBar() xor rand(0, 1)) { + + } + } + + public function doFoo2() + { + // left side: always constant (always false) + if ($this->doBar2() xor rand(0, 1)) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): null + { + + } + + public function doBar2(): null + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): null + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/loose-comparison-in-trait.php b/tests/PHPStan/Rules/Comparison/data/loose-comparison-in-trait.php new file mode 100644 index 00000000000..9456460a62f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/loose-comparison-in-trait.php @@ -0,0 +1,61 @@ +doBar() == null) { + + } + } + + public function doFoo2() + { + // always false + if ($this->doBar2() == null) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + /** @return 1 */ + public function doBar(): int + { + + } + + /** @return 1 */ + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?int + { + + } + + /** @return 1 */ + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/match-in-trait.php b/tests/PHPStan/Rules/Comparison/data/match-in-trait.php new file mode 100644 index 00000000000..45e069dad71 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/match-in-trait.php @@ -0,0 +1,60 @@ += 8.0 + +namespace MatchInTrait; + +trait FooTrait +{ + + public function doFoo() + { + // sometimes constant, sometimes not + match (true) { + $this->doBar() => 'yes', + default => 'no', + }; + } + + public function doFoo2() + { + // always false + match (true) { + $this->doBar2() => 'yes', + default => 'no', + }; + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): false + { + + } + + public function doBar2(): false + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): bool + { + + } + + public function doBar2(): false + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/number-comparison-in-trait.php b/tests/PHPStan/Rules/Comparison/data/number-comparison-in-trait.php new file mode 100644 index 00000000000..be62971b3e0 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/number-comparison-in-trait.php @@ -0,0 +1,61 @@ +doBar() > 0) { + + } + } + + public function doFoo2() + { + // always constant + if ($this->doBar2() > 0) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + /** @return 1 */ + public function doBar(): int + { + + } + + /** @return 1 */ + public function doBar2(): int + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): int + { + + } + + /** @return 1 */ + public function doBar2(): int + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/ternary-in-trait.php b/tests/PHPStan/Rules/Comparison/data/ternary-in-trait.php new file mode 100644 index 00000000000..5c0376725cc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/ternary-in-trait.php @@ -0,0 +1,54 @@ +doBar() ? 'yes' : 'no'; + } + + public function doFoo2() + { + // always truthy + $x = $this->doBar2() ? 'yes' : 'no'; + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php b/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php new file mode 100644 index 00000000000..c2f34f1b4da --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php @@ -0,0 +1,58 @@ +doBar()) { + + } + } + + public function doFoo2() + { + // always falsy + while ($this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): null + { + + } + + public function doBar2(): null + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): null + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/while-true-in-trait.php b/tests/PHPStan/Rules/Comparison/data/while-true-in-trait.php new file mode 100644 index 00000000000..0ea5164fa8b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/while-true-in-trait.php @@ -0,0 +1,58 @@ +doBar()) { + + } + } + + public function doFoo2(): void + { + // always truthy + while ($this->doBar2()) { + + } + } + +} + +class Foo +{ + + use FooTrait; + + public function doBar(): \stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} + +class FooAnother +{ + + use FooTrait; + + public function doBar(): ?\stdClass + { + + } + + public function doBar2(): \stdClass + { + + } + +} From 4a9202b962863b78ac3735298a2a7da6bb38d7dd Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 15:12:00 +0100 Subject: [PATCH 55/70] Regression tests Fixes phpstan/phpstan#13023 Closes https://github.com/phpstan/phpstan/issues/7599 Closes https://github.com/phpstan/phpstan/issues/13023 Closes https://github.com/phpstan/phpstan/issues/13474 Closes https://github.com/phpstan/phpstan/issues/13687 Closes https://github.com/phpstan/phpstan/issues/12798 Closes https://github.com/phpstan/phpstan/issues/11949 Closes https://github.com/phpstan/phpstan/issues/12267 Closes https://github.com/phpstan/phpstan/issues/9515 Closes https://github.com/phpstan/phpstan/issues/4570 Closes https://github.com/phpstan/phpstan/issues/4121 Closes https://github.com/phpstan/phpstan/issues/8060 --- .../Classes/ImpossibleInstanceOfRuleTest.php | 6 ++ .../PHPStan/Rules/Classes/data/bug-12267.php | 53 +++++++++++ .../BooleanNotConstantConditionRuleTest.php | 6 ++ ...mpossibleCheckTypeFunctionCallRuleTest.php | 44 +++++++++ ...rictComparisonOfDifferentTypesRuleTest.php | 15 +++ ...rnaryOperatorConstantConditionRuleTest.php | 6 ++ .../Rules/Comparison/data/bug-11949.php | 68 ++++++++++++++ .../Rules/Comparison/data/bug-12267.php | 43 +++++++++ .../Rules/Comparison/data/bug-12798.php | 53 +++++++++++ .../Rules/Comparison/data/bug-13023.php | 25 +++++ .../Rules/Comparison/data/bug-13474.php | 91 +++++++++++++++++++ .../Rules/Comparison/data/bug-13687.php | 34 +++++++ .../Rules/Comparison/data/bug-4121.php | 23 +++++ .../Rules/Comparison/data/bug-4570.php | 35 +++++++ .../Rules/Comparison/data/bug-7599.php | 41 +++++++++ .../Rules/Comparison/data/bug-8060.php | 39 ++++++++ .../Rules/Comparison/data/bug-9095.php | 34 +++++++ .../Rules/Comparison/data/bug-9515.php | 39 ++++++++ 18 files changed, 655 insertions(+) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-12267.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-11949.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-12267.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-12798.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13023.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13474.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13687.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-4121.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-4570.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-7599.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-8060.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-9095.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-9515.php diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index fcc567c51d9..f7752941d98 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -536,6 +536,12 @@ public function testBug10036(): void ]); } + public function testBug12267(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12267.php'], []); + } + #[RequiresPhp('>= 8.0')] public function testNewIsAlwaysFinalClass(): void { diff --git a/tests/PHPStan/Rules/Classes/data/bug-12267.php b/tests/PHPStan/Rules/Classes/data/bug-12267.php new file mode 100644 index 00000000000..8317471d39d --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-12267.php @@ -0,0 +1,53 @@ + */ + protected function getFileExistsHelpBlock(string $field): array + { + if (!($this->model instanceof A11yPhase)) { + return []; + } + + return []; + } +} + +/** + * @extends Form + */ +class EditA11yPhaseForm extends Form +{ + use ContainsA11yPhaseResultFields; +} + +/** + * @extends Form + */ +class SubmitA11yAuditPhaseForm extends Form +{ + use ContainsA11yPhaseResultFields; +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index 404b6ebe989..fae6233ed77 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -236,6 +236,12 @@ public function testBug5984(): void $this->analyse([__DIR__ . '/data/bug-5984.php'], []); } + public function testBug12267(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12267.php'], []); + } + public function testBug6702(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 80a3c72c215..8a32b0ddf66 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1147,6 +1147,50 @@ public function testBug13628(): void $this->analyse([__DIR__ . '/data/bug-13628.php'], []); } + public function testBug13023(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13023.php'], []); + } + + public function testBug9095(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-9095.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug7599(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7599.php'], []); + } + + public function testBug13474(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13474.php'], []); + } + + public function testBug13687(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13687.php'], []); + } + + #[RequiresPhp('>= 8.1')] + public function testBug12798(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12798.php'], []); + } + + public function testBug4570(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4570.php'], []); + } + public function testBug9666(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index a62b33caa59..f457963e726 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1041,6 +1041,21 @@ public function testBug3761(): void $this->analyse([__DIR__ . '/data/bug-3761.php'], []); } + public function testBug8060(): void + { + $this->analyse([__DIR__ . '/data/bug-8060.php'], []); + } + + public function testBug9515(): void + { + $this->analyse([__DIR__ . '/data/bug-9515.php'], []); + } + + public function testBug4121(): void + { + $this->analyse([__DIR__ . '/data/bug-4121.php'], []); + } + public function testBug13208(): void { $this->analyse([__DIR__ . '/data/bug-13208.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index f6df113491b..90e904c1ace 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -106,6 +106,12 @@ public function testBug7580(): void $this->analyse([__DIR__ . '/data/bug-7580.php'], []); } + public function testBug11949(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-11949.php'], []); + } + public function testBug3370(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11949.php b/tests/PHPStan/Rules/Comparison/data/bug-11949.php new file mode 100644 index 00000000000..b1373e56237 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11949.php @@ -0,0 +1,68 @@ + */ + static protected ?array $_translatedValues; + + static public function getValueIndex(string $value): int + { + return ($i = array_search($value, self::NAMES)) === false ? -1 : $i; + } + + /** @return array */ + static public function getTranslatedValues(): array + { + return self::$_translatedValues ??= array_map(static::getTranslatedValue(...), array_combine(self::NAMES, self::NAMES)); + } + + static public function getTranslatedValue(string $value): string + { + return self::TRANSLATION ? trans(self::TRANSLATION . $value) : $value; + } + +} + +abstract class UserStatus +{ + + use EnumString; + + const ACTIVE = 'active'; + const PENDING = 'pending'; + const BLOCKED = 'blocked'; + + protected const NAMES = [ + self::ACTIVE, + self::PENDING, + self::BLOCKED, + ]; + + protected const TRANSLATION = 'users.statuses.'; + +} + +abstract class SystemCheckStatus +{ + + use EnumString; + + const SUCCESS = 'success'; + const FAILURE = 'failure'; + + protected const NAMES = [ + self::SUCCESS, + self::FAILURE, + ]; + + protected const TRANSLATION = ''; + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12267.php b/tests/PHPStan/Rules/Comparison/data/bug-12267.php new file mode 100644 index 00000000000..4d300a9b4eb --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12267.php @@ -0,0 +1,43 @@ +model) { + return; + } + + echo $this->model; + } +} + +class Class1 +{ + /** @use PrintSomething */ + use PrintSomething; + + public function what(): void + { + $this->printIt(); + } +} + +class Class2 +{ + /** @use PrintSomething<\Exception> */ + use PrintSomething; + + public function what(): void + { + $this->printIt(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12798.php b/tests/PHPStan/Rules/Comparison/data/bug-12798.php new file mode 100644 index 00000000000..d6d29084b56 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12798.php @@ -0,0 +1,53 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug12798; + +interface Colorable +{ + public function color(): string; +} + +trait HasColors +{ + /** @return array */ + public static function colors(): array + { + /** @phpstan-ignore return.type */ + return array_reduce(self::cases(), function (array $colors, self $case) { + $key = is_subclass_of($case, \BackedEnum::class) ? $case->value : $case->name; + $color = is_subclass_of($case, Colorable::class) ? $case->color() : 'gray'; + + $colors[$key] = $color; + return $colors; + }, []); + } +} + +enum AlertLevelBacked: int implements Colorable +{ + use HasColors; + + case Low = 1; + case Medium = 2; + case Critical = 3; + + public function color(): string + { + return match ($this) { + self::Low => 'green', + self::Medium => 'yellow', + self::Critical => 'red', + }; + } +} + +enum AlertLevel +{ + use HasColors; + + case Low; + case Medium; + case Critical; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13023.php b/tests/PHPStan/Rules/Comparison/data/bug-13023.php new file mode 100644 index 00000000000..dae307df47d --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13023.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug13474; + +/** + * @template TValue of mixed + */ +interface ModelInterface +{ + /** + * @return TValue + */ + public function getValue(): mixed; +} + +/** + * @implements ModelInterface + */ +class ModelA implements ModelInterface +{ + #[\Override] + public function getValue(): int + { + return 0; + } +} + +/** + * @implements ModelInterface + */ +class ModelB implements ModelInterface +{ + #[\Override] + public function getValue(): string + { + return 'foo'; + } +} + +/** + * @template T of ModelInterface + */ +trait ModelTrait +{ + /** + * @return T + */ + abstract function model(): ModelInterface; + + /** + * @return template-type + */ + public function getValue(): mixed + { + return $this->model()->getValue(); + } + + public function test(): void + { + if (is_string($this->getValue())) { + echo 'string'; + return; + } + + echo 'other'; + } +} + +class TestA +{ + /** @use ModelTrait */ + use ModelTrait; + + #[\Override] + function model(): ModelA + { + return new ModelA(); + } +} + +class TestB +{ + /** @use ModelTrait */ + use ModelTrait; + + #[\Override] + function model(): ModelB + { + return new ModelB(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13687.php b/tests/PHPStan/Rules/Comparison/data/bug-13687.php new file mode 100644 index 00000000000..0ccb02a4263 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13687.php @@ -0,0 +1,34 @@ +bar(); + } + + if (property_exists($this, 'baz')) { + $a = $this->baz; + } + } +} + +class A +{ + use MyTrait; + + public string $baz = 'baz'; +} + +class B +{ + use MyTrait; + + public function bar(): void + { + echo 'bar'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4121.php b/tests/PHPStan/Rules/Comparison/data/bug-4121.php new file mode 100644 index 00000000000..a790dbe7f51 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4121.php @@ -0,0 +1,23 @@ + 'abc', + 'valueToFetch' => '123', + ]; +} + +final class SecondConsumer +{ + use MyLogic; + + private const MY_CONST_ARRAY = [ + 'someValue' => 'abc', + 'someOtherValue' => '123', + ]; +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7599.php b/tests/PHPStan/Rules/Comparison/data/bug-7599.php new file mode 100644 index 00000000000..37210e72415 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7599.php @@ -0,0 +1,41 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug7599; + +trait TraitForEnum +{ + /** + * @return array + */ + public static function fooMethod(): array + { + return array_map( + fn(self $enum): string => method_exists($enum, 'barMethod') + ? $enum->barMethod() + : $enum->name, + static::cases() + ); + } +} + +enum TestEnum: string +{ + use TraitForEnum; + + case Foo = 'foo'; + case Bar = 'bar'; +} + +enum SecondEnum: string +{ + use TraitForEnum; + + case Baz = 'baz'; + + public function barMethod(): string + { + return 'blah'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-8060.php b/tests/PHPStan/Rules/Comparison/data/bug-8060.php new file mode 100644 index 00000000000..b762a452ba5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-8060.php @@ -0,0 +1,39 @@ +getAnything(); + + if ($anything !== null) { + return; + } + + echo 'foo'; + } + + abstract protected function getAnything(): ?string; +} + +class Example +{ + use ExampleTrait; + + protected function getAnything(): string + { + return 'foo'; + } +} + +class Example2 +{ + use ExampleTrait; + + protected function getAnything(): ?string + { + return 'foo'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9095.php b/tests/PHPStan/Rules/Comparison/data/bug-9095.php new file mode 100644 index 00000000000..32fa108496f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9095.php @@ -0,0 +1,34 @@ +bar(); + } +} + +class EmptyClass +{ + use SomeTrait; +} + +trait SomeTrait +{ + public function bar(): void + { + if (property_exists($this, 'message')) { + if (!is_string($this->message)) { + return; + } + + echo $this->message . "\n"; + } + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9515.php b/tests/PHPStan/Rules/Comparison/data/bug-9515.php new file mode 100644 index 00000000000..cd2bb14525b --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9515.php @@ -0,0 +1,39 @@ +getFoo() !== null) { + $str .= ' World'; + } + + return $str; + } +} + +class Bar +{ + use Foo; + + public function getFoo(): string + { + return "Bar"; + } +} + +class Zar +{ + use Foo; + + public function getFoo(): null + { + return null; + } +} From 43c2d272a0e19bf403c367fd111f339adc55792d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 15:49:55 +0100 Subject: [PATCH 56/70] ImpossibleInstanceOfRule - add PossiblyImpureTipHelper and ConstantConditionInTraitHelper --- .../Classes/ImpossibleInstanceOfRule.php | 46 +++++++++---- .../Classes/ImpossibleInstanceOfRuleTest.php | 62 +++++++++++++++-- .../data/impossible-instanceof-in-trait.php | 52 ++++++++++++++ .../data/possibly-impure-instanceof-tip.php | 69 +++++++++++++++++++ 4 files changed, 209 insertions(+), 20 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/impossible-instanceof-in-trait.php create mode 100644 tests/PHPStan/Rules/Classes/data/possibly-impure-instanceof-tip.php diff --git a/src/Rules/Classes/ImpossibleInstanceOfRule.php b/src/Rules/Classes/ImpossibleInstanceOfRule.php index 2adbba15a26..8dbee61149e 100644 --- a/src/Rules/Classes/ImpossibleInstanceOfRule.php +++ b/src/Rules/Classes/ImpossibleInstanceOfRule.php @@ -3,10 +3,14 @@ namespace PHPStan\Rules\Classes; use PhpParser\Node; +use PHPStan\Analyser\CollectedDataEmitter; +use PHPStan\Analyser\NodeCallbackInvoker; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Parser\LastConditionVisitor; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; +use PHPStan\Rules\Comparison\PossiblyImpureTipHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -29,6 +33,8 @@ final class ImpossibleInstanceOfRule implements Rule public function __construct( private RuleLevelHelper $ruleLevelHelper, + private PossiblyImpureTipHelper $possiblyImpureTipHelper, + private ConstantConditionInTraitHelper $constantConditionInTraitHelper, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, #[AutowiredParameter] @@ -44,7 +50,7 @@ public function getNodeType(): string return Node\Expr\Instanceof_::class; } - public function processNode(Node $node, Scope $scope): array + public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { if ($node->class instanceof Node\Name) { $className = $scope->resolveName($node->class); @@ -74,40 +80,48 @@ public function processNode(Node $node, Scope $scope): array $instanceofType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node); if (!$instanceofType instanceof ConstantBooleanType) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } $addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder { if (!$this->treatPhpDocTypesAsCertain) { - return $ruleErrorBuilder; + return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } $instanceofTypeWithoutPhpDocs = $scope->getNativeType($node); if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) { - return $ruleErrorBuilder; + return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } if (!$this->treatPhpDocTypesAsCertainTip) { - return $ruleErrorBuilder; + return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } - return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + $ruleErrorBuilder = $ruleErrorBuilder->treatPhpDocTypesAsCertainTip(); + + return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); }; if (!$instanceofType->getValue()) { $exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->expr) : $scope->getNativeType($node->expr); - return [ - $addTip(RuleErrorBuilder::message(sprintf( - 'Instanceof between %s and %s will always evaluate to false.', - $exprType->describe(VerbosityLevel::typeOnly()), - $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), - )))->identifier('instanceof.alwaysFalse')->build(), - ]; + $ruleError = $addTip(RuleErrorBuilder::message(sprintf( + 'Instanceof between %s and %s will always evaluate to false.', + $exprType->describe(VerbosityLevel::typeOnly()), + $classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)), + )))->identifier('instanceof.alwaysFalse')->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError); + return []; + } + + return [$ruleError]; } $isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME); if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) { + $this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node); return []; } @@ -123,7 +137,13 @@ public function processNode(Node $node, Scope $scope): array $errorBuilder->identifier('instanceof.alwaysTrue'); - return [$errorBuilder->build()]; + $ruleError = $errorBuilder->build(); + if ($scope->isInTrait()) { + $this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError); + return []; + } + + return [$ruleError]; } } diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index f7752941d98..cbe569ed60a 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -2,8 +2,12 @@ namespace PHPStan\Rules\Classes; +use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper; +use PHPStan\Rules\Comparison\ConstantConditionInTraitRule; +use PHPStan\Rules\Comparison\PossiblyImpureTipHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; +use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -11,7 +15,7 @@ use const PHP_VERSION_ID; /** - * @extends RuleTestCase + * @extends RuleTestCase */ class ImpossibleInstanceOfRuleTest extends RuleTestCase { @@ -33,12 +37,18 @@ protected function getRule(): Rule discoveringSymbolsTip: true, ); - return new ImpossibleInstanceOfRule( - $ruleLevelHelper, - treatPhpDocTypesAsCertain: $this->treatPhpDocTypesAsCertain, - reportAlwaysTrueInLastCondition: $this->reportAlwaysTrueInLastCondition, - treatPhpDocTypesAsCertainTip: true, - ); + // @phpstan-ignore argument.type + return new CompositeRule([ + new ImpossibleInstanceOfRule( + $ruleLevelHelper, + new PossiblyImpureTipHelper(true), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + treatPhpDocTypesAsCertain: $this->treatPhpDocTypesAsCertain, + reportAlwaysTrueInLastCondition: $this->reportAlwaysTrueInLastCondition, + treatPhpDocTypesAsCertainTip: true, + ), + new ConstantConditionInTraitRule(), + ]); } protected function shouldTreatPhpDocTypesAsCertain(): bool @@ -602,4 +612,42 @@ public function testBug13975(string $file): void $this->analyse([$file], []); } + public function testPossiblyImpureTip(): void + { + $this->treatPhpDocTypesAsCertain = true; + $learnMore = ' Learn more: https://phpstan.org/blog/remembering-and-forgetting-returned-values'; + $this->analyse([__DIR__ . '/data/possibly-impure-instanceof-tip.php'], [ + // maybe-impure: tip expected + [ + 'Instanceof between PossiblyImpureInstanceofTip\Cat and PossiblyImpureInstanceofTip\Cat will always evaluate to true.', + 41, + 'If PossiblyImpureInstanceofTip\Holder::maybeImpureMethod() is impure, add @phpstan-impure PHPDoc tag above its declaration.' . $learnMore, + ], + // pure: no tip, error explained by type + [ + 'Instanceof between PossiblyImpureInstanceofTip\Cat and PossiblyImpureInstanceofTip\Cat will always evaluate to true.', + 53, + ], + // impure: no error - $holder invalidated + ]); + } + + public function testInTrait(): void + { + $this->treatPhpDocTypesAsCertain = true; + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/impossible-instanceof-in-trait.php'], [ + [ + 'Instanceof between ImpossibleInstanceofInTrait\Cat and stdClass will always evaluate to false.', + 25, + $tipText, + ], + [ + 'Instanceof between ImpossibleInstanceofInTrait\Dog and stdClass will always evaluate to false.', + 25, + $tipText, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/impossible-instanceof-in-trait.php b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-in-trait.php new file mode 100644 index 00000000000..1419e085e22 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/impossible-instanceof-in-trait.php @@ -0,0 +1,52 @@ +animal instanceof Dog) { + + } + } + + public function doFoo2(): void + { + // always false + if ($this->animal instanceof \stdClass) { + + } + } + +} + +class Foo +{ + + /** @use FooTrait */ + use FooTrait; + + /** @var Dog */ + protected $animal; + +} + +class FooAnother +{ + + /** @use FooTrait */ + use FooTrait; + + /** @var Cat */ + protected $animal; + +} diff --git a/tests/PHPStan/Rules/Classes/data/possibly-impure-instanceof-tip.php b/tests/PHPStan/Rules/Classes/data/possibly-impure-instanceof-tip.php new file mode 100644 index 00000000000..bac1fbcce67 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/possibly-impure-instanceof-tip.php @@ -0,0 +1,69 @@ +getAnimal() instanceof Cat) { + $holder->maybeImpureMethod(); + + // tip expected: maybeImpureMethod() might have changed the object + if ($holder->getAnimal() instanceof Cat) { + return; + } + } +} + +function testPure(Holder $holder): void +{ + if ($holder->getAnimal() instanceof Cat) { + $holder->pureMethod(); + + // no tip - pureMethod() cannot change anything + if ($holder->getAnimal() instanceof Cat) { + return; + } + } +} + +function testImpure(Holder $holder): void +{ + if ($holder->getAnimal() instanceof Cat) { + $holder->impureMethod(); + + // no error - $holder invalidated by impure call + if ($holder->getAnimal() instanceof Cat) { + return; + } + } +} From 8175b6e54ab506b5f2126127faa534bbed5a425e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 16:04:33 +0100 Subject: [PATCH 57/70] Regression test Closes https://github.com/phpstan/phpstan/issues/10353 --- .../Classes/ImpossibleInstanceOfRuleTest.php | 6 +++ .../PHPStan/Rules/Classes/data/bug-10353.php | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 tests/PHPStan/Rules/Classes/data/bug-10353.php diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php index cbe569ed60a..6dc06a51159 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php @@ -546,6 +546,12 @@ public function testBug10036(): void ]); } + public function testBug10353(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-10353.php'], []); + } + public function testBug12267(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Classes/data/bug-10353.php b/tests/PHPStan/Rules/Classes/data/bug-10353.php new file mode 100644 index 00000000000..bf4305af417 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-10353.php @@ -0,0 +1,37 @@ +test(); + } +} + +class OtherClass +{ + use Foo; + + function bar(): string + { + return $this->test(); + } +} From 686ab8ab6bebcf4c1ad403488b8264bcecc46e27 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 16:15:09 +0100 Subject: [PATCH 58/70] Report errors in traits with the same message just once --- .github/workflows/e2e-tests.yml | 6 +++++ e2e/in-trait/phpstan.neon | 4 ++++ e2e/in-trait/src/Bar.php | 20 ++++++++++++++++ e2e/in-trait/src/Foo.php | 20 ++++++++++++++++ e2e/in-trait/src/FooTrait.php | 23 +++++++++++++++++++ src/Analyser/Error.php | 22 ++++++++++++++++++ .../ConstantConditionInTraitRule.php | 2 +- 7 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 e2e/in-trait/phpstan.neon create mode 100644 e2e/in-trait/src/Bar.php create mode 100644 e2e/in-trait/src/Foo.php create mode 100644 e2e/in-trait/src/FooTrait.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 29a350009cd..7cc8f5dd43c 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -267,6 +267,12 @@ jobs: cd e2e/bug-11857 composer install ../../bin/phpstan + - script: | + cd e2e/in-trait + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw") + ../bashunit -a contains 'FooTrait.php:10:Strict comparison using === between int<0, max> and false will always evaluate to false.' "$OUTPUT" + ../bashunit -a contains 'FooTrait.php (in context of class E2EInTrait\Bar):18:Strict comparison using === between E2EInTrait\Bar and null will always evaluate to false.' "$OUTPUT" + ../bashunit -a contains 'FooTrait.php (in context of class E2EInTrait\Foo):18:Strict comparison using === between E2EInTrait\Foo and null will always evaluate to false.' "$OUTPUT" - script: | cd e2e/result-cache-meta-extension composer install diff --git a/e2e/in-trait/phpstan.neon b/e2e/in-trait/phpstan.neon new file mode 100644 index 00000000000..c308dcf5421 --- /dev/null +++ b/e2e/in-trait/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 8 + paths: + - src diff --git a/e2e/in-trait/src/Bar.php b/e2e/in-trait/src/Bar.php new file mode 100644 index 00000000000..a5a07b82067 --- /dev/null +++ b/e2e/in-trait/src/Bar.php @@ -0,0 +1,20 @@ +getSth() === null) { + + } + + if ($this->getSth2() === null) { + + } + } + +} diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index 9af5f2b297d..107db06f08a 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -106,6 +106,28 @@ public function changeTraitFilePath(string $newFilePath): self ); } + public function removeTraitContext(): self + { + if ($this->traitFilePath === null) { + throw new ShouldNotHappenException(); + } + + return new self( + $this->message, + $this->traitFilePath, + $this->line, + $this->canBeIgnored, + $this->filePath, + null, + $this->tip, + $this->nodeLine, + $this->nodeType, + $this->identifier, + $this->metadata, + $this->fixedErrorDiff, + ); + } + public function getTraitFilePath(): ?string { return $this->traitFilePath; diff --git a/src/Rules/Comparison/ConstantConditionInTraitRule.php b/src/Rules/Comparison/ConstantConditionInTraitRule.php index 83ca84f220e..fd595520e12 100644 --- a/src/Rules/Comparison/ConstantConditionInTraitRule.php +++ b/src/Rules/Comparison/ConstantConditionInTraitRule.php @@ -77,7 +77,7 @@ public function processNode(Node $node, Scope $scope): array if (count($uniquedErrors) === 1) { // report directly in trait, no "in context of" - $transformedErrors[] = new TransformedRuleError($uniquedErrors[0]); + $transformedErrors[] = new TransformedRuleError($uniquedErrors[0]->removeTraitContext()); continue; } From 7b668ec776381dcbee70d3a83fabe61a90844038 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 16:22:06 +0100 Subject: [PATCH 59/70] Fix lint --- tests/PHPStan/Rules/Comparison/data/bug-9515.php | 4 +++- tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php | 2 +- .../Rules/Comparison/data/elseif-condition-in-trait.php | 2 +- tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php | 2 +- tests/PHPStan/Rules/Comparison/data/match-in-trait.php | 2 +- tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9515.php b/tests/PHPStan/Rules/Comparison/data/bug-9515.php index cd2bb14525b..07667db002f 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-9515.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-9515.php @@ -1,4 +1,6 @@ -= 8.2 + +declare(strict_types = 1); namespace Bug9515; diff --git a/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php b/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php index 1e5e9c279f6..05c5fb89007 100644 --- a/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php +++ b/tests/PHPStan/Rules/Comparison/data/do-while-in-trait.php @@ -1,4 +1,4 @@ -= 8.2 namespace DoWhileInTrait; diff --git a/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php b/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php index 9815ff99550..0ec93827730 100644 --- a/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php +++ b/tests/PHPStan/Rules/Comparison/data/elseif-condition-in-trait.php @@ -1,4 +1,4 @@ -= 8.2 namespace ElseIfConditionInTrait; diff --git a/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php b/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php index 969f7807fd2..9a9c140b35b 100644 --- a/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php +++ b/tests/PHPStan/Rules/Comparison/data/logical-xor-in-trait.php @@ -1,4 +1,4 @@ -= 8.2 namespace LogicalXorInTrait; diff --git a/tests/PHPStan/Rules/Comparison/data/match-in-trait.php b/tests/PHPStan/Rules/Comparison/data/match-in-trait.php index 45e069dad71..bf257ff0550 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-in-trait.php +++ b/tests/PHPStan/Rules/Comparison/data/match-in-trait.php @@ -1,4 +1,4 @@ -= 8.0 += 8.2 namespace MatchInTrait; diff --git a/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php b/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php index c2f34f1b4da..a5538629436 100644 --- a/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php +++ b/tests/PHPStan/Rules/Comparison/data/while-false-in-trait.php @@ -1,4 +1,4 @@ -= 8.2 namespace WhileFalseInTrait; From 43aed2c2c625b86678c0ac6be3c4edf3ca2b5798 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 16:24:06 +0100 Subject: [PATCH 60/70] Fix tests --- .../Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php | 2 ++ .../Rules/Comparison/ElseIfConstantConditionRuleTest.php | 1 + .../Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php | 1 + .../Rules/Comparison/LogicalXorConstantConditionRuleTest.php | 2 ++ .../Comparison/StrictComparisonOfDifferentTypesRuleTest.php | 1 + .../Comparison/TernaryOperatorConstantConditionRuleTest.php | 2 ++ .../Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php | 2 ++ tests/PHPStan/Rules/Comparison/data/bug-11949.php | 2 +- 8 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php index f0045408012..8c8e736a579 100644 --- a/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/DoWhileLoopConstantConditionRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -82,6 +83,7 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] public function testInTrait(): void { $this->analyse([__DIR__ . '/data/do-while-in-trait.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php index 342be2c84b1..5575663828d 100644 --- a/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ElseIfConstantConditionRuleTest.php @@ -158,6 +158,7 @@ public function testBug6947(): void ]); } + #[RequiresPhp('>= 8.2')] public function testInTrait(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 8a32b0ddf66..bea6b5a12cd 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1166,6 +1166,7 @@ public function testBug7599(): void $this->analyse([__DIR__ . '/data/bug-7599.php'], []); } + #[RequiresPhp('>= 8.0')] public function testBug13474(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php index bea49f94bc1..c68f739131a 100644 --- a/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/LogicalXorConstantConditionRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule as TRule; use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -77,6 +78,7 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] public function testInTrait(): void { $this->analyse([__DIR__ . '/data/logical-xor-in-trait.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index f457963e726..df6773794f1 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1046,6 +1046,7 @@ public function testBug8060(): void $this->analyse([__DIR__ . '/data/bug-8060.php'], []); } + #[RequiresPhp('>= 8.2')] public function testBug9515(): void { $this->analyse([__DIR__ . '/data/bug-9515.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php index 90e904c1ace..3bd4459d290 100644 --- a/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/TernaryOperatorConstantConditionRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -106,6 +107,7 @@ public function testBug7580(): void $this->analyse([__DIR__ . '/data/bug-7580.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug11949(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php index caf89ddfe30..e4356732de2 100644 --- a/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/WhileLoopAlwaysFalseConditionRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; /** * @extends RuleTestCase @@ -50,6 +51,7 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] public function testInTrait(): void { $this->analyse([__DIR__ . '/data/while-false-in-trait.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11949.php b/tests/PHPStan/Rules/Comparison/data/bug-11949.php index b1373e56237..4de1ea766b3 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-11949.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-11949.php @@ -1,4 +1,4 @@ -= 8.1 namespace Bug11949; From b47a204e3add46a834f8b1a303b06548d33fed5e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 27 Mar 2026 16:29:59 +0100 Subject: [PATCH 61/70] Remove dead `@phpstan-ignore` in traits --- src/Type/Generic/TemplateTypeTrait.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index d823f9b20d5..b3020a07470 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -132,7 +132,7 @@ public function subtract(Type $typeToRemove): Type public function getTypeWithoutSubtractedType(): Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + if (!$bound instanceof SubtractableType) { return $this; } @@ -149,7 +149,7 @@ public function getTypeWithoutSubtractedType(): Type public function changeSubtractedType(?Type $subtractedType): Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + if (!$bound instanceof SubtractableType) { return $this; } @@ -166,7 +166,7 @@ public function changeSubtractedType(?Type $subtractedType): Type public function getSubtractedType(): ?Type { $bound = $this->getBound(); - if (!$bound instanceof SubtractableType) { // @phpstan-ignore instanceof.alwaysTrue + if (!$bound instanceof SubtractableType) { return null; } From bbf5afba969cc478cca0ed94d524734032ddd0e6 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 26 Mar 2026 11:03:53 +0100 Subject: [PATCH 62/70] reportUnsafeArrayStringKeyCasting - detect implementation --- conf/config.neon | 1 + conf/parametersSchema.neon | 1 + src/DependencyInjection/ContainerFactory.php | 1 + ...eportUnsafeArrayStringKeyCastingToggle.php | 34 +++++++ .../AccessoryDecimalIntegerStringType.php | 2 +- src/Type/ArrayType.php | 39 +++++++- ...ringKeyCastingDetectTypeAcceptanceTest.php | 45 +++++++++ ...tringKeyCastingDetectTypeInferenceTest.php | 40 ++++++++ ...ringKeyCastingUnsafeTypeAcceptanceTest.php | 34 +++++++ ...nsafe-array-string-key-casting-accepts.php | 42 +++++++++ ...unsafe-array-string-key-casting-detect.php | 91 +++++++++++++++++++ .../Analyser/nsrt/decimal-int-string.php | 10 ++ ...portUnsafeArrayStringKeyCastingDetect.neon | 2 + 13 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 src/DependencyInjection/ReportUnsafeArrayStringKeyCastingToggle.php create mode 100644 tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest.php create mode 100644 tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest.php create mode 100644 tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest.php create mode 100644 tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-accepts.php create mode 100644 tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-detect.php create mode 100644 tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingDetect.neon diff --git a/conf/config.neon b/conf/config.neon index cb8c20ad481..67a5301b571 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -82,6 +82,7 @@ parameters: reportWrongPhpDocTypeInVarTag: false reportAnyTypeWideningInVarTag: false reportNonIntStringArrayKey: false + reportUnsafeArrayStringKeyCasting: null reportPossiblyNonexistentGeneralArrayOffset: false reportPossiblyNonexistentConstantArrayOffset: false checkMissingOverrideMethodAttribute: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index bc79fe7c401..300ea5ac343 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -91,6 +91,7 @@ parametersSchema: reportWrongPhpDocTypeInVarTag: bool() reportAnyTypeWideningInVarTag: bool() reportNonIntStringArrayKey: bool() + reportUnsafeArrayStringKeyCasting: schema(string(), pattern('detect|prevent'), nullable()) reportPossiblyNonexistentGeneralArrayOffset: bool() reportPossiblyNonexistentConstantArrayOffset: bool() checkMissingOverrideMethodAttribute: bool() diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index cac88d0e39b..6e8f0a385e7 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -202,6 +202,7 @@ public static function postInitializeContainer(Container $container): void $container->getService('typeSpecifier'); BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']); + ReportUnsafeArrayStringKeyCastingToggle::setLevel($container->getParameter('reportUnsafeArrayStringKeyCasting')); } public function getCurrentWorkingDirectory(): string diff --git a/src/DependencyInjection/ReportUnsafeArrayStringKeyCastingToggle.php b/src/DependencyInjection/ReportUnsafeArrayStringKeyCastingToggle.php new file mode 100644 index 00000000000..e2f13563fec --- /dev/null +++ b/src/DependencyInjection/ReportUnsafeArrayStringKeyCastingToggle.php @@ -0,0 +1,34 @@ +inverse) { - return new StringType(); + return $this; } return new IntegerType(); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 288caefdc63..3a326a13d2b 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type; +use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; @@ -12,6 +13,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Constant\ConstantArrayType; @@ -47,6 +49,8 @@ class ArrayType implements Type private Type $keyType; + private ?Type $cachedIterableKeyType = null; + /** @api */ public function __construct(Type $keyType, private Type $itemType) { @@ -198,15 +202,44 @@ public function getArraySize(): Type public function getIterableKeyType(): Type { + if ($this->cachedIterableKeyType !== null) { + return $this->cachedIterableKeyType; + } $keyType = $this->keyType; if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) { - return new BenevolentUnionType([new IntegerType(), new StringType()]); + $keyType = new BenevolentUnionType([new IntegerType(), new StringType()]); } if ($keyType instanceof StrictMixedType) { - return new BenevolentUnionType([new IntegerType(), new StringType()]); + $keyType = new BenevolentUnionType([new IntegerType(), new StringType()]); + } + + $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel(); + if ($level === null) { + return $this->cachedIterableKeyType = $keyType; + } + + if ($level === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) { + return $this->cachedIterableKeyType = $keyType; + } + + if ($level !== ReportUnsafeArrayStringKeyCastingToggle::DETECT) { // @phpstan-ignore notIdentical.alwaysFalse + throw new ShouldNotHappenException(); } - return $keyType; + return $this->cachedIterableKeyType = TypeTraverser::map($keyType, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + if ($type->isString()->yes() && !$type->isDecimalIntegerString()->no()) { + return TypeCombinator::union( + new IntegerType(), + TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true)), + ); + } + + return $type; + }); } public function getFirstIterableKeyType(): Type diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest.php new file mode 100644 index 00000000000..02412d59406 --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest.php @@ -0,0 +1,45 @@ + + */ +class ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return self::getContainer()->getByType(CallMethodsRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [ + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 33, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 39, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/reportUnsafeArrayStringKeyCastingDetect.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest.php new file mode 100644 index 00000000000..1ad96508af6 --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest.php @@ -0,0 +1,40 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/reportUnsafeArrayStringKeyCastingDetect.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest.php new file mode 100644 index 00000000000..429a6a438ae --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest.php @@ -0,0 +1,34 @@ + + */ +class ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return self::getContainer()->getByType(CallMethodsRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [ + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 33, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 39, + ], + ]); + } + +} diff --git a/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-accepts.php b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-accepts.php new file mode 100644 index 00000000000..d04e42a4990 --- /dev/null +++ b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-accepts.php @@ -0,0 +1,42 @@ + $a */ + public function doFoo(array $a): void + { + + } + + /** @param array $a */ + public function doBar(array $a): void + { + + } + + /** @param array $a */ + public function doBaz(array $a): void + { + + } + + public function doTest(string $s): void + { + $a = [$s => new stdClass()]; + $this->doFoo($a); + $this->doBar($a); + $this->doBaz($a); + + $b = []; + $b[$s] = new stdClass(); + $this->doFoo($b); + $this->doBar($b); + $this->doBaz($b); + } + +} diff --git a/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-detect.php b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-detect.php new file mode 100644 index 00000000000..df88c4437db --- /dev/null +++ b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-detect.php @@ -0,0 +1,91 @@ + $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + /** + * @param array $a + */ + public function doBar(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('(int|non-decimal-int-string)', $k); + } + } + + /** + * @param array $a + */ + public function doBaz(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + public function doArrayCreationAndAssign(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + +} + +class FooNonDecimalIntString +{ + + /** + * @param array $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('non-decimal-int-string', $k); + } + } + + /** + * @param array $a + */ + public function doBaz(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + /** @param non-decimal-int-string $s */ + public function doArrayCreationAndAssign(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/decimal-int-string.php b/tests/PHPStan/Analyser/nsrt/decimal-int-string.php index 63747525028..fe5c5fdd529 100644 --- a/tests/PHPStan/Analyser/nsrt/decimal-int-string.php +++ b/tests/PHPStan/Analyser/nsrt/decimal-int-string.php @@ -35,6 +35,16 @@ public function doBar(string $s): void assertType('float|int', $s + $s); } + public function doBaz(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + /** * @param non-decimal-int-string $s */ diff --git a/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingDetect.neon b/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingDetect.neon new file mode 100644 index 00000000000..1ea800ca917 --- /dev/null +++ b/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingDetect.neon @@ -0,0 +1,2 @@ +parameters: + reportUnsafeArrayStringKeyCasting: detect From 3012e242770bbefea84b18029c42ba3f3a1b0da9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 29 Mar 2026 15:07:50 +0200 Subject: [PATCH 63/70] reportUnsafeArrayStringKeyCasting - prevent implementation --- .../ValidateIgnoredErrorsExtension.php | 1 + src/PhpDoc/TypeNodeResolver.php | 31 ++++++- .../Accessory/AccessoryNumericStringType.php | 17 +++- src/Type/ArrayType.php | 9 +- src/Type/StringType.php | 12 ++- ...ingKeyCastingPreventTypeAcceptanceTest.php | 53 +++++++++++ ...ringKeyCastingPreventTypeInferenceTest.php | 40 ++++++++ ...nsafe-array-string-key-casting-prevent.php | 91 +++++++++++++++++++ ...ortUnsafeArrayStringKeyCastingPrevent.neon | 2 + 9 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest.php create mode 100644 tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeInferenceTest.php create mode 100644 tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-prevent.php create mode 100644 tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingPrevent.neon diff --git a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php index 6bcd2eb995d..3ee37fc86b6 100644 --- a/src/DependencyInjection/ValidateIgnoredErrorsExtension.php +++ b/src/DependencyInjection/ValidateIgnoredErrorsExtension.php @@ -139,6 +139,7 @@ public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry } }, new OversizedArrayBuilder(), true), + reportUnsafeArrayStringKeyCasting: null, ), ), ); diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 815789377a7..27e7ae27ac9 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -10,7 +10,9 @@ use PhpParser\Node\Name; use PHPStan\Analyser\ConstantResolver; use PHPStan\Analyser\NameScope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode; @@ -106,6 +108,7 @@ use PHPStan\Type\TypeAliasResolver; use PHPStan\Type\TypeAliasResolverProvider; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use PHPStan\Type\ValueOfType; @@ -128,6 +131,9 @@ use function strtolower; use function substr; +/** + * @phpstan-import-type Level from ReportUnsafeArrayStringKeyCastingToggle as ReportUnsafeArrayStringKeyCastingLevel + */ #[AutowiredService] final class TypeNodeResolver { @@ -135,12 +141,17 @@ final class TypeNodeResolver /** @var array */ private array $genericTypeResolvingStack = []; + /** + * @param ReportUnsafeArrayStringKeyCastingLevel $reportUnsafeArrayStringKeyCasting + */ public function __construct( private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider, private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider, private TypeAliasResolverProvider $typeAliasResolverProvider, private ConstantResolver $constantResolver, private InitializerExprTypeResolver $initializerExprTypeResolver, + #[AutowiredParameter] + private ?string $reportUnsafeArrayStringKeyCasting, ) { } @@ -661,7 +672,7 @@ private function resolveConditionalTypeForParameterNode(ConditionalTypeForParame private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type { $itemType = $this->resolve($typeNode->type, $nameScope); - return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $itemType); + return new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $itemType); } private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type @@ -686,9 +697,23 @@ static function (string $variance): TemplateTypeVariance { if (in_array($mainTypeName, ['array', 'non-empty-array'], true)) { if (count($genericTypes) === 1) { // array - $arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]); + $arrayType = new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $genericTypes[0]); } elseif (count($genericTypes) === 2) { // array - $keyType = TypeCombinator::intersect($genericTypes[0]->toArrayKey(), new UnionType([ + $originalKey = $genericTypes[0]; + if ($this->reportUnsafeArrayStringKeyCasting === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) { + $originalKey = TypeTraverser::map($originalKey, static function (Type $type, callable $traverse) { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof StringType) { + return TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true)); + } + + return $type; + }); + } + $keyType = TypeCombinator::intersect($originalKey->toArrayKey(), new UnionType([ new IntegerType(), new StringType(), ]))->toArrayKey(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 62e320e5de5..d0a7d65ac0d 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Accessory; +use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; @@ -209,12 +210,20 @@ public function toArray(): Type public function toArrayKey(): Type { + $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel(); + if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT) { + new UnionType([ + new IntegerType(), + new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + ]), + ]); + } + return new UnionType([ new IntegerType(), - new IntersectionType([ - new StringType(), - new AccessoryNumericStringType(), - ]), + new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]), ]); } diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 3a326a13d2b..8a82601946f 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -34,6 +34,7 @@ use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; use function array_merge; use function count; +use function in_array; use function sprintf; /** @api */ @@ -54,11 +55,11 @@ class ArrayType implements Type /** @api */ public function __construct(Type $keyType, private Type $itemType) { - if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') { + if (in_array($keyType->describe(VerbosityLevel::value()), ['(int|string)', '(int|non-decimal-int-string)'], true)) { $keyType = new MixedType(); } if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) { - $keyType = new UnionType([new StringType(), new IntegerType()]); + $keyType = (new UnionType([new StringType(), new IntegerType()]))->toArrayKey(); } $this->keyType = $keyType; @@ -207,10 +208,10 @@ public function getIterableKeyType(): Type } $keyType = $this->keyType; if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) { - $keyType = new BenevolentUnionType([new IntegerType(), new StringType()]); + $keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(); } if ($keyType instanceof StrictMixedType) { - $keyType = new BenevolentUnionType([new IntegerType(), new StringType()]); + $keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(); } $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 03361300331..afbdcf92a43 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -2,12 +2,14 @@ namespace PHPStan\Type; +use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle; use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; @@ -177,7 +179,15 @@ public function toArray(): Type public function toArrayKey(): Type { - return $this; + $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel(); + if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT) { + return $this; + } + + return new UnionType([ + new IntegerType(), + TypeCombinator::intersect($this, new AccessoryDecimalIntegerStringType(inverse: true)), + ]); } public function toCoercedArgumentType(bool $strictTypes): Type diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest.php new file mode 100644 index 00000000000..846ab7512a5 --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest.php @@ -0,0 +1,53 @@ + + */ +class ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest extends RuleTestCase +{ + + public function getRule(): Rule + { + return self::getContainer()->getByType(CallMethodsRule::class); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [ + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doFoo() expects array, non-empty-array given.', + 31, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 33, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doFoo() expects array, non-empty-array given.', + 37, + ], + [ + 'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array, non-empty-array given.', + 39, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/reportUnsafeArrayStringKeyCastingPrevent.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeInferenceTest.php b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeInferenceTest.php new file mode 100644 index 00000000000..0f8b64b4b51 --- /dev/null +++ b/tests/PHPStan/Analyser/ReportUnsafeArrayStringKeyCastingPreventTypeInferenceTest.php @@ -0,0 +1,40 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/reportUnsafeArrayStringKeyCastingPrevent.neon', + ], + ); + } + +} diff --git a/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-prevent.php b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-prevent.php new file mode 100644 index 00000000000..163a996bd25 --- /dev/null +++ b/tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-prevent.php @@ -0,0 +1,91 @@ + $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('non-decimal-int-string', $k); + } + } + + /** + * @param array $a + */ + public function doBar(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('(int|non-decimal-int-string)', $k); + } + } + + /** + * @param array $a + */ + public function doBaz(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + public function doArrayCreationAndAssign(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + +} + +class FooNonDecimalIntString +{ + + /** + * @param array $a + */ + public function doFoo(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('non-decimal-int-string', $k); + } + } + + /** + * @param array $a + */ + public function doBaz(array $a): void + { + assertType('array', $a); + foreach ($a as $k => $v) { + assertType('int|non-decimal-int-string', $k); + } + } + + /** @param non-decimal-int-string $s */ + public function doArrayCreationAndAssign(string $s): void + { + $a = [$s => 1]; + assertType('non-empty-array', $a); + + $b = []; + $b[$s] = 2; + assertType('non-empty-array', $b); + } + +} diff --git a/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingPrevent.neon b/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingPrevent.neon new file mode 100644 index 00000000000..f35820e8667 --- /dev/null +++ b/tests/PHPStan/Analyser/reportUnsafeArrayStringKeyCastingPrevent.neon @@ -0,0 +1,2 @@ +parameters: + reportUnsafeArrayStringKeyCasting: prevent From 3da8941511ce503c3f2acbefb993fbefc301ea5e Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 29 Mar 2026 15:54:46 +0200 Subject: [PATCH 64/70] Fix --- src/Type/ClassStringType.php | 2 +- src/Type/StringType.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Type/ClassStringType.php b/src/Type/ClassStringType.php index 2c22f319db0..55f24671a20 100644 --- a/src/Type/ClassStringType.php +++ b/src/Type/ClassStringType.php @@ -46,7 +46,7 @@ public function isString(): TrinaryLogic public function isNumericString(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return TrinaryLogic::createNo(); } public function isDecimalIntegerString(): TrinaryLogic diff --git a/src/Type/StringType.php b/src/Type/StringType.php index afbdcf92a43..e549f4ba826 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -184,6 +184,13 @@ public function toArrayKey(): Type return $this; } + $isDecimalIntString = $this->isDecimalIntegerString(); + if ($isDecimalIntString->no()) { + return $this; + } elseif ($isDecimalIntString->yes()) { + return new IntegerType(); + } + return new UnionType([ new IntegerType(), TypeCombinator::intersect($this, new AccessoryDecimalIntegerStringType(inverse: true)), From 6f6897de3a44dec9c80d56dd175a807a70db3261 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Sun, 29 Mar 2026 16:07:27 +0200 Subject: [PATCH 65/70] Issue bot - support reportUnsafeArrayStringKeyCasting --- issue-bot/src/Console/RunCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 15b47ac80e7..b2291373095 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -194,6 +194,7 @@ private function analyseHash(LoopInterface $loop, OutputInterface $output, int $ 'checkTooWideReturnTypesInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, 'checkTooWideParameterOutInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, 'checkTooWideThrowTypesInProtectedAndPublicMethods' => $options['checkTooWideTypesInProtectedAndPublicMethods'] ?? false, + 'reportUnsafeArrayStringKeyCasting' => $options['reportUnsafeArrayStringKeyCasting'] ?? null, ]; $parameters['exceptions'] = [ 'implicitThrows' => $options['implicitThrows'] ?? true, From 0d7fe195a10fa7f54ff249a077d4c5e148f911f9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 31 Mar 2026 12:14:49 +0200 Subject: [PATCH 66/70] Normalize decimal-int-string intersection with lowercase-string/uppercase-string --- .../AccessoryDecimalIntegerStringType.php | 13 ++++-- tests/PHPStan/Type/TypeCombinatorTest.php | 41 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/Type/Accessory/AccessoryDecimalIntegerStringType.php b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php index 93f2eed9f47..c83657bbd56 100644 --- a/src/Type/Accessory/AccessoryDecimalIntegerStringType.php +++ b/src/Type/Accessory/AccessoryDecimalIntegerStringType.php @@ -124,7 +124,14 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult return $otherType->isSuperTypeOf($this); } - if ($otherType instanceof AccessoryNumericStringType && !$this->inverse) { + if ( + ( + $otherType instanceof AccessoryNumericStringType + || $otherType instanceof AccessoryLowercaseStringType + || $otherType instanceof AccessoryUppercaseStringType + ) + && !$this->inverse + ) { return IsSuperTypeOfResult::createYes(); } @@ -357,12 +364,12 @@ public function isLiteralString(): TrinaryLogic public function isLowercaseString(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(); } public function isUppercaseString(): TrinaryLogic { - return TrinaryLogic::createMaybe(); + return $this->inverse ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes(); } public function isClassString(): TrinaryLogic diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index 9ea18aac9b5..6a04347b13d 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -2874,6 +2874,24 @@ public static function dataUnion(): iterable StringType::class, 'string', ]; + + yield [ + [ + $decimalIntString, + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + IntersectionType::class, + 'lowercase-string', + ]; + + yield [ + [ + $nonDecimalIntString, + new IntersectionType([new StringType(), new AccessoryLowercaseStringType()]), + ], + StringType::class, + 'string', + ]; } /** @@ -5024,6 +5042,29 @@ public static function dataIntersect(): iterable NeverType::class, '*NEVER*=implicit', ]; + + yield [ + [ + $decimalIntString, + new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + ]), + ], + IntersectionType::class, + 'decimal-int-string', + ]; + yield [ + [ + $nonDecimalIntString, + new IntersectionType([ + new StringType(), + new AccessoryLowercaseStringType(), + ]), + ], + IntersectionType::class, + 'lowercase-string&non-decimal-int-string', + ]; } /** From 399ce26031981c26a170b414d9905d63874ec2bf Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 1 Apr 2026 08:58:57 +0200 Subject: [PATCH 67/70] Fix CS --- src/Type/TypeCombinator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 202ecb5fc7b..2051b65ea73 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -27,8 +27,8 @@ use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateUnionType; -use function array_filter; use function array_fill; +use function array_filter; use function array_key_exists; use function array_key_first; use function array_merge; From 5df1fd4330b4c5356dca4b96aff52ff9d6d86a56 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 1 Apr 2026 11:12:47 +0200 Subject: [PATCH 68/70] Update baseline --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index e99255404ef..1ceaa3e986f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1722,7 +1722,7 @@ parameters: - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType - count: 5 + count: 6 path: src/Type/TypeCombinator.php - From 301cf483ecef1f0ae39386211bddbdcb0eea083e Mon Sep 17 00:00:00 2001 From: Theodore Brown Date: Wed, 8 Apr 2026 01:49:33 -0500 Subject: [PATCH 69/70] Respect sys_temp_dir from the parent process (#5390) --- .github/workflows/e2e-tests.yml | 11 +++++++++++ e2e/bug-14093/phpstan.neon | 4 ++++ e2e/bug-14093/test.php | 6 ++++++ src/Process/ProcessHelper.php | 4 ++++ 4 files changed, 25 insertions(+) create mode 100644 e2e/bug-14093/phpstan.neon create mode 100644 e2e/bug-14093/test.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7cc8f5dd43c..c174980ac98 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -457,6 +457,17 @@ jobs: OUTPUT=$(../../bin/phpstan analyze -l 0 -vvv test.php 2>&1) echo "$OUTPUT" ../bashunit -a contains 'Parallel processing scheduler' "$OUTPUT" + - script: | + cd e2e/bug-14093 + # Use a relative sys_temp_dir so the value starts with a letter (not /), just like Windows + # paths starting with a drive letter. If the sys_temp_dir value isn't correctly quoted, + # PHP evaluates ~ as a bitwise NOT rather than a literal character, truncating the path. + mkdir tmp~1 + php -d "sys_temp_dir='tmp~1'" ../../bin/phpstan analyze + if [ -d tmp ]; then + echo "FAIL: tmp was created by a worker, meaning sys_temp_dir='tmp~1' was incorrectly evaluated to 'tmp'" + exit 1 + fi steps: - name: Harden the runner (Audit all outbound calls) diff --git a/e2e/bug-14093/phpstan.neon b/e2e/bug-14093/phpstan.neon new file mode 100644 index 00000000000..14f9de4dceb --- /dev/null +++ b/e2e/bug-14093/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 8 + paths: + - test.php diff --git a/e2e/bug-14093/test.php b/e2e/bug-14093/test.php new file mode 100644 index 00000000000..29c13da5315 --- /dev/null +++ b/e2e/bug-14093/test.php @@ -0,0 +1,6 @@ +getOption('memory-limit') === null) { From dcc8f9a97f706037c0e4e29ddb4e11a260cf3d08 Mon Sep 17 00:00:00 2001 From: Sven Reichel Date: Wed, 15 Apr 2026 02:05:40 +0200 Subject: [PATCH 70/70] Update session_set_save_handler parameters --- resources/functionMap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/functionMap.php b/resources/functionMap.php index 7134875187a..71866bb604e 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -9284,7 +9284,7 @@ 'session_save_path' => ['string|false', 'newname='=>'string'], 'session_set_cookie_params' => ['bool', 'lifetime'=>'int', 'path='=>'string', 'domain='=>'string', 'secure='=>'bool', 'httponly='=>'bool'], 'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string,domain?:string,secure?:bool,httponly?:bool,samesite?:\'None\'|\'Lax\'|\'Strict\'|\'none\'|\'lax\'|\'strict\'}'], -'session_set_save_handler' => ['bool', 'open'=>'callable(string,string):bool', 'close'=>'callable():bool', 'read'=>'callable(string):string', 'write'=>'callable(string,string):bool', 'destroy'=>'callable(string):bool', 'gc'=>'callable(string):bool', 'create_sid='=>'callable():string', 'validate_sid='=>'callable(string):bool', 'update_timestamp='=>'callable(string):bool'], +'session_set_save_handler' => ['bool', 'open'=>'callable(string,string):bool', 'close'=>'callable():bool', 'read'=>'callable(string):string', 'write'=>'callable(string,string):bool', 'destroy'=>'callable(string):bool', 'gc'=>'callable(int):bool', 'create_sid='=>'callable():string', 'validate_sid='=>'callable(string):bool', 'update_timestamp='=>'callable(string):bool'], 'session_set_save_handler\'1' => ['bool', 'sessionhandler'=>'SessionHandlerInterface', 'register_shutdown='=>'bool'], 'session_start' => ['bool', 'options='=>'array'], 'session_status' => ['PHP_SESSION_NONE|PHP_SESSION_DISABLED|PHP_SESSION_ACTIVE'],