• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wol-soft / php-json-schema-model-generator / 28856540538

07 Jul 2026 09:31AM UTC coverage: 98.076%. First build
28856540538

Pull #157

github

wol-soft
Warn and skip on unevaluatedProperties dead cells + edge-case tests

UnevaluatedPropertiesValidatorFactory now consolidates all four
sibling shapes that leave the unevaluatedProperties bucket
permanently empty into a single deadCodeReason() helper:

- additionalProperties: true — every extra flows to the model but
  the accumulator does not credit it, so the validator would still
  fire and defeat the intent of `additionalProperties: true`.
- additionalProperties: {schema} — every extra is claimed and
  validated by additionalProperties; the unevaluated set is empty.
- additionalProperties: false — every extra is rejected before the
  post-composition phase, so the unevaluated validator never runs.
- denyAdditionalProperties() generator flag with additionalProperties
  absent — synthesises the same false shape at configuration time.

Each cell emits a distinct warning via the existing echo channel
so build-output greps can identify the specific dead shape, and
skips validator emission entirely.

Tests added:

- Object-side dead-code data provider covering all four shapes plus
  the denyAdditionalProperties() variant. Rows pass the
  GeneratorConfiguration directly (setOutputEnabled(true) on the
  base, chained with setDenyAdditionalProperties(true) on the deny
  row) so the test signature drops the closure/fallback boilerplate.
- testAdditionalFalseRejectsExtrasEvenWhenUnevaluatedSchemaWouldAccept
  proves the factory suppressed the validator: an extra that would
  satisfy the unevaluated integer schema is still rejected because
  additionalProperties: false runs first.
- testContradictoryInnerSchemaThrowsSchemaExceptionPointingAtFile
  pins that an unevaluatedProperties schema with contradictory allOf
  types surfaces via the existing "conflicting types" SchemaException
  path, with the file identifier preserved.
- testPropertyNamesRejectionPrecedesUnevaluated data-provider variant
  covering both direct-exception and error-collection modes. The
  re... (continued)
Pull Request #157: Unevaluated properties

712 of 774 new or added lines in 38 files covered. (91.99%)

7237 of 7379 relevant lines covered (98.08%)

563.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.96
/src/Model/Property/CompositionPropertyDecorator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\Model\Property;
6

7
use PHPModelGenerator\Exception\SchemaException;
8
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
9
use PHPModelGenerator\Model\SchemaDefinition\ResolvedDefinitionsCollection;
10
use PHPModelGenerator\Utils\RenderHelper;
11

12
/**
13
 * Class CompositionPropertyDecorator
14
 *
15
 * @package PHPModelGenerator\Model\Property
16
 */
