Skip to content

Commit 79f22d4

Browse files
Merge pull request #6 from mralaminahamed/chore/working-toolchain-and-docs
Make the toolchain work, and the docs match it
2 parents 93a24ec + 7c29115 commit 79f22d4

8 files changed

Lines changed: 337 additions & 8 deletions

File tree

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: ci-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
check:
17+
runs-on: ubuntu-latest
18+
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
# The floor this package declares, and current stable. Consumers sit on both.
23+
php: ['7.4', '8.3']
24+
25+
steps:
26+
- uses: actions/checkout@v5
27+
28+
- uses: shivammathur/setup-php@v2
29+
with:
30+
php-version: ${{ matrix.php }}
31+
coverage: none
32+
33+
- uses: ramsey/composer-install@v3
34+
35+
- run: composer cs
36+
37+
- run: composer analyze
38+
39+
- run: composer test
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Upstream check
2+
3+
# The failure a stubs package actually has is staleness: the plugin ships a new release, this package
4+
# does not, and nobody finds out until a consumer's static analysis reports a symbol that should
5+
# exist. Nothing here looks broken in the meantime, which is why it needs a schedule rather than a
6+
# reviewer.
7+
8+
on:
9+
schedule:
10+
# Mondays, 06:00 UTC.
11+
- cron: '0 6 * * 1'
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: read
16+
issues: write
17+
18+
jobs:
19+
compare:
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- uses: actions/checkout@v5
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Compare the latest release against the latest tag
28+
id: compare
29+
run: |
30+
set -euo pipefail
31+
32+
latest="$(
33+
curl -fsSL "https://api.wordpress.org/plugins/info/1.0/ninja-forms.json" \
34+
| php -r 'echo json_decode(file_get_contents("php://stdin"), true)["version"] ?? "";'
35+
)"
36+
37+
tagged="$(git tag --sort=-v:refname | head -n1 | sed 's/^v//')"
38+
39+
# Validated before either value goes near a later step. One comes from a public API and the
40+
# other from a tag, and a version string is the only shape that should reach an issue body.
41+
for v in "$latest" "$tagged"; do
42+
if ! printf '%s' "$v" | grep -qE '^[0-9]+(\.[0-9]+)*$'; then
43+
echo "ERROR: refusing to act on a version that is not numeric: '$v'" >&2
44+
exit 1
45+
fi
46+
done
47+
48+
echo "latest=$latest" >> "$GITHUB_OUTPUT"
49+
echo "tagged=$tagged" >> "$GITHUB_OUTPUT"
50+
echo "stale=$( [ "$latest" = "$tagged" ] && echo false || echo true )" >> "$GITHUB_OUTPUT"
51+
52+
echo "tagged $tagged, latest $latest"
53+
54+
- name: Open an issue when a newer release exists
55+
if: steps.compare.outputs.stale == 'true'
56+
env:
57+
GH_TOKEN: ${{ github.token }}
58+
LATEST: ${{ steps.compare.outputs.latest }}
59+
TAGGED: ${{ steps.compare.outputs.tagged }}
60+
run: |
61+
set -euo pipefail
62+
63+
title="ninja-forms ${LATEST} is out (stubs tagged ${TAGGED})"
64+
65+
# Reuse an open issue rather than filing one every Monday until somebody acts.
66+
if gh issue list --state open --search "$title" --json title --jq '.[].title' | grep -Fxq "$title"; then
67+
echo "already reported"
68+
exit 0
69+
fi
70+
71+
gh issue create \
72+
--title "$title" \
73+
--body "$(printf '%s\n' \
74+
"WordPress.org lists **${LATEST}**; the newest tag here is **${TAGGED}**." \
75+
"" \
76+
"Regenerate and release:" \
77+
"" \
78+
'```bash' \
79+
"composer release" \
80+
'```' \
81+
)"

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@
44
/.idea/
55
/.vscode/
66
.DS_Store
7+
8+
# PHPUnit
9+
.phpunit.result.cache
10+
coverage/

CHANGELOG.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Changelog
2+
3+
All notable changes to this package are documented here.
4+
5+
**Versions track the upstream plugin.** A release of this package stubs the matching upstream
6+
release, so you can require the same version you run against. That is also why the entries below are
7+
mostly "regenerated against X" — the stubs have no behaviour of their own to change.
8+
9+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
10+
11+
## [Unreleased]
12+
13+
### Added
14+
15+
- `tests/StubsTest.php` and `phpunit.xml.dist`, so `composer test` runs. It asserts every generated
16+
stub file parses and actually declares something — the two ways a regeneration fails silently.
17+
- `phpcs.xml.dist`, so `composer cs` and `composer cs-fix` have a ruleset. Generated `.stub` files
18+
and `source/` are excluded; only the PHP written by hand here is checked.
19+
- CI on push and pull request across PHP 7.4 and 8.3, running `cs`, `analyze` and `test`.
20+
- This changelog.
21+
22+
### Fixed
23+
24+
- `composer test`, `composer cs` and `composer check` all failed: `phpunit` had no configuration and
25+
`phpcs` had no ruleset.
26+
- `composer analyze` reported errors that are inherent to stubs. The generated files were in
27+
PHPStan's `paths`, which asks it to check the bodies of declarations that are empty by definition.
28+
They are `scanFiles` now — the symbols become known without any claim about bodies a stub cannot
29+
have — and `paths` covers the hand-written PHP instead.
30+
31+
## [3.15.0] - 2026-08-16
32+
33+
Regenerated against upstream 3.15.0.
34+
35+
## [3.14.12] - 2026-08-16
36+
37+
Regenerated against upstream 3.14.12.
38+
39+
## [3.14.11] - 2026-08-16
40+
41+
Regenerated against upstream 3.14.11.
42+
43+
## [3.14.10] - 2026-08-16
44+
45+
Regenerated against upstream 3.14.10.
46+
47+
## [3.14.9] - 2026-08-16
48+
49+
Regenerated against upstream 3.14.9.
50+
51+
## [3.14.8] - 2026-08-16
52+
53+
Regenerated against upstream 3.14.8.
54+
55+
## [3.14.7] - 2026-08-16
56+
57+
Regenerated against upstream 3.14.7.
58+
59+
## [3.14.6] - 2026-08-16
60+
61+
Regenerated against upstream 3.14.6.
62+
63+
## [3.14.5] - 2026-05-27
64+
65+
Regenerated against upstream 3.14.5.
66+
67+
## [3.14.4] - 2026-05-27
68+
69+
Regenerated against upstream 3.14.4.
70+
71+
## [3.14.3] - 2026-05-27
72+
73+
Regenerated against upstream 3.14.3.
74+
75+
## [3.14.2] - 2026-05-27
76+
77+
Regenerated against upstream 3.14.2.

phpcs.xml.dist

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0"?>
2+
<ruleset name="ninja-forms-stubs">
3+
<description>Coding standards for the files this repository writes by hand.</description>
4+
5+
<file>bin</file>
6+
<file>configs</file>
7+
<file>tests</file>
8+
9+
<!--
10+
The .stub files are generated by php-stubs/generator and rewritten wholesale by
11+
`composer generate`, so linting them would report on output nobody edits. source/ is the
12+
upstream copy, and vendor/ is not ours either.
13+
-->
14+
<exclude-pattern>*/source/*</exclude-pattern>
15+
<exclude-pattern>*/vendor/*</exclude-pattern>
16+
<exclude-pattern>*.stub</exclude-pattern>
17+
18+
<arg name="extensions" value="php"/>
19+
<arg name="colors"/>
20+
<arg value="ps"/>
21+
22+
<rule ref="PSR12"/>
23+
</ruleset>

phpstan.neon

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
parameters:
2+
level: 5
3+
4+
# The hand-written PHP, which is all this repository actually authors.
25
paths:
3-
- ninja-forms-stubs.stub
6+
- configs
7+
- tests
8+
9+
# The stubs are SCANNED, not analysed.
10+
#
11+
# They used to sit in `paths`, which asks PHPStan to check the bodies of declarations that are
12+
# empty by definition -- "should return string but return statement is missing" for every one,
13+
# plus unknown-class errors for whatever WordPress internals upstream touches. None of it is a
14+
# defect in this package, and PHPStan 2.x classifies several of those as non-ignorable, so
15+
# neither an ignoreErrors pattern nor a baseline can hold them.
16+
#
17+
# Scanning makes the symbols known without asserting anything about their bodies, which is the
18+
# only claim a generated stub can honour. What the stubs are checked FOR -- that they parse, and
19+
# that they declare something -- is tests/StubsTest.php.
420
scanFiles:
521
- ninja-forms-constants-stubs.stub
22+
- ninja-forms-stubs.stub
623
bootstrapFiles:
724
- configs/bootstrap.php
8-
level: 5
9-
ignoreErrors:
10-
- '#but return statement is missing\.$#'
11-
- '#has an unused parameter#'
12-
- '#^(Property|Static property|Method|Static method) \S+ is unused\.$#'
13-
- '#is never read, only written\.$#'
14-
- '#has invalid (return )?type (WP_Error|WP_Customize_Manager|WP_Theme|WP_User|WP_Site|WP_Upgrader)#'

phpunit.xml.dist

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
4+
bootstrap="vendor/autoload.php"
5+
colors="true"
6+
failOnWarning="true"
7+
failOnRisky="true">
8+
<testsuites>
9+
<testsuite name="stubs">
10+
<directory suffix="Test.php">tests</directory>
11+
</testsuite>
12+
</testsuites>
13+
</phpunit>

tests/StubsTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NinjaFormsStubs\Tests;
6+
7+
use PHPUnit\Framework\TestCase;
8+
9+
/**
10+
* Smoke checks over the generated stubs.
11+
*
12+
* A stubs package fails quietly: a regeneration truncates the output, or produces a file that no
13+
* longer parses, and nothing here looks wrong until a consumer's static analysis reports symbols
14+
* that should exist. These assert the two properties a generated file must hold whatever it
15+
* contains -- it parses, and it actually declares something.
16+
*/
17+
final class StubsTest extends TestCase
18+
{
19+
private const ROOT = __DIR__ . '/..';
20+
21+
/**
22+
* Every generated stub file in the repository root.
23+
*
24+
* Discovered rather than listed, so a repository that grows a second stub file gets it covered
25+
* without anyone remembering to add it here.
26+
*
27+
* @return array<string, array{0: string}>
28+
*/
29+
public function stubFileProvider(): array
30+
{
31+
$cases = [];
32+
33+
foreach ((array) glob(self::ROOT . '/*.stub') as $path) {
34+
$cases[basename((string) $path)] = [basename((string) $path)];
35+
}
36+
37+
return $cases;
38+
}
39+
40+
public function testTheRepositoryShipsAtLeastOneStubFile(): void
41+
{
42+
$this->assertNotEmpty($this->stubFileProvider(), 'no .stub files found -- generation produced nothing');
43+
}
44+
45+
/**
46+
* @dataProvider stubFileProvider
47+
*/
48+
public function testTheStubFileIsValidPhp(string $file): void
49+
{
50+
$path = self::ROOT . '/' . $file;
51+
52+
$this->assertFileExists($path);
53+
54+
// TOKEN_PARSE makes the tokenizer raise ParseError on invalid source rather than returning a
55+
// best-effort token list, which is what makes this an actual syntax check.
56+
$this->assertNotEmpty(token_get_all((string) file_get_contents($path), TOKEN_PARSE));
57+
}
58+
59+
/**
60+
* @dataProvider stubFileProvider
61+
*/
62+
public function testTheStubFileDeclaresSomething(string $file): void
63+
{
64+
if (strpos($file, 'constants') !== false) {
65+
$this->markTestSkipped('a constants stub is legitimately empty when the plugin defines none');
66+
}
67+
68+
$source = (string) file_get_contents(self::ROOT . '/' . $file);
69+
70+
// A generated file that parses but contains only its header is the shape a truncated or
71+
// failed generation takes -- valid PHP, and useless.
72+
foreach (['function ', 'class ', 'interface ', 'trait ', 'define(', 'const '] as $needle) {
73+
if (stripos($source, $needle) !== false) {
74+
$this->addToAssertionCount(1);
75+
76+
return;
77+
}
78+
}
79+
80+
$this->fail($file . ' parses but declares nothing -- a truncated or empty generation');
81+
}
82+
}

0 commit comments

Comments
 (0)