From 23fb60afd8efcb968c2e6d6ab5e97c5aec973cc7 Mon Sep 17 00:00:00 2001 From: Nikita Katkov Date: Fri, 12 Jul 2024 21:59:47 +0200 Subject: [PATCH] [json] IJPL-63554 Implemented fast exit for json schema validators - If requested, validation will stop as soon as any error is found. This is extremelly important performance optimisation that plays well with the recenty introduced if-else branch computation. The number of calls to JsonSchemaResolver.isCorrect() increased dramatically, even more json-schema subsystem refactoring was demanded. The existing API didn't assume any kind of laziness or cancellability. The refactoring is performed in a way to cause minimal number of changes in code and API. It'd be great to rewrite the entire validation code to sequence/analogs once and drop complicated JsonAnnotationsCollectionMode GitOrigin-RevId: 4e62f7db76ed6b4071accbe1b80151c4b4664342 --- .../JsonAnnotationsCollectionMode.kt | 6 ++ .../extension/JsonSchemaValidation.java | 17 +++- .../extension/JsonValidationHost.java | 4 + .../impl/JsonComplianceCheckerOptions.java | 16 +++ .../impl/JsonSchemaAnnotatorChecker.java | 13 ++- .../impl/jsonSchemaValidityCache.kt | 9 +- .../versions/v202012/Array2020Validator.kt | 49 +++++----- .../impl/validations/ArrayValidation.java | 98 +++++++++++++------ .../validations/ConstantSchemaValidation.kt | 4 +- .../impl/validations/EnumValidation.java | 17 ++-- .../impl/validations/NotValidation.java | 28 ++++-- .../impl/validations/NumericValidation.java | 70 ++++++++----- .../impl/validations/ObjectValidation.java | 71 +++++++++----- .../impl/validations/StringValidation.java | 31 +++--- .../impl/validations/TypeValidation.java | 12 ++- 15 files changed, 293 insertions(+), 152 deletions(-) create mode 100644 json/src/com/jetbrains/jsonSchema/extension/JsonAnnotationsCollectionMode.kt diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonAnnotationsCollectionMode.kt b/json/src/com/jetbrains/jsonSchema/extension/JsonAnnotationsCollectionMode.kt new file mode 100644 index 000000000000..b239cb94f809 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonAnnotationsCollectionMode.kt @@ -0,0 +1,6 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.jsonSchema.extension + +internal enum class JsonAnnotationsCollectionMode { + FIND_ALL, FIND_FIRST +} \ No newline at end of file diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaValidation.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaValidation.java index 71ac7c0c720a..4b4c134f1c85 100644 --- a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaValidation.java +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaValidation.java @@ -9,9 +9,16 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public interface JsonSchemaValidation { - void validate(@NotNull JsonValueAdapter propValue, - @NotNull JsonSchemaObject schema, - @Nullable JsonSchemaType schemaType, - @NotNull JsonValidationHost consumer, - @NotNull JsonComplianceCheckerOptions options); + /** + * Validates given property adapter against given JSON-schema node considering the validation options. Results are recorded by the provided consumer instance. + * + * @return FALSE if the inspected propValue has errors, TRUE if the propValue is valid. + * The implementations might consider returning the value as soon as the first error is found, or continue processing all the possible errors. + * This behaviour is controlled by the {@link JsonComplianceCheckerOptions#shouldStopValidationAfterAnyErrorFound()} method. + */ + boolean validate(@NotNull JsonValueAdapter propValue, + @NotNull JsonSchemaObject schema, + @Nullable JsonSchemaType schemaType, + @NotNull JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options); } diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonValidationHost.java b/json/src/com/jetbrains/jsonSchema/extension/JsonValidationHost.java index 7386cc7a6f68..8e3793776d2e 100644 --- a/json/src/com/jetbrains/jsonSchema/extension/JsonValidationHost.java +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonValidationHost.java @@ -7,6 +7,8 @@ import com.jetbrains.jsonSchema.impl.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Map; + public interface JsonValidationHost { void error(final String error, final PsiElement holder, JsonErrorPriority priority); void error(final PsiElement newHolder, JsonValidationError error); @@ -29,4 +31,6 @@ public interface JsonValidationHost { void addErrorsFrom(JsonValidationHost otherHost); boolean hasRecordedErrorsFor(@NotNull JsonValueAdapter inspectedValueAdapter); + + @NotNull Map getErrors(); } diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java b/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java index b5772f785c26..3ac4cde4b4bf 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java @@ -1,6 +1,9 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.jsonSchema.impl; +import com.jetbrains.jsonSchema.extension.JsonAnnotationsCollectionMode; +import org.jetbrains.annotations.NotNull; + public final class JsonComplianceCheckerOptions { public static final JsonComplianceCheckerOptions RELAX_ENUM_CHECK = new JsonComplianceCheckerOptions(true, false); @@ -8,6 +11,7 @@ public final class JsonComplianceCheckerOptions { private final boolean isForceStrict; private final boolean isReportMissingOptionalProperties; + private final JsonAnnotationsCollectionMode errorsCollectionMode; public JsonComplianceCheckerOptions(boolean caseInsensitiveEnumCheck) { this(caseInsensitiveEnumCheck, false); @@ -20,6 +24,14 @@ public final class JsonComplianceCheckerOptions { public JsonComplianceCheckerOptions(boolean isCaseInsensitiveEnumCheck, boolean isForceStrict, boolean isReportMissingOptionalProperties) { + this(isCaseInsensitiveEnumCheck, isForceStrict, isReportMissingOptionalProperties, JsonAnnotationsCollectionMode.FIND_ALL); + } + + public JsonComplianceCheckerOptions(boolean isCaseInsensitiveEnumCheck, + boolean isForceStrict, + boolean isReportMissingOptionalProperties, + @NotNull JsonAnnotationsCollectionMode errorsCollectionMode) { + this.errorsCollectionMode = errorsCollectionMode; this.isCaseInsensitiveEnumCheck = isCaseInsensitiveEnumCheck; this.isForceStrict = isForceStrict; this.isReportMissingOptionalProperties = isReportMissingOptionalProperties; @@ -40,4 +52,8 @@ public final class JsonComplianceCheckerOptions { public boolean isReportMissingOptionalProperties() { return isReportMissingOptionalProperties; } + + public boolean shouldStopValidationAfterAnyErrorFound() { + return JsonAnnotationsCollectionMode.FIND_FIRST.equals(errorsCollectionMode); + } } diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java index 612ff3c4f023..734ba8771148 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java @@ -42,7 +42,8 @@ public final class JsonSchemaAnnotatorChecker implements JsonValidationHost { myErrors = new HashMap<>(); } - public Map getErrors() { + @Override + public @NotNull Map getErrors() { return myErrors; } @@ -165,14 +166,18 @@ public final class JsonSchemaAnnotatorChecker implements JsonValidationHost { @Override public boolean isValid() { - return myErrors.size() == 0 && !myHadTypeError; + return myErrors.isEmpty() && !myHadTypeError; } - public void checkByScheme(@NotNull JsonValueAdapter value, @NotNull JsonSchemaObject schema) { + public boolean checkByScheme(@NotNull JsonValueAdapter value, @NotNull JsonSchemaObject schema) { final JsonSchemaType instanceFieldType = JsonSchemaType.getType(value); + + var isValid = true; for (JsonSchemaValidation validation : schema.getValidations(instanceFieldType, value)) { - validation.validate(value, schema, instanceFieldType, this, myOptions); + isValid = validation.validate(value, schema, instanceFieldType, this, myOptions); + if (!isValid && myOptions.shouldStopValidationAfterAnyErrorFound()) return false; } + return isValid; } @Override diff --git a/json/src/com/jetbrains/jsonSchema/impl/jsonSchemaValidityCache.kt b/json/src/com/jetbrains/jsonSchema/impl/jsonSchemaValidityCache.kt index 4dd29bebacfb..cf0f5ca9ec2e 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/jsonSchemaValidityCache.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/jsonSchemaValidityCache.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.util.Key import com.intellij.psi.util.CachedValue import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager +import com.jetbrains.jsonSchema.extension.JsonAnnotationsCollectionMode import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter import com.jetbrains.jsonSchema.ide.JsonSchemaService import java.util.concurrent.ConcurrentHashMap @@ -32,7 +33,13 @@ internal fun getOrComputeAdapterValidityAgainstGivenSchema(value: JsonValueAdapt return cachedValue } - val checker = JsonSchemaAnnotatorChecker(value.delegate.project, JsonComplianceCheckerOptions.RELAX_ENUM_CHECK) + val checker = JsonSchemaAnnotatorChecker( + value.delegate.project, + JsonComplianceCheckerOptions(false, + false, + false, + JsonAnnotationsCollectionMode.FIND_FIRST) + ) checker.checkByScheme(value, schema) val computedValue = checker.isCorrect diff --git a/json/src/com/jetbrains/jsonSchema/impl/light/versions/v202012/Array2020Validator.kt b/json/src/com/jetbrains/jsonSchema/impl/light/versions/v202012/Array2020Validator.kt index 45d8bf0d331f..252609f6ef9b 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/light/versions/v202012/Array2020Validator.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/light/versions/v202012/Array2020Validator.kt @@ -7,62 +7,67 @@ import com.jetbrains.jsonSchema.extension.JsonValidationHost import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter import com.jetbrains.jsonSchema.impl.JsonComplianceCheckerOptions import com.jetbrains.jsonSchema.impl.JsonSchemaObject -import com.jetbrains.jsonSchema.impl.JsonSchemaType import com.jetbrains.jsonSchema.impl.validations.ArrayValidation import org.jetbrains.annotations.Nls internal object Array2020Validator : ArrayValidation() { - override fun validate(valueAdapter: JsonValueAdapter, schema: JsonSchemaObject, schemaType: JsonSchemaType?, consumer: JsonValidationHost, options: JsonComplianceCheckerOptions) { - val arrayItems = valueAdapter.getAsArray()?.elements ?: return - - validateUniqueItems(valueAdapter, arrayItems, schema, consumer) - validateAgainstContainsSchema(valueAdapter, arrayItems, schema, consumer, options) - validateIndividualItems(arrayItems, schema, consumer) - validateArrayLength(valueAdapter, arrayItems, schema, consumer) - validateArrayLengthHeuristically(valueAdapter, arrayItems, schema, consumer) - } - - private fun validateIndividualItems(instanceArrayItems: List, schema: JsonSchemaObject, consumer: JsonValidationHost) { + override fun validateIndividualItems(instanceArrayItems: List, schema: JsonSchemaObject, consumer: JsonValidationHost, options: JsonComplianceCheckerOptions): Boolean { val additionalItemsSchemaList = schema.itemsSchemaList val firstRegularItemIndex = if (additionalItemsSchemaList.isNullOrEmpty()) 0 else additionalItemsSchemaList.size + var isValid = true + // check instance items with positional schema for (index in 0 until firstRegularItemIndex) { val positionalSchema = additionalItemsSchemaList?.get(index) ?: break val inspectedInstanceItem = instanceArrayItems.getOrNull(index) ?: break consumer.checkObjectBySchemaRecordErrors(positionalSchema, inspectedInstanceItem) + + isValid = isValid && consumer.errors.isEmpty() + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false } // check the rest of instance items with regular schema val additionalItemsSchema = schema.additionalItemsSchema if (additionalItemsSchema != null) { - validateAgainstNonPositionalSchema(additionalItemsSchema, instanceArrayItems, firstRegularItemIndex, consumer, JsonBundle.message("schema.validation.array.no.extra")) - return + isValid = isValid && validateAgainstNonPositionalSchema(additionalItemsSchema, instanceArrayItems, firstRegularItemIndex, consumer, options, JsonBundle.message("schema.validation.array.no.extra")) + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false } val unevaluatedItemsSchema = schema.unevaluatedItemsSchema if (unevaluatedItemsSchema != null) { - validateAgainstNonPositionalSchema(unevaluatedItemsSchema, instanceArrayItems, firstRegularItemIndex, consumer, JsonBundle.message("schema.validation.array.no.unevaluated")) + isValid = isValid && validateAgainstNonPositionalSchema(unevaluatedItemsSchema, instanceArrayItems, firstRegularItemIndex, consumer, options, JsonBundle.message("schema.validation.array.no.unevaluated")) + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false } + + return isValid } - private fun validateAgainstNonPositionalSchema(nonPositionalItemsSchema: JsonSchemaObject, - instanceArrayItems: List, - firstRegularItemIndex: Int, - consumer: JsonValidationHost, - errorMessage: @Nls String) { + private fun validateAgainstNonPositionalSchema( + nonPositionalItemsSchema: JsonSchemaObject, + instanceArrayItems: List, + firstRegularItemIndex: Int, + consumer: JsonValidationHost, + options: JsonComplianceCheckerOptions, + errorMessage: @Nls String, + ): Boolean { if (nonPositionalItemsSchema.constantSchema == true) { - return + return true } if (nonPositionalItemsSchema.constantSchema == false && instanceArrayItems.getOrNull(firstRegularItemIndex) != null) { consumer.error(errorMessage, instanceArrayItems[firstRegularItemIndex].delegate, JsonErrorPriority.LOW_PRIORITY) - return + return false } + var isValid = true for (index in firstRegularItemIndex until instanceArrayItems.size) { val instanceArrayItem = instanceArrayItems.getOrNull(index) ?: break consumer.checkObjectBySchemaRecordErrors(nonPositionalItemsSchema, instanceArrayItem) + + isValid = isValid && consumer.errors.isEmpty() + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false } + return isValid } } \ No newline at end of file diff --git a/json/src/com/jetbrains/jsonSchema/impl/validations/ArrayValidation.java b/json/src/com/jetbrains/jsonSchema/impl/validations/ArrayValidation.java index d9d24cc4b1b7..abe017ad022e 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/validations/ArrayValidation.java +++ b/json/src/com/jetbrains/jsonSchema/impl/validations/ArrayValidation.java @@ -22,40 +22,56 @@ import java.util.Map; public class ArrayValidation implements JsonSchemaValidation { public static final ArrayValidation INSTANCE = new ArrayValidation(); @Override - public void validate(@NotNull JsonValueAdapter propValue, - @NotNull JsonSchemaObject schema, - @Nullable JsonSchemaType schemaType, - @NotNull JsonValidationHost consumer, - @NotNull JsonComplianceCheckerOptions options) { - checkArray(propValue, schema, consumer, options); + public boolean validate(@NotNull JsonValueAdapter propValue, + @NotNull JsonSchemaObject schema, + @Nullable JsonSchemaType schemaType, + @NotNull JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { + return checkArray(propValue, schema, consumer, options); } - private static void checkArray(JsonValueAdapter value, + private boolean checkArray(JsonValueAdapter value, JsonSchemaObject schema, JsonValidationHost consumer, JsonComplianceCheckerOptions options) { final JsonArrayValueAdapter asArray = value.getAsArray(); - if (asArray == null) return; + if (asArray == null) return true; final List elements = asArray.getElements(); - checkArrayItems(value, elements, schema, consumer, options); + return checkArrayItems(value, elements, schema, consumer, options); } - private static void checkArrayItems(@NotNull JsonValueAdapter array, - final @NotNull List list, - final JsonSchemaObject schema, - JsonValidationHost consumer, - JsonComplianceCheckerOptions options) { - validateUniqueItems(array, list, schema, consumer); - validateAgainstContainsSchema(array, list, schema, consumer, options); - validateIndividualItems(list, schema, consumer); - validateArrayLength(array, list, schema, consumer); - validateArrayLengthHeuristically(array, list, schema, consumer); + protected boolean checkArrayItems(@NotNull JsonValueAdapter array, + final @NotNull List list, + final JsonSchemaObject schema, + JsonValidationHost consumer, + JsonComplianceCheckerOptions options) { + if (options.shouldStopValidationAfterAnyErrorFound()) { + return validateUniqueItems(array, list, schema, consumer, options) && + validateAgainstContainsSchema(array, list, schema, consumer, options) && + validateIndividualItems(list, schema, consumer, options) && + validateArrayLength(array, list, schema, consumer, options) && + validateArrayLengthHeuristically(array, list, schema, consumer, options); + } + else { + return validateUniqueItems(array, list, schema, consumer, options) & + validateAgainstContainsSchema(array, list, schema, consumer, options) & + validateIndividualItems(list, schema, consumer, options) & + validateArrayLength(array, list, schema, consumer, options) & + validateArrayLengthHeuristically(array, list, schema, consumer, options); + } } - private static void validateIndividualItems(@NotNull List list, JsonSchemaObject schema, JsonValidationHost consumer) { + protected boolean validateIndividualItems(@NotNull List list, + JsonSchemaObject schema, + JsonValidationHost consumer, + JsonComplianceCheckerOptions options) { + var isValid = true; + if (schema.getItemsSchema() != null) { for (JsonValueAdapter item : list) { consumer.checkObjectBySchemaRecordErrors(schema.getItemsSchema(), item); + isValid &= consumer.getErrors().isEmpty(); + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false; } } else if (schema.getItemsSchemaList() != null) { @@ -63,45 +79,60 @@ public class ArrayValidation implements JsonSchemaValidation { for (JsonValueAdapter arrayValue : list) { if (iterator.hasNext()) { consumer.checkObjectBySchemaRecordErrors(iterator.next(), arrayValue); + isValid &= consumer.getErrors().isEmpty(); + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false; } else { if (!Boolean.TRUE.equals(schema.getAdditionalItemsAllowed())) { consumer.error(JsonBundle.message("schema.validation.array.no.extra"), arrayValue.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } else if (schema.getAdditionalItemsSchema() != null) { consumer.checkObjectBySchemaRecordErrors(schema.getAdditionalItemsSchema(), arrayValue); + isValid &= consumer.getErrors().isEmpty(); + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false; } } } } + + return isValid; } - protected static void validateArrayLengthHeuristically(@NotNull JsonValueAdapter array, - @NotNull List list, - JsonSchemaObject schema, - JsonValidationHost consumer) { + protected static boolean validateArrayLengthHeuristically(@NotNull JsonValueAdapter array, + @NotNull List list, + JsonSchemaObject schema, + JsonValidationHost consumer, + JsonComplianceCheckerOptions options) { // these two are not correct by the schema spec, but are used in some schemas if (schema.getMinLength() != null && list.size() < schema.getMinLength()) { consumer.error(JsonBundle.message("schema.validation.array.shorter.than", schema.getMinLength()), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + return false; } if (schema.getMaxLength() != null && list.size() > schema.getMaxLength()) { consumer.error(JsonBundle.message("schema.validation.array.longer.than", schema.getMaxLength()), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + return false; } + return true; } - protected static void validateArrayLength(@NotNull JsonValueAdapter array, - @NotNull List list, - JsonSchemaObject schema, - JsonValidationHost consumer) { + protected static boolean validateArrayLength(@NotNull JsonValueAdapter array, + @NotNull List list, + JsonSchemaObject schema, + JsonValidationHost consumer, JsonComplianceCheckerOptions options) { if (schema.getMinItems() != null && list.size() < schema.getMinItems()) { consumer.error(JsonBundle.message("schema.validation.array.shorter.than", schema.getMinItems()), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + return false; } if (schema.getMaxItems() != null && list.size() > schema.getMaxItems()) { consumer.error(JsonBundle.message("schema.validation.array.longer.than", schema.getMaxItems()), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + return false; } + return true; } - protected static void validateAgainstContainsSchema(@NotNull JsonValueAdapter array, + protected static boolean validateAgainstContainsSchema(@NotNull JsonValueAdapter array, @NotNull List list, JsonSchemaObject schema, JsonValidationHost consumer, @@ -117,14 +148,17 @@ public class ArrayValidation implements JsonSchemaValidation { } if (!match) { consumer.error(JsonBundle.message("schema.validation.array.not.contains"), array.getDelegate(), JsonErrorPriority.MEDIUM_PRIORITY); + return false; } } + return true; } - protected static void validateUniqueItems(@NotNull JsonValueAdapter array, + protected static boolean validateUniqueItems(@NotNull JsonValueAdapter array, @NotNull List list, JsonSchemaObject schema, - JsonValidationHost consumer) { + JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { if (schema.isUniqueItems()) { final MultiMap valueTexts = new MultiMap<>(); final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(array.getDelegate(), schema); @@ -138,9 +172,11 @@ public class ArrayValidation implements JsonSchemaValidation { for (JsonValueAdapter item: entry.getValue()) { if (!item.shouldCheckAsValue()) continue; consumer.error(JsonBundle.message("schema.validation.not.unique"), item.getDelegate(), JsonErrorPriority.TYPE_MISMATCH); + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } } } + return true; } } diff --git a/json/src/com/jetbrains/jsonSchema/impl/validations/ConstantSchemaValidation.kt b/json/src/com/jetbrains/jsonSchema/impl/validations/ConstantSchemaValidation.kt index 789035b4c46b..07d6fc6a1dc0 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/validations/ConstantSchemaValidation.kt +++ b/json/src/com/jetbrains/jsonSchema/impl/validations/ConstantSchemaValidation.kt @@ -11,9 +11,11 @@ import com.jetbrains.jsonSchema.impl.JsonSchemaObject import com.jetbrains.jsonSchema.impl.JsonSchemaType internal object ConstantSchemaValidation: JsonSchemaValidation { - override fun validate(propValue: JsonValueAdapter, schema: JsonSchemaObject, schemaType: JsonSchemaType?, consumer: JsonValidationHost, options: JsonComplianceCheckerOptions) { + override fun validate(propValue: JsonValueAdapter, schema: JsonSchemaObject, schemaType: JsonSchemaType?, consumer: JsonValidationHost, options: JsonComplianceCheckerOptions): Boolean { if (schema.constantSchema == false) { consumer.error(JsonBundle.message("schema.validation.constant.schema"), propValue.delegate.parent, JsonErrorPriority.LOW_PRIORITY) + return false } + return true } } \ No newline at end of file diff --git a/json/src/com/jetbrains/jsonSchema/impl/validations/EnumValidation.java b/json/src/com/jetbrains/jsonSchema/impl/validations/EnumValidation.java index 13ca1e0b61b9..3fac218a0ed3 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/validations/EnumValidation.java +++ b/json/src/com/jetbrains/jsonSchema/impl/validations/EnumValidation.java @@ -24,25 +24,26 @@ import static com.jetbrains.jsonSchema.impl.light.SchemaKeywordsKt.X_INTELLIJ_CA public final class EnumValidation implements JsonSchemaValidation { public static final EnumValidation INSTANCE = new EnumValidation(); @Override - public void validate(@NotNull JsonValueAdapter propValue, - @NotNull JsonSchemaObject schema, - @Nullable JsonSchemaType schemaType, - @NotNull JsonValidationHost consumer, - @NotNull JsonComplianceCheckerOptions options) { + public boolean validate(@NotNull JsonValueAdapter propValue, + @NotNull JsonSchemaObject schema, + @Nullable JsonSchemaType schemaType, + @NotNull JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { List enumItems = schema.getEnum(); - if (enumItems == null) return; + if (enumItems == null) return true; final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(propValue.getDelegate(), schema); - if (walker == null) return; + if (walker == null) return true; final String text = StringUtil.notNullize(walker.getNodeTextForValidation(propValue.getDelegate())); boolean caseInsensitive = Boolean.parseBoolean(schema.readChildNodeValue(X_INTELLIJ_CASE_INSENSITIVE)) || schema.isForceCaseInsensitive(); BiFunction eq = options.isCaseInsensitiveEnumCheck() || caseInsensitive ? String::equalsIgnoreCase : String::equals; for (Object object : enumItems) { - if (checkEnumValue(object, walker, propValue, text, eq)) return; + if (checkEnumValue(object, walker, propValue, text, eq)) return true; } consumer.error(JsonBundle.message("schema.validation.enum.mismatch", StringUtil.join(enumItems, o -> o.toString(), ", ")), propValue.getDelegate(), JsonValidationError.FixableIssueKind.NonEnumValue, null, JsonErrorPriority.MEDIUM_PRIORITY); + return false; } private static boolean checkEnumValue(@NotNull Object object, diff --git a/json/src/com/jetbrains/jsonSchema/impl/validations/NotValidation.java b/json/src/com/jetbrains/jsonSchema/impl/validations/NotValidation.java index 4fa2dabf5e86..b8362444a0d5 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/validations/NotValidation.java +++ b/json/src/com/jetbrains/jsonSchema/impl/validations/NotValidation.java @@ -2,6 +2,7 @@ package com.jetbrains.jsonSchema.impl.validations; import com.intellij.json.JsonBundle; +import com.jetbrains.jsonSchema.extension.JsonAnnotationsCollectionMode; import com.jetbrains.jsonSchema.extension.JsonErrorPriority; import com.jetbrains.jsonSchema.extension.JsonSchemaValidation; import com.jetbrains.jsonSchema.extension.JsonValidationHost; @@ -18,20 +19,29 @@ import java.util.Collection; public final class NotValidation implements JsonSchemaValidation { public static final NotValidation INSTANCE = new NotValidation(); @Override - public void validate(@NotNull JsonValueAdapter propValue, - @NotNull JsonSchemaObject schema, - @Nullable JsonSchemaType schemaType, - @NotNull JsonValidationHost consumer, - @NotNull JsonComplianceCheckerOptions options) { + public boolean validate(@NotNull JsonValueAdapter propValue, + @NotNull JsonSchemaObject schema, + @Nullable JsonSchemaType schemaType, + @NotNull JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { final MatchResult result = consumer.resolve(schema.getNot(), propValue); - if (result.mySchemas.isEmpty() && result.myExcludingSchemas.isEmpty()) return; + if (result.mySchemas.isEmpty() && result.myExcludingSchemas.isEmpty()) return true; // if 'not' uses reference to owning schema back -> do not check, seems it does not make any sense if (result.mySchemas.stream().anyMatch(s -> schema.equals(s)) || result.myExcludingSchemas.stream().flatMap(Collection::stream) - .anyMatch(s -> schema.equals(s))) return; + .anyMatch(s -> schema.equals(s))) return true; - final JsonValidationHost checker = consumer.checkByMatchResult(propValue, result, options.withForcedStrict()); - if (checker == null || checker.isValid()) consumer.error(JsonBundle.message("schema.validation.against.not"), propValue.getDelegate(), JsonErrorPriority.NOT_SCHEMA); + final JsonValidationHost checker = + consumer.checkByMatchResult(propValue, + result, + new JsonComplianceCheckerOptions(options.isCaseInsensitiveEnumCheck(), true, false, + JsonAnnotationsCollectionMode.FIND_FIRST)); + if (checker == null || checker.isValid()) { + consumer.error(JsonBundle.message("schema.validation.against.not"), propValue.getDelegate(), JsonErrorPriority.NOT_SCHEMA); + return false; + } + + return true; } } diff --git a/json/src/com/jetbrains/jsonSchema/impl/validations/NumericValidation.java b/json/src/com/jetbrains/jsonSchema/impl/validations/NumericValidation.java index 3f33274d4818..3f4bbf953256 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/validations/NumericValidation.java +++ b/json/src/com/jetbrains/jsonSchema/impl/validations/NumericValidation.java @@ -13,20 +13,21 @@ import org.jetbrains.annotations.Nullable; public final class NumericValidation implements JsonSchemaValidation { public static final NumericValidation INSTANCE = new NumericValidation(); - private static void checkNumber(PsiElement propValue, - JsonSchemaObject schema, - JsonSchemaType schemaType, - JsonValidationHost consumer) { + private static boolean checkNumber(PsiElement propValue, + JsonSchemaObject schema, + JsonSchemaType schemaType, + JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { Number value; String valueText = JsonSchemaAnnotatorChecker.getValue(propValue, schema); - if (valueText == null) return; + if (valueText == null) return true; if (JsonSchemaType._integer.equals(schemaType)) { value = JsonSchemaType.getIntegerValue(valueText); if (value == null) { consumer.error(JsonBundle.message("schema.validation.integer.expected"), propValue, JsonValidationError.FixableIssueKind.TypeMismatch, new JsonValidationError.TypeMismatchIssueData(new JsonSchemaType[]{schemaType}), JsonErrorPriority.TYPE_MISMATCH); - return; + return false; } } else { @@ -38,8 +39,9 @@ public final class NumericValidation implements JsonSchemaValidation { consumer.error(JsonBundle.message("schema.validation.number.expected"), propValue, JsonValidationError.FixableIssueKind.TypeMismatch, new JsonValidationError.TypeMismatchIssueData(new JsonSchemaType[]{schemaType}), JsonErrorPriority.TYPE_MISMATCH); + return false; } - return; + return true; } } final Number multipleOf = schema.getMultipleOf(); @@ -49,77 +51,95 @@ public final class NumericValidation implements JsonSchemaValidation { final String multipleOfValue = String.valueOf(Math.abs(multipleOf.doubleValue() - multipleOf.intValue()) < 0.000001 ? multipleOf.intValue() : multipleOf); consumer.error(JsonBundle.message("schema.validation.not.multiple.of", multipleOfValue), propValue, JsonErrorPriority.LOW_PRIORITY); - return; + return false; } } - checkMinimum(schema, value, propValue, consumer); - checkMaximum(schema, value, propValue, consumer); + return checkMinimum(schema, value, propValue, consumer, options) & + checkMaximum(schema, value, propValue, consumer, options); } - private static void checkMaximum(JsonSchemaObject schema, + private static boolean checkMaximum(JsonSchemaObject schema, Number value, PsiElement propertyValue, - JsonValidationHost consumer) { - + JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { + var isValid = true; Number exclusiveMaximumNumber = schema.getExclusiveMaximumNumber(); if (exclusiveMaximumNumber != null) { final double doubleValue = exclusiveMaximumNumber.doubleValue(); if (value.doubleValue() >= doubleValue) { consumer.error(JsonBundle.message("schema.validation.greater.than.exclusive.maximum", exclusiveMaximumNumber), propertyValue, JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } Number maximum = schema.getMaximum(); - if (maximum == null) return; + if (maximum == null) return isValid; boolean isExclusive = Boolean.TRUE.equals(schema.isExclusiveMaximum()); final double doubleValue = maximum.doubleValue(); if (isExclusive) { if (value.doubleValue() >= doubleValue) { consumer.error(JsonBundle.message("schema.validation.greater.than.exclusive.maximum", maximum), propertyValue, JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } else { if (value.doubleValue() > doubleValue) { consumer.error(JsonBundle.message("schema.validation.greater.than.maximum", maximum), propertyValue, JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } + + return isValid; } - private static void checkMinimum(JsonSchemaObject schema, - Number value, - PsiElement propertyValue, - JsonValidationHost consumer) { + private static boolean checkMinimum(JsonSchemaObject schema, + Number value, + PsiElement propertyValue, + JsonValidationHost consumer, @NotNull JsonComplianceCheckerOptions options) { + var isValid = true; // schema v6 - exclusiveMinimum is numeric now Number exclusiveMinimumNumber = schema.getExclusiveMinimumNumber(); if (exclusiveMinimumNumber != null) { final double doubleValue = exclusiveMinimumNumber.doubleValue(); if (value.doubleValue() <= doubleValue) { consumer.error(JsonBundle.message("schema.validation.less.than.exclusive.minimum", exclusiveMinimumNumber), propertyValue, JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } Number minimum = schema.getMinimum(); - if (minimum == null) return; + if (minimum == null) return isValid; boolean isExclusive = Boolean.TRUE.equals(schema.isExclusiveMinimum()); final double doubleValue = minimum.doubleValue(); if (isExclusive) { if (value.doubleValue() <= doubleValue) { consumer.error(JsonBundle.message("schema.validation.less.than.exclusive.minimum", minimum), propertyValue, JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } else { if (value.doubleValue() < doubleValue) { consumer.error(JsonBundle.message("schema.validation.less.than.minimum", minimum), propertyValue, JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } + + return isValid; } @Override - public void validate(@NotNull JsonValueAdapter propValue, - @NotNull JsonSchemaObject schema, - @Nullable JsonSchemaType schemaType, - @NotNull JsonValidationHost consumer, - @NotNull JsonComplianceCheckerOptions options) { - checkNumber(propValue.getDelegate(), schema, schemaType, consumer); + public boolean validate(@NotNull JsonValueAdapter propValue, + @NotNull JsonSchemaObject schema, + @Nullable JsonSchemaType schemaType, + @NotNull JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { + return checkNumber(propValue.getDelegate(), schema, schemaType, consumer, options); } } diff --git a/json/src/com/jetbrains/jsonSchema/impl/validations/ObjectValidation.java b/json/src/com/jetbrains/jsonSchema/impl/validations/ObjectValidation.java index 4f6b751d7cb3..56b06a3f8984 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/validations/ObjectValidation.java +++ b/json/src/com/jetbrains/jsonSchema/impl/validations/ObjectValidation.java @@ -30,19 +30,22 @@ public final class ObjectValidation implements JsonSchemaValidation { public static final ObjectValidation INSTANCE = new ObjectValidation(); @Override - public void validate(@NotNull JsonValueAdapter propValue, - @NotNull JsonSchemaObject schema, - @Nullable JsonSchemaType schemaType, - @NotNull JsonValidationHost consumer, - @NotNull JsonComplianceCheckerOptions options) { - checkObject(propValue, schema, consumer, options); + public boolean validate(@NotNull JsonValueAdapter propValue, + @NotNull JsonSchemaObject schema, + @Nullable JsonSchemaType schemaType, + @NotNull JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { + return checkObject(propValue, schema, consumer, options); } - private static void checkObject(@NotNull JsonValueAdapter value, - @NotNull JsonSchemaObject schema, - JsonValidationHost consumer, JsonComplianceCheckerOptions options) { + private static boolean checkObject(@NotNull JsonValueAdapter value, + @NotNull JsonSchemaObject schema, + JsonValidationHost consumer, + JsonComplianceCheckerOptions options) { final JsonObjectValueAdapter object = value.getAsObject(); - if (object == null) return; + if (object == null) return true; + + var isValid = true; final List propertyList = object.getPropertyList(); final Set set = new HashSet<>(); @@ -56,6 +59,8 @@ public final class ObjectValidation implements JsonSchemaValidation { nameValueAdapter), options); if (checker != null) { consumer.addErrorsFrom(checker); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } } @@ -66,10 +71,14 @@ public final class ObjectValidation implements JsonSchemaValidation { consumer.error(JsonBundle.message("json.schema.annotation.not.allowed.property", name), property.getDelegate(), JsonValidationError.FixableIssueKind.ProhibitedProperty, new JsonValidationError.ProhibitedPropertyIssueData(name), JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } else if (ThreeState.UNSURE.equals(pair.getFirst()) && pair.second.getConstantSchema() == null) { for (JsonValueAdapter propertyValue : property.getValues()) { consumer.checkObjectBySchemaRecordErrors(pair.getSecond(), propertyValue); + isValid &= consumer.getErrors().isEmpty(); + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false; } } set.add(name); @@ -86,15 +95,21 @@ public final class ObjectValidation implements JsonSchemaValidation { consumer.error(JsonBundle.message("schema.validation.missing.required.property.or.properties", data.getMessage(false)), value.getDelegate(), JsonValidationError.FixableIssueKind.MissingProperty, data, JsonErrorPriority.MISSING_PROPS); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } if (schema.getMinProperties() != null && propertyList.size() < schema.getMinProperties()) { consumer.error(JsonBundle.message("schema.validation.number.of.props.less.than", schema.getMinProperties()), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } if (schema.getMaxProperties() != null && propertyList.size() > schema.getMaxProperties()) { consumer.error(JsonBundle.message("schema.validation.number.of.props.greater.than", schema.getMaxProperties()), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } final Map> dependencies = schema.getPropertyDependencies(); if (dependencies != null) { @@ -110,32 +125,35 @@ public final class ObjectValidation implements JsonSchemaValidation { value.getDelegate(), JsonValidationError.FixableIssueKind.MissingProperty, data, JsonErrorPriority.MISSING_PROPS); + isValid = false; + if (options.shouldStopValidationAfterAnyErrorFound()) return false; } } } } - final var schemaDependencies = schema.getSchemaDependencyNames(); - StreamEx.of(schemaDependencies) - .forEach(name -> { - var dependency = schema.getSchemaDependencyByName(name); - if (set.contains(name) && dependency != null) { - consumer.checkObjectBySchemaRecordErrors(dependency, value); - } - }); - - reportUnevaluatedPropertiesSchemaViolation(consumer, schema, object); + for (String name : StreamEx.of(schema.getSchemaDependencyNames())) { + var dependency = schema.getSchemaDependencyByName(name); + if (set.contains(name) && dependency != null) { + consumer.checkObjectBySchemaRecordErrors(dependency, value); + isValid &= consumer.getErrors().isEmpty(); + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false; + } + } } + return checkUnevaluatedPropertiesSchemaViolation(consumer, schema, object, options); } - private static void reportUnevaluatedPropertiesSchemaViolation(@NotNull JsonValidationHost consumer, - @NotNull JsonSchemaObject schemaNode, - @NotNull JsonObjectValueAdapter inspectedObject) { + private static boolean checkUnevaluatedPropertiesSchemaViolation(@NotNull JsonValidationHost consumer, + @NotNull JsonSchemaObject schemaNode, + @NotNull JsonObjectValueAdapter inspectedObject, + @NotNull JsonComplianceCheckerOptions options) { var unevaluatedPropertiesSchema = schemaNode.getUnevaluatedPropertiesSchema(); - if (unevaluatedPropertiesSchema == null) return; + if (unevaluatedPropertiesSchema == null) return true; var constantSchemaValue = unevaluatedPropertiesSchema.getConstantSchema(); - if (Boolean.TRUE.equals(constantSchemaValue)) return; + if (Boolean.TRUE.equals(constantSchemaValue)) return true; + var isValid = true; for (JsonPropertyAdapter childPropertyAdapter : inspectedObject.getPropertyList()) { if (isCoveredByAdjacentSchemas(consumer, childPropertyAdapter, schemaNode)) { continue; @@ -145,7 +163,10 @@ public final class ObjectValidation implements JsonSchemaValidation { if (childPropertyNameAdapter == null) continue; consumer.checkObjectBySchemaRecordErrors(unevaluatedPropertiesSchema, childPropertyNameAdapter); + isValid &= consumer.getErrors().isEmpty(); + if (!isValid && options.shouldStopValidationAfterAnyErrorFound()) return false; } + return isValid; } private static boolean isCoveredByAdjacentSchemas(@NotNull JsonValidationHost validationHost, diff --git a/json/src/com/jetbrains/jsonSchema/impl/validations/StringValidation.java b/json/src/com/jetbrains/jsonSchema/impl/validations/StringValidation.java index c0e7f5596f52..459b00a2790b 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/validations/StringValidation.java +++ b/json/src/com/jetbrains/jsonSchema/impl/validations/StringValidation.java @@ -19,45 +19,44 @@ import static com.jetbrains.jsonSchema.impl.JsonSchemaAnnotatorChecker.getValue; public final class StringValidation implements JsonSchemaValidation { public static final StringValidation INSTANCE = new StringValidation(); @Override - public void validate(@NotNull JsonValueAdapter propValue, - @NotNull JsonSchemaObject schema, - @Nullable JsonSchemaType schemaType, - @NotNull JsonValidationHost consumer, - @NotNull JsonComplianceCheckerOptions options) { - checkString(propValue.getDelegate(), schema, consumer); + public boolean validate(@NotNull JsonValueAdapter propValue, + @NotNull JsonSchemaObject schema, + @Nullable JsonSchemaType schemaType, + @NotNull JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { + return checkString(propValue.getDelegate(), schema, consumer, options); } - private static void checkString(PsiElement propValue, + private static boolean checkString(PsiElement propValue, JsonSchemaObject schema, - JsonValidationHost consumer) { + JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { String v = getValue(propValue, schema); - if (v == null) return; + if (v == null) return true; final String value = StringUtil.unquoteString(v); if (schema.getMinLength() != null) { if (value.length() < schema.getMinLength()) { consumer.error(JsonBundle.message("schema.validation.string.shorter.than", schema.getMinLength()), propValue, JsonErrorPriority.LOW_PRIORITY); - return; + return false; } } if (schema.getMaxLength() != null) { if (value.length() > schema.getMaxLength()) { consumer.error(JsonBundle.message("schema.validation.string.longer.than", schema.getMaxLength()), propValue, JsonErrorPriority.LOW_PRIORITY); - return; + return false; } } if (schema.getPattern() != null) { if (schema.getPatternError() != null) { consumer.error(JsonBundle.message("schema.validation.invalid.string.pattern", StringUtil.convertLineSeparators(schema.getPatternError())), propValue, JsonErrorPriority.LOW_PRIORITY); + return false; } if (!schema.checkByPattern(value)) { consumer.error(JsonBundle.message("schema.validation.string.violates.pattern", StringUtil.convertLineSeparators(schema.getPattern())), propValue, JsonErrorPriority.LOW_PRIORITY); + return false; } } - // I think we are not gonna to support format, there are a couple of RFCs there to check upon.. - /* - if (schema.getFormat() != null) { - LOG.info("Unsupported property used: 'format'"); - }*/ + return true; } } diff --git a/json/src/com/jetbrains/jsonSchema/impl/validations/TypeValidation.java b/json/src/com/jetbrains/jsonSchema/impl/validations/TypeValidation.java index 382978b1c5e7..2a88b8413b9a 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/validations/TypeValidation.java +++ b/json/src/com/jetbrains/jsonSchema/impl/validations/TypeValidation.java @@ -16,14 +16,16 @@ import java.util.Collections; public final class TypeValidation implements JsonSchemaValidation { public static final TypeValidation INSTANCE = new TypeValidation(); @Override - public void validate(@NotNull JsonValueAdapter propValue, - @NotNull JsonSchemaObject schema, - @Nullable JsonSchemaType schemaType, - @NotNull JsonValidationHost consumer, - @NotNull JsonComplianceCheckerOptions options) { + public boolean validate(@NotNull JsonValueAdapter propValue, + @NotNull JsonSchemaObject schema, + @Nullable JsonSchemaType schemaType, + @NotNull JsonValidationHost consumer, + @NotNull JsonComplianceCheckerOptions options) { JsonSchemaType otherType = JsonSchemaAnnotatorChecker.getMatchingSchemaType(schema, schemaType); if (otherType != null && !otherType.equals(schemaType) && !otherType.equals(propValue.getAlternateType(schemaType))) { consumer.typeError(propValue.getDelegate(), propValue.getAlternateType(schemaType), JsonSchemaAnnotatorChecker.getExpectedTypes(Collections.singleton(schema))); + return false; } + return true; } }