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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ tests/ export-ignore
phpdoc.dist.xml export-ignore
phpunit.xml.dist export-ignore
phpunit10.xml.dist export-ignore
phpstan-bootstrap.php export-ignore
phpstan.neon.dist export-ignore

#
# Auto detect text files and perform LF normalization
Expand Down
16 changes: 16 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ This project uses [PHP_CodeSniffer][] to detect coding standard violations and a

[PHP_CodeSniffer]: https://github.com/PHPCSStandards/PHP_CodeSniffer

## Static Analysis

This project uses [PHPStan][] for static analysis. The configuration lives in `phpstan.neon.dist`; findings which are known false positives or deliberate are listed under `ignoreErrors` in that file, each with an explanation.

PHPStan requires PHP 7.4 or higher, while this library supports PHP 5.6 and higher, so it is not installed via Composer.
To run it locally, download the PHAR file and run it from the root of the repository:

```sh
curl -sSLo phpstan.phar https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar
php phpstan.phar analyse
```

A `phpstan.neon` file can be used for local overrides; it is ignored by Git.

[PHPStan]: https://phpstan.org/

## Unit Tests

PRs should include unit tests for all changes.
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/cs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,36 @@ jobs:
- name: Show PHPCS results in PR
if: ${{ always() && steps.phpcs.outcome == 'failure' }}
run: cs2pr ./phpcs-report.xml

phpstan: #----------------------------------------------------------------------
name: 'PHPStan'
runs-on: ubuntu-latest
permissions:
contents: read # Needed to clone the repo.

steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: 'latest'
coverage: none
# PHPStan needs PHP 7.4+, so it is installed as a tool rather than
# as a Composer dev dependency of this PHP 5.6+ package.
tools: phpstan

# Install dependencies and handle caching in one go.
# @link https://github.com/marketplace/actions/install-php-dependencies-with-composer
- name: Install Composer dependencies
uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # 4.0.0
with:
# Bust the cache at least once a month - output format: YYYY-MM.
custom-cache-suffix: $(date -u "+%Y-%m")

# Run static analysis. The github error format annotates findings inline in PRs.
- name: Run PHPStan
run: phpstan analyse --error-format=github
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ phpcs.xml
phpunit.xml
phpunit10.xml

# Ignore local overrides of the PHPStan config file.
phpstan.neon

# Ignore temporary files for ghpages builds.
phpdoc.xml
build/ghpages/.phpdoc
Expand Down
2 changes: 0 additions & 2 deletions library/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@

/**
* Constant to silence deprecation notices about use of the old PSR-0 based class names.
*
* @var bool
*/
define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true);
}
Expand Down
14 changes: 14 additions & 0 deletions phpstan-bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
/**
* Bootstrap file for PHPStan.
*
* The Composer autoloader registers the autoloader for the deprecated PSR-0 `Requests_*` class
* names (see library/Deprecated.php). When PHPStan reflects on those class names, which the
* autoloader tests reference, that autoloader triggers an E_USER_DEPRECATED notice, which PHPStan
* reports as an internal error. The constant below is the documented way to silence the notice and
* only affects the analysis run.
*/

if (defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS') === false) {
define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true);
}
164 changes: 164 additions & 0 deletions phpstan.neon.dist

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This file needs to be listed in the .gitattributes export-ignores.
Along the same lines, phpstan.neon (local override) needs to be added to .gitignore.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added phpstan.neon.dist (and the new phpstan-bootstrap.php) to the export-ignore list, and phpstan.neon to .gitignore.

Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
parameters:
level: 5
bootstrapFiles:
- phpstan-bootstrap.php
- tests/bootstrap.php
paths:
- build/ghpages
- examples
- library
- src
- tests
excludePaths:
analyse:
- build/ghpages/vendor (?)
# Externally maintained file (based on the SimplePie IRI class), not held to this project's standards.
- src/Iri.php
treatPhpDocTypesAsCertain: false

ignoreErrors:
# The library supports PHP 5.6+, where cURL handles are resources. PHPStan analyses against the
# PHP 8 stubs, in which curl_close() only accepts a CurlHandle object and the resource type can
# never occur.
-
identifier: argument.type
message: '#function curl_close expects CurlHandle, resource given#'
path: src/Transport/Curl.php
-
identifier: argument.type
message: '#function curl_close expects CurlHandle, resource given#'
path: tests/Utility/InputValidator/IsCurlHandleTest.php
-
identifier: property.unusedType
message: '#is never assigned resource#'
path: src/Transport/Curl.php
-
identifier: property.unusedType
message: '#is never assigned resource#'
path: tests/Utility/InputValidator/IsCurlHandleTest.php

# OPENSSL_TLSEXT_SERVER_NAME is only defined when OpenSSL is compiled with SNI support; the guard
# is needed on older PHP builds even though it is always true on the PHP version running PHPStan.
-
identifier: booleanAnd.rightAlwaysTrue
path: src/Transport/Fsockopen.php

# PHPStan's stub for spl_autoload_register() declares the callback as returning void. PHP ignores
# the return value, and the Requests autoloaders return bool to report whether a class was loaded.
-
identifier: argument.type
message: '#function spl_autoload_register expects#'
path: src/Autoload.php
-
identifier: argument.type
message: '#function spl_autoload_register expects#'
path: tests/bootstrap.php

# REQUESTS_SILENCE_PSR0_DEPRECATIONS is defined by the integrator before the library loads. PHPStan
# resolves it from the define() call in library/Requests.php and treats it as always true.
-
identifier: notIdentical.alwaysFalse
message: '#REQUESTS_SILENCE_PSR0_DEPRECATIONS|between true and true#'
path: src/Autoload.php
-
identifier: notIdentical.alwaysFalse
message: '#between true and true#'
path: library/Requests.php

# The `cookies` option accepts false per the documentation, but set_defaults() replaces false with an
# empty Jar via the empty() check above, so the guard can never see false. Kept as-is pending a
# decision on whether `cookies => false` should disable cookie handling.
-
identifier: notIdentical.alwaysTrue
path: src/Requests.php

# The REQUESTS_TEST_SERVER_*_AVAILABLE constants are defined by tests/bootstrap.php from the environment;
# PHPStan sees the values the bootstrap produced on this machine and treats the checks as constant.
-
identifier: booleanAnd.alwaysFalse
path: tests/TestCase.php
-
identifier: identical.alwaysFalse
path: tests/TestCase.php
-
identifier: booleanAnd.rightAlwaysFalse
path: tests/Proxy/Http/HttpTest.php
-
identifier: booleanNot.alwaysTrue
path: tests/Proxy/Http/HttpTest.php

# PHPUnit version shim: setMethods() exists on PHPUnit < 10 and addMethods() on PHPUnit >= 8.
# PHPStan only sees the installed PHPUnit version.
-
identifier: function.alreadyNarrowedType
path: tests/TestCase.php
-
identifier: method.notFound
message: '#setMethods\(\)#'
path: tests/TestCase.php

# Tests which deliberately pass invalid input to verify the exception thrown or the resulting behaviour.
-
identifier: argument.type
path: tests/Cookie/ParseTest.php
-
identifier: argument.type
path: tests/Hooks/RegisterTest.php
-
identifier: argument.type
path: tests/Proxy/Http/HttpTest.php
-
identifier: argument.type
path: tests/Utility/FilteredIterator/SerializationTest.php
-
identifier: array.invalidKey
path: tests/Utility/CaseInsensitiveDictionary/*
-
identifier: offsetAssign.dimType
path: tests/*
-
identifier: offsetAssign.valueType
path: tests/Response/Headers/*
-
identifier: assign.propertyType
message: '#Iri::\$host#'
path: tests/Iri/IriTest.php

# Tests of magic property access (__get/__set/__isset) on properties which intentionally do not exist.
-
identifier: property.notFound
path: tests/Iri/IriTest.php
-
identifier: property.notFound
path: tests/Session/MagicPropertyAccessTest.php

# Session exposes request options as magic properties through __get()/__set().
-
identifier: property.notFound
message: '#Session::\$useragent#'
path: examples/session.php

# The example uses a placeholder path the reader is expected to replace.
-
identifier: requireOnce.fileNotFound
path: examples/preload-aliases.php

# Assertions on values PHPStan can prove at analysis time. They document the test environment
# (extension availability, constant definitions) rather than exercise logic.
-
identifier: method.alreadyNarrowedType
path: tests/*

# Tests disabled at the top with markTestSkipped() while their body is kept for reference
# (see issues #966 and #1077).
-
identifier: deadCode.unreachable
path: tests/Transport/BaseTestCase.php

# Minimal ArrayAccess fixture used only to satisfy type checks; it is never read from.
-
identifier: property.onlyWritten
path: tests/Fixtures/ArrayAccessibleObject.php
-
identifier: return.missing
path: tests/Fixtures/ArrayAccessibleObject.php
2 changes: 2 additions & 0 deletions src/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
* Cookie storage object
*
* @package Requests\Cookies
*
* @phpstan-consistent-constructor
*/
class Cookie {
/**
Expand Down
2 changes: 1 addition & 1 deletion src/Exception/Http/StatusUnknown.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ final class StatusUnknown extends Http {
/**
* HTTP status code
*
* @var int|bool Code if available, false if an error occurred
* @var int Code if available, 0 if an error occurred
*/
protected $code = 0;

Expand Down
4 changes: 3 additions & 1 deletion src/IdnaEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ public static function to_ascii($text) {
/**
* Check whether a given text string contains only ASCII characters
*
* @internal (Testing found regex was the fastest implementation)
* @internal
*
* Testing found regex was the fastest implementation.
*
* @param string $text Text to examine.
* @return bool Is the text string ASCII-only?
Expand Down
1 change: 1 addition & 0 deletions src/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,7 @@ protected static function parse_response($headers, $url, $req_headers, $req_data
* `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object
*
* @param string $response Full response text including headers and body (will be overwritten with Response instance)
* @param-out \WpOrg\Requests\Response|\WpOrg\Requests\Exception $response
* @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()}
* @return void
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class Response {
*
* @var \WpOrg\Requests\Response\Headers Array-like object representing headers
*/
public $headers = [];
public $headers;

/**
* Status code, false if non-blocking
Expand Down Expand Up @@ -91,7 +91,7 @@ class Response {
*
* @var \WpOrg\Requests\Cookie\Jar Array-like object representing a cookie jar
*/
public $cookies = [];
public $cookies;

/**
* Constructor
Expand Down
12 changes: 6 additions & 6 deletions src/Transport/Curl.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public function __construct() {
$this->handle = curl_init();

curl_setopt($this->handle, CURLOPT_HEADER, false);
curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, true);
if ($this->version >= self::CURL_7_10_5) {
curl_setopt($this->handle, CURLOPT_ENCODING, '');
}
Expand Down Expand Up @@ -200,7 +200,7 @@ public function request($url, $headers = [], $data = [], $options = []) {
if (isset($options['verify'])) {
if ($options['verify'] === false) {
curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, false);
} elseif (is_string($options['verify'])) {
curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']);
}
Expand Down Expand Up @@ -451,17 +451,17 @@ private function setup_handle($url, $headers, $data, $options) {
$timeout = max($options['timeout'], 1);

if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
curl_setopt($this->handle, CURLOPT_TIMEOUT, (int) ceil($timeout));
} else {
// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound
curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, (int) round($timeout * 1000));
}

if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, (int) ceil($options['connect_timeout']));
} else {
// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound
curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, (int) round($options['connect_timeout'] * 1000));
}

curl_setopt($this->handle, CURLOPT_URL, $url);
Expand Down
2 changes: 1 addition & 1 deletion src/Utility/CaseInsensitiveDictionary.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function offsetGet($offset) {
* Set the given item
*
* @param string $offset Item name
* @param string $value Item value
* @param mixed $value Item value
*
* @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`)
*/
Expand Down
2 changes: 1 addition & 1 deletion tests/Exception/Http/StatusCodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public static function dataUnknownStatusCodes() {
*
* @dataProvider dataKnownStatusCodes
*
* @param int status_code HTTP status code.
* @param int $status_code HTTP status code.
* @param string $expected_exception_class Exception class to expect.
*
* @return void
Expand Down
Loading
Loading