Skip to content

Add PHPStan static analysis at level 5#104

Open
pjcdawkins wants to merge 1 commit into3.xfrom
add-phpstan
Open

Add PHPStan static analysis at level 5#104
pjcdawkins wants to merge 1 commit into3.xfrom
add-phpstan

Conversation

@pjcdawkins
Copy link
Collaborator

@pjcdawkins pjcdawkins commented Mar 20, 2026

Summary

  • Add phpstan/phpstan ^2 at level 5 with a baseline of 11 errors (all ClientInterface::get/patch/post() shorthand methods not on the Guzzle interface)
  • Globally ignore new.static (deliberate polymorphism pattern across the resource model)
  • Add ECS and PHPStan steps to GitHub Actions CI workflow
  • Add Makefile with lint-ecs, lint-phpstan, and test targets

PHPStan fixes

  • Project::systemInformation() return type missing false case
  • RestoreOptions: remove redundant @return PHPDocs conflicting with native static return type
  • LogItem: static:: to self:: for private method call
  • Tree::getBlob(): reorder to guard clauses first, remove dead code
  • Resolver: simplify always-true explode() assignment in condition
  • ResourceWithReferences: remove always-true getResponse() check (BadResponseException always has a response)
  • Result::getEntity(): remove redundant isset() on initialized property
  • Subscription::create(): new self() to new static() to match return type

ConnectorInterface

  • Add getConfig() and getAccessToken() to ConnectorInterface
  • Guard deprecated getAccountsEndpoint() calls behind instanceof Connector

ECS

  • Fix 3 violations: missing declare(strict_types=1), brace style, superfluous @return

Test plan

  • make lint passes (ECS + PHPStan)
  • make test passes (PHPUnit)
  • CI workflow runs all three

🤖 Generated with Claude Code

@pjcdawkins pjcdawkins force-pushed the add-phpstan branch 2 times, most recently from d4486eb to f1e61a7 Compare March 20, 2026 17:43
Add phpstan/phpstan ^2 at level 5 with a baseline of 13 errors (all
interface method gaps on ClientInterface and ConnectorInterface).
Globally ignore new.static (deliberate polymorphism pattern).

Add PHPStan step to GitHub Actions CI and a Makefile with lint/test
targets.

Fixes found by PHPStan:
- Project::systemInformation() return type missing false case
- RestoreOptions: redundant @return PHPDocs conflicting with native
  static return type
- LogItem: static:: to self:: for private method
- Tree::getBlob(): always-true instanceof and unreachable code
- Resolver: always-true explode() assignment in condition
- ResourceWithReferences: always-true getResponse() check
- Result::getEntity(): redundant isset() on initialized property
- Subscription::create(): new self() vs new static() return mismatch
- EnvironmentTest: incorrect @var annotation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces PHPStan static analysis (level 5) with a baseline, integrates ECS + PHPStan into CI, and includes several small code/doc fixes to satisfy static analysis and style checks across the client and model layers.

Changes:

  • Add PHPStan (phpstan/phpstan ^2) configuration + baseline, plus CI steps and Makefile targets for linting/testing.
  • Update ConnectorInterface and adjust PlatformClient to avoid deprecated getAccountsEndpoint() usage unless using the concrete Connector.
  • Apply a set of targeted code/doc tweaks across models to satisfy PHPStan/ECS (return types, dead code removal, doc conflicts).

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Model/EnvironmentTest.php Removes a now-unneeded/undesired local array-shape annotation for test cases.
src/PlatformClient.php Guards deprecated accounts-endpoint usage behind instanceof Connector; adjusts API URL fallback logic.
src/Model/Subscription.php Uses new static(...) to match static return type for polymorphic construction.
src/Model/Settings.php Adds strict_types and expands empty class body to satisfy ECS style.
src/Model/Result.php Removes redundant isset() check on an initialized typed property.
src/Model/ResourceWithReferences.php Simplifies BadResponseException handling to always use the response status code.
src/Model/Ref/Resolver.php Simplifies explode() handling to satisfy static analysis.
src/Model/Project.php Updates systemInformation() return type to include false.
src/Model/Git/Tree.php Simplifies getBlob() control flow and removes dead code.
src/Model/Backups/RestoreOptions.php Removes redundant PHPDocs that conflict with native static return types.
src/Model/AutoscalingSettings.php Adds strict_types and expands empty class body to satisfy ECS style.
src/Model/ActivityLog/LogItem.php Switches private decode calls to self:: and updates return PHPDocs.
src/Connection/ConnectorInterface.php Adds getConfig() and getAccessToken() to the connector contract.
phpstan.neon Adds PHPStan configuration (level 5), baseline include, and ignore for new.static.
phpstan-baseline.neon Adds baseline ignores for known Guzzle ClientInterface shorthand-method findings.
composer.json Adds phpstan/phpstan as a dev dependency.
composer.lock Locks PHPStan dependency.
Makefile Adds lint-ecs, lint-phpstan, and test targets.
.github/workflows/ci.yml Runs ECS + PHPStan in CI before PHPUnit.
Comments suppressed due to low confidence (2)

