json schema: fix some external references navigation (both for property names into schema and from $ref)

refactor resolve logic so that it is more clear
this fixes WEB-24965 JSON Schema: navigation from property names in package.json to package json schema does not work
and WEB-21364 JSON Schema:Unable to create relative references to local files
This commit is contained in:
irengrig
2017-01-11 17:52:24 +01:00
parent 4841db826f
commit d505ac086e
15 changed files with 775 additions and 275 deletions
@@ -19,7 +19,6 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.jetbrains.jsonSchema.JsonSchemaFileType;
import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -48,8 +47,7 @@ public class JsonSchemaProjectSelfProviderFactory implements JsonSchemaProviderF
@Override
public boolean isAvailable(@NotNull Project project, @NotNull VirtualFile file) {
if (!JsonSchemaFileType.INSTANCE.equals(file.getFileType())) return false;
return JsonSchemaMappingsProjectConfiguration.getInstance(project).isRegisteredSchemaFile(file);
return JsonSchemaFileType.INSTANCE.equals(file.getFileType());
}
@NotNull
@@ -0,0 +1,72 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.jsonSchema.extension.schema;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.jsonSchema.impl.JsonSchemaResourcesRootsProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author Irina.Chernushina on 1/10/2017.
*/
public class JsonSchemaByPropertyIndexResolver {
@NotNull private final String myReferenceName;
@NotNull private final Project myProject;
@Nullable private final VirtualFile mySchemaFile;
private VirtualFile myFile;
private Integer myOffset;
public JsonSchemaByPropertyIndexResolver(@NotNull String referenceName,
@NotNull Project project,
@Nullable VirtualFile schemaFile) {
myReferenceName = referenceName;
myProject = project;
mySchemaFile = schemaFile;
}
public PsiElement resolveByName() {
final GlobalSearchScope scope;
if (mySchemaFile != null) {
scope = GlobalSearchScope.fileScope(myProject, mySchemaFile);
} else {
scope = JsonSchemaResourcesRootsProvider.enlarge(myProject, GlobalSearchScope.allScope(myProject));
}
FileBasedIndex.getInstance().processValues(JsonSchemaFileIndex.PROPERTIES_INDEX, myReferenceName, null, (file, value) -> {
if (!scope.contains(file)) return true;
myFile = file;
myOffset = value;
return false;
}, scope);
if (myFile != null) {
myOffset = myOffset == null ? 0 : myOffset;
final PsiFile file = PsiManager.getInstance(myProject).findFile(myFile);
if (file != null) {
return file.findElementAt(myOffset);
}
}
return null;
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.jsonSchema.extension.schema;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.PairConsumer;
import com.jetbrains.jsonSchema.ide.JsonSchemaService;
import com.jetbrains.jsonSchema.impl.JsonSchemaObject;
import com.jetbrains.jsonSchema.impl.JsonSchemaReader;
import com.jetbrains.jsonSchema.impl.JsonSchemaWalker;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import static com.jetbrains.jsonSchema.extension.schema.JsonSchemaInsideSchemaResolver.PROPERTIES;
/**
* @author Irina.Chernushina on 1/10/2017.
*/
public class JsonSchemaBySchemaObjectResolver {
@NotNull private final Project myProject;
@NotNull private final VirtualFile mySchemaFile;
@NotNull private final String myShortPropertyName;
@NotNull private final List<JsonSchemaWalker.Step> myPosition;
@NotNull private final PairConsumer<VirtualFile, String> myConsumer;
public JsonSchemaBySchemaObjectResolver(@NotNull Project project,
@NotNull VirtualFile schemaFile,
@NotNull String shortPropertyName,
@NotNull List<JsonSchemaWalker.Step> position,
@NotNull PairConsumer<VirtualFile, String> consumer) {
myProject = project;
mySchemaFile = schemaFile;
myShortPropertyName = shortPropertyName;
myPosition = position.subList(0, position.size() - 1);
myConsumer = consumer;
}
public void iterateMatchingDefinitions() {
final JsonSchemaWalker.CompletionSchemesConsumer consumer = new JsonSchemaWalker.CompletionSchemesConsumer() {
@Override
public void consume(boolean isName, @NotNull JsonSchemaObject schema) {
//myConsumer.consume(mySchemaFile, myShortPropertyName);//todo using short property name here is wrong
processDefinitionAddress(schema, myShortPropertyName);
List<JsonSchemaObject> list = new ArrayList<>();
if (schema.getAllOf() != null) list.addAll(schema.getAllOf());
if (schema.getAnyOf() != null) list.addAll(schema.getAnyOf());
if (schema.getOneOf() != null) list.addAll(schema.getOneOf());
for (JsonSchemaObject schemaObject : list) {
processDefinitionAddress(schemaObject, myShortPropertyName);
}
}
};
JsonSchemaService.Impl.getEx(myProject).visitSchemaObject(
mySchemaFile,
object -> {
if (myPosition.isEmpty()) {
consumer.consume(true, object);
return true;
}
JsonSchemaWalker.extractSchemaVariants(consumer, object, true, myPosition);
return true;
});
}
private void processDefinitionAddress(@NotNull JsonSchemaObject parSchema, @NotNull String propertyName) {
final String definitionAddress = parSchema.getDefinitionAddress();
if (StringUtil.isEmptyOrSpaces(definitionAddress)) return;
final JsonSchemaReader.SchemaUrlSplitter splitter = new JsonSchemaReader.SchemaUrlSplitter(definitionAddress);
if (!splitter.isAbsolute()) {
VirtualFile schemaFile = mySchemaFile;
if (parSchema.getId() != null) {
schemaFile = JsonSchemaService.Impl.getEx(myProject).getSchemaFileById(parSchema.getId(), mySchemaFile);
if (schemaFile == null) return;
}
final String newReferenceName = definitionAddress.substring(1) + PROPERTIES + propertyName;
myConsumer.consume(schemaFile, newReferenceName);
} else {
String relative = splitter.getRelativePath();
if (StringUtil.isEmptyOrSpaces(relative)) {
relative = PROPERTIES + propertyName;
} else {
relative += ((relative.endsWith("/") ? PROPERTIES.substring(1) : PROPERTIES) + propertyName);
}
assert splitter.getSchemaId() != null;
final VirtualFile schemaFile = JsonSchemaService.Impl.getEx(myProject).getSchemaFileById(parSchema.getId(), mySchemaFile);
if (schemaFile == null) return;
myConsumer.consume(schemaFile, relative);
}
}
}
@@ -1,239 +0,0 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.jsonSchema.extension.schema;
import com.intellij.json.psi.JsonObject;
import com.intellij.json.psi.JsonProperty;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.jsonSchema.JsonSchemaFileType;
import com.jetbrains.jsonSchema.ide.JsonSchemaService;
import com.jetbrains.jsonSchema.impl.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @author Irina.Chernushina on 7/7/2016.
*/
public class JsonSchemaDefinitionResolver {
public static final String PROPERTIES = "/properties/";
@Nullable private String myRef;
@Nullable JsonSchemaObject mySchemaObject;
@NotNull private final PsiElement myElement;
@Nullable final String mySchemaId;
public JsonSchemaDefinitionResolver(@NotNull PsiElement element, @Nullable String schemaId) {
myElement = element;
mySchemaId = schemaId;
}
public JsonSchemaDefinitionResolver setSchemaObject(@NotNull final JsonSchemaObject value) {
mySchemaObject = value;
return this;
}
public JsonSchemaDefinitionResolver setRef(@NotNull final String value) {
myRef = value;
return this;
}
@Nullable
public PsiElement doResolve() {
PsiElement result = tryResolveByName();
if (result != null) return result;
if (mySchemaId == null) {
return tryResolveBySchemaObject();
}
return null;
}
@Nullable
public PsiElement doResolveInSchemaFile() {
if (!JsonSchemaFileType.INSTANCE.equals(myElement.getContainingFile().getFileType())) return null;
if (myRef == null) initializeName();
if (myRef == null) return null;
final PsiElement element = resolveInSomeSchema(myRef, myElement.getProject(), null, myElement.getContainingFile().getVirtualFile());
if (element != null) return element;
if (mySchemaId == null) {
return tryResolveBySchemaObject();
}
return null;
}
private PsiElement tryResolveBySchemaObject() {
if (!(myElement.getParent() instanceof JsonProperty)) return null;
final Ref<PsiElement> ref = new Ref<>();
final String propertyName = ((JsonProperty)myElement.getParent()).getName();
JsonSchemaService.Impl.getEx(myElement.getProject()).iterateSchemaObjects(
myElement.getContainingFile().getVirtualFile(),
object -> {
final JsonSchemaWalker.CompletionSchemesConsumer consumer = new JsonSchemaWalker.CompletionSchemesConsumer() {
@Override
public void consume(boolean isName, @NotNull JsonSchemaObject schema) {
if (!ref.isNull()) return;
ref.set(processDefinitionAddress(schema, propertyName));
if (!ref.isNull()) return;
List<JsonSchemaObject> list = new ArrayList<>();
if (schema.getAllOf() != null) list.addAll(schema.getAllOf());
if (schema.getAnyOf() != null) list.addAll(schema.getAnyOf());
if (schema.getOneOf() != null) list.addAll(schema.getOneOf());
for (JsonSchemaObject schemaObject : list) {
ref.set(processDefinitionAddress(schemaObject, propertyName));
if (!ref.isNull()) return;
}
}
};
final List<JsonSchemaWalker.Step> position = JsonSchemaWalker.findPosition(((JsonProperty)myElement.getParent()).getNameElement(), true);
if (position == null || position.isEmpty()) return true; // to continue iteration
JsonSchemaWalker.extractSchemaVariants(consumer, object, true, position);
return ref.isNull();
});
return ref.get();
}
@Nullable
private PsiElement processDefinitionAddress(JsonSchemaObject parSchema, String propertyName) {
final String definitionAddress = parSchema.getDefinitionAddress();
if (StringUtil.isEmptyOrSpaces(definitionAddress)) return null;
final JsonSchemaReader.SchemaUrlSplitter splitter = new JsonSchemaReader.SchemaUrlSplitter(definitionAddress);
if (!splitter.isAbsolute()) {
VirtualFile schemaFile = null;
if (parSchema.getId() != null) {
schemaFile = JsonSchemaService.Impl.getEx(myElement.getProject()).getSchemaFileById(parSchema.getId());
}
return resolveInSomeSchema(definitionAddress.substring(1) + PROPERTIES + propertyName, myElement.getProject(), parSchema.getId(), schemaFile);
} else {
String relative = splitter.getRelativePath();
if (StringUtil.isEmptyOrSpaces(relative)) {
relative = PROPERTIES + propertyName;
} else {
relative += ((relative.endsWith("/") ? PROPERTIES.substring(1) : PROPERTIES) + propertyName);
}
return resolveInSomeSchema(relative, myElement.getProject(), splitter.getSchemaId(), null);
}
}
private PsiElement tryResolveByName() {
if (myRef == null) initializeName();
if (myRef == null) return null;
if (mySchemaId == null) {
final JsonSchemaServiceEx schemaServiceEx = JsonSchemaService.Impl.getEx(myElement.getProject());
final Collection<Pair<VirtualFile, String>> pairs = schemaServiceEx.getSchemaFilesByFile(myElement.getContainingFile().getVirtualFile());
if (pairs != null && ! pairs.isEmpty()) {
for (Pair<VirtualFile, String> pair : pairs) {
final PsiElement element = resolveInSomeSchema(myRef, myElement.getProject(), pair.getSecond(), pair.getFirst());
if (element != null) return element;
}
}
return null;
}
return resolveInSomeSchema(myRef, myElement.getProject(), mySchemaId, null);
}
@Nullable
private PsiElement resolveInSomeSchema(@NotNull String referenceName,
@NotNull final Project project,
@Nullable final String schemaId,
final @Nullable VirtualFile filterFile) {
final Ref<Pair<VirtualFile, Integer>> reference = new Ref<>();
final FileBasedIndex index = FileBasedIndex.getInstance();
final GlobalSearchScope fileScope = filterFile == null ? null : GlobalSearchScope.fileScope(project, filterFile);
final GlobalSearchScope enlarged = fileScope != null && JsonSchemaResourcesRootsProvider.ourFiles.getValue().contains(filterFile) ? fileScope :
JsonSchemaResourcesRootsProvider.enlarge(project, fileScope == null ? GlobalSearchScope.allScope(project) : fileScope);
index.processValues(JsonSchemaFileIndex.PROPERTIES_INDEX, referenceName, null, new FileBasedIndex.ValueProcessor<Integer>() {
@Override
public boolean process(@NotNull VirtualFile file, Integer value) {
final VirtualFile extractedFile = extractFileFromSchemaId();
if (extractedFile != null) {
if (!file.equals(extractedFile)) return false;
}
else if (schemaId != null) {
if (!JsonSchemaService.Impl.getEx(project).checkFileForId(schemaId, file)) {
return true;
}
}
reference.set(Pair.create(file, value));
return false;
}
}, enlarged);
if (!reference.isNull()) {
final Pair<VirtualFile, Integer> pair = reference.get();
final PsiFile file = PsiManager.getInstance(project).findFile(pair.getFirst());
if (file != null) {
return file.findElementAt(pair.getSecond());
}
}
return null;
}
@Nullable
private VirtualFile extractFileFromSchemaId() {
if (mySchemaId == null) return null;
VirtualFile dir = myElement.getContainingFile().getVirtualFile();
if (!dir.isDirectory()) dir = dir.getParent();
if (dir == null || !dir.isValid()) return null;
return VfsUtil.findRelativeFile(dir, JsonSchemaExportedDefinitions.normalizeId(mySchemaId).
replace("\\", "/").split("/"));
}
private void initializeName() {
final List<String> names = new ArrayList<>();
final PsiElement parent = myElement.getParent();
if (!(parent instanceof JsonProperty)) return;
JsonProperty element = (JsonProperty)parent;
while (true) {
names.add(StringUtil.unquoteString(element.getName()));
if (!(element.getParent() instanceof JsonObject)) break;
final PsiElement grand = element.getParent().getParent();
if (grand instanceof JsonProperty && ((JsonProperty)grand).getValue() != null &&
((JsonProperty)grand).getValue().equals(element.getParent())) {
element = (JsonProperty) grand;
} else break;
}
final StringBuilder path = new StringBuilder();
Collections.reverse(names);
for (String name : names) {
path.append(PROPERTIES).append(name);
}
myRef = path.toString();
}
}
@@ -0,0 +1,86 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.jsonSchema.extension.schema;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.jetbrains.jsonSchema.impl.JsonSchemaExportedDefinitions;
import com.jetbrains.jsonSchema.impl.JsonSchemaWalker;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
/**
* @author Irina.Chernushina on 1/10/2017.
*/
public class JsonSchemaInsideSchemaResolver {
public static final String PROPERTIES = "/properties/";
@NotNull private final Project myProject;
@NotNull private final VirtualFile mySchemaFile;
@NotNull private final String myReference;
@NotNull private final List<JsonSchemaWalker.Step> mySteps;
@NotNull private final MultiMap<VirtualFile, String> myVisitedDefinitions = new MultiMap<VirtualFile, String>() {
@NotNull
@Override
protected Collection<String> createCollection() {
return new HashSet<>();
}
};
public JsonSchemaInsideSchemaResolver(@NotNull Project project,
@NotNull VirtualFile schemaFile,
@NotNull String reference, @NotNull List<JsonSchemaWalker.Step> steps) {
myProject = project;
mySchemaFile = schemaFile;
myReference = reference;
mySteps = steps;
}
public PsiElement resolveInSchemaRecursively() {
final ArrayDeque<Trinity<VirtualFile, List<JsonSchemaWalker.Step>, String>> queue = new ArrayDeque<>();
queue.add(Trinity.create(mySchemaFile, mySteps, myReference));
myVisitedDefinitions.putValue(mySchemaFile, myReference);
while (!queue.isEmpty()) {
final Trinity<VirtualFile, List<JsonSchemaWalker.Step>, String> trinity = queue.removeFirst();
final VirtualFile schemaFile = trinity.getFirst();
final String reference = JsonSchemaExportedDefinitions.normalizeId(trinity.getThird());
final PsiElement element = new JsonSchemaByPropertyIndexResolver(reference, myProject, schemaFile).resolveByName();
if (element != null) return element;
final List<String> parts = ContainerUtil.filter(reference.replace("\\", "/").split("/"), s -> !StringUtil.isEmptyOrSpaces(s));
final String shortName = parts.get(parts.size() - 1);
final List<JsonSchemaWalker.Step> steps = trinity.getSecond();
new JsonSchemaBySchemaObjectResolver(myProject, schemaFile, shortName, steps,
(file, relativeReference) -> {
final Pair<List<JsonSchemaWalker.Step>, String> innerSteps = JsonSchemaWalker.buildSteps(relativeReference);
if (mySchemaFile.equals(file) &&
(myReference.equals(relativeReference) || myVisitedDefinitions.get(file).contains(relativeReference)))
return;
myVisitedDefinitions.putValue(file, relativeReference);
queue.add(Trinity.create(file, innerSteps.getFirst(), relativeReference));
}).iterateMatchingDefinitions();
}
return null;
}
}
@@ -16,15 +16,23 @@
package com.jetbrains.jsonSchema.extension.schema;
import com.intellij.json.psi.JsonValue;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.ElementManipulators;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceProvider;
import com.intellij.util.ProcessingContext;
import com.jetbrains.jsonSchema.impl.JsonSchemaExportedDefinitions;
import com.jetbrains.jsonSchema.impl.JsonSchemaReader;
import com.jetbrains.jsonSchema.impl.JsonSchemaServiceEx;
import com.jetbrains.jsonSchema.impl.JsonSchemaWalker;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* @author Irina.Chernushina on 3/31/2016.
*/
@@ -50,18 +58,22 @@ public class JsonSchemaRefReferenceProvider extends PsiReferenceProvider {
@Override
public PsiElement resolveInner() {
final String text = getCanonicalText();
String id = null;
String ref = text.substring(1);
final boolean isGlobal = !text.startsWith("#");
if (isGlobal) {
final int idx = text.indexOf("#");
if (idx <= 0) return null;
id = text.substring(0, idx);
ref = text.substring(idx + 1);
return new JsonSchemaDefinitionResolver(getElement(), id).setRef(ref).doResolve();
final JsonSchemaReader.SchemaUrlSplitter splitter = new JsonSchemaReader.SchemaUrlSplitter(text);
VirtualFile schemaFile = getElement().getContainingFile().getVirtualFile();
if (splitter.isAbsolute()) {
assert splitter.getSchemaId() != null;
schemaFile = JsonSchemaServiceEx.Impl.getEx(getElement().getProject()).getSchemaFileById(splitter.getSchemaId(), schemaFile);
if (schemaFile == null) return null;
}
if (StringUtil.isEmptyOrSpaces(splitter.getRelativePath())) {
return myElement.getManager().findFile(schemaFile);
}
return new JsonSchemaDefinitionResolver(getElement(), null).setRef(ref).doResolveInSchemaFile();
final String normalized = JsonSchemaExportedDefinitions.normalizeId(splitter.getRelativePath());
final Pair<List<JsonSchemaWalker.Step>, String> steps = JsonSchemaWalker.buildSteps(normalized);
return new JsonSchemaInsideSchemaResolver(myElement.getProject(), schemaFile, normalized, steps.getFirst())
.resolveInSchemaRecursively();
}
}
}
@@ -15,17 +15,30 @@
*/
package com.jetbrains.jsonSchema.impl;
import com.intellij.json.psi.JsonObject;
import com.intellij.json.psi.JsonProperty;
import com.intellij.json.psi.JsonStringLiteral;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.ElementManipulators;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.PsiReferenceProvider;
import com.intellij.util.ProcessingContext;
import com.jetbrains.jsonSchema.extension.schema.JsonSchemaBaseReference;
import com.jetbrains.jsonSchema.extension.schema.JsonSchemaDefinitionResolver;
import com.jetbrains.jsonSchema.extension.schema.JsonSchemaInsideSchemaResolver;
import com.jetbrains.jsonSchema.ide.JsonSchemaService;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static com.jetbrains.jsonSchema.extension.schema.JsonSchemaInsideSchemaResolver.PROPERTIES;
/**
* @author Irina.Chernushina on 4/15/2016.
*/
@@ -44,7 +57,46 @@ public class JsonPropertyName2SchemaDefinitionReferenceProvider extends PsiRefer
@Nullable
@Override
public PsiElement resolveInner() {
return new JsonSchemaDefinitionResolver(getElement(), null).doResolve();
final String reference = getReference();
if (reference == null) return null;
final JsonSchemaServiceEx schemaServiceEx = JsonSchemaService.Impl.getEx(myElement.getProject());
final Collection<Pair<VirtualFile, String>> pairs = schemaServiceEx.getSchemaFilesByFile(myElement.getContainingFile().getVirtualFile());
if (pairs != null && ! pairs.isEmpty()) {
for (Pair<VirtualFile, String> pair : pairs) {
final VirtualFile schemaFile = pair.getFirst();
final List<JsonSchemaWalker.Step> steps = JsonSchemaWalker.findPosition(getElement(), true, true);
if (steps == null) continue;
//steps.add(0, new JsonSchemaWalker.Step(JsonSchemaWalker.StateType._unknown, new JsonSchemaWalker.PropertyTransition("properties")));
final PsiElement element =
new JsonSchemaInsideSchemaResolver(myElement.getProject(), schemaFile, reference, steps).resolveInSchemaRecursively();
if (element != null) return element;
}
}
return null;
}
private String getReference() {
final List<String> names = new ArrayList<>();
final PsiElement parent = getElement().getParent();
if (!(parent instanceof JsonProperty)) return null;
JsonProperty element = (JsonProperty)parent;
while (true) {
names.add(StringUtil.unquoteString(element.getName()));
if (!(element.getParent() instanceof JsonObject)) break;
final PsiElement grand = element.getParent().getParent();
if (grand instanceof JsonProperty && ((JsonProperty)grand).getValue() != null &&
((JsonProperty)grand).getValue().equals(element.getParent())) {
element = (JsonProperty)grand;
}
else break;
}
final StringBuilder path = new StringBuilder();
Collections.reverse(names);
for (String name : names) {
path.append(PROPERTIES).append(name);
}
return path.toString();
}
}
}
@@ -139,7 +139,8 @@ public class JsonSchemaExportedDefinitions {
}
@NotNull
public static String normalizeId(@NotNull final String id) {
return id.endsWith("#") ? id.substring(0, id.length() - 1) : id;
public static String normalizeId(@NotNull String id) {
id = id.endsWith("#") ? id.substring(0, id.length() - 1) : id;
return id.startsWith("#") ? id.substring(1) : id;
}
}
@@ -17,6 +17,7 @@ package com.jetbrains.jsonSchema.impl;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Processor;
import com.jetbrains.jsonSchema.ide.JsonSchemaService;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -29,10 +30,12 @@ import java.util.Set;
*/
public interface JsonSchemaServiceEx extends JsonSchemaService {
void visitSchemaObject(@NotNull VirtualFile schemaFile, @NotNull Processor<JsonSchemaObject> consumer);
boolean checkFileForId(@NotNull String id, @NotNull VirtualFile file);
@Nullable
VirtualFile getSchemaFileById(@NotNull String id);
VirtualFile getSchemaFileById(@NotNull String id, VirtualFile referent);
@Nullable
Collection<Pair<VirtualFile, String>> getSchemaFilesByFile(@NotNull final VirtualFile file);
@@ -17,6 +17,7 @@ import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NullableLazyValue;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
@@ -35,6 +36,7 @@ import com.jetbrains.jsonSchema.extension.JsonSchemaProviderFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
@@ -163,6 +165,16 @@ public class JsonSchemaServiceImpl implements JsonSchemaServiceEx {
wrapper.iterateSchemaObjects(consumer);
}
@Override
public void visitSchemaObject(@NotNull final VirtualFile schemaFile, @NotNull Processor<JsonSchemaObject> consumer) {
final JsonSchemaObjectCodeInsightWrapper wrapper;
synchronized (myLock) {
wrapper = myWrappers.get(schemaFile);
}
if (wrapper == null) return;
wrapper.iterateSchemaObjects(consumer);
}
@Nullable
@Override
public List<Pair<Boolean, String>> getMatchingSchemaDescriptors(@Nullable VirtualFile file) {
@@ -366,8 +378,16 @@ public class JsonSchemaServiceImpl implements JsonSchemaServiceEx {
@Override
@Nullable
public VirtualFile getSchemaFileById(@NotNull String id) {
return myDefinitions.getSchemaFileById(id);
public VirtualFile getSchemaFileById(@NotNull String id, @Nullable VirtualFile referent) {
final VirtualFile schemaFile = myDefinitions.getSchemaFileById(id);
if (schemaFile != null) return schemaFile;
final String normalizedId = JsonSchemaExportedDefinitions.normalizeId(id);
if (FileUtil.isAbsolute(normalizedId) || referent == null) return VfsUtil.findFileByIoFile(new File(normalizedId), false);
VirtualFile dir = referent.isDirectory() ? referent : referent.getParent();
if (dir != null && dir.isValid()) {
return VfsUtil.findRelativeFile(dir, normalizedId.replace("\\", "/").split("/"));
}
return null;
}
@Override
@@ -13,10 +13,8 @@ import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author Irina.Chernushina on 10/22/2015.
@@ -48,7 +46,7 @@ public class JsonSchemaWalker {
public static void findSchemasForAnnotation(@NotNull final PsiElement element, @NotNull final CompletionSchemesConsumer consumer,
@NotNull final JsonSchemaObject rootSchema) {
final List<Step> position = findPosition(element, false);
final List<Step> position = findPosition(element, false, true);
if (position == null || position.isEmpty()) return;
extractSchemaVariants(consumer, rootSchema, false, position);
@@ -59,7 +57,7 @@ public class JsonSchemaWalker {
final PsiElement checkable = goUpToCheckable(element);
if (checkable == null) return;
final boolean isName = isName(checkable);
final List<Step> position = findPosition(checkable, isName);
final List<Step> position = findPosition(checkable, isName, !isName);
if (position == null || position.isEmpty()) {
if (isName) consumer.consume(true, rootSchema);
return;
@@ -68,6 +66,13 @@ public class JsonSchemaWalker {
extractSchemaVariants(consumer, rootSchema, isName, position);
}
public static Pair<List<Step>, String> buildSteps(@NotNull String nameInSchema) {
final String[] chain = JsonSchemaExportedDefinitions.normalizeId(nameInSchema).replace("\\", "/").split("/");
final List<Step> steps = Arrays.stream(chain).map(item -> new Step(StateType._unknown, new PropertyTransition(item)))
.collect(Collectors.toList());
return Pair.create(steps, chain[chain.length - 1]);
}
public static void extractSchemaVariants(@NotNull CompletionSchemesConsumer consumer,
@NotNull JsonSchemaObject rootSchema, boolean isName, List<Step> position) {
final ArrayDeque<Pair<JsonSchemaObject, Integer>> queue = new ArrayDeque<>();
@@ -82,7 +87,8 @@ public class JsonSchemaWalker {
consumer.consume(isName, schema);
continue;
}
if (step.getTransition() != null && !step.getTransition().possibleFromState(step.getType())) continue;
if (step.getTransition() != null && !StateType._unknown.equals(step.getType())
&& !step.getTransition().possibleFromState(step.getType())) continue;
final Condition<JsonSchemaObject> byTypeFilter = object -> byStateType(step.getType(), object);
// not??
@@ -112,7 +118,7 @@ public class JsonSchemaWalker {
for (JsonSchemaObject object : list) {
final TransitionResultConsumer transitionResultConsumer = new TransitionResultConsumer();
step.getTransition().step(object, transitionResultConsumer);
// nothing or anything does not contribute to competion
// nothing or anything does not contribute to completion
if (transitionResultConsumer.getSchema() != null) {
if ((pair.getSecond() + 1) >= position.size()) consumer.consume(isName, transitionResultConsumer.getSchema());
else queue.add(Pair.create(transitionResultConsumer.getSchema(), pair.getSecond() + 1));
@@ -123,6 +129,7 @@ public class JsonSchemaWalker {
}
private static boolean byStateType(@NotNull final StateType type, @NotNull final JsonSchemaObject schema) {
if (StateType._unknown.equals(type)) return true;
final JsonSchemaType requiredType = type.getCorrespondingJsonType();
if (requiredType == null) return true;
if (schema.getType() != null) {
@@ -159,7 +166,7 @@ public class JsonSchemaWalker {
return null;
}
public static List<Step> findPosition(@NotNull final PsiElement element, boolean isName) {
public static List<Step> findPosition(@NotNull final PsiElement element, boolean isName, boolean forceLastTransition) {
final List<Step> steps = new ArrayList<>();
if (!(element.getParent() instanceof JsonObject) && !isName) {
steps.add(new Step(StateType._value, null));
@@ -186,12 +193,12 @@ public class JsonSchemaWalker {
current = current.getParent();
if (!(current instanceof JsonObject)) return null;//incorrect syntax?
// if either value or not first in the chain - needed for completion variant
if (position != element || !isName) {
if (position != element || forceLastTransition) {
steps.add(new Step(StateType._object, new PropertyTransition(propertyName)));
}
} else if (current instanceof JsonObject && position instanceof JsonProperty) {
// if either value or not first in the chain - needed for completion variant
if (position != element || !isName) {
if (position != element || forceLastTransition) {
final String propertyName = ((JsonProperty)position).getName();
steps.add(new Step(StateType._object, new PropertyTransition(propertyName)));
}
@@ -225,10 +232,10 @@ public class JsonSchemaWalker {
}
}
private static class PropertyTransition implements Transition {
public static class PropertyTransition implements Transition {
@NotNull private final String myName;
private PropertyTransition(@NotNull String name) {
protected PropertyTransition(@NotNull String name) {
myName = name;
}
@@ -259,6 +266,11 @@ public class JsonSchemaWalker {
}
}
}
@NotNull
public String getName() {
return myName;
}
}
private static class ArrayTransition implements Transition {
@@ -297,8 +309,8 @@ public class JsonSchemaWalker {
void step(@NotNull JsonSchemaObject parent, @NotNull TransitionResultConsumer resultConsumer);
}
private enum StateType {
_object(JsonSchemaType._object), _array(JsonSchemaType._array), _value(null);
public enum StateType {
_object(JsonSchemaType._object), _array(JsonSchemaType._array), _value(null), _unknown(null);
@Nullable
private final JsonSchemaType myCorrespondingJsonType;
@@ -32,6 +32,7 @@ import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import java.util.Collections;
import java.util.List;
/**
* @author Irina.Chernushina on 3/28/2016.
@@ -290,7 +291,7 @@ public class JsonSchemaCrossReferencesTest extends JsonSchemaHeavyAbstractTest {
public void registerSchemes() {
final String moduleDir = getModuleDir(getProject());
addSchema(new JsonSchemaMappingsConfigurationBase.SchemaInfo("one", moduleDir + "/refToDefinitionInFileSchema.json", false, Collections.emptyList()));
addSchema(new JsonSchemaMappingsConfigurationBase.SchemaInfo("one", moduleDir + "/definitionsSchema.json", false, Collections.emptyList()));
addSchema(new JsonSchemaMappingsConfigurationBase.SchemaInfo("two", moduleDir + "/definitionsSchema.json", false, Collections.emptyList()));
}
@Override
@@ -311,6 +312,62 @@ public class JsonSchemaCrossReferencesTest extends JsonSchemaHeavyAbstractTest {
});
}
public void testFindRefToOtherFile() throws Exception {
skeleton(new Callback() {
@Override
public void registerSchemes() {
final String moduleDir = getModuleDir(getProject());
addSchema(new JsonSchemaMappingsConfigurationBase.SchemaInfo("one", moduleDir + "/refToOtherFileSchema.json", false, Collections.emptyList()));
addSchema(new JsonSchemaMappingsConfigurationBase.SchemaInfo("two", moduleDir + "/definitionsSchema.json", false, Collections.emptyList()));
}
@Override
public void configureFiles() throws Exception {
configureByFiles(null, "/refToOtherFileSchema.json", "/definitionsSchema.json");
}
@Override
public void doCheck() {
int offset = myEditor.getCaretModel().getPrimaryCaret().getOffset();
final PsiReference referenceAt = myFile.findReferenceAt(offset);
Assert.assertNotNull(referenceAt);
final PsiElement resolve = referenceAt.resolve();
Assert.assertNotNull(resolve);
Assert.assertEquals("definitionsSchema.json", resolve.getContainingFile().getName());
}
});
}
public void testNavigateToPropertyDefinitionInPackageJsonSchema() throws Exception {
skeleton(new Callback() {
@Override
public void registerSchemes() {
final String moduleDir = getModuleDir(getProject());
final List<JsonSchemaMappingsConfigurationBase.Item> patterns = Collections.singletonList(
new JsonSchemaMappingsConfigurationBase.Item("package.json", true, false));
addSchema(new JsonSchemaMappingsConfigurationBase.SchemaInfo("one", moduleDir + "/packageJsonSchema.json", false, patterns));
}
@Override
public void configureFiles() throws Exception {
configureByFiles(null, "/package.json", "/packageJsonSchema.json");
}
@Override
public void doCheck() {
final String text = myFile.getText();
final int indexOf = text.indexOf("dependencies");
assertTrue(indexOf > 0);
final PsiReference referenceAt = myFile.findReferenceAt(indexOf);
Assert.assertNotNull(referenceAt);
final PsiElement resolve = referenceAt.resolve();
Assert.assertNotNull(resolve);
Assert.assertEquals("packageJsonSchema.json", resolve.getContainingFile().getName());
Assert.assertEquals("\"dependencies\"", resolve.getText());
}
});
}
@NotNull
private static String getModuleDir(@NotNull final Project project) {
String moduleDir = null;
@@ -0,0 +1,10 @@
{
"name": "ufr-cards-frontend-stub",
"version": "0.0.1",
"description": "",
"dependencies": {
"stylelint": "1.0"
},
"stylelint": {
}
}
@@ -0,0 +1,297 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "JSON schema for package.json files",
"definitions": {
"person": {
"type": [ "object", "string" ],
"required": [ "name" ],
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"email": {
"type": "string",
"format": "email"
}
}
},
"coreProperties": {
"type": "object",
"patternProperties": {
"^_": {
"description": "Any property starting with _ is valid.",
"additionalProperties": true,
"additionalItems": true
}
},
"properties": {
"name": {
"title": "Name of the package",
"type": "string",
"maxLength": 214,
"minLength": 1
},
"version": {
"title": "Version of the package",
"type": "string"
},
"description": {
"title": "Description of the package",
"type": "string"
},
"keywords": {
"title": "Array of keyword strings",
"type": "array",
"items": {
"type": "string"
}
},
"homepage": {
"title": "Url to the project homepage",
"type": "string",
"format": "uri"
},
"bugs": {
"title": "Project's issue tracker and/or email address",
"type": [ "object", "string" ],
"properties": {
"url": {
"type": "string",
"title": "The url to the project's issue tracker",
"format": "uri"
},
"email": {
"type": "string",
"title": "The email address to which issues should be reported",
"format": "email"
}
}
},
"license": {
"type": "string",
"title": "License of the project"
},
"licenses": {
"title": "Licenses of the project",
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
}
}
}
},
"author": {
"title": "Author of package",
"type": [ "object", "string" ],
"required": [ "name" ],
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
},
"email": {
"type": "string",
"format": "email"
}
}
},
"contributors": {
"title": "Contributors of the package",
"type": "array",
"items": {
"$ref": "#/definitions/person"
}
},
"files": {
"title": "Files to include in the project",
"type": "array",
"items": {
"type": "string"
}
},
"main": {
"title": "Entry module to the package",
"type": "string"
},
"jsnext:main": {
"title": "Entry module to the package, ES6 version",
"type": "string"
},
"bin": {
"title": "Executable files to install into PATH",
"type": [ "string", "object" ],
"additionalProperties": {
"type": "string"
}
},
"man": {
"title": "Documentation files for man",
"type": [ "array", "string" ],
"items": {
"type": "string"
}
},
"directories": {
"title": "Structure of the package",
"type": "object",
"properties": {
"bin": {
"title": "Folder for binary files",
"type": "string"
},
"doc": {
"title": "Folder for markdown files",
"type": "string"
},
"example": {
"title": "Folder for example scripts",
"type": "string"
},
"lib": {
"title": "Folder where the bulk of your library is",
"type": "string"
},
"man": {
"title": "Folder that is full of man pages",
"type": "string"
},
"test": {
"title": "Folder for tests",
"type": "string"
}
}
},
"repository": {
"title": "Repository of code",
"type": ["object", "string"],
"properties": {
"type": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
}
}
},
"scripts": {
"title": "Commands to run at various times in the lifecycle of package",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"config": {
"title": "Configuration parameters",
"type": "object",
"additionalProperties": true
},
"dependencies": {
"title": "Dependencies of the package",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"devDependencies": {
"title": "Development dependencies of the package",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"bundleDependencies": {
"type": "array",
"title": "Dependencies bundled when publishing the package",
"items": {
"type": "string"
}
},
"bundledDependencies": {
"type": "array",
"title": "Dependencies bundled when publishing the package",
"items": {
"type": "string"
}
},
"optionalDependencies": {
"title": "Options dependencies of the package",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"peerDependencies": {
"title": "Peer dependencies of the package",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"engines": {
"title": "Compatible node and npm versions",
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"engineStrict": {
"title": "Require compatible engines only",
"type": "boolean"
},
"os": {
"title": "Compatible operating systems",
"type": "array",
"items": {
"type": "string"
}
},
"cpu": {
"title": "Compatible CPU architectures",
"type": "array",
"items": {
"type": "string"
}
},
"preferGlobal": {
"title": "true, if global installation is preferred",
"type": "boolean"
},
"private": {
"title": "true, if package shouldn't be published",
"type": "boolean"
},
"publishConfig": {
"title": "Set of config values to use at publish-time",
"type": "object",
"additionalProperties": true
},
"eslintConfig" : {
"$ref": "https://github.com/SchemaStore/schemastore/tree/master/src/schemas/json/eslintrc.json#"
},
"stylelint" : {
"$ref": "https://jetbrains.com/stylelintrc.json#"
}
}
}
},
"allOf": [
{ "$ref": "#/definitions/coreProperties" }
]
}
@@ -0,0 +1,7 @@
{
"properties": {
"searcher": {
"$ref": "<caret>definitionsSchema.json"
}
}
}