RUBY-22379 YAML Schema Validation should support anchors and references

This commit is contained in:
Anton Lobov
2018-09-06 18:10:56 +02:00
parent bbede64a84
commit 9d65c2b37b
10 changed files with 218 additions and 53 deletions
@@ -3,6 +3,7 @@ package com.jetbrains.jsonSchema.extension;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.util.ThreeState;
import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
@@ -45,6 +46,10 @@ public interface JsonLikePsiWalker {
@Nullable
JsonValueAdapter createValueAdapter(@NotNull PsiElement element);
default TextRange adjustErrorHighlightingRange(@NotNull PsiElement element) {
return element.getTextRange();
}
@Nullable
static JsonLikePsiWalker getWalker(@NotNull final PsiElement element, JsonSchemaObject schemaObject) {
if (JSON_ORIGINAL_PSI_WALKER.handles(element)) return JSON_ORIGINAL_PSI_WALKER;
@@ -68,10 +73,11 @@ public interface JsonLikePsiWalker {
default QuickFixAdapter getQuickFixAdapter(Project project) { return null; }
interface QuickFixAdapter {
@Nullable PsiElement getPropertyValue(PsiElement property);
default @NotNull PsiElement adjustValue(@NotNull PsiElement value) { return value; }
@Nullable String getPropertyName(PsiElement property);
@NotNull PsiElement createProperty(@NotNull final String name, @NotNull final String value);
boolean ensureComma(PsiElement backward, PsiElement self, PsiElement newElement);
void removeIfComma(PsiElement forward);
boolean fixWhitespaceBefore();
boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element);
}
}
@@ -209,7 +209,7 @@ public class JsonOriginalPsiWalker implements JsonLikePsiWalker {
}
@Override
public boolean fixWhitespaceBefore() {
public boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element) {
return true;
}
};
@@ -661,6 +661,12 @@ class JsonSchemaAnnotatorChecker {
if (JsonSchemaType._integer.equals(input) && matchTypes.contains(JsonSchemaType._number)) {
return input;
}
if (JsonSchemaType._string_number.equals(input) &&
(matchTypes.contains(JsonSchemaType._number)
|| matchTypes.contains(JsonSchemaType._integer)
|| matchTypes.contains(JsonSchemaType._string))) {
return input;
}
//nothing matches, lets return one of the list so that other heuristics does not match
return matchTypes.iterator().next();
}
@@ -81,54 +81,54 @@ public class JsonSchemaComplianceChecker {
}
private void createWarnings(@Nullable JsonSchemaAnnotatorChecker checker) {
if (checker != null && ! checker.isCorrect()) {
// compute intersecting ranges - we'll solve warning priorities based on this information
List<TextRange> ranges = ContainerUtil.newArrayList();
List<List<Map.Entry<PsiElement, JsonValidationError>>> entries = ContainerUtil.newArrayList();
for (Map.Entry<PsiElement, JsonValidationError> entry : checker.getErrors().entrySet()) {
TextRange range = entry.getKey().getTextRange();
boolean processed = false;
for (int i = 0; i < ranges.size(); i++) {
TextRange currRange = ranges.get(i);
if (currRange.intersects(range)) {
ranges.set(i, new TextRange(Math.min(currRange.getStartOffset(), range.getStartOffset()), Math.max(currRange.getEndOffset(), range.getEndOffset())));
entries.get(i).add(entry);
processed = true;
break;
}
if (checker == null || checker.isCorrect()) return;
// compute intersecting ranges - we'll solve warning priorities based on this information
List<TextRange> ranges = ContainerUtil.newArrayList();
List<List<Map.Entry<PsiElement, JsonValidationError>>> entries = ContainerUtil.newArrayList();
for (Map.Entry<PsiElement, JsonValidationError> entry : checker.getErrors().entrySet()) {
TextRange range = entry.getKey().getTextRange();
boolean processed = false;
for (int i = 0; i < ranges.size(); i++) {
TextRange currRange = ranges.get(i);
if (currRange.intersects(range)) {
ranges.set(i, new TextRange(Math.min(currRange.getStartOffset(), range.getStartOffset()), Math.max(currRange.getEndOffset(), range.getEndOffset())));
entries.get(i).add(entry);
processed = true;
break;
}
if (processed) continue;
ranges.add(range);
entries.add(ContainerUtil.newArrayList(entry));
}
if (processed) continue;
// for each set of intersecting ranges, compute the best errors to show
for (List<Map.Entry<PsiElement, JsonValidationError>> entryList : entries) {
int min = entryList.stream().map(v -> v.getValue().getPriority().ordinal()).min(Integer::compareTo).orElse(Integer.MAX_VALUE);
for (Map.Entry<PsiElement, JsonValidationError> entry : entryList) {
JsonValidationError validationError = entry.getValue();
PsiElement psiElement = entry.getKey();
if (validationError.getPriority().ordinal() > min) {
continue;
}
registerError(psiElement, validationError);
ranges.add(range);
entries.add(ContainerUtil.newArrayList(entry));
}
// for each set of intersecting ranges, compute the best errors to show
for (List<Map.Entry<PsiElement, JsonValidationError>> entryList : entries) {
int min = entryList.stream().map(v -> v.getValue().getPriority().ordinal()).min(Integer::compareTo).orElse(Integer.MAX_VALUE);
for (Map.Entry<PsiElement, JsonValidationError> entry : entryList) {
JsonValidationError validationError = entry.getValue();
PsiElement psiElement = entry.getKey();
if (validationError.getPriority().ordinal() > min) {
continue;
}
TextRange range = myWalker.adjustErrorHighlightingRange(psiElement);
range = range.shiftLeft(psiElement.getTextRange().getStartOffset());
registerError(psiElement, range, validationError);
}
}
}
private void registerError(@NotNull PsiElement psiElement, @NotNull JsonValidationError validationError) {
private void registerError(@NotNull PsiElement psiElement, @NotNull TextRange range, @NotNull JsonValidationError validationError) {
if (checkIfAlreadyProcessed(psiElement)) return;
String value = validationError.getMessage();
if (myMessagePrefix != null) value = myMessagePrefix + value;
LocalQuickFix[] fix = validationError.createFixes(myWalker.getQuickFixAdapter(myHolder.getProject()));
if (fix.length == 0) {
myHolder.registerProblem(psiElement, value);
myHolder.registerProblem(psiElement, range, value);
}
else {
myHolder.registerProblem(psiElement, value, fix);
myHolder.registerProblem(psiElement, range, value, fix);
}
}
@@ -48,7 +48,8 @@ public class SuggestEnumValuesFix implements LocalQuickFix, BatchQuickFix<Common
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiElement element = descriptor.getPsiElement();
PsiElement initialElement = descriptor.getPsiElement();
PsiElement element = myQuickFixAdapter.adjustValue(initialElement);
FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(element.getContainingFile().getVirtualFile());
boolean whitespaceBefore = false;
if (element.getPrevSibling() instanceof PsiWhiteSpace) {
@@ -57,7 +58,7 @@ public class SuggestEnumValuesFix implements LocalQuickFix, BatchQuickFix<Common
WriteAction.run(() -> element.delete());
EditorEx editor = EditorUtil.getEditorEx(fileEditor);
assert editor != null;
if (myQuickFixAdapter.fixWhitespaceBefore() && whitespaceBefore) {
if (myQuickFixAdapter.fixWhitespaceBefore(initialElement, element) && whitespaceBefore) {
WriteAction.run(() -> {
int offset = editor.getCaretModel().getOffset();
editor.getDocument().insertString(offset, " ");
@@ -1,13 +1,16 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.yaml.schema;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.psi.YAMLAnchor;
import org.jetbrains.yaml.psi.YAMLValue;
import java.util.regex.Pattern;
@@ -34,10 +37,22 @@ public class YamlGenericValueAdapter implements JsonValueAdapter {
@Override
public boolean isStringLiteral() {
String text = myValue.getText();
String text = getTextWithoutRefs();
return !hasNonStringTags(text); /*values should always validate as string*/
}
private String getTextWithoutRefs() {
YAMLAnchor[] anchors = PsiTreeUtil.getChildrenOfType(myValue, YAMLAnchor.class);
if (anchors == null || anchors.length == 0) return myValue.getText();
int endOffset = anchors[anchors.length - 1].getTextRange().getEndOffset();
TextRange valueTextRange = myValue.getTextRange();
int offset = valueTextRange.getEndOffset();
TextRange range = new TextRange(endOffset, offset);
range = range.shiftLeft(valueTextRange.getStartOffset());
String text = myValue.getText();
return text.substring(range.getStartOffset()).trim();
}
private static boolean hasNonStringTags(@NotNull String text) {
return hasTag(text, "bool")
|| hasTag(text, "null")
@@ -51,19 +66,19 @@ public class YamlGenericValueAdapter implements JsonValueAdapter {
@Override
public boolean isNumberLiteral() {
String text = myValue.getText();
String text = getTextWithoutRefs();
return isNumber(text);
}
@Override
public boolean isBooleanLiteral() {
String text = myValue.getText();
String text = getTextWithoutRefs();
return "true".equals(text) || "false".equals(text) || hasTag(text, "bool");
}
@Override
public boolean isNull() {
String text = myValue.getText();
String text = getTextWithoutRefs();
return "null".equals(text) || hasTag(text, "null");
}
@@ -5,6 +5,7 @@ import com.intellij.codeInsight.completion.CompletionUtil;
import com.intellij.codeInsight.completion.CompletionUtilCore;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
@@ -121,8 +122,7 @@ public class YamlJsonPsiWalker implements JsonLikePsiWalker {
object = otherObject;
}
if (object == null) return Collections.emptySet();
return object.getKeyValues().stream().filter(p -> p != null && p.getName() != null)
.map(p -> p.getName()).collect(Collectors.toSet());
return new YamlObjectAdapter(object).getPropertyList().stream().map(p -> p.getName()).collect(Collectors.toSet());
}
@Nullable
@@ -210,12 +210,21 @@ public class YamlJsonPsiWalker implements JsonLikePsiWalker {
@Override
public String getNodeTextForValidation(PsiElement element) {
String text = element.getText();
if (!StringUtil.startsWith(text, "!!")) return text;
if (!StringUtil.startsWith(text, "!!") && !StringUtil.startsWithChar(text, '&')) return text;
// remove tags
int spaceIndex = text.indexOf(' ');
return spaceIndex > 0 ? text.substring(spaceIndex + 1) : text;
}
@Override
public TextRange adjustErrorHighlightingRange(@NotNull PsiElement element) {
YAMLAnchor[] anchors = PsiTreeUtil.getChildrenOfType(element, YAMLAnchor.class);
if (anchors == null || anchors.length == 0) return element.getTextRange();
YAMLAnchor lastAnchor = anchors[anchors.length - 1];
PsiElement next = PsiTreeUtil.skipWhitespacesForward(lastAnchor);
return next == null ? element.getTextRange() : next.getTextRange();
}
@Override
public QuickFixAdapter getQuickFixAdapter(Project project) {
return new QuickFixAdapter() {
@@ -225,7 +234,19 @@ public class YamlJsonPsiWalker implements JsonLikePsiWalker {
@Override
public PsiElement getPropertyValue(PsiElement property) {
assert property instanceof YAMLKeyValue;
return ((YAMLKeyValue)property).getValue();
YAMLValue value = ((YAMLKeyValue)property).getValue();
if (value == null) return null;
return adjustValue(property);
}
@NotNull
@Override
public PsiElement adjustValue(@NotNull PsiElement value) {
if (!(value instanceof YAMLValue)) return value;
YAMLAnchor[] anchors = PsiTreeUtil.getChildrenOfType(value, YAMLAnchor.class);
if (anchors == null || anchors.length == 0) return value;
PsiElement next = PsiTreeUtil.skipWhitespacesForward(anchors[anchors.length - 1]);
return next == null ? value : next;
}
@Nullable
@@ -262,8 +283,8 @@ public class YamlJsonPsiWalker implements JsonLikePsiWalker {
}
@Override
public boolean fixWhitespaceBefore() {
return false;
public boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element) {
return initialElement instanceof YAMLValue && initialElement != element;
}
};
}
@@ -2,15 +2,18 @@
package org.jetbrains.yaml.schema;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.util.RecursionManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter;
import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.psi.YAMLKeyValue;
import org.jetbrains.yaml.psi.YAMLMapping;
import org.jetbrains.yaml.psi.*;
import java.util.Collection;
import java.util.List;
@@ -88,8 +91,40 @@ public class YamlObjectAdapter implements JsonObjectValueAdapter {
Collection<YAMLKeyValue> keyValues = myObject.getKeyValues();
List<JsonPropertyAdapter> adapters = ContainerUtil.newArrayListWithCapacity(keyValues.size());
for (YAMLKeyValue value : keyValues) {
if (addPropertiesFromReferencedObject(adapters, value)) continue;
adapters.add(new YamlPropertyAdapter(value));
}
return adapters;
}
private boolean addPropertiesFromReferencedObject(List<JsonPropertyAdapter> adapters, YAMLKeyValue value) {
String keyText = value.getKeyText();
if (!"<<".equals(keyText)) return false;
YAMLValue yamlValue = value.getValue();
PsiElement resolved = resolveYamlAlias(yamlValue);
if (resolved != null) {
YAMLMapping mapping = ObjectUtils.tryCast(resolved, YAMLMapping.class);
if (mapping == null) return false;
List<JsonPropertyAdapter> propertyAdapters =
RecursionManager.doPreventingRecursion(myObject, false, () -> new YamlObjectAdapter(mapping).getPropertyList());
if (propertyAdapters != null) {
adapters.addAll(propertyAdapters);
return true;
}
}
if (yamlValue instanceof YAMLMapping) {
if (PsiTreeUtil.getChildOfType(yamlValue, YAMLAnchor.class) == null) return false;
adapters.addAll(new YamlObjectAdapter((YAMLMapping)yamlValue).getPropertyList());
return true;
}
return false;
}
@Nullable
static PsiElement resolveYamlAlias(YAMLValue yamlValue) {
PsiReference reference = yamlValue instanceof YAMLAlias ? yamlValue.getReference() : null;
PsiElement resolved = reference == null ? null : reference.resolve();
resolved = resolved == null ? null : resolved.getParent();
return resolved;
}
}
@@ -1,16 +1,14 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.yaml.schema;
import com.intellij.openapi.util.RecursionManager;
import com.intellij.psi.PsiElement;
import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter;
import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter;
import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.psi.YAMLKeyValue;
import org.jetbrains.yaml.psi.YAMLMapping;
import org.jetbrains.yaml.psi.YAMLSequence;
import org.jetbrains.yaml.psi.YAMLValue;
import org.jetbrains.yaml.psi.*;
public class YamlPropertyAdapter implements JsonPropertyAdapter {
@@ -51,6 +49,13 @@ public class YamlPropertyAdapter implements JsonPropertyAdapter {
@NotNull
public static JsonValueAdapter createValueAdapterByType(@NotNull YAMLValue value) {
if (value instanceof YAMLAlias) {
PsiElement result = YamlObjectAdapter.resolveYamlAlias(value);
if (result instanceof YAMLValue) {
JsonValueAdapter adapter = RecursionManager.doPreventingRecursion(value, false, () -> createValueAdapterByType((YAMLValue)result));
if (adapter != null) return adapter;
}
}
if (value instanceof YAMLMapping) return new YamlObjectAdapter((YAMLMapping) value);
if (value instanceof YAMLSequence) return new YamlArrayAdapter((YAMLSequence) value);
return new YamlGenericValueAdapter(value);
@@ -689,6 +689,82 @@ public class YamlByJsonSchemaHighlightingTest extends JsonSchemaHighlightingTest
" IsDev: !Equals [!Ref AccountType, dev]");
}
@Language("JSON")
private static final String SCHEMA_FOR_REFS = "{\n" +
" \"type\": \"object\",\n" +
"\n" +
" \"properties\": {\n" +
" \"name\": { \"type\": \"string\", \"enum\": [\"aa\", \"bb\"] },\n" +
" \"bar\": {\n" +
" \"required\": [\n" +
" \"a\"\n" +
" ],\n" +
" \"properties\": {\n" +
" \"a\": {\n" +
" \"type\": [\"array\"]\n" +
" },\n" +
" \"b\": {" +
" \"type\": [\"number\"]" +
" }\n" +
" },\n" +
" \"additionalProperties\": false\n" +
" }\n" +
" }\n" +
"}\n";
public void testRefExtends() throws Exception {
// no warning about missing required property - it should be discovered in referenced object
// no warning about extra 'property' with name '<<' with additionalProperties=false
doTest(SCHEMA_FOR_REFS, "a: &a\n" +
" a: <warning descr=\"Schema validation: Type is not allowed. Expected: array.\">7</warning>\n" +
"\n" +
"bar:\n" +
" <<: *a\n" +
" b: 5\n");
}
public void testRefRefValid() throws Exception {
// no warnings - &a references &b, which is an array - validation passes
doTest(SCHEMA_FOR_REFS, "x: &b\n" +
" - x\n" +
" - y\n" +
"\n" +
"a: &a\n" +
" a: *b\n" +
"\n" +
"bar:\n" +
" <<: *a\n" +
" b: 5");
}
public void testRefRefInvalid() throws Exception {
doTest(SCHEMA_FOR_REFS, "x: &b <warning descr=\"Schema validation: Type is not allowed. Expected: array.\">7</warning>\n" +
"\n" +
"a: &a\n" +
" a: *b\n" +
"\n" +
"bar:\n" +
" <<: *a\n" +
" b: 5");
}
public void testRefRefScalarValid() throws Exception {
doTest(SCHEMA_FOR_REFS, "x: &b 7\n" +
"\n" +
"a: &a\n" +
" b: *b\n" +
"\n" +
"bar:\n" +
" <<: *a\n" +
" a: <warning descr=\"Schema validation: Type is not allowed. Expected: array.\">5</warning>");
}
public void testInlineRef() throws Exception {
doTest(SCHEMA_FOR_REFS, "bar:\n" +
" <<: &q\n" +
" a: <warning descr=\"Schema validation: Type is not allowed. Expected: array.\">5</warning>\n" +
" b: 5");
}
static String schema(final String s) {
return "{\"type\": \"object\", \"properties\": {\"prop\": " + s + "}}";
}