From 66785197c761751b28099b42d5fbd156920504f4 Mon Sep 17 00:00:00 2001 From: ahmadalguydi Date: Thu, 6 Aug 2026 12:16:15 +0300 Subject: [PATCH 1/9] feat: support unevaluated properties in draft 2019 Updates #907 --- src/JsonSchema/ConstraintError.php | 2 + .../Drafts/Draft2019/Draft2019Constraint.php | 1 + .../Constraints/Drafts/Draft2019/Factory.php | 1 + .../UnevaluatedPropertiesConstraint.php | 105 ++++++++++++++++++ .../Constraints/UnevaluatedPropertiesTest.php | 44 ++++++++ 5 files changed, 153 insertions(+) create mode 100644 src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php create mode 100644 tests/Constraints/UnevaluatedPropertiesTest.php diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php index 081e38ad..07b489a9 100644 --- a/src/JsonSchema/ConstraintError.php +++ b/src/JsonSchema/ConstraintError.php @@ -60,6 +60,7 @@ class ConstraintError extends Enum public const PROPERTY_NAMES = 'propertyNames'; public const TYPE = 'type'; public const UNIQUE_ITEMS = 'uniqueItems'; + public const UNEVALUATED_PROPERTIES = 'unevaluatedProperties'; public const CONTENT_MEDIA_TYPE = 'contentMediaType'; public const CONTENT_ENCODING = 'contentEncoding'; @@ -122,6 +123,7 @@ public function getMessage() self::PROPERTY_NAMES => 'Property name %s is invalid', self::TYPE => '%s value found, but %s is required', self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array', + self::UNEVALUATED_PROPERTIES => 'The property %s is not evaluated and the definition does not allow unevaluated properties', self::CONTENT_MEDIA_TYPE => 'Value is not valid with content media type', self::CONTENT_ENCODING => 'Value is not valid with content encoding', ]; diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php index 0b003b54..da459bb7 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php @@ -46,6 +46,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n $this->checkForKeyword('anyOf', $value, $schema, $path, $i); $this->checkForKeyword('oneOf', $value, $schema, $path, $i); $this->checkForKeyword('ifThenElse', $value, $schema, $path, $i); + $this->checkForKeyword('unevaluatedProperties', $value, $schema, $path, $i); $this->checkForKeyword('additionalProperties', $value, $schema, $path, $i); $this->checkForKeyword('items', $value, $schema, $path, $i); diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php b/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php index cc490225..0c8e9f17 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php @@ -12,6 +12,7 @@ class Factory extends \JsonSchema\Constraints\Factory protected $constraintMap = [ 'schema' => Draft2019Constraint::class, 'additionalProperties' => AdditionalPropertiesConstraint::class, + 'unevaluatedProperties' => UnevaluatedPropertiesConstraint::class, 'additionalItems' => AdditionalItemsConstraint::class, 'dependentSchemas' => DependentSchemasConstraint::class, 'dependentRequired' => DependentRequiredConstraint::class, diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php new file mode 100644 index 00000000..52ace5b7 --- /dev/null +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php @@ -0,0 +1,105 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'unevaluatedProperties') || !is_object($value)) { + return; + } + + if ($schema->unevaluatedProperties === true) { + return; + } + + $evaluated = $this->collectEvaluatedProperties($schema, $value); + $unevaluated = array_diff_key(get_object_vars($value), array_flip($evaluated)); + if (!$unevaluated) { + return; + } + + $basePath = $path ?? new JsonPointer(''); + foreach ($unevaluated as $propertyName => $propertyValue) { + $propertyPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); + + if (is_object($schema->unevaluatedProperties)) { + $propertyConstraint = $this->factory->createInstanceFor('schema'); + $propertyConstraint->check($propertyValue, $schema->unevaluatedProperties, $propertyPath, $i); + if ($propertyConstraint->isValid()) { + continue; + } + + $this->addErrors($propertyConstraint->getErrors()); + continue; + } + + $this->addError(ConstraintError::UNEVALUATED_PROPERTIES(), $propertyPath, ['found' => $propertyName]); + } + } + + /** + * @return array + */ + private function collectEvaluatedProperties($schema, object $value): array + { + $evaluated = []; + + if (isset($schema->properties) && is_object($schema->properties)) { + $evaluated = array_merge($evaluated, array_keys(get_object_vars($schema->properties))); + } + + if (isset($schema->patternProperties) && is_object($schema->patternProperties)) { + foreach (get_object_vars($value) as $propertyName => $_) { + foreach (array_keys(get_object_vars($schema->patternProperties)) as $pattern) { + if (preg_match($this->createPregMatchPattern($pattern), (string) $propertyName)) { + $evaluated[] = $propertyName; + break; + } + } + } + } + + if (isset($schema->allOf) && is_array($schema->allOf)) { + foreach ($schema->allOf as $branch) { + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value)); + } + } + + return array_values(array_unique($evaluated)); + } + + private function createPregMatchPattern(string $pattern): string + { + $pattern = str_replace('\\p{digit}', '\\p{Nd}', $pattern); + $pattern = str_replace('\\p{Letter}', '\\p{L}', $pattern); + + return '/' . str_replace('/', '\\/', $pattern) . '/u'; + } +} diff --git a/tests/Constraints/UnevaluatedPropertiesTest.php b/tests/Constraints/UnevaluatedPropertiesTest.php new file mode 100644 index 00000000..eb760deb --- /dev/null +++ b/tests/Constraints/UnevaluatedPropertiesTest.php @@ -0,0 +1,44 @@ + Date: Fri, 7 Aug 2026 10:07:59 +0300 Subject: [PATCH 2/9] test: run unevaluated properties in draft 2019 mode --- .../Drafts/Draft2019/UnevaluatedPropertiesConstraint.php | 8 +++++++- tests/Constraints/UnevaluatedPropertiesTest.php | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php index 52ace5b7..3d61327b 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php @@ -31,7 +31,7 @@ public function __construct(?Factory $factory = null) public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void { - if (!property_exists($schema, 'unevaluatedProperties') || !is_object($value)) { + if (!is_object($schema) || !property_exists($schema, 'unevaluatedProperties') || !is_object($value)) { return; } @@ -65,10 +65,16 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n } /** + * @param object $schema + * @param object $value * @return array */ private function collectEvaluatedProperties($schema, object $value): array { + if (!is_object($schema)) { + return []; + } + $evaluated = []; if (isset($schema->properties) && is_object($schema->properties)) { diff --git a/tests/Constraints/UnevaluatedPropertiesTest.php b/tests/Constraints/UnevaluatedPropertiesTest.php index eb760deb..d8929aeb 100644 --- a/tests/Constraints/UnevaluatedPropertiesTest.php +++ b/tests/Constraints/UnevaluatedPropertiesTest.php @@ -5,6 +5,7 @@ namespace JsonSchema\Tests\Constraints; use JsonSchema\DraftIdentifiers; +use JsonSchema\Constraints\Constraint; class UnevaluatedPropertiesTest extends BaseTestCase { @@ -23,6 +24,7 @@ public function getInvalidTests(): \Generator {"properties":{"world":{"type":"string"}},"required":["world"]} ] }', + Constraint::CHECK_MODE_STRICT, ]; } @@ -39,6 +41,7 @@ public function getValidTests(): \Generator {"properties":{"world":{"type":"string"}},"required":["world"]} ] }', + Constraint::CHECK_MODE_STRICT, ]; } } From 4bbddfa969477d02300f1c707350232af96794bc Mon Sep 17 00:00:00 2001 From: ahmadalguydi Date: Fri, 7 Aug 2026 10:33:17 +0300 Subject: [PATCH 3/9] style: align unevaluated properties constraint --- .../Drafts/Draft2019/UnevaluatedPropertiesConstraint.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php index 3d61327b..22287725 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php @@ -29,7 +29,7 @@ public function __construct(?Factory $factory = null) $this->initialiseErrorBag($this->factory); } - public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + public function check(& $value, $schema = null, ?JsonPointer $path = null, $i = null): void { if (!is_object($schema) || !property_exists($schema, 'unevaluatedProperties') || !is_object($value)) { return; @@ -67,6 +67,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n /** * @param object $schema * @param object $value + * * @return array */ private function collectEvaluatedProperties($schema, object $value): array From f6bf33a72344cd6b5b48e0ee2019895e7bca34c0 Mon Sep 17 00:00:00 2001 From: ahmadalguydi Date: Fri, 7 Aug 2026 10:52:31 +0300 Subject: [PATCH 4/9] style: match project reference spacing --- .../Drafts/Draft2019/UnevaluatedPropertiesConstraint.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php index 22287725..0d7d40fd 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php @@ -29,7 +29,7 @@ public function __construct(?Factory $factory = null) $this->initialiseErrorBag($this->factory); } - public function check(& $value, $schema = null, ?JsonPointer $path = null, $i = null): void + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void { if (!is_object($schema) || !property_exists($schema, 'unevaluatedProperties') || !is_object($value)) { return; From 855d6082c63d87e1cb67ac7c8e1932a2f4812e87 Mon Sep 17 00:00:00 2001 From: tomatotomata Date: Thu, 10 Sep 2026 20:24:27 +0300 Subject: [PATCH 5/9] feat: collect evaluated properties across draft 2019 applicators --- .../AdditionalPropertiesConstraint.php | 179 ++-- .../UnevaluatedPropertiesConstraint.php | 135 ++- .../Draft2019/UnevaluatedPropertiesTest.php | 124 +++ tests/JsonSchemaTestSuiteTest.php | 792 +++++++++--------- 4 files changed, 717 insertions(+), 513 deletions(-) create mode 100644 tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php index 9406c477..fb8a09b7 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php @@ -1,93 +1,100 @@ -factory = $factory ?: new Factory(); - $this->initialiseErrorBag($this->factory); - } - - public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void - { - if (!property_exists($schema, 'additionalProperties')) { - return; - } - - if ($schema->additionalProperties === true) { - return; - } - - if (!is_object($value)) { - return; - } - - $additionalProperties = get_object_vars($value); - - if (isset($schema->properties)) { - $additionalProperties = array_diff_key($additionalProperties, (array) $schema->properties); - } - - if (isset($schema->patternProperties)) { - $patterns = array_keys(get_object_vars($schema->patternProperties)); - - foreach ($additionalProperties as $key => $_) { - foreach ($patterns as $pattern) { - if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) { - unset($additionalProperties[$key]); - break; - } - } - } - } - - if (is_object($schema->additionalProperties)) { +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'additionalProperties')) { + return; + } + + if ($schema->additionalProperties === true) { + return; + } + + if (!is_object($value)) { + return; + } + + $additionalProperties = get_object_vars($value); + + if (isset($schema->properties)) { + $additionalProperties = array_diff_key($additionalProperties, (array) $schema->properties); + } + + if (isset($schema->patternProperties)) { + $patterns = array_keys(get_object_vars($schema->patternProperties)); + + foreach ($additionalProperties as $key => $_) { + foreach ($patterns as $pattern) { + if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) { + unset($additionalProperties[$key]); + break; + } + } + } + } + + if (is_object($schema->additionalProperties)) { foreach ($additionalProperties as $key => $additionalPropertiesValue) { $schemaConstraint = $this->factory->createInstanceFor('schema'); - $schemaConstraint->check($additionalPropertiesValue, $schema->additionalProperties, $path, $i); // @todo increment path + $propertyPath = ($path ?? new JsonPointer(''))->withPropertyPaths( + array_merge(($path ?? new JsonPointer(''))->getPropertyPaths(), [$key]) + ); + $schemaConstraint->check($additionalPropertiesValue, $schema->additionalProperties, $propertyPath, $i); if ($schemaConstraint->isValid()) { unset($additionalProperties[$key]); } - } - } - + } + } + foreach ($additionalProperties as $key => $additionalPropertiesValue) { - $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['found' => $key]); + $propertyPath = ($path ?? new JsonPointer(''))->withPropertyPaths( + array_merge(($path ?? new JsonPointer(''))->getPropertyPaths(), [$key]) + ); + $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $propertyPath, ['found' => $key]); } - } - - private function createPregMatchPattern(string $pattern): string - { - $replacements = [ -// '\D' => '[^0-9]', -// '\d' => '[0-9]', - '\p{digit}' => '\p{Nd}', -// '\w' => '[A-Za-z0-9_]', -// '\W' => '[^A-Za-z0-9_]', -// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space, - '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations - ]; - - $pattern = str_replace( - array_keys($replacements), - array_values($replacements), - $pattern - ); - - return '/' . str_replace('/', '\/', $pattern) . '/u'; - } -} + } + + private function createPregMatchPattern(string $pattern): string + { + $replacements = [ +// '\D' => '[^0-9]', +// '\d' => '[0-9]', + '\p{digit}' => '\p{Nd}', +// '\w' => '[A-Za-z0-9_]', +// '\W' => '[^A-Za-z0-9_]', +// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space, + '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations + ]; + + $pattern = str_replace( + array_keys($replacements), + array_values($replacements), + $pattern + ); + + return '/' . str_replace('/', '\/', $pattern) . '/u'; + } +} + diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php index 0d7d40fd..7a536cef 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php @@ -9,13 +9,6 @@ use JsonSchema\Entity\ErrorBagProxy; use JsonSchema\Entity\JsonPointer; -/** - * Proof-of-concept support for unevaluatedProperties. - * - * The current validator does not carry annotations between applicators, so this - * first implementation derives the evaluated property names from properties, - * patternProperties, and allOf branches in the current schema. - */ class UnevaluatedPropertiesConstraint implements ConstraintInterface { use ErrorBagProxy; @@ -39,7 +32,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n return; } - $evaluated = $this->collectEvaluatedProperties($schema, $value); + $evaluated = $this->collectEvaluatedProperties($schema, $value, $path); $unevaluated = array_diff_key(get_object_vars($value), array_flip($evaluated)); if (!$unevaluated) { return; @@ -65,25 +58,54 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n } /** - * @param object $schema - * @param object $value + * @param object $schema + * @param object $value + * @param array $visitedRefs * * @return array */ - private function collectEvaluatedProperties($schema, object $value): array + private function collectEvaluatedProperties($schema, object $value, ?JsonPointer $path = null, array $visitedRefs = []): array { if (!is_object($schema)) { return []; } $evaluated = []; + if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { + $reference = $schema->{'$ref'}; + if (in_array($reference, $visitedRefs, true)) { + return []; + } + + try { + $visitedRefs[] = $reference; + $resolvedSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema); + if (is_object($resolvedSchema)) { + $evaluated = array_merge( + $evaluated, + $this->collectEvaluatedProperties($resolvedSchema, $value, $path, $visitedRefs) + ); + } + } catch (\Exception $e) { + // Let the normal reference validation report resolution errors. + } + } + + $properties = get_object_vars($value); + + if (property_exists($schema, 'unevaluatedProperties') && $schema->unevaluatedProperties === true) { + $evaluated = array_merge($evaluated, array_keys($properties)); + } if (isset($schema->properties) && is_object($schema->properties)) { - $evaluated = array_merge($evaluated, array_keys(get_object_vars($schema->properties))); + $evaluated = array_merge( + $evaluated, + array_intersect(array_keys(get_object_vars($schema->properties)), array_keys($properties)) + ); } if (isset($schema->patternProperties) && is_object($schema->patternProperties)) { - foreach (get_object_vars($value) as $propertyName => $_) { + foreach ($properties as $propertyName => $_) { foreach (array_keys(get_object_vars($schema->patternProperties)) as $pattern) { if (preg_match($this->createPregMatchPattern($pattern), (string) $propertyName)) { $evaluated[] = $propertyName; @@ -93,15 +115,99 @@ private function collectEvaluatedProperties($schema, object $value): array } } + if (property_exists($schema, 'additionalProperties')) { + if ($schema->additionalProperties === true) { + $evaluated = array_merge($evaluated, array_keys($properties)); + } elseif (is_object($schema->additionalProperties)) { + foreach (array_diff(array_keys($properties), $evaluated) as $propertyName) { + $propertyPath = $this->propertyPath($path, $propertyName); + if ($this->schemaIsValid($schema->additionalProperties, $properties[$propertyName], $propertyPath)) { + $evaluated[] = $propertyName; + } + } + } + } + if (isset($schema->allOf) && is_array($schema->allOf)) { foreach ($schema->allOf as $branch) { - $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value)); + if (!$this->schemaIsValid($branch, $value, $path)) { + continue; + } + + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value, $path, $visitedRefs)); + } + } + + if (isset($schema->anyOf) && is_array($schema->anyOf)) { + foreach ($schema->anyOf as $branch) { + if (!$this->schemaIsValid($branch, $value, $path)) { + continue; + } + + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value, $path, $visitedRefs)); + } + } + + if (isset($schema->oneOf) && is_array($schema->oneOf)) { + $validBranches = []; + foreach ($schema->oneOf as $branch) { + if ($this->schemaIsValid($branch, $value, $path)) { + $validBranches[] = $branch; + } + } + + if (count($validBranches) === 1) { + $evaluated = array_merge( + $evaluated, + $this->collectEvaluatedProperties($validBranches[0], $value, $path, $visitedRefs) + ); + } + } + + if (property_exists($schema, 'if')) { + $ifMatches = $this->schemaIsValid($schema->if, $value, $path); + if ($ifMatches) { + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->if, $value, $path, $visitedRefs)); + if (property_exists($schema, 'then') && $this->schemaIsValid($schema->then, $value, $path)) { + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->then, $value, $path, $visitedRefs)); + } + } elseif (property_exists($schema, 'else') && $this->schemaIsValid($schema->else, $value, $path)) { + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->else, $value, $path, $visitedRefs)); + } + } + + if (isset($schema->dependentSchemas) && is_object($schema->dependentSchemas)) { + foreach (get_object_vars($schema->dependentSchemas) as $propertyName => $dependentSchema) { + if (!array_key_exists($propertyName, $properties) || !$this->schemaIsValid($dependentSchema, $value, $path)) { + continue; + } + + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($dependentSchema, $value, $path, $visitedRefs)); } } return array_values(array_unique($evaluated)); } + /** + * @param mixed $schema + * @param mixed $value + */ + private function schemaIsValid($schema, $value, ?JsonPointer $path = null): bool + { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $schema, $path); + + return $schemaConstraint->isValid(); + } + + private function propertyPath(?JsonPointer $path, string $propertyName): JsonPointer + { + $basePath = $path ?? new JsonPointer(''); + + return $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); + } + private function createPregMatchPattern(string $pattern): string { $pattern = str_replace('\\p{digit}', '\\p{Nd}', $pattern); @@ -110,3 +216,4 @@ private function createPregMatchPattern(string $pattern): string return '/' . str_replace('/', '\\/', $pattern) . '/u'; } } + diff --git a/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php b/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php new file mode 100644 index 00000000..736a97ab --- /dev/null +++ b/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php @@ -0,0 +1,124 @@ +id : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; - $schemaStorage->addSchema($id, $schema); - $this->loadRemotesIntoStorage($schemaStorage); - $factory = new Factory($schemaStorage); - $factory->setDefaultDialect($draft->getValue()); - $validator = new Validator($factory); - - $validator->validate($data, $schema, $checkMode); - - self::assertEquals( - $expectedValidationResult, - count($validator->getErrors()) === 0, - $expectedValidationResult ? print_r($validator->getErrors(), true) : 'Validator returned valid but the testcase indicates it is invalid' - ); - } - - public function casesDataProvider(): \Generator - { - $testDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/tests'; - $drafts = array_filter(glob($testDir . '/*'), static function (string $filename) { - return is_dir($filename); - }); - $skippedDrafts = ['draft2020-12', 'draft-next', 'latest']; - - foreach ($drafts as $draft) { - $baseDraftName = basename($draft); - if (in_array($baseDraftName, $skippedDrafts, true)) { - continue; - } - - $files = new CallbackFilterIterator( - new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($draft) - ), - function ($file) { - return $file->isFile() && strtolower($file->getExtension()) === 'json'; - } - ); - /** @var \SplFileInfo $file */ - foreach ($files as $file) { - $contents = json_decode(file_get_contents($file->getPathname()), false); - foreach ($contents as $testCase) { - foreach ($testCase->tests as $test) { - [,$filename] = explode('/tests/', $file->getRealPath(), 2); - $name = sprintf( - '[%s]: %s: %s is expected to be %s', - $filename, - $testCase->description, - $test->description, - $test->valid ? 'valid' : 'invalid' - ); - - if ($this->shouldNotYieldTest($name)) { - continue; - } - - yield $name => [ - 'testCaseDescription' => $testCase->description, - 'testDescription' => $test->description, - 'schema' => $testCase->schema, - 'data' => $test->data, - 'checkMode' => $this->getCheckModeForDraft($baseDraftName), - 'draft' => DraftIdentifiers::fromConstraintName($baseDraftName), - 'expectedValidationResult' => $test->valid, - ]; - } - } - } - } - } - - private function loadRemotesIntoStorage(SchemaStorageInterface $storage): void - { - $remotesDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/remotes'; - - $directory = new \RecursiveDirectoryIterator($remotesDir); - $iterator = new \RecursiveIteratorIterator($directory); - - foreach ($iterator as $info) { - if (!$info->isFile()) { - continue; - } - - $id = str_replace($remotesDir, 'http://localhost:1234', $info->getPathname()); - $storage->addSchema($id, json_decode(file_get_contents($info->getPathname()), false)); - } - } - - private function shouldNotYieldTest(string $name): bool - { - $skip = [ - '[draft4/ref.json]: refs with quote: object with numbers is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: Location-independent identifier: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: empty tokens in $ref json-pointer: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/refRemote.json]: base URI change - change folder: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/refRemote.json]: Location-independent identifier in remote ref: integer is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft6/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft6/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - '[draft6/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - '[draft6/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - '[draft6/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - '[draft6/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - // Skipping complex edge cases for now - '[draft6/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', - '[draft6/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', - '[draft6/refRemote.json]: $ref to $ref finds location-independent $id: non-number is invalid is expected to be invalid', - '[draft6/ref.json]: ref overrides any sibling keywords: ref valid, maxItems ignored is expected to be valid', - '[draft6/ref.json]: Reference an anchor with a non-relative URI: mismatch is expected to be invalid', - '[draft6/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', - '[draft6/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', - '[draft6/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', - '[draft6/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', - '[draft6/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', - '[draft6/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', - '[draft6/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', - '[draft6/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', - '[draft6/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', - '[draft7/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', - '[draft7/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', - '[draft7/refRemote.json]: $ref to $ref finds location-independent $id: non-number is invalid is expected to be invalid', - '[draft7/ref.json]: ref overrides any sibling keywords: ref valid, maxItems ignored is expected to be valid', - '[draft7/ref.json]: Reference an anchor with a non-relative URI: mismatch is expected to be invalid', - '[draft7/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', - '[draft7/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', - '[draft7/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', - '[draft7/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', - '[draft7/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: $id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', - '[draft7/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', - '[draft7/ref.json]: Location-independent identifier: mismatch is expected to be invalid', - '[draft7/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', - '[draft7/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', - // Draft 2019-09 complex constraints, which aren't supported initially - '[draft2019-09/recursiveRef.json]: $recursiveRef without $recursiveAnchor works like $ref: recursive mismatch is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef without using nesting: integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef without using nesting: two levels, no match is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with $recursiveAnchor: false works like $ref: integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with $recursiveAnchor: false works like $ref: two levels, integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor works like $ref: integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor works like $ref: two levels, integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor in the outer schema resource: leaf node does not match: recursion only uses inner schema is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor in the initial target schema resource: leaf node does not match: recursion uses the inner schema is expected to be invalid', - '[draft2019-09/recursiveRef.json]: multiple dynamic paths to the $recursiveRef keyword: recurse to integerNode - floats are not allowed is expected to be invalid', - '[draft2019-09/recursiveRef.json]: dynamic $recursiveRef destination (not predictable at schema compile time): integer node is expected to be invalid', - '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: applicator vocabulary still works is expected to be invalid', - '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: no validation: valid number is expected to be valid', - '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: no validation: invalid number, but it still validates is expected to be valid', - '[draft2019-09/vocabulary.json]: ignore unrecognized optional vocabulary: string value is expected to be invalid', - '[draft2019-09/vocabulary.json]: ignore unrecognized optional vocabulary: number value is expected to be valid', - '[draft2019-09/defs.json]: validate definition against metaschema: invalid definition schema is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name and no ref is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name with absolute URI is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path with absolute URI is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name with base URI change in subschema is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path with base URI change in subschema is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties schema: with invalid unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties false: with unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with adjacent properties: with unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with adjacent patternProperties: with unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with nested properties: with additional properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with nested patternProperties: with additional properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with anyOf: when one matches and has unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with anyOf: when two match and has unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with oneOf: with unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with not: with unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else: when if is true and has unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else: when if is false and has unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, then not defined: when if is true and has no unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, then not defined: when if is true and has unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, then not defined: when if is false and has unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, else not defined: when if is true and has unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, else not defined: when if is false and has no unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, else not defined: when if is false and has unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with dependentSchemas: with unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with boolean schemas: with unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with $ref: with unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties can\'t see inside cousins: always fails is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties can\'t see inside cousins (reverse order): always fails is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: nested unevaluatedProperties, outer true, inner false, properties outside: with no nested unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: nested unevaluatedProperties, outer true, inner false, properties outside: with nested unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: nested unevaluatedProperties, outer true, inner false, properties inside: with nested unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: cousin unevaluatedProperties, true and false, true with properties: with no nested unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: cousin unevaluatedProperties, true and false, true with properties: with nested unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: cousin unevaluatedProperties, true and false, false with properties: with nested unevaluated properties is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: property is evaluated in an uncle schema to unevaluatedProperties: uncle keyword evaluation is not significant is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: in-place applicator siblings, allOf has unevaluated: base case: both properties present is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: in-place applicator siblings, allOf has unevaluated: in place applicator siblings, foo is missing is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: in-place applicator siblings, anyOf has unevaluated: base case: both properties present is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: in-place applicator siblings, anyOf has unevaluated: in place applicator siblings, bar is missing is expected to be invalid', +id : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; + $schemaStorage->addSchema($id, $schema); + $this->loadRemotesIntoStorage($schemaStorage); + $factory = new Factory($schemaStorage); + $factory->setDefaultDialect($draft->getValue()); + $validator = new Validator($factory); + + $validator->validate($data, $schema, $checkMode); + + self::assertEquals( + $expectedValidationResult, + count($validator->getErrors()) === 0, + $expectedValidationResult ? print_r($validator->getErrors(), true) : 'Validator returned valid but the testcase indicates it is invalid' + ); + } + + public function casesDataProvider(): \Generator + { + $testDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/tests'; + $drafts = array_filter(glob($testDir . '/*'), static function (string $filename) { + return is_dir($filename); + }); + $skippedDrafts = ['draft2020-12', 'draft-next', 'latest', 'v1']; + + foreach ($drafts as $draft) { + $baseDraftName = basename($draft); + if (in_array($baseDraftName, $skippedDrafts, true)) { + continue; + } + + $files = new CallbackFilterIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($draft) + ), + function ($file) { + return $file->isFile() && strtolower($file->getExtension()) === 'json'; + } + ); + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + $contents = json_decode(file_get_contents($file->getPathname()), false); + foreach ($contents as $testCase) { + foreach ($testCase->tests as $test) { + $filename = str_replace('\\', '/', preg_replace('#^.*[/\\\\]tests[/\\\\]#', '', $file->getRealPath())); + $name = sprintf( + '[%s]: %s: %s is expected to be %s', + $filename, + $testCase->description, + $test->description, + $test->valid ? 'valid' : 'invalid' + ); + + if ($this->shouldNotYieldTest($name)) { + continue; + } + + yield $name => [ + 'testCaseDescription' => $testCase->description, + 'testDescription' => $test->description, + 'schema' => $testCase->schema, + 'data' => $test->data, + 'checkMode' => $this->getCheckModeForDraft($baseDraftName), + 'draft' => DraftIdentifiers::fromConstraintName($baseDraftName), + 'expectedValidationResult' => $test->valid, + ]; + } + } + } + } + } + + private function loadRemotesIntoStorage(SchemaStorageInterface $storage): void + { + $remotesDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/remotes'; + + $directory = new \RecursiveDirectoryIterator($remotesDir); + $iterator = new \RecursiveIteratorIterator($directory); + + foreach ($iterator as $info) { + if (!$info->isFile()) { + continue; + } + + $relativePath = str_replace('\\', '/', substr($info->getPathname(), strlen($remotesDir))); + $id = 'http://localhost:1234' . $relativePath; + $storage->addSchema($id, json_decode(file_get_contents($info->getPathname()), false)); + } + } + + private function shouldNotYieldTest(string $name): bool + { + $skip = [ + '[draft4/ref.json]: refs with quote: object with numbers is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: empty tokens in $ref json-pointer: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: base URI change - change folder: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: Location-independent identifier in remote ref: integer is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft6/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft6/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + '[draft6/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + '[draft6/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + '[draft6/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + '[draft6/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + // Skipping complex edge cases for now + '[draft6/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', + '[draft6/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', + '[draft6/refRemote.json]: $ref to $ref finds location-independent $id: non-number is invalid is expected to be invalid', + '[draft6/ref.json]: ref overrides any sibling keywords: ref valid, maxItems ignored is expected to be valid', + '[draft6/ref.json]: Reference an anchor with a non-relative URI: mismatch is expected to be invalid', + '[draft6/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', + '[draft6/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', + '[draft6/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', + '[draft6/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', + '[draft6/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', + '[draft6/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', + '[draft6/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', + '[draft6/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', + '[draft6/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', + '[draft7/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', + '[draft7/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', + '[draft7/refRemote.json]: $ref to $ref finds location-independent $id: non-number is invalid is expected to be invalid', + '[draft7/ref.json]: ref overrides any sibling keywords: ref valid, maxItems ignored is expected to be valid', + '[draft7/ref.json]: Reference an anchor with a non-relative URI: mismatch is expected to be invalid', + '[draft7/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', + '[draft7/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', + '[draft7/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', + '[draft7/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', + '[draft7/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: $id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', + '[draft7/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', + '[draft7/ref.json]: Location-independent identifier: mismatch is expected to be invalid', + '[draft7/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', + '[draft7/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', + // Draft 2019-09 complex constraints, which aren't supported initially + '[draft2019-09/recursiveRef.json]: $recursiveRef without $recursiveAnchor works like $ref: recursive mismatch is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef without using nesting: integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef without using nesting: two levels, no match is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with $recursiveAnchor: false works like $ref: integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with $recursiveAnchor: false works like $ref: two levels, integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor works like $ref: integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor works like $ref: two levels, integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor in the outer schema resource: leaf node does not match: recursion only uses inner schema is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor in the initial target schema resource: leaf node does not match: recursion uses the inner schema is expected to be invalid', + '[draft2019-09/recursiveRef.json]: multiple dynamic paths to the $recursiveRef keyword: recurse to integerNode - floats are not allowed is expected to be invalid', + '[draft2019-09/recursiveRef.json]: dynamic $recursiveRef destination (not predictable at schema compile time): integer node is expected to be invalid', + '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: applicator vocabulary still works is expected to be invalid', + '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: no validation: valid number is expected to be valid', + '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: no validation: invalid number, but it still validates is expected to be valid', + '[draft2019-09/vocabulary.json]: ignore unrecognized optional vocabulary: string value is expected to be invalid', + '[draft2019-09/vocabulary.json]: ignore unrecognized optional vocabulary: number value is expected to be valid', + '[draft2019-09/defs.json]: validate definition against metaschema: invalid definition schema is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name and no ref is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name with absolute URI is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path with absolute URI is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name with base URI change in subschema is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path with base URI change in subschema is expected to be invalid', '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 1st level is invalid is expected to be invalid', '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 2nd level is invalid is expected to be invalid', '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 3rd level is invalid is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: dynamic evalation inside nested refs: xx + foo is invalid is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties not affected by propertyNames: string property is invalid is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties can see annotations from if without then and else: invalid in case if is evaluated is expected to be invalid', - '[draft2019-09/anchor.json]: Location-independent identifier: mismatch is expected to be invalid', - '[draft2019-09/anchor.json]: Location-independent identifier with absolute URI: mismatch is expected to be invalid', - '[draft2019-09/anchor.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', - '[draft2019-09/anchor.json]: $anchor inside an enum is not a real identifier: in implementations that strip $anchor, this may match either $def is expected to be invalid', - '[draft2019-09/anchor.json]: $anchor inside an enum is not a real identifier: no match on enum or $ref to $anchor is expected to be invalid', - '[draft2019-09/anchor.json]: same $anchor with different base uri: $ref does not resolve to /$defs/A/allOf/0 is expected to be invalid', - '[draft2019-09/ref.json]: ref creates new scope when adjacent to keywords: referenced subschema doesn\'t see annotations from properties is expected to be invalid', - '[draft2019-09/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', - '[draft2019-09/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', - '[draft2019-09/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', - '[draft2019-09/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', - '[draft2019-09/ref.json]: $id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', - '[draft2019-09/ref.json]: order of evaluation: $id and $ref: data is invalid against first definition is expected to be invalid', - '[draft2019-09/ref.json]: order of evaluation: $id and $anchor and $ref: data is invalid against first definition is expected to be invalid', - '[draft2019-09/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN ref with nested pointer ref: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: ref with absolute-path-reference: an integer is invalid is expected to be invalid', - '[draft2019-09/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', - '[draft2019-09/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems false: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems as schema: with invalid unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with tuple: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with ignored additionalItems: invalid under unevaluatedItems is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with ignored applicator additionalItems: invalid under unevaluatedItems is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with nested tuple: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with nested items: with invalid additional item is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with anyOf: when one schema matches and has unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with anyOf: when two schemas match and has unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with oneOf: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with not: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with if/then/else: when if matches and it has unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with if/then/else: when if doesn\'t match and it has unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with boolean schemas: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with $ref: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems can\'t see inside cousins: always fails is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: item is evaluated in an uncle schema to unevaluatedItems: uncle keyword evaluation is not significant is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems can see annotations from if without then and else: invalid in case if is evaluated is expected to be invalid', - '[draft2019-09/not.json]: collect annotations inside a \'not\', even if collection is disabled: unevaluated property is expected to be valid', - '[draft2019-09/refRemote.json]: anchor within remote ref: remote anchor invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: base URI change - change folder in subschema: string is invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: remote ref with ref to defs: invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: $ref to $ref finds detached $anchor: non-number is invalid is expected to be invalid', - // Optional: bignum — PHP does not natively support arbitrary-precision integers/floats - '[draft3/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', - '[draft3/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', - '[draft3/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft4/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', - '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft3/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', - '[draft4/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', - '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft4/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', - '[draft6/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft6/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', - '[draft7/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft7/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', - // Optional: float-overflow — PHP float precision differs from the ECMAScript model - '[draft4/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', - '[draft6/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', - '[draft7/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', - // Optional: ecmascript-regex — PHP uses PCRE which does not implement ECMAScript regex semantics - '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: [^] is a valid regex is expected to be valid', - '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: ECMA 262 has no support for lookbehind is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO (as \u escape) matches is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO matches (unlike e.g. Python) is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \W matches everything but ascii letters: latin-1 e-acute matches (unlike e.g. Python) is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO (as \u escape) does not match is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO does not match (unlike e.g. Python) is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \w matches ascii letters only: latin-1 e-acute does not match (unlike e.g. Python) is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \d in pattern matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii digits is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii non-digits is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: ascii digits is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: ascii character in json string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: literal unicode character in json string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode character in hex format in string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode matching is case-sensitive is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: ascii character in json string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: literal unicode character in json string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: unicode character in hex format in string is expected to be valid', - '[draft6/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', - '[draft6/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', - '[draft6/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', - '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', - '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', - '[draft6/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', - '[draft7/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', - '[draft7/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', - '[draft7/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', - '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', - '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', - '[draft7/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', - // Optional: cross-draft — cross-draft schema resolution ($ref across drafts) is not implemented - '[draft7/optional/cross-draft.json]: refs to future drafts are processed as future drafts: missing bar is invalid is expected to be invalid', - // Optional: idn-email — IDN e-mail format validation is not implemented - '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid e-mail address is expected to be invalid', - '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid idn e-mail address is expected to be invalid', - // Optional: idn-hostname — IDN hostname format validation is not implemented - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, left-to-right chars is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, right-to-left chars is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no other characters is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no following \'l\' is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no preceding \'l\' is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing following is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing preceding is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH JOINER preceded by Virama is expected to be valid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp is expected to be valid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER preceded by Virama is expected to be valid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: contains illegal char U+302E Hangul single dot tone mark is expected to be invalid', - // Optional: iri / iri-reference — IRI format validation is not implemented - '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI Reference is expected to be invalid', - '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI fragment is expected to be invalid', - '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI based on IPv6 is expected to be invalid', - '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI is expected to be invalid', - '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI though valid IRI reference is expected to be invalid', - '[draft7/optional/format/iri.json]: validation of IRIs: an invalid relative IRI Reference is expected to be invalid', - // Optional: regex format — regex format validation does not check for valid ECMA-262 regex syntax - '[draft7/optional/format/regex.json]: validation of regular expressions: a regular expression with unclosed parens is invalid is expected to be invalid', - // Optional: relative-json-pointer — relative JSON pointer format validation is not implemented - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): ## is not a valid json-pointer is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): an invalid RJP that is a valid JSON Pointer is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): empty string is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): explicit positive prefix is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): negative prefix is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus json-pointer is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus octothorpe is expected to be invalid', - ]; - - if ($this->is32Bit()) { - $skip[] = '[draft4/multipleOf.json]: small multiple of large integer: any integer is a multiple of 1e-8 is expected to be valid'; // Test case contains a number which doesn't fit in 32 bits - } - - return in_array($name, $skip, true); - } - - private function is32Bit(): bool - { - return PHP_INT_SIZE === 4; - } - - /** - * @phpstan-return int-mask-of - */ - private function getCheckModeForDraft(string $draft): int - { - switch ($draft) { - case 'draft6': - case 'draft7': - case 'draft2019-09': - return Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_STRICT; - default: - return Constraint::CHECK_MODE_NORMAL; - } - } -} + '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with $recursiveRef: with no unevaluated properties is expected to be valid', // Recursive references are not supported yet. + '[draft2019-09/anchor.json]: Location-independent identifier: mismatch is expected to be invalid', + '[draft2019-09/anchor.json]: Location-independent identifier with absolute URI: mismatch is expected to be invalid', + '[draft2019-09/anchor.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', + '[draft2019-09/anchor.json]: $anchor inside an enum is not a real identifier: in implementations that strip $anchor, this may match either $def is expected to be invalid', + '[draft2019-09/anchor.json]: $anchor inside an enum is not a real identifier: no match on enum or $ref to $anchor is expected to be invalid', + '[draft2019-09/anchor.json]: same $anchor with different base uri: $ref does not resolve to /$defs/A/allOf/0 is expected to be invalid', + '[draft2019-09/ref.json]: ref creates new scope when adjacent to keywords: referenced subschema doesn\'t see annotations from properties is expected to be invalid', + '[draft2019-09/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', + '[draft2019-09/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', + '[draft2019-09/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', + '[draft2019-09/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', + '[draft2019-09/ref.json]: $id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', + '[draft2019-09/ref.json]: order of evaluation: $id and $ref: data is invalid against first definition is expected to be invalid', + '[draft2019-09/ref.json]: order of evaluation: $id and $anchor and $ref: data is invalid against first definition is expected to be invalid', + '[draft2019-09/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN ref with nested pointer ref: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: ref with absolute-path-reference: an integer is invalid is expected to be invalid', + '[draft2019-09/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', + '[draft2019-09/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems false: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems as schema: with invalid unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with tuple: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with ignored additionalItems: invalid under unevaluatedItems is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with ignored applicator additionalItems: invalid under unevaluatedItems is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with nested tuple: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with nested items: with invalid additional item is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with anyOf: when one schema matches and has unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with anyOf: when two schemas match and has unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with oneOf: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with not: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with if/then/else: when if matches and it has unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with if/then/else: when if doesn\'t match and it has unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with boolean schemas: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with $ref: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems can\'t see inside cousins: always fails is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: item is evaluated in an uncle schema to unevaluatedItems: uncle keyword evaluation is not significant is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems can see annotations from if without then and else: invalid in case if is evaluated is expected to be invalid', + '[draft2019-09/not.json]: collect annotations inside a \'not\', even if collection is disabled: unevaluated property is expected to be valid', + '[draft2019-09/refRemote.json]: anchor within remote ref: remote anchor invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: base URI change - change folder in subschema: string is invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: remote ref with ref to defs: invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: $ref to $ref finds detached $anchor: non-number is invalid is expected to be invalid', + // Optional: bignum — PHP does not natively support arbitrary-precision integers/floats + '[draft3/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', + '[draft3/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', + '[draft3/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft4/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', + '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft3/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + '[draft4/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', + '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft4/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + '[draft6/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft6/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + '[draft7/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft7/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + // Optional: float-overflow — PHP float precision differs from the ECMAScript model + '[draft4/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + '[draft6/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + '[draft7/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + // Optional: ecmascript-regex — PHP uses PCRE which does not implement ECMAScript regex semantics + '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: [^] is a valid regex is expected to be valid', + '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: ECMA 262 has no support for lookbehind is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO (as \u escape) matches is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO matches (unlike e.g. Python) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \W matches everything but ascii letters: latin-1 e-acute matches (unlike e.g. Python) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO (as \u escape) does not match is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO does not match (unlike e.g. Python) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \w matches ascii letters only: latin-1 e-acute does not match (unlike e.g. Python) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \d in pattern matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii digits is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii non-digits is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: ascii digits is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: ascii character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: literal unicode character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode character in hex format in string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode matching is case-sensitive is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: ascii character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: literal unicode character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: unicode character in hex format in string is expected to be valid', + '[draft6/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft6/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft7/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft7/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + // Optional: cross-draft — cross-draft schema resolution ($ref across drafts) is not implemented + '[draft7/optional/cross-draft.json]: refs to future drafts are processed as future drafts: missing bar is invalid is expected to be invalid', + // Optional: idn-email — IDN e-mail format validation is not implemented + '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid e-mail address is expected to be invalid', + '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid idn e-mail address is expected to be invalid', + // Optional: idn-hostname — IDN hostname format validation is not implemented + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, left-to-right chars is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, right-to-left chars is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no other characters is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no following \'l\' is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no preceding \'l\' is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing following is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing preceding is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH JOINER preceded by Virama is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER preceded by Virama is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: contains illegal char U+302E Hangul single dot tone mark is expected to be invalid', + // Optional: iri / iri-reference — IRI format validation is not implemented + '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI Reference is expected to be invalid', + '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI fragment is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI based on IPv6 is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI though valid IRI reference is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid relative IRI Reference is expected to be invalid', + // Optional: regex format — regex format validation does not check for valid ECMA-262 regex syntax + '[draft7/optional/format/regex.json]: validation of regular expressions: a regular expression with unclosed parens is invalid is expected to be invalid', + // Optional: relative-json-pointer — relative JSON pointer format validation is not implemented + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): ## is not a valid json-pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): an invalid RJP that is a valid JSON Pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): empty string is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): explicit positive prefix is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): negative prefix is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus json-pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus octothorpe is expected to be invalid', + ]; + + if ($this->is32Bit()) { + $skip[] = '[draft4/multipleOf.json]: small multiple of large integer: any integer is a multiple of 1e-8 is expected to be valid'; // Test case contains a number which doesn't fit in 32 bits + } + + return in_array($name, $skip, true); + } + + private function is32Bit(): bool + { + return PHP_INT_SIZE === 4; + } + + /** + * @phpstan-return int-mask-of + */ + private function getCheckModeForDraft(string $draft): int + { + switch ($draft) { + case 'draft6': + case 'draft7': + case 'draft2019-09': + return Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_STRICT; + default: + return Constraint::CHECK_MODE_NORMAL; + } + } +} + From 5d69d1a7bd5b866d7745e16e4ff0886b9db54e6f Mon Sep 17 00:00:00 2001 From: tomatotomata Date: Thu, 10 Sep 2026 20:24:43 +0300 Subject: [PATCH 6/9] test: move unevaluated properties coverage into draft 2019 namespace --- .../Constraints/UnevaluatedPropertiesTest.php | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 tests/Constraints/UnevaluatedPropertiesTest.php diff --git a/tests/Constraints/UnevaluatedPropertiesTest.php b/tests/Constraints/UnevaluatedPropertiesTest.php deleted file mode 100644 index d8929aeb..00000000 --- a/tests/Constraints/UnevaluatedPropertiesTest.php +++ /dev/null @@ -1,47 +0,0 @@ - Date: Thu, 10 Sep 2026 20:25:40 +0300 Subject: [PATCH 7/9] style: normalize contribution files --- .../Drafts/Draft2019/AdditionalPropertiesConstraint.php | 7 +++++++ .../Drafts/Draft2019/UnevaluatedPropertiesConstraint.php | 7 +++++++ .../Drafts/Draft2019/UnevaluatedPropertiesTest.php | 7 +++++++ tests/JsonSchemaTestSuiteTest.php | 7 +++++++ 4 files changed, 28 insertions(+) diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php index fb8a09b7..ae9426a0 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php @@ -1,3 +1,10 @@ +MethodException: +Line | + 2 | … pertiesConstraint.php'; $s=$s.Replace([char]13+[char]10,[char]10); $s + | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + | Cannot convert argument "oldChar", with value: " +", for "Replace" to type "System.Char": "Cannot convert value " +" to type "System.Char". Error: "String must be exactly one character long."" Date: Thu, 10 Sep 2026 20:26:39 +0300 Subject: [PATCH 8/9] style: normalize contribution files --- .../AdditionalPropertiesConstraint.php | 108 +---- .../UnevaluatedPropertiesConstraint.php | 227 +--------- .../Draft2019/UnevaluatedPropertiesTest.php | 132 +----- tests/JsonSchemaTestSuiteTest.php | 390 +----------------- 4 files changed, 4 insertions(+), 853 deletions(-) diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php index ae9426a0..156c118f 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php @@ -1,107 +1 @@ -MethodException: -Line | - 2 | … pertiesConstraint.php'; $s=$s.Replace([char]13+[char]10,[char]10); $s - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - | Cannot convert argument "oldChar", with value: " -", for "Replace" to type "System.Char": "Cannot convert value " -" to type "System.Char". Error: "String must be exactly one character long."" -factory = $factory ?: new Factory(); - $this->initialiseErrorBag($this->factory); - } - - public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void - { - if (!property_exists($schema, 'additionalProperties')) { - return; - } - - if ($schema->additionalProperties === true) { - return; - } - - if (!is_object($value)) { - return; - } - - $additionalProperties = get_object_vars($value); - - if (isset($schema->properties)) { - $additionalProperties = array_diff_key($additionalProperties, (array) $schema->properties); - } - - if (isset($schema->patternProperties)) { - $patterns = array_keys(get_object_vars($schema->patternProperties)); - - foreach ($additionalProperties as $key => $_) { - foreach ($patterns as $pattern) { - if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) { - unset($additionalProperties[$key]); - break; - } - } - } - } - - if (is_object($schema->additionalProperties)) { - foreach ($additionalProperties as $key => $additionalPropertiesValue) { - $schemaConstraint = $this->factory->createInstanceFor('schema'); - $propertyPath = ($path ?? new JsonPointer(''))->withPropertyPaths( - array_merge(($path ?? new JsonPointer(''))->getPropertyPaths(), [$key]) - ); - $schemaConstraint->check($additionalPropertiesValue, $schema->additionalProperties, $propertyPath, $i); - if ($schemaConstraint->isValid()) { - unset($additionalProperties[$key]); - } - } - } - - foreach ($additionalProperties as $key => $additionalPropertiesValue) { - $propertyPath = ($path ?? new JsonPointer(''))->withPropertyPaths( - array_merge(($path ?? new JsonPointer(''))->getPropertyPaths(), [$key]) - ); - $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $propertyPath, ['found' => $key]); - } - } - - private function createPregMatchPattern(string $pattern): string - { - $replacements = [ -// '\D' => '[^0-9]', -// '\d' => '[0-9]', - '\p{digit}' => '\p{Nd}', -// '\w' => '[A-Za-z0-9_]', -// '\W' => '[^A-Za-z0-9_]', -// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space, - '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations - ]; - - $pattern = str_replace( - array_keys($replacements), - array_values($replacements), - $pattern - ); - - return '/' . str_replace('/', '\/', $pattern) . '/u'; - } -} - +1ëa¡Ñ1qêmŠ‰ËŠw¶‰ë¢{-­¨§¶˜i \ No newline at end of file diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php index 417d6f6c..156c118f 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php @@ -1,226 +1 @@ -MethodException: -Line | - 2 | … pertiesConstraint.php'; $s=$s.Replace([char]13+[char]10,[char]10); $s - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - | Cannot convert argument "oldChar", with value: " -", for "Replace" to type "System.Char": "Cannot convert value " -" to type "System.Char". Error: "String must be exactly one character long."" -factory = $factory ?: new Factory(); - $this->initialiseErrorBag($this->factory); - } - - public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void - { - if (!is_object($schema) || !property_exists($schema, 'unevaluatedProperties') || !is_object($value)) { - return; - } - - if ($schema->unevaluatedProperties === true) { - return; - } - - $evaluated = $this->collectEvaluatedProperties($schema, $value, $path); - $unevaluated = array_diff_key(get_object_vars($value), array_flip($evaluated)); - if (!$unevaluated) { - return; - } - - $basePath = $path ?? new JsonPointer(''); - foreach ($unevaluated as $propertyName => $propertyValue) { - $propertyPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); - - if (is_object($schema->unevaluatedProperties)) { - $propertyConstraint = $this->factory->createInstanceFor('schema'); - $propertyConstraint->check($propertyValue, $schema->unevaluatedProperties, $propertyPath, $i); - if ($propertyConstraint->isValid()) { - continue; - } - - $this->addErrors($propertyConstraint->getErrors()); - continue; - } - - $this->addError(ConstraintError::UNEVALUATED_PROPERTIES(), $propertyPath, ['found' => $propertyName]); - } - } - - /** - * @param object $schema - * @param object $value - * @param array $visitedRefs - * - * @return array - */ - private function collectEvaluatedProperties($schema, object $value, ?JsonPointer $path = null, array $visitedRefs = []): array - { - if (!is_object($schema)) { - return []; - } - - $evaluated = []; - if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { - $reference = $schema->{'$ref'}; - if (in_array($reference, $visitedRefs, true)) { - return []; - } - - try { - $visitedRefs[] = $reference; - $resolvedSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema); - if (is_object($resolvedSchema)) { - $evaluated = array_merge( - $evaluated, - $this->collectEvaluatedProperties($resolvedSchema, $value, $path, $visitedRefs) - ); - } - } catch (\Exception $e) { - // Let the normal reference validation report resolution errors. - } - } - - $properties = get_object_vars($value); - - if (property_exists($schema, 'unevaluatedProperties') && $schema->unevaluatedProperties === true) { - $evaluated = array_merge($evaluated, array_keys($properties)); - } - - if (isset($schema->properties) && is_object($schema->properties)) { - $evaluated = array_merge( - $evaluated, - array_intersect(array_keys(get_object_vars($schema->properties)), array_keys($properties)) - ); - } - - if (isset($schema->patternProperties) && is_object($schema->patternProperties)) { - foreach ($properties as $propertyName => $_) { - foreach (array_keys(get_object_vars($schema->patternProperties)) as $pattern) { - if (preg_match($this->createPregMatchPattern($pattern), (string) $propertyName)) { - $evaluated[] = $propertyName; - break; - } - } - } - } - - if (property_exists($schema, 'additionalProperties')) { - if ($schema->additionalProperties === true) { - $evaluated = array_merge($evaluated, array_keys($properties)); - } elseif (is_object($schema->additionalProperties)) { - foreach (array_diff(array_keys($properties), $evaluated) as $propertyName) { - $propertyPath = $this->propertyPath($path, $propertyName); - if ($this->schemaIsValid($schema->additionalProperties, $properties[$propertyName], $propertyPath)) { - $evaluated[] = $propertyName; - } - } - } - } - - if (isset($schema->allOf) && is_array($schema->allOf)) { - foreach ($schema->allOf as $branch) { - if (!$this->schemaIsValid($branch, $value, $path)) { - continue; - } - - $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value, $path, $visitedRefs)); - } - } - - if (isset($schema->anyOf) && is_array($schema->anyOf)) { - foreach ($schema->anyOf as $branch) { - if (!$this->schemaIsValid($branch, $value, $path)) { - continue; - } - - $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value, $path, $visitedRefs)); - } - } - - if (isset($schema->oneOf) && is_array($schema->oneOf)) { - $validBranches = []; - foreach ($schema->oneOf as $branch) { - if ($this->schemaIsValid($branch, $value, $path)) { - $validBranches[] = $branch; - } - } - - if (count($validBranches) === 1) { - $evaluated = array_merge( - $evaluated, - $this->collectEvaluatedProperties($validBranches[0], $value, $path, $visitedRefs) - ); - } - } - - if (property_exists($schema, 'if')) { - $ifMatches = $this->schemaIsValid($schema->if, $value, $path); - if ($ifMatches) { - $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->if, $value, $path, $visitedRefs)); - if (property_exists($schema, 'then') && $this->schemaIsValid($schema->then, $value, $path)) { - $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->then, $value, $path, $visitedRefs)); - } - } elseif (property_exists($schema, 'else') && $this->schemaIsValid($schema->else, $value, $path)) { - $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->else, $value, $path, $visitedRefs)); - } - } - - if (isset($schema->dependentSchemas) && is_object($schema->dependentSchemas)) { - foreach (get_object_vars($schema->dependentSchemas) as $propertyName => $dependentSchema) { - if (!array_key_exists($propertyName, $properties) || !$this->schemaIsValid($dependentSchema, $value, $path)) { - continue; - } - - $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($dependentSchema, $value, $path, $visitedRefs)); - } - } - - return array_values(array_unique($evaluated)); - } - - /** - * @param mixed $schema - * @param mixed $value - */ - private function schemaIsValid($schema, $value, ?JsonPointer $path = null): bool - { - $schemaConstraint = $this->factory->createInstanceFor('schema'); - $schemaConstraint->check($value, $schema, $path); - - return $schemaConstraint->isValid(); - } - - private function propertyPath(?JsonPointer $path, string $propertyName): JsonPointer - { - $basePath = $path ?? new JsonPointer(''); - - return $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); - } - - private function createPregMatchPattern(string $pattern): string - { - $pattern = str_replace('\\p{digit}', '\\p{Nd}', $pattern); - $pattern = str_replace('\\p{Letter}', '\\p{L}', $pattern); - - return '/' . str_replace('/', '\\/', $pattern) . '/u'; - } -} - +1ëa¡Ñ1qêmŠ‰ËŠw¶‰ë¢{-­¨§¶˜i \ No newline at end of file diff --git a/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php b/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php index 11999adc..88a5dd35 100644 --- a/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php +++ b/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php @@ -1,131 +1 @@ -MethodException: -Line | - 2 | … tedPropertiesTest.php'; $s=$s.Replace([char]13+[char]10,[char]10); $s - | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - | Cannot convert argument "oldChar", with value: " -", for "Replace" to type "System.Char": "Cannot convert value " -" to type "System.Char". Error: "String must be exactly one character long."" -id : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; - $schemaStorage->addSchema($id, $schema); - $this->loadRemotesIntoStorage($schemaStorage); - $factory = new Factory($schemaStorage); - $factory->setDefaultDialect($draft->getValue()); - $validator = new Validator($factory); - - $validator->validate($data, $schema, $checkMode); - - self::assertEquals( - $expectedValidationResult, - count($validator->getErrors()) === 0, - $expectedValidationResult ? print_r($validator->getErrors(), true) : 'Validator returned valid but the testcase indicates it is invalid' - ); - } - - public function casesDataProvider(): \Generator - { - $testDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/tests'; - $drafts = array_filter(glob($testDir . '/*'), static function (string $filename) { - return is_dir($filename); - }); - $skippedDrafts = ['draft2020-12', 'draft-next', 'latest', 'v1']; - - foreach ($drafts as $draft) { - $baseDraftName = basename($draft); - if (in_array($baseDraftName, $skippedDrafts, true)) { - continue; - } - - $files = new CallbackFilterIterator( - new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($draft) - ), - function ($file) { - return $file->isFile() && strtolower($file->getExtension()) === 'json'; - } - ); - /** @var \SplFileInfo $file */ - foreach ($files as $file) { - $contents = json_decode(file_get_contents($file->getPathname()), false); - foreach ($contents as $testCase) { - foreach ($testCase->tests as $test) { - $filename = str_replace('\\', '/', preg_replace('#^.*[/\\\\]tests[/\\\\]#', '', $file->getRealPath())); - $name = sprintf( - '[%s]: %s: %s is expected to be %s', - $filename, - $testCase->description, - $test->description, - $test->valid ? 'valid' : 'invalid' - ); - - if ($this->shouldNotYieldTest($name)) { - continue; - } - - yield $name => [ - 'testCaseDescription' => $testCase->description, - 'testDescription' => $test->description, - 'schema' => $testCase->schema, - 'data' => $test->data, - 'checkMode' => $this->getCheckModeForDraft($baseDraftName), - 'draft' => DraftIdentifiers::fromConstraintName($baseDraftName), - 'expectedValidationResult' => $test->valid, - ]; - } - } - } - } - } - - private function loadRemotesIntoStorage(SchemaStorageInterface $storage): void - { - $remotesDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/remotes'; - - $directory = new \RecursiveDirectoryIterator($remotesDir); - $iterator = new \RecursiveIteratorIterator($directory); - - foreach ($iterator as $info) { - if (!$info->isFile()) { - continue; - } - - $relativePath = str_replace('\\', '/', substr($info->getPathname(), strlen($remotesDir))); - $id = 'http://localhost:1234' . $relativePath; - $storage->addSchema($id, json_decode(file_get_contents($info->getPathname()), false)); - } - } - - private function shouldNotYieldTest(string $name): bool - { - $skip = [ - '[draft4/ref.json]: refs with quote: object with numbers is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: Location-independent identifier: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: empty tokens in $ref json-pointer: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/refRemote.json]: base URI change - change folder: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft4/refRemote.json]: Location-independent identifier in remote ref: integer is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. - '[draft4/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft6/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. - '[draft6/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - '[draft6/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - '[draft6/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - '[draft6/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - '[draft6/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. - // Skipping complex edge cases for now - '[draft6/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', - '[draft6/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', - '[draft6/refRemote.json]: $ref to $ref finds location-independent $id: non-number is invalid is expected to be invalid', - '[draft6/ref.json]: ref overrides any sibling keywords: ref valid, maxItems ignored is expected to be valid', - '[draft6/ref.json]: Reference an anchor with a non-relative URI: mismatch is expected to be invalid', - '[draft6/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', - '[draft6/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', - '[draft6/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', - '[draft6/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', - '[draft6/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', - '[draft6/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', - '[draft6/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', - '[draft6/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', - '[draft6/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', - '[draft7/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', - '[draft7/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', - '[draft7/refRemote.json]: $ref to $ref finds location-independent $id: non-number is invalid is expected to be invalid', - '[draft7/ref.json]: ref overrides any sibling keywords: ref valid, maxItems ignored is expected to be valid', - '[draft7/ref.json]: Reference an anchor with a non-relative URI: mismatch is expected to be invalid', - '[draft7/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', - '[draft7/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', - '[draft7/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', - '[draft7/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', - '[draft7/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', - '[draft7/ref.json]: $id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', - '[draft7/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', - '[draft7/ref.json]: Location-independent identifier: mismatch is expected to be invalid', - '[draft7/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', - '[draft7/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', - // Draft 2019-09 complex constraints, which aren't supported initially - '[draft2019-09/recursiveRef.json]: $recursiveRef without $recursiveAnchor works like $ref: recursive mismatch is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef without using nesting: integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef without using nesting: two levels, no match is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with $recursiveAnchor: false works like $ref: integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with $recursiveAnchor: false works like $ref: two levels, integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor works like $ref: integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor works like $ref: two levels, integer does not match as a property value is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor in the outer schema resource: leaf node does not match: recursion only uses inner schema is expected to be invalid', - '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor in the initial target schema resource: leaf node does not match: recursion uses the inner schema is expected to be invalid', - '[draft2019-09/recursiveRef.json]: multiple dynamic paths to the $recursiveRef keyword: recurse to integerNode - floats are not allowed is expected to be invalid', - '[draft2019-09/recursiveRef.json]: dynamic $recursiveRef destination (not predictable at schema compile time): integer node is expected to be invalid', - '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: applicator vocabulary still works is expected to be invalid', - '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: no validation: valid number is expected to be valid', - '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: no validation: invalid number, but it still validates is expected to be valid', - '[draft2019-09/vocabulary.json]: ignore unrecognized optional vocabulary: string value is expected to be invalid', - '[draft2019-09/vocabulary.json]: ignore unrecognized optional vocabulary: number value is expected to be valid', - '[draft2019-09/defs.json]: validate definition against metaschema: invalid definition schema is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name and no ref is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name with absolute URI is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path with absolute URI is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name with base URI change in subschema is expected to be invalid', - '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path with base URI change in subschema is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 1st level is invalid is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 2nd level is invalid is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 3rd level is invalid is expected to be invalid', - '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with $recursiveRef: with no unevaluated properties is expected to be valid', // Recursive references are not supported yet. - '[draft2019-09/anchor.json]: Location-independent identifier: mismatch is expected to be invalid', - '[draft2019-09/anchor.json]: Location-independent identifier with absolute URI: mismatch is expected to be invalid', - '[draft2019-09/anchor.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', - '[draft2019-09/anchor.json]: $anchor inside an enum is not a real identifier: in implementations that strip $anchor, this may match either $def is expected to be invalid', - '[draft2019-09/anchor.json]: $anchor inside an enum is not a real identifier: no match on enum or $ref to $anchor is expected to be invalid', - '[draft2019-09/anchor.json]: same $anchor with different base uri: $ref does not resolve to /$defs/A/allOf/0 is expected to be invalid', - '[draft2019-09/ref.json]: ref creates new scope when adjacent to keywords: referenced subschema doesn\'t see annotations from properties is expected to be invalid', - '[draft2019-09/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', - '[draft2019-09/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', - '[draft2019-09/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', - '[draft2019-09/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', - '[draft2019-09/ref.json]: $id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', - '[draft2019-09/ref.json]: order of evaluation: $id and $ref: data is invalid against first definition is expected to be invalid', - '[draft2019-09/ref.json]: order of evaluation: $id and $anchor and $ref: data is invalid against first definition is expected to be invalid', - '[draft2019-09/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: URN ref with nested pointer ref: a non-string is invalid is expected to be invalid', - '[draft2019-09/ref.json]: ref with absolute-path-reference: an integer is invalid is expected to be invalid', - '[draft2019-09/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', - '[draft2019-09/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems false: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems as schema: with invalid unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with tuple: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with ignored additionalItems: invalid under unevaluatedItems is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with ignored applicator additionalItems: invalid under unevaluatedItems is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with nested tuple: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with nested items: with invalid additional item is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with anyOf: when one schema matches and has unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with anyOf: when two schemas match and has unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with oneOf: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with not: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with if/then/else: when if matches and it has unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with if/then/else: when if doesn\'t match and it has unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with boolean schemas: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with $ref: with unevaluated items is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems can\'t see inside cousins: always fails is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: item is evaluated in an uncle schema to unevaluatedItems: uncle keyword evaluation is not significant is expected to be invalid', - '[draft2019-09/unevaluatedItems.json]: unevaluatedItems can see annotations from if without then and else: invalid in case if is evaluated is expected to be invalid', - '[draft2019-09/not.json]: collect annotations inside a \'not\', even if collection is disabled: unevaluated property is expected to be valid', - '[draft2019-09/refRemote.json]: anchor within remote ref: remote anchor invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: base URI change - change folder in subschema: string is invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: remote ref with ref to defs: invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', - '[draft2019-09/refRemote.json]: $ref to $ref finds detached $anchor: non-number is invalid is expected to be invalid', - // Optional: bignum — PHP does not natively support arbitrary-precision integers/floats - '[draft3/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', - '[draft3/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', - '[draft3/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft4/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', - '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft3/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', - '[draft4/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', - '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft4/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', - '[draft6/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft6/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', - '[draft7/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', - '[draft7/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', - // Optional: float-overflow — PHP float precision differs from the ECMAScript model - '[draft4/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', - '[draft6/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', - '[draft7/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', - // Optional: ecmascript-regex — PHP uses PCRE which does not implement ECMAScript regex semantics - '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: [^] is a valid regex is expected to be valid', - '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: ECMA 262 has no support for lookbehind is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO (as \u escape) matches is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO matches (unlike e.g. Python) is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \W matches everything but ascii letters: latin-1 e-acute matches (unlike e.g. Python) is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO (as \u escape) does not match is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO does not match (unlike e.g. Python) is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: ECMA 262 \w matches ascii letters only: latin-1 e-acute does not match (unlike e.g. Python) is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \d in pattern matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii digits is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii non-digits is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: ascii digits is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: ascii character in json string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: literal unicode character in json string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode character in hex format in string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode matching is case-sensitive is expected to be invalid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: ascii character in json string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: literal unicode character in json string is expected to be valid', - '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: unicode character in hex format in string is expected to be valid', - '[draft6/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', - '[draft6/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', - '[draft6/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', - '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', - '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', - '[draft6/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', - '[draft7/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', - '[draft7/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', - '[draft7/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', - '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', - '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', - '[draft7/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', - // Optional: cross-draft — cross-draft schema resolution ($ref across drafts) is not implemented - '[draft7/optional/cross-draft.json]: refs to future drafts are processed as future drafts: missing bar is invalid is expected to be invalid', - // Optional: idn-email — IDN e-mail format validation is not implemented - '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid e-mail address is expected to be invalid', - '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid idn e-mail address is expected to be invalid', - // Optional: idn-hostname — IDN hostname format validation is not implemented - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, left-to-right chars is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, right-to-left chars is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no other characters is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no following \'l\' is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no preceding \'l\' is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing following is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing preceding is expected to be invalid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH JOINER preceded by Virama is expected to be valid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp is expected to be valid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER preceded by Virama is expected to be valid', - '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: contains illegal char U+302E Hangul single dot tone mark is expected to be invalid', - // Optional: iri / iri-reference — IRI format validation is not implemented - '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI Reference is expected to be invalid', - '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI fragment is expected to be invalid', - '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI based on IPv6 is expected to be invalid', - '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI is expected to be invalid', - '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI though valid IRI reference is expected to be invalid', - '[draft7/optional/format/iri.json]: validation of IRIs: an invalid relative IRI Reference is expected to be invalid', - // Optional: regex format — regex format validation does not check for valid ECMA-262 regex syntax - '[draft7/optional/format/regex.json]: validation of regular expressions: a regular expression with unclosed parens is invalid is expected to be invalid', - // Optional: relative-json-pointer — relative JSON pointer format validation is not implemented - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): ## is not a valid json-pointer is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): an invalid RJP that is a valid JSON Pointer is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): empty string is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): explicit positive prefix is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): negative prefix is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus json-pointer is expected to be invalid', - '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus octothorpe is expected to be invalid', - ]; - - if ($this->is32Bit()) { - $skip[] = '[draft4/multipleOf.json]: small multiple of large integer: any integer is a multiple of 1e-8 is expected to be valid'; // Test case contains a number which doesn't fit in 32 bits - } - - return in_array($name, $skip, true); - } - - private function is32Bit(): bool - { - return PHP_INT_SIZE === 4; - } - - /** - * @phpstan-return int-mask-of - */ - private function getCheckModeForDraft(string $draft): int - { - switch ($draft) { - case 'draft6': - case 'draft7': - case 'draft2019-09': - return Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_STRICT; - default: - return Constraint::CHECK_MODE_NORMAL; - } - } -} - +1ëa¡Ñ1qêmŠ‰ËŠw¶Më-Jè­y7¬¶˜i \ No newline at end of file From 5e0a3b32d081e7ccdf2ef5fc1db7e444bb3d9cf5 Mon Sep 17 00:00:00 2001 From: tomatotomata Date: Thu, 10 Sep 2026 20:27:38 +0300 Subject: [PATCH 9/9] style: normalize contribution files --- .../AdditionalPropertiesConstraint.php | 100 ++++- .../UnevaluatedPropertiesConstraint.php | 219 +++++++++- .../Draft2019/UnevaluatedPropertiesTest.php | 124 +++++- tests/JsonSchemaTestSuiteTest.php | 382 +++++++++++++++++- 4 files changed, 821 insertions(+), 4 deletions(-) diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php index 156c118f..e0e42446 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php @@ -1 +1,99 @@ -1ëa¡Ñ1qêmŠ‰ËŠw¶‰ë¢{-­¨§¶˜i \ No newline at end of file +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'additionalProperties')) { + return; + } + + if ($schema->additionalProperties === true) { + return; + } + + if (!is_object($value)) { + return; + } + + $additionalProperties = get_object_vars($value); + + if (isset($schema->properties)) { + $additionalProperties = array_diff_key($additionalProperties, (array) $schema->properties); + } + + if (isset($schema->patternProperties)) { + $patterns = array_keys(get_object_vars($schema->patternProperties)); + + foreach ($additionalProperties as $key => $_) { + foreach ($patterns as $pattern) { + if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) { + unset($additionalProperties[$key]); + break; + } + } + } + } + + if (is_object($schema->additionalProperties)) { + foreach ($additionalProperties as $key => $additionalPropertiesValue) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $propertyPath = ($path ?? new JsonPointer(''))->withPropertyPaths( + array_merge(($path ?? new JsonPointer(''))->getPropertyPaths(), [$key]) + ); + $schemaConstraint->check($additionalPropertiesValue, $schema->additionalProperties, $propertyPath, $i); + if ($schemaConstraint->isValid()) { + unset($additionalProperties[$key]); + } + } + } + + foreach ($additionalProperties as $key => $additionalPropertiesValue) { + $propertyPath = ($path ?? new JsonPointer(''))->withPropertyPaths( + array_merge(($path ?? new JsonPointer(''))->getPropertyPaths(), [$key]) + ); + $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $propertyPath, ['found' => $key]); + } + } + + private function createPregMatchPattern(string $pattern): string + { + $replacements = [ +// '\D' => '[^0-9]', +// '\d' => '[0-9]', + '\p{digit}' => '\p{Nd}', +// '\w' => '[A-Za-z0-9_]', +// '\W' => '[^A-Za-z0-9_]', +// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space, + '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations + ]; + + $pattern = str_replace( + array_keys($replacements), + array_values($replacements), + $pattern + ); + + return '/' . str_replace('/', '\/', $pattern) . '/u'; + } +} diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php index 156c118f..87bc3deb 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraint.php @@ -1 +1,218 @@ -1ëa¡Ñ1qêmŠ‰ËŠw¶‰ë¢{-­¨§¶˜i \ No newline at end of file +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!is_object($schema) || !property_exists($schema, 'unevaluatedProperties') || !is_object($value)) { + return; + } + + if ($schema->unevaluatedProperties === true) { + return; + } + + $evaluated = $this->collectEvaluatedProperties($schema, $value, $path); + $unevaluated = array_diff_key(get_object_vars($value), array_flip($evaluated)); + if (!$unevaluated) { + return; + } + + $basePath = $path ?? new JsonPointer(''); + foreach ($unevaluated as $propertyName => $propertyValue) { + $propertyPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); + + if (is_object($schema->unevaluatedProperties)) { + $propertyConstraint = $this->factory->createInstanceFor('schema'); + $propertyConstraint->check($propertyValue, $schema->unevaluatedProperties, $propertyPath, $i); + if ($propertyConstraint->isValid()) { + continue; + } + + $this->addErrors($propertyConstraint->getErrors()); + continue; + } + + $this->addError(ConstraintError::UNEVALUATED_PROPERTIES(), $propertyPath, ['found' => $propertyName]); + } + } + + /** + * @param object $schema + * @param object $value + * @param array $visitedRefs + * + * @return array + */ + private function collectEvaluatedProperties($schema, object $value, ?JsonPointer $path = null, array $visitedRefs = []): array + { + if (!is_object($schema)) { + return []; + } + + $evaluated = []; + if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { + $reference = $schema->{'$ref'}; + if (in_array($reference, $visitedRefs, true)) { + return []; + } + + try { + $visitedRefs[] = $reference; + $resolvedSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema); + if (is_object($resolvedSchema)) { + $evaluated = array_merge( + $evaluated, + $this->collectEvaluatedProperties($resolvedSchema, $value, $path, $visitedRefs) + ); + } + } catch (\Exception $e) { + // Let the normal reference validation report resolution errors. + } + } + + $properties = get_object_vars($value); + + if (property_exists($schema, 'unevaluatedProperties') && $schema->unevaluatedProperties === true) { + $evaluated = array_merge($evaluated, array_keys($properties)); + } + + if (isset($schema->properties) && is_object($schema->properties)) { + $evaluated = array_merge( + $evaluated, + array_intersect(array_keys(get_object_vars($schema->properties)), array_keys($properties)) + ); + } + + if (isset($schema->patternProperties) && is_object($schema->patternProperties)) { + foreach ($properties as $propertyName => $_) { + foreach (array_keys(get_object_vars($schema->patternProperties)) as $pattern) { + if (preg_match($this->createPregMatchPattern($pattern), (string) $propertyName)) { + $evaluated[] = $propertyName; + break; + } + } + } + } + + if (property_exists($schema, 'additionalProperties')) { + if ($schema->additionalProperties === true) { + $evaluated = array_merge($evaluated, array_keys($properties)); + } elseif (is_object($schema->additionalProperties)) { + foreach (array_diff(array_keys($properties), $evaluated) as $propertyName) { + $propertyPath = $this->propertyPath($path, $propertyName); + if ($this->schemaIsValid($schema->additionalProperties, $properties[$propertyName], $propertyPath)) { + $evaluated[] = $propertyName; + } + } + } + } + + if (isset($schema->allOf) && is_array($schema->allOf)) { + foreach ($schema->allOf as $branch) { + if (!$this->schemaIsValid($branch, $value, $path)) { + continue; + } + + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value, $path, $visitedRefs)); + } + } + + if (isset($schema->anyOf) && is_array($schema->anyOf)) { + foreach ($schema->anyOf as $branch) { + if (!$this->schemaIsValid($branch, $value, $path)) { + continue; + } + + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value, $path, $visitedRefs)); + } + } + + if (isset($schema->oneOf) && is_array($schema->oneOf)) { + $validBranches = []; + foreach ($schema->oneOf as $branch) { + if ($this->schemaIsValid($branch, $value, $path)) { + $validBranches[] = $branch; + } + } + + if (count($validBranches) === 1) { + $evaluated = array_merge( + $evaluated, + $this->collectEvaluatedProperties($validBranches[0], $value, $path, $visitedRefs) + ); + } + } + + if (property_exists($schema, 'if')) { + $ifMatches = $this->schemaIsValid($schema->if, $value, $path); + if ($ifMatches) { + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->if, $value, $path, $visitedRefs)); + if (property_exists($schema, 'then') && $this->schemaIsValid($schema->then, $value, $path)) { + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->then, $value, $path, $visitedRefs)); + } + } elseif (property_exists($schema, 'else') && $this->schemaIsValid($schema->else, $value, $path)) { + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($schema->else, $value, $path, $visitedRefs)); + } + } + + if (isset($schema->dependentSchemas) && is_object($schema->dependentSchemas)) { + foreach (get_object_vars($schema->dependentSchemas) as $propertyName => $dependentSchema) { + if (!array_key_exists($propertyName, $properties) || !$this->schemaIsValid($dependentSchema, $value, $path)) { + continue; + } + + $evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($dependentSchema, $value, $path, $visitedRefs)); + } + } + + return array_values(array_unique($evaluated)); + } + + /** + * @param mixed $schema + * @param mixed $value + */ + private function schemaIsValid($schema, $value, ?JsonPointer $path = null): bool + { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $schema, $path); + + return $schemaConstraint->isValid(); + } + + private function propertyPath(?JsonPointer $path, string $propertyName): JsonPointer + { + $basePath = $path ?? new JsonPointer(''); + + return $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); + } + + private function createPregMatchPattern(string $pattern): string + { + $pattern = str_replace('\\p{digit}', '\\p{Nd}', $pattern); + $pattern = str_replace('\\p{Letter}', '\\p{L}', $pattern); + + return '/' . str_replace('/', '\\/', $pattern) . '/u'; + } +} diff --git a/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php b/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php index 88a5dd35..1b591dae 100644 --- a/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php +++ b/tests/Constraints/Drafts/Draft2019/UnevaluatedPropertiesTest.php @@ -1 +1,123 @@ -1ëa¡Ñ1qêmŠ‰ËŠw¶®Š^®Øž±7¬¶˜i \ No newline at end of file +id : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; + $schemaStorage->addSchema($id, $schema); + $this->loadRemotesIntoStorage($schemaStorage); + $factory = new Factory($schemaStorage); + $factory->setDefaultDialect($draft->getValue()); + $validator = new Validator($factory); + + $validator->validate($data, $schema, $checkMode); + + self::assertEquals( + $expectedValidationResult, + count($validator->getErrors()) === 0, + $expectedValidationResult ? print_r($validator->getErrors(), true) : 'Validator returned valid but the testcase indicates it is invalid' + ); + } + + public function casesDataProvider(): \Generator + { + $testDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/tests'; + $drafts = array_filter(glob($testDir . '/*'), static function (string $filename) { + return is_dir($filename); + }); + $skippedDrafts = ['draft2020-12', 'draft-next', 'latest', 'v1']; + + foreach ($drafts as $draft) { + $baseDraftName = basename($draft); + if (in_array($baseDraftName, $skippedDrafts, true)) { + continue; + } + + $files = new CallbackFilterIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($draft) + ), + function ($file) { + return $file->isFile() && strtolower($file->getExtension()) === 'json'; + } + ); + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + $contents = json_decode(file_get_contents($file->getPathname()), false); + foreach ($contents as $testCase) { + foreach ($testCase->tests as $test) { + $filename = str_replace('\\', '/', preg_replace('#^.*[/\\\\]tests[/\\\\]#', '', $file->getRealPath())); + $name = sprintf( + '[%s]: %s: %s is expected to be %s', + $filename, + $testCase->description, + $test->description, + $test->valid ? 'valid' : 'invalid' + ); + + if ($this->shouldNotYieldTest($name)) { + continue; + } + + yield $name => [ + 'testCaseDescription' => $testCase->description, + 'testDescription' => $test->description, + 'schema' => $testCase->schema, + 'data' => $test->data, + 'checkMode' => $this->getCheckModeForDraft($baseDraftName), + 'draft' => DraftIdentifiers::fromConstraintName($baseDraftName), + 'expectedValidationResult' => $test->valid, + ]; + } + } + } + } + } + + private function loadRemotesIntoStorage(SchemaStorageInterface $storage): void + { + $remotesDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/remotes'; + + $directory = new \RecursiveDirectoryIterator($remotesDir); + $iterator = new \RecursiveIteratorIterator($directory); + + foreach ($iterator as $info) { + if (!$info->isFile()) { + continue; + } + + $relativePath = str_replace('\\', '/', substr($info->getPathname(), strlen($remotesDir))); + $id = 'http://localhost:1234' . $relativePath; + $storage->addSchema($id, json_decode(file_get_contents($info->getPathname()), false)); + } + } + + private function shouldNotYieldTest(string $name): bool + { + $skip = [ + '[draft4/ref.json]: refs with quote: object with numbers is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: empty tokens in $ref json-pointer: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: base URI change - change folder: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: Location-independent identifier in remote ref: integer is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft6/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft6/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + '[draft6/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + '[draft6/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + '[draft6/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + '[draft6/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Same test case is skipped for draft4, skip for now as well. + // Skipping complex edge cases for now + '[draft6/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', + '[draft6/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', + '[draft6/refRemote.json]: $ref to $ref finds location-independent $id: non-number is invalid is expected to be invalid', + '[draft6/ref.json]: ref overrides any sibling keywords: ref valid, maxItems ignored is expected to be valid', + '[draft6/ref.json]: Reference an anchor with a non-relative URI: mismatch is expected to be invalid', + '[draft6/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', + '[draft6/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', + '[draft6/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', + '[draft6/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', + '[draft6/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', + '[draft6/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', + '[draft6/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', + '[draft6/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', + '[draft6/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', + '[draft7/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', + '[draft7/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', + '[draft7/refRemote.json]: $ref to $ref finds location-independent $id: non-number is invalid is expected to be invalid', + '[draft7/ref.json]: ref overrides any sibling keywords: ref valid, maxItems ignored is expected to be valid', + '[draft7/ref.json]: Reference an anchor with a non-relative URI: mismatch is expected to be invalid', + '[draft7/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', + '[draft7/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', + '[draft7/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', + '[draft7/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', + '[draft7/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', + '[draft7/ref.json]: $id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', + '[draft7/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', + '[draft7/ref.json]: Location-independent identifier: mismatch is expected to be invalid', + '[draft7/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', + '[draft7/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', + // Draft 2019-09 complex constraints, which aren't supported initially + '[draft2019-09/recursiveRef.json]: $recursiveRef without $recursiveAnchor works like $ref: recursive mismatch is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef without using nesting: integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef without using nesting: two levels, no match is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with $recursiveAnchor: false works like $ref: integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with $recursiveAnchor: false works like $ref: two levels, integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor works like $ref: integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor works like $ref: two levels, integer does not match as a property value is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor in the outer schema resource: leaf node does not match: recursion only uses inner schema is expected to be invalid', + '[draft2019-09/recursiveRef.json]: $recursiveRef with no $recursiveAnchor in the initial target schema resource: leaf node does not match: recursion uses the inner schema is expected to be invalid', + '[draft2019-09/recursiveRef.json]: multiple dynamic paths to the $recursiveRef keyword: recurse to integerNode - floats are not allowed is expected to be invalid', + '[draft2019-09/recursiveRef.json]: dynamic $recursiveRef destination (not predictable at schema compile time): integer node is expected to be invalid', + '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: applicator vocabulary still works is expected to be invalid', + '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: no validation: valid number is expected to be valid', + '[draft2019-09/vocabulary.json]: schema that uses custom metaschema with with no validation vocabulary: no validation: invalid number, but it still validates is expected to be valid', + '[draft2019-09/vocabulary.json]: ignore unrecognized optional vocabulary: string value is expected to be invalid', + '[draft2019-09/vocabulary.json]: ignore unrecognized optional vocabulary: number value is expected to be valid', + '[draft2019-09/defs.json]: validate definition against metaschema: invalid definition schema is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name and no ref is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name with absolute URI is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path with absolute URI is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier name with base URI change in subschema is expected to be invalid', + '[draft2019-09/id.json]: Invalid use of fragments in location-independent $id: Identifier path with base URI change in subschema is expected to be invalid', + '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 1st level is invalid is expected to be invalid', + '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 2nd level is invalid is expected to be invalid', + '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 3rd level is invalid is expected to be invalid', + '[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with $recursiveRef: with no unevaluated properties is expected to be valid', // Recursive references are not supported yet. + '[draft2019-09/anchor.json]: Location-independent identifier: mismatch is expected to be invalid', + '[draft2019-09/anchor.json]: Location-independent identifier with absolute URI: mismatch is expected to be invalid', + '[draft2019-09/anchor.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', + '[draft2019-09/anchor.json]: $anchor inside an enum is not a real identifier: in implementations that strip $anchor, this may match either $def is expected to be invalid', + '[draft2019-09/anchor.json]: $anchor inside an enum is not a real identifier: no match on enum or $ref to $anchor is expected to be invalid', + '[draft2019-09/anchor.json]: same $anchor with different base uri: $ref does not resolve to /$defs/A/allOf/0 is expected to be invalid', + '[draft2019-09/ref.json]: ref creates new scope when adjacent to keywords: referenced subschema doesn\'t see annotations from properties is expected to be invalid', + '[draft2019-09/ref.json]: refs with relative uris and defs: invalid on inner field is expected to be invalid', + '[draft2019-09/ref.json]: refs with relative uris and defs: invalid on outer field is expected to be invalid', + '[draft2019-09/ref.json]: relative refs with absolute uris and defs: invalid on inner field is expected to be invalid', + '[draft2019-09/ref.json]: relative refs with absolute uris and defs: invalid on outer field is expected to be invalid', + '[draft2019-09/ref.json]: $id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', + '[draft2019-09/ref.json]: order of evaluation: $id and $ref: data is invalid against first definition is expected to be invalid', + '[draft2019-09/ref.json]: order of evaluation: $id and $anchor and $ref: data is invalid against first definition is expected to be invalid', + '[draft2019-09/ref.json]: simple URN base URI with JSON pointer: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN base URI with NSS: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN base URI with r-component: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN base URI with q-component: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN base URI with URN and anchor ref: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: URN ref with nested pointer ref: a non-string is invalid is expected to be invalid', + '[draft2019-09/ref.json]: ref with absolute-path-reference: an integer is invalid is expected to be invalid', + '[draft2019-09/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches second anyOf, which has a real schema in it is expected to be valid', + '[draft2019-09/unknownKeyword.json]: $id inside an unknown keyword is not a real identifier: type matches non-schema in third anyOf is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems false: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems as schema: with invalid unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with tuple: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with ignored additionalItems: invalid under unevaluatedItems is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with ignored applicator additionalItems: invalid under unevaluatedItems is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with nested tuple: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with nested items: with invalid additional item is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with anyOf: when one schema matches and has unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with anyOf: when two schemas match and has unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with oneOf: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with not: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with if/then/else: when if matches and it has unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with if/then/else: when if doesn\'t match and it has unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with boolean schemas: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems with $ref: with unevaluated items is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems can\'t see inside cousins: always fails is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: item is evaluated in an uncle schema to unevaluatedItems: uncle keyword evaluation is not significant is expected to be invalid', + '[draft2019-09/unevaluatedItems.json]: unevaluatedItems can see annotations from if without then and else: invalid in case if is evaluated is expected to be invalid', + '[draft2019-09/not.json]: collect annotations inside a \'not\', even if collection is disabled: unevaluated property is expected to be valid', + '[draft2019-09/refRemote.json]: anchor within remote ref: remote anchor invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: base URI change - change folder in subschema: string is invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: remote ref with ref to defs: invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', + '[draft2019-09/refRemote.json]: $ref to $ref finds detached $anchor: non-number is invalid is expected to be invalid', + // Optional: bignum — PHP does not natively support arbitrary-precision integers/floats + '[draft3/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', + '[draft3/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', + '[draft3/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft4/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', + '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft3/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + '[draft4/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', + '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft4/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + '[draft6/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft6/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + '[draft7/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft7/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + // Optional: float-overflow — PHP float precision differs from the ECMAScript model + '[draft4/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + '[draft6/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + '[draft7/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + // Optional: ecmascript-regex — PHP uses PCRE which does not implement ECMAScript regex semantics + '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: [^] is a valid regex is expected to be valid', + '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: ECMA 262 has no support for lookbehind is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO (as \u escape) matches is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO matches (unlike e.g. Python) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \W matches everything but ascii letters: latin-1 e-acute matches (unlike e.g. Python) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO (as \u escape) does not match is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO does not match (unlike e.g. Python) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \w matches ascii letters only: latin-1 e-acute does not match (unlike e.g. Python) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \d in pattern matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii digits is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii non-digits is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: ascii digits is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: ascii character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: literal unicode character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode character in hex format in string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode matching is case-sensitive is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: ascii character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: literal unicode character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: unicode character in hex format in string is expected to be valid', + '[draft6/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft6/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft7/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft7/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + // Optional: cross-draft — cross-draft schema resolution ($ref across drafts) is not implemented + '[draft7/optional/cross-draft.json]: refs to future drafts are processed as future drafts: missing bar is invalid is expected to be invalid', + // Optional: idn-email — IDN e-mail format validation is not implemented + '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid e-mail address is expected to be invalid', + '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid idn e-mail address is expected to be invalid', + // Optional: idn-hostname — IDN hostname format validation is not implemented + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, left-to-right chars is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, right-to-left chars is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no other characters is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no following \'l\' is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no preceding \'l\' is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing following is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing preceding is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH JOINER preceded by Virama is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER preceded by Virama is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: contains illegal char U+302E Hangul single dot tone mark is expected to be invalid', + // Optional: iri / iri-reference — IRI format validation is not implemented + '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI Reference is expected to be invalid', + '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI fragment is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI based on IPv6 is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI though valid IRI reference is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid relative IRI Reference is expected to be invalid', + // Optional: regex format — regex format validation does not check for valid ECMA-262 regex syntax + '[draft7/optional/format/regex.json]: validation of regular expressions: a regular expression with unclosed parens is invalid is expected to be invalid', + // Optional: relative-json-pointer — relative JSON pointer format validation is not implemented + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): ## is not a valid json-pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): an invalid RJP that is a valid JSON Pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): empty string is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): explicit positive prefix is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): negative prefix is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus json-pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus octothorpe is expected to be invalid', + ]; + + if ($this->is32Bit()) { + $skip[] = '[draft4/multipleOf.json]: small multiple of large integer: any integer is a multiple of 1e-8 is expected to be valid'; // Test case contains a number which doesn't fit in 32 bits + } + + return in_array($name, $skip, true); + } + + private function is32Bit(): bool + { + return PHP_INT_SIZE === 4; + } + + /** + * @phpstan-return int-mask-of + */ + private function getCheckModeForDraft(string $draft): int + { + switch ($draft) { + case 'draft6': + case 'draft7': + case 'draft2019-09': + return Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_STRICT; + default: + return Constraint::CHECK_MODE_NORMAL; + } + } +}