[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
This commit is contained in:
Nikita Katkov
2024-07-12 21:59:47 +02:00
committed by intellij-monorepo-bot
parent 51749309f5
commit 23fb60afd8
15 changed files with 293 additions and 152 deletions

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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<PsiElement, JsonValidationError> getErrors();
}

View File

@@ -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);
}
}

View File

@@ -42,7 +42,8 @@ public final class JsonSchemaAnnotatorChecker implements JsonValidationHost {
myErrors = new HashMap<>();
}
public Map<PsiElement, JsonValidationError> getErrors() {
@Override
public @NotNull Map<PsiElement, JsonValidationError> 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

View File

@@ -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

View File

@@ -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<JsonValueAdapter>, schema: JsonSchemaObject, consumer: JsonValidationHost) {
override fun validateIndividualItems(instanceArrayItems: List<JsonValueAdapter>, 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<JsonValueAdapter>,
firstRegularItemIndex: Int,
consumer: JsonValidationHost,
errorMessage: @Nls String) {
private fun validateAgainstNonPositionalSchema(
nonPositionalItemsSchema: JsonSchemaObject,
instanceArrayItems: List<JsonValueAdapter>,
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
}
}

View File

@@ -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<JsonValueAdapter> 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<JsonValueAdapter> 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<JsonValueAdapter> 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<JsonValueAdapter> list, JsonSchemaObject schema, JsonValidationHost consumer) {
protected boolean validateIndividualItems(@NotNull List<JsonValueAdapter> 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<JsonValueAdapter> list,
JsonSchemaObject schema,
JsonValidationHost consumer) {
protected static boolean validateArrayLengthHeuristically(@NotNull JsonValueAdapter array,
@NotNull List<JsonValueAdapter> 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<JsonValueAdapter> list,
JsonSchemaObject schema,
JsonValidationHost consumer) {
protected static boolean validateArrayLength(@NotNull JsonValueAdapter array,
@NotNull List<JsonValueAdapter> 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<JsonValueAdapter> 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<JsonValueAdapter> list,
JsonSchemaObject schema,
JsonValidationHost consumer) {
JsonValidationHost consumer,
@NotNull JsonComplianceCheckerOptions options) {
if (schema.isUniqueItems()) {
final MultiMap<String, JsonValueAdapter> 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;
}
}

View File

@@ -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
}
}

View File

@@ -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<Object> 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<String, String, Boolean> 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,

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<JsonPropertyAdapter> propertyList = object.getPropertyList();
final Set<String> 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<String, List<String>> 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,

View File

@@ -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;
}
}

View File

@@ -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;
}
}