17
class CompositionPropertyDecorator extends PropertyProxy
18
{
19
    private const string PROPERTY_KEY = 'composition';
20

21
    /**
22
     * Store all properties from nested schemas of the composed property validator. If the composition validator fails
23
     * all affected properties must be set to null to adopt only valid values in the base model.
24
     *
25
     * @var PropertyInterface[]
26
     */
27
    protected $affectedObjectProperties = [];
28

29
    private bool $alwaysTrueBranch = false;
30

31
    /**
32
     * CompositionPropertyDecorator constructor.
33
     *
34
     * @throws SchemaException
35
     */
36
    public function __construct(string $propertyName, JsonSchema $jsonSchema, PropertyInterface $property)
943✔
37
    {
38
        parent::__construct(
943✔
39
            $propertyName,
943✔
40
            $jsonSchema,
943✔
41
            new ResolvedDefinitionsCollection([self::PROPERTY_KEY => $property]),
943✔
42
            self::PROPERTY_KEY,
943✔
43
        );
943✔
44

45
        $property->onResolve(function (): void {
943✔
46
            $this->resolve();
940✔
47
        });
943✔
48
    }
49

50
    /**
51
     * Append an object property which is affected by the composition validator
52
     */
53
    public function appendAffectedObjectProperty(PropertyInterface $property): void
435✔
54
    {
55
        $this->affectedObjectProperties[] = $property;
435✔
56
    }
57

58
    /**
59
     * @return PropertyInterface[]
60
     */
61
    public function getAffectedObjectProperties(): array
58✔
62
    {
63
        return $this->affectedObjectProperties;
58✔
64
    }
65

66
    public function markAsAlwaysTrueBranch(): void
13✔
67
    {
68
        $this->alwaysTrueBranch = true;
13✔
69
    }
70

71
    public function isAlwaysTrueBranch(): bool
219✔
72
    {
73
        return $this->alwaysTrueBranch;
219✔
74
    }
75

76
    /**
77
     * Return the branch-level JSON schema (the composition element schema, which may contain
78
     * additionalProperties constraints). This is distinct from getJsonSchema(), which proxies
79
     * to the inner wrapped property's schema via PropertyProxy.
80
     */
81
    public function getBranchSchema(): JsonSchema
428✔
82
    {
83
        return $this->jsonSchema;
428✔
84
    }
85

86
    /**
87
     * Return the wrapped property whose validators back this branch. Iterating its
88
     * validators directly returns the source validator instances, whereas
89
     * `getOrderedValidators()` on the decorator returns fresh `withProperty(...)` clones on
90
     * every call. Mutations targeted at clones are invisible at render time; mutations
91
     * targeted at the wrapped property's source validators propagate.
92
     */
93
    public function getWrappedProperty(): PropertyInterface
13✔
94
    {
95
        return $this->definitionsCollection->offsetGet(self::PROPERTY_KEY);
13✔
96
    }
97

98
    /**
99
     * Returns the property names declared in this branch's `properties` keyword.
100
     *
101
     * Used by the composition post processor to harvest names that must invalidate the
102
     * setter-side validation cache when those properties change.
103
     *
104
     * @return string[]
105
     */
NEW
106
    public function getBranchDeclaredPropertyNames(): array
×
107
    {
NEW
108
        return array_keys($this->jsonSchema->getJson()['properties'] ?? []);
×
109
    }
110

111
    /**
112
     * Returns the declared property names as a PHP array literal ready for direct template
113
     * embedding. var_export emits a syntactically-valid PHP literal regardless of which
114
     * characters the names contain (quotes, backslashes, multibyte sequences).
115
     */
NEW
116
    public function getBranchDeclaredPropertyNamesPhpLiteral(): string
×
117
    {
NEW
118
        return var_export($this->getBranchDeclaredPropertyNames(), true);
×
119
    }
120

121
    /**
122
     * Returns the patternProperties regexes as a PHP array literal ready for direct template
123
     * embedding. Each pattern is wrapped with `/` delimiters and any embedded `/` is escaped so
124
     * the result can be passed straight to `preg_match`.
125
     */
NEW
126
    public function getBranchPatternPropertyPatternsPhpLiteral(): string
×
127
    {
NEW
128
        return RenderHelper::varExportPcrePatterns(
×
NEW
129
            array_keys($this->jsonSchema->getJson()['patternProperties'] ?? []),
×
NEW
130
        );
×
131
    }
132

133
    /**
134
     * Returns true if this branch's JSON schema explicitly declares an `additionalProperties`
135
     * value that is not `false` (i.e., `true` or a schema object).
136
     *
137
     * Absent `additionalProperties` returns false: omitting the keyword does not contribute
138
     * annotation results that unevaluatedProperties checks. When this method returns true,
139
     * a successful branch is treated as evaluating every model key.
140
     */
141
    public function branchHasNonFalseAdditionalProperties(): bool
33✔
142
    {
143
        $branchJson = $this->jsonSchema->getJson();
33✔
144

145
        return isset($branchJson['additionalProperties'])
33✔
146
            && $branchJson['additionalProperties'] !== false;
33✔
147
    }
148

149
    /**
150
     * True when the branch declares `items` as a schema object (not a tuple list, not a
151
     * boolean). A schema-form `items` claims every index in the validated array.
152
     */
153
    public function branchHasItemsSchema(): bool
11✔
154
    {
155
        $items = $this->jsonSchema->getJson()['items'] ?? null;
11✔
156

157
        return is_array($items) && $items !== [] && !array_is_list($items);
11✔
158
    }
159

160
    /**
161
     * Count of items in a tuple-form `items` array. Returns 0 when items is absent, boolean,
162
     * or schema-form. The tuple claims indices 0..count-1 when the branch succeeds.
163
     */
164
    public function getBranchTupleItemsCount(): int
11✔
165
    {
166
        $items = $this->jsonSchema->getJson()['items'] ?? null;
11✔
167

168
        if (!is_array($items) || $items === [] || !array_is_list($items)) {
11✔
169
            return 0;
2✔
170
        }
171

172
        return count($items);
9✔
173
    }
174

175
    /**
176
     * True when the branch declares `additionalItems` as anything other than `false`. The
177
     * `false` value rejects tail indices; any other value (true or schema object) accepts
178
     * them and contributes them to the evaluated set.
179
     */
180
    public function branchHasNonFalseAdditionalItems(): bool
11✔
181
    {
182
        $branchJson = $this->jsonSchema->getJson();
11✔
183

184
        return array_key_exists('additionalItems', $branchJson)
11✔
185
            && $branchJson['additionalItems'] !== false;
11✔
186
    }
187

188
    /**
189
     * True when the branch declares the `contains` keyword. The actual matched indices are
190
     * collected at runtime by the contains validator into a local accumulator; the composition
191
     * template unions that accumulator into the branch's evaluated set on success.
192
     */
193
    public function branchHasContains(): bool
13✔
194
    {
195
        return array_key_exists('contains', $this->jsonSchema->getJson());
13✔
196
    }
197

198
    /**
199
     * True when the branch carries at least one array-side applicator (`items`,
200
     * `additionalItems`, `contains`) and no object-side applicators (`properties`,
201
     * `additionalProperties`, `patternProperties`). Drives whether the composition template
202
     * writes the branch's slot into `_compositionAnnotated`.
203
     */
204
    public function branchIsArrayKind(): bool
13✔
205
    {
206
        $branchJson = $this->jsonSchema->getJson();
13✔
207

208
        $hasArrayApplicator = array_key_exists('items', $branchJson)
13✔
209
            || array_key_exists('additionalItems', $branchJson)
13✔
210
            || array_key_exists('contains', $branchJson);
13✔
211

212
        $hasObjectApplicator = array_key_exists('properties', $branchJson)
13✔
213
            || array_key_exists('additionalProperties', $branchJson)
13✔
214
            || array_key_exists('patternProperties', $branchJson);
13✔
215

216
        return $hasArrayApplicator && !$hasObjectApplicator;
13✔
217
    }
218
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc