Skip to content

Commit 6ca28a1

Browse files
authored
Merge pull request #21 from LibreSign/fix/strict-quality-gates
Harden quality gates and finalize mutation coverage
2 parents f26ec41 + 82d5a76 commit 6ca28a1

21 files changed

Lines changed: 1053 additions & 524 deletions

.github/workflows/duplication.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ on:
1111
jobs:
1212
duplication:
1313
runs-on: ubuntu-latest
14-
continue-on-error: true
1514
steps:
1615
- uses: actions/checkout@v6
1716
- name: Detect minimum PHP from composer.json

.github/workflows/mutation.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ name: mutation
55

66
on:
77
pull_request:
8+
push:
9+
branches: [main]
810

911
jobs:
1012
mutation:
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-FileCopyrightText: 2026 LibreSign
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
name: test-integration
5+
6+
on:
7+
pull_request:
8+
push:
9+
branches: [main]
10+
11+
jobs:
12+
test-integration:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v6
16+
- name: Detect minimum PHP from composer.json
17+
id: php_min
18+
run: |
19+
php_version=$(grep -Po '"php"\s*:\s*"\K[^"]+' composer.json | grep -Eo '[0-9]+\.[0-9]+' | head -n1)
20+
[[ -n "$php_version" ]] || { echo "Could not determine minimum PHP version"; exit 1; }
21+
echo "version=$php_version" >> "$GITHUB_OUTPUT"
22+
- uses: shivammathur/setup-php@v2
23+
with:
24+
php-version: ${{ steps.php_min.outputs.version }}
25+
- run: composer install --no-interaction --prefer-dist
26+
- run: composer bin phpunit install --no-interaction --prefer-dist
27+
- run: composer run test:integration

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"psalm": "vendor-bin/psalm/vendor/vimeo/psalm/psalm --no-progress",
5858
"phpmd": "vendor-bin/phpmd/vendor/phpmd/phpmd/src/bin/phpmd src,tests text phpmd.xml",
5959
"psalm:update-baseline": "vendor-bin/psalm/vendor/vimeo/psalm/psalm --set-baseline=psalm-baseline.xml",
60-
"duplication:check": "vendor-bin/phpcpd/vendor/sebastian/phpcpd/phpcpd --min-lines=8 --min-tokens=70 --exclude vendor --exclude tests/Fixtures --exclude tests/Snapshots src tests",
60+
"duplication:check": "vendor-bin/phpcpd/vendor/sebastian/phpcpd/phpcpd --min-lines=6 --min-tokens=50 --exclude vendor --exclude tests/Fixtures --exclude tests/Snapshots src tests",
6161
"composer:validate": "composer validate --strict",
6262
"composer:normalize:check": "vendor-bin/qa/vendor/ergebnis/composer-normalize/bin/composer-normalize --dry-run",
6363
"bc:check": "vendor-bin/qa/vendor/roave/backward-compatibility-check/bin/roave-backward-compatibility-check --from=origin/main --to=HEAD",
@@ -69,6 +69,7 @@
6969
"sh:security": "shellcheck scripts/*.sh",
7070
"profile:xdebug": "XDEBUG_MODE=profile php benchmarks/profile.php",
7171
"test:unit": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite unit",
72+
"test:integration": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite integration",
7273
"test:coverage": "XDEBUG_MODE=coverage vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --coverage-text --coverage-clover=build/coverage/clover.xml",
7374
"mutation:test": "vendor-bin/mutation/vendor/infection/infection/bin/infection --threads=max",
7475
"performance:check": "php benchmarks/compiler-benchmark.php",

infection.json5

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"phpUnit": {
1313
"customPath": "vendor-bin/phpunit/vendor/bin/phpunit"
1414
},
15-
"minMsi": 75,
16-
"minCoveredMsi": 82,
15+
"minMsi": 100,
16+
"minCoveredMsi": 100,
1717
"testFramework": "phpunit",
1818
"timeout": 120
1919
}

phpunit.xml.dist

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
<phpunit bootstrap="vendor/autoload.php"
33
colors="true"
44
cacheDirectory="build/.phpunit-cache"
5-
beStrictAboutTestsThatDoNotTestAnything="true">
5+
beStrictAboutTestsThatDoNotTestAnything="true"
6+
failOnDeprecation="true"
7+
failOnIncomplete="true"
8+
failOnNotice="true"
9+
failOnRisky="true"
10+
failOnSkipped="true"
11+
failOnWarning="true">
612
<testsuites>
713
<testsuite name="unit">
814
<directory>tests/Unit</directory>

psalm.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0"?>
22
<!-- SPDX-FileCopyrightText: 2026 LibreSign -->
33
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
4-
<psalm errorLevel="3"
4+
<psalm errorLevel="1"
55
resolveFromConfigFile="true"
66
findUnusedBaselineEntry="true"
77
findUnusedCode="false">

src/Html/SubsetHtmlParser.php

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace LibreSign\XObjectTemplate\Html;
99

10+
use DOMAttr;
1011
use DOMDocument;
1112
use DOMElement;
1213
use DOMNode;
@@ -58,7 +59,7 @@ public function parse(string $html): array
5859
}
5960

6061
$nodes = [];
61-
foreach ($body->childNodes as $child) {
62+
foreach ($this->iterateChildNodes($body) as $child) {
6263
$parsed = $this->parseDomNode($child, '');
6364
if ($parsed !== null) {
6465
$nodes[] = $parsed;
@@ -92,7 +93,7 @@ private function parseElementNode(DOMElement $node, string $inheritedStyle): Nod
9293
}
9394

9495
$children = [];
95-
foreach ($node->childNodes as $childNode) {
96+
foreach ($this->iterateChildNodes($node) as $childNode) {
9697
$child = $this->parseDomNode($childNode, $effectiveStyle);
9798
if ($child !== null) {
9899
$children[] = $child;
@@ -128,16 +129,40 @@ private function parseTextNode(DOMNode $node, string $inheritedStyle): ?Node
128129
private function collectAttributes(DOMElement $node): array
129130
{
130131
$attributes = [];
131-
$nodeAttrs = $node->attributes;
132-
if ($nodeAttrs !== null) {
133-
foreach ($nodeAttrs as $attribute) {
134-
$attributes[$attribute->name] = trim($attribute->value);
135-
}
132+
foreach ($this->iterateAttributes($node) as $attribute) {
133+
$attributes[$attribute->name] = trim($attribute->value);
136134
}
137135

138136
return $attributes;
139137
}
140138

139+
/**
140+
* @return \Generator<int, DOMNode>
141+
*/
142+
private function iterateChildNodes(DOMNode $node): \Generator
143+
{
144+
/** @var DOMNode $child */
145+
foreach ($node->childNodes as $child) {
146+
yield $child;
147+
}
148+
}
149+
150+
/**
151+
* @return \Generator<int, DOMAttr>
152+
*/
153+
private function iterateAttributes(DOMElement $node): \Generator
154+
{
155+
$attributes = $node->attributes;
156+
if ($attributes === null) {
157+
return;
158+
}
159+
160+
/** @var DOMAttr $attribute */
161+
foreach ($attributes as $attribute) {
162+
yield $attribute;
163+
}
164+
}
165+
141166
private function mergeStyle(string $inheritedStyle, string $ownStyle): string
142167
{
143168
$inheritedStyle = $this->filterInheritableStyle($inheritedStyle);

src/Layout/StructuredLayoutRenderer.php

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,15 @@ private function renderBlockContainer(
274274
);
275275
}
276276

277-
$renderedHeight = $localClipBox === null
278-
? $this->boxResolver->resolveAutoContainerHeight($box['height'], $padding, $contentHeight)
279-
: $this->boxResolver->resolveFixedContainerHeight($box['height'], $padding, $contentHeight);
280-
281-
$this->appendDecoration($style, $box, $renderedHeight, $canvasHeight, $decorations);
282-
283-
return $renderedHeight;
277+
return $this->finalizeContainerRendering(
278+
$style,
279+
$box,
280+
$padding,
281+
$contentHeight,
282+
$localClipBox,
283+
$canvasHeight,
284+
$decorations,
285+
);
284286
}
285287

286288
/**
@@ -324,12 +326,15 @@ private function renderFlexContainer(
324326
);
325327

326328
if ($items === []) {
327-
$renderedHeight = $localClipBox === null
328-
? $this->boxResolver->resolveAutoContainerHeight($box['height'], $padding, 0.0)
329-
: $this->boxResolver->resolveFixedContainerHeight($box['height'], $padding, 0.0);
330-
$this->appendDecoration($style, $box, $renderedHeight, $canvasHeight, $decorations);
331-
332-
return $renderedHeight;
329+
return $this->finalizeContainerRendering(
330+
$style,
331+
$box,
332+
$padding,
333+
0.0,
334+
$localClipBox,
335+
$canvasHeight,
336+
$decorations,
337+
);
333338
}
334339

335340
$metrics = $this->flexPlanner->calculateMetrics($items, $direction, $justifyContent, $gap, $contentBox);
@@ -359,9 +364,37 @@ private function renderFlexContainer(
359364
}
360365

361366
$contentHeight = $direction === 'row' ? $metrics['crossAxisSize'] : $metrics['totalMainAxisSize'];
367+
368+
return $this->finalizeContainerRendering(
369+
$style,
370+
$box,
371+
$padding,
372+
$contentHeight,
373+
$localClipBox,
374+
$canvasHeight,
375+
$decorations,
376+
);
377+
}
378+
379+
/**
380+
* @param array{x: float, y: float, width: float, height: float} $box
381+
* @param array{top: float, right: float, bottom: float, left: float} $padding
382+
* @param list<LayoutDecoration> $decorations
383+
* @param array{x: float, y: float, width: float, height: float}|null $localClipBox
384+
*/
385+
private function finalizeContainerRendering(
386+
StyleMap $style,
387+
array $box,
388+
array $padding,
389+
float $contentHeight,
390+
?array $localClipBox,
391+
float $canvasHeight,
392+
array &$decorations,
393+
): float {
362394
$renderedHeight = $localClipBox === null
363395
? $this->boxResolver->resolveAutoContainerHeight($box['height'], $padding, $contentHeight)
364396
: $this->boxResolver->resolveFixedContainerHeight($box['height'], $padding, $contentHeight);
397+
365398
$this->appendDecoration($style, $box, $renderedHeight, $canvasHeight, $decorations);
366399

367400
return $renderedHeight;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Pdf;
9+
10+
use InvalidArgumentException;
11+
use LibreSign\XObjectTemplate\Dto\CompileResult;
12+
13+
/** @internal */
14+
class CompileResultResourceExtractor
15+
{
16+
/**
17+
* @return array<string, array<string, mixed>>
18+
*/
19+
public function extract(CompileResult $result, string $resourceType, string $itemMessageTemplate): array
20+
{
21+
if (!array_key_exists($resourceType, $result->resources)) {
22+
return [];
23+
}
24+
25+
/** @var mixed $rawResources */
26+
$rawResources = $result->resources[$resourceType];
27+
$resources = $this->requireStringKeyedArray(
28+
$rawResources,
29+
sprintf('%s resources must be an array.', $resourceType),
30+
);
31+
32+
$normalizedResources = [];
33+
foreach (array_keys($resources) as $alias) {
34+
$normalizedResources[$alias] = $this->requireStringKeyedArray(
35+
$resources[$alias],
36+
sprintf($itemMessageTemplate, $alias),
37+
);
38+
}
39+
40+
return $normalizedResources;
41+
}
42+
43+
/**
44+
* @return array<string, mixed>
45+
*/
46+
private function requireStringKeyedArray(mixed $value, string $message): array
47+
{
48+
if (!is_array($value)) {
49+
throw new InvalidArgumentException($message);
50+
}
51+
52+
foreach (array_keys($value) as $key) {
53+
if (!is_string($key)) {
54+
throw new InvalidArgumentException($message);
55+
}
56+
}
57+
58+
/** @var array<string, mixed> $value */
59+
return $value;
60+
}
61+
}

0 commit comments

Comments
 (0)