From 9d65c2b37bbf3bd8317262ae54c0e2bd83304a2a Mon Sep 17 00:00:00 2001 From: Anton Lobov Date: Thu, 6 Sep 2018 18:10:12 +0200 Subject: [PATCH] RUBY-22379 YAML Schema Validation should support anchors and references --- .../extension/JsonLikePsiWalker.java | 8 +- .../impl/JsonOriginalPsiWalker.java | 2 +- .../impl/JsonSchemaAnnotatorChecker.java | 6 ++ .../impl/JsonSchemaComplianceChecker.java | 66 ++++++++-------- .../impl/fixes/SuggestEnumValuesFix.java | 5 +- .../yaml/schema/YamlGenericValueAdapter.java | 23 +++++- .../yaml/schema/YamlJsonPsiWalker.java | 33 ++++++-- .../yaml/schema/YamlObjectAdapter.java | 39 +++++++++- .../yaml/schema/YamlPropertyAdapter.java | 13 +++- .../YamlByJsonSchemaHighlightingTest.java | 76 +++++++++++++++++++ 10 files changed, 218 insertions(+), 53 deletions(-) diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java index d4bca6db8210..5503f96e576d 100644 --- a/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java @@ -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); } } diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java index 02079c27d6d1..1262aa5c0f9e 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java @@ -209,7 +209,7 @@ public class JsonOriginalPsiWalker implements JsonLikePsiWalker { } @Override - public boolean fixWhitespaceBefore() { + public boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element) { return true; } }; diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java index 43b1dfb979eb..78068d528536 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java @@ -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(); } diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java index 12c81a9852d1..f4c9d63298e4 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java @@ -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 ranges = ContainerUtil.newArrayList(); - List>> entries = ContainerUtil.newArrayList(); - for (Map.Entry 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 ranges = ContainerUtil.newArrayList(); + List>> entries = ContainerUtil.newArrayList(); + for (Map.Entry 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> entryList : entries) { - int min = entryList.stream().map(v -> v.getValue().getPriority().ordinal()).min(Integer::compareTo).orElse(Integer.MAX_VALUE); - for (Map.Entry 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> entryList : entries) { + int min = entryList.stream().map(v -> v.getValue().getPriority().ordinal()).min(Integer::compareTo).orElse(Integer.MAX_VALUE); + for (Map.Entry 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); } } diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java index a7a14560f3a6..3d00f1f2576b 100644 --- a/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java +++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java @@ -48,7 +48,8 @@ public class SuggestEnumValuesFix implements LocalQuickFix, BatchQuickFix 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, " "); diff --git a/plugins/yaml/src/org/jetbrains/yaml/schema/YamlGenericValueAdapter.java b/plugins/yaml/src/org/jetbrains/yaml/schema/YamlGenericValueAdapter.java index 6c6ea9829d2f..bbccad4ba437 100644 --- a/plugins/yaml/src/org/jetbrains/yaml/schema/YamlGenericValueAdapter.java +++ b/plugins/yaml/src/org/jetbrains/yaml/schema/YamlGenericValueAdapter.java @@ -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"); } diff --git a/plugins/yaml/src/org/jetbrains/yaml/schema/YamlJsonPsiWalker.java b/plugins/yaml/src/org/jetbrains/yaml/schema/YamlJsonPsiWalker.java index c246ee5ba54b..58b5b180ec47 100644 --- a/plugins/yaml/src/org/jetbrains/yaml/schema/YamlJsonPsiWalker.java +++ b/plugins/yaml/src/org/jetbrains/yaml/schema/YamlJsonPsiWalker.java @@ -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; } }; } diff --git a/plugins/yaml/src/org/jetbrains/yaml/schema/YamlObjectAdapter.java b/plugins/yaml/src/org/jetbrains/yaml/schema/YamlObjectAdapter.java index 8a8615efd513..31dc831d8f05 100644 --- a/plugins/yaml/src/org/jetbrains/yaml/schema/YamlObjectAdapter.java +++ b/plugins/yaml/src/org/jetbrains/yaml/schema/YamlObjectAdapter.java @@ -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 keyValues = myObject.getKeyValues(); List 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 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 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; + } } diff --git a/plugins/yaml/src/org/jetbrains/yaml/schema/YamlPropertyAdapter.java b/plugins/yaml/src/org/jetbrains/yaml/schema/YamlPropertyAdapter.java index 25c6884e2094..80437de728aa 100644 --- a/plugins/yaml/src/org/jetbrains/yaml/schema/YamlPropertyAdapter.java +++ b/plugins/yaml/src/org/jetbrains/yaml/schema/YamlPropertyAdapter.java @@ -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); diff --git a/plugins/yaml/testSrc/org/jetbrains/yaml/schema/YamlByJsonSchemaHighlightingTest.java b/plugins/yaml/testSrc/org/jetbrains/yaml/schema/YamlByJsonSchemaHighlightingTest.java index 427ad3c7267b..78589809136b 100644 --- a/plugins/yaml/testSrc/org/jetbrains/yaml/schema/YamlByJsonSchemaHighlightingTest.java +++ b/plugins/yaml/testSrc/org/jetbrains/yaml/schema/YamlByJsonSchemaHighlightingTest.java @@ -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: 7\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 7\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: 5"); + } + + public void testInlineRef() throws Exception { + doTest(SCHEMA_FOR_REFS, "bar:\n" + + " <<: &q\n" + + " a: 5\n" + + " b: 5"); + } + static String schema(final String s) { return "{\"type\": \"object\", \"properties\": {\"prop\": " + s + "}}"; }