phpstan.neon:13

  • phpstan.neon uses tab indentation, but .editorconfig sets indent_style=space for all files. This makes the file inconsistent with repository formatting rules and can cause noisy diffs/auto-formatting issues. Consider converting indentation to spaces in this file (and keeping it consistent with phpstan-baseline.neon too).
includes:
	- phpstan-baseline.neon

parameters:
	level: 5
	paths:
		- src
		- tests
	ignoreErrors:
		# Deliberate pattern: resource classes use new static() for polymorphism.
		-
			identifier: new.static

src/Model/ActivityLog/LogItem.php:57

  • LogItem::multipleFromJsonStream() still constructs items with new static(...) (via static::singleFromJson()), but the updated PHPDoc says @return self[]. This is inconsistent with multipleFromJsonStreamWithSeal() (which documents static[]) and can mislead static analysis/consumers if LogItem is ever subclassed. Consider using static[] here (or switching construction to new self(...) if the intent is to always return LogItem).
    /**
     * @return self[]
     *@deprecated use LogItem::multipleFromJsonStreamWithSeal() instead
     */
    public static function multipleFromJsonStream(string $str): array
    {
        $items = [];
        foreach (explode("\n", trim($str, "\n")) as $line) {
            if ($line === '') {
                continue;
            }
            $item = static::singleFromJson($line);
            if ($item !== false) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +65
/**
* Get the connector configuration.
*/
public function getConfig(): array;

/**
* Returns the access token saved in the session, if any.
*/
public function getAccessToken(): ?string;

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

Adding new required methods to ConnectorInterface (getConfig(), getAccessToken()) is a backwards-incompatible change for any downstream code that implements this interface. If maintaining BC is important, consider introducing a new extended interface (e.g., ConfigAwareConnectorInterface) or keeping these methods off the interface and using instanceof/method_exists checks where needed.

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +61
ignoreErrors:
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Activity.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Address.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 2
path: src/Model/Organization/Organization.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Organization.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Profile.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Ref/Resolver.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/SetupOptions.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Team/Team.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/UserAccess/ProjectUserAccess.php

-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:post\(\)\.$#'
identifier: method.notFound
count: 1
path: src/PlatformClient.php
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

phpstan-baseline.neon uses tab indentation, but .editorconfig sets indent_style=space for all files. To keep formatting consistent (and avoid future whitespace churn), consider regenerating or reformatting the baseline using spaces.

Suggested change
ignoreErrors:
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Activity.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Address.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 2
path: src/Model/Organization/Organization.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Organization.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Profile.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Ref/Resolver.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/SetupOptions.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Team/Team.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/UserAccess/ProjectUserAccess.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:post\(\)\.$#'
identifier: method.notFound
count: 1
path: src/PlatformClient.php
ignoreErrors:
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Activity.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Address.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 2
path: src/Model/Organization/Organization.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Organization.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Organization/Profile.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Ref/Resolver.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:get\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/SetupOptions.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/Team/Team.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:patch\(\)\.$#'
identifier: method.notFound
count: 1
path: src/Model/UserAccess/ProjectUserAccess.php
-
message: '#^Call to an undefined method GuzzleHttp\\ClientInterface\:\:post\(\)\.$#'
identifier: method.notFound
count: 1
path: src/PlatformClient.php

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants