diff options
author | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-08-28 14:13:29 +0200 |
---|---|---|
committer | Andrej Shadura <andrew.shadura@collabora.co.uk> | 2019-08-29 17:48:13 +0200 |
commit | e19ef5983707e6a5c8d127f1ac8f02754cef82fd (patch) | |
tree | 9e3852cb9abc81ed6aa444465928d45fd7763dea /json/src |
New upstream version 0~183.5153.4+dfsg
Diffstat (limited to 'json/src')
177 files changed, 18614 insertions, 0 deletions
diff --git a/json/src/com/intellij/json/JsonBraceMatcher.java b/json/src/com/intellij/json/JsonBraceMatcher.java new file mode 100644 index 00000000..d4a8aad3 --- /dev/null +++ b/json/src/com/intellij/json/JsonBraceMatcher.java @@ -0,0 +1,34 @@ +package com.intellij.json; + +import com.intellij.lang.BracePair; +import com.intellij.lang.PairedBraceMatcher; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class JsonBraceMatcher implements PairedBraceMatcher { + private static final BracePair[] PAIRS = { + new BracePair(JsonElementTypes.L_BRACKET, JsonElementTypes.R_BRACKET, true), + new BracePair(JsonElementTypes.L_CURLY, JsonElementTypes.R_CURLY, true) + }; + + @NotNull + @Override + public BracePair[] getPairs() { + return PAIRS; + } + + @Override + public boolean isPairedBracesAllowedBeforeType(@NotNull IElementType lbraceType, @Nullable IElementType contextType) { + return true; + } + + @Override + public int getCodeConstructStart(PsiFile file, int openingBraceOffset) { + return openingBraceOffset; + } +} diff --git a/json/src/com/intellij/json/JsonBundle.java b/json/src/com/intellij/json/JsonBundle.java new file mode 100644 index 00000000..9ccab90f --- /dev/null +++ b/json/src/com/intellij/json/JsonBundle.java @@ -0,0 +1,36 @@ +package com.intellij.json; + +import com.intellij.CommonBundle; +import com.intellij.reference.SoftReference; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.PropertyKey; + +import java.lang.ref.Reference; +import java.util.ResourceBundle; + +/** + * @author Mikhail Golubev + */ +public class JsonBundle { + + private static Reference<ResourceBundle> ourBundle; + @NonNls public static final String BUNDLE = "com.intellij.json.JsonBundle"; + + private JsonBundle() { + // empty + } + + public static String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, @NotNull Object... params) { + return CommonBundle.message(getBundle(), key, params); + } + + private static ResourceBundle getBundle() { + ResourceBundle bundle = SoftReference.dereference(ourBundle); + if (bundle == null) { + bundle = ResourceBundle.getBundle(BUNDLE); + ourBundle = new SoftReference<>(bundle); + } + return bundle; + } +} diff --git a/json/src/com/intellij/json/JsonBundle.properties b/json/src/com/intellij/json/JsonBundle.properties new file mode 100644 index 00000000..2089ac9a --- /dev/null +++ b/json/src/com/intellij/json/JsonBundle.properties @@ -0,0 +1,66 @@ +json.array=array +json.object=object +json.property=property + +syntax.error.missing.closing.quote=Missing closing quote +syntax.error.illegal.escape.sequence=Illegal escape sequence +syntax.error.illegal.unicode.escape.sequence=Illegal unicode escape sequence +syntax.error.illegal.floating.point.literal=Illegal floating point literal +syntax.error.control.char.in.string=Control character ''{0}'' is not allowed in string literals + +# Inspections +json.inspection.group=JSON and JSON5 + +inspection.compliance.name=Compliance with JSON standard +inspection.compliance5.name=Compliance with JSON5 standard +inspection.compliance.msg.comments=JSON standard does not allow comments. Use JSMin or similar tool to remove comments before parsing. +inspection.compliance.msg.single.quoted.strings=JSON standard does not allow single quoted strings +inspection.compliance.msg.bad.token=JSON standard does not allow such tokens +inspection.compliance.msg.illegal.property.key=JSON standard allows only double quoted string as property key +inspection.compliance.msg.trailing.comma=JSON standard does not allow trailing comma +inspection.compliance.msg.multiple.top.level.values=JSON standard allows only one top-level value + +inspection.compliance.option.comments=Warn about comments +inspection.compliance.option.multiple.top.level.values=Warn about multiple top-level values +inspection.compliance.option.trailing.comma=Warn about trailing commas +inspection.compliance.option.nan.infinity=Warn about NaN and Infinity/-Infinity numeric values + +inspection.duplicate.keys.name=Duplicate keys in object literals +inspection.duplicate.keys.msg.duplicate.keys=Object contains duplicate keys ''{0}'' + +# Formatter +formatter.align.properties.caption=Align + +formatter.align.properties.none=Do not align +formatter.align.properties.on.colon=On colon +formatter.align.properties.on.value=On value + +# Quickfixes and editor actions +quickfix.add.double.quotes.desc=Wrap with double quotes + +surround.with.object.literal.desc=object literal +surround.with.array.literal.desc=array literal +surround.with.quotes.desc=quotes +json.template.context.type=JSON + +json.copy.to.clipboard=Copy {0} to clipboard + +#json schema +json.schema.add.schema.chooser.title=Select JSON Schema File +json.schema.annotation.not.allowed.property=Property ''{0}'' is not allowed +json.schema.conflicting.mappings=Warning: conflicting mappings. <a href="#">Show details</a> +json.schema.file.selector.title=Schema file or URL: +json.schema.file.not.found=File not found +json.schema.inspection.compliance.name=Compliance with JSON schema +json.schema.inspection.case.insensitive.enum=Case insensitive matching for enum values + +json.schema.ref.refs.inspection.name=Unresolved '$ref' and '$schema' references +json.schema.ref.file.not.found=File ''{0}'' not found +json.schema.ref.cannot.resolve.path=Cannot resolve path ''{0}'' +json.schema.ref.cannot.resolve.id=Cannot resolve id ''{0}'' +json.schema.ref.no.array.element=Array doesn''t contain element with index '{0}' +json.schema.ref.no.property=Property ''{0}'' not found + +settings.json.schema.add.mapping=Add mapping +settings.json.schema.edit.mapping=Edit mapping +settings.json.schema.remove.mapping=Remove mapping
\ No newline at end of file diff --git a/json/src/com/intellij/json/JsonDialectUtil.java b/json/src/com/intellij/json/JsonDialectUtil.java new file mode 100644 index 00000000..610e3f99 --- /dev/null +++ b/json/src/com/intellij/json/JsonDialectUtil.java @@ -0,0 +1,25 @@ +// 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 com.intellij.json; + +import com.intellij.lang.Language; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonDialectUtil { + public static boolean isStandardJson(@NotNull PsiElement element) { + return isStandardJson(getLanguage(element)); + } + + public static Language getLanguage(@NotNull PsiElement element) { + PsiFile file = element.getContainingFile(); + if (file == null) return JsonLanguage.INSTANCE; + Language language = file.getLanguage(); + return language instanceof JsonLanguage ? language : JsonLanguage.INSTANCE; + } + + public static boolean isStandardJson(@Nullable Language language) { + return language == JsonLanguage.INSTANCE; + } +} diff --git a/json/src/com/intellij/json/JsonElementType.java b/json/src/com/intellij/json/JsonElementType.java new file mode 100644 index 00000000..54e05dfc --- /dev/null +++ b/json/src/com/intellij/json/JsonElementType.java @@ -0,0 +1,11 @@ +package com.intellij.json; + +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +public class JsonElementType extends IElementType { + public JsonElementType(@NotNull @NonNls String debugName) { + super(debugName, JsonLanguage.INSTANCE); + } +} diff --git a/json/src/com/intellij/json/JsonFileType.java b/json/src/com/intellij/json/JsonFileType.java new file mode 100644 index 00000000..9b891800 --- /dev/null +++ b/json/src/com/intellij/json/JsonFileType.java @@ -0,0 +1,50 @@ +package com.intellij.json; + +import com.intellij.icons.AllIcons; +import com.intellij.lang.Language; +import com.intellij.openapi.fileTypes.LanguageFileType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** + * @author Mikhail Golubev + */ +public class JsonFileType extends LanguageFileType{ + public static final JsonFileType INSTANCE = new JsonFileType(); + public static final String DEFAULT_EXTENSION = "json"; + + protected JsonFileType(Language language) { + super(language); + } + + public JsonFileType() { + super(JsonLanguage.INSTANCE); + } + + @NotNull + @Override + public String getName() { + return "JSON"; + } + + @NotNull + @Override + public String getDescription() { + return "JSON"; + } + + @NotNull + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } + + @Nullable + @Override + public Icon getIcon() { + // TODO: add JSON icon instead + return AllIcons.FileTypes.Json; + } +} diff --git a/json/src/com/intellij/json/JsonFileTypeFactory.java b/json/src/com/intellij/json/JsonFileTypeFactory.java new file mode 100644 index 00000000..2cfaa2f1 --- /dev/null +++ b/json/src/com/intellij/json/JsonFileTypeFactory.java @@ -0,0 +1,15 @@ +package com.intellij.json; + +import com.intellij.openapi.fileTypes.FileTypeConsumer; +import com.intellij.openapi.fileTypes.FileTypeFactory; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonFileTypeFactory extends FileTypeFactory { + @Override + public void createFileTypes(@NotNull FileTypeConsumer consumer) { + consumer.consume(JsonFileType.INSTANCE, JsonFileType.DEFAULT_EXTENSION); + } +} diff --git a/json/src/com/intellij/json/JsonLanguage.java b/json/src/com/intellij/json/JsonLanguage.java new file mode 100644 index 00000000..e43ac474 --- /dev/null +++ b/json/src/com/intellij/json/JsonLanguage.java @@ -0,0 +1,22 @@ +package com.intellij.json; + +import com.intellij.lang.Language; + +public class JsonLanguage extends Language { + public static final JsonLanguage INSTANCE = new JsonLanguage(); + + protected JsonLanguage(String ID, String... mimeTypes) { + super(INSTANCE, ID, mimeTypes); + } + + private JsonLanguage() { + super("JSON", "application/json", "application/vnd.api+json", "application/hal+json"); + } + + @Override + public boolean isCaseSensitive() { + return true; + } + + public boolean hasPermissiveStrings() { return false; } +} diff --git a/json/src/com/intellij/json/JsonLexer.java b/json/src/com/intellij/json/JsonLexer.java new file mode 100644 index 00000000..ce771b9d --- /dev/null +++ b/json/src/com/intellij/json/JsonLexer.java @@ -0,0 +1,12 @@ +package com.intellij.json; + +import com.intellij.lexer.FlexAdapter; + +/** + * @author Mikhail Golubev + */ +public class JsonLexer extends FlexAdapter { + public JsonLexer() { + super(new _JsonLexer()); + } +} diff --git a/json/src/com/intellij/json/JsonNamesValidator.java b/json/src/com/intellij/json/JsonNamesValidator.java new file mode 100644 index 00000000..98a5bb34 --- /dev/null +++ b/json/src/com/intellij/json/JsonNamesValidator.java @@ -0,0 +1,38 @@ +/* + * @author max + */ +package com.intellij.json; + +import com.intellij.lang.refactoring.NamesValidator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +public class JsonNamesValidator implements NamesValidator { + + private final JsonLexer myLexer = new JsonLexer(); + + @Override + public synchronized boolean isKeyword(@NotNull String name, Project project) { + myLexer.start(name); + return JsonParserDefinition.JSON_KEYWORDS.contains( myLexer.getTokenType() ) && myLexer.getTokenEnd() == name.length(); + } + @Override + public synchronized boolean isIdentifier(@NotNull String name, final Project project) { + if (!StringUtil.startsWithChar(name,'\'') && !StringUtil.startsWithChar(name,'\"')) { + name = "\"" + name; + } + + if (!StringUtil.endsWithChar(name,'"') && !StringUtil.endsWithChar(name,'\"')) { + name += "\""; + } + + myLexer.start(name); + IElementType type = myLexer.getTokenType(); + + return myLexer.getTokenEnd() == name.length() && (type == JsonElementTypes.DOUBLE_QUOTED_STRING || + type == JsonElementTypes.SINGLE_QUOTED_STRING); + } + +} diff --git a/json/src/com/intellij/json/JsonParserDefinition.java b/json/src/com/intellij/json/JsonParserDefinition.java new file mode 100644 index 00000000..06ecf6cd --- /dev/null +++ b/json/src/com/intellij/json/JsonParserDefinition.java @@ -0,0 +1,83 @@ +package com.intellij.json; + +import com.intellij.json.psi.impl.JsonFileImpl; +import com.intellij.lang.ASTNode; +import com.intellij.lang.ParserDefinition; +import com.intellij.lang.PsiParser; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.project.Project; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IFileElementType; +import com.intellij.psi.tree.TokenSet; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.json.JsonElementTypes.*; + +public class JsonParserDefinition implements ParserDefinition { + public static final TokenSet WHITE_SPACES = TokenSet.WHITE_SPACE; + public static final TokenSet STRING_LITERALS = TokenSet.create(SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING); + + public static final IFileElementType FILE = new IFileElementType(JsonLanguage.INSTANCE); + + public static final TokenSet JSON_BRACES = TokenSet.create(L_CURLY, R_CURLY); + public static final TokenSet JSON_BRACKETS = TokenSet.create(L_BRACKET, R_BRACKET); + public static final TokenSet JSON_CONTAINERS = TokenSet.create(OBJECT, ARRAY); + public static final TokenSet JSON_BOOLEANS = TokenSet.create(TRUE, FALSE); + public static final TokenSet JSON_KEYWORDS = TokenSet.create(TRUE, FALSE, NULL); + public static final TokenSet JSON_LITERALS = TokenSet.create(STRING_LITERAL, NUMBER_LITERAL, NULL_LITERAL, TRUE, FALSE); + public static final TokenSet JSON_VALUES = TokenSet.orSet(JSON_CONTAINERS, JSON_LITERALS); + public static final TokenSet JSON_COMMENTARIES = TokenSet.create(BLOCK_COMMENT, LINE_COMMENT); + + + @NotNull + @Override + public Lexer createLexer(Project project) { + return new JsonLexer(); + } + + @Override + public PsiParser createParser(Project project) { + return new JsonParser(); + } + + @Override + public IFileElementType getFileNodeType() { + return FILE; + } + + @NotNull + @Override + public TokenSet getWhitespaceTokens() { + return WHITE_SPACES; + } + + @NotNull + @Override + public TokenSet getCommentTokens() { + return JSON_COMMENTARIES; + } + + @NotNull + @Override + public TokenSet getStringLiteralElements() { + return STRING_LITERALS; + } + + @NotNull + @Override + public PsiElement createElement(ASTNode astNode) { + return Factory.createElement(astNode); + } + + @Override + public PsiFile createFile(FileViewProvider fileViewProvider) { + return new JsonFileImpl(fileViewProvider, JsonLanguage.INSTANCE); + } + + @Override + public SpaceRequirements spaceExistenceTypeBetweenTokens(ASTNode astNode, ASTNode astNode2) { + return SpaceRequirements.MAY; + } +} diff --git a/json/src/com/intellij/json/JsonQuoteHandler.java b/json/src/com/intellij/json/JsonQuoteHandler.java new file mode 100644 index 00000000..d2ff4bcf --- /dev/null +++ b/json/src/com/intellij/json/JsonQuoteHandler.java @@ -0,0 +1,40 @@ +package com.intellij.json; + +import com.intellij.codeInsight.editorActions.MultiCharQuoteHandler; +import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler; +import com.intellij.json.editor.JsonTypedHandler; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.highlighter.HighlighterIterator; +import com.intellij.psi.PsiFile; +import com.intellij.psi.TokenType; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class JsonQuoteHandler extends SimpleTokenSetQuoteHandler implements MultiCharQuoteHandler { + public JsonQuoteHandler() { + super(JsonParserDefinition.STRING_LITERALS); + } + + @Nullable + @Override + public CharSequence getClosingQuote(@NotNull HighlighterIterator iterator, int offset) { + final IElementType tokenType = iterator.getTokenType(); + if (tokenType == TokenType.WHITE_SPACE) { + final int index = iterator.getStart() - 1; + if (index >= 0) { + return String.valueOf(iterator.getDocument().getCharsSequence().charAt(index)); + } + } + return tokenType == JsonElementTypes.SINGLE_QUOTED_STRING ? "'" : "\""; + } + + @Override + public void insertClosingQuote(@NotNull Editor editor, int offset, PsiFile file, @NotNull CharSequence closingQuote) { + editor.getDocument().insertString(offset, closingQuote); + JsonTypedHandler.processPairedBracesComma(closingQuote.charAt(0), editor, file); + } +} diff --git a/json/src/com/intellij/json/JsonSpellcheckerStrategy.java b/json/src/com/intellij/json/JsonSpellcheckerStrategy.java new file mode 100644 index 00000000..1463baae --- /dev/null +++ b/json/src/com/intellij/json/JsonSpellcheckerStrategy.java @@ -0,0 +1,87 @@ +package com.intellij.json; + +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiUtilCore; +import com.intellij.spellchecker.inspections.PlainTextSplitter; +import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy; +import com.intellij.spellchecker.tokenizer.TokenConsumer; +import com.intellij.spellchecker.tokenizer.Tokenizer; +import com.intellij.util.ThreeState; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import com.jetbrains.jsonSchema.impl.JsonSchemaResolver; +import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JsonSpellcheckerStrategy extends SpellcheckingStrategy { + private final Tokenizer<JsonStringLiteral> ourStringLiteralTokenizer = new Tokenizer<JsonStringLiteral>() { + @Override + public void tokenize(@NotNull JsonStringLiteral element, TokenConsumer consumer) { + final PlainTextSplitter textSplitter = PlainTextSplitter.getInstance(); + if (element.textContains('\\')) { + final List<Pair<TextRange, String>> fragments = element.getTextFragments(); + for (Pair<TextRange, String> fragment : fragments) { + final TextRange fragmentRange = fragment.getFirst(); + final String escaped = fragment.getSecond(); + // Fragment without escaping, also not a broken escape sequence or a unicode code point + if (escaped.length() == fragmentRange.getLength() && !escaped.startsWith("\\")) { + consumer.consumeToken(element, escaped, false, fragmentRange.getStartOffset(), TextRange.allOf(escaped), textSplitter); + } + } + } + else { + consumer.consumeToken(element, textSplitter); + } + } + }; + + private static boolean matchesNameFromSchema(@NotNull JsonStringLiteral element) { + final VirtualFile file = PsiUtilCore.getVirtualFile(element); + if (file == null) return false; + + final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject()); + if (!service.isApplicableToFile(file)) return false; + final JsonSchemaObject rootSchema = service.getSchemaObject(file); + if (rootSchema == null) return false; + + String value = element.getValue(); + if (StringUtil.isEmpty(value)) return false; + + JsonOriginalPsiWalker walker = JsonLikePsiWalker.JSON_ORIGINAL_PSI_WALKER; + final PsiElement checkable = walker.goUpToCheckable(element); + if (checkable == null) return false; + final ThreeState isName = walker.isName(checkable); + final List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(checkable, isName == ThreeState.NO); + if (position == null || position.isEmpty() && isName == ThreeState.NO) return false; + + final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(rootSchema, false, position).resolve(); + if (schemas.isEmpty()) return false; + + return schemas.stream().anyMatch(s -> s.getProperties().keySet().contains(value) + || s.getMatchingPatternPropertySchema(value) != null); + } + + @NotNull + @Override + public Tokenizer getTokenizer(PsiElement element) { + if (element instanceof JsonStringLiteral) { + return matchesNameFromSchema((JsonStringLiteral)element) + ? EMPTY_TOKENIZER + : ourStringLiteralTokenizer; + } + return super.getTokenizer(element); + } +} diff --git a/json/src/com/intellij/json/JsonTokenType.java b/json/src/com/intellij/json/JsonTokenType.java new file mode 100644 index 00000000..a7752cd8 --- /dev/null +++ b/json/src/com/intellij/json/JsonTokenType.java @@ -0,0 +1,11 @@ +package com.intellij.json; + +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +public class JsonTokenType extends IElementType { + public JsonTokenType(@NotNull @NonNls String debugName) { + super(debugName, JsonLanguage.INSTANCE); + } +} diff --git a/json/src/com/intellij/json/JsonUtil.java b/json/src/com/intellij/json/JsonUtil.java new file mode 100644 index 00000000..51f751b9 --- /dev/null +++ b/json/src/com/intellij/json/JsonUtil.java @@ -0,0 +1,94 @@ +// 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 com.intellij.json; + +import com.intellij.json.psi.*; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.util.ObjectUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Mikhail Golubev + */ +public class JsonUtil { + private JsonUtil() { + // empty + } + + /** + * Clone of C# "as" operator. + * Checks if expression has correct type and casts it if it has. Returns null otherwise. + * It saves coder from "instanceof / cast" chains. + * + * Copied from PyCharm's {@code PyUtil}. + * + * @param expression expression to check + * @param cls class to cast + * @param <T> class to cast + * @return expression casted to appropriate type (if could be casted). Null otherwise. + */ + @Nullable + @SuppressWarnings("unchecked") + public static <T> T as(@Nullable final Object expression, @NotNull final Class<T> cls) { + if (expression == null) { + return null; + } + if (cls.isAssignableFrom(expression.getClass())) { + return (T)expression; + } + return null; + } + + @Nullable + public static <T extends JsonElement> T getPropertyValueOfType(@NotNull final JsonObject object, @NotNull final String name, + @NotNull final Class<T> clazz) { + final JsonProperty property = object.findProperty(name); + if (property == null) return null; + return ObjectUtils.tryCast(property.getValue(), clazz); + } + + @Nullable + public static List<String> getChildAsStringList(@NotNull final JsonObject object, @NotNull final String name) { + final JsonArray array = getPropertyValueOfType(object, name, JsonArray.class); + if (array != null) return array.getValueList().stream().filter(value -> value instanceof JsonStringLiteral) + .map(value -> StringUtil.unquoteString(value.getText())).collect(Collectors.toList()); + return null; + } + + @Nullable + public static List<String> getChildAsSingleStringOrList(@NotNull final JsonObject object, @NotNull final String name) { + final List<String> list = getChildAsStringList(object, name); + if (list != null) return list; + final JsonStringLiteral literal = getPropertyValueOfType(object, name, JsonStringLiteral.class); + return literal == null ? null : Collections.singletonList(StringUtil.unquoteString(literal.getText())); + } + + public static boolean isArrayElement(@NotNull PsiElement element) { + return element instanceof JsonValue && element.getParent() instanceof JsonArray; + } + + public static int getArrayIndexOfItem(@NotNull PsiElement e) { + PsiElement parent = e.getParent(); + if (!(parent instanceof JsonArray)) return -1; + List<JsonValue> elements = ((JsonArray)parent).getValueList(); + for (int i = 0; i < elements.size(); i++) { + if (e == elements.get(i)) { + return i; + } + } + return -1; + } + + public static boolean isJsonFile(@NotNull VirtualFile file) { + FileType type = file.getFileType(); + return type instanceof LanguageFileType && ((LanguageFileType)type).getLanguage() instanceof JsonLanguage; + } +} diff --git a/json/src/com/intellij/json/_JsonLexer.flex b/json/src/com/intellij/json/_JsonLexer.flex new file mode 100644 index 00000000..e48e9b8d --- /dev/null +++ b/json/src/com/intellij/json/_JsonLexer.flex @@ -0,0 +1,58 @@ +package com.intellij.json; + +import com.intellij.lexer.FlexLexer; +import com.intellij.psi.tree.IElementType; + +import static com.intellij.psi.TokenType.BAD_CHARACTER; +import static com.intellij.psi.TokenType.WHITE_SPACE; +import static com.intellij.json.JsonElementTypes.*; + +%% + +%{ + public _JsonLexer() { + this((java.io.Reader)null); + } +%} + +%public +%class _JsonLexer +%implements FlexLexer +%function advance +%type IElementType +%unicode + +EOL=\R +WHITE_SPACE=\s+ + +LINE_COMMENT="//".* +BLOCK_COMMENT="/"\*([^*]|\*+[^*/])*(\*+"/")? +DOUBLE_QUOTED_STRING=\"([^\\\"\r\n]|\\[^\r\n])*\"? +SINGLE_QUOTED_STRING='([^\\'\r\n]|\\[^\r\n])*'? +NUMBER=(-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]*)?)|Infinity|-Infinity|NaN +IDENTIFIER=[[:jletterdigit:]~!()*\-."/"@\^<>=]+ + +%% +<YYINITIAL> { + {WHITE_SPACE} { return WHITE_SPACE; } + + "{" { return L_CURLY; } + "}" { return R_CURLY; } + "[" { return L_BRACKET; } + "]" { return R_BRACKET; } + "," { return COMMA; } + ":" { return COLON; } + "true" { return TRUE; } + "false" { return FALSE; } + "null" { return NULL; } + + {LINE_COMMENT} { return LINE_COMMENT; } + {BLOCK_COMMENT} { return BLOCK_COMMENT; } + {DOUBLE_QUOTED_STRING} { return DOUBLE_QUOTED_STRING; } + {SINGLE_QUOTED_STRING} { return SINGLE_QUOTED_STRING; } + {NUMBER} { return NUMBER; } + {IDENTIFIER} { return IDENTIFIER; } + +} + +[^] { return BAD_CHARACTER; } diff --git a/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java b/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java new file mode 100644 index 00000000..7ab178f8 --- /dev/null +++ b/json/src/com/intellij/json/breadcrumbs/JsonBreadcrumbsProvider.java @@ -0,0 +1,73 @@ +package com.intellij.json.breadcrumbs; + +import com.intellij.json.JsonBundle; +import com.intellij.json.JsonLanguage; +import com.intellij.json.JsonUtil; +import com.intellij.json.navigation.JsonQualifiedNameKind; +import com.intellij.json.navigation.JsonQualifiedNameProvider; +import com.intellij.json.psi.JsonProperty; +import com.intellij.lang.Language; +import com.intellij.openapi.ide.CopyPasteManager; +import com.intellij.psi.PsiElement; +import com.intellij.ui.breadcrumbs.BreadcrumbsProvider; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.impl.JsonSchemaDocumentationProvider; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JsonBreadcrumbsProvider implements BreadcrumbsProvider { + private static final Language[] LANGUAGES = new Language[]{JsonLanguage.INSTANCE}; + + @Override + public Language[] getLanguages() { + return LANGUAGES; + } + + @Override + public boolean acceptElement(@NotNull PsiElement e) { + return e instanceof JsonProperty || JsonUtil.isArrayElement(e); + } + + @NotNull + @Override + public String getElementInfo(@NotNull PsiElement e) { + if (e instanceof JsonProperty) { + return ((JsonProperty)e).getName(); + } + else if (JsonUtil.isArrayElement(e)) { + int i = JsonUtil.getArrayIndexOfItem(e); + if (i != -1) return String.valueOf(i); + } + throw new AssertionError("Breadcrumbs can be extracted only from JsonProperty elements or JsonArray child items"); + } + + @Nullable + @Override + public String getElementTooltip(@NotNull PsiElement e) { + return JsonSchemaDocumentationProvider.findSchemaAndGenerateDoc(e, null, true, null); + } + + @NotNull + @Override + public List<? extends Action> getContextActions(@NotNull PsiElement element) { + JsonQualifiedNameKind[] values = JsonQualifiedNameKind.values(); + List<Action> actions = ContainerUtil.newArrayListWithCapacity(values.length); + for (JsonQualifiedNameKind kind: values) { + actions.add(new AbstractAction(JsonBundle.message("json.copy.to.clipboard", kind.toString())) { + @Override + public void actionPerformed(ActionEvent e) { + CopyPasteManager.getInstance().setContents(new StringSelection(JsonQualifiedNameProvider.generateQualifiedName(element, kind))); + } + }); + } + return actions; + } +} diff --git a/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java b/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java new file mode 100644 index 00000000..4d05bf51 --- /dev/null +++ b/json/src/com/intellij/json/codeinsight/JsonCompletionContributor.java @@ -0,0 +1,61 @@ +/* + * Copyright 2000-2014 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.intellij.json.codeinsight; + +import com.intellij.codeInsight.completion.*; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.patterns.PsiElementPattern; +import com.intellij.psi.PsiElement; +import com.intellij.util.ProcessingContext; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.patterns.PlatformPatterns.psiElement; + +/** + * @author Mikhail Golubev + */ +public class JsonCompletionContributor extends CompletionContributor { + + private static final PsiElementPattern.Capture<PsiElement> AFTER_COLON_IN_PROPERTY = psiElement() + .afterLeaf(":").withSuperParent(2, JsonProperty.class) + .andNot(psiElement().withParent(JsonStringLiteral.class)); + + private static final PsiElementPattern.Capture<PsiElement> AFTER_COMMA_OR_BRACKET_IN_ARRAY = psiElement() + .afterLeaf("[", ",").withSuperParent(2, JsonArray.class) + .andNot(psiElement().withParent(JsonStringLiteral.class)); + + public JsonCompletionContributor() { + extend(CompletionType.BASIC, AFTER_COLON_IN_PROPERTY, MyKeywordsCompletionProvider.INSTANCE); + extend(CompletionType.BASIC, AFTER_COMMA_OR_BRACKET_IN_ARRAY, MyKeywordsCompletionProvider.INSTANCE); + } + + private static class MyKeywordsCompletionProvider extends CompletionProvider<CompletionParameters> { + private static final MyKeywordsCompletionProvider INSTANCE = new MyKeywordsCompletionProvider(); + private static final String[] KEYWORDS = new String[]{"null", "true", "false"}; + + @Override + protected void addCompletions(@NotNull CompletionParameters parameters, + @NotNull ProcessingContext context, + @NotNull CompletionResultSet result) { + for (String keyword : KEYWORDS) { + result.addElement(LookupElementBuilder.create(keyword).bold()); + } + } + } +} diff --git a/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java b/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java new file mode 100644 index 00000000..c9747679 --- /dev/null +++ b/json/src/com/intellij/json/codeinsight/JsonDuplicatePropertyKeysInspection.java @@ -0,0 +1,156 @@ +/* + * Copyright 2000-2015 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.intellij.json.codeinsight; + +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.LocalQuickFixBase; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.icons.AllIcons; +import com.intellij.json.JsonBundle; +import com.intellij.json.psi.JsonElementVisitor; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.ScrollType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.ui.popup.PopupStep; +import com.intellij.openapi.ui.popup.util.BaseListPopupStep; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.SmartPointerManager; +import com.intellij.psi.SmartPsiElementPointer; +import com.intellij.psi.util.PsiEditorUtil; +import com.intellij.util.containers.MultiMap; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Mikhail Golubev + */ +public class JsonDuplicatePropertyKeysInspection extends LocalInspectionTool { + @Nls + @NotNull + @Override + public String getDisplayName() { + return JsonBundle.message("inspection.duplicate.keys.name"); + } + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) { + return new JsonElementVisitor() { + @Override + public void visitObject(@NotNull JsonObject o) { + final MultiMap<String, PsiElement> keys = new MultiMap<>(); + for (JsonProperty property : o.getPropertyList()) { + keys.putValue(property.getName(), property.getNameElement()); + } + for (Map.Entry<String, Collection<PsiElement>> entry : keys.entrySet()) { + final Collection<PsiElement> sameNamedKeys = entry.getValue(); + final String entryKey = entry.getKey(); + if (sameNamedKeys.size() > 1) { + for (PsiElement element : sameNamedKeys) { + holder.registerProblem(element, JsonBundle.message("inspection.duplicate.keys.msg.duplicate.keys", entryKey), + new NavigateToDuplicatesFix(sameNamedKeys, element, entryKey)); + } + } + } + } + }; + } + + private static class NavigateToDuplicatesFix extends LocalQuickFixBase { + @NotNull private final Collection<SmartPsiElementPointer> mySameNamedKeys; + @NotNull private final SmartPsiElementPointer myElement; + @NotNull private final String myEntryKey; + + private NavigateToDuplicatesFix(@NotNull Collection<PsiElement> sameNamedKeys, @NotNull PsiElement element, @NotNull String entryKey) { + super("Navigate to duplicates"); + mySameNamedKeys = sameNamedKeys.stream().map(k -> SmartPointerManager.createPointer(k)).collect(Collectors.toList()); + myElement = SmartPointerManager.createPointer(element); + myEntryKey = entryKey; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + final Editor editor = + PsiEditorUtil.Service.getInstance().findEditorByPsiElement(descriptor.getPsiElement()); + if (editor == null) return; + applyFix(editor); + } + + + private void applyFix(@NotNull Editor editor) { + final PsiElement currentElement = myElement.getElement(); + if (mySameNamedKeys.size() == 2) { + final Iterator<SmartPsiElementPointer> iterator = mySameNamedKeys.iterator(); + final PsiElement next = iterator.next().getElement(); + PsiElement toNavigate = next != currentElement ? next : iterator.next().getElement(); + if (toNavigate == null) return; + navigateTo(editor, toNavigate); + } + else { + final List<PsiElement> allElements = + mySameNamedKeys.stream().map(k -> k.getElement()).filter(k -> k != currentElement).collect(Collectors.toList()); + JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<PsiElement>("Duplicates of '" + myEntryKey + "'", allElements) { + @NotNull + @Override + public Icon getIconFor(PsiElement aValue) { + return AllIcons.Nodes.Property; + } + + @NotNull + @Override + public String getTextFor(PsiElement value) { + return "'" + myEntryKey + "' at line #" + editor.getDocument().getLineNumber(value.getTextOffset()); + } + + @Override + public int getDefaultOptionIndex() { + return 0; + } + + @Nullable + @Override + public PopupStep onChosen(PsiElement selectedValue, boolean finalChoice) { + navigateTo(editor, selectedValue); + return PopupStep.FINAL_CHOICE; + } + + @Override + public boolean isSpeedSearchEnabled() { + return true; + } + }).showInBestPositionFor(editor); + } + } + + private static void navigateTo(@NotNull Editor editor, @NotNull PsiElement toNavigate) { + editor.getCaretModel().moveToOffset(toNavigate.getTextOffset()); + editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + } +} diff --git a/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java b/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java new file mode 100644 index 00000000..4ad034d8 --- /dev/null +++ b/json/src/com/intellij/json/codeinsight/JsonLiteralAnnotator.java @@ -0,0 +1,93 @@ +/* + * Copyright 2000-2015 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.intellij.json.codeinsight; + +import com.intellij.json.JsonBundle; +import com.intellij.json.highlighting.JsonSyntaxHighlighterFactory; +import com.intellij.json.psi.JsonNumberLiteral; +import com.intellij.json.psi.JsonPsiUtil; +import com.intellij.json.psi.JsonReferenceExpression; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.Annotator; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JsonLiteralAnnotator implements Annotator { + + private static class Holder { + private static final boolean DEBUG = ApplicationManager.getApplication().isUnitTestMode(); + } + + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + JsonLiteralChecker[] extensions = JsonLiteralChecker.EP_NAME.getExtensions(); + if (element instanceof JsonReferenceExpression) { + highlightPropertyKey(element, holder); + } + else if (element instanceof JsonStringLiteral) { + final JsonStringLiteral stringLiteral = (JsonStringLiteral)element; + final int elementOffset = element.getTextOffset(); + highlightPropertyKey(element, holder); + final String text = JsonPsiUtil.getElementTextWithoutHostEscaping(element); + final int length = text.length(); + + // Check that string literal is closed properly + if (length <= 1 || text.charAt(0) != text.charAt(length - 1) || JsonPsiUtil.isEscapedChar(text, length - 1)) { + holder.createErrorAnnotation(element, JsonBundle.message("syntax.error.missing.closing.quote")); + } + + // Check escapes + final List<Pair<TextRange, String>> fragments = stringLiteral.getTextFragments(); + for (Pair<TextRange, String> fragment: fragments) { + for (JsonLiteralChecker checker: extensions) { + if (!checker.isApplicable(element)) continue; + Pair<TextRange, String> error = checker.getErrorForStringFragment(fragment, stringLiteral); + if (error != null) { + holder.createErrorAnnotation(error.getFirst().shiftRight(elementOffset), error.second); + } + } + } + } + else if (element instanceof JsonNumberLiteral) { + String text = null; + for (JsonLiteralChecker checker: extensions) { + if (!checker.isApplicable(element)) continue; + if (text == null) { + text = JsonPsiUtil.getElementTextWithoutHostEscaping(element); + } + String error = checker.getErrorForNumericLiteral(text); + if (error != null) { + holder.createErrorAnnotation(element, error); + } + } + } + } + + private static void highlightPropertyKey(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (JsonPsiUtil.isPropertyKey(element)) { + holder.createInfoAnnotation(element, Holder.DEBUG ? "property key" : null).setTextAttributes(JsonSyntaxHighlighterFactory.JSON_PROPERTY_KEY); + } + } +} diff --git a/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java b/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java new file mode 100644 index 00000000..6b8fe130 --- /dev/null +++ b/json/src/com/intellij/json/codeinsight/JsonLiteralChecker.java @@ -0,0 +1,21 @@ +// 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 com.intellij.json.codeinsight; + +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.Nullable; + +public interface JsonLiteralChecker { + ExtensionPointName<JsonLiteralChecker> EP_NAME = ExtensionPointName.create("com.intellij.json.jsonLiteralChecker"); + + @Nullable + String getErrorForNumericLiteral(String literalText); + + @Nullable + Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragmentText, JsonStringLiteral stringLiteral); + + boolean isApplicable(PsiElement element); +} diff --git a/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java new file mode 100644 index 00000000..564c2d85 --- /dev/null +++ b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceInspection.java @@ -0,0 +1,234 @@ +// 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 com.intellij.json.codeinsight; + +import com.intellij.codeHighlighting.HighlightDisplayLevel; +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel; +import com.intellij.json.JsonBundle; +import com.intellij.json.JsonDialectUtil; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.psi.*; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** + * Compliance checks include + * <ul> + * <li>Usage of line and block commentaries</li> + * <li>Usage of single quoted strings</li> + * <li>Usage of identifiers (unqouted words)</li> + * <li>Not double quoted string literal is used as property key</li> + * <li>Multiple top-level values</li> + * </ul> + * + * @author Mikhail Golubev + */ +public class JsonStandardComplianceInspection extends LocalInspectionTool { + private static final Logger LOG = Logger.getInstance(JsonStandardComplianceInspection.class); + + public boolean myWarnAboutComments = true; + public boolean myWarnAboutNanInfinity = true; + public boolean myWarnAboutTrailingCommas = true; + public boolean myWarnAboutMultipleTopLevelValues = true; + + @Override + @NotNull + public String getDisplayName() { + return JsonBundle.message("inspection.compliance.name"); + } + + @NotNull + @Override + public HighlightDisplayLevel getDefaultLevel() { + return HighlightDisplayLevel.ERROR; + } + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) { + if (!JsonDialectUtil.isStandardJson(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR; + return new StandardJsonValidatingElementVisitor(holder); + } + + @Nullable + private static PsiElement findTrailingComma(@NotNull JsonContainer container, @NotNull IElementType ending) { + final PsiElement lastChild = container.getLastChild(); + if (lastChild.getNode().getElementType() != ending) { + return null; + } + final PsiElement beforeEnding = PsiTreeUtil.skipWhitespacesAndCommentsBackward(lastChild); + if (beforeEnding != null && beforeEnding.getNode().getElementType() == JsonElementTypes.COMMA) { + return beforeEnding; + } + return null; + } + + + @Override + public JComponent createOptionsPanel() { + final MultipleCheckboxOptionsPanel optionsPanel = new MultipleCheckboxOptionsPanel(this); + optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.comments"), "myWarnAboutComments"); + optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.multiple.top.level.values"), "myWarnAboutMultipleTopLevelValues"); + optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.trailing.comma"), "myWarnAboutTrailingCommas"); + optionsPanel.addCheckbox(JsonBundle.message("inspection.compliance.option.nan.infinity"), "myWarnAboutNanInfinity"); + return optionsPanel; + } + + private static class AddDoubleQuotesFix implements LocalQuickFix { + @NotNull + @Override + public String getFamilyName() { + return JsonBundle.message("quickfix.add.double.quotes.desc"); + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + final PsiElement element = descriptor.getPsiElement(); + final String rawText = element.getText(); + if (element instanceof JsonLiteral || element instanceof JsonReferenceExpression) { + String content = JsonPsiUtil.stripQuotes(rawText); + if (element instanceof JsonStringLiteral && rawText.startsWith("'")) { + content = escapeSingleQuotedStringContent(content); + } + final PsiElement replacement = new JsonElementGenerator(project).createValue("\"" + content + "\""); + CodeStyleManager.getInstance(project).performActionWithFormatterDisabled((Runnable)() -> element.replace(replacement)); + } + else { + LOG.error("Quick fix was applied to unexpected element", rawText, element.getParent().getText()); + } + } + + @NotNull + private static String escapeSingleQuotedStringContent(@NotNull String content) { + final StringBuilder result = new StringBuilder(); + boolean nextCharEscaped = false; + for (int i = 0; i < content.length(); i++) { + final char c = content.charAt(i); + if ((nextCharEscaped && c != '\'') || (!nextCharEscaped && c == '"')) { + result.append('\\'); + } + if (c != '\\' || nextCharEscaped) { + result.append(c); + nextCharEscaped = false; + } + else { + nextCharEscaped = true; + } + } + if (nextCharEscaped) { + result.append('\\'); + } + return result.toString(); + } + } + + protected class StandardJsonValidatingElementVisitor extends JsonElementVisitor { + private final ProblemsHolder myHolder; + + public StandardJsonValidatingElementVisitor(ProblemsHolder holder) {myHolder = holder;} + + protected boolean allowComments() { return false; } + protected boolean allowSingleQuotes() { return false; } + protected boolean allowIdentifierPropertyNames() { return false; } + protected boolean allowTrailingCommas() { return false; } + + protected boolean isValidPropertyName(@NotNull PsiElement literal) { + return literal instanceof JsonLiteral && JsonPsiUtil.getElementTextWithoutHostEscaping(literal).startsWith("\""); + } + + @Override + public void visitComment(PsiComment comment) { + if (!allowComments() && myWarnAboutComments) { + if (JsonStandardComplianceProvider.shouldWarnAboutComment(comment)) { + myHolder.registerProblem(comment, JsonBundle.message("inspection.compliance.msg.comments")); + } + } + } + + @Override + public void visitStringLiteral(@NotNull JsonStringLiteral stringLiteral) { + if (!allowSingleQuotes() && JsonPsiUtil.getElementTextWithoutHostEscaping(stringLiteral).startsWith("'")) { + myHolder.registerProblem(stringLiteral, JsonBundle.message("inspection.compliance.msg.single.quoted.strings"), + new AddDoubleQuotesFix()); + } + // May be illegal property key as well + super.visitStringLiteral(stringLiteral); + } + + @Override + public void visitLiteral(@NotNull JsonLiteral literal) { + if (JsonPsiUtil.isPropertyKey(literal) && !isValidPropertyName(literal)) { + myHolder.registerProblem(literal, JsonBundle.message("inspection.compliance.msg.illegal.property.key"), new AddDoubleQuotesFix()); + } + + // for standard JSON, the inspection for NaN, Infinity and -Infinity is now configurable + if (!allowNanInfinity() && literal instanceof JsonNumberLiteral && myWarnAboutNanInfinity) { + final String text = JsonPsiUtil.getElementTextWithoutHostEscaping(literal); + if (StandardJsonLiteralChecker.INF.equals(text) || + StandardJsonLiteralChecker.MINUS_INF.equals(text) || + StandardJsonLiteralChecker.NAN.equals(text)) { + myHolder.registerProblem(literal, JsonBundle.message("syntax.error.illegal.floating.point.literal")); + } + } + super.visitLiteral(literal); + } + + protected boolean allowNanInfinity() { + return false; + } + + @Override + public void visitReferenceExpression(@NotNull JsonReferenceExpression reference) { + if (!allowIdentifierPropertyNames() || !JsonPsiUtil.isPropertyKey(reference) || !isValidPropertyName(reference)) { + myHolder.registerProblem(reference, JsonBundle.message("inspection.compliance.msg.bad.token"), new AddDoubleQuotesFix()); + } + // May be illegal property key as well + super.visitReferenceExpression(reference); + } + + @Override + public void visitArray(@NotNull JsonArray array) { + if (myWarnAboutTrailingCommas && !allowTrailingCommas()) { + final PsiElement trailingComma = findTrailingComma(array, JsonElementTypes.R_BRACKET); + if (trailingComma != null) { + myHolder.registerProblem(trailingComma, JsonBundle.message("inspection.compliance.msg.trailing.comma")); + } + } + super.visitArray(array); + } + + @Override + public void visitObject(@NotNull JsonObject object) { + if (myWarnAboutTrailingCommas && !allowTrailingCommas()) { + final PsiElement trailingComma = findTrailingComma(object, JsonElementTypes.R_CURLY); + if (trailingComma != null) { + myHolder.registerProblem(trailingComma, JsonBundle.message("inspection.compliance.msg.trailing.comma")); + } + } + super.visitObject(object); + } + + @Override + public void visitValue(@NotNull JsonValue value) { + if (value.getContainingFile() instanceof JsonFile) { + final JsonFile jsonFile = (JsonFile)value.getContainingFile(); + if (myWarnAboutMultipleTopLevelValues && value.getParent() == jsonFile && value != jsonFile.getTopLevelValue()) { + myHolder.registerProblem(value, JsonBundle.message("inspection.compliance.msg.multiple.top.level.values")); + } + } + } + } +} diff --git a/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java new file mode 100644 index 00000000..996cc82a --- /dev/null +++ b/json/src/com/intellij/json/codeinsight/JsonStandardComplianceProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2014 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.intellij.json.codeinsight; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.psi.PsiComment; +import org.jetbrains.annotations.NotNull; + +/** + * Allows to configure a compliance level for JSON. + * For example, some tools ignore comments in JSON silently when parsing, so there is no need to warn users about it. + */ +public abstract class JsonStandardComplianceProvider { + public static final ExtensionPointName<JsonStandardComplianceProvider> EP_NAME = + ExtensionPointName.create("com.intellij.json.jsonStandardComplianceProvider"); + + public abstract boolean isCommentAllowed(@NotNull PsiComment comment); + + public static boolean shouldWarnAboutComment(@NotNull PsiComment comment) { + JsonStandardComplianceProvider[] providers = EP_NAME.getExtensions(); + if (providers.length == 0) { + return true; + } + for (JsonStandardComplianceProvider provider : providers) { + if (provider.isCommentAllowed(comment)) { + return false; + } + } + return true; + } +} diff --git a/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java b/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java new file mode 100644 index 00000000..ebb20bf5 --- /dev/null +++ b/json/src/com/intellij/json/codeinsight/JsonStringPropertyInsertHandler.java @@ -0,0 +1,84 @@ +// 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 com.intellij.json.codeinsight; + +import com.intellij.codeInsight.AutoPopupController; +import com.intellij.codeInsight.completion.InsertHandler; +import com.intellij.codeInsight.completion.InsertionContext; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ObjectUtils; +import org.jetbrains.annotations.NotNull; + +public class JsonStringPropertyInsertHandler implements InsertHandler<LookupElement> { + + private final String myNewValue; + + public JsonStringPropertyInsertHandler(@NotNull String newValue) { + myNewValue = newValue; + } + + @Override + public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) { + PsiElement element = context.getFile().findElementAt(context.getStartOffset()); + JsonStringLiteral literal = PsiTreeUtil.getParentOfType(element, JsonStringLiteral.class, false); + if (literal == null) return; + JsonProperty property = ObjectUtils.tryCast(literal.getParent(), JsonProperty.class); + if (property == null) return; + final TextRange toDelete; + String textToInsert = ""; + TextRange literalRange = literal.getTextRange(); + if (literal.getValue().equals(myNewValue)) { + toDelete = new TextRange(literalRange.getEndOffset(), literalRange.getEndOffset()); + } + else { + toDelete = literalRange; + textToInsert = StringUtil.wrapWithDoubleQuote(myNewValue); + } + int newCaretOffset = literalRange.getStartOffset() + 1 + myNewValue.length(); + boolean showAutoPopup = false; + if (property.getNameElement().equals(literal)) { + if (property.getValue() == null) { + textToInsert += ":\"\""; + newCaretOffset += 3; // "package<caret offset>":"<new caret offset>" + if (needCommaAfter(property)) { + textToInsert += ","; + } + showAutoPopup = true; + } + } + context.getDocument().replaceString(toDelete.getStartOffset(), toDelete.getEndOffset(), textToInsert); + context.getEditor().getCaretModel().moveToOffset(newCaretOffset); + reformat(context, toDelete.getStartOffset(), toDelete.getStartOffset() + textToInsert.length()); + if (showAutoPopup) { + AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null); + } + } + + private static boolean needCommaAfter(@NotNull JsonProperty property) { + PsiElement element = property.getNextSibling(); + while (element != null) { + if (element instanceof JsonProperty) { + return true; + } + if (element.getNode().getElementType() == JsonElementTypes.COMMA) { + return false; + } + element = element.getNextSibling(); + } + return false; + } + + private static void reformat(@NotNull InsertionContext context, int startOffset, int endOffset) { + PsiDocumentManager.getInstance(context.getProject()).commitDocument(context.getDocument()); + CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(context.getProject()); + codeStyleManager.reformatText(context.getFile(), startOffset, endOffset); + } +} diff --git a/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java b/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java new file mode 100644 index 00000000..06b87816 --- /dev/null +++ b/json/src/com/intellij/json/codeinsight/StandardJsonLiteralChecker.java @@ -0,0 +1,73 @@ +// 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 com.intellij.json.codeinsight; + +import com.intellij.json.JsonBundle; +import com.intellij.json.JsonDialectUtil; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Pattern; + +public class StandardJsonLiteralChecker implements JsonLiteralChecker { + public static final Pattern VALID_ESCAPE = Pattern.compile("\\\\([\"\\\\/bfnrt]|u[0-9a-fA-F]{4})"); + private static final Pattern VALID_NUMBER_LITERAL = Pattern.compile("-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?"); + public static final String INF = "Infinity"; + public static final String MINUS_INF = "-Infinity"; + public static final String NAN = "NaN"; + + @Nullable + @Override + public String getErrorForNumericLiteral(String literalText) { + if (!INF.equals(literalText) && + !MINUS_INF.equals(literalText) && + !NAN.equals(literalText) && + !VALID_NUMBER_LITERAL.matcher(literalText).matches()) { + return JsonBundle.message("syntax.error.illegal.floating.point.literal"); + } + return null; + } + + @Nullable + @Override + public Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragment, JsonStringLiteral stringLiteral) { + if (fragment.getSecond().chars().anyMatch(c -> c <= '\u001F')) { // fragments are cached, string values - aren't; go inside only if we encountered a potentially 'wrong' char + final String text = stringLiteral.getText(); + if (new TextRange(0, text.length()).contains(fragment.first)) { + final int startOffset = fragment.first.getStartOffset(); + final String part = text.substring(startOffset, fragment.first.getEndOffset()); + char[] array = part.toCharArray(); + for (int i = 0; i < array.length; i++) { + char c = array[i]; + if (c <= '\u001F') { + return Pair.create(new TextRange(startOffset + i, startOffset + i + 1), + JsonBundle + .message("syntax.error.control.char.in.string", "\\u" + Integer.toHexString(c | 0x10000).substring(1))); + } + } + } + } + final String error = getStringError(fragment.second); + return error == null ? null : Pair.create(fragment.first, error); + } + + @Nullable + public static String getStringError(String fragmentText) { + if (fragmentText.startsWith("\\") && fragmentText.length() > 1 && !VALID_ESCAPE.matcher(fragmentText).matches()) { + if (fragmentText.startsWith("\\u")) { + return JsonBundle.message("syntax.error.illegal.unicode.escape.sequence"); + } + else { + return JsonBundle.message("syntax.error.illegal.escape.sequence"); + } + } + return null; + } + + @Override + public boolean isApplicable(PsiElement element) { + return JsonDialectUtil.isStandardJson(element); + } +} diff --git a/json/src/com/intellij/json/editor/JsonCommenter.java b/json/src/com/intellij/json/editor/JsonCommenter.java new file mode 100644 index 00000000..699f7006 --- /dev/null +++ b/json/src/com/intellij/json/editor/JsonCommenter.java @@ -0,0 +1,41 @@ +package com.intellij.json.editor; + +import com.intellij.lang.Commenter; +import org.jetbrains.annotations.Nullable; + +/** + * JSON standard (RFC 4627) doesn't allow comments in documents, but they are added for compatibility with legacy JSON integration. + * + * @author Mikhail Golubev + */ +public class JsonCommenter implements Commenter { + @Nullable + @Override + public String getLineCommentPrefix() { + return "//"; + } + + @Nullable + @Override + public String getBlockCommentPrefix() { + return "/*"; + } + + @Nullable + @Override + public String getBlockCommentSuffix() { + return "*/"; + } + + @Nullable + @Override + public String getCommentedBlockCommentPrefix() { + return null; + } + + @Nullable + @Override + public String getCommentedBlockCommentSuffix() { + return null; + } +} diff --git a/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java b/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java new file mode 100644 index 00000000..feb7068b --- /dev/null +++ b/json/src/com/intellij/json/editor/JsonCopyPastePostProcessor.java @@ -0,0 +1,169 @@ +// 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 com.intellij.json.editor; + +import com.intellij.codeInsight.editorActions.CopyPastePostProcessor; +import com.intellij.codeInsight.editorActions.TextBlockTransferableData; +import com.intellij.ide.scratch.ScratchFileType; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.JsonFileType; +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonValue; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.RangeMarker; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.util.Collections; +import java.util.List; + +public class JsonCopyPastePostProcessor extends CopyPastePostProcessor<TextBlockTransferableData> { + static final List<TextBlockTransferableData> DATA_LIST = Collections.singletonList(new DumbData()); + static class DumbData implements TextBlockTransferableData { + private static final DataFlavor DATA_FLAVOR = new DataFlavor(JsonCopyPastePostProcessor.class, "class: JsonCopyPastePostProcessor"); + @Override + public DataFlavor getFlavor() { + return DATA_FLAVOR; + } + + @Override + public int getOffsetCount() { + return 0; + } + + @Override + public int getOffsets(int[] offsets, int index) { + return index; + } + + @Override + public int setOffsets(int[] offsets, int index) { + return index; + } + } + + @NotNull + @Override + public List<TextBlockTransferableData> collectTransferableData(PsiFile file, Editor editor, int[] startOffsets, int[] endOffsets) { + return ContainerUtil.emptyList(); + } + + @NotNull + @Override + public List<TextBlockTransferableData> extractTransferableData(Transferable content) { + // if this list is empty, processTransferableData won't be called + return DATA_LIST; + } + + @Override + public void processTransferableData(Project project, + Editor editor, + RangeMarker bounds, + int caretOffset, + Ref<Boolean> indented, + List<TextBlockTransferableData> values) { + fixCommasOnPaste(project, editor, bounds); + } + + private static void fixCommasOnPaste(@NotNull Project project, @NotNull Editor editor, @NotNull RangeMarker bounds) { + if (!JsonEditorOptions.getInstance().COMMA_ON_PASTE) return; + + if (!isJsonEditor(project, editor)) return; + + final PsiDocumentManager manager = PsiDocumentManager.getInstance(project); + manager.commitDocument(editor.getDocument()); + final PsiFile psiFile = manager.getPsiFile(editor.getDocument()); + if (psiFile == null) return; + fixTrailingComma(bounds, psiFile, manager); + fixLeadingComma(bounds, psiFile, manager); + } + + private static boolean isJsonEditor(@NotNull Project project, + @NotNull Editor editor) { + final VirtualFile file = FileDocumentManager.getInstance().getFile(editor.getDocument()); + if (file == null) return false; + final FileType fileType = file.getFileType(); + if (fileType instanceof JsonFileType) return true; + if (!(fileType instanceof ScratchFileType)) return false; + return PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()) instanceof JsonFile; + } + + private static void fixLeadingComma(@NotNull RangeMarker bounds, @NotNull PsiFile psiFile, @NotNull PsiDocumentManager manager) { + final PsiElement startElement = skipWhitespaces(psiFile.findElementAt(bounds.getStartOffset())); + PsiElement propertyOrArrayItem = startElement instanceof JsonProperty ? startElement : getParentPropertyOrArrayItem(startElement); + + if (propertyOrArrayItem == null) return; + + PsiElement prevSibling = PsiTreeUtil.skipWhitespacesBackward(propertyOrArrayItem); + if (prevSibling instanceof PsiErrorElement) { + final int offset = prevSibling.getTextRange().getEndOffset(); + ApplicationManager.getApplication().runWriteAction(() -> bounds.getDocument().insertString(offset, ",")); + manager.commitDocument(bounds.getDocument()); + } + } + + @Nullable + private static PsiElement getParentPropertyOrArrayItem(@Nullable PsiElement startElement) { + PsiElement propertyOrArrayItem = PsiTreeUtil.getParentOfType(startElement, JsonProperty.class, JsonArray.class); + if (propertyOrArrayItem instanceof JsonArray) { + for (JsonValue value : ((JsonArray)propertyOrArrayItem).getValueList()) { + if (PsiTreeUtil.isAncestor(value, startElement, false)) { + return value; + } + } + return null; + } + return propertyOrArrayItem; + } + + private static void fixTrailingComma(@NotNull RangeMarker bounds, @NotNull PsiFile psiFile, @NotNull PsiDocumentManager manager) { + PsiElement endElement = skipWhitespaces(psiFile.findElementAt(bounds.getEndOffset() - 1)); + if (endElement != null && endElement.getTextOffset() >= bounds.getEndOffset()) { + endElement = PsiTreeUtil.skipWhitespacesBackward(endElement); + } + + if (endElement instanceof LeafPsiElement && ((LeafPsiElement)endElement).getElementType() == JsonElementTypes.COMMA) { + final PsiElement nextNext = skipWhitespaces(endElement.getNextSibling()); + if (nextNext instanceof LeafPsiElement && (((LeafPsiElement)nextNext).getElementType() == JsonElementTypes.R_CURLY || + ((LeafPsiElement)nextNext).getElementType() == JsonElementTypes.R_BRACKET)) { + PsiElement finalEndElement = endElement; + ApplicationManager.getApplication().runWriteAction(() -> finalEndElement.delete()); + } + } + else { + final PsiElement property = getParentPropertyOrArrayItem(endElement); + if (endElement instanceof PsiErrorElement || property != null && skipWhitespaces(property.getNextSibling()) instanceof PsiErrorElement) { + PsiElement finalEndElement1 = endElement; + ApplicationManager.getApplication().runWriteAction(() -> bounds.getDocument().insertString(getOffset(property, finalEndElement1), ",")); + manager.commitDocument(bounds.getDocument()); + } + } + } + + private static int getOffset(@Nullable PsiElement property, @Nullable PsiElement finalEndElement1) { + if (finalEndElement1 instanceof PsiErrorElement) return finalEndElement1.getTextOffset(); + assert finalEndElement1 != null; + return property != null ? property.getTextRange().getEndOffset() : finalEndElement1.getTextOffset(); + } + + @Nullable + private static PsiElement skipWhitespaces(@Nullable PsiElement element) { + while (element instanceof PsiWhiteSpace) { + element = element.getNextSibling(); + } + return element; + } +} diff --git a/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java b/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java new file mode 100644 index 00000000..e85e7167 --- /dev/null +++ b/json/src/com/intellij/json/editor/JsonCopyPasteProcessor.java @@ -0,0 +1,72 @@ +// 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 com.intellij.json.editor; + +import com.intellij.codeInsight.editorActions.CopyPastePreProcessor; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.RawText; +import com.intellij.openapi.editor.SelectionModel; +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; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonCopyPasteProcessor implements CopyPastePreProcessor { + @Nullable + @Override + public String preprocessOnCopy(PsiFile file, int[] startOffsets, int[] endOffsets, String text) { + if (!JsonEditorOptions.getInstance().ESCAPE_PASTED_TEXT) { + return null; + } + if (!file.isPhysical() || startOffsets.length > 1 || endOffsets.length > 1) { + return null; + } + final int selectionStart = startOffsets[0]; + final int selectionEnd = endOffsets[0]; + final JsonStringLiteral literalExpression = getSingleElementFromSelectionOrNull(file, selectionStart, selectionEnd); + + if (literalExpression == null) { + return null; + } + + return StringUtil.unescapeStringCharacters(StringUtil.replaceUnicodeEscapeSequences(text)); + } + + @Nullable + private static JsonStringLiteral getSingleElementFromSelectionOrNull(PsiFile file, int start, int end) { + final PsiElement element = file.findElementAt(start); + final JsonStringLiteral literalExpression = PsiTreeUtil.getParentOfType(element, JsonStringLiteral.class); + if (literalExpression == null) return null; + TextRange textRange = literalExpression.getTextRange(); + if (start <= textRange.getStartOffset() || end >= textRange.getEndOffset()) return null; + String text = literalExpression.getText(); + if (!text.startsWith("\"") || !text.endsWith("\"")) return null; + return literalExpression; + } + + @NotNull + @Override + public String preprocessOnPaste(Project project, PsiFile file, Editor editor, String text, RawText rawText) { + if (!JsonEditorOptions.getInstance().ESCAPE_PASTED_TEXT) { + return text; + } + if (!file.isPhysical()) { + return text; + } + + final SelectionModel selectionModel = editor.getSelectionModel(); + final int selectionStart = selectionModel.getSelectionStart(); + final int selectionEnd = selectionModel.getSelectionEnd(); + + final JsonStringLiteral literalExpression = getSingleElementFromSelectionOrNull(file, selectionStart, selectionEnd); + if (literalExpression == null) { + return text; + } + + return StringUtil.escapeStringCharacters(text); + } +} diff --git a/json/src/com/intellij/json/editor/JsonEditorOptions.java b/json/src/com/intellij/json/editor/JsonEditorOptions.java new file mode 100644 index 00000000..53726fc9 --- /dev/null +++ b/json/src/com/intellij/json/editor/JsonEditorOptions.java @@ -0,0 +1,38 @@ +// 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 com.intellij.json.editor; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.util.xmlb.XmlSerializerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@State( + name = "JsonEditorOptions", + storages = @Storage("editor.xml") +) +public class JsonEditorOptions implements PersistentStateComponent<JsonEditorOptions> { + public boolean COMMA_ON_ENTER = true; + public boolean COMMA_ON_MATCHING_BRACES = true; + public boolean COMMA_ON_PASTE = true; + public boolean AUTO_QUOTE_PROP_NAME = true; + public boolean AUTO_WHITESPACE_AFTER_COLON = true; + public boolean ESCAPE_PASTED_TEXT = true; + + @Nullable + @Override + public JsonEditorOptions getState() { + return this; + } + + @Override + public void loadState(@NotNull JsonEditorOptions state) { + XmlSerializerUtil.copyBean(state, this); + } + + public static JsonEditorOptions getInstance() { + return ServiceManager.getService(JsonEditorOptions.class); + } +} diff --git a/json/src/com/intellij/json/editor/JsonEnterHandler.java b/json/src/com/intellij/json/editor/JsonEnterHandler.java new file mode 100644 index 00000000..53a2a047 --- /dev/null +++ b/json/src/com/intellij/json/editor/JsonEnterHandler.java @@ -0,0 +1,147 @@ +// 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 com.intellij.json.editor; + +import com.intellij.codeInsight.editorActions.EnterHandler; +import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.JsonLanguage; +import com.intellij.json.psi.*; +import com.intellij.lang.Language; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.util.Ref; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiErrorElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.tree.IElementType; +import com.intellij.util.ObjectUtils; +import org.jetbrains.annotations.NotNull; + +public class JsonEnterHandler extends EnterHandlerDelegateAdapter { + @Override + public Result preprocessEnter(@NotNull PsiFile file, + @NotNull Editor editor, + @NotNull Ref<Integer> caretOffsetRef, + @NotNull Ref<Integer> caretAdvanceRef, + @NotNull DataContext dataContext, + EditorActionHandler originalHandler) { + if (!JsonEditorOptions.getInstance().COMMA_ON_ENTER) { + return Result.Continue; + } + + Language language = EnterHandler.getLanguage(dataContext); + if (!(language instanceof JsonLanguage)) { + return Result.Continue; + } + + int caretOffset = caretOffsetRef.get().intValue(); + PsiElement psiAtOffset = file.findElementAt(caretOffset); + + if (psiAtOffset == null) { + return Result.Continue; + } + + if (psiAtOffset instanceof LeafPsiElement && handleComma(caretOffsetRef, psiAtOffset, editor)) { + return Result.Continue; + } + + JsonValue literal = ObjectUtils.tryCast(psiAtOffset.getParent(), JsonValue.class); + if (literal != null && (!(literal instanceof JsonStringLiteral) || !((JsonLanguage)language).hasPermissiveStrings())) { + handleJsonValue(literal, editor, caretOffsetRef); + } + + return Result.Continue; + } + + private static boolean handleComma(@NotNull Ref<Integer> caretOffsetRef, @NotNull PsiElement psiAtOffset, @NotNull Editor editor) { + PsiElement nextSibling = psiAtOffset; + while (nextSibling instanceof PsiWhiteSpace) { + nextSibling = nextSibling.getNextSibling(); + } + + LeafPsiElement leafPsiElement = ObjectUtils.tryCast(nextSibling, LeafPsiElement.class); + IElementType elementType = leafPsiElement == null ? null : leafPsiElement.getElementType(); + if (elementType == JsonElementTypes.COMMA || elementType == JsonElementTypes.R_CURLY) { + PsiElement prevSibling = nextSibling.getPrevSibling(); + while (prevSibling instanceof PsiWhiteSpace) { + prevSibling = prevSibling.getPrevSibling(); + } + + if (prevSibling instanceof JsonProperty && ((JsonProperty)prevSibling).getValue() != null) { + int offset = elementType == JsonElementTypes.COMMA ? nextSibling.getTextRange().getEndOffset() : prevSibling.getTextRange().getEndOffset(); + if (offset < editor.getDocument().getTextLength()) { + if (elementType == JsonElementTypes.R_CURLY) { + editor.getDocument().insertString(offset, ","); + offset++; + } + caretOffsetRef.set(offset); + } + return true; + } + return false; + } + + if (nextSibling instanceof JsonProperty) { + PsiElement prevSibling = nextSibling.getPrevSibling(); + while (prevSibling instanceof PsiWhiteSpace || prevSibling instanceof PsiErrorElement) { + prevSibling = prevSibling.getPrevSibling(); + } + + if (prevSibling instanceof JsonProperty) { + int offset = prevSibling.getTextRange().getEndOffset(); + if (offset < editor.getDocument().getTextLength()) { + editor.getDocument().insertString(offset, ","); + caretOffsetRef.set(offset + 1); + } + return true; + } + } + + return false; + } + + private static void handleJsonValue(@NotNull JsonValue literal, @NotNull Editor editor, @NotNull Ref<Integer> caretOffsetRef) { + PsiElement parent = literal.getParent(); + if (!(parent instanceof JsonProperty) || ((JsonProperty)parent).getValue() != literal) { + return; + } + + PsiElement nextSibling = parent.getNextSibling(); + while (nextSibling instanceof PsiWhiteSpace || nextSibling instanceof PsiErrorElement) { + nextSibling = nextSibling.getNextSibling(); + } + + int offset = literal.getTextRange().getEndOffset(); + + if (literal instanceof JsonObject || literal instanceof JsonArray) { + if (nextSibling instanceof LeafPsiElement && ((LeafPsiElement)nextSibling).getElementType() == JsonElementTypes.COMMA + || !(nextSibling instanceof JsonProperty)) { + return; + } + Document document = editor.getDocument(); + if (offset < document.getTextLength()) { + document.insertString(offset, ","); + } + return; + } + + if (nextSibling instanceof LeafPsiElement && ((LeafPsiElement)nextSibling).getElementType() == JsonElementTypes.COMMA) { + offset = nextSibling.getTextRange().getEndOffset(); + } + else { + Document document = editor.getDocument(); + if (offset < document.getTextLength()) { + document.insertString(offset, ","); + } + offset++; + } + + if (offset < editor.getDocument().getTextLength()) { + caretOffsetRef.set(offset); + } + } +} diff --git a/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java b/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java new file mode 100644 index 00000000..83ac5b2f --- /dev/null +++ b/json/src/com/intellij/json/editor/JsonSmartKeysConfigurable.java @@ -0,0 +1,42 @@ +// 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 com.intellij.json.editor; + +import com.intellij.openapi.options.BeanConfigurable; +import com.intellij.openapi.options.UnnamedConfigurable; +import com.intellij.ui.IdeBorderFactory; + +import javax.swing.*; + +public class JsonSmartKeysConfigurable extends BeanConfigurable<JsonEditorOptions> implements UnnamedConfigurable { + public JsonSmartKeysConfigurable() { + super(JsonEditorOptions.getInstance()); + JsonEditorOptions settings = getInstance(); + + checkBox("Insert missing comma on enter", + () -> settings.COMMA_ON_ENTER, + v -> settings.COMMA_ON_ENTER = v); + checkBox("Insert missing comma after matching braces and quotes", + () -> settings.COMMA_ON_MATCHING_BRACES, + v -> settings.COMMA_ON_MATCHING_BRACES = v); + checkBox("Automatically manage commas when pasting JSON fragments", + () -> settings.COMMA_ON_PASTE, + v -> settings.COMMA_ON_PASTE = v); + checkBox("Escape text on paste in string literals", + () -> settings.ESCAPE_PASTED_TEXT, + v -> settings.ESCAPE_PASTED_TEXT = v); + checkBox("Automatically add quotes to property names when typing ':'", + () -> settings.AUTO_QUOTE_PROP_NAME, + v -> settings.AUTO_QUOTE_PROP_NAME = v); + checkBox("Automatically add whitespace when typing ':' after property namess", + () -> settings.AUTO_WHITESPACE_AFTER_COLON, + v -> settings.AUTO_WHITESPACE_AFTER_COLON = v); + } + + @Override + public JComponent createComponent() { + JComponent result = super.createComponent(); + assert result != null; + result.setBorder(IdeBorderFactory.createTitledBorder("JSON")); + return result; + } +} diff --git a/json/src/com/intellij/json/editor/JsonTypedHandler.java b/json/src/com/intellij/json/editor/JsonTypedHandler.java new file mode 100644 index 00000000..7214f238 --- /dev/null +++ b/json/src/com/intellij/json/editor/JsonTypedHandler.java @@ -0,0 +1,142 @@ +// 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 com.intellij.json.editor; + +import com.intellij.codeInsight.editorActions.TypedHandlerDelegate; +import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessor; +import com.intellij.json.JsonDialectUtil; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.psi.*; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.tree.TokenSet; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; + +public class JsonTypedHandler extends TypedHandlerDelegate { + + private boolean myWhitespaceAdded; + + @NotNull + @Override + public Result charTyped(char c, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { + if (file instanceof JsonFile) { + processPairedBracesComma(c, editor, file); + addWhiteSpaceAfterColonIfNeeded(c, editor, file); + removeRedundantWhitespaceIfAfterColon(c, editor, file); + } + return Result.CONTINUE; + } + + private void removeRedundantWhitespaceIfAfterColon(char c, Editor editor, PsiFile file) { + if (!myWhitespaceAdded || c != ' ' || !JsonEditorOptions.getInstance().AUTO_WHITESPACE_AFTER_COLON) { + if (c != ':') { + myWhitespaceAdded = false; + } + return; + } + int offset = editor.getCaretModel().getOffset(); + PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument()); + final PsiElement element = file.findElementAt(offset); + if (element instanceof PsiWhiteSpace) { + editor.getDocument().deleteString(offset - 1, offset); + } + myWhitespaceAdded = false; + } + + @NotNull + @Override + public Result beforeCharTyped(char c, + @NotNull Project project, + @NotNull Editor editor, + @NotNull PsiFile file, + @NotNull FileType fileType) { + if (file instanceof JsonFile) { + addPropertyNameQuotesIfNeeded(c, editor, file); + } + return Result.CONTINUE; + } + + private void addWhiteSpaceAfterColonIfNeeded(char c, + @NotNull Editor editor, + @NotNull PsiFile file) { + if (c != ':' || !JsonEditorOptions.getInstance().AUTO_WHITESPACE_AFTER_COLON) { + if (c != ' ') { + myWhitespaceAdded = false; + } + return; + } + int offset = editor.getCaretModel().getOffset(); + PsiDocumentManager.getInstance(file.getProject()).commitDocument(editor.getDocument()); + PsiElement element = PsiTreeUtil.getParentOfType(PsiTreeUtil.skipWhitespacesBackward(file.findElementAt(offset)), JsonProperty.class, false); + if (element == null) { + myWhitespaceAdded = false; + return; + } + final ASTNode[] children = element.getNode().getChildren(TokenSet.create(JsonElementTypes.COLON)); + if (children.length == 0) { + myWhitespaceAdded = false; + return; + } + final ASTNode colon = children[0]; + final ASTNode next = colon.getTreeNext(); + final String text = next.getText(); + if (text.length() == 0 || !StringUtil.isEmptyOrSpaces(text) || StringUtil.isLineBreak(text.charAt(0))) { + final int insOffset = colon.getStartOffset() + 1; + editor.getDocument().insertString(insOffset, " "); + editor.getCaretModel().moveToOffset(insOffset + 1); + myWhitespaceAdded = true; + } + else { + myWhitespaceAdded = false; + } + } + + private static void addPropertyNameQuotesIfNeeded(char c, + @NotNull Editor editor, + @NotNull PsiFile file) { + if (c != ':' || !JsonDialectUtil.isStandardJson(file) || !JsonEditorOptions.getInstance().AUTO_QUOTE_PROP_NAME) return; + int offset = editor.getCaretModel().getOffset(); + PsiElement element = PsiTreeUtil.skipWhitespacesBackward(file.findElementAt(offset)); + if (!(element instanceof JsonProperty)) return; + final JsonValue nameElement = ((JsonProperty)element).getNameElement(); + if (nameElement instanceof JsonReferenceExpression) { + ((JsonProperty)element).setName(nameElement.getText()); + PsiDocumentManager.getInstance(file.getProject()).doPostponedOperationsAndUnblockDocument(editor.getDocument()); + } + } + + public static void processPairedBracesComma(char c, + @NotNull Editor editor, + @NotNull PsiFile file) { + if (!JsonEditorOptions.getInstance().COMMA_ON_MATCHING_BRACES) return; + if (c != '[' && c != '{' && c != '"' && c != '\'') return; + SmartEnterProcessor.commitDocument(editor); + int offset = editor.getCaretModel().getOffset(); + PsiElement element = file.findElementAt(offset); + if (element == null) return; + PsiElement parent = element.getParent(); + if (c == '[' && parent instanceof JsonArray + || c == '{' && parent instanceof JsonObject + || (c == '"' || c == '\'') && parent instanceof JsonStringLiteral) { + if (shouldAddCommaInParentContainer((JsonValue)parent)) { + editor.getDocument().insertString(parent.getTextRange().getEndOffset(), ","); + } + } + } + + private static boolean shouldAddCommaInParentContainer(@NotNull JsonValue item) { + PsiElement parent = item.getParent(); + if (parent instanceof JsonArray || parent instanceof JsonProperty) { + PsiElement nextElement = PsiTreeUtil.skipWhitespacesForward(parent instanceof JsonProperty ? parent : item); + if (nextElement instanceof PsiErrorElement) { + PsiElement forward = PsiTreeUtil.skipWhitespacesForward(nextElement); + return parent instanceof JsonProperty ? forward instanceof JsonProperty : forward instanceof JsonValue; + } + } + return false; + } +} diff --git a/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java b/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java new file mode 100644 index 00000000..9bfc3103 --- /dev/null +++ b/json/src/com/intellij/json/editor/folding/JsonFoldingBuilder.java @@ -0,0 +1,110 @@ +package com.intellij.json.editor.folding; + +import com.intellij.json.JsonElementTypes; +import com.intellij.json.psi.*; +import com.intellij.lang.ASTNode; +import com.intellij.lang.folding.FoldingBuilder; +import com.intellij.lang.folding.FoldingDescriptor; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.util.Couple; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JsonFoldingBuilder implements FoldingBuilder, DumbAware { + @NotNull + @Override + public FoldingDescriptor[] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) { + final List<FoldingDescriptor> descriptors = new ArrayList<>(); + collectDescriptorsRecursively(node, document, descriptors); + return descriptors.toArray(FoldingDescriptor.EMPTY); + } + + private static void collectDescriptorsRecursively(@NotNull ASTNode node, + @NotNull Document document, + @NotNull List<FoldingDescriptor> descriptors) { + final IElementType type = node.getElementType(); + if ((type == JsonElementTypes.OBJECT || type == JsonElementTypes.ARRAY) && spanMultipleLines(node, document)) { + descriptors.add(new FoldingDescriptor(node, node.getTextRange())); + } + else if (type == JsonElementTypes.BLOCK_COMMENT) { + descriptors.add(new FoldingDescriptor(node, node.getTextRange())); + } + else if (type == JsonElementTypes.LINE_COMMENT) { + final Couple<PsiElement> commentRange = expandLineCommentsRange(node.getPsi()); + final int startOffset = commentRange.getFirst().getTextRange().getStartOffset(); + final int endOffset = commentRange.getSecond().getTextRange().getEndOffset(); + if (document.getLineNumber(startOffset) != document.getLineNumber(endOffset)) { + descriptors.add(new FoldingDescriptor(node, new TextRange(startOffset, endOffset))); + } + } + + for (ASTNode child : node.getChildren(null)) { + collectDescriptorsRecursively(child, document, descriptors); + } + } + + @Nullable + @Override + public String getPlaceholderText(@NotNull ASTNode node) { + final IElementType type = node.getElementType(); + if (type == JsonElementTypes.OBJECT) { + final JsonObject object = node.getPsi(JsonObject.class); + final List<JsonProperty> properties = object.getPropertyList(); + JsonProperty candidate = null; + for (JsonProperty property : properties) { + final String name = property.getName(); + final JsonValue value = property.getValue(); + if (value instanceof JsonLiteral) { + if ("id".equals(name) || "name".equals(name)) { + candidate = property; + break; + } + if (candidate == null) { + candidate = property; + } + } + } + if (candidate != null) { + return "{\"" + candidate.getName() + "\": " + candidate.getValue().getText() + "...}"; + } + return "{...}"; + } + else if (type == JsonElementTypes.ARRAY) { + return "[...]"; + } + else if (type == JsonElementTypes.LINE_COMMENT) { + return "//..."; + } + else if (type == JsonElementTypes.BLOCK_COMMENT) { + return "/*...*/"; + } + return "..."; + } + + @Override + public boolean isCollapsedByDefault(@NotNull ASTNode node) { + return false; + } + + @NotNull + public static Couple<PsiElement> expandLineCommentsRange(@NotNull PsiElement anchor) { + return Couple.of(JsonPsiUtil.findFurthestSiblingOfSameType(anchor, false), JsonPsiUtil.findFurthestSiblingOfSameType(anchor, true)); + } + + private static boolean spanMultipleLines(@NotNull ASTNode node, @NotNull Document document) { + final TextRange range = node.getTextRange(); + int endOffset = range.getEndOffset(); + return document.getLineNumber(range.getStartOffset()) + < (endOffset < document.getTextLength() ? document.getLineNumber(endOffset) : document.getLineCount() - 1); + } +} diff --git a/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java b/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java new file mode 100644 index 00000000..ff82d1c8 --- /dev/null +++ b/json/src/com/intellij/json/editor/lineMover/JsonLineMover.java @@ -0,0 +1,197 @@ +package com.intellij.json.editor.lineMover; + +import com.intellij.codeInsight.editorActions.moveUpDown.LineMover; +import com.intellij.codeInsight.editorActions.moveUpDown.LineRange; +import com.intellij.json.psi.*; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonLineMover extends LineMover { + private enum Direction { + Same, + Inside, + Outside + } + + private Direction myDirection = Direction.Same; + + @Override + public boolean checkAvailable(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) { + myDirection = Direction.Same; + + if (!(file instanceof JsonFile) || !super.checkAvailable(editor, file, info, down)) { + return false; + } + + Pair<PsiElement, PsiElement> movedElementRange = getElementRange(editor, file, info.toMove); + if (!isValidElementRange(movedElementRange)) { + return false; + } + + // Tweak range to move if it's necessary + movedElementRange = expandCommentsInRange(movedElementRange); + + PsiElement movedSecond = movedElementRange.getSecond(); + PsiElement movedFirst = movedElementRange.getFirst(); + + info.toMove = new LineRange(movedFirst, movedSecond); + + // Adjust destination range to prevent illegal offsets + final int lineCount = editor.getDocument().getLineCount(); + if (down) { + info.toMove2 = new LineRange(info.toMove.endLine, Math.min(info.toMove.endLine + 1, lineCount)); + } + else { + info.toMove2 = new LineRange(Math.max(info.toMove.startLine - 1, 0), info.toMove.startLine); + } + + if (movedFirst instanceof PsiComment && movedSecond instanceof PsiComment) { + return true; + } + + // Check whether additional comma is needed + final Pair<PsiElement, PsiElement> destElementRange = getElementRange(editor, file, info.toMove2); + + if (destElementRange != null) { + PsiElement destFirst = destElementRange.getFirst(); + PsiElement destSecond = destElementRange.getSecond(); + + if (destFirst == destSecond && !(destFirst instanceof JsonProperty) && !(destFirst instanceof JsonValue)) { + PsiElement parent = destFirst.getParent(); + if (((JsonFile)parent.getContainingFile()).getTopLevelValue() == parent) { + info.prohibitMove(); + return true; + } + } + + PsiElement firstParent = destFirst.getParent(); + PsiElement secondParent = destSecond.getParent(); + + JsonValue firstParentParent = PsiTreeUtil.getParentOfType(firstParent, JsonObject.class, JsonArray.class); + if (firstParentParent == secondParent) { + myDirection = down ? Direction.Outside : Direction.Inside; + } + JsonValue secondParentParent = PsiTreeUtil.getParentOfType(secondParent, JsonObject.class, JsonArray.class); + if (firstParent == secondParentParent) { + myDirection = down ? Direction.Inside : Direction.Outside; + } + } + return true; + } + + @NotNull + private static Pair<PsiElement, PsiElement> expandCommentsInRange(@NotNull Pair<PsiElement, PsiElement> range) { + final PsiElement upper = JsonPsiUtil.findFurthestSiblingOfSameType(range.getFirst(), false); + final PsiElement lower = JsonPsiUtil.findFurthestSiblingOfSameType(range.getSecond(), true); + return Pair.create(upper, lower); + } + + @Override + public void beforeMove(@NotNull Editor editor, @NotNull MoveInfo info, boolean down) { + + } + + @Override + public void afterMove(@NotNull Editor editor, @NotNull PsiFile file, @NotNull MoveInfo info, boolean down) { + int diff = (info.toMove.endLine - info.toMove.startLine) - (info.toMove2.endLine - info.toMove2.startLine); + switch (myDirection) { + case Same: + addCommaIfNeeded(editor.getDocument(), down ? info.toMove.endLine - 1 - diff : info.toMove2.endLine - 1 + diff); + trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff); + break; + case Inside: + if (!down) { + addCommaIfNeeded(editor.getDocument(), info.toMove2.startLine - 1); + } + trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.startLine : info.toMove2.startLine); + trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff); + break; + case Outside: + addCommaIfNeeded(editor.getDocument(), down ? info.toMove.startLine : info.toMove2.startLine); + trimCommaIfNeeded(editor.getDocument(), file, down ? info.toMove.endLine : info.toMove2.endLine + diff); + if (down) { + trimCommaIfNeeded(editor.getDocument(), file, info.toMove.startLine - 1); + addCommaIfNeeded(editor.getDocument(), info.toMove.endLine); + trimCommaIfNeeded(editor.getDocument(), file, info.toMove.endLine); + } + break; + } + } + + private static int getForwardLineNumber(Document document, PsiElement element) { + while (element instanceof PsiWhiteSpace || element instanceof PsiComment) { + element = element.getNextSibling(); + } + if (element == null) return -1; + + TextRange range = element.getTextRange(); + return document.getLineNumber(range.getEndOffset()); + } + + private static int getBackwardLineNumber(Document document, PsiElement element) { + while (element instanceof PsiWhiteSpace || element instanceof PsiComment) { + element = element.getPrevSibling(); + } + if (element == null) return -1; + + TextRange range = element.getTextRange(); + return document.getLineNumber(range.getEndOffset()); + } + + private static void trimCommaIfNeeded(Document document, PsiFile file, int line) { + int offset = document.getLineEndOffset(line); + if (doTrimComma(document, offset + 1, offset)) return; + + PsiElement element = file.findElementAt(offset - 1); + int forward = getForwardLineNumber(document, element); + int backward = getBackwardLineNumber(document, element); + if (forward < 0 || backward < 0) return; + doTrimComma(document, document.getLineEndOffset(forward) - 1, document.getLineEndOffset(backward)); + } + + private static boolean doTrimComma(Document document, int forwardOffset, int backwardOffset) { + CharSequence charSequence = document.getCharsSequence(); + if (backwardOffset <= 0) return true; + if (charSequence.charAt(backwardOffset - 1) == ',') { + int offsetAfter = skipWhitespaces(charSequence, forwardOffset); + if (offsetAfter >= charSequence.length()) return true; + char ch = charSequence.charAt(offsetAfter); + + if (ch == ']' || ch == '}') { + document.deleteString(backwardOffset - 1, backwardOffset); + } + if (ch != '/') return true; + } + return false; + } + + private static int skipWhitespaces(CharSequence charSequence, int offset2) { + while (offset2 < charSequence.length() && Character.isWhitespace(charSequence.charAt(offset2))) { + offset2++; + } + return offset2; + } + + private static void addCommaIfNeeded(Document document, int line) { + int offset = document.getLineEndOffset(line); + if (offset > 0 && document.getCharsSequence().charAt(offset - 1) != ',') { + document.insertString(offset, ","); + } + } + + private static boolean isValidElementRange(@Nullable Pair<PsiElement, PsiElement> elementRange) { + if (elementRange == null) { + return false; + } + return elementRange.getFirst().getParent() == elementRange.getSecond().getParent(); + } +} diff --git a/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java b/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java new file mode 100644 index 00000000..ada84f24 --- /dev/null +++ b/json/src/com/intellij/json/editor/selection/JsonBasicWordSelectionFilter.java @@ -0,0 +1,16 @@ +package com.intellij.json.editor.selection; + +import com.intellij.json.JsonParserDefinition; +import com.intellij.openapi.util.Condition; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiUtilCore; + +/** + * @author Mikhail Golubev + */ +public class JsonBasicWordSelectionFilter implements Condition<PsiElement> { + @Override + public boolean value(PsiElement element) { + return !(JsonParserDefinition.STRING_LITERALS.contains(PsiUtilCore.getElementType(element))); + } +} diff --git a/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java b/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java new file mode 100644 index 00000000..aa0fc8f3 --- /dev/null +++ b/json/src/com/intellij/json/editor/selection/JsonStringLiteralSelectionHandler.java @@ -0,0 +1,43 @@ +package com.intellij.json.editor.selection; + +import com.intellij.codeInsight.editorActions.ExtendWordSelectionHandlerBase; +import com.intellij.codeInsight.editorActions.SelectWordUtil; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.lang.injection.InjectedLanguageManager; +import com.intellij.lexer.StringLiteralLexer; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.ElementManipulators; +import com.intellij.psi.PsiElement; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +import static com.intellij.json.JsonElementTypes.SINGLE_QUOTED_STRING; + +/** + * @author Mikhail Golubev + */ +public class JsonStringLiteralSelectionHandler extends ExtendWordSelectionHandlerBase { + @Override + public boolean canSelect(@NotNull PsiElement e) { + if (!(e.getParent() instanceof JsonStringLiteral)) { + return false; + } + return !InjectedLanguageManager.getInstance(e.getProject()).isInjectedFragment(e.getContainingFile()); + } + + @Override + public List<TextRange> select(@NotNull PsiElement e, @NotNull CharSequence editorText, int cursorOffset, @NotNull Editor editor) { + final IElementType type = e.getNode().getElementType(); + final StringLiteralLexer lexer = new StringLiteralLexer(type == SINGLE_QUOTED_STRING ? '\'' : '"', type, false, "/", false, false); + final List<TextRange> result = new ArrayList<>(); + SelectWordUtil.addWordHonoringEscapeSequences(editorText, e.getTextRange(), cursorOffset, lexer, result); + + final PsiElement parent = e.getParent(); + result.add(ElementManipulators.getValueTextRange(parent).shiftRight(parent.getTextOffset())); + return result; + } +} diff --git a/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java b/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java new file mode 100644 index 00000000..ed695d74 --- /dev/null +++ b/json/src/com/intellij/json/editor/smartEnter/JsonSmartEnterProcessor.java @@ -0,0 +1,123 @@ +package com.intellij.json.editor.smartEnter; + +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonValue; +import com.intellij.lang.SmartEnterProcessorWithFixers; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import static com.intellij.json.JsonElementTypes.COLON; +import static com.intellij.json.JsonElementTypes.COMMA; + +/** + * This processor allows + * <ul> + * <li>Insert colon after key inside object property</li> + * <li>Insert comma after array element or object property</li> + * </ul> + * + * @author Mikhail Golubev + */ +public class JsonSmartEnterProcessor extends SmartEnterProcessorWithFixers { + public static final Logger LOG = Logger.getInstance(JsonSmartEnterProcessor.class); + + private boolean myShouldAddNewline = false; + + public JsonSmartEnterProcessor() { + addFixers(new JsonObjectPropertyFixer(), new JsonArrayElementFixer()); + addEnterProcessors(new JsonEnterProcessor()); + } + + @Override + protected void collectAdditionalElements(@NotNull PsiElement element, @NotNull List<PsiElement> result) { + // include all parents as well + PsiElement parent = element.getParent(); + while (parent != null && !(parent instanceof JsonFile)) { + result.add(parent); + parent = parent.getParent(); + } + } + + private static boolean terminatedOnCurrentLine(@NotNull Editor editor, @NotNull PsiElement element) { + final Document document = editor.getDocument(); + final int caretOffset = editor.getCaretModel().getCurrentCaret().getOffset(); + final int elementEndOffset = element.getTextRange().getEndOffset(); + if (document.getLineNumber(elementEndOffset) != document.getLineNumber(caretOffset)) { + return false; + } + // Skip empty PsiError elements if comma is missing + PsiElement nextLeaf = PsiTreeUtil.nextLeaf(element, true); + return nextLeaf == null || (nextLeaf instanceof PsiWhiteSpace && nextLeaf.getText().contains("\n")); + } + + private static boolean isFollowedByTerminal(@NotNull PsiElement element, IElementType type) { + final PsiElement nextLeaf = PsiTreeUtil.nextVisibleLeaf(element); + return nextLeaf != null && nextLeaf.getNode().getElementType() == type; + } + + private static class JsonArrayElementFixer extends SmartEnterProcessorWithFixers.Fixer<JsonSmartEnterProcessor> { + @Override + public void apply(@NotNull Editor editor, @NotNull JsonSmartEnterProcessor processor, @NotNull PsiElement element) + throws IncorrectOperationException { + if (element instanceof JsonValue && element.getParent() instanceof JsonArray) { + final JsonValue arrayElement = (JsonValue)element; + if (terminatedOnCurrentLine(editor, arrayElement) && !isFollowedByTerminal(element, COMMA)) { + editor.getDocument().insertString(arrayElement.getTextRange().getEndOffset(), ","); + processor.myShouldAddNewline = true; + } + } + } + } + + private class JsonObjectPropertyFixer extends SmartEnterProcessorWithFixers.Fixer<JsonSmartEnterProcessor> { + @Override + public void apply(@NotNull Editor editor, @NotNull JsonSmartEnterProcessor processor, @NotNull PsiElement element) + throws IncorrectOperationException { + if (element instanceof JsonProperty) { + final JsonValue propertyValue = ((JsonProperty)element).getValue(); + if (propertyValue != null) { + if (terminatedOnCurrentLine(editor, propertyValue) && !isFollowedByTerminal(propertyValue, COMMA)) { + editor.getDocument().insertString(propertyValue.getTextRange().getEndOffset(), ","); + processor.myShouldAddNewline = true; + } + } + else { + final JsonValue propertyKey = ((JsonProperty)element).getNameElement(); + final int keyEndOffset = propertyKey.getTextRange().getEndOffset(); + //processor.myFirstErrorOffset = keyEndOffset; + if (terminatedOnCurrentLine(editor, propertyKey) && !isFollowedByTerminal(propertyKey, COLON)) { + processor.myFirstErrorOffset = keyEndOffset + 2; + editor.getDocument().insertString(keyEndOffset, ": "); + } + } + } + } + } + + private class JsonEnterProcessor extends SmartEnterProcessorWithFixers.FixEnterProcessor { + @Override + public boolean doEnter(PsiElement atCaret, PsiFile file, @NotNull Editor editor, boolean modified) { + if (myShouldAddNewline) { + try { + plainEnter(editor); + } + finally { + myShouldAddNewline = false; + } + } + return true; + } + } +} diff --git a/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java b/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java new file mode 100644 index 00000000..fb7dfd3c --- /dev/null +++ b/json/src/com/intellij/json/findUsages/JsonFindUsagesProvider.java @@ -0,0 +1,54 @@ +package com.intellij.json.findUsages; + +import com.intellij.json.psi.JsonProperty; +import com.intellij.lang.HelpID; +import com.intellij.lang.cacheBuilder.WordsScanner; +import com.intellij.lang.findUsages.FindUsagesProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiNamedElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class JsonFindUsagesProvider implements FindUsagesProvider { + @Nullable + @Override + public WordsScanner getWordsScanner() { + return new JsonWordScanner(); + } + + @Override + public boolean canFindUsagesFor(@NotNull PsiElement psiElement) { + return psiElement instanceof PsiNamedElement; + } + + @Nullable + @Override + public String getHelpId(@NotNull PsiElement psiElement) { + return HelpID.FIND_OTHER_USAGES; + } + + @NotNull + @Override + public String getType(@NotNull PsiElement element) { + if (element instanceof JsonProperty) { + return "property"; + } + return ""; + } + + @NotNull + @Override + public String getDescriptiveName(@NotNull PsiElement element) { + final String name = element instanceof PsiNamedElement ? ((PsiNamedElement)element).getName() : null; + return name != null ? name : "<unnamed>"; + } + + @NotNull + @Override + public String getNodeText(@NotNull PsiElement element, boolean useFullName) { + return getDescriptiveName(element); + } +} diff --git a/json/src/com/intellij/json/findUsages/JsonWordScanner.java b/json/src/com/intellij/json/findUsages/JsonWordScanner.java new file mode 100644 index 00000000..df570922 --- /dev/null +++ b/json/src/com/intellij/json/findUsages/JsonWordScanner.java @@ -0,0 +1,19 @@ +package com.intellij.json.findUsages; + +import com.intellij.json.JsonElementTypes; +import com.intellij.json.JsonLexer; +import com.intellij.lang.cacheBuilder.DefaultWordsScanner; +import com.intellij.psi.tree.TokenSet; + +import static com.intellij.json.JsonParserDefinition.JSON_COMMENTARIES; +import static com.intellij.json.JsonParserDefinition.JSON_LITERALS; + +/** + * @author Mikhail Golubev + */ +public class JsonWordScanner extends DefaultWordsScanner { + public JsonWordScanner() { + super(new JsonLexer(), TokenSet.create(JsonElementTypes.IDENTIFIER), JSON_COMMENTARIES, JSON_LITERALS); + setMayHaveFileRefsInLiterals(true); + } +} diff --git a/json/src/com/intellij/json/formatter/JsonBlock.java b/json/src/com/intellij/json/formatter/JsonBlock.java new file mode 100644 index 00000000..992e0001 --- /dev/null +++ b/json/src/com/intellij/json/formatter/JsonBlock.java @@ -0,0 +1,221 @@ +package com.intellij.json.formatter; + +import com.intellij.formatting.*; +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonPsiUtil; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.TokenType; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.tree.TokenSet; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +import static com.intellij.json.JsonElementTypes.*; +import static com.intellij.json.JsonParserDefinition.JSON_CONTAINERS; +import static com.intellij.json.formatter.JsonCodeStyleSettings.ALIGN_PROPERTY_ON_COLON; +import static com.intellij.json.formatter.JsonCodeStyleSettings.ALIGN_PROPERTY_ON_VALUE; +import static com.intellij.json.psi.JsonPsiUtil.hasElementType; + +/** + * @author Mikhail Golubev + */ +public class JsonBlock implements ASTBlock { + private static final TokenSet JSON_OPEN_BRACES = TokenSet.create(L_BRACKET, L_CURLY); + private static final TokenSet JSON_CLOSE_BRACES = TokenSet.create(R_BRACKET, R_CURLY); + private static final TokenSet JSON_ALL_BRACES = TokenSet.orSet(JSON_OPEN_BRACES, JSON_CLOSE_BRACES); + + private final JsonBlock myParent; + + private final ASTNode myNode; + private final PsiElement myPsiElement; + private final Alignment myAlignment; + private final Indent myIndent; + private final Wrap myWrap; + private final JsonCodeStyleSettings myCustomSettings; + private final SpacingBuilder mySpacingBuilder; + // lazy initialized on first call to #getSubBlocks() + private List<Block> mySubBlocks = null; + + private final Alignment myPropertyValueAlignment; + private final Wrap myChildWrap; + + /** + * @deprecated Please use overload with settings JsonCodeStyleSettings and spacingBuilder. + * Getting settings should be done only for the root block. + */ + @Deprecated + @SuppressWarnings("unused") //used externally + public JsonBlock(@Nullable JsonBlock parent, + @NotNull ASTNode node, + @NotNull CodeStyleSettings settings, + @Nullable Alignment alignment, + @NotNull Indent indent, + @Nullable Wrap wrap) { + this(parent, node, settings.getCustomSettings(JsonCodeStyleSettings.class), alignment, indent, wrap, + JsonFormattingBuilderModel.createSpacingBuilder(settings)); + } + + public JsonBlock(@Nullable JsonBlock parent, + @NotNull ASTNode node, + @NotNull JsonCodeStyleSettings customSettings, + @Nullable Alignment alignment, + @NotNull Indent indent, + @Nullable Wrap wrap, + @NotNull SpacingBuilder spacingBuilder) { + myParent = parent; + myNode = node; + myPsiElement = node.getPsi(); + myAlignment = alignment; + myIndent = indent; + myWrap = wrap; + mySpacingBuilder = spacingBuilder; + myCustomSettings = customSettings; + + if (myPsiElement instanceof JsonObject) { + myChildWrap = Wrap.createWrap(myCustomSettings.OBJECT_WRAPPING, true); + } + else if (myPsiElement instanceof JsonArray) { + myChildWrap = Wrap.createWrap(myCustomSettings.ARRAY_WRAPPING, true); + } + else { + myChildWrap = null; + } + + myPropertyValueAlignment = myPsiElement instanceof JsonObject ? Alignment.createAlignment(true) : null; + } + + @Override + public ASTNode getNode() { + return myNode; + } + + @NotNull + @Override + public TextRange getTextRange() { + return myNode.getTextRange(); + } + + @NotNull + @Override + public List<Block> getSubBlocks() { + if (mySubBlocks == null) { + int propertyAlignment = myCustomSettings.PROPERTY_ALIGNMENT; + ASTNode[] children = myNode.getChildren(null); + mySubBlocks = ContainerUtil.newArrayListWithCapacity(children.length); + for (ASTNode child: children) { + if (isWhitespaceOrEmpty(child)) continue; + mySubBlocks.add(makeSubBlock(child, propertyAlignment)); + } + } + return mySubBlocks; + } + + private Block makeSubBlock(@NotNull ASTNode childNode, int propertyAlignment) { + Indent indent = Indent.getNoneIndent(); + Alignment alignment = null; + Wrap wrap = null; + + if (hasElementType(myNode, JSON_CONTAINERS)) { + if (hasElementType(childNode, COMMA)) { + wrap = Wrap.createWrap(WrapType.NONE, true); + } + else if (!hasElementType(childNode, JSON_ALL_BRACES)) { + assert myChildWrap != null; + wrap = myChildWrap; + indent = Indent.getNormalIndent(); + } + else if (hasElementType(childNode, JSON_OPEN_BRACES)) { + if (JsonPsiUtil.isPropertyValue(myPsiElement) && propertyAlignment == ALIGN_PROPERTY_ON_VALUE) { + // WEB-13587 Align compound values on opening brace/bracket, not the whole block + assert myParent != null && myParent.myParent != null && myParent.myParent.myPropertyValueAlignment != null; + alignment = myParent.myParent.myPropertyValueAlignment; + } + } + } + // Handle properties alignment + else if (hasElementType(myNode, PROPERTY) ) { + assert myParent != null && myParent.myPropertyValueAlignment != null; + if (hasElementType(childNode, COLON) && propertyAlignment == ALIGN_PROPERTY_ON_COLON) { + alignment = myParent.myPropertyValueAlignment; + } + else if (JsonPsiUtil.isPropertyValue(childNode.getPsi()) && propertyAlignment == ALIGN_PROPERTY_ON_VALUE) { + if (!hasElementType(childNode, JSON_CONTAINERS)) { + alignment = myParent.myPropertyValueAlignment; + } + } + } + return new JsonBlock(this, childNode, myCustomSettings, alignment, indent, wrap, mySpacingBuilder); + } + + @Nullable + @Override + public Wrap getWrap() { + return myWrap; + } + + @Nullable + @Override + public Indent getIndent() { + return myIndent; + } + + @Nullable + @Override + public Alignment getAlignment() { + return myAlignment; + } + + @Nullable + @Override + public Spacing getSpacing(@Nullable Block child1, @NotNull Block child2) { + return mySpacingBuilder.getSpacing(this, child1, child2); + } + + @NotNull + @Override + public ChildAttributes getChildAttributes(int newChildIndex) { + if (hasElementType(myNode, JSON_CONTAINERS)) { + // WEB-13675: For some reason including alignment in child attributes causes + // indents to consist solely of spaces when both USE_TABS and SMART_TAB + // options are enabled. + return new ChildAttributes(Indent.getNormalIndent(), null); + } + else if (myNode.getPsi() instanceof PsiFile) { + return new ChildAttributes(Indent.getNoneIndent(), null); + } + // Will use continuation indent for cases like { "foo"<caret> } + return new ChildAttributes(null, null); + } + + @Override + public boolean isIncomplete() { + final ASTNode lastChildNode = myNode.getLastChildNode(); + if (hasElementType(myNode, OBJECT)) { + return lastChildNode != null && lastChildNode.getElementType() != R_CURLY; + } + else if (hasElementType(myNode, ARRAY)) { + return lastChildNode != null && lastChildNode.getElementType() != R_BRACKET; + } + else if (hasElementType(myNode, PROPERTY)) { + return ((JsonProperty)myPsiElement).getValue() == null; + } + return false; + } + + @Override + public boolean isLeaf() { + return myNode.getFirstChildNode() == null; + } + + private static boolean isWhitespaceOrEmpty(ASTNode node) { + return node.getElementType() == TokenType.WHITE_SPACE || node.getTextLength() == 0; + } +} diff --git a/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java b/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java new file mode 100644 index 00000000..fa19c5e6 --- /dev/null +++ b/json/src/com/intellij/json/formatter/JsonCodeStyleSettings.java @@ -0,0 +1,78 @@ +package com.intellij.json.formatter; + +import com.intellij.json.JsonBundle; +import com.intellij.json.JsonLanguage; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import com.intellij.psi.codeStyle.CustomCodeStyleSettings; +import org.intellij.lang.annotations.MagicConstant; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonCodeStyleSettings extends CustomCodeStyleSettings { + + public static int DO_NOT_ALIGN_PROPERTY = PropertyAlignment.DO_NOT_ALIGN.getId(); + public static int ALIGN_PROPERTY_ON_VALUE = PropertyAlignment.ALIGN_ON_VALUE.getId(); + public static int ALIGN_PROPERTY_ON_COLON = PropertyAlignment.ALIGN_ON_COLON.getId(); + + public boolean SPACE_AFTER_COLON = true; + public boolean SPACE_BEFORE_COLON = false; + public boolean KEEP_TRAILING_COMMA = false; + + // TODO: check whether it's possible to migrate CustomCodeStyleSettings to newer com.intellij.util.xmlb.XmlSerializer + /** + * Contains value of {@link com.intellij.json.formatter.JsonCodeStyleSettings.PropertyAlignment#getId()} + * + * @see #DO_NOT_ALIGN_PROPERTY + * @see #ALIGN_PROPERTY_ON_VALUE + * @see #ALIGN_PROPERTY_ON_COLON + */ + public int PROPERTY_ALIGNMENT = PropertyAlignment.DO_NOT_ALIGN.getId(); + + @MagicConstant(flags = { + CommonCodeStyleSettings.DO_NOT_WRAP, + CommonCodeStyleSettings.WRAP_ALWAYS, + CommonCodeStyleSettings.WRAP_AS_NEEDED, + CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM + }) + public int OBJECT_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS; + + // This was default policy for array elements wrapping in JavaScript's JSON. + // CHOP_DOWN_IF_LONG seems more appropriate however for short arrays. + @MagicConstant(flags = { + CommonCodeStyleSettings.DO_NOT_WRAP, + CommonCodeStyleSettings.WRAP_ALWAYS, + CommonCodeStyleSettings.WRAP_AS_NEEDED, + CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM + }) + public int ARRAY_WRAPPING = CommonCodeStyleSettings.WRAP_ALWAYS; + + public JsonCodeStyleSettings(CodeStyleSettings container) { + super(JsonLanguage.INSTANCE.getID(), container); + } + + public enum PropertyAlignment { + DO_NOT_ALIGN(JsonBundle.message("formatter.align.properties.none"), 0), + ALIGN_ON_VALUE(JsonBundle.message("formatter.align.properties.on.value"), 1), + ALIGN_ON_COLON(JsonBundle.message("formatter.align.properties.on.colon"), 2); + + private final String myDescription; + private final int myId; + + PropertyAlignment(@NotNull String description, int id) { + myDescription = description; + myId = id; + } + + @NotNull + public String getDescription() { + return myDescription; + } + + public int getId() { + return myId; + } + } +} diff --git a/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java b/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java new file mode 100644 index 00000000..6a54a233 --- /dev/null +++ b/json/src/com/intellij/json/formatter/JsonCodeStyleSettingsProvider.java @@ -0,0 +1,57 @@ +package com.intellij.json.formatter; + +import com.intellij.application.options.CodeStyleAbstractConfigurable; +import com.intellij.application.options.CodeStyleAbstractPanel; +import com.intellij.application.options.TabbedLanguageCodeStylePanel; +import com.intellij.json.JsonLanguage; +import com.intellij.lang.Language; +import com.intellij.openapi.options.Configurable; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CodeStyleSettingsProvider; +import com.intellij.psi.codeStyle.CustomCodeStyleSettings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class JsonCodeStyleSettingsProvider extends CodeStyleSettingsProvider { + @NotNull + @Override + public Configurable createSettingsPage(CodeStyleSettings settings, CodeStyleSettings originalSettings) { + return new CodeStyleAbstractConfigurable(settings, originalSettings, "JSON") { + @Override + protected CodeStyleAbstractPanel createPanel(CodeStyleSettings settings) { + final Language language = JsonLanguage.INSTANCE; + final CodeStyleSettings currentSettings = getCurrentSettings(); + return new TabbedLanguageCodeStylePanel(language, currentSettings, settings) { + @Override + protected void initTabs(CodeStyleSettings settings) { + addIndentOptionsTab(settings); + addSpacesTab(settings); + addBlankLinesTab(settings); + addWrappingAndBracesTab(settings); + } + }; + } + + @Nullable + @Override + public String getHelpTopic() { + return "reference.settingsdialog.codestyle.json"; + } + }; + } + + @Nullable + @Override + public String getConfigurableDisplayName() { + return JsonLanguage.INSTANCE.getDisplayName(); + } + + @Nullable + @Override + public CustomCodeStyleSettings createCustomSettings(CodeStyleSettings settings) { + return new JsonCodeStyleSettings(settings); + } +} diff --git a/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java b/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java new file mode 100644 index 00000000..ed8d7503 --- /dev/null +++ b/json/src/com/intellij/json/formatter/JsonFormattingBuilderModel.java @@ -0,0 +1,42 @@ +package com.intellij.json.formatter; + +import com.intellij.formatting.*; +import com.intellij.json.JsonLanguage; +import com.intellij.psi.PsiElement; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.json.JsonElementTypes.*; + +/** + * @author Mikhail Golubev + */ +public class JsonFormattingBuilderModel implements FormattingModelBuilder { + @NotNull + @Override + public FormattingModel createModel(PsiElement element, CodeStyleSettings settings) { + JsonCodeStyleSettings customSettings = settings.getCustomSettings(JsonCodeStyleSettings.class); + SpacingBuilder spacingBuilder = createSpacingBuilder(settings); + final JsonBlock block = new JsonBlock(null, element.getNode(), customSettings, null, Indent.getSmartIndent(Indent.Type.CONTINUATION), null, spacingBuilder); + return FormattingModelProvider.createFormattingModelForPsiFile(element.getContainingFile(), block, settings); + } + + @NotNull + static SpacingBuilder createSpacingBuilder(CodeStyleSettings settings) { + final JsonCodeStyleSettings jsonSettings = settings.getCustomSettings(JsonCodeStyleSettings.class); + final CommonCodeStyleSettings commonSettings = settings.getCommonSettings(JsonLanguage.INSTANCE); + + final int spacesBeforeComma = commonSettings.SPACE_BEFORE_COMMA ? 1 : 0; + final int spacesBeforeColon = jsonSettings.SPACE_BEFORE_COLON ? 1 : 0; + final int spacesAfterColon = jsonSettings.SPACE_AFTER_COLON ? 1 : 0; + + return new SpacingBuilder(settings, JsonLanguage.INSTANCE) + .before(COLON).spacing(spacesBeforeColon, spacesBeforeColon, 0, false, 0) + .after(COLON).spacing(spacesAfterColon, spacesAfterColon, 0, false, 0) + .withinPair(L_BRACKET, R_BRACKET).spaceIf(commonSettings.SPACE_WITHIN_BRACKETS, true) + .withinPair(L_CURLY, R_CURLY).spaceIf(commonSettings.SPACE_WITHIN_BRACES, true) + .before(COMMA).spacing(spacesBeforeComma, spacesBeforeComma, 0, false, 0) + .after(COMMA).spaceIf(commonSettings.SPACE_AFTER_COMMA); + } +} diff --git a/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java b/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java new file mode 100644 index 00000000..eb727926 --- /dev/null +++ b/json/src/com/intellij/json/formatter/JsonLanguageCodeStyleSettingsProvider.java @@ -0,0 +1,116 @@ +package com.intellij.json.formatter; + +import com.intellij.application.options.IndentOptionsEditor; +import com.intellij.application.options.SmartIndentOptionsEditor; +import com.intellij.json.JsonBundle; +import com.intellij.json.JsonLanguage; +import com.intellij.lang.Language; +import com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider; +import com.intellij.util.ArrayUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import static com.intellij.psi.codeStyle.CodeStyleSettingsCustomizable.SPACES_OTHER; + +/** + * @author Mikhail Golubev + */ +public class JsonLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettingsProvider { + private static final String[] ALIGN_OPTIONS = Arrays.stream(JsonCodeStyleSettings.PropertyAlignment.values()) + .map(alignment -> alignment.getDescription()) + .toArray(value -> new String[value]); + + private static final int[] ALIGN_VALUES = + ArrayUtil.toIntArray(Arrays.stream(JsonCodeStyleSettings.PropertyAlignment.values()) + .map(alignment -> alignment.getId()) + .collect(Collectors.toList())); + + private static final String SAMPLE = "{\n" + + " \"json literals are\": {\n" + + " \"strings\": [\"foo\", \"bar\", \"\\u0062\\u0061\\u0072\"],\n" + + " \"numbers\": [42, 6.62606975e-34],\n" + + " \"boolean values\": [true, false,],\n" + + " \"objects\": {\"null\": null,\"another\": null,}\n" + + " }\n" + + "}"; + + + @Override + public void customizeSettings(@NotNull CodeStyleSettingsCustomizable consumer, @NotNull SettingsType settingsType) { + if (settingsType == SettingsType.SPACING_SETTINGS) { + consumer.showStandardOptions("SPACE_WITHIN_BRACKETS", + "SPACE_WITHIN_BRACES", + "SPACE_AFTER_COMMA", + "SPACE_BEFORE_COMMA"); + consumer.renameStandardOption("SPACE_WITHIN_BRACES", "Braces"); + consumer.showCustomOption(JsonCodeStyleSettings.class, "SPACE_BEFORE_COLON", "Before ':'", SPACES_OTHER); + consumer.showCustomOption(JsonCodeStyleSettings.class, "SPACE_AFTER_COLON", "After ':'", SPACES_OTHER); + } + else if (settingsType == SettingsType.BLANK_LINES_SETTINGS) { + consumer.showStandardOptions("KEEP_BLANK_LINES_IN_CODE"); + } + else if (settingsType == SettingsType.WRAPPING_AND_BRACES_SETTINGS) { + consumer.showStandardOptions("RIGHT_MARGIN", + "WRAP_ON_TYPING", + "KEEP_LINE_BREAKS", + "WRAP_LONG_LINES"); + + consumer.showCustomOption(JsonCodeStyleSettings.class, + "KEEP_TRAILING_COMMA", + "Trailing comma", + CodeStyleSettingsCustomizable.WRAPPING_KEEP); + + consumer.showCustomOption(JsonCodeStyleSettings.class, + "ARRAY_WRAPPING", + "Arrays", + null, + CodeStyleSettingsCustomizable.WRAP_OPTIONS, + CodeStyleSettingsCustomizable.WRAP_VALUES); + + consumer.showCustomOption(JsonCodeStyleSettings.class, + "OBJECT_WRAPPING", + "Objects", + null, + CodeStyleSettingsCustomizable.WRAP_OPTIONS, + CodeStyleSettingsCustomizable.WRAP_VALUES); + + consumer.showCustomOption(JsonCodeStyleSettings.class, + "PROPERTY_ALIGNMENT", + JsonBundle.message("formatter.align.properties.caption"), + "Objects", + ALIGN_OPTIONS, + ALIGN_VALUES); + + } + } + + @NotNull + @Override + public Language getLanguage() { + return JsonLanguage.INSTANCE; + } + + @Nullable + @Override + public IndentOptionsEditor getIndentOptionsEditor() { + return new SmartIndentOptionsEditor(); + } + + @Override + public String getCodeSample(@NotNull SettingsType settingsType) { + return SAMPLE; + } + + @Override + protected void customizeDefaults(@NotNull CommonCodeStyleSettings commonSettings, + @NotNull CommonCodeStyleSettings.IndentOptions indentOptions) { + indentOptions.INDENT_SIZE = 2; + // strip all blank lines by default + commonSettings.KEEP_BLANK_LINES_IN_CODE = 0; + } +} diff --git a/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java b/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java new file mode 100644 index 00000000..3116dc52 --- /dev/null +++ b/json/src/com/intellij/json/formatter/JsonLineWrapPositionStrategy.java @@ -0,0 +1,70 @@ +// 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 com.intellij.json.formatter; + +import com.intellij.json.JsonElementTypes; +import com.intellij.openapi.editor.DefaultLineWrapPositionStrategy; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtilCore; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonLineWrapPositionStrategy extends DefaultLineWrapPositionStrategy { + @Override + public int calculateWrapPosition(@NotNull Document document, + @Nullable Project project, + int startOffset, + int endOffset, + int maxPreferredOffset, + boolean allowToBeyondMaxPreferredOffset, + boolean isSoftWrap) { + if (isSoftWrap) { + return super.calculateWrapPosition(document, project, startOffset, endOffset, maxPreferredOffset, allowToBeyondMaxPreferredOffset, + true); + } + if (project == null) return -1; + final int wrapPosition = getMinWrapPosition(document, project, maxPreferredOffset); + if (wrapPosition == SKIP_WRAPPING) return -1; + int minWrapPosition = Math.max(startOffset, wrapPosition); + return super + .calculateWrapPosition(document, project, minWrapPosition, endOffset, maxPreferredOffset, allowToBeyondMaxPreferredOffset, isSoftWrap); + } + + private static final int SKIP_WRAPPING = -2; + private static int getMinWrapPosition(@NotNull Document document, @NotNull Project project, int offset) { + PsiDocumentManager manager = PsiDocumentManager.getInstance(project); + if (manager.isUncommited(document)) manager.commitDocument(document); + PsiFile psiFile = manager.getPsiFile(document); + if (psiFile != null) { + PsiElement currElement = psiFile.findElementAt(offset); + final IElementType elementType = PsiUtilCore.getElementType(currElement); + if (elementType == JsonElementTypes.DOUBLE_QUOTED_STRING + || elementType == JsonElementTypes.SINGLE_QUOTED_STRING + || elementType == JsonElementTypes.LITERAL + || elementType == JsonElementTypes.BOOLEAN_LITERAL + || elementType == JsonElementTypes.TRUE + || elementType == JsonElementTypes.FALSE + || elementType == JsonElementTypes.IDENTIFIER + || elementType == JsonElementTypes.NULL_LITERAL + || elementType == JsonElementTypes.NUMBER_LITERAL) { + return currElement.getTextRange().getEndOffset(); + } + if (elementType == JsonElementTypes.COLON) { + return SKIP_WRAPPING; + } + if (currElement != null) { + if (currElement instanceof PsiComment || + PsiUtilCore.getElementType(PsiTreeUtil.skipWhitespacesForward(currElement)) == JsonElementTypes.COMMA) { + return SKIP_WRAPPING; + } + } + } + return -1; + } +} diff --git a/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java b/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java new file mode 100644 index 00000000..b3423fd9 --- /dev/null +++ b/json/src/com/intellij/json/formatter/JsonTrailingCommaRemover.java @@ -0,0 +1,111 @@ +/* + * 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.intellij.json.formatter; + +import com.intellij.application.options.CodeStyle; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.JsonLanguage; +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.impl.JsonRecursiveElementVisitor; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor; +import com.intellij.util.DocumentUtil; +import com.intellij.util.ObjectUtils; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonTrailingCommaRemover implements PreFormatProcessor { + + @NotNull + @Override + public TextRange process(@NotNull ASTNode element, @NotNull TextRange range) { + PsiElement rootPsi = element.getPsi(); + if (rootPsi.getLanguage() != JsonLanguage.INSTANCE) { + return range; + } + JsonCodeStyleSettings settings = CodeStyle.getCustomSettings(rootPsi.getContainingFile(), JsonCodeStyleSettings.class); + if (settings.KEEP_TRAILING_COMMA) { + return range; + } + PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(rootPsi.getProject()); + Document document = psiDocumentManager.getDocument(rootPsi.getContainingFile()); + if (document == null) { + return range; + } + DocumentUtil.executeInBulk(document, true, () -> { + psiDocumentManager.doPostponedOperationsAndUnblockDocument(document); + PsiElementVisitor visitor = new Visitor(document); + rootPsi.accept(visitor); + psiDocumentManager.commitDocument(document); + }); + return range; + } + + private static class Visitor extends JsonRecursiveElementVisitor { + private final Document myDocument; + private int myOffsetDelta; + + Visitor(Document document) { + myDocument = document; + } + + @Override + public void visitArray(@NotNull JsonArray o) { + super.visitArray(o); + PsiElement lastChild = o.getLastChild(); + if (lastChild == null || lastChild.getNode().getElementType() != JsonElementTypes.R_BRACKET) { + return; + } + deleteTrailingCommas(ObjectUtils.coalesce(ContainerUtil.getLastItem(o.getValueList()), o.getFirstChild())); + } + + @Override + public void visitObject(@NotNull JsonObject o) { + super.visitObject(o); + PsiElement lastChild = o.getLastChild(); + if (lastChild == null || lastChild.getNode().getElementType() != JsonElementTypes.R_CURLY) { + return; + } + deleteTrailingCommas(ObjectUtils.coalesce(ContainerUtil.getLastItem(o.getPropertyList()), o.getFirstChild())); + } + + private void deleteTrailingCommas(@Nullable PsiElement lastElementOrOpeningBrace) { + PsiElement element = lastElementOrOpeningBrace != null ? lastElementOrOpeningBrace.getNextSibling() : null; + + while (element != null) { + if (element.getNode().getElementType() == JsonElementTypes.COMMA || + element instanceof PsiErrorElement && ",".equals(element.getText())) { + deleteNode(element.getNode()); + } + else if (!(element instanceof PsiComment || element instanceof PsiWhiteSpace)) { + break; + } + element = element.getNextSibling(); + } + } + + private void deleteNode(@NotNull ASTNode node) { + int length = node.getTextLength(); + myDocument.deleteString(node.getStartOffset() + myOffsetDelta, node.getStartOffset() + length + myOffsetDelta); + myOffsetDelta -= length; + } + } +} diff --git a/json/src/com/intellij/json/highlighting/JsonColorsPage.java b/json/src/com/intellij/json/highlighting/JsonColorsPage.java new file mode 100644 index 00000000..69075e6c --- /dev/null +++ b/json/src/com/intellij/json/highlighting/JsonColorsPage.java @@ -0,0 +1,105 @@ +package com.intellij.json.highlighting; + +import com.google.common.collect.ImmutableMap; +import com.intellij.icons.AllIcons; +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.fileTypes.SyntaxHighlighter; +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; +import com.intellij.openapi.options.colors.AttributesDescriptor; +import com.intellij.openapi.options.colors.ColorDescriptor; +import com.intellij.openapi.options.colors.ColorSettingsPage; +import com.intellij.psi.codeStyle.DisplayPriority; +import com.intellij.psi.codeStyle.DisplayPrioritySortable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.Map; + +import static com.intellij.json.highlighting.JsonSyntaxHighlighterFactory.*; + +/** + * @author Mikhail Golubev + */ +public class JsonColorsPage implements ColorSettingsPage, DisplayPrioritySortable { + private static final Map<String, TextAttributesKey> ourAdditionalHighlighting = ImmutableMap.of("propertyKey", JSON_PROPERTY_KEY); + + private static final AttributesDescriptor[] ourAttributeDescriptors = new AttributesDescriptor[]{ + new AttributesDescriptor("Property key", JSON_PROPERTY_KEY), + + new AttributesDescriptor("Braces", JSON_BRACES), + new AttributesDescriptor("Brackets", JSON_BRACKETS), + new AttributesDescriptor("Comma", JSON_COMMA), + new AttributesDescriptor("Colon", JSON_COLON), + new AttributesDescriptor("Number", JSON_NUMBER), + new AttributesDescriptor("String", JSON_STRING), + new AttributesDescriptor("Keyword", JSON_KEYWORD), + new AttributesDescriptor("Line comment", JSON_LINE_COMMENT), + new AttributesDescriptor("Block comment", JSON_BLOCK_COMMENT), + //new AttributesDescriptor("", JSON_IDENTIFIER), + new AttributesDescriptor("Valid escape sequence", JSON_VALID_ESCAPE), + new AttributesDescriptor("Invalid escape sequence", JSON_INVALID_ESCAPE), + }; + + @Nullable + @Override + public Icon getIcon() { + return AllIcons.FileTypes.Json; + } + + @NotNull + @Override + public SyntaxHighlighter getHighlighter() { + return SyntaxHighlighterFactory.getSyntaxHighlighter(JsonLanguage.INSTANCE, null, null); + } + + @NotNull + @Override + public String getDemoText() { + return "{\n" + + " // Line comments are not included in standard but nonetheless allowed.\n" + + " /* As well as block comments. */\n" + + " <propertyKey>\"the only keywords are\"</propertyKey>: [true, false, null],\n" + + " <propertyKey>\"strings with\"</propertyKey>: {\n" + + " <propertyKey>\"no escapes\"</propertyKey>: \"pseudopolinomiality\"\n" + + " <propertyKey>\"valid escapes\"</propertyKey>: \"C-style\\r\\n and unicode\\u0021\",\n" + + " <propertyKey>\"illegal escapes\"</propertyKey>: \"\\0377\\x\\\"\n" + + " },\n" + + " <propertyKey>\"some numbers\"</propertyKey>: [\n" + + " 42,\n" + + " -0.0e-0,\n" + + " 6.626e-34\n" + + " ] \n" + + "}"; + } + + @Nullable + @Override + public Map<String, TextAttributesKey> getAdditionalHighlightingTagToDescriptorMap() { + return ourAdditionalHighlighting; + } + + @NotNull + @Override + public AttributesDescriptor[] getAttributeDescriptors() { + return ourAttributeDescriptors; + } + + @NotNull + @Override + public ColorDescriptor[] getColorDescriptors() { + return ColorDescriptor.EMPTY_ARRAY; + } + + @NotNull + @Override + public String getDisplayName() { + return "JSON"; + } + + @Override + public DisplayPriority getPriority() { + return DisplayPriority.LANGUAGE_SETTINGS; + } +} diff --git a/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java b/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java new file mode 100644 index 00000000..fd80e8cb --- /dev/null +++ b/json/src/com/intellij/json/highlighting/JsonSyntaxHighlighterFactory.java @@ -0,0 +1,164 @@ +// 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 com.intellij.json.highlighting; + +import com.intellij.json.JsonElementTypes; +import com.intellij.json.JsonFileType; +import com.intellij.json.JsonLanguage; +import com.intellij.json.JsonLexer; +import com.intellij.lang.Language; +import com.intellij.lexer.LayeredLexer; +import com.intellij.lexer.Lexer; +import com.intellij.lexer.StringLiteralLexer; +import com.intellij.openapi.editor.HighlighterColors; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.SyntaxHighlighter; +import com.intellij.openapi.fileTypes.SyntaxHighlighterBase; +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.StringEscapesTokenTypes; +import com.intellij.psi.TokenType; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import static com.intellij.openapi.editor.DefaultLanguageHighlighterColors.*; + +public class JsonSyntaxHighlighterFactory extends SyntaxHighlighterFactory { + + private static final String PERMISSIVE_ESCAPES; + static { + final StringBuilder escapesBuilder = new StringBuilder("/"); + for (char c = '\1'; c < '\255'; c++) { + if (c != 'x' && c != 'u' && !Character.isDigit(c) && c != '\n' && c != '\r') { + escapesBuilder.append(c); + } + } + PERMISSIVE_ESCAPES = escapesBuilder.toString(); + } + + public static final TextAttributesKey JSON_BRACKETS = TextAttributesKey.createTextAttributesKey("JSON.BRACKETS", BRACKETS); + public static final TextAttributesKey JSON_BRACES = TextAttributesKey.createTextAttributesKey("JSON.BRACES", BRACES); + public static final TextAttributesKey JSON_COMMA = TextAttributesKey.createTextAttributesKey("JSON.COMMA", COMMA); + public static final TextAttributesKey JSON_COLON = TextAttributesKey.createTextAttributesKey("JSON.COLON", SEMICOLON); + public static final TextAttributesKey JSON_NUMBER = TextAttributesKey.createTextAttributesKey("JSON.NUMBER", NUMBER); + public static final TextAttributesKey JSON_STRING = TextAttributesKey.createTextAttributesKey("JSON.STRING", STRING); + public static final TextAttributesKey JSON_KEYWORD = TextAttributesKey.createTextAttributesKey("JSON.KEYWORD", KEYWORD); + public static final TextAttributesKey JSON_LINE_COMMENT = TextAttributesKey.createTextAttributesKey("JSON.LINE_COMMENT", LINE_COMMENT); + public static final TextAttributesKey JSON_BLOCK_COMMENT = TextAttributesKey.createTextAttributesKey("JSON.BLOCK_COMMENT", BLOCK_COMMENT); + + // Artificial element type + public static final TextAttributesKey JSON_IDENTIFIER = TextAttributesKey.createTextAttributesKey("JSON.IDENTIFIER", IDENTIFIER); + + // Added by annotators + public static final TextAttributesKey JSON_PROPERTY_KEY = TextAttributesKey.createTextAttributesKey("JSON.PROPERTY_KEY", INSTANCE_FIELD); + + // String escapes + public static final TextAttributesKey JSON_VALID_ESCAPE = + TextAttributesKey.createTextAttributesKey("JSON.VALID_ESCAPE", VALID_STRING_ESCAPE); + public static final TextAttributesKey JSON_INVALID_ESCAPE = + TextAttributesKey.createTextAttributesKey("JSON.INVALID_ESCAPE", INVALID_STRING_ESCAPE); + + + @NotNull + @Override + public SyntaxHighlighter getSyntaxHighlighter(@Nullable Project project, @Nullable VirtualFile virtualFile) { + return new MyHighlighter(virtualFile); + } + + private class MyHighlighter extends SyntaxHighlighterBase { + private final Map<IElementType, TextAttributesKey> ourAttributes = new HashMap<>(); + + @Nullable + private final VirtualFile myFile; + + { + fillMap(ourAttributes, JSON_BRACES, JsonElementTypes.L_CURLY, JsonElementTypes.R_CURLY); + fillMap(ourAttributes, JSON_BRACKETS, JsonElementTypes.L_BRACKET, JsonElementTypes.R_BRACKET); + fillMap(ourAttributes, JSON_COMMA, JsonElementTypes.COMMA); + fillMap(ourAttributes, JSON_COLON, JsonElementTypes.COLON); + fillMap(ourAttributes, JSON_STRING, JsonElementTypes.DOUBLE_QUOTED_STRING); + fillMap(ourAttributes, JSON_STRING, JsonElementTypes.SINGLE_QUOTED_STRING); + fillMap(ourAttributes, JSON_NUMBER, JsonElementTypes.NUMBER); + fillMap(ourAttributes, JSON_KEYWORD, JsonElementTypes.TRUE, JsonElementTypes.FALSE, JsonElementTypes.NULL); + fillMap(ourAttributes, JSON_LINE_COMMENT, JsonElementTypes.LINE_COMMENT); + fillMap(ourAttributes, JSON_BLOCK_COMMENT, JsonElementTypes.BLOCK_COMMENT); + // TODO may be it's worth to add more sensible highlighting for identifiers + fillMap(ourAttributes, JSON_IDENTIFIER, JsonElementTypes.IDENTIFIER); + fillMap(ourAttributes, HighlighterColors.BAD_CHARACTER, TokenType.BAD_CHARACTER); + + fillMap(ourAttributes, JSON_VALID_ESCAPE, StringEscapesTokenTypes.VALID_STRING_ESCAPE_TOKEN); + fillMap(ourAttributes, JSON_INVALID_ESCAPE, StringEscapesTokenTypes.INVALID_CHARACTER_ESCAPE_TOKEN); + fillMap(ourAttributes, JSON_INVALID_ESCAPE, StringEscapesTokenTypes.INVALID_UNICODE_ESCAPE_TOKEN); + } + + MyHighlighter(@Nullable VirtualFile file) { + myFile = file; + } + + @NotNull + @Override + public Lexer getHighlightingLexer() { + LayeredLexer layeredLexer = new LayeredLexer(getLexer()); + boolean isPermissiveDialect = isPermissiveDialect(); + layeredLexer.registerSelfStoppingLayer(new StringLiteralLexer('\"', JsonElementTypes.DOUBLE_QUOTED_STRING, isCanEscapeEol(), + isPermissiveDialect ? PERMISSIVE_ESCAPES : "/", false, isPermissiveDialect) { + @NotNull + @Override + protected IElementType handleSingleSlashEscapeSequence() { + return isPermissiveDialect ? myOriginalLiteralToken : super.handleSingleSlashEscapeSequence(); + } + + @Override + protected boolean shouldAllowSlashZero() { + return isPermissiveDialect; + } + }, + new IElementType[]{JsonElementTypes.DOUBLE_QUOTED_STRING}, IElementType.EMPTY_ARRAY); + layeredLexer.registerSelfStoppingLayer(new StringLiteralLexer('\'', JsonElementTypes.SINGLE_QUOTED_STRING, isCanEscapeEol(), + isPermissiveDialect ? PERMISSIVE_ESCAPES : "/", false, isPermissiveDialect){ + @NotNull + @Override + protected IElementType handleSingleSlashEscapeSequence() { + return isPermissiveDialect ? myOriginalLiteralToken : super.handleSingleSlashEscapeSequence(); + } + + @Override + protected boolean shouldAllowSlashZero() { + return isPermissiveDialect; + } + }, + new IElementType[]{JsonElementTypes.SINGLE_QUOTED_STRING}, IElementType.EMPTY_ARRAY); + return layeredLexer; + } + + private boolean isPermissiveDialect() { + FileType fileType = myFile == null ? null : myFile.getFileType(); + boolean isPermissiveDialect = false; + if (fileType instanceof JsonFileType) { + Language language = ((JsonFileType)fileType).getLanguage(); + isPermissiveDialect = language instanceof JsonLanguage && ((JsonLanguage)language).hasPermissiveStrings(); + } + return isPermissiveDialect; + } + + @NotNull + @Override + public TextAttributesKey[] getTokenHighlights(IElementType type) { + return pack(ourAttributes.get(type)); + } + } + + @NotNull + protected Lexer getLexer() { + return new JsonLexer(); + } + + protected boolean isCanEscapeEol() { + return false; + } +} diff --git a/json/src/com/intellij/json/json5/Json5FileType.java b/json/src/com/intellij/json/json5/Json5FileType.java new file mode 100644 index 00000000..b9979faf --- /dev/null +++ b/json/src/com/intellij/json/json5/Json5FileType.java @@ -0,0 +1,32 @@ +// 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 com.intellij.json.json5; + +import com.intellij.json.JsonFileType; +import org.jetbrains.annotations.NotNull; + +public class Json5FileType extends JsonFileType { + public static final Json5FileType INSTANCE = new Json5FileType(); + public static final String DEFAULT_EXTENSION = "json5"; + + public Json5FileType() { + super(Json5Language.INSTANCE); + } + + @NotNull + @Override + public String getName() { + return "JSON5"; + } + + @NotNull + @Override + public String getDescription() { + return "JSON5"; + } + + @NotNull + @Override + public String getDefaultExtension() { + return DEFAULT_EXTENSION; + } +} diff --git a/json/src/com/intellij/json/json5/Json5FileTypeFactory.java b/json/src/com/intellij/json/json5/Json5FileTypeFactory.java new file mode 100644 index 00000000..bf6a0254 --- /dev/null +++ b/json/src/com/intellij/json/json5/Json5FileTypeFactory.java @@ -0,0 +1,13 @@ +// 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 com.intellij.json.json5; + +import com.intellij.openapi.fileTypes.FileTypeConsumer; +import com.intellij.openapi.fileTypes.FileTypeFactory; +import org.jetbrains.annotations.NotNull; + +public class Json5FileTypeFactory extends FileTypeFactory { + @Override + public void createFileTypes(@NotNull FileTypeConsumer consumer) { + consumer.consume(Json5FileType.INSTANCE, Json5FileType.DEFAULT_EXTENSION); + } +} diff --git a/json/src/com/intellij/json/json5/Json5Language.java b/json/src/com/intellij/json/json5/Json5Language.java new file mode 100644 index 00000000..a6412639 --- /dev/null +++ b/json/src/com/intellij/json/json5/Json5Language.java @@ -0,0 +1,17 @@ +// 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 com.intellij.json.json5; + +import com.intellij.json.JsonLanguage; + +public class Json5Language extends JsonLanguage { + public static final Json5Language INSTANCE = new Json5Language(); + + protected Json5Language() { + super("JSON5", "application/json5"); + } + + @Override + public boolean hasPermissiveStrings() { + return true; + } +} diff --git a/json/src/com/intellij/json/json5/Json5Lexer.java b/json/src/com/intellij/json/json5/Json5Lexer.java new file mode 100644 index 00000000..0617e379 --- /dev/null +++ b/json/src/com/intellij/json/json5/Json5Lexer.java @@ -0,0 +1,10 @@ +// 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 com.intellij.json.json5; + +import com.intellij.lexer.FlexAdapter; + +public class Json5Lexer extends FlexAdapter { + public Json5Lexer() { + super(new _Json5Lexer()); + } +} diff --git a/json/src/com/intellij/json/json5/Json5ParserDefinition.java b/json/src/com/intellij/json/json5/Json5ParserDefinition.java new file mode 100644 index 00000000..d0b146bc --- /dev/null +++ b/json/src/com/intellij/json/json5/Json5ParserDefinition.java @@ -0,0 +1,31 @@ +// 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 com.intellij.json.json5; + +import com.intellij.json.JsonParserDefinition; +import com.intellij.json.psi.impl.JsonFileImpl; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.project.Project; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IFileElementType; +import org.jetbrains.annotations.NotNull; + +public class Json5ParserDefinition extends JsonParserDefinition { + public static final IFileElementType FILE = new IFileElementType(Json5Language.INSTANCE); + + @NotNull + @Override + public Lexer createLexer(Project project) { + return new Json5Lexer(); + } + + @Override + public PsiFile createFile(FileViewProvider fileViewProvider) { + return new JsonFileImpl(fileViewProvider, Json5Language.INSTANCE); + } + + @Override + public IFileElementType getFileNodeType() { + return FILE; + } +} diff --git a/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java b/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java new file mode 100644 index 00000000..6ba518b4 --- /dev/null +++ b/json/src/com/intellij/json/json5/Json5PsiWalkerFactory.java @@ -0,0 +1,36 @@ +// 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 com.intellij.json.json5; + +import com.intellij.codeInsight.completion.CompletionUtil; +import com.intellij.json.JsonDialectUtil; +import com.intellij.psi.PsiElement; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalkerFactory; +import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import org.jetbrains.annotations.NotNull; + +public class Json5PsiWalkerFactory implements JsonLikePsiWalkerFactory { + @Override + public boolean handles(@NotNull PsiElement element) { + PsiElement parent = element.getParent(); + if (parent == null) return false; + return JsonDialectUtil.getLanguage(CompletionUtil.getOriginalOrSelf(parent)) == Json5Language.INSTANCE; + } + + @NotNull + @Override + public JsonLikePsiWalker create(@NotNull JsonSchemaObject schemaObject) { + return new JsonOriginalPsiWalker() { + @Override + public boolean isNameQuoted() { + return false; + } + + @Override + public boolean onlyDoubleQuotesForStringLiterals() { + return false; + } + }; + } +} diff --git a/json/src/com/intellij/json/json5/_Json5Lexer.flex b/json/src/com/intellij/json/json5/_Json5Lexer.flex new file mode 100644 index 00000000..14f4130f --- /dev/null +++ b/json/src/com/intellij/json/json5/_Json5Lexer.flex @@ -0,0 +1,64 @@ +package com.intellij.json.json5; + +import com.intellij.lexer.FlexLexer; +import com.intellij.psi.tree.IElementType; + +import static com.intellij.psi.TokenType.BAD_CHARACTER; +import static com.intellij.psi.TokenType.WHITE_SPACE; +import static com.intellij.json.JsonElementTypes.*; + +%% + +%{ + public _Json5Lexer() { + this((java.io.Reader)null); + } +%} + +%public +%class _Json5Lexer +%implements FlexLexer +%function advance +%type IElementType +%unicode + +EOL=\R +WHITE_SPACE=\s+ +HEX_DIGIT=[0-9A-Fa-f] + +LINE_COMMENT="//".* +BLOCK_COMMENT="/"\*([^*]|\*+[^*/])*(\*+"/")? +LINE_TERMINATOR_SEQUENCE=\R +CRLF= [\ \t \f]* {LINE_TERMINATOR_SEQUENCE} +DOUBLE_QUOTED_STRING=\"([^\\\"\r\n]|\\[^\r\n]|\\{CRLF})*\"? +SINGLE_QUOTED_STRING='([^\\'\r\n]|\\[^\r\n]|\\{CRLF})*'? +JSON5_NUMBER=(\+|-)?(0|[1-9][0-9]*)?\.?([0-9]+)?([eE][+-]?[0-9]*)? +HEX_DIGITS=({HEX_DIGIT})+ +HEX_INTEGER_LITERAL=(\+|-)?0[Xx]({HEX_DIGITS}) +NUMBER={JSON5_NUMBER}|{HEX_INTEGER_LITERAL}|Infinity|-Infinity|\+Infinity|NaN|-NaN|\+NaN +IDENTIFIER=[[:jletterdigit:]~!()*\-."/"@\^<>=]+ + +%% +<YYINITIAL> { + {WHITE_SPACE} { return WHITE_SPACE; } + + "{" { return L_CURLY; } + "}" { return R_CURLY; } + "[" { return L_BRACKET; } + "]" { return R_BRACKET; } + "," { return COMMA; } + ":" { return COLON; } + "true" { return TRUE; } + "false" { return FALSE; } + "null" { return NULL; } + + {LINE_COMMENT} { return LINE_COMMENT; } + {BLOCK_COMMENT} { return BLOCK_COMMENT; } + {DOUBLE_QUOTED_STRING} { return DOUBLE_QUOTED_STRING; } + {SINGLE_QUOTED_STRING} { return SINGLE_QUOTED_STRING; } + {NUMBER} { return NUMBER; } + {IDENTIFIER} { return IDENTIFIER; } + +} + +[^] { return BAD_CHARACTER; } diff --git a/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java b/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java new file mode 100644 index 00000000..1ed1f273 --- /dev/null +++ b/json/src/com/intellij/json/json5/codeinsight/Json5JsonLiteralChecker.java @@ -0,0 +1,52 @@ +// 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 com.intellij.json.json5.codeinsight; + +import com.intellij.json.JsonDialectUtil; +import com.intellij.json.codeinsight.JsonLiteralChecker; +import com.intellij.json.codeinsight.StandardJsonLiteralChecker; +import com.intellij.json.json5.Json5Language; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Pattern; + +public class Json5JsonLiteralChecker implements JsonLiteralChecker { + private static final Pattern VALID_HEX_ESCAPE = Pattern.compile("\\\\(x[0-9a-fA-F]{2})"); + private static final Pattern INVALID_NUMERIC_ESCAPE = Pattern.compile("\\\\[1-9]"); + @Nullable + @Override + public String getErrorForNumericLiteral(String literalText) { + return null; + } + + @Nullable + @Override + public Pair<TextRange, String> getErrorForStringFragment(Pair<TextRange, String> fragment, JsonStringLiteral stringLiteral) { + String fragmentText = fragment.second; + if (fragmentText.startsWith("\\") && fragmentText.length() > 1 && fragmentText.endsWith("\n")) { + if (StringUtil.isEmptyOrSpaces(fragmentText.substring(1, fragmentText.length() - 1))) { + return null; + } + } + + if (fragmentText.startsWith("\\x") && VALID_HEX_ESCAPE.matcher(fragmentText).matches()) { + return null; + } + + if (!StandardJsonLiteralChecker.VALID_ESCAPE.matcher(fragmentText).matches() && !INVALID_NUMERIC_ESCAPE.matcher(fragmentText).matches()) { + return null; + } + + final String error = StandardJsonLiteralChecker.getStringError(fragmentText); + return error == null ? null : Pair.create(fragment.first, error); + } + + @Override + public boolean isApplicable(PsiElement element) { + return JsonDialectUtil.getLanguage(element) == Json5Language.INSTANCE; + } +} diff --git a/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java b/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java new file mode 100644 index 00000000..d61cffa1 --- /dev/null +++ b/json/src/com/intellij/json/json5/codeinsight/Json5StandardComplianceInspection.java @@ -0,0 +1,81 @@ +// 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 com.intellij.json.json5.codeinsight; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.json.JsonBundle; +import com.intellij.json.JsonDialectUtil; +import com.intellij.json.codeinsight.JsonStandardComplianceInspection; +import com.intellij.json.json5.Json5Language; +import com.intellij.json.psi.JsonLiteral; +import com.intellij.json.psi.JsonPsiUtil; +import com.intellij.json.psi.JsonReferenceExpression; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public class Json5StandardComplianceInspection extends JsonStandardComplianceInspection { + + @Override + @NotNull + public String getDisplayName() { + return JsonBundle.message("inspection.compliance5.name"); + } + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) { + if (!(JsonDialectUtil.getLanguage(holder.getFile()) instanceof Json5Language)) return PsiElementVisitor.EMPTY_VISITOR; + return new StandardJson5ValidatingElementVisitor(holder); + } + + @Override + public JComponent createOptionsPanel() { + return null; + } + + private class StandardJson5ValidatingElementVisitor extends StandardJsonValidatingElementVisitor { + StandardJson5ValidatingElementVisitor(ProblemsHolder holder) { + super(holder); + } + + @Override + protected boolean allowComments() { + return true; + } + + @Override + protected boolean allowSingleQuotes() { + return true; + } + + @Override + protected boolean allowIdentifierPropertyNames() { + return true; + } + + @Override + protected boolean allowTrailingCommas() { + return true; + } + + @Override + protected boolean allowNanInfinity() { + return true; + } + + @Override + protected boolean isValidPropertyName(@NotNull PsiElement literal) { + if (literal instanceof JsonLiteral) { + String textWithoutHostEscaping = JsonPsiUtil.getElementTextWithoutHostEscaping(literal); + return textWithoutHostEscaping.startsWith("\"") || textWithoutHostEscaping.startsWith("'"); + } + if (literal instanceof JsonReferenceExpression) { + return StringUtil.isJavaIdentifier(literal.getText()); + } + return false; + } + } +} diff --git a/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java b/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java new file mode 100644 index 00000000..f20466f5 --- /dev/null +++ b/json/src/com/intellij/json/json5/highlighting/Json5SyntaxHighlightingFactory.java @@ -0,0 +1,20 @@ +// 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 com.intellij.json.json5.highlighting; + +import com.intellij.json.highlighting.JsonSyntaxHighlighterFactory; +import com.intellij.json.json5.Json5Lexer; +import com.intellij.lexer.Lexer; +import org.jetbrains.annotations.NotNull; + +public class Json5SyntaxHighlightingFactory extends JsonSyntaxHighlighterFactory { + @NotNull + @Override + protected Lexer getLexer() { + return new Json5Lexer(); + } + + @Override + protected boolean isCanEscapeEol() { + return true; + } +} diff --git a/json/src/com/intellij/json/liveTemplates/JsonContextType.java b/json/src/com/intellij/json/liveTemplates/JsonContextType.java new file mode 100644 index 00000000..cc5ef222 --- /dev/null +++ b/json/src/com/intellij/json/liveTemplates/JsonContextType.java @@ -0,0 +1,22 @@ +package com.intellij.json.liveTemplates; + +import com.intellij.codeInsight.template.FileTypeBasedContextType; +import com.intellij.json.JsonBundle; +import com.intellij.json.JsonFileType; +import com.intellij.json.psi.JsonFile; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +/** + * @author Konstantin.Ulitin + */ +public class JsonContextType extends FileTypeBasedContextType { + protected JsonContextType() { + super("JSON", JsonBundle.message("json.template.context.type"), JsonFileType.INSTANCE); + } + + @Override + public boolean isInContext(@NotNull PsiFile file, int offset) { + return file instanceof JsonFile; + } +} diff --git a/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java b/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java new file mode 100644 index 00000000..0e21d5fa --- /dev/null +++ b/json/src/com/intellij/json/liveTemplates/JsonInLiteralsContextType.java @@ -0,0 +1,21 @@ +// 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 com.intellij.json.liveTemplates; + +import com.intellij.codeInsight.template.TemplateContextType; +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.patterns.PlatformPatterns.psiElement; + +public class JsonInLiteralsContextType extends TemplateContextType { + protected JsonInLiteralsContextType() { + super("JSON_STRING_VALUES", "JSON String Values", JsonContextType.class); + } + + @Override + public boolean isInContext(@NotNull PsiFile file, int offset) { + return file instanceof JsonFile && psiElement().inside(JsonStringLiteral.class).accepts(file.findElementAt(offset)); + } +} diff --git a/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java b/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java new file mode 100644 index 00000000..145e1b28 --- /dev/null +++ b/json/src/com/intellij/json/liveTemplates/JsonInPropertyKeysContextType.java @@ -0,0 +1,33 @@ +// 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 com.intellij.json.liveTemplates; + +import com.intellij.codeInsight.template.TemplateContextType; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonPsiUtil; +import com.intellij.json.psi.JsonValue; +import com.intellij.patterns.PatternCondition; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.util.ProcessingContext; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.patterns.PlatformPatterns.psiElement; + +public class JsonInPropertyKeysContextType extends TemplateContextType { + protected JsonInPropertyKeysContextType() { + super("JSON_PROPERTY_KEYS", "JSON Property Keys", JsonContextType.class); + } + + @Override + public boolean isInContext(@NotNull PsiFile file, int offset) { + return file instanceof JsonFile && psiElement().inside(psiElement(JsonValue.class) + .with(new PatternCondition<PsiElement>("insidePropertyKey") { + @Override + public boolean accepts(@NotNull PsiElement element, + ProcessingContext context) { + return JsonPsiUtil.isPropertyKey(element); + } + })).beforeLeaf(psiElement(JsonElementTypes.COLON)).accepts(file.findElementAt(offset)); + } +}
\ No newline at end of file diff --git a/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java b/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java new file mode 100644 index 00000000..f82115fc --- /dev/null +++ b/json/src/com/intellij/json/navigation/JsonQualifiedNameKind.java @@ -0,0 +1,20 @@ +// 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 com.intellij.json.navigation; + +import kotlin.NotImplementedError; + +public enum JsonQualifiedNameKind { + Qualified, + JsonPointer; + + @Override + public String toString() { + switch (this) { + case Qualified: + return "qualified name"; + case JsonPointer: + return "JSON Pointer"; + } + throw new NotImplementedError("Unknown name kind: " + this.name()); + } +} diff --git a/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java b/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java new file mode 100644 index 00000000..31ee728c --- /dev/null +++ b/json/src/com/intellij/json/navigation/JsonQualifiedNameProvider.java @@ -0,0 +1,69 @@ +package com.intellij.json.navigation; + +import com.intellij.ide.actions.QualifiedNameProvider; +import com.intellij.json.JsonUtil; +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonElement; +import com.intellij.json.psi.JsonProperty; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.jsonSchema.JsonPointerUtil; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class JsonQualifiedNameProvider implements QualifiedNameProvider { + @Nullable + @Override + public PsiElement adjustElementToCopy(PsiElement element) { + return null; + } + + @Nullable + @Override + public String getQualifiedName(PsiElement element) { + return generateQualifiedName(element, JsonQualifiedNameKind.Qualified); + } + + public static String generateQualifiedName(PsiElement element, JsonQualifiedNameKind qualifiedNameKind) { + if (!(element instanceof JsonElement)) { + return null; + } + JsonElement parentProperty = PsiTreeUtil.getNonStrictParentOfType(element, JsonProperty.class, JsonArray.class); + StringBuilder builder = new StringBuilder(); + while (parentProperty != null) { + if (parentProperty instanceof JsonProperty) { + String name = parentProperty.getName(); + if (qualifiedNameKind == JsonQualifiedNameKind.JsonPointer) { + name = name == null ? null : JsonPointerUtil.escapeForJsonPointer(name); + } + builder.insert(0, name); + builder.insert(0, qualifiedNameKind == JsonQualifiedNameKind.JsonPointer ? "/" : "."); + } + else { + int index = JsonUtil.getArrayIndexOfItem(element instanceof JsonProperty ? element.getParent() : element); + if (index == -1) return null; + builder.insert(0, qualifiedNameKind == JsonQualifiedNameKind.JsonPointer ? ("/" + index) : ("[" + index + "]")); + } + element = parentProperty; + parentProperty = PsiTreeUtil.getParentOfType(parentProperty, JsonProperty.class, JsonArray.class); + } + + if (builder.length() == 0) return null; + + // if the first operation is array indexing, we insert the 'root' element $ + if (builder.charAt(0) == '[') { + builder.insert(0, "$"); + } + + return StringUtil.trimStart(builder.toString(), "."); + } + + @Override + public PsiElement qualifiedNameToElement(String fqn, Project project) { + return null; + } +} diff --git a/json/src/com/intellij/json/psi/JsonElement.java b/json/src/com/intellij/json/psi/JsonElement.java new file mode 100644 index 00000000..f3345255 --- /dev/null +++ b/json/src/com/intellij/json/psi/JsonElement.java @@ -0,0 +1,10 @@ +package com.intellij.json.psi; + +import com.intellij.psi.NavigatablePsiElement; +import com.intellij.psi.PsiElement; + +/** + * @author Mikhail Golubev + */ +public interface JsonElement extends PsiElement, NavigatablePsiElement { +} diff --git a/json/src/com/intellij/json/psi/JsonElementGenerator.java b/json/src/com/intellij/json/psi/JsonElementGenerator.java new file mode 100644 index 00000000..535f9d58 --- /dev/null +++ b/json/src/com/intellij/json/psi/JsonElementGenerator.java @@ -0,0 +1,79 @@ +package com.intellij.json.psi; + +import com.intellij.json.JsonFileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiFileFactory; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonElementGenerator { + private final Project myProject; + + public JsonElementGenerator(@NotNull Project project) { + myProject = project; + } + + /** + * Create lightweight in-memory {@link com.intellij.json.psi.JsonFile} filled with {@code content}. + * + * @param content content of the file to be created + * @return created file + */ + @NotNull + public PsiFile createDummyFile(@NotNull String content) { + final PsiFileFactory psiFileFactory = PsiFileFactory.getInstance(myProject); + return psiFileFactory.createFileFromText("dummy." + JsonFileType.INSTANCE.getDefaultExtension(), JsonFileType.INSTANCE, content); + } + + /** + * Create JSON value from supplied content. + * + * @param content properly escaped text of JSON value, e.g. Java literal {@code "\"new\\nline\""} if you want to create string literal + * @param <T> type of the JSON value desired + * @return element created from given text + * + * @see #createStringLiteral(String) + */ + @NotNull + public <T extends JsonValue> T createValue(@NotNull String content) { + final PsiFile file = createDummyFile("{\"foo\": " + content + "}"); + //noinspection unchecked,ConstantConditions + return (T)((JsonObject)file.getFirstChild()).getPropertyList().get(0).getValue(); + } + + @NotNull + public JsonObject createObject(@NotNull String content) { + final PsiFile file = createDummyFile("{" + content + "}"); + // noinspection ConstantConditions + return (JsonObject) file.getFirstChild(); + } + + /** + * Create JSON string literal from supplied <em>unescaped</em> content. + * + * @param unescapedContent unescaped content of string literal, e.g. Java literal {@code "new\nline"} (compare with {@link #createValue(String)}). + * @return JSON string literal created from given text + */ + @NotNull + public JsonStringLiteral createStringLiteral(@NotNull String unescapedContent) { + return createValue('"' + StringUtil.escapeStringCharacters(unescapedContent) + '"'); + } + + @NotNull + public JsonProperty createProperty(@NotNull final String name, @NotNull final String value) { + final PsiFile file = createDummyFile("{\"" + name + "\": " + value + "}"); + // noinspection ConstantConditions + return ((JsonObject) file.getFirstChild()).getPropertyList().get(0); + } + + @NotNull + public PsiElement createComma() { + final JsonArray jsonArray1 = createValue("[1, 2]"); + return jsonArray1.getValueList().get(0).getNextSibling(); + } +} diff --git a/json/src/com/intellij/json/psi/JsonFile.java b/json/src/com/intellij/json/psi/JsonFile.java new file mode 100644 index 00000000..25fb4829 --- /dev/null +++ b/json/src/com/intellij/json/psi/JsonFile.java @@ -0,0 +1,23 @@ +package com.intellij.json.psi; + +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public interface JsonFile extends JsonElement, PsiFile { + /** + * Returns {@link JsonArray} or {@link JsonObject} value according to JSON standard. + * + * @return top-level JSON element if any or {@code null} otherwise + */ + @Nullable + JsonValue getTopLevelValue(); + + @NotNull + List<JsonValue> getAllTopLevelValues(); +} diff --git a/json/src/com/intellij/json/psi/JsonParserUtil.java b/json/src/com/intellij/json/psi/JsonParserUtil.java new file mode 100644 index 00000000..d0f5f467 --- /dev/null +++ b/json/src/com/intellij/json/psi/JsonParserUtil.java @@ -0,0 +1,25 @@ +package com.intellij.json.psi; + +import com.intellij.json.JsonElementTypes; +import com.intellij.lang.PsiBuilder; +import com.intellij.lang.parser.GeneratedParserUtilBase; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonParserUtil extends GeneratedParserUtilBase { + + public static boolean notTrailingComma(@NotNull PsiBuilder builder, int level) { + if (builder.getTokenType() != JsonElementTypes.COMMA) { + return false; + } + final IElementType afterComma = builder.lookAhead(1); + if (afterComma == JsonElementTypes.R_BRACKET || afterComma == JsonElementTypes.R_CURLY) { + builder.error("trailing comma"); + } + builder.advanceLexer(); + return true; + } +} diff --git a/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java b/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java new file mode 100644 index 00000000..4c4a9385 --- /dev/null +++ b/json/src/com/intellij/json/psi/JsonPsiChangeUtils.java @@ -0,0 +1,42 @@ +package com.intellij.json.psi; + +import com.intellij.json.JsonElementTypes; +import com.intellij.lang.ASTNode; +import com.intellij.psi.TokenType; + +public class JsonPsiChangeUtils { + public static void removeCommaSeparatedFromList(final ASTNode myNode, final ASTNode parent) { + ASTNode from = myNode, to = myNode.getTreeNext(); + + boolean seenComma = false; + + ASTNode toCandidate = to; + while (toCandidate != null && toCandidate.getElementType() == TokenType.WHITE_SPACE) { + toCandidate = toCandidate.getTreeNext(); + } + + if (toCandidate != null && toCandidate.getElementType() == JsonElementTypes.COMMA) { + toCandidate = toCandidate.getTreeNext(); + to = toCandidate; + seenComma = true; + + if (to != null && to.getElementType() == TokenType.WHITE_SPACE) { + to = to.getTreeNext(); + } + } + + if (!seenComma) { + ASTNode treePrev = from.getTreePrev(); + + while (treePrev != null && treePrev.getElementType() == TokenType.WHITE_SPACE) { + from = treePrev; + treePrev = treePrev.getTreePrev(); + } + if (treePrev != null && treePrev.getElementType() == JsonElementTypes.COMMA) { + from = treePrev; + } + } + + parent.removeRange(from, to); + } +} diff --git a/json/src/com/intellij/json/psi/JsonPsiUtil.java b/json/src/com/intellij/json/psi/JsonPsiUtil.java new file mode 100644 index 00000000..78cf5b34 --- /dev/null +++ b/json/src/com/intellij/json/psi/JsonPsiUtil.java @@ -0,0 +1,231 @@ +package com.intellij.json.psi; + +import com.intellij.json.JsonElementTypes; +import com.intellij.lang.ASTNode; +import com.intellij.lang.injection.InjectedLanguageManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.TokenType; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.tree.TokenSet; +import com.intellij.util.ObjectUtils; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static com.intellij.json.JsonParserDefinition.JSON_COMMENTARIES; + +/** + * Various helper methods for working with PSI of JSON language. + * + * @author Mikhail Golubev + */ +@SuppressWarnings("UnusedDeclaration") +public class JsonPsiUtil { + private JsonPsiUtil() { + // empty + } + + + /** + * Checks that PSI element represents item of JSON array. + * + * @param element PSI element to check + * @return whether this PSI element is array element + */ + public static boolean isArrayElement(@NotNull PsiElement element) { + return element instanceof JsonValue && element.getParent() instanceof JsonArray; + } + + /** + * Checks that PSI element represents key of JSON property (key-value pair of JSON object) + * + * @param element PSI element to check + * @return whether this PSI element is property key + */ + public static boolean isPropertyKey(@NotNull PsiElement element) { + final PsiElement parent = element.getParent(); + return parent instanceof JsonProperty && element == ((JsonProperty)parent).getNameElement(); + } + + /** + * Checks that PSI element represents value of JSON property (key-value pair of JSON object) + * + * @param element PSI element to check + * @return whether this PSI element is property value + */ + public static boolean isPropertyValue(@NotNull PsiElement element) { + final PsiElement parent = element.getParent(); + return parent instanceof JsonProperty && element == ((JsonProperty)parent).getValue(); + } + + /** + * Find the furthest sibling element with the same type as given anchor. + * <p/> + * Ignore white spaces for any type of element except {@link com.intellij.json.JsonElementTypes#LINE_COMMENT} + * where non indentation white space (that has new line in the middle) will stop the search. + * + * @param anchor element to start from + * @param after whether to scan through sibling elements forward or backward + * @return described element or anchor if search stops immediately + */ + @NotNull + public static PsiElement findFurthestSiblingOfSameType(@NotNull PsiElement anchor, boolean after) { + ASTNode node = anchor.getNode(); + // Compare by node type to distinguish between different types of comments + final IElementType expectedType = node.getElementType(); + ASTNode lastSeen = node; + while (node != null) { + final IElementType elementType = node.getElementType(); + if (elementType == expectedType) { + lastSeen = node; + } + else if (elementType == TokenType.WHITE_SPACE) { + if (expectedType == JsonElementTypes.LINE_COMMENT && node.getText().indexOf('\n', 1) != -1) { + break; + } + } + else if (!JSON_COMMENTARIES.contains(elementType) || JSON_COMMENTARIES.contains(expectedType)) { + break; + } + node = after ? node.getTreeNext() : node.getTreePrev(); + } + return lastSeen.getPsi(); + } + + /** + * Check that element type of the given AST node belongs to the token set. + * <p/> + * It slightly less verbose than {@code set.contains(node.getElementType())} and overloaded methods with the same name + * allow check ASTNode/PsiElement against both concrete element types and token sets in uniform way. + */ + public static boolean hasElementType(@NotNull ASTNode node, @NotNull TokenSet set) { + return set.contains(node.getElementType()); + } + + /** + * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.TokenSet) + */ + public static boolean hasElementType(@NotNull ASTNode node, IElementType... types) { + return hasElementType(node, TokenSet.create(types)); + } + + /** + * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.TokenSet) + */ + public static boolean hasElementType(@NotNull PsiElement element, @NotNull TokenSet set) { + return element.getNode() != null && hasElementType(element.getNode(), set); + } + + /** + * @see #hasElementType(com.intellij.lang.ASTNode, com.intellij.psi.tree.IElementType...) + */ + public static boolean hasElementType(@NotNull PsiElement element, IElementType... types) { + return element.getNode() != null && hasElementType(element.getNode(), types); + } + + /** + * Returns text of the given PSI element. Unlike obvious {@link PsiElement#getText()} this method unescapes text of the element if latter + * belongs to injected code fragment using {@link InjectedLanguageManager#getUnescapedText(PsiElement)}. + * + * @param element PSI element which text is needed + * @return text of the element with any host escaping removed + */ + @NotNull + public static String getElementTextWithoutHostEscaping(@NotNull PsiElement element) { + final InjectedLanguageManager manager = InjectedLanguageManager.getInstance(element.getProject()); + if (manager.isInjectedFragment(element.getContainingFile())) { + return manager.getUnescapedText(element); + } + else { + return element.getText(); + } + } + + /** + * Returns content of the string literal (without escaping) striving to preserve as much of user data as possible. + * <ul> + * <li>If literal length is greater than one and it starts and ends with the same quote and the last quote is not escaped, returns + * text without first and last characters.</li> + * <li>Otherwise if literal still begins with a quote, returns text without first character only.</li> + * <li>Returns unmodified text in all other cases.</li> + * </ul> + * + * @param text presumably result of {@link JsonStringLiteral#getText()} + * @return + */ + @NotNull + public static String stripQuotes(@NotNull String text) { + if (text.length() > 0) { + final char firstChar = text.charAt(0); + final char lastChar = text.charAt(text.length() - 1); + if (firstChar == '\'' || firstChar == '"') { + if (text.length() > 1 && firstChar == lastChar && !isEscapedChar(text, text.length() - 1)) { + return text.substring(1, text.length() - 1); + } + return text.substring(1); + } + } + return text; + } + + /** + * Checks that character in given position is escaped with backslashes. + * + * @param text text character belongs to + * @param position position of the character + * @return whether character at given position is escaped, i.e. preceded by odd number of backslashes + */ + public static boolean isEscapedChar(@NotNull String text, int position) { + int count = 0; + for (int i = position - 1; i >= 0 && text.charAt(i) == '\\'; i--) { + count++; + } + return count % 2 != 0; + } + + /** + * Add new property and necessary comma either at the beginning of the object literal or at its end. + * + * @param object object literal + * @param property new property, probably created via {@link JsonElementGenerator} + * @param first if true make new property first in the object, otherwise append in the end of property list + * @return property as returned by {@link PsiElement#addAfter(PsiElement, PsiElement)} + */ + @NotNull + public static PsiElement addProperty(@NotNull JsonObject object, @NotNull JsonProperty property, boolean first) { + final List<JsonProperty> propertyList = object.getPropertyList(); + if (!first) { + final JsonProperty lastProperty = ContainerUtil.getLastItem(propertyList); + if (lastProperty != null) { + final PsiElement addedProperty = object.addAfter(property, lastProperty); + object.addBefore(new JsonElementGenerator(object.getProject()).createComma(), addedProperty); + return addedProperty; + } + } + final PsiElement leftBrace = object.getFirstChild(); + assert hasElementType(leftBrace, JsonElementTypes.L_CURLY); + final PsiElement addedProperty = object.addAfter(property, leftBrace); + if (!propertyList.isEmpty()) { + object.addAfter(new JsonElementGenerator(object.getProject()).createComma(), addedProperty); + } + return addedProperty; + } + + @NotNull + public static Set<String> getOtherSiblingPropertyNames(@Nullable JsonProperty property) { + if (property == null) return Collections.emptySet(); + JsonObject object = ObjectUtils.tryCast(property.getParent(), JsonObject.class); + if (object == null) return Collections.emptySet(); + Set<String> result = ContainerUtil.newHashSet(); + for (JsonProperty jsonProperty : object.getPropertyList()) { + if (jsonProperty != property) { + result.add(jsonProperty.getName()); + } + } + return result; + } +} diff --git a/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java b/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java new file mode 100644 index 00000000..855655f7 --- /dev/null +++ b/json/src/com/intellij/json/psi/JsonStringLiteralManipulator.java @@ -0,0 +1,32 @@ +package com.intellij.json.psi; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.AbstractElementManipulator; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; + +public class JsonStringLiteralManipulator extends AbstractElementManipulator<JsonStringLiteral> { + + @Override + public JsonStringLiteral handleContentChange(@NotNull JsonStringLiteral element, @NotNull TextRange range, String newContent) + throws IncorrectOperationException { + assert new TextRange(0, element.getTextLength()).contains(range); + + final String originalContent = element.getText(); + final TextRange withoutQuotes = getRangeInElement(element); + final JsonElementGenerator generator = new JsonElementGenerator(element.getProject()); + final String replacement = originalContent.substring(withoutQuotes.getStartOffset(), range.getStartOffset()) + + newContent + + originalContent.substring(range.getEndOffset(), withoutQuotes.getEndOffset()); + return (JsonStringLiteral)element.replace(generator.createStringLiteral(replacement)); + } + + @NotNull + @Override + public TextRange getRangeInElement(@NotNull JsonStringLiteral element) { + final String content = element.getText(); + final int startOffset = content.startsWith("'") || content.startsWith("\"") ? 1 : 0; + final int endOffset = content.length() > 1 && (content.endsWith("'") || content.endsWith("\"")) ? -1 : 0; + return new TextRange(startOffset, content.length() + endOffset); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java b/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java new file mode 100644 index 00000000..1f7b7290 --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JSStringLiteralEscaper.java @@ -0,0 +1,194 @@ +package com.intellij.json.psi.impl; + +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.LiteralTextEscaper; +import com.intellij.psi.PsiLanguageInjectionHost; +import org.jetbrains.annotations.NotNull; + +public abstract class JSStringLiteralEscaper<T extends PsiLanguageInjectionHost> extends LiteralTextEscaper<T> { + private int[] outSourceOffsets; + + public JSStringLiteralEscaper(T host) { + super(host); + } + + @Override + public boolean decode(@NotNull final TextRange rangeInsideHost, @NotNull StringBuilder outChars) { + String subText = rangeInsideHost.substring(myHost.getText()); + + Ref<int[]> sourceOffsetsRef = new Ref<>(); + boolean result = parseStringCharacters(subText, outChars, sourceOffsetsRef, isRegExpLiteral(), !isOneLine()); + outSourceOffsets = sourceOffsetsRef.get(); + return result; + } + + protected abstract boolean isRegExpLiteral(); + + @Override + public int getOffsetInHost(int offsetInDecoded, @NotNull final TextRange rangeInsideHost) { + int result = offsetInDecoded < outSourceOffsets.length ? outSourceOffsets[offsetInDecoded] : -1; + if (result == -1) return -1; + return (result <= rangeInsideHost.getLength() ? result : rangeInsideHost.getLength()) + rangeInsideHost.getStartOffset(); + } + + @Override + public boolean isOneLine() { + return true; + } + + public static boolean parseStringCharacters(String chars, StringBuilder outChars, Ref<int[]> sourceOffsetsRef, boolean regExp, boolean escapeBacktick) { + int[] sourceOffsets = new int[chars.length() + 1]; + sourceOffsetsRef.set(sourceOffsets); + + if (chars.indexOf('\\') < 0) { + outChars.append(chars); + for (int i = 0; i < sourceOffsets.length; i++) { + sourceOffsets[i] = i; + } + return true; + } + + int index = 0; + final int outOffset = outChars.length(); + while (index < chars.length()) { + char c = chars.charAt(index++); + + sourceOffsets[outChars.length() - outOffset] = index - 1; + sourceOffsets[outChars.length() + 1 - outOffset] = index; + + if (c != '\\') { + outChars.append(c); + continue; + } + if (index == chars.length()) return false; + c = chars.charAt(index++); + if (escapeBacktick && c == '`') { + outChars.append(c); + } + else if (regExp) { + if (c != '/') { + outChars.append('\\'); + } + outChars.append(c); + } + else { + switch (c) { + case 'b': + outChars.append('\b'); + break; + + case 't': + outChars.append('\t'); + break; + + case 'n': + outChars.append('\n'); + break; + + case 'f': + outChars.append('\f'); + break; + + case 'r': + outChars.append('\r'); + break; + + case '"': + outChars.append('"'); + break; + + case '/': + outChars.append('/'); + break; + + case '\n': + outChars.append('\n'); + break; + case '\'': + outChars.append('\''); + break; + + case '\\': + outChars.append('\\'); + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': { + char startC = c; + int v = (int)c - '0'; + if (index < chars.length()) { + c = chars.charAt(index++); + if ('0' <= c && c <= '7') { + v <<= 3; + v += c - '0'; + if (startC <= '3' && index < chars.length()) { + c = chars.charAt(index++); + if ('0' <= c && c <= '7') { + v <<= 3; + v += c - '0'; + } + else { + index--; + } + } + } + else { + index--; + } + } + outChars.append((char)v); + } + break; + case 'x': + if (index + 2 <= chars.length()) { + try { + int v = Integer.parseInt(chars.substring(index, index + 2), 16); + outChars.append((char)v); + index += 2; + } + catch (Exception e) { + return false; + } + } + else { + return false; + } + break; + case 'u': + if (index + 4 <= chars.length()) { + try { + int v = Integer.parseInt(chars.substring(index, index + 4), 16); + //line separators are invalid here + if (v == 0x000a || v == 0x000d) return false; + c = chars.charAt(index); + if (c == '+' || c == '-') return false; + outChars.append((char)v); + index += 4; + } + catch (Exception e) { + return false; + } + } + else { + return false; + } + break; + + default: + outChars.append(c); + break; + } + } + + sourceOffsets[outChars.length() - outOffset] = index; + } + return true; + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonElementImpl.java b/json/src/com/intellij/json/psi/impl/JsonElementImpl.java new file mode 100644 index 00000000..e65b4b68 --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonElementImpl.java @@ -0,0 +1,23 @@ +package com.intellij.json.psi.impl; + +import com.intellij.extapi.psi.ASTWrapperPsiElement; +import com.intellij.json.psi.JsonElement; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonElementImpl extends ASTWrapperPsiElement implements JsonElement { + + public JsonElementImpl(@NotNull ASTNode node) { + super(node); + } + + @Override + public String toString() { + final String className = getClass().getSimpleName(); + return StringUtil.trimEnd(className, "Impl"); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonFileImpl.java b/json/src/com/intellij/json/psi/impl/JsonFileImpl.java new file mode 100644 index 00000000..8ddcdd2c --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonFileImpl.java @@ -0,0 +1,43 @@ +package com.intellij.json.psi.impl; + +import com.intellij.extapi.psi.PsiFileBase; +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonValue; +import com.intellij.lang.Language; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class JsonFileImpl extends PsiFileBase implements JsonFile { + + public JsonFileImpl(FileViewProvider fileViewProvider, Language language) { + super(fileViewProvider, language); + } + + @NotNull + @Override + public FileType getFileType() { + return getViewProvider().getFileType(); + } + + @Nullable + @Override + public JsonValue getTopLevelValue() { + return PsiTreeUtil.getChildOfType(this, JsonValue.class); + } + + @NotNull + @Override + public List<JsonValue> getAllTopLevelValues() { + return PsiTreeUtil.getChildrenOfTypeAsList(this, JsonValue.class); + } + + @Override + public String toString() { + return "JsonFile: " + getName(); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java b/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java new file mode 100644 index 00000000..3ada2002 --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonLiteralMixin.java @@ -0,0 +1,22 @@ +/* + * 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 com.intellij.json.psi.impl; + +import com.intellij.json.psi.JsonLiteral; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry; +import org.jetbrains.annotations.NotNull; + +abstract class JsonLiteralMixin extends JsonElementImpl implements JsonLiteral { + protected JsonLiteralMixin(ASTNode node) { + super(node); + } + + @NotNull + @Override + public PsiReference[] getReferences() { + return ReferenceProvidersRegistry.getReferencesFromProviders(this); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java b/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java new file mode 100644 index 00000000..91c5c3a8 --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonObjectMixin.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2015 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.intellij.json.psi.impl; + +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.lang.ASTNode; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Mikhail Golubev + */ +public abstract class JsonObjectMixin extends JsonContainerImpl implements JsonObject { + private final CachedValueProvider<Map<String, JsonProperty>> myPropertyCache = + () -> { + final Map<String, JsonProperty> cache = new HashMap<>(); + for (JsonProperty property : getPropertyList()) { + final String propertyName = property.getName(); + // Preserve the old behavior - return the first value in findProperty() + if (!cache.containsKey(propertyName)) { + cache.put(propertyName, property); + } + } + // Cached value is invalidated every time file containing this object is modified + return CachedValueProvider.Result.createSingleDependency(cache, this); + }; + + public JsonObjectMixin(@NotNull ASTNode node) { + super(node); + } + + @Nullable + @Override + public JsonProperty findProperty(@NotNull String name) { + return CachedValuesManager.getCachedValue(this, myPropertyCache).get(name); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java b/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java new file mode 100644 index 00000000..3fc29aef --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonPropertyMixin.java @@ -0,0 +1,42 @@ +package com.intellij.json.psi.impl; + +import com.intellij.json.psi.JsonElementGenerator; +import com.intellij.json.psi.JsonProperty; +import com.intellij.lang.ASTNode; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry; +import com.intellij.util.ArrayUtil; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +abstract class JsonPropertyMixin extends JsonElementImpl implements JsonProperty { + JsonPropertyMixin(@NotNull ASTNode node) { + super(node); + } + + @Override + public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException { + final JsonElementGenerator generator = new JsonElementGenerator(getProject()); + // Strip only both quotes in case user wants some exotic name like key' + getNameElement().replace(generator.createStringLiteral(StringUtil.unquoteString(name))); + return this; + } + + @Override + public PsiReference getReference() { + return new JsonPropertyNameReference(this); + } + + @NotNull + @Override + public PsiReference[] getReferences() { + final PsiReference[] fromProviders = ReferenceProvidersRegistry.getReferencesFromProviders(this); + return ArrayUtil.prepend(new JsonPropertyNameReference(this), fromProviders); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java b/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java new file mode 100644 index 00000000..5b51dcb9 --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonPropertyNameReference.java @@ -0,0 +1,74 @@ +package com.intellij.json.psi.impl; + +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonValue; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.ElementManipulators; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class JsonPropertyNameReference implements PsiReference { + private final JsonProperty myProperty; + + public JsonPropertyNameReference(@NotNull JsonProperty property) { + myProperty = property; + } + + @NotNull + @Override + public PsiElement getElement() { + return myProperty; + } + + @NotNull + @Override + public TextRange getRangeInElement() { + final JsonValue nameElement = myProperty.getNameElement(); + // Either value of string with quotes stripped or element's text as is + return ElementManipulators.getValueTextRange(nameElement); + } + + @Nullable + @Override + public PsiElement resolve() { + return myProperty; + } + + @NotNull + @Override + public String getCanonicalText() { + return myProperty.getName(); + } + + @Override + public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException { + return myProperty.setName(newElementName); + } + + @Override + public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { + return null; + } + + @Override + public boolean isReferenceTo(@NotNull PsiElement element) { + if (!(element instanceof JsonProperty)) { + return false; + } + // May reference to the property with the same name for compatibility with JavaScript JSON support + final JsonProperty otherProperty = (JsonProperty)element; + final PsiElement selfResolve = resolve(); + return otherProperty.getName().equals(getCanonicalText()) && selfResolve != otherProperty; + } + + @Override + public boolean isSoft() { + return true; + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java b/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java new file mode 100644 index 00000000..126e3f7d --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonPsiImplUtils.java @@ -0,0 +1,233 @@ +package com.intellij.json.psi.impl; + +import com.intellij.icons.AllIcons; +import com.intellij.json.JsonBundle; +import com.intellij.json.JsonDialectUtil; +import com.intellij.json.JsonLanguage; +import com.intellij.json.JsonParserDefinition; +import com.intellij.json.codeinsight.JsonStandardComplianceInspection; +import com.intellij.json.psi.*; +import com.intellij.lang.ASTNode; +import com.intellij.lang.Language; +import com.intellij.navigation.ItemPresentation; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; +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.intellij.util.PlatformIcons; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class JsonPsiImplUtils { + static final Key<List<Pair<TextRange, String>>> STRING_FRAGMENTS = new Key<>("JSON string fragments"); + + @NotNull + public static String getName(@NotNull JsonProperty property) { + return StringUtil.unescapeStringCharacters(JsonPsiUtil.stripQuotes(property.getNameElement().getText())); + } + + /** + * Actually only JSON string literal should be accepted as valid name of property according to standard, + * but for compatibility with JavaScript integration any JSON literals as well as identifiers (unquoted words) + * are possible and highlighted as error later. + * + * @see JsonStandardComplianceInspection + */ + @NotNull + public static JsonValue getNameElement(@NotNull JsonProperty property) { + final PsiElement firstChild = property.getFirstChild(); + assert firstChild instanceof JsonLiteral || firstChild instanceof JsonReferenceExpression; + return (JsonValue)firstChild; + } + + @Nullable + public static JsonValue getValue(@NotNull JsonProperty property) { + return PsiTreeUtil.getNextSiblingOfType(getNameElement(property), JsonValue.class); + } + + public static boolean isQuotedString(@NotNull JsonLiteral literal) { + return literal.getNode().findChildByType(JsonParserDefinition.STRING_LITERALS) != null; + } + + @Nullable + public static ItemPresentation getPresentation(@NotNull final JsonProperty property) { + return new ItemPresentation() { + @Nullable + @Override + public String getPresentableText() { + return property.getName(); + } + + @Nullable + @Override + public String getLocationString() { + final JsonValue value = property.getValue(); + return value instanceof JsonLiteral ? value.getText() : null; + } + + @Nullable + @Override + public Icon getIcon(boolean unused) { + if (property.getValue() instanceof JsonArray) { + return AllIcons.Json.Property_brackets; + } + if (property.getValue() instanceof JsonObject) { + return AllIcons.Json.Property_braces; + } + return PlatformIcons.PROPERTY_ICON; + } + }; + } + + @Nullable + public static ItemPresentation getPresentation(@NotNull final JsonArray array) { + return new ItemPresentation() { + @Nullable + @Override + public String getPresentableText() { + return JsonBundle.message("json.array"); + } + + @Nullable + @Override + public String getLocationString() { + return null; + } + + @Nullable + @Override + public Icon getIcon(boolean unused) { + return AllIcons.Json.Array; + } + }; + } + + @Nullable + public static ItemPresentation getPresentation(@NotNull final JsonObject object) { + return new ItemPresentation() { + @Nullable + @Override + public String getPresentableText() { + return JsonBundle.message("json.object"); + } + + @Nullable + @Override + public String getLocationString() { + return null; + } + + @Nullable + @Override + public Icon getIcon(boolean unused) { + return AllIcons.Json.Object; + } + }; + } + + private static final String ourEscapesTable = "\"\"\\\\//b\bf\fn\nr\rt\t"; + + @NotNull + public static List<Pair<TextRange, String>> getTextFragments(@NotNull JsonStringLiteral literal) { + List<Pair<TextRange, String>> result = literal.getUserData(STRING_FRAGMENTS); + if (result == null) { + result = new ArrayList<>(); + final String text = literal.getText(); + final int length = text.length(); + int pos = 1, unescapedSequenceStart = 1; + while (pos < length) { + if (text.charAt(pos) == '\\') { + if (unescapedSequenceStart != pos) { + result.add(Pair.create(new TextRange(unescapedSequenceStart, pos), text.substring(unescapedSequenceStart, pos))); + } + if (pos == length - 1) { + result.add(Pair.create(new TextRange(pos, pos + 1), "\\")); + break; + } + final char next = text.charAt(pos + 1); + switch (next) { + case '"': + case '\\': + case '/': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + final int idx = ourEscapesTable.indexOf(next); + result.add(Pair.create(new TextRange(pos, pos + 2), ourEscapesTable.substring(idx + 1, idx + 2))); + pos += 2; + break; + case 'u': + int i = pos + 2; + for (; i < pos + 6; i++) { + if (i == length || !StringUtil.isHexDigit(text.charAt(i))) { + break; + } + } + result.add(Pair.create(new TextRange(pos, i), text.substring(pos, i))); + pos = i; + break; + case 'x': + Language language = JsonDialectUtil.getLanguage(literal); + if (language instanceof JsonLanguage && ((JsonLanguage)language).hasPermissiveStrings()) { + int i2 = pos + 2; + for (; i2 < pos + 4; i2++) { + if (i2 == length || !StringUtil.isHexDigit(text.charAt(i2))) { + break; + } + } + result.add(Pair.create(new TextRange(pos, i2), text.substring(pos, i2))); + pos = i2; + break; + } + default: + result.add(Pair.create(new TextRange(pos, pos + 2), text.substring(pos, pos + 2))); + pos += 2; + } + unescapedSequenceStart = pos; + } + else { + pos++; + } + } + final int contentEnd = text.charAt(0) == text.charAt(length - 1) ? length - 1 : length; + if (unescapedSequenceStart < contentEnd) { + result.add(Pair.create(new TextRange(unescapedSequenceStart, contentEnd), text.substring(unescapedSequenceStart, contentEnd))); + } + result = Collections.unmodifiableList(result); + literal.putUserData(STRING_FRAGMENTS, result); + } + return result; + } + + public static void delete(@NotNull JsonProperty property) { + final ASTNode myNode = property.getNode(); + JsonPsiChangeUtils.removeCommaSeparatedFromList(myNode, myNode.getTreeParent()); + } + + @NotNull + public static String getValue(@NotNull JsonStringLiteral literal) { + return StringUtil.unescapeStringCharacters(JsonPsiUtil.stripQuotes(literal.getText())); + } + + public static boolean isPropertyName(@NotNull JsonStringLiteral literal) { + final PsiElement parent = literal.getParent(); + return parent instanceof JsonProperty && ((JsonProperty)parent).getNameElement() == literal; + } + + public static boolean getValue(@NotNull JsonBooleanLiteral literal) { + return literal.textMatches("true"); + } + + public static double getValue(@NotNull JsonNumberLiteral literal) { + return Double.parseDouble(literal.getText()); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java b/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java new file mode 100644 index 00000000..3c0eb0e4 --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonRecursiveElementVisitor.java @@ -0,0 +1,17 @@ +// 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 com.intellij.json.psi.impl; + +import com.intellij.json.psi.JsonElementVisitor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiRecursiveVisitor; + +/** + * @author Mikhail Golubev + */ +public class JsonRecursiveElementVisitor extends JsonElementVisitor implements PsiRecursiveVisitor { + + @Override + public void visitElement(final PsiElement element) { + element.acceptChildren(this); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java b/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java new file mode 100644 index 00000000..99baebef --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonStringLiteralMixin.java @@ -0,0 +1,45 @@ +package com.intellij.json.psi.impl; + +import com.intellij.lang.ASTNode; +import com.intellij.psi.LiteralTextEscaper; +import com.intellij.psi.PsiLanguageInjectionHost; +import com.intellij.psi.impl.source.tree.LeafElement; +import org.jetbrains.annotations.NotNull; + +/** + * @author Konstantin.Ulitin + */ +public abstract class JsonStringLiteralMixin extends JsonLiteralImpl implements PsiLanguageInjectionHost { + protected JsonStringLiteralMixin(ASTNode node) { + super(node); + } + + @Override + public boolean isValidHost() { + return true; + } + + @Override + public PsiLanguageInjectionHost updateText(@NotNull String text) { + ASTNode valueNode = getNode().getFirstChildNode(); + assert valueNode instanceof LeafElement; + ((LeafElement)valueNode).replaceWithText(text); + return this; + } + + @NotNull + @Override + public LiteralTextEscaper<? extends PsiLanguageInjectionHost> createLiteralTextEscaper() { + return new JSStringLiteralEscaper<PsiLanguageInjectionHost>(this) { + @Override + protected boolean isRegExpLiteral() { + return false; + } + }; + } + + @Override + public void subtreeChanged() { + putUserData(JsonPsiImplUtils.STRING_FRAGMENTS, null); + } +} diff --git a/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java b/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java new file mode 100644 index 00000000..0b54775e --- /dev/null +++ b/json/src/com/intellij/json/psi/impl/JsonTreeChangePreprocessor.java @@ -0,0 +1,40 @@ +/* + * 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.intellij.json.psi.impl; + +import com.intellij.json.JsonLanguage; +import com.intellij.json.psi.JsonFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiManager; +import com.intellij.psi.impl.PsiTreeChangeEventImpl; +import com.intellij.psi.impl.PsiTreeChangePreprocessorBase; +import org.jetbrains.annotations.NotNull; + +public class JsonTreeChangePreprocessor extends PsiTreeChangePreprocessorBase { + public JsonTreeChangePreprocessor(@NotNull PsiManager psiManager) { + super(psiManager); + } + + @Override + protected boolean acceptsEvent(@NotNull PsiTreeChangeEventImpl event) { + return event.getFile() instanceof JsonFile; + } + + @Override + protected boolean isOutOfCodeBlock(@NotNull PsiElement element) { + return element.getLanguage() instanceof JsonLanguage; + } +}
\ No newline at end of file diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java b/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java new file mode 100644 index 00000000..9a626219 --- /dev/null +++ b/json/src/com/intellij/json/structureView/JsonStructureViewBuilderFactory.java @@ -0,0 +1,32 @@ +package com.intellij.json.structureView; + +import com.intellij.ide.structureView.StructureViewBuilder; +import com.intellij.ide.structureView.StructureViewModel; +import com.intellij.ide.structureView.TreeBasedStructureViewBuilder; +import com.intellij.json.psi.JsonFile; +import com.intellij.lang.PsiStructureViewFactory; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class JsonStructureViewBuilderFactory implements PsiStructureViewFactory { + @Nullable + @Override + public StructureViewBuilder getStructureViewBuilder(@NotNull final PsiFile psiFile) { + if (!(psiFile instanceof JsonFile)) { + return null; + } + + return new TreeBasedStructureViewBuilder() { + @NotNull + @Override + public StructureViewModel createStructureViewModel(@Nullable Editor editor) { + return new JsonStructureViewModel(psiFile, editor); + } + }; + } +} diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewElement.java b/json/src/com/intellij/json/structureView/JsonStructureViewElement.java new file mode 100644 index 00000000..df705efb --- /dev/null +++ b/json/src/com/intellij/json/structureView/JsonStructureViewElement.java @@ -0,0 +1,86 @@ +package com.intellij.json.structureView; + +import com.intellij.ide.structureView.StructureViewTreeElement; +import com.intellij.ide.util.treeView.smartTree.TreeElement; +import com.intellij.json.psi.*; +import com.intellij.navigation.ItemPresentation; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ArrayUtil; +import com.intellij.util.Function; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JsonStructureViewElement implements StructureViewTreeElement { + private final JsonElement myElement; + + public JsonStructureViewElement(@NotNull JsonElement element) { + assert PsiTreeUtil.instanceOf(element, JsonFile.class, JsonProperty.class, JsonObject.class, JsonArray.class); + myElement = element; + } + + @Override + public JsonElement getValue() { + return myElement; + } + + @Override + public void navigate(boolean requestFocus) { + myElement.navigate(requestFocus); + } + + @Override + public boolean canNavigate() { + return myElement.canNavigate(); + } + + @Override + public boolean canNavigateToSource() { + return myElement.canNavigateToSource(); + } + + @NotNull + @Override + public ItemPresentation getPresentation() { + final ItemPresentation presentation = myElement.getPresentation(); + assert presentation != null; + return presentation; + } + + @NotNull + @Override + public TreeElement[] getChildren() { + JsonElement value = null; + if (myElement instanceof JsonFile) { + value = ((JsonFile)myElement).getTopLevelValue(); + } + else if (myElement instanceof JsonProperty) { + value = ((JsonProperty)myElement).getValue(); + } + else if (PsiTreeUtil.instanceOf(myElement, JsonObject.class, JsonArray.class)) { + value = myElement; + } + if (value instanceof JsonObject) { + final JsonObject object = ((JsonObject)value); + return ContainerUtil.map2Array(object.getPropertyList(), TreeElement.class, (Function<JsonProperty, TreeElement>)property -> new JsonStructureViewElement(property)); + } + else if (value instanceof JsonArray) { + final JsonArray array = (JsonArray)value; + final List<TreeElement> childObjects = ContainerUtil.mapNotNull(array.getValueList(), value1 -> { + if (value1 instanceof JsonObject && !((JsonObject)value1).getPropertyList().isEmpty()) { + return new JsonStructureViewElement(value1); + } + else if (value1 instanceof JsonArray && PsiTreeUtil.findChildOfType(value1, JsonProperty.class) != null) { + return new JsonStructureViewElement(value1); + } + return null; + }); + return ArrayUtil.toObjectArray(childObjects, TreeElement.class); + } + return EMPTY_ARRAY; + } +} diff --git a/json/src/com/intellij/json/structureView/JsonStructureViewModel.java b/json/src/com/intellij/json/structureView/JsonStructureViewModel.java new file mode 100644 index 00000000..a3bcf62b --- /dev/null +++ b/json/src/com/intellij/json/structureView/JsonStructureViewModel.java @@ -0,0 +1,37 @@ +package com.intellij.json.structureView; + +import com.intellij.ide.structureView.StructureViewModel; +import com.intellij.ide.structureView.StructureViewModelBase; +import com.intellij.ide.structureView.StructureViewTreeElement; +import com.intellij.ide.util.treeView.smartTree.Sorter; +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Mikhail Golubev + */ +public class JsonStructureViewModel extends StructureViewModelBase implements StructureViewModel.ElementInfoProvider { + + public JsonStructureViewModel(@NotNull PsiFile psiFile, @Nullable Editor editor) { + super(psiFile, editor, new JsonStructureViewElement((JsonFile)psiFile)); + withSuitableClasses(JsonFile.class, JsonProperty.class, JsonObject.class, JsonArray.class); + withSorters(Sorter.ALPHA_SORTER); + } + + @Override + public boolean isAlwaysShowsPlus(StructureViewTreeElement element) { + return false; + } + + @Override + public boolean isAlwaysLeaf(StructureViewTreeElement element) { + return false; + } + +} diff --git a/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java b/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java new file mode 100644 index 00000000..60f32e84 --- /dev/null +++ b/json/src/com/intellij/json/surroundWith/JsonSurroundDescriptor.java @@ -0,0 +1,84 @@ +package com.intellij.json.surroundWith; + +import com.intellij.json.JsonElementTypes; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonValue; +import com.intellij.lang.surroundWith.SurroundDescriptor; +import com.intellij.lang.surroundWith.Surrounder; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JsonSurroundDescriptor implements SurroundDescriptor { + private static final Surrounder[] ourSurrounders = new Surrounder[]{ + new JsonWithObjectLiteralSurrounder(), + new JsonWithArrayLiteralSurrounder(), + new JsonWithQuotesSurrounder() + }; + + @NotNull + @Override + public PsiElement[] getElementsToSurround(PsiFile file, int startOffset, int endOffset) { + PsiElement firstElement = file.findElementAt(startOffset); + PsiElement lastElement = file.findElementAt(endOffset - 1); + + // Extend selection beyond possible delimiters + while (firstElement != null && + (firstElement instanceof PsiWhiteSpace || firstElement.getNode().getElementType() == JsonElementTypes.COMMA)) { + firstElement = firstElement.getNextSibling(); + } + while (lastElement != null && + (lastElement instanceof PsiWhiteSpace || lastElement.getNode().getElementType() == JsonElementTypes.COMMA)) { + lastElement = lastElement.getPrevSibling(); + } + if (firstElement != null) { + startOffset = firstElement.getTextRange().getStartOffset(); + } + if (lastElement != null) { + endOffset = lastElement.getTextRange().getEndOffset(); + } + + final JsonProperty property = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, endOffset, JsonProperty.class); + if (property != null) { + return collectElements(endOffset, property, JsonProperty.class); + } + + final JsonValue value = PsiTreeUtil.findElementOfClassAtRange(file, startOffset, endOffset, JsonValue.class); + if (value != null) { + return collectElements(endOffset, value, JsonValue.class); + } + return PsiElement.EMPTY_ARRAY; + } + + @NotNull + private static <T extends PsiElement> PsiElement[] collectElements(int endOffset, @NotNull T property, @NotNull Class<T> kind) { + final List<T> properties = ContainerUtil.newArrayList(property); + PsiElement nextSibling = property.getNextSibling(); + while (nextSibling != null && nextSibling.getTextRange().getEndOffset() <= endOffset) { + if (kind.isInstance(nextSibling)) { + properties.add(kind.cast(nextSibling)); + } + nextSibling = nextSibling.getNextSibling(); + } + return properties.toArray(PsiElement.EMPTY_ARRAY); + } + + @NotNull + @Override + public Surrounder[] getSurrounders() { + return ourSurrounders; + } + + @Override + public boolean isExclusive() { + return false; + } +} diff --git a/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java b/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java new file mode 100644 index 00000000..f4321e71 --- /dev/null +++ b/json/src/com/intellij/json/surroundWith/JsonSurrounderBase.java @@ -0,0 +1,57 @@ +// 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 com.intellij.json.surroundWith; + +import com.intellij.json.psi.JsonElementGenerator; +import com.intellij.json.psi.JsonPsiUtil; +import com.intellij.json.psi.JsonValue; +import com.intellij.lang.surroundWith.Surrounder; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class JsonSurrounderBase implements Surrounder { + @Override + public boolean isApplicable(@NotNull PsiElement[] elements) { + return elements.length >= 1 && elements[0] instanceof JsonValue && !JsonPsiUtil.isPropertyKey(elements[0]); + } + + @Nullable + @Override + public TextRange surroundElements(@NotNull Project project, @NotNull Editor editor, @NotNull PsiElement[] elements) + throws IncorrectOperationException { + if (!isApplicable(elements)) { + return null; + } + + final JsonElementGenerator generator = new JsonElementGenerator(project); + + if (elements.length == 1) { + JsonValue replacement = generator.createValue(createReplacementText(elements[0].getText())); + elements[0].replace(replacement); + } + else { + final String propertiesText = getTextAndRemoveMisc(elements[0], elements[elements.length - 1]); + JsonValue replacement = generator.createValue(createReplacementText(propertiesText)); + elements[0].replace(replacement); + } + return null; + } + + @NotNull + protected static String getTextAndRemoveMisc(@NotNull PsiElement firstProperty, @NotNull PsiElement lastProperty) { + final TextRange replacedRange = new TextRange(firstProperty.getTextOffset(), lastProperty.getTextRange().getEndOffset()); + final String propertiesText = replacedRange.substring(firstProperty.getContainingFile().getText()); + if (firstProperty != lastProperty) { + final PsiElement parent = firstProperty.getParent(); + parent.deleteChildRange(firstProperty.getNextSibling(), lastProperty); + } + return propertiesText; + } + + @NotNull + protected abstract String createReplacementText(@NotNull String textInRange); +} diff --git a/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java new file mode 100644 index 00000000..7f6091f3 --- /dev/null +++ b/json/src/com/intellij/json/surroundWith/JsonWithArrayLiteralSurrounder.java @@ -0,0 +1,18 @@ +// 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 com.intellij.json.surroundWith; + +import com.intellij.json.JsonBundle; +import org.jetbrains.annotations.NotNull; + +public class JsonWithArrayLiteralSurrounder extends JsonSurrounderBase { + @Override + public String getTemplateDescription() { + return JsonBundle.message("surround.with.array.literal.desc"); + } + + @NotNull + @Override + protected String createReplacementText(@NotNull String firstElement) { + return "[" + firstElement + "]"; + } +} diff --git a/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java new file mode 100644 index 00000000..40182965 --- /dev/null +++ b/json/src/com/intellij/json/surroundWith/JsonWithObjectLiteralSurrounder.java @@ -0,0 +1,85 @@ +package com.intellij.json.surroundWith; + +import com.intellij.json.JsonBundle; +import com.intellij.json.psi.*; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * This surrounder ported from JavaScript allows to wrap single JSON value or several consecutive JSON properties + * in object literal. + * <p/> + * Examples: + * <ol> + * <li>{@code [42]} converts to {@code [{"property": 42}]}</li> + * <li><pre> + * { + * "foo": 42, + * "bar": false + * } + * </pre> converts to <pre> + * { + * "property": { + * "foo": 42, + * "bar": false + * } + * } + * </pre></li> + * </ol> + * + * @author Mikhail Golubev + */ +public class JsonWithObjectLiteralSurrounder extends JsonSurrounderBase { + @Override + public String getTemplateDescription() { + return JsonBundle.message("surround.with.object.literal.desc"); + } + + @Override + public boolean isApplicable(@NotNull PsiElement[] elements) { + return !JsonPsiUtil.isPropertyKey(elements[0]) && (elements[0] instanceof JsonProperty || elements.length == 1); + } + + @Nullable + @Override + public TextRange surroundElements(@NotNull Project project, + @NotNull Editor editor, + @NotNull PsiElement[] elements) throws IncorrectOperationException { + + if (!isApplicable(elements)) { + return null; + } + + final JsonElementGenerator generator = new JsonElementGenerator(project); + + final PsiElement firstElement = elements[0]; + final JsonElement newNameElement; + if (firstElement instanceof JsonValue) { + assert elements.length == 1 : "Only single JSON value can be wrapped in object literal"; + JsonObject replacement = generator.createValue(createReplacementText(firstElement.getText())); + replacement = (JsonObject)firstElement.replace(replacement); + newNameElement = replacement.getPropertyList().get(0).getNameElement(); + } + else { + assert firstElement instanceof JsonProperty; + final String propertiesText = getTextAndRemoveMisc(firstElement, elements[elements.length - 1]); + final JsonObject tempJsonObject = generator.createValue(createReplacementText("{\n" + propertiesText) + "\n}"); + JsonProperty replacement = tempJsonObject.getPropertyList().get(0); + replacement = (JsonProperty)firstElement.replace(replacement); + newNameElement = replacement.getNameElement(); + } + final TextRange rangeWithQuotes = newNameElement.getTextRange(); + return new TextRange(rangeWithQuotes.getStartOffset() + 1, rangeWithQuotes.getEndOffset() - 1); + } + + @NotNull + @Override + protected String createReplacementText(@NotNull String textInRange) { + return "{\n\"property\": " + textInRange + "\n}"; + } +} diff --git a/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java b/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java new file mode 100644 index 00000000..665090f6 --- /dev/null +++ b/json/src/com/intellij/json/surroundWith/JsonWithQuotesSurrounder.java @@ -0,0 +1,19 @@ +// 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 com.intellij.json.surroundWith; + +import com.intellij.json.JsonBundle; +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; + +public class JsonWithQuotesSurrounder extends JsonSurrounderBase { + @Override + public String getTemplateDescription() { + return JsonBundle.message("surround.with.quotes.desc"); + } + + @NotNull + @Override + protected String createReplacementText(@NotNull String firstElement) { + return "\"" + StringUtil.escapeStringCharacters(firstElement) + "\""; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java b/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java new file mode 100644 index 00000000..abcb943e --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonMappingKind.java @@ -0,0 +1,41 @@ +// 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 com.jetbrains.jsonSchema; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.util.text.StringUtil; + +import javax.swing.*; + +public enum JsonMappingKind { + File, + Pattern, + Directory; + + public String getDescription() { + switch (this) { + case File: + return "file"; + case Pattern: + return "file path pattern"; + case Directory: + return "directory"; + } + return ""; + } + + public String getPrefix() { + return StringUtil.capitalize(getDescription()) + ": "; + } + + public Icon getIcon() { + switch (this) { + case File: + return AllIcons.FileTypes.Any_type; + case Pattern: + return AllIcons.FileTypes.Unknown; + case Directory: + return AllIcons.Nodes.Folder; + } + return null; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java b/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java new file mode 100644 index 00000000..e2c87367 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonPointerResolver.java @@ -0,0 +1,60 @@ +// 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 com.jetbrains.jsonSchema; + +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonValue; +import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class JsonPointerResolver { + private final JsonValue myRoot; + private final String myPointer; + + public JsonPointerResolver(@NotNull JsonValue root, @NotNull String pointer) { + myRoot = root; + myPointer = pointer; + } + + @Nullable + public JsonValue resolve() { + JsonValue root = myRoot; + final List<JsonSchemaVariantsTreeBuilder.Step> steps = JsonSchemaVariantsTreeBuilder.buildSteps(myPointer); + for (JsonSchemaVariantsTreeBuilder.Step step : steps) { + String name = step.getName(); + if (name != null) { + if (!(root instanceof JsonObject)) return null; + JsonProperty property = ((JsonObject)root).findProperty(name); + root = property == null ? null : property.getValue(); + } + else { + int idx = step.getIdx(); + if (idx < 0) return null; + + if (!(root instanceof JsonArray)) { + if (root instanceof JsonObject) { + JsonProperty property = ((JsonObject)root).findProperty(String.valueOf(idx)); + if (property == null) { + return null; + } + root = property.getValue(); + continue; + } + else { + return null; + } + } + List<JsonValue> list = ((JsonArray)root).getValueList(); + if (idx >= list.size()) return null; + root = list.get(idx); + } + } + return root; + } + + +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java b/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java new file mode 100644 index 00000000..2cb1c37c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonPointerUtil.java @@ -0,0 +1,38 @@ +// 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 com.jetbrains.jsonSchema; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.io.URLUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class JsonPointerUtil { + @NotNull + public static String escapeForJsonPointer(@NotNull String name) { + if (StringUtil.isEmptyOrSpaces(name)) { + return URLUtil.encodeURIComponent(name); + } + return StringUtil.replace(StringUtil.replace(name, "~", "~0"), "/", "~1"); + } + + @NotNull + public static String unescapeJsonPointerPart(@NotNull String part) { + part = URLUtil.unescapePercentSequences(part); + return StringUtil.replace(StringUtil.replace(part, "~0", "~"), "~1", "/"); + } + + public static boolean isSelfReference(@Nullable String ref) { + return "#".equals(ref) || "#/".equals(ref) || StringUtil.isEmpty(ref); + } + + public static List<String> split(@NotNull String pointer) { + return StringUtil.split(pointer, "/", true, false); + } + + @NotNull + public static String normalizeSlashes(@NotNull String ref) { + return StringUtil.trimStart(ref.replace('\\', '/'), "/"); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java new file mode 100644 index 00000000..96b838fe --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogConfigurable.java @@ -0,0 +1,110 @@ +// 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 com.jetbrains.jsonSchema; + +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.panel.ComponentPanelBuilder; +import com.intellij.ui.components.JBCheckBox; +import com.intellij.ui.components.JBPanel; +import com.intellij.util.ui.FormBuilder; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +public class JsonSchemaCatalogConfigurable implements Configurable { + @NonNls public static final String SETTINGS_JSON_SCHEMA_CATALOG = "settings.json.schema.catalog"; + public static final String JSON_SCHEMA_CATALOG = "Remote JSON Schemas"; + @NotNull private final Project myProject; + private final JBCheckBox myCatalogCheckBox; + private final JBCheckBox myRemoteCheckBox; + private final JBCheckBox myPreferRemoteCheckBox; + + public JsonSchemaCatalogConfigurable(@NotNull final Project project) { + myProject = project; + myCatalogCheckBox = new JBCheckBox("Use schemastore.org JSON Schema catalog"); + myRemoteCheckBox = new JBCheckBox("Allow downloading JSON Schemas from remote sources"); + myPreferRemoteCheckBox = new JBCheckBox("Always download the most recent version of schemas"); + } + + @Nullable + @Override + public JComponent createComponent() { + FormBuilder builder = FormBuilder.createFormBuilder(); + + builder.addComponent(myRemoteCheckBox); + builder.addVerticalGap(2); + myRemoteCheckBox.addChangeListener(c -> { + boolean selected = myRemoteCheckBox.isSelected(); + myCatalogCheckBox.setEnabled(selected); + myPreferRemoteCheckBox.setEnabled(selected); + if (!selected) { + myCatalogCheckBox.setSelected(false); + myPreferRemoteCheckBox.setSelected(false); + } + }); + addWithComment(builder, myCatalogCheckBox, + "Schemas will be downloaded and assigned using the <a href=\"http://schemastore.org/json/\">SchemaStore API</a>"); + addWithComment(builder, myPreferRemoteCheckBox, + "Schemas will always be downloaded from the SchemaStore, even if some of them are bundled with the IDE"); + return wrap(builder.getPanel()); + } + + private static void addWithComment(FormBuilder builder, JBCheckBox box, String s) { + builder.addComponent(new ComponentPanelBuilder(box).withComment(s).createPanel()); + } + + private static JPanel wrap(JComponent panel) { + JPanel wrapper = new JBPanel(new BorderLayout()); + wrapper.add(panel, BorderLayout.NORTH); + return wrapper; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myCatalogCheckBox; + } + + @Override + public void reset() { + JsonSchemaCatalogProjectConfiguration.MyState state = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).getState(); + final boolean remoteEnabled = state == null || state.myIsRemoteActivityEnabled; + myRemoteCheckBox.setSelected(remoteEnabled); + myCatalogCheckBox.setEnabled(remoteEnabled); + myPreferRemoteCheckBox.setEnabled(remoteEnabled); + myCatalogCheckBox.setSelected(state == null || state.myIsCatalogEnabled); + myPreferRemoteCheckBox.setSelected(state == null || state.myIsPreferRemoteSchemas); + } + + @Override + public boolean isModified() { + JsonSchemaCatalogProjectConfiguration.MyState state = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).getState(); + return state == null + || state.myIsCatalogEnabled != myCatalogCheckBox.isSelected() + || state.myIsPreferRemoteSchemas != myPreferRemoteCheckBox.isSelected() + || state.myIsRemoteActivityEnabled != myRemoteCheckBox.isSelected(); + } + + @Override + public void apply() throws ConfigurationException { + JsonSchemaCatalogProjectConfiguration.getInstance(myProject).setState(myCatalogCheckBox.isSelected(), + myRemoteCheckBox.isSelected(), + myPreferRemoteCheckBox.isSelected()); + } + + @Nls(capitalization = Nls.Capitalization.Title) + @Override + public String getDisplayName() { + return JSON_SCHEMA_CATALOG; + } + + @Nullable + @Override + public String getHelpTopic() { + return SETTINGS_JSON_SCHEMA_CATALOG; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java new file mode 100644 index 00000000..2a2c3abf --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaCatalogProjectConfiguration.java @@ -0,0 +1,86 @@ +// 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 com.jetbrains.jsonSchema; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.openapi.project.Project; +import com.intellij.util.containers.ConcurrentList; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xmlb.annotations.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@State(name = "JsonSchemaCatalogProjectConfiguration", storages = @Storage("jsonCatalog.xml")) +public class JsonSchemaCatalogProjectConfiguration implements PersistentStateComponent<JsonSchemaCatalogProjectConfiguration.MyState> { + public volatile MyState myState = new MyState(); + private final ConcurrentList<Runnable> myChangeHandlers = ContainerUtil.createConcurrentList(); + + public boolean isCatalogEnabled() { + MyState state = getState(); + return state != null && state.myIsCatalogEnabled; + } + + public boolean isPreferRemoteSchemas() { + MyState state = getState(); + return state != null && state.myIsPreferRemoteSchemas; + } + + public void addChangeHandler(Runnable runnable) { + myChangeHandlers.add(runnable); + } + + public static JsonSchemaCatalogProjectConfiguration getInstance(@NotNull final Project project) { + return ServiceManager.getService(project, JsonSchemaCatalogProjectConfiguration.class); + } + + public JsonSchemaCatalogProjectConfiguration() { + } + + public void setState(boolean isEnabled, boolean isRemoteActivityEnabled, boolean isPreferRemoteSchemas) { + myState = new MyState(isEnabled, isRemoteActivityEnabled, isPreferRemoteSchemas); + for (Runnable handler : myChangeHandlers) { + handler.run(); + } + } + + @Nullable + @Override + public MyState getState() { + return myState; + } + + public boolean isRemoteActivityEnabled() { + MyState state = getState(); + return state != null && state.myIsRemoteActivityEnabled; + } + + @Override + public void loadState(@NotNull MyState state) { + myState = state; + for (Runnable handler : myChangeHandlers) { + handler.run(); + } + } + + static class MyState { + @Tag("enabled") + public boolean myIsCatalogEnabled = true; + + @Tag("remoteActivityEnabled") + public boolean myIsRemoteActivityEnabled = true; + + @Tag("preferRemoteSchemas") + public boolean myIsPreferRemoteSchemas = false; + + MyState() { + } + + MyState(boolean isCatalogEnabled, boolean isRemoteActivityEnabled, boolean isPreferRemoteSchemas) { + myIsCatalogEnabled = isCatalogEnabled; + myIsRemoteActivityEnabled = isRemoteActivityEnabled; + myIsPreferRemoteSchemas = isPreferRemoteSchemas; + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java new file mode 100644 index 00000000..48d49167 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaFileType.java @@ -0,0 +1,67 @@ +/* + * 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; + +import com.intellij.icons.AllIcons; +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.fileTypes.ex.FileTypeIdentifiableByVirtualFile; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** + * To make plugin github.com/BlueBoxWare/LibGDXPlugin happy + * @author Irina.Chernushina on 4/1/2016. + */ +public class JsonSchemaFileType extends LanguageFileType implements FileTypeIdentifiableByVirtualFile { + public static final JsonSchemaFileType INSTANCE = new JsonSchemaFileType(); + + public JsonSchemaFileType() { + super(JsonLanguage.INSTANCE); + } + + @NotNull + @Override + public String getName() { + return "JSON Schema"; + } + + @NotNull + @Override + public String getDescription() { + return "JSON Schema"; + } + + @NotNull + @Override + public String getDefaultExtension() { + return "json"; + } + + @Nullable + @Override + public Icon getIcon() { + return AllIcons.FileTypes.JsonSchema; + } + + @Override + public boolean isMyFileType(@NotNull VirtualFile file) { + return false; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java new file mode 100644 index 00000000..78014791 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaIconProvider.java @@ -0,0 +1,40 @@ +/* + * 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; + +import com.intellij.icons.AllIcons; +import com.intellij.ide.IconProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +/** + * @author Irina.Chernushina on 5/23/2017. + */ +public class JsonSchemaIconProvider extends IconProvider { + @Nullable + @Override + public Icon getIcon(@NotNull PsiElement element, int flags) { + if (element instanceof PsiFile && JsonSchemaService.isSchemaFile((PsiFile)element)) { + return AllIcons.FileTypes.JsonSchema; + } + return null; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java new file mode 100644 index 00000000..ace33637 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaMappingsProjectConfiguration.java @@ -0,0 +1,137 @@ +/* + * 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 com.jetbrains.jsonSchema; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.xmlb.annotations.Tag; +import com.intellij.util.xmlb.annotations.XCollection; +import com.jetbrains.jsonSchema.extension.JsonSchemaInfo; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.*; + +@State(name = "JsonSchemaMappingsProjectConfiguration", storages = @Storage("jsonSchemas.xml")) +public class JsonSchemaMappingsProjectConfiguration implements PersistentStateComponent<JsonSchemaMappingsProjectConfiguration.MyState> { + @NotNull private final Project myProject; + public volatile MyState myState = new MyState(); + + @Nullable + public UserDefinedJsonSchemaConfiguration findMappingBySchemaInfo(JsonSchemaInfo value) { + for (UserDefinedJsonSchemaConfiguration configuration : myState.myState.values()) { + if (areSimilar(value, configuration)) return configuration; + } + return null; + } + + public boolean areSimilar(JsonSchemaInfo value, UserDefinedJsonSchemaConfiguration configuration) { + return Objects.equals(normalizePath(value.getUrl(myProject)), normalizePath(configuration.getRelativePathToSchema())); + } + + @Nullable + @Contract("null -> null; !null -> !null") + public String normalizePath(@Nullable String valueUrl) { + if (valueUrl == null) return null; + if (StringUtil.contains(valueUrl, "..")) { + valueUrl = new File(valueUrl).getAbsolutePath(); + } + return valueUrl.replace('\\', '/'); + } + + @Nullable + public UserDefinedJsonSchemaConfiguration findMappingForFile(VirtualFile file) { + VirtualFile projectBaseDir = myProject.getBaseDir(); + for (UserDefinedJsonSchemaConfiguration configuration : myState.myState.values()) { + for (UserDefinedJsonSchemaConfiguration.Item pattern : configuration.patterns) { + if (pattern.mappingKind != JsonMappingKind.File) continue; + VirtualFile relativeFile = VfsUtil.findRelativeFile(projectBaseDir, pattern.getPathParts()); + if (Objects.equals(relativeFile, file) || file.getUrl().equals(pattern.getPath())) { + return configuration; + } + } + } + return null; + } + + public static JsonSchemaMappingsProjectConfiguration getInstance(@NotNull final Project project) { + return ServiceManager.getService(project, JsonSchemaMappingsProjectConfiguration.class); + } + + public JsonSchemaMappingsProjectConfiguration(@NotNull Project project) { + myProject = project; + } + + @Nullable + @Override + public MyState getState() { + return myState; + } + + public void schemaFileMoved(@NotNull final Project project, + @NotNull final String oldRelativePath, + @NotNull final String newRelativePath) { + final Optional<UserDefinedJsonSchemaConfiguration> old = myState.myState.values().stream() + .filter(schema -> FileUtil.pathsEqual(schema.getRelativePathToSchema(), oldRelativePath)) + .findFirst(); + old.ifPresent(configuration -> { + configuration.setRelativePathToSchema(newRelativePath); + JsonSchemaService.Impl.get(project).reset(); + }); + } + + public void removeConfiguration(UserDefinedJsonSchemaConfiguration configuration) { + for (Map.Entry<String, UserDefinedJsonSchemaConfiguration> entry : myState.myState.entrySet()) { + if (entry.getValue() == configuration) { + myState.myState.remove(entry.getKey()); + return; + } + } + } + + public void addConfiguration(UserDefinedJsonSchemaConfiguration configuration) { + String name = configuration.getName(); + while (myState.myState.containsKey(name)) { + name += "1"; + } + myState.myState.put(name, configuration); + } + + public Map<String, UserDefinedJsonSchemaConfiguration> getStateMap() { + return Collections.unmodifiableMap(myState.myState); + } + + @Override + public void loadState(@NotNull MyState state) { + myState = state; + JsonSchemaService.Impl.get(myProject).reset(); + } + + public void setState(@NotNull Map<String, UserDefinedJsonSchemaConfiguration> state) { + myState = new MyState(state); + } + + static class MyState { + @Tag("state") + @XCollection + public Map<String, UserDefinedJsonSchemaConfiguration> myState = new TreeMap<>(); + + MyState() { + } + + MyState(Map<String, UserDefinedJsonSchemaConfiguration> state) { + myState = state; + } + } +}
\ No newline at end of file diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java new file mode 100644 index 00000000..5e4ecda6 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaRefactoringListenerProvider.java @@ -0,0 +1,67 @@ +/* + * 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; + +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiUtilBase; +import com.intellij.refactoring.listeners.RefactoringElementListener; +import com.intellij.refactoring.listeners.RefactoringElementListenerProvider; +import com.intellij.refactoring.listeners.UndoRefactoringElementAdapter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Irina.Chernushina on 2/17/2016. + */ +public class JsonSchemaRefactoringListenerProvider implements RefactoringElementListenerProvider { + @Nullable + @Override + public RefactoringElementListener getListener(PsiElement element) { + if (element == null) { + return null; + } + final VirtualFile oldFile = PsiUtilBase.asVirtualFile(element); + if (oldFile == null || !(oldFile.getFileType() instanceof LanguageFileType) || + !(((LanguageFileType)oldFile.getFileType()).getLanguage().isKindOf(JsonLanguage.INSTANCE))) { + return null; + } + final Project project = element.getProject(); + if (project.getBaseDir() == null) return null; + + final String oldRelativePath = VfsUtilCore.getRelativePath(oldFile, project.getBaseDir()); + if (oldRelativePath != null) { + final JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project); + return new UndoRefactoringElementAdapter() { + @Override + protected void refactored(@NotNull PsiElement element, @Nullable String oldQualifiedName) { + final VirtualFile newFile = PsiUtilBase.asVirtualFile(element); + if (newFile != null) { + final String newRelativePath = VfsUtilCore.getRelativePath(newFile, project.getBaseDir()); + if (newRelativePath != null) { + configuration.schemaFileMoved(project, oldRelativePath, newRelativePath); + } + } + } + }; + } + return null; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java b/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java new file mode 100644 index 00000000..b778efae --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/JsonSchemaVfsListener.java @@ -0,0 +1,115 @@ +// 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 com.jetbrains.jsonSchema; + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.json.JsonFileType; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.ZipperUpdater; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileContentsChangedAdapter; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.impl.BulkVirtualFileListenerAdapter; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiTreeAnyChangeAbstractAdapter; +import com.intellij.util.Alarm; +import com.intellij.util.concurrency.SequentialTaskExecutor; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.messages.MessageBusConnection; +import com.intellij.util.messages.Topic; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonSchemaServiceImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ExecutorService; + +/** + * @author Irina.Chernushina on 3/30/2016. + */ +public class JsonSchemaVfsListener extends BulkVirtualFileListenerAdapter { + public static final Topic<Runnable> JSON_SCHEMA_CHANGED = Topic.create("JsonSchemaVfsListener.Json.Schema.Changed", Runnable.class); + public static final Topic<Runnable> JSON_DEPS_CHANGED = Topic.create("JsonSchemaVfsListener.Json.Deps.Changed", Runnable.class); + + public static void startListening(@NotNull Project project, @NotNull JsonSchemaService service, @NotNull MessageBusConnection connection) { + final MyUpdater updater = new MyUpdater(project, service); + connection.subscribe(VirtualFileManager.VFS_CHANGES, new JsonSchemaVfsListener(updater)); + PsiManager.getInstance(project).addPsiTreeChangeListener(new PsiTreeAnyChangeAbstractAdapter() { + @Override + protected void onChange(@Nullable PsiFile file) { + if (file != null) updater.onFileChange(file.getViewProvider().getVirtualFile()); + } + }); + } + + private JsonSchemaVfsListener(@NotNull MyUpdater updater) { + super(new VirtualFileContentsChangedAdapter() { + @NotNull private final MyUpdater myUpdater = updater; + @Override + protected void onFileChange(@NotNull final VirtualFile schemaFile) { + myUpdater.onFileChange(schemaFile); + } + + @Override + protected void onBeforeFileChange(@NotNull VirtualFile schemaFile) { + myUpdater.onFileChange(schemaFile); + } + }); + } + + private static class MyUpdater { + @NotNull private final Project myProject; + private final ZipperUpdater myUpdater; + @NotNull private final JsonSchemaService myService; + private final Set<VirtualFile> myDirtySchemas = ContainerUtil.newConcurrentSet(); + private final Runnable myRunnable; + private final ExecutorService myTaskExecutor = SequentialTaskExecutor.createSequentialApplicationPoolExecutor( + "JsonVfsUpdaterExecutor"); + + protected MyUpdater(@NotNull Project project, @NotNull JsonSchemaService service) { + myProject = project; + myUpdater = new ZipperUpdater(200, Alarm.ThreadToUse.POOLED_THREAD, project); + myService = service; + myRunnable = () -> { + if (myProject.isDisposed()) return; + Collection<VirtualFile> scope = new HashSet<>(myDirtySchemas); + if (scope.stream().anyMatch(f -> service.possiblyHasReference(f.getName()))) { + myProject.getMessageBus().syncPublisher(JSON_DEPS_CHANGED).run(); + } + myDirtySchemas.removeAll(scope); + if (scope.isEmpty()) return; + + Collection<VirtualFile> finalScope = ContainerUtil.filter(scope, file -> myService.isApplicableToFile(file) + && ((JsonSchemaServiceImpl)myService).isMappedSchema(file, false)); + if (finalScope.isEmpty()) return; + if (myProject.isDisposed()) return; + myProject.getMessageBus().syncPublisher(JSON_SCHEMA_CHANGED).run(); + + final DaemonCodeAnalyzer analyzer = DaemonCodeAnalyzer.getInstance(project); + final PsiManager psiManager = PsiManager.getInstance(project); + final Editor[] editors = EditorFactory.getInstance().getAllEditors(); + Arrays.stream(editors).filter(editor -> editor instanceof EditorEx) + .map(editor -> ((EditorEx)editor).getVirtualFile()) + .filter(file -> file != null && file.isValid()) + .forEach(file -> { + final Collection<VirtualFile> schemaFiles = ((JsonSchemaServiceImpl)myService).getSchemasForFile(file, false, true); + if (schemaFiles.stream().anyMatch(finalScope::contains)) { + ReadAction.nonBlocking(() -> Optional.ofNullable(psiManager.findFile(file)).ifPresent(analyzer::restart)).submit(myTaskExecutor); + } + }); + }; + } + + protected void onFileChange(@NotNull final VirtualFile schemaFile) { + if (JsonFileType.DEFAULT_EXTENSION.equals(schemaFile.getExtension())) { + myDirtySchemas.add(schemaFile); + myUpdater.queue(myRunnable); + } + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java b/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java new file mode 100644 index 00000000..8ab70270 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/UserDefinedJsonSchemaConfiguration.java @@ -0,0 +1,323 @@ +/* + * 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; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.AtomicClearableLazyValue; +import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ArrayUtil; +import com.intellij.util.PairProcessor; +import com.intellij.util.PatternUtil; +import com.intellij.util.SmartList; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xmlb.annotations.Tag; +import com.intellij.util.xmlb.annotations.Transient; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +/** + * @author Irina.Chernushina on 4/19/2017. + */ +@Tag("SchemaInfo") +public class UserDefinedJsonSchemaConfiguration { + private final static Comparator<Item> ITEM_COMPARATOR = (o1, o2) -> { + if (o1.isPattern() != o2.isPattern()) return o1.isPattern() ? -1 : 1; + if (o1.isDirectory() != o2.isDirectory()) return o1.isDirectory() ? -1 : 1; + return o1.path.compareToIgnoreCase(o2.path); + }; + + public String name; + public String relativePathToSchema; + public JsonSchemaVersion schemaVersion = JsonSchemaVersion.SCHEMA_4; + public boolean applicationDefined; + public List<Item> patterns = new SmartList<>(); + @Transient + private final AtomicClearableLazyValue<List<PairProcessor<Project, VirtualFile>>> myCalculatedPatterns = + new AtomicClearableLazyValue<List<PairProcessor<Project, VirtualFile>>>() { + @NotNull + @Override + protected List<PairProcessor<Project, VirtualFile>> compute() { + return recalculatePatterns(); + } + }; + + public UserDefinedJsonSchemaConfiguration() { + } + + public UserDefinedJsonSchemaConfiguration(@NotNull String name, + JsonSchemaVersion schemaVersion, + @NotNull String relativePathToSchema, + boolean applicationDefined, + @Nullable List<Item> patterns) { + this.name = name; + this.relativePathToSchema = relativePathToSchema; + this.schemaVersion = schemaVersion; + this.applicationDefined = applicationDefined; + setPatterns(patterns); + } + + public String getName() { + return name; + } + + public void setName(@NotNull String name) { + this.name = name; + } + + public String getRelativePathToSchema() { + return relativePathToSchema; + } + + public JsonSchemaVersion getSchemaVersion() { + return schemaVersion; + } + + public void setSchemaVersion(JsonSchemaVersion schemaVersion) { + this.schemaVersion = schemaVersion; + } + + public void setRelativePathToSchema(String relativePathToSchema) { + this.relativePathToSchema = relativePathToSchema; + } + + public boolean isApplicationDefined() { + return applicationDefined; + } + + public void setApplicationDefined(boolean applicationDefined) { + this.applicationDefined = applicationDefined; + } + + public List<Item> getPatterns() { + return patterns; + } + + public void setPatterns(@Nullable List<Item> patterns) { + this.patterns.clear(); + if (patterns != null) this.patterns.addAll(patterns); + Collections.sort(this.patterns, ITEM_COMPARATOR); + myCalculatedPatterns.drop(); + } + + public void refreshPatterns() { + myCalculatedPatterns.drop(); + } + + @NotNull + public List<PairProcessor<Project, VirtualFile>> getCalculatedPatterns() { + return myCalculatedPatterns.getValue(); + } + + private List<PairProcessor<Project, VirtualFile>> recalculatePatterns() { + final List<PairProcessor<Project, VirtualFile>> result = new SmartList<>(); + for (final Item patternText : patterns) { + switch (patternText.mappingKind) { + case File: + result.add((project, vfile) -> vfile.equals(getRelativeFile(project, patternText)) || vfile.getUrl().equals(patternText.getPath())); + break; + case Pattern: + String pathText = patternText.getPath().replace(File.separatorChar, '/').replace('\\', '/'); + final Pattern pattern = pathText.isEmpty() + ? PatternUtil.NOTHING + : pathText.indexOf('/') >= 0 + ? PatternUtil.compileSafe(".*/" + PatternUtil.convertToRegex(pathText), PatternUtil.NOTHING) + : PatternUtil.fromMask(pathText); + result.add((project, file) -> JsonSchemaObject.matchPattern(pattern, pathText.indexOf('/') >= 0 + ? file.getPath() + : file.getName())); + break; + case Directory: + result.add((project, vfile) -> { + final VirtualFile relativeFile = getRelativeFile(project, patternText); + if (relativeFile == null || !VfsUtilCore.isAncestor(relativeFile, vfile, true)) return false; + JsonSchemaService service = JsonSchemaService.Impl.get(project); + return service.isApplicableToFile(vfile); + }); + break; + } + } + return result; + } + + @Nullable + private static VirtualFile getRelativeFile(@NotNull final Project project, @NotNull final Item pattern) { + if (project.getBasePath() == null) { + return null; + } + + final String path = FileUtilRt.toSystemIndependentName(StringUtil.notNullize(pattern.path)); + final List<String> parts = pathToPartsList(path); + if (parts.isEmpty()) { + return project.getBaseDir(); + } + else { + return VfsUtil.findRelativeFile(project.getBaseDir(), ArrayUtil.toStringArray(parts)); + } + } + + @NotNull + private static List<String> pathToPartsList(@NotNull String path) { + return ContainerUtil.filter(StringUtil.split(path, "/"), s -> !".".equals(s)); + } + + @NotNull + private static String[] pathToParts(@NotNull String path) { + return ArrayUtil.toStringArray(pathToPartsList(path)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserDefinedJsonSchemaConfiguration info = (UserDefinedJsonSchemaConfiguration)o; + + if (applicationDefined != info.applicationDefined) return false; + if (schemaVersion != info.schemaVersion) return false; + if (!Objects.equals(name, info.name)) return false; + if (!Objects.equals(relativePathToSchema, info.relativePathToSchema)) return false; + + return Objects.equals(patterns, info.patterns); + } + + @Override + public int hashCode() { + int result = name != null ? name.hashCode() : 0; + result = 31 * result + (relativePathToSchema != null ? relativePathToSchema.hashCode() : 0); + result = 31 * result + (applicationDefined ? 1 : 0); + result = 31 * result + (patterns != null ? patterns.hashCode() : 0); + result = 31 * result + schemaVersion.hashCode(); + return result; + } + + public static class Item { + public String path; + public JsonMappingKind mappingKind = JsonMappingKind.File; + + public Item() { + } + + public Item(String path, JsonMappingKind mappingKind) { + this.path = neutralizePath(path); + this.mappingKind = mappingKind; + } + + public Item(String path, boolean isPattern, boolean isDirectory) { + this.path = neutralizePath(path); + this.mappingKind = isPattern ? JsonMappingKind.Pattern : isDirectory ? JsonMappingKind.Directory : JsonMappingKind.File; + } + + @NotNull + private static String normalizePath(String path) { + if (preserveSlashes(path)) return path; + return StringUtil.trimEnd(FileUtilRt.toSystemDependentName(path), File.separatorChar); + } + + private static boolean preserveSlashes(String path) { + // http/https URLs to schemas + // mock URLs of fragments editor + return StringUtil.startsWith(path, "http:") + || StringUtil.startsWith(path, "https:") + || StringUtil.startsWith(path, "mock:"); + } + + @NotNull + private static String neutralizePath(String path) { + if (preserveSlashes(path)) return path; + return StringUtil.trimEnd(FileUtilRt.toSystemIndependentName(path), '/'); + } + + public String getPath() { + return normalizePath(path); + } + + public void setPath(String path) { + this.path = neutralizePath(path); + } + + public String getError() { + switch (mappingKind) { + case File: + return !StringUtil.isEmpty(path) ? null : "Empty file path doesn't match anything"; + case Pattern: + return !StringUtil.isEmpty(path) ? null : "Empty pattern matches nothing"; + case Directory: + return null; + } + + return "Unknown mapping kind"; + } + + public boolean isPattern() { + return mappingKind == JsonMappingKind.Pattern; + } + + public void setPattern(boolean pattern) { + mappingKind = pattern ? JsonMappingKind.Pattern : JsonMappingKind.File; + } + + public boolean isDirectory() { + return mappingKind == JsonMappingKind.Directory; + } + + public void setDirectory(boolean directory) { + mappingKind = directory ? JsonMappingKind.Directory : JsonMappingKind.File; + } + + public String getPresentation() { + if (mappingKind == JsonMappingKind.Directory && StringUtil.isEmpty(path)) { + return mappingKind.getPrefix() + "[Project Directory]"; + } + return mappingKind.getPrefix() + getPath(); + } + + public String[] getPathParts() { + return pathToParts(path); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Item item = (Item)o; + + if (mappingKind != item.mappingKind) return false; + return Objects.equals(path, item.path); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(path); + result = 31 * result + Objects.hashCode(mappingKind); + return result; + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java new file mode 100644 index 00000000..c805a06a --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalker.java @@ -0,0 +1,81 @@ +// 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 com.jetbrains.jsonSchema.extension; + +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; +import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter; +import com.jetbrains.jsonSchema.impl.JsonOriginalPsiWalker; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import com.jetbrains.jsonSchema.impl.JsonSchemaVariantsTreeBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +/** + * @author Irina.Chernushina on 2/15/2017. + */ +public interface JsonLikePsiWalker { + JsonOriginalPsiWalker JSON_ORIGINAL_PSI_WALKER = new JsonOriginalPsiWalker(); + + /** + * Returns YES in place where a property name is expected, + * NO in place where a property value is expected, + * UNSURE where both property name and property value can be present + */ + ThreeState isName(PsiElement element); + + boolean isPropertyWithValue(@NotNull PsiElement element); + PsiElement goUpToCheckable(@NotNull final PsiElement element); + @Nullable + List<JsonSchemaVariantsTreeBuilder.Step> findPosition(@NotNull final PsiElement element, boolean forceLastTransition); + boolean isNameQuoted(); + boolean onlyDoubleQuotesForStringLiterals(); + default boolean quotesForStringLiterals() { return true; } + boolean hasPropertiesBehindAndNoComma(@NotNull PsiElement element); + Set<String> getPropertyNamesOfParentObject(@NotNull PsiElement originalPosition, PsiElement computedPosition); + @Nullable + JsonPropertyAdapter getParentPropertyAdapter(@NotNull PsiElement element); + boolean isTopJsonElement(@NotNull PsiElement element); + @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; + + return JsonLikePsiWalkerFactory.EXTENSION_POINT_NAME.getExtensionList().stream() + .filter(extension -> extension.handles(element)) + .findFirst() + .map(extension -> extension.create(schemaObject)) + .orElse(null); + } + + default String getDefaultObjectValue() { return "{}"; } + @Nullable default String defaultObjectValueDescription() { return null; } + default String getDefaultArrayValue() { return "[]"; } + @Nullable default String defaultArrayValueDescription() { return null; } + + default boolean invokeEnterBeforeObjectAndArray() { return false; } + + default String getNodeTextForValidation(PsiElement element) { return element.getText(); } + + 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(PsiElement initialElement, PsiElement element); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java new file mode 100644 index 00000000..aa803d3b --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonLikePsiWalkerFactory.java @@ -0,0 +1,33 @@ +/* + * 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; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.psi.PsiElement; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import org.jetbrains.annotations.NotNull; + +/** + * @author Irina.Chernushina on 3/7/2017. + */ +public interface JsonLikePsiWalkerFactory { + ExtensionPointName<JsonLikePsiWalkerFactory> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonLikePsiWalkerFactory"); + + boolean handles(@NotNull PsiElement element); + + @NotNull + JsonLikePsiWalker create(@NotNull JsonSchemaObject schemaObject); +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java new file mode 100644 index 00000000..f0926b2e --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaEnabler.java @@ -0,0 +1,31 @@ +// 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 com.jetbrains.jsonSchema.extension; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.vfs.VirtualFile; + +/** + * This API provides a mechanism to enable JSON schemas in particular files + * This interface should be implemented if you want a particular kind of virtual files to have access to JsonSchemaService APIs + * + * This API is new in IntelliJ IDEA Platform 2018.2 + */ +public interface JsonSchemaEnabler { + ExtensionPointName<JsonSchemaEnabler> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonSchemaEnabler"); + + /** + * This method should return true if JSON schema mechanism should become applicable to corresponding file. + * This method SHOULD NOT ADDRESS INDEXES. + * @param file Virtual file to check for + * @return true if available, false otherwise + */ + boolean isEnabledForFile(VirtualFile file); + + /** + * This method enables/disables JSON schema selection widget + * This method SHOULD NOT ADDRESS INDEXES + */ + default boolean shouldShowSwitcherWidget(VirtualFile file) { + return true; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java new file mode 100644 index 00000000..9047afee --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaFileProvider.java @@ -0,0 +1,37 @@ +package com.jetbrains.jsonSchema.extension; + + +import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface JsonSchemaFileProvider { + boolean isAvailable(@NotNull VirtualFile file); + + @NotNull + String getName(); + + @Nullable + VirtualFile getSchemaFile(); + + @NotNull + SchemaType getSchemaType(); + + default JsonSchemaVersion getSchemaVersion() { + return JsonSchemaVersion.SCHEMA_4; + } + + @Nullable + default String getThirdPartyApiInformation() { + return null; + } + + default boolean isUserVisible() { return true; } + + @NotNull + default String getPresentableName() { return getName(); } + + @Nullable + default String getRemoteSource() { return null; } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java new file mode 100644 index 00000000..e6bb993c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaImportedProviderMarker.java @@ -0,0 +1,22 @@ +/* + * 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; + +/** + * @author Irina.Chernushina on 2/15/2016. + */ +public interface JsonSchemaImportedProviderMarker { +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java new file mode 100644 index 00000000..c05c5c5e --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaInfo.java @@ -0,0 +1,132 @@ +// 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 com.jetbrains.jsonSchema.extension; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.impl.JsonSchemaType; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Set; + +public class JsonSchemaInfo { + @Nullable private final JsonSchemaFileProvider myProvider; + @Nullable private final String myUrl; + @NotNull private final static Set<String> myDumbNames = ContainerUtil.set( + "schema", + "lib", + "cli", + "packages", + "master", + "format", + "angular", // the only angular-related schema is the 'angular-cli', so we skip the repo name + "config"); + + public JsonSchemaInfo(@NotNull JsonSchemaFileProvider provider) { + myProvider = provider; + myUrl = null; + } + + public JsonSchemaInfo(@NotNull String url) { + myUrl = url; + myProvider = null; + } + + @Nullable + public JsonSchemaFileProvider getProvider() { + return myProvider; + } + + @NotNull + public String getUrl(Project project) { + if (myProvider != null) { + String remoteSource = myProvider.getRemoteSource(); + if (remoteSource != null) { + return remoteSource; + } + + VirtualFile schemaFile = myProvider.getSchemaFile(); + if (schemaFile == null) return ""; + + if (schemaFile instanceof HttpVirtualFile) { + return schemaFile.getUrl(); + } + + return getRelativePath(project, schemaFile.getPath()); + } + else { + assert myUrl != null; + return myUrl; + } + } + + @NotNull + public String getDescription() { + if (myProvider != null) { + String providerName = myProvider.getPresentableName(); + return sanitizeName(providerName); + } + + assert myUrl != null; + + // the only weird case + if ("http://json.schemastore.org/config".equals(myUrl) + || "https://schemastore.azurewebsites.net/schemas/json/config.json".equals(myUrl)) { + return "asp.net config"; + } + + String url = myUrl.replace('\\', '/'); + + return ContainerUtil.reverse(StringUtil.split(url, "/")) + .stream() + .map(p -> sanitizeName(p)) + .filter(p -> !isVeryDumbName(p)) + .findFirst().orElse(sanitizeName(myUrl)); + } + + public static boolean isVeryDumbName(@Nullable String possibleName) { + if (StringUtil.isEmptyOrSpaces(possibleName) || myDumbNames.contains(possibleName)) return true; + return StringUtil.split(possibleName, ".").stream().allMatch(s -> JsonSchemaType.isInteger(s)); + } + + @NotNull + private static String sanitizeName(@NotNull String providerName) { + return StringUtil.trimEnd(StringUtil.trimEnd(StringUtil.trimEnd(providerName, ".json"), "-schema"), ".schema"); + } + + @NotNull + public JsonSchemaVersion getSchemaVersion() { + return myProvider != null ? myProvider.getSchemaVersion() : JsonSchemaVersion.SCHEMA_4; + } + + @NotNull + public static String getRelativePath(@NotNull Project project, @NotNull String text) { + text = text.trim(); + if (project.isDefault() || project.getBasePath() == null) return text; + if (StringUtil.isEmptyOrSpaces(text)) return text; + final File ioFile = new File(text); + if (!ioFile.isAbsolute()) return text; + VirtualFile file = VfsUtil.findFileByIoFile(ioFile, false); + if (file == null) return text; + final String relativePath = VfsUtilCore.getRelativePath(file, project.getBaseDir()); + if (relativePath != null) return relativePath; + if (isMeaningfulAncestor(VfsUtilCore.getCommonAncestor(file, project.getBaseDir()))) { + String path = VfsUtilCore.findRelativePath(project.getBaseDir(), file, File.separatorChar); + if (path != null) return path; + } + return text; + } + + private static boolean isMeaningfulAncestor(@Nullable VirtualFile ancestor) { + if (ancestor == null) return false; + VirtualFile homeDir = VfsUtil.getUserHomeDir(); + return homeDir != null && VfsUtilCore.isAncestor(homeDir, ancestor, true); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java new file mode 100644 index 00000000..829c8c50 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProjectSelfProviderFactory.java @@ -0,0 +1,125 @@ +// 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 com.jetbrains.jsonSchema.extension; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.NullableLazyValue; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import kotlin.NotImplementedError; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * @author Irina.Chernushina on 2/24/2016. + */ +public class JsonSchemaProjectSelfProviderFactory implements JsonSchemaProviderFactory { + public static final int TOTAL_PROVIDERS = 3; + private static final String SCHEMA_JSON_FILE_NAME = "schema.json"; + private static final String SCHEMA06_JSON_FILE_NAME = "schema06.json"; + private static final String SCHEMA07_JSON_FILE_NAME = "schema07.json"; + + @NotNull + @Override + public List<JsonSchemaFileProvider> getProviders(@NotNull final Project project) { + return ContainerUtil.list(new MyJsonSchemaFileProvider(project, SCHEMA_JSON_FILE_NAME), + new MyJsonSchemaFileProvider(project, SCHEMA06_JSON_FILE_NAME), + new MyJsonSchemaFileProvider(project, SCHEMA07_JSON_FILE_NAME)); + } + + public static class MyJsonSchemaFileProvider implements JsonSchemaFileProvider { + @NotNull private final Project myProject; + @NotNull private final NullableLazyValue<VirtualFile> mySchemaFile; + @NotNull private final String myFileName; + + public boolean isSchemaV4() { + return SCHEMA_JSON_FILE_NAME.equals(myFileName); + } + public boolean isSchemaV6() { + return SCHEMA06_JSON_FILE_NAME.equals(myFileName); + } + public boolean isSchemaV7() { + return SCHEMA07_JSON_FILE_NAME.equals(myFileName); + } + + private MyJsonSchemaFileProvider(@NotNull final Project project, @NotNull String fileName) { + myProject = project; + myFileName = fileName; + // schema file can not be static here, because in schema's user data we cache project-scope objects (i.e. which can refer to project) + mySchemaFile = NullableLazyValue.createValue(() -> JsonSchemaProviderFactory.getResourceFile(JsonSchemaProjectSelfProviderFactory.class, "/jsonSchema/" + fileName)); + } + + @Override + public boolean isAvailable(@NotNull VirtualFile file) { + if (myProject.isDisposed()) return false; + JsonSchemaService service = JsonSchemaService.Impl.get(myProject); + if (!service.isApplicableToFile(file)) return false; + JsonSchemaVersion schemaVersion = service.getSchemaVersion(file); + if (schemaVersion == null) return false; + switch (schemaVersion) { + case SCHEMA_4: + return isSchemaV4(); + case SCHEMA_6: + return isSchemaV6(); + case SCHEMA_7: + return isSchemaV7(); + } + + throw new NotImplementedError("Unknown schema version: " + schemaVersion); + } + + @Override + public JsonSchemaVersion getSchemaVersion() { + return isSchemaV4() ? JsonSchemaVersion.SCHEMA_4 : isSchemaV7() ? JsonSchemaVersion.SCHEMA_7 : JsonSchemaVersion.SCHEMA_6; + } + + @NotNull + @Override + public String getName() { + return myFileName; + } + + @Nullable + @Override + public VirtualFile getSchemaFile() { + return mySchemaFile.getValue(); + } + + @NotNull + @Override + public SchemaType getSchemaType() { + return SchemaType.schema; + } + + @Nullable + @Override + public String getRemoteSource() { + switch (myFileName) { + case SCHEMA_JSON_FILE_NAME: + return "http://json-schema.org/draft-04/schema"; + case SCHEMA06_JSON_FILE_NAME: + return "http://json-schema.org/draft-06/schema"; + case SCHEMA07_JSON_FILE_NAME: + return "http://json-schema.org/draft-07/schema"; + } + return null; + } + + @NotNull + @Override + public String getPresentableName() { + switch (myFileName) { + case SCHEMA_JSON_FILE_NAME: + return "JSON schema v4"; + case SCHEMA06_JSON_FILE_NAME: + return "JSON schema v6"; + case SCHEMA07_JSON_FILE_NAME: + return "JSON schema v7"; + } + return getName(); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java new file mode 100644 index 00000000..70df36dd --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaProviderFactory.java @@ -0,0 +1,42 @@ +// 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 com.jetbrains.jsonSchema.extension; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +import java.net.URL; +import java.util.List; + +public interface JsonSchemaProviderFactory { + ExtensionPointName<JsonSchemaProviderFactory> EP_NAME = ExtensionPointName.create("JavaScript.JsonSchema.ProviderFactory"); + Logger LOG = Logger.getInstance(JsonSchemaProviderFactory.class); + + @NotNull + List<JsonSchemaFileProvider> getProviders(@NotNull Project project); + + /** + * Finds a {@link VirtualFile} instance corresponding to a specified resource path (relative or absolute). + * + * @param baseClass + * @param resourcePath String identifying a resource (relative or absolute) + * See {@link Class#getResource(String)} for more details + * @return VirtualFile instance, or null if not found + */ + static VirtualFile getResourceFile(@NotNull Class baseClass, @NotNull String resourcePath) { + URL url = baseClass.getResource(resourcePath); + if (url == null) { + LOG.error("Cannot find resource " + resourcePath); + return null; + } + VirtualFile file = VfsUtil.findFileByURL(url); + if (file == null) { + LOG.error("Cannot find file by " + resourcePath); + return null; + } + return file; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java new file mode 100644 index 00000000..09117dd5 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonSchemaUserDefinedProviderFactory.java @@ -0,0 +1,138 @@ +// 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 com.jetbrains.jsonSchema.extension; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.PairProcessor; +import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import com.jetbrains.jsonSchema.remote.JsonFileResolver; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath; + +/** + * @author Irina.Chernushina on 2/13/2016. + */ +public class JsonSchemaUserDefinedProviderFactory implements JsonSchemaProviderFactory { + @NotNull + @Override + public List<JsonSchemaFileProvider> getProviders(@NotNull Project project) { + final JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project); + + final Map<String, UserDefinedJsonSchemaConfiguration> map = configuration.getStateMap(); + final List<JsonSchemaFileProvider> providers = map.values().stream() + .map(schema -> createProvider(project, schema)).collect(Collectors.toList()); + + return providers.isEmpty() ? Collections.emptyList() : providers; + } + + @NotNull + public MyProvider createProvider(@NotNull Project project, + UserDefinedJsonSchemaConfiguration schema) { + String relPath = schema.getRelativePathToSchema(); + return new MyProvider(project, schema.getSchemaVersion(), schema.getName(), + isHttpPath(relPath) || new File(relPath).isAbsolute() + ? relPath + : new File(project.getBasePath(), + relPath).getAbsolutePath(), + schema.getCalculatedPatterns()); + } + + static class MyProvider implements JsonSchemaFileProvider, JsonSchemaImportedProviderMarker { + @NotNull private final Project myProject; + @NotNull private final JsonSchemaVersion myVersion; + @NotNull private final String myName; + @NotNull private final String myFile; + private VirtualFile myVirtualFile; + @NotNull private final List<? extends PairProcessor<Project, VirtualFile>> myPatterns; + + MyProvider(@NotNull final Project project, + @NotNull final JsonSchemaVersion version, + @NotNull final String name, + @NotNull final String file, + @NotNull final List<? extends PairProcessor<Project, VirtualFile>> patterns) { + myProject = project; + myVersion = version; + myName = name; + myFile = file; + myPatterns = patterns; + } + + @Override + public JsonSchemaVersion getSchemaVersion() { + return myVersion; + } + + @Nullable + @Override + public VirtualFile getSchemaFile() { + if (myVirtualFile != null && myVirtualFile.isValid()) return myVirtualFile; + String path = myFile; + if (isHttpPath(path)) { + myVirtualFile = JsonFileResolver.urlToFile(path); + } + else { + final LocalFileSystem lfs = LocalFileSystem.getInstance(); + myVirtualFile = lfs.findFileByPath(myFile); + if (myVirtualFile == null) { + myVirtualFile = lfs.refreshAndFindFileByPath(myFile); + } + } + return myVirtualFile; + } + + @NotNull + @Override + public SchemaType getSchemaType() { + return SchemaType.userSchema; + } + + @NotNull + @Override + public String getName() { + return myName; + } + + @Override + public boolean isAvailable(@NotNull VirtualFile file) { + //noinspection SimplifiableIfStatement + if (myPatterns.isEmpty() || file.isDirectory() || !file.isValid() || getSchemaFile() == null) return false; + return myPatterns.stream().anyMatch(processor -> processor.process(myProject, file)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MyProvider provider = (MyProvider)o; + + if (!myName.equals(provider.myName)) return false; + return FileUtil.pathsEqual(myFile, provider.myFile); + } + + @Override + public int hashCode() { + int result = myName.hashCode(); + result = 31 * result + FileUtil.pathHashCode(myFile); + return result; + } + + @Nullable + @Override + public String getRemoteSource() { + return isHttpPath(myFile) ? myFile : null; + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java b/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java new file mode 100644 index 00000000..54dc903f --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/JsonWidgetSuppressor.java @@ -0,0 +1,17 @@ +// 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 com.jetbrains.jsonSchema.extension; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +public interface JsonWidgetSuppressor { + ExtensionPointName<JsonWidgetSuppressor> EXTENSION_POINT_NAME = ExtensionPointName.create("com.intellij.json.jsonWidgetSuppressor"); + + /** + * Allows to suppress JSON widget for particular files + * This method can access indexes and PSI + */ + boolean suppressSwitcherWidget(@NotNull VirtualFile file, @NotNull Project project); +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java b/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java new file mode 100644 index 00000000..b59e5a1c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/SchemaType.java @@ -0,0 +1,23 @@ +/* + * 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; + +/** + * @author Irina.Chernushina on 3/29/2016. + */ +public enum SchemaType { + schema, embeddedSchema, userSchema, remoteSchema +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java new file mode 100644 index 00000000..af247f78 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonArrayValueAdapter.java @@ -0,0 +1,32 @@ +/* + * 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.adapters; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public interface JsonArrayValueAdapter extends JsonValueAdapter { + @NotNull List<JsonValueAdapter> getElements(); + + @Override + default boolean isNull() { + return false; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java new file mode 100644 index 00000000..627ddf56 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonObjectValueAdapter.java @@ -0,0 +1,32 @@ +/* + * 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.adapters; + +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public interface JsonObjectValueAdapter extends JsonValueAdapter { + @NotNull List<JsonPropertyAdapter> getPropertyList(); + + @Override + default boolean isNull() { + return false; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java new file mode 100644 index 00000000..e82279bf --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonPropertyAdapter.java @@ -0,0 +1,33 @@ +/* + * 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.adapters; + +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public interface JsonPropertyAdapter { + @Nullable String getName(); + @Nullable JsonValueAdapter getNameValueAdapter(); + @NotNull Collection<JsonValueAdapter> getValues(); + @NotNull PsiElement getDelegate(); + @Nullable JsonObjectValueAdapter getParentObject(); +} diff --git a/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java new file mode 100644 index 00000000..1d8f2742 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/extension/adapters/JsonValueAdapter.java @@ -0,0 +1,40 @@ +/* + * 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.adapters; + +import com.intellij.psi.PsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public interface JsonValueAdapter { + default boolean isShouldBeIgnored() {return false;} + boolean isObject(); + boolean isArray(); + boolean isStringLiteral(); + boolean isNumberLiteral(); + boolean isBooleanLiteral(); + boolean isNull(); + + @NotNull PsiElement getDelegate(); + + @Nullable JsonObjectValueAdapter getAsObject(); + @Nullable JsonArrayValueAdapter getAsArray(); + + default boolean shouldCheckIntegralRequirements() {return true;} +} diff --git a/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java b/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java new file mode 100644 index 00000000..2070e3fc --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/ide/JsonSchemaService.java @@ -0,0 +1,75 @@ +// 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 com.jetbrains.jsonSchema.ide; + +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.ModificationTracker; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; +import com.jetbrains.jsonSchema.extension.JsonSchemaInfo; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + +public interface JsonSchemaService { + class Impl { + public static JsonSchemaService get(@NotNull Project project) { + return ServiceManager.getService(project, JsonSchemaService.class); + } + } + + static boolean isSchemaFile(@NotNull PsiFile psiFile) { + final VirtualFile file = psiFile.getViewProvider().getVirtualFile(); + JsonSchemaService service = Impl.get(psiFile.getProject()); + return service.isSchemaFile(file) && service.isApplicableToFile(file); + } + + boolean isSchemaFile(@NotNull VirtualFile file); + + @Nullable + JsonSchemaVersion getSchemaVersion(@NotNull VirtualFile file); + + @NotNull + Collection<VirtualFile> getSchemaFilesForFile(@NotNull VirtualFile file); + + void registerRemoteUpdateCallback(Runnable callback); + void unregisterRemoteUpdateCallback(Runnable callback); + void registerResetAction(Runnable action); + void unregisterResetAction(Runnable action); + + void registerReference(String ref); + boolean possiblyHasReference(String ref); + + void triggerUpdateRemote(); + + @Nullable + JsonSchemaObject getSchemaObject(@NotNull VirtualFile file); + + @Nullable + JsonSchemaObject getSchemaObjectForSchemaFile(@NotNull VirtualFile schemaFile); + + @Nullable + VirtualFile findSchemaFileByReference(@NotNull String reference, @Nullable VirtualFile referent); + + @Nullable + JsonSchemaFileProvider getSchemaProvider(@NotNull final VirtualFile schemaFile); + + void reset(); + + ModificationTracker getAnySchemaChangeTracker(); + + List<JsonSchemaInfo> getAllUserVisibleSchemas(); + + boolean isApplicableToFile(@Nullable VirtualFile file); + + @NotNull + static String normalizeId(@NotNull String id) { + id = id.endsWith("#") ? id.substring(0, id.length() - 1) : id; + return id.startsWith("#") ? id.substring(1) : id; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java b/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java new file mode 100644 index 00000000..b069841a --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/CachedValueProviderOnPsiFile.java @@ -0,0 +1,41 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.CachedValue; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; +import com.intellij.util.Function; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class CachedValueProviderOnPsiFile<T> implements CachedValueProvider<T> { + private final PsiFile myPsiFile; + + public CachedValueProviderOnPsiFile(@NotNull PsiFile psiFile) { + myPsiFile = psiFile; + } + + @Nullable + @Override + public Result<T> compute() { + return CachedValueProvider.Result.create(evaluate(myPsiFile), myPsiFile); + } + + @Nullable + public abstract T evaluate(@NotNull PsiFile psiFile); + + @Nullable + public static <T> T getOrCompute(@NotNull PsiFile psiFile, @NotNull Function<? super PsiFile, ? extends T> eval, @NotNull Key<CachedValue<T>> key) { + final CachedValueProvider<T> provider = new CachedValueProviderOnPsiFile<T>(psiFile) { + @Override + @Nullable + public T evaluate(@NotNull PsiFile psiFile) { + return eval.fun(psiFile); + } + }; + return ReadAction.compute(() -> CachedValuesManager.getCachedValue(psiFile, key, provider)); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java b/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java new file mode 100644 index 00000000..70f66a56 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/EnumArrayValueWrapper.java @@ -0,0 +1,25 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.stream.Collectors; + +public class EnumArrayValueWrapper { + @NotNull private final Object[] myValues; + + public EnumArrayValueWrapper(@NotNull Object[] values) { + myValues = values; + } + + @NotNull + public Object[] getValues() { + return myValues; + } + + @Override + public String toString() { + return "[" + Arrays.stream(myValues).map(v -> v.toString()).collect(Collectors.joining(", ")) + "]"; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java b/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java new file mode 100644 index 00000000..b8da4134 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/EnumObjectValueWrapper.java @@ -0,0 +1,25 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.stream.Collectors; + +public class EnumObjectValueWrapper { + @NotNull private final Map<String, Object> myValues; + + public EnumObjectValueWrapper(@NotNull Map<String, Object> values) { + myValues = values; + } + + @NotNull + public Map<String, Object> getValues() { + return myValues; + } + + @Override + public String toString() { + return "{" + myValues.entrySet().stream().map(v -> "\"" + v.getKey() + "\": " + v.getValue()).collect(Collectors.joining(", ")) + "}"; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java b/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java new file mode 100644 index 00000000..40773ae4 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonCachedValues.java @@ -0,0 +1,192 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.json.navigation.JsonQualifiedNameKind; +import com.intellij.json.navigation.JsonQualifiedNameProvider; +import com.intellij.json.psi.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.SyntaxTraverser; +import com.intellij.psi.util.CachedValue; +import com.intellij.util.AstLoadingFilter; +import com.intellij.util.Function; +import com.intellij.util.ObjectUtils; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.remote.JsonFileResolver; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class JsonCachedValues { + private static final Key<CachedValue<JsonSchemaObject>> JSON_OBJECT_CACHE_KEY = Key.create("JsonSchemaObjectCache"); + + @Nullable + public static JsonSchemaObject getSchemaObject(@NotNull VirtualFile schemaFile, @NotNull Project project) { + JsonFileResolver.startFetchingHttpFileIfNeeded(schemaFile, project); + return computeForFile(schemaFile, project, JsonCachedValues::computeSchemaObject, JSON_OBJECT_CACHE_KEY); + } + + @Nullable + private static JsonSchemaObject computeSchemaObject(@NotNull PsiFile f) { + final JsonObject topLevelValue = AstLoadingFilter.forceAllowTreeLoading( + f, + () -> ObjectUtils.tryCast(((JsonFile)f).getTopLevelValue(), JsonObject.class)); + if (topLevelValue != null) { + return new JsonSchemaReader().read(topLevelValue); + } + return null; + } + + static final String URL_CACHE_KEY = "JsonSchemaUrlCache"; + private static final Key<CachedValue<String>> SCHEMA_URL_KEY = Key.create(URL_CACHE_KEY); + @Nullable + public static String getSchemaUrlFromSchemaProperty(@NotNull VirtualFile file, + @NotNull Project project) { + String value = JsonSchemaFileValuesIndex.getCachedValue(project, file, URL_CACHE_KEY); + if (value != null) { + return JsonSchemaFileValuesIndex.NULL.equals(value) ? null : value; + } + + PsiFile psiFile = resolveFile(file, project); + return !(psiFile instanceof JsonFile) ? null : CachedValueProviderOnPsiFile + .getOrCompute(psiFile, JsonCachedValues::fetchSchemaUrl, SCHEMA_URL_KEY); + } + + private static PsiFile resolveFile(@NotNull VirtualFile file, + @NotNull Project project) { + if (project.isDisposed() || !file.isValid()) return null; + return PsiManager.getInstance(project).findFile(file); + } + + @Nullable + static String fetchSchemaUrl(@Nullable PsiFile psiFile) { + if (!(psiFile instanceof JsonFile)) return null; + final String url = JsonSchemaFileValuesIndex.readTopLevelProps(psiFile.getFileType(), psiFile.getText()).get(URL_CACHE_KEY); + return url == null || JsonSchemaFileValuesIndex.NULL.equals(url) ? null : url; + } + + static final String ID_CACHE_KEY = "JsonSchemaIdCache"; + static final String OBSOLETE_ID_CACHE_KEY = "JsonSchemaObsoleteIdCache"; + private static final Key<CachedValue<String>> SCHEMA_ID_CACHE_KEY = Key.create(ID_CACHE_KEY); + @Nullable + public static String getSchemaId(@NotNull final VirtualFile schemaFile, + @NotNull final Project project) { + String value = JsonSchemaFileValuesIndex.getCachedValue(project, schemaFile, ID_CACHE_KEY); + if (value != null && !JsonSchemaFileValuesIndex.NULL.equals(value)) return JsonSchemaService.normalizeId(value); + String obsoleteValue = JsonSchemaFileValuesIndex.getCachedValue(project, schemaFile, OBSOLETE_ID_CACHE_KEY); + if (obsoleteValue != null && !JsonSchemaFileValuesIndex.NULL.equals(obsoleteValue)) return JsonSchemaService.normalizeId(obsoleteValue); + if (JsonSchemaFileValuesIndex.NULL.equals(value) || JsonSchemaFileValuesIndex.NULL.equals(obsoleteValue)) return null; + + final String result = computeForFile(schemaFile, project, JsonCachedValues::fetchSchemaId, SCHEMA_ID_CACHE_KEY); + return result == null ? null : JsonSchemaService.normalizeId(result); + } + + @Nullable + private static <T> T computeForFile(@NotNull final VirtualFile schemaFile, + @NotNull final Project project, + @NotNull Function<? super PsiFile, ? extends T> eval, + @NotNull Key<CachedValue<T>> cacheKey) { + final PsiFile psiFile = resolveFile(schemaFile, project); + if (!(psiFile instanceof JsonFile)) return null; + return CachedValueProviderOnPsiFile.getOrCompute(psiFile, eval, cacheKey); + } + + static final String ID_PATHS_CACHE_KEY = "JsonSchemaIdToPointerCache"; + private static final Key<CachedValue<Map<String, String>>> SCHEMA_ID_PATHS_CACHE_KEY = Key.create(ID_PATHS_CACHE_KEY); + public static Collection<String> getAllIdsInFile(PsiFile psiFile) { + final Map<String, String> map = CachedValueProviderOnPsiFile.getOrCompute(psiFile, JsonCachedValues::computeIdsMap, SCHEMA_ID_PATHS_CACHE_KEY); + return map == null ? ContainerUtil.emptyList() : map.keySet(); + } + @Nullable + public static String resolveId(PsiFile psiFile, String id) { + final Map<String, String> map = CachedValueProviderOnPsiFile.getOrCompute(psiFile, JsonCachedValues::computeIdsMap, SCHEMA_ID_PATHS_CACHE_KEY); + return map == null ? null : map.get(id); + } + + private static Map<String, String> computeIdsMap(PsiFile file) { + return SyntaxTraverser.psiTraverser(file).filter(JsonProperty.class).filter(p -> "$id".equals(p.getName())) + .filter(p -> p.getValue() instanceof JsonStringLiteral) + .toMap(p -> ((JsonStringLiteral)Objects.requireNonNull(p.getValue())).getValue(), + p -> JsonQualifiedNameProvider.generateQualifiedName(p.getParent(), JsonQualifiedNameKind.JsonPointer)); + } + + @Nullable + static String fetchSchemaId(@NotNull PsiFile psiFile) { + if (!(psiFile instanceof JsonFile)) return null; + final Map<String, String> props = JsonSchemaFileValuesIndex.readTopLevelProps(psiFile.getFileType(), psiFile.getText()); + final String id = props.get(ID_CACHE_KEY); + if (id != null && !JsonSchemaFileValuesIndex.NULL.equals(id)) return id; + final String obsoleteId = props.get(OBSOLETE_ID_CACHE_KEY); + return obsoleteId == null || JsonSchemaFileValuesIndex.NULL.equals(obsoleteId) ? null : obsoleteId; + } + + + private static final Key<CachedValue<List<Pair<Collection<String>, String>>>> SCHEMA_CATALOG_CACHE_KEY = Key.create("JsonSchemaCatalogCache"); + @Nullable + public static List<Pair<Collection<String>, String>> getSchemaCatalog(@NotNull final VirtualFile catalog, + @NotNull final Project project) { + if (!catalog.isValid()) return null; + return computeForFile(catalog, project, JsonCachedValues::computeSchemaCatalog, SCHEMA_CATALOG_CACHE_KEY); + } + + private static List<Pair<Collection<String>, String>> computeSchemaCatalog(PsiFile catalog) { + if (!catalog.isValid()) return null; + JsonValue value = AstLoadingFilter.forceAllowTreeLoading(catalog, () -> ((JsonFile)catalog).getTopLevelValue()); + if (!(value instanceof JsonObject)) return null; + + JsonProperty schemas = ((JsonObject)value).findProperty("schemas"); + if (schemas == null) return null; + + JsonValue schemasValue = schemas.getValue(); + if (!(schemasValue instanceof JsonArray)) return null; + List<Pair<Collection<String>, String>> catalogMap = ContainerUtil.newArrayList(); + fillMap((JsonArray)schemasValue, catalogMap); + return catalogMap; + } + + private static void fillMap(@NotNull JsonArray array, @NotNull List<Pair<Collection<String>, String>> catalogMap) { + for (JsonValue value: array.getValueList()) { + if (!(value instanceof JsonObject)) continue; + JsonProperty fileMatch = ((JsonObject)value).findProperty("fileMatch"); + Collection<String> masks = fileMatch == null ? ContainerUtil.emptyList() : resolveMasks(fileMatch.getValue()); + JsonProperty url = ((JsonObject)value).findProperty("url"); + if (url == null) continue; + JsonValue urlValue = url.getValue(); + if (urlValue instanceof JsonStringLiteral) { + String urlStringValue = ((JsonStringLiteral)urlValue).getValue(); + if (!StringUtil.isEmpty(urlStringValue)) { + catalogMap.add(Pair.create(masks, urlStringValue)); + } + } + } + } + + @NotNull + private static Collection<String> resolveMasks(@Nullable JsonValue value) { + if (value instanceof JsonStringLiteral) { + return ContainerUtil.createMaybeSingletonList(((JsonStringLiteral)value).getValue()); + } + + if (value instanceof JsonArray) { + List<String> strings = ContainerUtil.newArrayList(); + for (JsonValue val: ((JsonArray)value).getValueList()) { + if (val instanceof JsonStringLiteral) { + strings.add(((JsonStringLiteral)val).getValue()); + } + } + return strings; + } + + return ContainerUtil.emptyList(); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java b/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java new file mode 100644 index 00000000..0ef6403c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonComplianceCheckerOptions.java @@ -0,0 +1,13 @@ +// 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 com.jetbrains.jsonSchema.impl; + +public class JsonComplianceCheckerOptions { + public static final JsonComplianceCheckerOptions RELAX_ENUM_CHECK = new JsonComplianceCheckerOptions(true); + + private final boolean isCaseInsensitiveEnumCheck; + public JsonComplianceCheckerOptions(boolean caseInsensitiveEnumCheck) {isCaseInsensitiveEnumCheck = caseInsensitiveEnumCheck;} + + public boolean isCaseInsensitiveEnumCheck() { + return isCaseInsensitiveEnumCheck; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java b/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java new file mode 100644 index 00000000..660ae3bd --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonErrorPriority.java @@ -0,0 +1,10 @@ +// 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 com.jetbrains.jsonSchema.impl; + +public enum JsonErrorPriority { + NOT_SCHEMA, + TYPE_MISMATCH, + MEDIUM_PRIORITY, + MISSING_PROPS, + LOW_PRIORITY +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java new file mode 100644 index 00000000..1262aa5c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonOriginalPsiWalker.java @@ -0,0 +1,217 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.codeInsight.completion.CompletionUtil; +import com.intellij.json.JsonDialectUtil; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.psi.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ThreeState; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter; +import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter; +import com.jetbrains.jsonSchema.impl.adapters.JsonJsonPropertyAdapter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 2/16/2017. + */ +public class JsonOriginalPsiWalker implements JsonLikePsiWalker { + public static final JsonOriginalPsiWalker INSTANCE = new JsonOriginalPsiWalker(); + + public boolean handles(@NotNull PsiElement element) { + PsiElement parent = element.getParent(); + return parent != null && (element instanceof JsonElement || element instanceof LeafPsiElement && parent instanceof JsonElement) + && JsonDialectUtil.isStandardJson(CompletionUtil.getOriginalOrSelf(parent)); + } + + @Override + public ThreeState isName(PsiElement element) { + final PsiElement parent = element.getParent(); + if (parent instanceof JsonObject) { + return ThreeState.YES; + } else if (parent instanceof JsonProperty) { + return PsiTreeUtil.isAncestor(((JsonProperty)parent).getNameElement(), element, false) ? ThreeState.YES : ThreeState.NO; + } + return ThreeState.NO; + } + + @Override + public boolean isPropertyWithValue(@NotNull PsiElement element) { + return element instanceof JsonProperty && ((JsonProperty)element).getValue() != null; + } + + @Override + public PsiElement goUpToCheckable(@NotNull PsiElement element) { + PsiElement current = element; + while (current != null && !(current instanceof PsiFile)) { + if (current instanceof JsonValue || current instanceof JsonProperty) { + return current; + } + current = current.getParent(); + } + return null; + } + + @Nullable + @Override + public List<JsonSchemaVariantsTreeBuilder.Step> findPosition(@NotNull PsiElement element, boolean forceLastTransition) { + final List<JsonSchemaVariantsTreeBuilder.Step> steps = new ArrayList<>(); + PsiElement current = element; + while (! (current instanceof PsiFile)) { + final PsiElement position = current; + current = current.getParent(); + if (current instanceof JsonArray) { + JsonArray array = (JsonArray)current; + final List<JsonValue> list = array.getValueList(); + int idx = -1; + for (int i = 0; i < list.size(); i++) { + final JsonValue value = list.get(i); + if (value.equals(position)) { + idx = i; + break; + } + } + steps.add(JsonSchemaVariantsTreeBuilder.Step.createArrayElementStep(idx)); + } else if (current instanceof JsonProperty) { + final String propertyName = ((JsonProperty)current).getName(); + 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 || forceLastTransition) { + steps.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(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 || forceLastTransition) { + final String propertyName = ((JsonProperty)position).getName(); + steps.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(propertyName)); + } + } else if (current instanceof PsiFile) { + break; + } else { + return null;//something went wrong + } + } + Collections.reverse(steps); + return steps; + } + + @Override + public boolean isNameQuoted() { + return true; + } + + @Override + public boolean onlyDoubleQuotesForStringLiterals() { + return true; + } + + @Override + public boolean hasPropertiesBehindAndNoComma(@NotNull PsiElement element) { + PsiElement current = element instanceof JsonProperty ? element : PsiTreeUtil.getParentOfType(element, JsonProperty.class); + while (current != null && current.getNode().getElementType() != JsonElementTypes.COMMA) { + current = current.getNextSibling(); + } + int commaOffset = current == null ? Integer.MAX_VALUE : current.getTextRange().getStartOffset(); + final int offset = element.getTextRange().getStartOffset(); + final JsonObject object = PsiTreeUtil.getParentOfType(element, JsonObject.class); + if (object != null) { + for (JsonProperty property : object.getPropertyList()) { + final int pOffset = property.getTextRange().getStartOffset(); + if (pOffset >= offset && !PsiTreeUtil.isAncestor(property, element, false)) { + return pOffset < commaOffset; + } + } + } + return false; + } + + @Override + public Set<String> getPropertyNamesOfParentObject(@NotNull PsiElement originalPosition, PsiElement computedPosition) { + final JsonObject object = PsiTreeUtil.getParentOfType(originalPosition, JsonObject.class); + if (object != null) { + return object.getPropertyList().stream() + .filter(p -> !isNameQuoted() || p.getNameElement() instanceof JsonStringLiteral) + .map(p -> StringUtil.unquoteString(p.getName())).collect(Collectors.toSet()); + } + return Collections.emptySet(); + } + + @Override + public JsonPropertyAdapter getParentPropertyAdapter(@NotNull PsiElement element) { + final JsonProperty property = PsiTreeUtil.getParentOfType(element, JsonProperty.class, false); + if (property == null) return null; + return new JsonJsonPropertyAdapter(property); + } + + @Override + public boolean isTopJsonElement(@NotNull PsiElement element) { + return element instanceof PsiFile; + } + + @Nullable + @Override + public JsonValueAdapter createValueAdapter(@NotNull PsiElement element) { + return element instanceof JsonValue ? JsonJsonPropertyAdapter.createAdapterByType((JsonValue)element) : null; + } + + @Override + public QuickFixAdapter getQuickFixAdapter(Project project) { + return new QuickFixAdapter() { + private final JsonElementGenerator myGenerator = new JsonElementGenerator(project); + @Nullable + @Override + public PsiElement getPropertyValue(PsiElement property) { + assert property instanceof JsonProperty; + return ((JsonProperty)property).getValue(); + } + + @NotNull + @Override + public String getPropertyName(PsiElement property) { + assert property instanceof JsonProperty; + return ((JsonProperty)property).getName(); + } + + @NotNull + @Override + public PsiElement createProperty(@NotNull String name, @NotNull String value) { + return myGenerator.createProperty(name, value); + } + + @Override + public boolean ensureComma(PsiElement backward, PsiElement self, PsiElement newElement) { + if (backward instanceof JsonProperty) { + self.addAfter(myGenerator.createComma(), backward); + return true; + } + return false; + } + + @Override + public void removeIfComma(PsiElement forward) { + if (forward instanceof LeafPsiElement && ((LeafPsiElement)forward).getElementType() == JsonElementTypes.COMMA) { + forward.delete(); + } + } + + @Override + public boolean fixWhitespaceBefore(PsiElement initialElement, PsiElement element) { + return true; + } + }; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java new file mode 100644 index 00000000..e0a9ed84 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonPointerReferenceProvider.java @@ -0,0 +1,262 @@ +/* + * 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.impl; + +import com.intellij.codeInsight.completion.CompletionUtil; +import com.intellij.codeInsight.completion.CompletionUtilCore; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.icons.AllIcons; +import com.intellij.json.JsonFileType; +import com.intellij.json.psi.*; +import com.intellij.openapi.paths.WebReference; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceProvider; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileInfoManager; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet; +import com.intellij.util.ArrayUtil; +import com.intellij.util.ProcessingContext; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.JsonPointerResolver; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.List; + +import static com.jetbrains.jsonSchema.JsonPointerUtil.*; +import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath; + +/** + * @author Irina.Chernushina on 3/31/2016. + */ +public class JsonPointerReferenceProvider extends PsiReferenceProvider { + private final boolean myOnlyFilePart; + + public JsonPointerReferenceProvider(boolean onlyFilePart) { + myOnlyFilePart = onlyFilePart; + } + + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { + if (!(element instanceof JsonStringLiteral)) return PsiReference.EMPTY_ARRAY; + List<PsiReference> refs = ContainerUtil.newArrayList(); + + List<Pair<TextRange, String>> fragments = ((JsonStringLiteral)element).getTextFragments(); + if (fragments.size() != 1) return PsiReference.EMPTY_ARRAY; + Pair<TextRange, String> fragment = fragments.get(0); + String originalText = element.getText(); + int hash = originalText.indexOf('#'); + final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(fragment.second); + String id = splitter.getSchemaId(); + if (id != null) { + if (id.startsWith("#")) { + refs.add(new JsonSchemaIdReference((JsonValue)element, id)); + } + else { + addFileOrWebReferences(element, refs, hash, id); + } + } + if (!myOnlyFilePart) { + String relativePath = normalizeSlashes(JsonSchemaService.normalizeId(splitter.getRelativePath())); + List<String> parts1 = split(relativePath); + String[] strings = ContainerUtil.toArray(parts1, String[]::new); + List<String> parts2 = split(normalizeSlashes(originalText.substring(hash + 1))); + if (strings.length == parts2.size()) { + int start = hash + 2; + for (int i = 0; i < parts2.size(); i++) { + int length = parts2.get(i).length(); + if (i == parts2.size() - 1) length--; + refs.add(new JsonPointerReference((JsonValue)element, new TextRange(start, start + length), + (id == null ? "" : id) + "#/" + StringUtil.join(strings, 0, i + 1, "/"))); + start += length + 1; + } + } + } + return refs.size() == 0 ? PsiReference.EMPTY_ARRAY : ContainerUtil.toArray(refs, PsiReference[]::new); + } + + private void addFileOrWebReferences(@NotNull PsiElement element, List<PsiReference> refs, int hashIndex, String id) { + if (isHttpPath(id)) { + refs.add(new WebReference(element, new TextRange(1, hashIndex >= 0 ? hashIndex : id.length() + 1), id)); + return; + } + + ContainerUtil.addAll(refs, new FileReferenceSet(id, element, 1, null, true, + true, new JsonFileType[]{JsonFileType.INSTANCE}) { + @Override + public boolean isEmptyPathAllowed() { + return true; + } + + @Override + protected boolean isSoft() { + return true; + } + + @Override + public FileReference createFileReference(TextRange range, int index, String text) { + if (hashIndex != -1 && range.getStartOffset() >= hashIndex) return null; + if (hashIndex != -1 && range.getEndOffset() > hashIndex) { + range = new TextRange(range.getStartOffset(), hashIndex); + text = text.substring(0, text.indexOf('#')); + } + return new FileReference(this, range, index, text) { + @Override + protected Object createLookupItem(PsiElement candidate) { + return FileInfoManager.getFileLookupItem(candidate); + } + }; + } + }.getAllReferences()); + } + + @Nullable + static PsiElement resolveForPath(PsiElement element, String text, boolean alwaysRoot) { + final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject()); + final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(text); + VirtualFile schemaFile = CompletionUtil.getOriginalOrSelf(element.getContainingFile()).getVirtualFile(); + if (splitter.isAbsolute()) { + assert splitter.getSchemaId() != null; + schemaFile = service.findSchemaFileByReference(splitter.getSchemaId(), schemaFile); + if (schemaFile == null) return null; + } + + final String normalized = JsonSchemaService.normalizeId(splitter.getRelativePath()); + if (!alwaysRoot && (StringUtil.isEmptyOrSpaces(normalized) || split(normalizeSlashes(normalized)).size() == 0)) { + return element.getManager().findFile(schemaFile); + } + final List<String> chain = split(normalizeSlashes(normalized)); + final JsonSchemaObject schemaObject = service.getSchemaObjectForSchemaFile(schemaFile); + if (schemaObject == null) return null; + + return new JsonPointerResolver(schemaObject.getJsonObject(), StringUtil.join(chain, "/")).resolve(); + } + + public static class JsonSchemaIdReference extends JsonSchemaBaseReference<JsonValue> { + private final String myText; + + private JsonSchemaIdReference(JsonValue element, String text) { + super(element, getRange(element)); + myText = text; + } + + @NotNull + private static TextRange getRange(JsonValue element) { + final TextRange range = element.getTextRange().shiftLeft(element.getTextOffset()); + return new TextRange(range.getStartOffset() + 1, range.getEndOffset() - 1); + } + + @Nullable + @Override + public PsiElement resolveInner() { + final String id = JsonCachedValues.resolveId(myElement.getContainingFile(), myText); + if (id == null) return null; + return resolveForPath(myElement, "#" + id, false); + } + + @NotNull + @Override + public Object[] getVariants() { + return JsonCachedValues.getAllIdsInFile(myElement.getContainingFile()).toArray(); + } + } + + private static class JsonPointerReference extends JsonSchemaBaseReference<JsonValue> { + private final String myFullPath; + + JsonPointerReference(JsonValue element, TextRange textRange, String curPath) { + super(element, textRange); + myFullPath = curPath; + } + + @NotNull + @Override + public String getCanonicalText() { + return myFullPath; + } + + @Nullable + @Override + public PsiElement resolveInner() { + return resolveForPath(myElement, getCanonicalText(), false); + } + + @Override + protected boolean isIdenticalTo(JsonSchemaBaseReference that) { + return super.isIdenticalTo(that) && getRangeInElement().equals(that.getRangeInElement()); + } + + @NotNull + @Override + public Object[] getVariants() { + String text = getCanonicalText(); + int index = text.indexOf(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED); + if (index >= 0) { + String part = text.substring(0, index); + text = prepare(part); + String prefix = null; + PsiElement element = resolveForPath(myElement, text, true); + int indexOfSlash = part.lastIndexOf('/'); + if (indexOfSlash != -1 && indexOfSlash < text.length() - 1 && indexOfSlash < index) { + prefix = text.substring(indexOfSlash + 1); + element = resolveForPath(myElement, prepare(text.substring(0, indexOfSlash)), true); + } + String finalPrefix = prefix; + if (element instanceof JsonObject) { + return ((JsonObject)element).getPropertyList().stream() + .filter(p -> p.getValue() instanceof JsonContainer && (finalPrefix == null || p.getName().startsWith(finalPrefix))) + .map(p -> LookupElementBuilder.create(p, escapeForJsonPointer(p.getName())) + .withIcon(getIcon(p.getValue()))).toArray(); + } + else if (element instanceof JsonArray) { + List<JsonValue> list = ((JsonArray)element).getValueList(); + List<Object> values = ContainerUtil.newLinkedList(); + for (int i = 0; i < list.size(); i++) { + String stringValue = String.valueOf(i); + if (prefix != null && !stringValue.startsWith(prefix)) continue; + values.add(LookupElementBuilder.create(stringValue).withIcon(getIcon(list.get(i)))); + } + return ContainerUtil.toArray(values, Object[]::new); + } + } + + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + private static Icon getIcon(JsonValue value) { + if (value instanceof JsonObject) { + return AllIcons.Json.Object; + } + else if (value instanceof JsonArray) { + return AllIcons.Json.Array; + } + return AllIcons.Nodes.Property; + } + } + + @NotNull + private static String prepare(String part) { + return part.endsWith("#/") ? part : StringUtil.trimEnd(part, '/'); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java new file mode 100644 index 00000000..44cd3704 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonRequiredPropsReferenceProvider.java @@ -0,0 +1,62 @@ +// 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 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.json.psi.JsonValue; +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 org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public class JsonRequiredPropsReferenceProvider extends PsiReferenceProvider { + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { + return new PsiReference[] {new JsonRequiredPropReference((JsonStringLiteral)element)}; + } + + @Nullable + public static JsonObject findPropertiesObject(PsiElement element) { + PsiElement parent = getParentSafe(getParentSafe(getParentSafe(element))); + if (!(parent instanceof JsonObject)) return null; + Optional<JsonProperty> propertiesProp = + ((JsonObject)parent).getPropertyList().stream().filter(p -> "properties".equals(p.getName())).findFirst(); + if (propertiesProp.isPresent()) { + JsonValue value = propertiesProp.get().getValue(); + if (value instanceof JsonObject) { + return (JsonObject)value; + } + } + return null; + } + + private static PsiElement getParentSafe(@Nullable PsiElement element) { + return element == null ? null : element.getParent(); + } + + private static class JsonRequiredPropReference extends JsonSchemaBaseReference<JsonStringLiteral> { + JsonRequiredPropReference(JsonStringLiteral element) { + super(element, ElementManipulators.getValueTextRange(element)); + } + + @Nullable + @Override + public PsiElement resolveInner() { + JsonObject propertiesObject = findPropertiesObject(getElement()); + if (propertiesObject != null) { + String name = getElement().getValue(); + for (JsonProperty property : propertiesObject.getPropertyList()) { + if (name.equals(property.getName())) return property; + } + } + return null; + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java new file mode 100644 index 00000000..1fb1f5a2 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaAnnotatorChecker.java @@ -0,0 +1,1147 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.json.JsonBundle; +import com.intellij.json.psi.JsonContainer; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.util.ObjectUtils; +import com.intellij.util.SmartList; +import com.intellij.util.ThreeState; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.MultiMap; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.extension.adapters.JsonArrayValueAdapter; +import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter; +import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter; +import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 4/25/2017. + */ +class JsonSchemaAnnotatorChecker { + private static final Set<JsonSchemaType> PRIMITIVE_TYPES = + ContainerUtil.set(JsonSchemaType._integer, JsonSchemaType._number, JsonSchemaType._boolean, JsonSchemaType._string, JsonSchemaType._null); + private final Map<PsiElement, JsonValidationError> myErrors; + private final JsonComplianceCheckerOptions myOptions; + private boolean myHadTypeError; + private static final String ENUM_MISMATCH_PREFIX = "Value should be one of: "; + + protected JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions options) { + myOptions = options; + myErrors = new HashMap<>(); + } + + public Map<PsiElement, JsonValidationError> getErrors() { + return myErrors; + } + + public boolean isHadTypeError() { + return myHadTypeError; + } + + public static JsonSchemaAnnotatorChecker checkByMatchResult(@NotNull JsonValueAdapter elementToCheck, + @NotNull final MatchResult result, + @NotNull JsonComplianceCheckerOptions options) { + final List<JsonSchemaAnnotatorChecker> checkers = new ArrayList<>(); + if (result.myExcludingSchemas.isEmpty() && result.mySchemas.size() == 1) { + final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options); + checker.checkByScheme(elementToCheck, result.mySchemas.iterator().next()); + checkers.add(checker); + } + else { + if (!result.mySchemas.isEmpty()) { + checkers.add(processSchemasVariants(result.mySchemas, elementToCheck, false, options).getSecond()); + } + if (!result.myExcludingSchemas.isEmpty()) { + // we can have several oneOf groups, each about, for instance, a part of properties + // - then we should allow properties from neighbour schemas (even if additionalProperties=false) + final List<JsonSchemaAnnotatorChecker> list = result.myExcludingSchemas.stream() + .map(group -> processSchemasVariants(group, elementToCheck, true, options).getSecond()).collect(Collectors.toList()); + checkers.add(mergeErrors(list, options, result.myExcludingSchemas)); + } + } + if (checkers.isEmpty()) return null; + if (checkers.size() == 1) return checkers.get(0); + + return checkers.stream() + .filter(checker -> !checker.isHadTypeError()) + .findFirst() + .orElse(checkers.get(0)); + } + + private static JsonSchemaAnnotatorChecker mergeErrors(@NotNull List<JsonSchemaAnnotatorChecker> list, + @NotNull JsonComplianceCheckerOptions options, + List<Collection<? extends JsonSchemaObject>> excludingSchemas) { + final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options); + + for (JsonSchemaAnnotatorChecker ch: list) { + for (Map.Entry<PsiElement, JsonValidationError> element: ch.myErrors.entrySet()) { + JsonValidationError error = element.getValue(); + if (error.getFixableIssueKind() == JsonValidationError.FixableIssueKind.ProhibitedProperty) { + String propertyName = ((JsonValidationError.ProhibitedPropertyIssueData)error.getIssueData()).propertyName; + boolean skip = false; + for (Collection<? extends JsonSchemaObject> objects : excludingSchemas) { + Set<String> keys = objects.stream().map(o -> o.getProperties().keySet()).flatMap(Set::stream).collect(Collectors.toSet()); + if (keys.contains(propertyName)) skip = true; + } + if (skip) continue; + } + checker.myErrors.put(element.getKey(), error); + } + } + return checker; + } + + private void error(final String error, final PsiElement holder, + JsonErrorPriority priority) { + error(error, holder, JsonValidationError.FixableIssueKind.None, null, priority); + } + + private void error(final PsiElement newHolder, JsonValidationError error) { + error(error.getMessage(), newHolder, error.getFixableIssueKind(), error.getIssueData(), error.getPriority()); + } + + private void error(final String error, final PsiElement holder, + JsonValidationError.FixableIssueKind fixableIssueKind, + JsonValidationError.IssueData data, + JsonErrorPriority priority) { + if (myErrors.containsKey(holder)) return; + myErrors.put(holder, new JsonValidationError(error, fixableIssueKind, data, priority)); + } + + private void typeError(final @NotNull PsiElement value, final @NotNull JsonSchemaType... allowedTypes) { + if (allowedTypes.length > 0) { + if (allowedTypes.length == 1) { + error(String.format("Type is not allowed. Expected: %s.", allowedTypes[0].getName()), value, + JsonValidationError.FixableIssueKind.ProhibitedType, + new JsonValidationError.TypeMismatchIssueData(allowedTypes), + JsonErrorPriority.TYPE_MISMATCH); + } else { + final String typesText = Arrays.stream(allowedTypes) + .map(JsonSchemaType::getName) + .distinct() + .sorted(Comparator.naturalOrder()) + .collect(Collectors.joining(", ")); + error(String.format("Type is not allowed. Expected one of: %s.", typesText), value, + JsonValidationError.FixableIssueKind.ProhibitedType, + new JsonValidationError.TypeMismatchIssueData(allowedTypes), + JsonErrorPriority.TYPE_MISMATCH); + } + } else { + error("Type is not allowed", value, JsonErrorPriority.TYPE_MISMATCH); + } + myHadTypeError = true; + } + + public void checkByScheme(@NotNull JsonValueAdapter value, @NotNull JsonSchemaObject schema) { + final JsonSchemaType type = JsonSchemaType.getType(value); + checkForEnum(value.getDelegate(), schema); + boolean checkedNumber = false; + boolean checkedString = false; + boolean checkedArray = false; + boolean checkedObject = false; + if (type != null) { + JsonSchemaType schemaType = getMatchingSchemaType(schema, type); + if (schemaType != null && !schemaType.equals(type)) { + typeError(value.getDelegate(), schemaType); + } + else { + if (JsonSchemaType._string_number.equals(type)) { + checkNumber(value.getDelegate(), schema, type); + checkedNumber = true; + checkString(value.getDelegate(), schema); + checkedString = true; + } + else if (JsonSchemaType._number.equals(type) || JsonSchemaType._integer.equals(type)) { + checkNumber(value.getDelegate(), schema, type); + checkedNumber = true; + } + else if (JsonSchemaType._string.equals(type)) { + checkString(value.getDelegate(), schema); + checkedString = true; + } + else if (JsonSchemaType._array.equals(type)) { + checkArray(value, schema); + checkedArray = true; + } + else if (JsonSchemaType._object.equals(type)) { + checkObject(value, schema); + checkedObject = true; + } + } + } + + if ((!myHadTypeError || myErrors.isEmpty()) && !value.isShouldBeIgnored()) { + PsiElement delegate = value.getDelegate(); + if (!checkedNumber && schema.hasNumericChecks() && value.isNumberLiteral()) { + checkNumber(delegate, schema, JsonSchemaType._number); + } + if (!checkedString && schema.hasStringChecks() && value.isStringLiteral()) { + checkString(delegate, schema); + checkedString = true; + } + if (!checkedArray && schema.hasArrayChecks() && value.isArray()) { + checkArray(value, schema); + checkedArray = true; + } + if (hasMinMaxLengthChecks(schema)) { + if (value.isStringLiteral()) { + if (!checkedString) { + checkString(delegate, schema); + } + } + else if (value.isArray()) { + if (!checkedArray) { + checkArray(value, schema); + } + } + } + if (!checkedObject && schema.hasObjectChecks() && value.isObject()) { + checkObject(value, schema); + } + } + + if (schema.getNot() != null) { + final MatchResult result = new JsonSchemaResolver(schema.getNot()).detailedResolve(); + if (result.mySchemas.isEmpty() && result.myExcludingSchemas.isEmpty()) return; + + // 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.getJsonObject().equals(s.getJsonObject())) || + result.myExcludingSchemas.stream().flatMap(Collection::stream) + .anyMatch(s -> schema.getJsonObject().equals(s.getJsonObject()))) return; + + final JsonSchemaAnnotatorChecker checker = checkByMatchResult(value, result, myOptions); + if (checker == null || checker.isCorrect()) error("Validates against 'not' schema", value.getDelegate(), JsonErrorPriority.NOT_SCHEMA); + } + + if (schema.getIf() != null) { + MatchResult result = new JsonSchemaResolver(schema.getIf()).detailedResolve(); + if (result.mySchemas.isEmpty() && result.myExcludingSchemas.isEmpty()) return; + + final JsonSchemaAnnotatorChecker checker = checkByMatchResult(value, result, myOptions); + if (checker != null) { + if (checker.isCorrect()) { + JsonSchemaObject then = schema.getThen(); + if (then == null) { + error("Validates against 'if' branch but no 'then' branch is present", value.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + } + else { + checkObjectBySchemaRecordErrors(then, value); + } + } + else { + JsonSchemaObject schemaElse = schema.getElse(); + if (schemaElse == null) { + error("Validates counter 'if' branch but no 'else' branch is present", value.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + } + else { + checkObjectBySchemaRecordErrors(schemaElse, value); + } + } + } + } + } + + private void checkObjectBySchemaRecordErrors(@NotNull JsonSchemaObject schema, @NotNull JsonValueAdapter object) { + final JsonSchemaAnnotatorChecker checker = checkByMatchResult(object, new JsonSchemaResolver(schema).detailedResolve(), myOptions); + if (checker != null) { + myHadTypeError = checker.isHadTypeError(); + myErrors.putAll(checker.getErrors()); + } + } + + private void checkObject(@NotNull JsonValueAdapter value, @NotNull JsonSchemaObject schema) { + final JsonObjectValueAdapter object = value.getAsObject(); + if (object == null) return; + + final List<JsonPropertyAdapter> propertyList = object.getPropertyList(); + final Set<String> set = new HashSet<>(); + for (JsonPropertyAdapter property : propertyList) { + final String name = StringUtil.notNullize(property.getName()); + JsonSchemaObject propertyNamesSchema = schema.getPropertyNamesSchema(); + if (propertyNamesSchema != null) { + JsonValueAdapter nameValueAdapter = property.getNameValueAdapter(); + if (nameValueAdapter != null) { + checkByScheme(nameValueAdapter, propertyNamesSchema); + } + } + + final JsonSchemaVariantsTreeBuilder.Step step = JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(name); + final Pair<ThreeState, JsonSchemaObject> pair = step.step(schema, true); + if (ThreeState.NO.equals(pair.getFirst()) && !set.contains(name)) { + error(JsonBundle.message("json.schema.annotation.not.allowed.property", name), property.getDelegate(), + JsonValidationError.FixableIssueKind.ProhibitedProperty, + new JsonValidationError.ProhibitedPropertyIssueData(name), JsonErrorPriority.LOW_PRIORITY); + } + else if (ThreeState.UNSURE.equals(pair.getFirst())) { + for (JsonValueAdapter propertyValue : property.getValues()) { + checkObjectBySchemaRecordErrors(pair.getSecond(), propertyValue); + } + } + set.add(name); + } + + if (object.shouldCheckIntegralRequirements()) { + final Set<String> required = schema.getRequired(); + if (required != null) { + HashSet<String> requiredNames = ContainerUtil.newHashSet(required); + requiredNames.removeAll(set); + if (!requiredNames.isEmpty()) { + JsonValidationError.MissingMultiplePropsIssueData data = createMissingPropertiesData(schema, requiredNames); + error("Missing required " + data.getMessage(false), value.getDelegate(), JsonValidationError.FixableIssueKind.MissingProperty, data, + JsonErrorPriority.MISSING_PROPS); + } + } + if (schema.getMinProperties() != null && propertyList.size() < schema.getMinProperties()) { + error("Number of properties is less than " + schema.getMinProperties(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + } + if (schema.getMaxProperties() != null && propertyList.size() > schema.getMaxProperties()) { + error("Number of properties is greater than " + schema.getMaxProperties(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + } + final Map<String, List<String>> dependencies = schema.getPropertyDependencies(); + if (dependencies != null) { + for (Map.Entry<String, List<String>> entry : dependencies.entrySet()) { + if (set.contains(entry.getKey())) { + final List<String> list = entry.getValue(); + HashSet<String> deps = ContainerUtil.newHashSet(list); + deps.removeAll(set); + if (!deps.isEmpty()) { + JsonValidationError.MissingMultiplePropsIssueData data = createMissingPropertiesData(schema, deps); + error("Dependency is violated: " + data.getMessage(false) + " must be specified, since '" + entry.getKey() + "' is specified", + value.getDelegate(), + JsonValidationError.FixableIssueKind.MissingProperty, + data, JsonErrorPriority.MISSING_PROPS); + } + } + } + } + final Map<String, JsonSchemaObject> schemaDependencies = schema.getSchemaDependencies(); + if (schemaDependencies != null) { + for (Map.Entry<String, JsonSchemaObject> entry : schemaDependencies.entrySet()) { + if (set.contains(entry.getKey())) { + checkObjectBySchemaRecordErrors(entry.getValue(), value); + } + } + } + } + + validateAsJsonSchema(object.getDelegate()); + } + + @Nullable + private static Object getDefaultValueFromEnum(@NotNull JsonSchemaObject propertySchema, @NotNull Ref<Integer> enumCount) { + List<Object> enumValues = propertySchema.getEnum(); + if (enumValues != null) { + enumCount.set(enumValues.size()); + if (enumValues.size() == 1) { + Object defaultObject = enumValues.get(0); + return defaultObject instanceof String ? StringUtil.unquoteString((String)defaultObject) : defaultObject; + } + } + return null; + } + + @NotNull + private static JsonValidationError.MissingMultiplePropsIssueData createMissingPropertiesData(@NotNull JsonSchemaObject schema, + HashSet<String> requiredNames) { + List<JsonValidationError.MissingPropertyIssueData> allProps = ContainerUtil.newArrayList(); + for (String req: requiredNames) { + JsonSchemaObject propertySchema = resolvePropertySchema(schema, req); + Object defaultValue = propertySchema == null ? null : propertySchema.getDefault(); + Ref<Integer> enumCount = Ref.create(0); + + JsonSchemaType type = null; + + if (propertySchema != null) { + MatchResult result = null; + Object valueFromEnum = getDefaultValueFromEnum(propertySchema, enumCount); + if (valueFromEnum != null) { + defaultValue = valueFromEnum; + } + else { + result = new JsonSchemaResolver(propertySchema).detailedResolve(); + if (result.mySchemas.size() == 1) { + valueFromEnum = getDefaultValueFromEnum(result.mySchemas.get(0), enumCount); + if (valueFromEnum != null) { + defaultValue = valueFromEnum; + } + } + } + type = propertySchema.getType(); + if (type == null) { + if (result == null) { + result = new JsonSchemaResolver(propertySchema).detailedResolve(); + } + if (result.mySchemas.size() == 1) { + type = result.mySchemas.get(0).getType(); + } + } + } + allProps.add(new JsonValidationError.MissingPropertyIssueData(req, + type, + defaultValue, + enumCount.get())); + } + + return new JsonValidationError.MissingMultiplePropsIssueData(allProps); + } + + private static JsonSchemaObject resolvePropertySchema(@NotNull JsonSchemaObject schema, String req) { + if (schema.getProperties().containsKey(req)) { + return schema.getProperties().get(req); + } + else { + JsonSchemaObject propertySchema = schema.getMatchingPatternPropertySchema(req); + if (propertySchema != null) { + return propertySchema; + } + else { + JsonSchemaObject additionalPropertiesSchema = schema.getAdditionalPropertiesSchema(); + if (additionalPropertiesSchema != null) { + return additionalPropertiesSchema; + } + } + } + return null; + } + + private void validateAsJsonSchema(@NotNull PsiElement objElement) { + final JsonObject object = ObjectUtils.tryCast(objElement, JsonObject.class); + if (object == null) return; + + if (!JsonSchemaService.isSchemaFile(objElement.getContainingFile())) { + return; + } + + final VirtualFile schemaFile = object.getContainingFile().getVirtualFile(); + if (schemaFile == null) return; + + final JsonSchemaObject schemaObject = JsonSchemaService.Impl.get(object.getProject()).getSchemaObjectForSchemaFile(schemaFile); + if (schemaObject == null) return; + + final List<JsonSchemaVariantsTreeBuilder.Step> position = JsonOriginalPsiWalker.INSTANCE.findPosition(object, true); + if (position == null) return; + final List<JsonSchemaVariantsTreeBuilder.Step> steps = skipProperties(position); + // !! not root schema, because we validate the schema written in the file itself + final MatchResult result = new JsonSchemaResolver(schemaObject, false, steps).detailedResolve(); + for (JsonSchemaObject s: result.mySchemas) { + reportInvalidPatternProperties(s); + reportPatternErrors(s); + } + result.myExcludingSchemas.stream().flatMap(Collection::stream).filter(s -> schemaFile.equals(s.getSchemaFile())) + .forEach(schema -> { + reportInvalidPatternProperties(schema); + reportPatternErrors(schema); + }); + } + + private void reportPatternErrors(JsonSchemaObject schema) { + for (JsonSchemaObject prop : schema.getProperties().values()) { + final String patternError = prop.getPatternError(); + if (patternError == null || prop.getPattern() == null) { + continue; + } + + final JsonContainer element = prop.getJsonObject(); + if (!(element instanceof JsonObject) || !element.isValid()) { + continue; + } + + final JsonProperty pattern = ((JsonObject)element).findProperty("pattern"); + if (pattern != null) { + error(StringUtil.convertLineSeparators(patternError), pattern.getValue(), JsonErrorPriority.LOW_PRIORITY); + } + } + } + + private void reportInvalidPatternProperties(JsonSchemaObject schema) { + final Map<JsonContainer, String> invalidPatternProperties = schema.getInvalidPatternProperties(); + if (invalidPatternProperties == null) return; + + for (Map.Entry<JsonContainer, String> entry : invalidPatternProperties.entrySet()) { + final JsonContainer element = entry.getKey(); + if (element == null || !element.isValid()) continue; + final PsiElement parent = element.getParent(); + if (parent instanceof JsonProperty) { + error(StringUtil.convertLineSeparators(entry.getValue()), ((JsonProperty)parent).getNameElement(), JsonErrorPriority.LOW_PRIORITY); + } + } + } + + private static List<JsonSchemaVariantsTreeBuilder.Step> skipProperties(List<JsonSchemaVariantsTreeBuilder.Step> position) { + final Iterator<JsonSchemaVariantsTreeBuilder.Step> iterator = position.iterator(); + boolean canSkip = true; + while (iterator.hasNext()) { + final JsonSchemaVariantsTreeBuilder.Step step = iterator.next(); + if (canSkip && step.isFromObject() && JsonSchemaObject.PROPERTIES.equals(step.getName())) { + iterator.remove(); + canSkip = false; + } + else { + canSkip = true; + } + } + return position; + } + + private static boolean checkEnumValue(@NotNull Object object, + @NotNull JsonLikePsiWalker walker, + @Nullable JsonValueAdapter adapter, + @NotNull String text, + @NotNull BiFunction<String, String, Boolean> stringEq) { + if (object instanceof EnumArrayValueWrapper) { + if (adapter instanceof JsonArrayValueAdapter) { + List<JsonValueAdapter> elements = ((JsonArrayValueAdapter)adapter).getElements(); + Object[] values = ((EnumArrayValueWrapper)object).getValues(); + if (elements.size() == values.length) { + for (int i = 0; i < values.length; i++) { + if (!checkEnumValue(values[i], walker, elements.get(i), walker.getNodeTextForValidation(elements.get(i).getDelegate()), stringEq)) return false; + } + return true; + } + } + } + else if (object instanceof EnumObjectValueWrapper) { + if (adapter instanceof JsonObjectValueAdapter) { + List<JsonPropertyAdapter> props = ((JsonObjectValueAdapter)adapter).getPropertyList(); + Map<String, Object> values = ((EnumObjectValueWrapper)object).getValues(); + if (props.size() == values.size()) { + for (JsonPropertyAdapter prop : props) { + if (!values.containsKey(prop.getName())) return false; + for (JsonValueAdapter value : prop.getValues()) { + if (!checkEnumValue(values.get(prop.getName()), walker, value, walker.getNodeTextForValidation(value.getDelegate()), stringEq)) return false; + } + } + + return true; + } + } + } + else { + if (walker.onlyDoubleQuotesForStringLiterals()) { + if (stringEq.apply(object.toString(), text)) return true; + } + else { + if (equalsIgnoreQuotes(object.toString(), text, walker.quotesForStringLiterals(), stringEq)) return true; + } + } + + return false; + } + + private void checkForEnum(PsiElement value, JsonSchemaObject schema) { + List<Object> enumItems = schema.getEnum(); + if (enumItems == null) return; + final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(value, schema); + if (walker == null) return; + final String text = StringUtil.notNullize(walker.getNodeTextForValidation(value)); + BiFunction<String, String, Boolean> eq = myOptions.isCaseInsensitiveEnumCheck() ? String::equalsIgnoreCase : String::equals; + for (Object object : enumItems) { + if (checkEnumValue(object, walker, walker.createValueAdapter(value), text, eq)) return; + } + error(ENUM_MISMATCH_PREFIX + StringUtil.join(enumItems, o -> o.toString(), ", "), value, + JsonValidationError.FixableIssueKind.NonEnumValue, null, JsonErrorPriority.MEDIUM_PRIORITY); + } + + private static boolean equalsIgnoreQuotes(@NotNull final String s1, + @NotNull final String s2, + boolean requireQuotedValues, + BiFunction<String, String, Boolean> eq) { + final boolean quoted1 = StringUtil.isQuotedString(s1); + final boolean quoted2 = StringUtil.isQuotedString(s2); + if (requireQuotedValues && quoted1 != quoted2) return false; + if (requireQuotedValues && !quoted1) return eq.apply(s1, s2); + return eq.apply(StringUtil.unquoteString(s1), StringUtil.unquoteString(s2)); + } + + private void checkArray(JsonValueAdapter value, JsonSchemaObject schema) { + final JsonArrayValueAdapter asArray = value.getAsArray(); + if (asArray == null) return; + final List<JsonValueAdapter> elements = asArray.getElements(); + if (schema.getMinLength() != null && elements.size() < schema.getMinLength()) { + error("Array is shorter than " + schema.getMinLength(), value.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + return; + } + checkArrayItems(value, elements, schema); + } + + @NotNull + private static Pair<JsonSchemaObject, JsonSchemaAnnotatorChecker> processSchemasVariants( + @NotNull final Collection<? extends JsonSchemaObject> collection, + @NotNull final JsonValueAdapter value, boolean isOneOf, JsonComplianceCheckerOptions options) { + + final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(options); + final JsonSchemaType type = JsonSchemaType.getType(value); + JsonSchemaObject selected = null; + if (type == null) { + if (!value.isShouldBeIgnored()) checker.typeError(value.getDelegate(), getExpectedTypes(collection)); + } + else { + final List<JsonSchemaObject> filtered = ContainerUtil.newArrayListWithCapacity(collection.size()); + for (JsonSchemaObject schema: collection) { + if (!areSchemaTypesCompatible(schema, type)) continue; + filtered.add(schema); + } + if (filtered.isEmpty()) checker.typeError(value.getDelegate(), getExpectedTypes(collection)); + else if (filtered.size() == 1) { + selected = filtered.get(0); + checker.checkByScheme(value, selected); + } + else { + if (isOneOf) { + selected = checker.processOneOf(value, filtered); + } + else { + selected = checker.processAnyOf(value, filtered); + } + } + } + return Pair.create(selected, checker); + } + + private final static JsonSchemaType[] NO_TYPES = new JsonSchemaType[0]; + private static JsonSchemaType[] getExpectedTypes(final Collection<? extends JsonSchemaObject> schemas) { + final List<JsonSchemaType> list = new ArrayList<>(); + for (JsonSchemaObject schema : schemas) { + final JsonSchemaType type = schema.getType(); + if (type != null) { + list.add(type); + } else { + final Set<JsonSchemaType> variants = schema.getTypeVariants(); + if (variants != null) { + list.addAll(variants); + } + } + } + return list.isEmpty() ? NO_TYPES : list.toArray(NO_TYPES); + } + + public static boolean areSchemaTypesCompatible(@NotNull final JsonSchemaObject schema, @NotNull final JsonSchemaType type) { + final JsonSchemaType matchingSchemaType = getMatchingSchemaType(schema, type); + if (matchingSchemaType != null) return matchingSchemaType.equals(type); + if (schema.getEnum() != null) { + return PRIMITIVE_TYPES.contains(type); + } + return true; + } + + @Nullable + private static JsonSchemaType getMatchingSchemaType(@NotNull JsonSchemaObject schema, @NotNull JsonSchemaType input) { + if (schema.getType() != null) { + final JsonSchemaType matchType = schema.getType(); + if (matchType != null) { + if (JsonSchemaType._integer.equals(input) && JsonSchemaType._number.equals(matchType)) { + return input; + } + if (JsonSchemaType._string_number.equals(input) && (JsonSchemaType._number.equals(matchType) + || JsonSchemaType._integer.equals(matchType) + || JsonSchemaType._string.equals(matchType))) { + return input; + } + return matchType; + } + } + if (schema.getTypeVariants() != null) { + Set<JsonSchemaType> matchTypes = schema.getTypeVariants(); + if (matchTypes.contains(input)) { + return input; + } + 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(); + } + if (!schema.getProperties().isEmpty() && JsonSchemaType._object.equals(input)) return JsonSchemaType._object; + return null; + } + + private void checkArrayItems(@NotNull JsonValueAdapter array, @NotNull final List<JsonValueAdapter> list, final JsonSchemaObject schema) { + if (schema.isUniqueItems()) { + final MultiMap<String, JsonValueAdapter> valueTexts = new MultiMap<>(); + final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(array.getDelegate(), schema); + assert walker != null; + for (JsonValueAdapter adapter : list) { + valueTexts.putValue(walker.getNodeTextForValidation(adapter.getDelegate()), adapter); + } + + for (Map.Entry<String, Collection<JsonValueAdapter>> entry: valueTexts.entrySet()) { + if (entry.getValue().size() > 1) { + for (JsonValueAdapter item: entry.getValue()) { + error("Item is not unique", item.getDelegate(), JsonErrorPriority.TYPE_MISMATCH); + } + } + } + } + if (schema.getContainsSchema() != null) { + boolean match = false; + for (JsonValueAdapter item: list) { + final JsonSchemaAnnotatorChecker checker = checkByMatchResult(item, new JsonSchemaResolver(schema.getContainsSchema()).detailedResolve(), myOptions); + if (checker == null || checker.myErrors.size() == 0 && !checker.myHadTypeError) { + match = true; + break; + } + } + if (!match) { + error("No match for 'contains' rule", array.getDelegate(), JsonErrorPriority.MEDIUM_PRIORITY); + } + } + if (schema.getItemsSchema() != null) { + for (JsonValueAdapter item : list) { + checkObjectBySchemaRecordErrors(schema.getItemsSchema(), item); + } + } + else if (schema.getItemsSchemaList() != null) { + final Iterator<JsonSchemaObject> iterator = schema.getItemsSchemaList().iterator(); + for (JsonValueAdapter arrayValue : list) { + if (iterator.hasNext()) { + checkObjectBySchemaRecordErrors(iterator.next(), arrayValue); + } + else { + if (!Boolean.TRUE.equals(schema.getAdditionalItemsAllowed())) { + error("Additional items are not allowed", arrayValue.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + } + else if (schema.getAdditionalItemsSchema() != null) { + checkObjectBySchemaRecordErrors(schema.getAdditionalItemsSchema(), arrayValue); + } + } + } + } + if (schema.getMinItems() != null && list.size() < schema.getMinItems()) { + error("Array is shorter than " + schema.getMinItems(), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + } + if (schema.getMaxItems() != null && list.size() > schema.getMaxItems()) { + error("Array is longer than " + schema.getMaxItems(), array.getDelegate(), JsonErrorPriority.LOW_PRIORITY); + } + } + + private static boolean hasMinMaxLengthChecks(JsonSchemaObject schema) { + return schema.getMinLength() != null || schema.getMaxLength() != null; + } + + private void checkString(PsiElement propValue, JsonSchemaObject schema) { + final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(propValue, schema); + assert walker != null; + final String value = StringUtil.unquoteString(walker.getNodeTextForValidation(propValue)); + if (schema.getMinLength() != null) { + if (value.length() < schema.getMinLength()) { + error("String is shorter than " + schema.getMinLength(), propValue, JsonErrorPriority.LOW_PRIORITY); + return; + } + } + if (schema.getMaxLength() != null) { + if (value.length() > schema.getMaxLength()) { + error("String is longer than " + schema.getMaxLength(), propValue, JsonErrorPriority.LOW_PRIORITY); + return; + } + } + if (schema.getPattern() != null) { + if (schema.getPatternError() != null) { + error("Can not check string by pattern because of error: " + StringUtil.convertLineSeparators(schema.getPatternError()), + propValue, JsonErrorPriority.LOW_PRIORITY); + } + if (!schema.checkByPattern(value)) { + error("String is violating the pattern: '" + StringUtil.convertLineSeparators(schema.getPattern()) + "'", propValue, JsonErrorPriority.LOW_PRIORITY); + } + } + // 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'"); + }*/ + } + + private void checkNumber(PsiElement propValue, JsonSchemaObject schema, JsonSchemaType schemaType) { + Number value; + final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(propValue, schema); + assert walker != null; + String valueText = walker.getNodeTextForValidation(propValue); + if (JsonSchemaType._integer.equals(schemaType)) { + try { + value = Integer.valueOf(valueText); + } + catch (NumberFormatException e) { + error("Integer value expected", propValue, + JsonValidationError.FixableIssueKind.TypeMismatch, + new JsonValidationError.TypeMismatchIssueData(new JsonSchemaType[]{schemaType}), JsonErrorPriority.TYPE_MISMATCH); + return; + } + } + else { + try { + value = Double.valueOf(valueText); + } + catch (NumberFormatException e) { + if (!JsonSchemaType._string_number.equals(schemaType)) { + error("Double value expected", propValue, + JsonValidationError.FixableIssueKind.TypeMismatch, + new JsonValidationError.TypeMismatchIssueData(new JsonSchemaType[]{schemaType}), JsonErrorPriority.TYPE_MISMATCH); + } + return; + } + } + final Number multipleOf = schema.getMultipleOf(); + if (multipleOf != null) { + final double leftOver = value.doubleValue() % multipleOf.doubleValue(); + if (leftOver > 0.000001) { + final String multipleOfValue = String.valueOf(Math.abs(multipleOf.doubleValue() - multipleOf.intValue()) < 0.000001 ? + multipleOf.intValue() : multipleOf); + error("Is not multiple of " + multipleOfValue, propValue, JsonErrorPriority.LOW_PRIORITY); + return; + } + } + + checkMinimum(schema, value, propValue, schemaType); + checkMaximum(schema, value, propValue, schemaType); + } + + private void checkMaximum(JsonSchemaObject schema, Number value, PsiElement propertyValue, + @NotNull JsonSchemaType propValueType) { + + Number exclusiveMaximumNumber = schema.getExclusiveMaximumNumber(); + if (exclusiveMaximumNumber != null) { + if (JsonSchemaType._integer.equals(propValueType)) { + final int intValue = exclusiveMaximumNumber.intValue(); + if (value.intValue() >= intValue) { + error("Greater than an exclusive maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + else { + final double doubleValue = exclusiveMaximumNumber.doubleValue(); + if (value.doubleValue() >= doubleValue) { + error("Greater than an exclusive maximum " + exclusiveMaximumNumber, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + } + Number maximum = schema.getMaximum(); + if (maximum == null) return; + boolean isExclusive = Boolean.TRUE.equals(schema.isExclusiveMaximum()); + if (JsonSchemaType._integer.equals(propValueType)) { + final int intValue = maximum.intValue(); + if (isExclusive) { + if (value.intValue() >= intValue) { + error("Greater than an exclusive maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + else { + if (value.intValue() > intValue) { + error("Greater than a maximum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + } + else { + final double doubleValue = maximum.doubleValue(); + if (isExclusive) { + if (value.doubleValue() >= doubleValue) { + error("Greater than an exclusive maximum " + maximum, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + else { + if (value.doubleValue() > doubleValue) { + error("Greater than a maximum " + maximum, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + } + } + + private void checkMinimum(JsonSchemaObject schema, Number value, PsiElement propertyValue, + @NotNull JsonSchemaType schemaType) { + // schema v6 - exclusiveMinimum is numeric now + Number exclusiveMinimumNumber = schema.getExclusiveMinimumNumber(); + if (exclusiveMinimumNumber != null) { + if (JsonSchemaType._integer.equals(schemaType)) { + final int intValue = exclusiveMinimumNumber.intValue(); + if (value.intValue() <= intValue) { + error("Less than an exclusive minimum" + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + else { + final double doubleValue = exclusiveMinimumNumber.doubleValue(); + if (value.doubleValue() <= doubleValue) { + error("Less than an exclusive minimum " + exclusiveMinimumNumber, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + } + + Number minimum = schema.getMinimum(); + if (minimum == null) return; + boolean isExclusive = Boolean.TRUE.equals(schema.isExclusiveMinimum()); + if (JsonSchemaType._integer.equals(schemaType)) { + final int intValue = minimum.intValue(); + if (isExclusive) { + if (value.intValue() <= intValue) { + error("Less than an exclusive minimum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + else { + if (value.intValue() < intValue) { + error("Less than a minimum " + intValue, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + } + else { + final double doubleValue = minimum.doubleValue(); + if (isExclusive) { + if (value.doubleValue() <= doubleValue) { + error("Less than an exclusive minimum " + minimum, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + else { + if (value.doubleValue() < doubleValue) { + error("Less than a minimum " + minimum, propertyValue, JsonErrorPriority.LOW_PRIORITY); + } + } + } + } + + // returns the schema, selected for annotation + private JsonSchemaObject processOneOf(@NotNull JsonValueAdapter value, List<JsonSchemaObject> oneOf) { + final List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers = ContainerUtil.newArrayList(); + final List<JsonSchemaObject> candidateErroneousSchemas = ContainerUtil.newArrayList(); + final List<JsonSchemaObject> correct = new SmartList<>(); + for (JsonSchemaObject object : oneOf) { + // skip it if something JS awaited, we do not process it currently + if (object.isShouldValidateAgainstJSType()) continue; + + final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(myOptions); + checker.checkByScheme(value, object); + + if (checker.isCorrect()) { + candidateErroneousCheckers.clear(); + candidateErroneousSchemas.clear(); + correct.add(object); + } + else { + candidateErroneousCheckers.add(checker); + candidateErroneousSchemas.add(object); + } + } + if (correct.size() == 1) return correct.get(0); + if (correct.size() > 0) { + final JsonSchemaType type = JsonSchemaType.getType(value); + if (type != null) { + // also check maybe some currently not checked properties like format are different with schemes + // todo note that JsonSchemaObject#equals is broken by design, so normally it shouldn't be used until rewritten + // but for now we use it here to avoid similar schemas being marked as duplicates + if (ContainerUtil.newHashSet(correct).size() > 1 && !schemesDifferWithNotCheckedProperties(correct)) { + error("Validates to more than one variant", value.getDelegate(), JsonErrorPriority.MEDIUM_PRIORITY); + } + } + return ContainerUtil.getLastItem(correct); + } + + return showErrorsAndGetLeastErroneous(candidateErroneousCheckers, candidateErroneousSchemas, true); + } + + private static boolean schemesDifferWithNotCheckedProperties(@NotNull final List<JsonSchemaObject> list) { + return list.stream().anyMatch(s -> !StringUtil.isEmptyOrSpaces(s.getFormat())); + } + + private enum AverageFailureAmount { + Light, + MissingItems, + Medium, + Hard, + NotSchema + } + + @NotNull + private static AverageFailureAmount getAverageFailureAmount(@NotNull JsonSchemaAnnotatorChecker checker) { + int lowPriorityCount = 0; + boolean hasMedium = false; + boolean hasMissing = false; + boolean hasHard = false; + Collection<JsonValidationError> values = checker.getErrors().values(); + for (JsonValidationError value: values) { + switch (value.getPriority()) { + case LOW_PRIORITY: + lowPriorityCount++; + break; + case MISSING_PROPS: + hasMissing = true; + break; + case MEDIUM_PRIORITY: + hasMedium = true; + break; + case TYPE_MISMATCH: + hasHard = true; + break; + case NOT_SCHEMA: + return AverageFailureAmount.NotSchema; + } + } + + if (hasHard) { + return AverageFailureAmount.Hard; + } + + // missing props should win against other conditions + if (hasMissing) { + return AverageFailureAmount.MissingItems; + } + + if (hasMedium) { + return AverageFailureAmount.Medium; + } + + return lowPriorityCount <= 3 ? AverageFailureAmount.Light : AverageFailureAmount.Medium; + } + + // returns the schema, selected for annotation + private JsonSchemaObject processAnyOf(@NotNull JsonValueAdapter value, List<JsonSchemaObject> anyOf) { + final List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers = ContainerUtil.newArrayList(); + final List<JsonSchemaObject> candidateErroneousSchemas = ContainerUtil.newArrayList(); + + for (JsonSchemaObject object : anyOf) { + final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(myOptions); + checker.checkByScheme(value, object); + if (checker.isCorrect()) { + return object; + } + // maybe we still find the correct schema - continue to iterate + candidateErroneousCheckers.add(checker); + candidateErroneousSchemas.add(object); + } + + return showErrorsAndGetLeastErroneous(candidateErroneousCheckers, candidateErroneousSchemas, false); + } + + /** + * Filters schema validation results to get the result with the "minimal" amount of errors. + * This is needed in case of oneOf or anyOf conditions, when there exist no match. + * I.e., when we have multiple schema candidates, but none is applicable. + * In this case we need to show the most "suitable" error messages + * - by detecting the most "likely" schema corresponding to the current entity + */ + @Nullable + private JsonSchemaObject showErrorsAndGetLeastErroneous(@NotNull List<JsonSchemaAnnotatorChecker> candidateErroneousCheckers, + @NotNull List<JsonSchemaObject> candidateErroneousSchemas, + boolean isOneOf) { + JsonSchemaObject current = null; + JsonSchemaObject currentWithMinAverage = null; + Optional<AverageFailureAmount> minAverage = candidateErroneousCheckers.stream() + .map(c -> getAverageFailureAmount(c)) + .min(Comparator.comparingInt(c -> c.ordinal())); + int min = minAverage.orElse(AverageFailureAmount.Hard).ordinal(); + + int minErrorCount = candidateErroneousCheckers.stream().map(c -> c.getErrors().size()).min(Integer::compareTo).orElse(Integer.MAX_VALUE); + + MultiMap<PsiElement, JsonValidationError> errorsWithMinAverage = MultiMap.create(); + MultiMap<PsiElement, JsonValidationError> allErrors = MultiMap.create(); + for (int i = 0; i < candidateErroneousCheckers.size(); i++) { + JsonSchemaAnnotatorChecker checker = candidateErroneousCheckers.get(i); + final boolean isMoreThanMinErrors = checker.getErrors().size() > minErrorCount; + final boolean isMoreThanAverage = getAverageFailureAmount(checker).ordinal() > min; + if (!isMoreThanMinErrors) { + if (isMoreThanAverage) { + currentWithMinAverage = candidateErroneousSchemas.get(i); + } + else { + current = candidateErroneousSchemas.get(i); + } + + for (Map.Entry<PsiElement, JsonValidationError> entry: checker.getErrors().entrySet()) { + (isMoreThanAverage ? errorsWithMinAverage : allErrors).putValue(entry.getKey(), entry.getValue()); + } + } + } + + if (allErrors.isEmpty()) allErrors = errorsWithMinAverage; + + for (Map.Entry<PsiElement, Collection<JsonValidationError>> entry : allErrors.entrySet()) { + Collection<JsonValidationError> value = entry.getValue(); + if (value.size() == 0) continue; + if (value.size() == 1) { + error(entry.getKey(), value.iterator().next()); + continue; + } + JsonValidationError error = tryMergeErrors(value, isOneOf); + if (error != null) { + error(entry.getKey(), error); + } + else { + for (JsonValidationError validationError : value) { + error(entry.getKey(), validationError); + } + } + } + + if (current == null) { + current = currentWithMinAverage; + } + if (current == null) { + current = ContainerUtil.getLastItem(candidateErroneousSchemas); + } + + return current; + } + + @Nullable + private static JsonValidationError tryMergeErrors(@NotNull Collection<JsonValidationError> errors, boolean isOneOf) { + JsonValidationError.FixableIssueKind commonIssueKind = null; + for (JsonValidationError error : errors) { + JsonValidationError.FixableIssueKind currentIssueKind = error.getFixableIssueKind(); + if (currentIssueKind == JsonValidationError.FixableIssueKind.None) return null; + else if (commonIssueKind == null) commonIssueKind = currentIssueKind; + else if (currentIssueKind != commonIssueKind) return null; + } + + if (commonIssueKind == JsonValidationError.FixableIssueKind.NonEnumValue) { + return new JsonValidationError(ENUM_MISMATCH_PREFIX + + errors + .stream() + .map(e -> StringUtil.trimStart(e.getMessage(), ENUM_MISMATCH_PREFIX)) + .map(e -> StringUtil.split(e, ", ")) + .flatMap(e -> e.stream()) + .distinct() + .collect(Collectors.joining(", ")), commonIssueKind, null, errors.iterator().next().getPriority()); + } + + if (commonIssueKind == JsonValidationError.FixableIssueKind.MissingProperty) { + String prefix = isOneOf ? "One of the following property sets is required: " : "Should have at least one of the following property sets: "; + return new JsonValidationError(prefix + + errors.stream().map(e -> (JsonValidationError.MissingMultiplePropsIssueData)e.getIssueData()) + .map(d -> d.getMessage(false)).collect(Collectors.joining(", or ")), + isOneOf ? JsonValidationError.FixableIssueKind.MissingOneOfProperty : JsonValidationError.FixableIssueKind.MissingAnyOfProperty, + new JsonValidationError.MissingOneOfPropsIssueData(errors.stream().map(e -> (JsonValidationError.MissingMultiplePropsIssueData)e.getIssueData()).collect( + Collectors.toList())), errors.iterator().next().getPriority()); + } + + if (commonIssueKind == JsonValidationError.FixableIssueKind.ProhibitedType) { + final Set<JsonSchemaType> allTypes = errors.stream().map(e -> (JsonValidationError.TypeMismatchIssueData)e.getIssueData()) + .flatMap(d -> Arrays.stream(d.expectedTypes)).collect(Collectors.toSet()); + + if (allTypes.size() == 1) return errors.iterator().next(); + + String commonTypeMessage = "Type is not allowed. Expected one of: " + allTypes.stream().map(t -> t.getDescription()).sorted().collect(Collectors.joining(", ")) + "."; + return new JsonValidationError(commonTypeMessage, JsonValidationError.FixableIssueKind.TypeMismatch, + new JsonValidationError.TypeMismatchIssueData(ContainerUtil.toArray(allTypes, JsonSchemaType[]::new)), + errors.iterator().next().getPriority()); + } + + return null; + } + + public boolean isCorrect() { + return myErrors.isEmpty(); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java new file mode 100644 index 00000000..39992c7d --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaBaseReference.java @@ -0,0 +1,58 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.impl.source.resolve.ResolveCache; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Irina.Chernushina on 3/31/2016. + */ +public abstract class JsonSchemaBaseReference<T extends PsiElement> extends PsiReferenceBase<T> { + public JsonSchemaBaseReference(T element, TextRange textRange) { + super(element, textRange, true); + } + + @Nullable + @Override + public PsiElement resolve() { + return ResolveCache.getInstance(getElement().getProject()).resolveWithCaching(this, MyResolver.INSTANCE, false, false); + } + + @Nullable + public abstract PsiElement resolveInner(); + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsonSchemaBaseReference that = (JsonSchemaBaseReference)o; + + return isIdenticalTo(that); + } + + protected boolean isIdenticalTo(JsonSchemaBaseReference that) { + return myElement.equals(that.myElement); + } + + @Override + public int hashCode() { + return myElement.hashCode(); + } + + private static class MyResolver implements ResolveCache.Resolver { + private static final MyResolver INSTANCE = new MyResolver(); + + @Override + @Nullable + public PsiElement resolve(@NotNull PsiReference ref, boolean incompleteCode) { + return ((JsonSchemaBaseReference)ref).resolveInner(); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java new file mode 100644 index 00000000..cbe848a6 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaCompletionContributor.java @@ -0,0 +1,672 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.codeInsight.AutoPopupController; +import com.intellij.codeInsight.completion.*; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.icons.AllIcons; +import com.intellij.ide.DataManager; +import com.intellij.internal.statistic.service.fus.collectors.FUSApplicationUsageTrigger; +import com.intellij.json.psi.*; +import com.intellij.openapi.actionSystem.IdeActions; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorModificationUtil; +import com.intellij.openapi.editor.SelectionModel; +import com.intellij.openapi.editor.actionSystem.CaretSpecificDataContext; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; +import com.intellij.openapi.editor.actionSystem.EditorActionManager; +import com.intellij.openapi.editor.actions.EditorActionUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.TokenType; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiUtilCore; +import com.intellij.util.Consumer; +import com.intellij.util.ObjectUtils; +import com.intellij.util.ThreeState; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; +import com.jetbrains.jsonSchema.extension.SchemaType; +import com.jetbrains.jsonSchema.extension.adapters.JsonObjectValueAdapter; +import com.jetbrains.jsonSchema.extension.adapters.JsonPropertyAdapter; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +import javax.swing.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 10/1/2015. + */ +public class JsonSchemaCompletionContributor extends CompletionContributor { + private static final String BUILTIN_USAGE_KEY = "json.schema.builtin.completion"; + private static final String SCHEMA_USAGE_KEY = "json.schema.schema.completion"; + private static final String USER_USAGE_KEY = "json.schema.user.completion"; + private static final String REMOTE_USAGE_KEY = "json.schema.remote.completion"; + + @Override + public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) { + final PsiElement position = parameters.getPosition(); + final VirtualFile file = PsiUtilCore.getVirtualFile(position); + if (file == null) return; + + final JsonSchemaService service = JsonSchemaService.Impl.get(position.getProject()); + if (!service.isApplicableToFile(file)) return; + final JsonSchemaObject rootSchema = service.getSchemaObject(file); + if (rootSchema == null) return; + PsiElement positionParent = position.getParent(); + if (positionParent != null) { + PsiElement parent = positionParent.getParent(); + if (parent instanceof JsonProperty && "$ref".equals(((JsonProperty)parent).getName()) && service.isSchemaFile(file)) { + return; + } + } + + final VirtualFile schemaFile = rootSchema.getSchemaFile(); + updateStat(service.getSchemaProvider(schemaFile), schemaFile); + doCompletion(parameters, result, rootSchema); + } + + public static void doCompletion(@NotNull final CompletionParameters parameters, + @NotNull final CompletionResultSet result, + @NotNull final JsonSchemaObject rootSchema) { + doCompletion(parameters, result, rootSchema, true); + } + + public static void doCompletion(@NotNull final CompletionParameters parameters, + @NotNull final CompletionResultSet result, + @NotNull final JsonSchemaObject rootSchema, + boolean stop) { + final PsiElement completionPosition = parameters.getOriginalPosition() != null ? parameters.getOriginalPosition() : + parameters.getPosition(); + new Worker(rootSchema, parameters.getPosition(), completionPosition, result).work(); + if (stop) { + result.stopHere(); + } + } + + @TestOnly + @NotNull + public static List<LookupElement> getCompletionVariants(@NotNull final JsonSchemaObject schema, + @NotNull final PsiElement position, @NotNull final PsiElement originalPosition) { + final List<LookupElement> result = new ArrayList<>(); + new Worker(schema, position, originalPosition, element -> result.add(element)).work(); + return result; + } + + private static void updateStat(@Nullable JsonSchemaFileProvider provider, VirtualFile schemaFile) { + if (provider == null) { + if (schemaFile instanceof HttpVirtualFile) { + // auto-detected and auto-downloaded JSON schemas + FUSApplicationUsageTrigger usageTrigger = FUSApplicationUsageTrigger.getInstance(); + usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, REMOTE_USAGE_KEY); + } + return; + } + final SchemaType schemaType = provider.getSchemaType(); + FUSApplicationUsageTrigger usageTrigger = FUSApplicationUsageTrigger.getInstance(); + switch (schemaType) { + case schema: + usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, SCHEMA_USAGE_KEY); + break; + case userSchema: + usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, USER_USAGE_KEY); + break; + case embeddedSchema: + usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, BUILTIN_USAGE_KEY); + break; + case remoteSchema: + // this works only for user-specified remote schemas in our settings, but not for auto-detected remote schemas + usageTrigger.trigger(JsonSchemaUsageTriggerCollector.class, REMOTE_USAGE_KEY); + break; + } + } + + private static class Worker { + @NotNull private final JsonSchemaObject myRootSchema; + @NotNull private final PsiElement myPosition; + @NotNull private final PsiElement myOriginalPosition; + @NotNull private final Consumer<LookupElement> myResultConsumer; + private final boolean myWrapInQuotes; + private final boolean myInsideStringLiteral; + // we need this set to filter same-named suggestions (they can be suggested by several matching schemes) + private final Set<LookupElement> myVariants; + private final JsonLikePsiWalker myWalker; + + Worker(@NotNull JsonSchemaObject rootSchema, @NotNull PsiElement position, + @NotNull PsiElement originalPosition, @NotNull final Consumer<LookupElement> resultConsumer) { + myRootSchema = rootSchema; + myPosition = position; + myOriginalPosition = originalPosition; + myResultConsumer = resultConsumer; + myVariants = new HashSet<>(); + myWalker = JsonLikePsiWalker.getWalker(myPosition, myRootSchema); + myWrapInQuotes = myWalker != null && myWalker.isNameQuoted() && !(position.getParent() instanceof JsonStringLiteral); + myInsideStringLiteral = position.getParent() instanceof JsonStringLiteral; + } + + public void work() { + if (myWalker == null) return; + final PsiElement checkable = myWalker.goUpToCheckable(myPosition); + if (checkable == null) return; + final ThreeState isName = myWalker.isName(checkable); + final List<JsonSchemaVariantsTreeBuilder.Step> position = myWalker.findPosition(checkable, isName == ThreeState.NO); + if (position == null || position.isEmpty() && isName == ThreeState.NO) return; + + final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(myRootSchema, false, position).resolve(); + final Set<String> knownNames = ContainerUtil.newHashSet(); + // too long here, refactor further + schemas.forEach(schema -> { + if (isName != ThreeState.NO) { + final boolean insertComma = myWalker.hasPropertiesBehindAndNoComma(myPosition); + final boolean hasValue = myWalker.isPropertyWithValue(checkable); + + final Collection<String> properties = myWalker.getPropertyNamesOfParentObject(myOriginalPosition, myPosition); + final JsonPropertyAdapter adapter = myWalker.getParentPropertyAdapter(myOriginalPosition); + + final Map<String, JsonSchemaObject> schemaProperties = schema.getProperties(); + addAllPropertyVariants(insertComma, hasValue, properties, adapter, schemaProperties, knownNames); + addIfThenElsePropertyNameVariants(schema, insertComma, hasValue, properties, adapter, knownNames); + } + + if (isName != ThreeState.YES) { + suggestValues(schema, isName == ThreeState.NO); + } + }); + + for (LookupElement variant : myVariants) { + myResultConsumer.consume(variant); + } + } + + private void addIfThenElsePropertyNameVariants(@NotNull JsonSchemaObject schema, + boolean insertComma, + boolean hasValue, + @NotNull Collection<String> properties, + @Nullable JsonPropertyAdapter adapter, + Set<String> knownNames) { + if (schema.getIf() == null) return; + + JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(myPosition, schema); + JsonPropertyAdapter propertyAdapter = walker == null ? null : walker.getParentPropertyAdapter(myPosition); + if (propertyAdapter == null) return; + + JsonObjectValueAdapter object = propertyAdapter.getParentObject(); + if (object == null) return; + + JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions.RELAX_ENUM_CHECK); + checker.checkByScheme(object, schema.getIf()); + if (checker.isCorrect()) { + JsonSchemaObject then = schema.getThen(); + if (then != null) { + addAllPropertyVariants(insertComma, hasValue, properties, adapter, then.getProperties(), knownNames); + } + } + else { + JsonSchemaObject schemaElse = schema.getElse(); + if (schemaElse != null) { + addAllPropertyVariants(insertComma, hasValue, properties, adapter, schemaElse.getProperties(), knownNames); + } + } + } + + private void addAllPropertyVariants(boolean insertComma, + boolean hasValue, + Collection<String> properties, + JsonPropertyAdapter adapter, + Map<String, JsonSchemaObject> schemaProperties, Set<String> knownNames) { + schemaProperties.keySet().stream() + .filter(name -> !properties.contains(name) && !knownNames.contains(name) || adapter != null && name.equals(adapter.getName())) + .forEach(name -> {knownNames.add(name); addPropertyVariant(name, schemaProperties.get(name), hasValue, insertComma);}); + } + + private void suggestValues(JsonSchemaObject schema, boolean isSurelyValue) { + suggestValuesForSchemaVariants(schema.getAnyOf(), isSurelyValue); + suggestValuesForSchemaVariants(schema.getOneOf(), isSurelyValue); + suggestValuesForSchemaVariants(schema.getAllOf(), isSurelyValue); + + if (schema.getEnum() != null) { + for (Object o : schema.getEnum()) { + if (myInsideStringLiteral && !(o instanceof String)) continue; + addValueVariant(o.toString(), null); + } + } + else if (isSurelyValue) { + final JsonSchemaType type = schema.guessType(); + suggestSpecialValues(type); + if (type != null) { + suggestByType(schema, type); + } else if (schema.getTypeVariants() != null) { + for (JsonSchemaType schemaType : schema.getTypeVariants()) { + suggestByType(schema, schemaType); + } + } + } + } + + private void suggestSpecialValues(@Nullable JsonSchemaType type) { + if (JsonSchemaVersion.isSchemaSchemaId(myRootSchema.getId()) && type == JsonSchemaType._string) { + JsonPropertyAdapter propertyAdapter = myWalker.getParentPropertyAdapter(myOriginalPosition); + if (propertyAdapter == null || !"required".equals(propertyAdapter.getName())) return; + PsiElement checkable = myWalker.goUpToCheckable(myPosition); + if (!(checkable instanceof JsonStringLiteral) && !(checkable instanceof JsonReferenceExpression)) return; + JsonObject propertiesObject = JsonRequiredPropsReferenceProvider.findPropertiesObject(checkable); + if (propertiesObject == null) return; + PsiElement parent = checkable.getParent(); + Set<String> items = parent instanceof JsonArray ? ((JsonArray)parent).getValueList().stream() + .filter(v -> v instanceof JsonStringLiteral).map(v -> ((JsonStringLiteral)v).getValue()).collect(Collectors.toSet()) : ContainerUtil.newHashSet(); + propertiesObject.getPropertyList().stream().map(p -> p.getName()).filter(n -> !items.contains(n)).forEach(n -> addStringVariant(n)); + } + } + + private void suggestByType(JsonSchemaObject schema, JsonSchemaType type) { + if (JsonSchemaType._string.equals(type)) { + addPossibleStringValue(schema); + } + if (myInsideStringLiteral){ + return; + } + if (JsonSchemaType._boolean.equals(type)) { + addPossibleBooleanValue(type); + } else if (JsonSchemaType._null.equals(type)) { + addValueVariant("null", null); + } else if (JsonSchemaType._array.equals(type)) { + String value = myWalker.getDefaultArrayValue(); + addValueVariant(value, null, + myWalker.defaultArrayValueDescription(), createArrayOrObjectLiteralInsertHandler(myWalker.invokeEnterBeforeObjectAndArray(), value.length())); + } else if (JsonSchemaType._object.equals(type)) { + String value = myWalker.getDefaultObjectValue(); + addValueVariant(value, null, + myWalker.defaultObjectValueDescription(), createArrayOrObjectLiteralInsertHandler(myWalker.invokeEnterBeforeObjectAndArray(), value.length())); + } + } + + private void addPossibleStringValue(JsonSchemaObject schema) { + Object defaultValue = schema.getDefault(); + String defaultValueString = defaultValue == null ? null : defaultValue.toString(); + addStringVariant(defaultValueString); + } + + private void addStringVariant(String defaultValueString) { + if (!StringUtil.isEmpty(defaultValueString)) { + String normalizedValue = defaultValueString; + boolean shouldQuote = myWalker.quotesForStringLiterals(); + boolean isQuoted = StringUtil.isQuotedString(normalizedValue); + if (shouldQuote && !isQuoted) { + normalizedValue = StringUtil.wrapWithDoubleQuote(normalizedValue); + } + else if (!shouldQuote && isQuoted) { + normalizedValue = StringUtil.unquoteString(normalizedValue); + } + addValueVariant(normalizedValue, null); + } + } + + private void suggestValuesForSchemaVariants(List<JsonSchemaObject> list, boolean isSurelyValue) { + if (list != null && list.size() > 0) { + for (JsonSchemaObject schemaObject : list) { + suggestValues(schemaObject, isSurelyValue); + } + } + } + + private void addPossibleBooleanValue(JsonSchemaType type) { + if (JsonSchemaType._boolean.equals(type)) { + addValueVariant("true", null); + addValueVariant("false", null); + } + } + + + private void addValueVariant(@NotNull String key, @SuppressWarnings("SameParameterValue") @Nullable final String description) { + addValueVariant(key, description, null, null); + } + + private void addValueVariant(@NotNull String key, + @SuppressWarnings("SameParameterValue") @Nullable final String description, + @Nullable final String altText, + @Nullable InsertHandler<LookupElement> handler) { + LookupElementBuilder builder = LookupElementBuilder.create(!myWrapInQuotes ? StringUtil.unquoteString(key) : key); + if (altText != null) { + builder = builder.withPresentableText(altText); + } + if (description != null) { + builder = builder.withTypeText(description); + } + if (handler != null) { + builder = builder.withInsertHandler(handler); + } + myVariants.add(builder); + } + + private void addPropertyVariant(@NotNull String key, @NotNull JsonSchemaObject jsonSchemaObject, boolean hasValue, boolean insertComma) { + final Collection<JsonSchemaObject> variants = new JsonSchemaResolver(jsonSchemaObject).resolve(); + jsonSchemaObject = ObjectUtils.coalesce(ContainerUtil.getFirstItem(variants), jsonSchemaObject); + key = !myWrapInQuotes ? key : StringUtil.wrapWithDoubleQuote(key); + LookupElementBuilder builder = LookupElementBuilder.create(key); + + final String typeText = JsonSchemaDocumentationProvider.getBestDocumentation(true, jsonSchemaObject); + if (!StringUtil.isEmptyOrSpaces(typeText)) { + final String text = StringUtil.removeHtmlTags(typeText); + final int firstSentenceMark = text.indexOf(". "); + builder = builder.withTypeText(firstSentenceMark == -1 ? text : text.substring(0, firstSentenceMark + 1), true); + } + else { + String type = jsonSchemaObject.getTypeDescription(true); + if (type != null) { + builder = builder.withTypeText(type, true); + } + } + + builder = builder.withIcon(getIcon(jsonSchemaObject.guessType())); + + if (hasSameType(variants)) { + final JsonSchemaType type = jsonSchemaObject.guessType(); + final List<Object> values = jsonSchemaObject.getEnum(); + Object defaultValue = jsonSchemaObject.getDefault(); + + boolean hasValues = !ContainerUtil.isEmpty(values); + if (type != null || hasValues || defaultValue != null) { + builder = builder.withInsertHandler( + !hasValues || values.stream().map(v -> v.getClass()).distinct().count() == 1 ? + createPropertyInsertHandler(jsonSchemaObject, hasValue, insertComma) : + createDefaultPropertyInsertHandler(true, insertComma)); + } + else { + builder = builder.withInsertHandler(createDefaultPropertyInsertHandler(false, insertComma)); + } + } else if (!hasValue) { + builder = builder.withInsertHandler(createDefaultPropertyInsertHandler(false, insertComma)); + } + + myVariants.add(builder); + } + + @NotNull + private static Icon getIcon(@Nullable JsonSchemaType type) { + if (type == null) return AllIcons.Nodes.Property; + switch (type) { + case _object: + return AllIcons.Json.Object; + case _array: + return AllIcons.Json.Array; + default: + return AllIcons.Nodes.Property; + } + } + + private static boolean hasSameType(@NotNull Collection<JsonSchemaObject> variants) { + return variants.stream().map(JsonSchemaObject::guessType).filter(Objects::nonNull).distinct().count() <= 1; + } + + private static InsertHandler<LookupElement> createArrayOrObjectLiteralInsertHandler(boolean newline, int insertedTextSize) { + return new InsertHandler<LookupElement>() { + @Override + public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) { + Editor editor = context.getEditor(); + + if (!newline) { + EditorModificationUtil.moveCaretRelatively(editor, -1); + } + else { + EditorModificationUtil.moveCaretRelatively(editor, -insertedTextSize); + PsiDocumentManager.getInstance(context.getProject()).commitDocument(editor.getDocument()); + invokeEnterHandler(editor); + EditorActionUtil.moveCaretToLineEnd(editor, false, false); + } + AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(editor, null); + } + }; + } + + private InsertHandler<LookupElement> createDefaultPropertyInsertHandler(@SuppressWarnings("SameParameterValue") boolean hasValue, + boolean insertComma) { + return new InsertHandler<LookupElement>() { + @Override + public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) { + ApplicationManager.getApplication().assertWriteAccessAllowed(); + Editor editor = context.getEditor(); + Project project = context.getProject(); + + if (handleInsideQuotesInsertion(context, editor, hasValue)) return; + + // inserting longer string for proper formatting + final String stringToInsert = ": 1" + (insertComma ? "," : ""); + EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true, 2); + formatInsertedString(context, stringToInsert.length()); + final int offset = editor.getCaretModel().getOffset(); + context.getDocument().deleteString(offset, offset + 1); + PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument()); + AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null); + } + }; + } + + @NotNull + private InsertHandler<LookupElement> createPropertyInsertHandler(@NotNull JsonSchemaObject jsonSchemaObject, + final boolean hasValue, + boolean insertComma) { + JsonSchemaType type = jsonSchemaObject.guessType(); + List<Object> values = jsonSchemaObject.getEnum(); + if (type == null && values != null && !values.isEmpty()) type = detectType(values); + final Object defaultValue = jsonSchemaObject.getDefault(); + final String defaultValueAsString = defaultValue == null || defaultValue instanceof JsonSchemaObject ? null : + (defaultValue instanceof String ? "\"" + defaultValue + "\"" : + String.valueOf(defaultValue)); + JsonSchemaType finalType = type; + return new InsertHandler<LookupElement>() { + @Override + public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) { + ApplicationManager.getApplication().assertWriteAccessAllowed(); + Editor editor = context.getEditor(); + Project project = context.getProject(); + String stringToInsert = null; + final String comma = insertComma ? "," : ""; + + if (handleInsideQuotesInsertion(context, editor, hasValue)) return; + + PsiElement element = context.getFile().findElementAt(editor.getCaretModel().getOffset()); + boolean insertColon = element == null || !":".equals(element.getText()); + if (!insertColon) { + editor.getCaretModel().moveToOffset(editor.getCaretModel().getOffset() + 1); + } + + if (finalType != null) { + boolean hadEnter; + switch (finalType) { + case _object: + EditorModificationUtil.insertStringAtCaret(editor, insertColon ? ": " : " ", + false, true, + insertColon ? 2 : 1); + hadEnter = false; + boolean invokeEnter = myWalker.invokeEnterBeforeObjectAndArray(); + if (insertColon && invokeEnter) { + invokeEnterHandler(editor); + hadEnter = true; + } + if (insertColon) { + stringToInsert = myWalker.getDefaultObjectValue() + comma; + EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, + false, true, + hadEnter ? 0 : 1); + } + + if (hadEnter || !insertColon) { + EditorActionUtil.moveCaretToLineEnd(editor, false, false); + } + + PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument()); + if (!hadEnter && stringToInsert != null) { + formatInsertedString(context, stringToInsert.length()); + } + if (stringToInsert != null && !invokeEnter) { + invokeEnterHandler(editor); + } + break; + case _boolean: + String value = String.valueOf(Boolean.TRUE.toString().equals(defaultValueAsString)); + stringToInsert = (insertColon ? ": " : " ") + value + comma; + SelectionModel model = editor.getSelectionModel(); + + EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, + false, true, + stringToInsert.length() - comma.length()); + formatInsertedString(context, stringToInsert.length()); + int start = editor.getSelectionModel().getSelectionStart(); + model.setSelection(start - value.length(), start); + AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null); + break; + case _array: + EditorModificationUtil.insertStringAtCaret(editor, insertColon ? ": " : " ", + false, true, + insertColon ? 2 : 1); + hadEnter = false; + if (insertColon && myWalker.invokeEnterBeforeObjectAndArray()) { + invokeEnterHandler(editor); + hadEnter = true; + } + if (insertColon) { + stringToInsert = myWalker.getDefaultArrayValue() + comma; + EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, + false, true, + hadEnter ? 0 : 1); + } + if (hadEnter) { + EditorActionUtil.moveCaretToLineEnd(editor, false, false); + } + + PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument()); + + if (stringToInsert != null) { + formatInsertedString(context, stringToInsert.length()); + } + break; + case _string: + case _integer: + insertPropertyWithEnum(context, editor, defaultValueAsString, values, finalType, comma, myWalker, insertColon); + break; + default: + } + } + else { + insertPropertyWithEnum(context, editor, defaultValueAsString, values, null, comma, myWalker, insertColon); + } + } + }; + } + + private static void invokeEnterHandler(Editor editor) { + EditorActionHandler handler = EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER); + Caret caret = editor.getCaretModel().getCurrentCaret(); + handler.execute(editor, caret, + new CaretSpecificDataContext(DataManager.getInstance().getDataContext(editor.getContentComponent()), caret)); + } + + private boolean handleInsideQuotesInsertion(@NotNull InsertionContext context, @NotNull Editor editor, boolean hasValue) { + if (myInsideStringLiteral) { + int offset = editor.getCaretModel().getOffset(); + PsiElement element = context.getFile().findElementAt(offset); + int tailOffset = context.getTailOffset(); + int guessEndOffset = tailOffset + 1; + if (element instanceof LeafPsiElement) { + if (handleIncompleteString(editor, element)) return false; + int endOffset = element.getTextRange().getEndOffset(); + if (endOffset > tailOffset) { + context.getDocument().deleteString(tailOffset, endOffset - 1); + } + } + if (hasValue) { + return true; + } + editor.getCaretModel().moveToOffset(guessEndOffset); + } else editor.getCaretModel().moveToOffset(context.getTailOffset()); + return false; + } + + private static boolean handleIncompleteString(@NotNull Editor editor, @NotNull PsiElement element) { + if (((LeafPsiElement)element).getElementType() == TokenType.WHITE_SPACE) { + PsiElement prevSibling = element.getPrevSibling(); + if (prevSibling instanceof JsonProperty) { + JsonValue nameElement = ((JsonProperty)prevSibling).getNameElement(); + if (!nameElement.getText().endsWith("\"")) { + editor.getCaretModel().moveToOffset(nameElement.getTextRange().getEndOffset()); + EditorModificationUtil.insertStringAtCaret(editor, "\"", false, true, 1); + return true; + } + } + } + return false; + } + + @Nullable + private static JsonSchemaType detectType(List<Object> values) { + JsonSchemaType type = null; + for (Object value : values) { + JsonSchemaType newType = null; + if (value instanceof Integer) newType = JsonSchemaType._integer; + if (type != null && !type.equals(newType)) return null; + type = newType; + } + return type; + } + } + + private static void insertPropertyWithEnum(InsertionContext context, + Editor editor, + String defaultValue, + List<Object> values, + JsonSchemaType type, + String comma, + JsonLikePsiWalker walker, + boolean insertColon) { + if (!walker.quotesForStringLiterals() && defaultValue != null) { + defaultValue = StringUtil.unquoteString(defaultValue); + } + final boolean isNumber = type != null && (JsonSchemaType._integer.equals(type) || JsonSchemaType._number.equals(type)) || + type == null && (defaultValue != null && + !StringUtil.isQuotedString(defaultValue) || values != null && ContainerUtil.and(values, v -> !(v instanceof String))); + boolean hasValues = !ContainerUtil.isEmpty(values); + boolean hasDefaultValue = !StringUtil.isEmpty(defaultValue); + boolean hasQuotes = isNumber || !walker.quotesForStringLiterals(); + final String colonWs = insertColon ? ": " : " "; + String stringToInsert = colonWs + (hasDefaultValue ? defaultValue : (hasQuotes ? "" : "\"\"")) + comma; + EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true, + insertColon ? 2 : 1); + if (!hasQuotes || hasDefaultValue) { + SelectionModel model = editor.getSelectionModel(); + int caretStart = model.getSelectionStart(); + int newOffset = caretStart + (hasDefaultValue ? defaultValue.length() : 1); + if (hasDefaultValue && !hasQuotes) newOffset--; + model.setSelection(hasQuotes ? caretStart : (caretStart + 1), newOffset); + editor.getCaretModel().moveToOffset(newOffset); + } + + if (!walker.invokeEnterBeforeObjectAndArray() && !stringToInsert.equals(colonWs + comma)) { + formatInsertedString(context, stringToInsert.length()); + } + + if (hasValues) { + AutoPopupController.getInstance(context.getProject()).autoPopupMemberLookup(context.getEditor(), null); + } + } + + public static void formatInsertedString(@NotNull InsertionContext context, + int offset) { + Project project = context.getProject(); + PsiDocumentManager.getInstance(project).commitDocument(context.getDocument()); + CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project); + codeStyleManager.reformatText(context.getFile(), context.getStartOffset(), context.getTailOffset() + offset); + } +}
\ No newline at end of file diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java new file mode 100644 index 00000000..12e52c43 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaComplianceChecker.java @@ -0,0 +1,157 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.codeInspection.LocalInspectionToolSession; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +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 java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class JsonSchemaComplianceChecker { + private static final Key<Set<PsiElement>> ANNOTATED_PROPERTIES = Key.create("JsonSchema.Properties.Annotated"); + + @NotNull private final JsonSchemaObject myRootSchema; + @NotNull private final ProblemsHolder myHolder; + @NotNull private final JsonLikePsiWalker myWalker; + private final LocalInspectionToolSession mySession; + @NotNull private final JsonComplianceCheckerOptions myOptions; + @Nullable private final String myMessagePrefix; + + public JsonSchemaComplianceChecker(@NotNull JsonSchemaObject rootSchema, + @NotNull ProblemsHolder holder, + @NotNull JsonLikePsiWalker walker, + @NotNull LocalInspectionToolSession session, + @NotNull JsonComplianceCheckerOptions options) { + this(rootSchema, holder, walker, session, options, null); + } + + public JsonSchemaComplianceChecker(@NotNull JsonSchemaObject rootSchema, + @NotNull ProblemsHolder holder, + @NotNull JsonLikePsiWalker walker, + @NotNull LocalInspectionToolSession session, + @NotNull JsonComplianceCheckerOptions options, + @Nullable String messagePrefix) { + myRootSchema = rootSchema; + myHolder = holder; + myWalker = walker; + mySession = session; + myOptions = options; + myMessagePrefix = messagePrefix; + } + + public void annotate(@NotNull final PsiElement element) { + final JsonPropertyAdapter firstProp = myWalker.getParentPropertyAdapter(element); + if (firstProp != null) { + final List<JsonSchemaVariantsTreeBuilder.Step> position = myWalker.findPosition(firstProp.getDelegate(), true); + if (position == null || position.isEmpty()) return; + final MatchResult result = new JsonSchemaResolver(myRootSchema, false, position).detailedResolve(); + for (JsonValueAdapter value : firstProp.getValues()) { + createWarnings(JsonSchemaAnnotatorChecker.checkByMatchResult(value, result, myOptions)); + } + } + checkRoot(element, firstProp); + } + + private void checkRoot(@NotNull PsiElement element, @Nullable JsonPropertyAdapter firstProp) { + JsonValueAdapter rootToCheck; + if (firstProp == null) { + rootToCheck = findTopLevelElement(myWalker, element); + } else { + rootToCheck = firstProp.getParentObject(); + if (rootToCheck == null || !myWalker.isTopJsonElement(rootToCheck.getDelegate().getParent())) { + return; + } + } + if (rootToCheck != null) { + final MatchResult matchResult = new JsonSchemaResolver(myRootSchema).detailedResolve(); + createWarnings(JsonSchemaAnnotatorChecker.checkByMatchResult(rootToCheck, matchResult, myOptions)); + } + } + + private void createWarnings(@Nullable JsonSchemaAnnotatorChecker checker) { + 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)); + } + + // 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 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, range, value); + } + else { + myHolder.registerProblem(psiElement, range, value, fix); + } + } + + private static JsonValueAdapter findTopLevelElement(@NotNull JsonLikePsiWalker walker, @NotNull PsiElement element) { + final Ref<PsiElement> ref = new Ref<>(); + PsiTreeUtil.findFirstParent(element, el -> { + final boolean isTop = walker.isTopJsonElement(el); + if (!isTop) ref.set(el); + return isTop; + }); + return ref.isNull() ? null : walker.createValueAdapter(ref.get()); + } + + private boolean checkIfAlreadyProcessed(@NotNull PsiElement property) { + Set<PsiElement> data = mySession.getUserData(ANNOTATED_PROPERTIES); + if (data == null) { + data = new HashSet<>(); + mySession.putUserData(ANNOTATED_PROPERTIES, data); + } + if (data.contains(property)) return true; + data.add(property); + return false; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java new file mode 100644 index 00000000..04c09278 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaConflictNotificationProvider.java @@ -0,0 +1,120 @@ +/* + * 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.impl; + +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.options.ShowSettingsUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.EditorNotificationPanel; +import com.intellij.ui.EditorNotifications; +import com.intellij.ui.LightColors; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; +import com.jetbrains.jsonSchema.extension.SchemaType; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.settings.mappings.JsonSchemaMappingsConfigurable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 2/19/2016. + */ +public class JsonSchemaConflictNotificationProvider extends EditorNotifications.Provider<EditorNotificationPanel> { + private static final Key<EditorNotificationPanel> KEY = Key.create("json.schema.conflict.notification.panel"); + + @NotNull + private final Project myProject; + @NotNull + private final JsonSchemaService myJsonSchemaService; + + public JsonSchemaConflictNotificationProvider(@NotNull Project project, + @NotNull JsonSchemaService jsonSchemaService) { + myProject = project; + myJsonSchemaService = jsonSchemaService; + } + + @NotNull + @Override + public Key<EditorNotificationPanel> getKey() { + return KEY; + } + + @Nullable + @Override + public EditorNotificationPanel createNotificationPanel(@NotNull VirtualFile file, @NotNull FileEditor fileEditor) { + if (!myJsonSchemaService.isApplicableToFile(file)) return null; + final Collection<VirtualFile> schemaFiles = ContainerUtil.newArrayList(); + if (!hasConflicts(schemaFiles, file)) return null; + + final String message = createMessage(schemaFiles, myJsonSchemaService, + "; ", "<html>There are several JSON Schemas mapped to this file: ", "</html>"); + if (message == null) return null; + + final EditorNotificationPanel panel = new EditorNotificationPanel(LightColors.RED); + panel.setText(message); + panel.createActionLabel("Edit JSON Schema Mappings", () -> { + ShowSettingsUtil.getInstance().editConfigurable(myProject, new JsonSchemaMappingsConfigurable(myProject)); + EditorNotifications.getInstance(myProject).updateNotifications(file); + }); + return panel; + } + + private boolean hasConflicts(@NotNull Collection<VirtualFile> files, @NotNull VirtualFile file) { + List<JsonSchemaFileProvider> providers = ((JsonSchemaServiceImpl)myJsonSchemaService).getProvidersForFile(file); + for (JsonSchemaFileProvider provider : providers) { + if (provider.getSchemaType() != SchemaType.userSchema) continue; + VirtualFile schemaFile = provider.getSchemaFile(); + if (schemaFile != null) { + files.add(schemaFile); + } + } + return files.size() > 1; + } + + public static String createMessage(@NotNull final Collection<? extends VirtualFile> schemaFiles, + @NotNull JsonSchemaService jsonSchemaService, + @NotNull String separator, + @NotNull String prefix, + @NotNull String suffix) { + final List<Pair<Boolean, String>> pairList = schemaFiles.stream() + .map(file -> jsonSchemaService.getSchemaProvider(file)) + .filter(Objects::nonNull) + .map(provider -> Pair.create(SchemaType.userSchema.equals(provider.getSchemaType()), provider.getName())) + .collect(Collectors.toList()); + + final long numOfSystemSchemas = pairList.stream().filter(pair -> !pair.getFirst()).count(); + // do not report anything if there is only one system schema and one user schema (user overrides schema that we provide) + if (pairList.size() == 2 && numOfSystemSchemas == 1) return null; + + final boolean withTypes = numOfSystemSchemas > 0; + return pairList.stream().map(pair -> { + if (withTypes) { + return String.format("%s schema '%s'", Boolean.TRUE.equals(pair.getFirst()) ? "user" : "system", pair.getSecond()); + } + else { + return pair.getSecond(); + } + }).collect(Collectors.joining(separator, prefix, suffix)); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java new file mode 100644 index 00000000..4bcb0732 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaDocumentationProvider.java @@ -0,0 +1,218 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.lang.documentation.DocumentationMarkup; +import com.intellij.lang.documentation.DocumentationProvider; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.impl.FakePsiElement; +import com.intellij.util.ObjectUtils; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; + + +public class JsonSchemaDocumentationProvider implements DocumentationProvider { + @Nullable + @Override + public String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) { + return findSchemaAndGenerateDoc(element, originalElement, true, null); + } + + @Nullable + @Override + public List<String> getUrlFor(PsiElement element, PsiElement originalElement) { + return null; + } + + @Nullable + @Override + public String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { + String forcedPropName = null; + if (element instanceof FakeDocElement) { + forcedPropName = ((FakeDocElement)element).myAltName; + element = ((FakeDocElement)element).myContextElement; + } + return findSchemaAndGenerateDoc(element, originalElement, false, forcedPropName); + } + + @Nullable + public static String findSchemaAndGenerateDoc(PsiElement element, + @Nullable PsiElement originalElement, + final boolean preferShort, + @Nullable String forcedPropName) { + if (element instanceof FakeDocElement) return null; + element = isWhitespaceOrComment(originalElement) ? element : ObjectUtils.coalesce(originalElement, element); + final PsiFile containingFile = element.getContainingFile(); + if (containingFile == null) return null; + final JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject()); + VirtualFile virtualFile = containingFile.getViewProvider().getVirtualFile(); + if (!service.isApplicableToFile(virtualFile)) return null; + final JsonSchemaObject rootSchema = service.getSchemaObject(virtualFile); + if (rootSchema == null) return null; + + return generateDoc(element, rootSchema, preferShort, forcedPropName); + } + + private static boolean isWhitespaceOrComment(@Nullable PsiElement originalElement) { + return originalElement instanceof PsiWhiteSpace || originalElement instanceof PsiComment; + } + + @Nullable + public static String generateDoc(@NotNull final PsiElement element, + @NotNull final JsonSchemaObject rootSchema, + final boolean preferShort, + @Nullable String forcedPropName) { + final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, rootSchema); + if (walker == null) return null; + + final PsiElement checkable = walker.goUpToCheckable(element); + if (checkable == null) return null; + final List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(checkable, true); + if (position == null) return null; + if (forcedPropName != null) { + if (isWhitespaceOrComment(element)) { + position.add(JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(forcedPropName)); + } + else { + if (position.isEmpty()) { + return null; + } + final JsonSchemaVariantsTreeBuilder.Step lastStep = position.get(position.size() - 1); + if (lastStep.getName() == null) return null; + position.set(position.size() - 1, JsonSchemaVariantsTreeBuilder.Step.createPropertyStep(forcedPropName)); + } + } + final Collection<JsonSchemaObject> schemas = new JsonSchemaResolver(rootSchema, true, position).resolve(); + + String htmlDescription = null; + List<JsonSchemaType> possibleTypes = ContainerUtil.newArrayList(); + for (JsonSchemaObject schema : schemas) { + if (htmlDescription == null) { + htmlDescription = getBestDocumentation(preferShort, schema); + } + if (schema.getType() != null && schema.getType() != JsonSchemaType._any) { + possibleTypes.add(schema.getType()); + } + else if (schema.getTypeVariants() != null) { + possibleTypes.addAll(schema.getTypeVariants()); + } + } + + return htmlDescription == null + ? null + : appendNameTypeAndApi(position, getThirdPartyApiInfo(element, rootSchema), possibleTypes, htmlDescription, preferShort); + } + + @NotNull + private static String appendNameTypeAndApi(@NotNull List<JsonSchemaVariantsTreeBuilder.Step> position, + @NotNull String apiInfo, + @NotNull List<JsonSchemaType> possibleTypes, + @NotNull String htmlDescription, boolean preferShort) { + if (position.size() == 0) return htmlDescription; + + JsonSchemaVariantsTreeBuilder.Step lastStep = position.get(position.size() - 1); + String name = lastStep.getName(); + if (name == null) return htmlDescription; + + String type = ""; + String schemaType = JsonSchemaObject.getTypesDescription(false, possibleTypes); + if (schemaType != null) { + type = ": " + schemaType; + } + + if (preferShort) { + htmlDescription = "<b>" + name + "</b>" + type + apiInfo + "<br/>" + htmlDescription; + } + else { + htmlDescription = DocumentationMarkup.DEFINITION_START + name + type + apiInfo + DocumentationMarkup.DEFINITION_END + + DocumentationMarkup.CONTENT_START + htmlDescription + DocumentationMarkup.CONTENT_END; + } + return htmlDescription; + } + + @NotNull + private static String getThirdPartyApiInfo(@NotNull PsiElement element, + @NotNull JsonSchemaObject rootSchema) { + JsonSchemaService service = JsonSchemaService.Impl.get(element.getProject()); + String apiInfo = ""; + JsonSchemaFileProvider provider = service.getSchemaProvider(rootSchema.getSchemaFile()); + if (provider != null) { + String information = provider.getThirdPartyApiInformation(); + if (information != null) { + apiInfo = " <i>(" + information + ")</i>"; + } + } + return apiInfo; + } + + @Nullable + public static String getBestDocumentation(boolean preferShort, @NotNull final JsonSchemaObject schema) { + final String htmlDescription = schema.getHtmlDescription(); + final String description = schema.getDescription(); + final String title = schema.getTitle(); + if (preferShort && !StringUtil.isEmptyOrSpaces(title)) { + return plainTextPostProcess(title); + } else if (!StringUtil.isEmptyOrSpaces(htmlDescription)) { + String desc = htmlDescription; + if (!StringUtil.isEmptyOrSpaces(title)) desc = plainTextPostProcess(title) + "<br/>" + desc; + return desc; + } else if (!StringUtil.isEmptyOrSpaces(description)) { + String desc = plainTextPostProcess(description); + if (!StringUtil.isEmptyOrSpaces(title)) desc = plainTextPostProcess(title) + "<br/>" + desc; + return desc; + } + return null; + } + + @NotNull + private static String plainTextPostProcess(String text) { + return StringUtil.escapeXml(text).replace("\\n", "<br/>"); + } + + @Nullable + @Override + public PsiElement getDocumentationElementForLookupItem(PsiManager psiManager, Object object, PsiElement element) { + if ((element instanceof JsonProperty || isWhitespaceOrComment(element) && element.getParent() instanceof JsonObject) && object instanceof String) { + return new FakeDocElement(element instanceof JsonProperty ? ((JsonProperty)element).getNameElement() : element, StringUtil.unquoteString((String)object)); + } + return null; + } + + @Nullable + @Override + public PsiElement getDocumentationElementForLink(PsiManager psiManager, String link, PsiElement context) { + return null; + } + + private static class FakeDocElement extends FakePsiElement { + private final PsiElement myContextElement; + private final String myAltName; + + private FakeDocElement(PsiElement context, String name) { + myContextElement = context; + myAltName = name; + } + + @Override + public PsiElement getParent() { + return myContextElement; + } + + @NotNull + @Override + public TextRange getTextRangeInParent() { + return myContextElement.getTextRange().shiftLeft(myContextElement.getTextOffset()); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java new file mode 100644 index 00000000..e4827358 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndex.java @@ -0,0 +1,171 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.json.JsonElementTypes; +import com.intellij.json.JsonFileType; +import com.intellij.json.JsonLexer; +import com.intellij.json.json5.Json5FileType; +import com.intellij.json.json5.Json5Lexer; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.TokenType; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.tree.IElementType; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.indexing.*; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JsonSchemaFileValuesIndex extends FileBasedIndexExtension<String, String> { + public static final ID<String, String> INDEX_ID = ID.create("json.file.root.values"); + private static final int VERSION = 5; + public static final String NULL = "$NULL$"; + + @NotNull + @Override + public ID<String, String> getName() { + return INDEX_ID; + } + + private final DataIndexer<String, String, FileContent> myIndexer = + new DataIndexer<String, String, FileContent>() { + @Override + @NotNull + public Map<String, String> map(@NotNull FileContent inputData) { + return readTopLevelProps(inputData.getFileType(), inputData.getContentAsText()); + } + }; + + @NotNull + @Override + public DataIndexer<String, String, FileContent> getIndexer() { + return myIndexer; + } + + @NotNull + @Override + public KeyDescriptor<String> getKeyDescriptor() { + return EnumeratorStringDescriptor.INSTANCE; + } + + @NotNull + @Override + public DataExternalizer<String> getValueExternalizer() { + return EnumeratorStringDescriptor.INSTANCE; + } + + @Override + public int getVersion() { + return VERSION; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return file -> file.getFileType() instanceof JsonFileType; + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Nullable + public static String getCachedValue(Project project, VirtualFile file, String requestedKey) { + if (project.isDisposed() || !file.isValid() || DumbService.isDumb(project)) return NULL; + List<String> values = FileBasedIndex.getInstance().getValues(INDEX_ID, requestedKey, GlobalSearchScope.fileScope(project, file)); + if (values.size() == 1) { + return values.get(0); + } + + return null; + } + + @NotNull + static Map<String, String> readTopLevelProps(@NotNull FileType fileType, @NotNull CharSequence content) { + if (!(fileType instanceof JsonFileType)) return ContainerUtil.newHashMap(); + + Lexer lexer = fileType == Json5FileType.INSTANCE ? new Json5Lexer() : new JsonLexer(); + final HashMap<String, String> map = ContainerUtil.newHashMap(); + lexer.start(content); + + // We only care about properties at the root level having the form of "property" : "value". + int nesting = 0; + boolean idFound = false; + boolean obsoleteIdFound = false; + boolean schemaFound = false; + while (!(idFound && schemaFound && obsoleteIdFound) && lexer.getCurrentPosition().getOffset() < lexer.getBufferEnd()) { + IElementType token = lexer.getTokenType(); + // Nesting level can only change at curly braces. + if (token == JsonElementTypes.L_CURLY) { + nesting++; + } + else if (token == JsonElementTypes.R_CURLY) { + nesting--; + } + else if (nesting == 1 && + (token == JsonElementTypes.DOUBLE_QUOTED_STRING + || token == JsonElementTypes.SINGLE_QUOTED_STRING + || token == JsonElementTypes.IDENTIFIER)) { + // We are looking for two special properties at the root level. + switch (lexer.getTokenText()) { + case "$id": + case "\"$id\"": + case "'$id'": + idFound |= captureValueIfString(lexer, map, JsonCachedValues.ID_CACHE_KEY); + break; + case "id": + case "\"id\"": + case "'id'": + obsoleteIdFound |= captureValueIfString(lexer, map, JsonCachedValues.OBSOLETE_ID_CACHE_KEY); + break; + case "$schema": + case "\"$schema\"": + case "'$schema'": + schemaFound |= captureValueIfString(lexer, map, JsonCachedValues.URL_CACHE_KEY); + break; + } + } + lexer.advance(); + } + if (!map.containsKey(JsonCachedValues.ID_CACHE_KEY)) map.put(JsonCachedValues.ID_CACHE_KEY, NULL); + if (!map.containsKey(JsonCachedValues.OBSOLETE_ID_CACHE_KEY)) map.put(JsonCachedValues.OBSOLETE_ID_CACHE_KEY, NULL); + if (!map.containsKey(JsonCachedValues.URL_CACHE_KEY)) map.put(JsonCachedValues.URL_CACHE_KEY, NULL); + return map; + } + + private static boolean captureValueIfString(@NotNull Lexer lexer, @NotNull HashMap<String, String> destMap, @NotNull String key) { + IElementType token; + lexer.advance(); + token = skipWhitespacesAndGetTokenType(lexer); + if (token == JsonElementTypes.COLON) { + lexer.advance(); + token = skipWhitespacesAndGetTokenType(lexer); + if (token == JsonElementTypes.DOUBLE_QUOTED_STRING || token == JsonElementTypes.SINGLE_QUOTED_STRING) { + destMap.put(key, lexer.getTokenText().substring(1, lexer.getTokenText().length() - 1)); + return true; + } + } + return false; + } + + @Nullable + private static IElementType skipWhitespacesAndGetTokenType(@NotNull Lexer lexer) { + while (lexer.getTokenType() == TokenType.WHITE_SPACE || + lexer.getTokenType() == JsonElementTypes.LINE_COMMENT || + lexer.getTokenType() == JsonElementTypes.BLOCK_COMMENT) { + lexer.advance(); + } + return lexer.getTokenType(); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java new file mode 100644 index 00000000..072bd5f5 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaGotoDeclarationHandler.java @@ -0,0 +1,48 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler; +import com.intellij.json.JsonElementTypes; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtilCore; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class JsonSchemaGotoDeclarationHandler implements GotoDeclarationHandler { + @Nullable + @Override + public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement, int offset, Editor editor) { + final IElementType elementType = PsiUtilCore.getElementType(sourceElement); + if (elementType != JsonElementTypes.DOUBLE_QUOTED_STRING && elementType != JsonElementTypes.SINGLE_QUOTED_STRING) return null; + final JsonStringLiteral literal = PsiTreeUtil.getParentOfType(sourceElement, JsonStringLiteral.class); + if (literal == null) return null; + final PsiElement parent = literal.getParent(); + if (parent instanceof JsonProperty && ((JsonProperty)parent).getNameElement() == literal) { + final JsonSchemaService service = JsonSchemaService.Impl.get(literal.getProject()); + final PsiFile containingFile = literal.getContainingFile(); + final VirtualFile file = containingFile.getVirtualFile(); + if (file == null || !service.isApplicableToFile(file)) return null; + final List<JsonSchemaVariantsTreeBuilder.Step> steps = JsonOriginalPsiWalker.INSTANCE.findPosition(literal, true); + if (steps == null) return null; + final JsonSchemaObject schemaObject = service.getSchemaObject(file); + if (schemaObject != null) { + final PsiElement target = new JsonSchemaResolver(schemaObject, false, steps) + .findNavigationTarget(false, ((JsonProperty)parent).getValue(), + JsonSchemaService.isSchemaFile(containingFile)); + if (target != null) { + return new PsiElement[] {target}; + } + } + } + return null; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java new file mode 100644 index 00000000..998542b6 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaInJsonFilesEnabler.java @@ -0,0 +1,13 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.json.JsonUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.jsonSchema.extension.JsonSchemaEnabler; + +public class JsonSchemaInJsonFilesEnabler implements JsonSchemaEnabler { + @Override + public boolean isEnabledForFile(VirtualFile file) { + return JsonUtil.isJsonFile(file); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java new file mode 100644 index 00000000..8976179e --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaObject.java @@ -0,0 +1,1180 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.intellij.json.psi.JsonContainer; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.ContainerUtilRt; +import com.jetbrains.jsonSchema.JsonSchemaVfsListener; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.remote.JsonFileResolver; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.jetbrains.jsonSchema.JsonPointerUtil.*; + +/** + * @author Irina.Chernushina on 8/28/2015. + */ +public class JsonSchemaObject { + private static final Logger LOG = Logger.getInstance(JsonSchemaObject.class); + + @NonNls public static final String DEFINITIONS = "definitions"; + @NonNls public static final String PROPERTIES = "properties"; + @NonNls public static final String ITEMS = "items"; + @NonNls public static final String ADDITIONAL_ITEMS = "additionalItems"; + @NonNls public static final String X_INTELLIJ_HTML_DESCRIPTION = "x-intellij-html-description"; + @Nullable private final JsonContainer myJsonObject; + @Nullable private Map<String, JsonSchemaObject> myDefinitionsMap; + @NotNull private static final JsonSchemaObject NULL_OBJ = new JsonSchemaObject(); + @NotNull private final ConcurrentMap<String, JsonSchemaObject> myComputedRefs = new ConcurrentHashMap<>(); + @NotNull private final AtomicBoolean mySubscribed = new AtomicBoolean(false); + @NotNull private Map<String, JsonSchemaObject> myProperties; + + @Nullable private PatternProperties myPatternProperties; + @Nullable private PropertyNamePattern myPattern; + + @Nullable private String myId; + @Nullable private String mySchema; + + @Nullable private String myTitle; + @Nullable private String myDescription; + @Nullable private String myHtmlDescription; + + @Nullable private JsonSchemaType myType; + @Nullable private Object myDefault; + @Nullable private String myRef; + @Nullable private String myFormat; + @Nullable private Set<JsonSchemaType> myTypeVariants; + @Nullable private Number myMultipleOf; + @Nullable private Number myMaximum; + private boolean myExclusiveMaximum; + @Nullable private Number myExclusiveMaximumNumber; + @Nullable private Number myMinimum; + private boolean myExclusiveMinimum; + @Nullable private Number myExclusiveMinimumNumber; + @Nullable private Integer myMaxLength; + @Nullable private Integer myMinLength; + + @Nullable private Boolean myAdditionalPropertiesAllowed; + @Nullable private JsonSchemaObject myAdditionalPropertiesSchema; + @Nullable private JsonSchemaObject myPropertyNamesSchema; + + @Nullable private Boolean myAdditionalItemsAllowed; + @Nullable private JsonSchemaObject myAdditionalItemsSchema; + + @Nullable private JsonSchemaObject myItemsSchema; + @Nullable private JsonSchemaObject myContainsSchema; + @Nullable private List<JsonSchemaObject> myItemsSchemaList; + + @Nullable private Integer myMaxItems; + @Nullable private Integer myMinItems; + + @Nullable private Boolean myUniqueItems; + + @Nullable private Integer myMaxProperties; + @Nullable private Integer myMinProperties; + @Nullable private Set<String> myRequired; + + @Nullable private Map<String, List<String>> myPropertyDependencies; + @Nullable private Map<String, JsonSchemaObject> mySchemaDependencies; + + @Nullable private List<Object> myEnum; + + @Nullable private List<JsonSchemaObject> myAllOf; + @Nullable private List<JsonSchemaObject> myAnyOf; + @Nullable private List<JsonSchemaObject> myOneOf; + @Nullable private JsonSchemaObject myNot; + @Nullable private JsonSchemaObject myIf; + @Nullable private JsonSchemaObject myThen; + @Nullable private JsonSchemaObject myElse; + private boolean myShouldValidateAgainstJSType; + + public boolean isValidByExclusion() { + return myIsValidByExclusion; + } + + private boolean myIsValidByExclusion = true; + + public JsonSchemaObject(@NotNull JsonContainer object) { + myJsonObject = object; + myProperties = new HashMap<>(); + } + + private JsonSchemaObject() { + myJsonObject = null; + myProperties = new HashMap<>(); + } + + @Nullable + private static JsonSchemaType getSubtypeOfBoth(@NotNull JsonSchemaType selfType, + @NotNull JsonSchemaType otherType) { + if (otherType == JsonSchemaType._any) return selfType; + if (selfType == JsonSchemaType._any) return otherType; + switch (selfType) { + case _string: + return otherType == JsonSchemaType._string || otherType == JsonSchemaType._string_number ? JsonSchemaType._string : null; + case _number: + if (otherType == JsonSchemaType._integer) return JsonSchemaType._integer; + return otherType == JsonSchemaType._number || otherType == JsonSchemaType._string_number ? JsonSchemaType._number : null; + case _integer: + return otherType == JsonSchemaType._number + || otherType == JsonSchemaType._string_number + || otherType == JsonSchemaType._integer ? JsonSchemaType._integer : null; + case _object: + return otherType == JsonSchemaType._object ? JsonSchemaType._object : null; + case _array: + return otherType == JsonSchemaType._array ? JsonSchemaType._array : null; + case _boolean: + return otherType == JsonSchemaType._boolean ? JsonSchemaType._boolean : null; + case _null: + return otherType == JsonSchemaType._null ? JsonSchemaType._null : null; + case _string_number: + return otherType == JsonSchemaType._integer + || otherType == JsonSchemaType._number + || otherType == JsonSchemaType._string + || otherType == JsonSchemaType._string_number ? otherType : null; + } + return otherType; + } + + @Nullable + private JsonSchemaType mergeTypes(@Nullable JsonSchemaType selfType, + @Nullable JsonSchemaType otherType, + @Nullable Set<JsonSchemaType> otherTypeVariants) { + if (selfType == null) return otherType; + if (otherType == null) { + if (otherTypeVariants != null && !otherTypeVariants.isEmpty()) { + Set<JsonSchemaType> filteredVariants = ContainerUtil.newHashSet(otherTypeVariants.size()); + for (JsonSchemaType variant : otherTypeVariants) { + JsonSchemaType subtype = getSubtypeOfBoth(selfType, variant); + if (subtype != null) filteredVariants.add(subtype); + } + if (filteredVariants.size() == 0) { + myIsValidByExclusion = false; + return selfType; + } + if (filteredVariants.size() == 1) { + return filteredVariants.iterator().next(); + } + return null; // will be handled by variants + } + return selfType; + } + + JsonSchemaType subtypeOfBoth = getSubtypeOfBoth(selfType, otherType); + if (subtypeOfBoth == null){ + myIsValidByExclusion = false; + return otherType; + } + return subtypeOfBoth; + } + + private Set<JsonSchemaType> mergeTypeVariantSets(@Nullable Set<JsonSchemaType> self, @Nullable Set<JsonSchemaType> other) { + if (self == null) return other; + if (other == null) return self; + + Set<JsonSchemaType> resultSet = ContainerUtil.newHashSet(self.size()); + for (JsonSchemaType type : self) { + JsonSchemaType merged = mergeTypes(type, null, other); + if (merged != null) resultSet.add(merged); + } + + if (resultSet.isEmpty()) { + myIsValidByExclusion = false; + return other; + } + + return resultSet; + } + + // peer pointer is not merged! + public void mergeValues(@NotNull JsonSchemaObject other) { + // we do not copy id, schema + mergeProperties(this, other); + myDefinitionsMap = copyMap(myDefinitionsMap, other.myDefinitionsMap); + final Map<String, JsonSchemaObject> map = copyMap(myPatternProperties == null ? null : myPatternProperties.mySchemasMap, + other.myPatternProperties == null ? null : other.myPatternProperties.mySchemasMap); + myPatternProperties = map == null ? null : new PatternProperties(map); + + if (!StringUtil.isEmptyOrSpaces(other.myTitle)) { + myTitle = other.myTitle; + } + if (!StringUtil.isEmptyOrSpaces(other.myDescription)) { + myDescription = other.myDescription; + } + if (!StringUtil.isEmptyOrSpaces(other.myHtmlDescription)) { + myHtmlDescription = other.myHtmlDescription; + } + + myType = mergeTypes(myType, other.myType, other.myTypeVariants); + + if (other.myDefault != null) myDefault = other.myDefault; + if (other.myRef != null) myRef = other.myRef; + if (other.myFormat != null) myFormat = other.myFormat; + myTypeVariants = mergeTypeVariantSets(myTypeVariants, other.myTypeVariants); + if (other.myMultipleOf != null) myMultipleOf = other.myMultipleOf; + if (other.myMaximum != null) myMaximum = other.myMaximum; + if (other.myExclusiveMaximumNumber != null) myExclusiveMaximumNumber = other.myExclusiveMaximumNumber; + myExclusiveMaximum |= other.myExclusiveMaximum; + if (other.myMinimum != null) myMinimum = other.myMinimum; + if (other.myExclusiveMinimumNumber != null) myExclusiveMinimumNumber = other.myExclusiveMinimumNumber; + myExclusiveMinimum |= other.myExclusiveMinimum; + if (other.myMaxLength != null) myMaxLength = other.myMaxLength; + if (other.myMinLength != null) myMinLength = other.myMinLength; + if (other.myPattern != null) myPattern = other.myPattern; + if (other.myAdditionalPropertiesAllowed != null) myAdditionalPropertiesAllowed = other.myAdditionalPropertiesAllowed; + if (other.myAdditionalPropertiesSchema != null) myAdditionalPropertiesSchema = other.myAdditionalPropertiesSchema; + if (other.myPropertyNamesSchema != null) myPropertyNamesSchema = other.myPropertyNamesSchema; + if (other.myAdditionalItemsAllowed != null) myAdditionalItemsAllowed = other.myAdditionalItemsAllowed; + if (other.myAdditionalItemsSchema != null) myAdditionalItemsSchema = other.myAdditionalItemsSchema; + if (other.myItemsSchema != null) myItemsSchema = other.myItemsSchema; + if (other.myContainsSchema != null) myContainsSchema = other.myContainsSchema; + myItemsSchemaList = copyList(myItemsSchemaList, other.myItemsSchemaList); + if (other.myMaxItems != null) myMaxItems = other.myMaxItems; + if (other.myMinItems != null) myMinItems = other.myMinItems; + if (other.myUniqueItems != null) myUniqueItems = other.myUniqueItems; + if (other.myMaxProperties != null) myMaxProperties = other.myMaxProperties; + if (other.myMinProperties != null) myMinProperties = other.myMinProperties; + if (myRequired != null && other.myRequired != null) { + myRequired.addAll(other.myRequired); + } + else if (other.myRequired != null) { + myRequired = other.myRequired; + } + myPropertyDependencies = copyMap(myPropertyDependencies, other.myPropertyDependencies); + mySchemaDependencies = copyMap(mySchemaDependencies, other.mySchemaDependencies); + if (other.myEnum != null) myEnum = other.myEnum; + myAllOf = copyList(myAllOf, other.myAllOf); + myAnyOf = copyList(myAnyOf, other.myAnyOf); + myOneOf = copyList(myOneOf, other.myOneOf); + if (other.myNot != null) myNot = other.myNot; + if (other.myIf != null) myIf = other.myIf; + if (other.myThen != null) myThen = other.myThen; + if (other.myElse != null) myElse = other.myElse; + myShouldValidateAgainstJSType |= other.myShouldValidateAgainstJSType; + } + + private static void mergeProperties(@NotNull JsonSchemaObject thisObject, @NotNull JsonSchemaObject otherObject) { + for (Map.Entry<String, JsonSchemaObject> prop: otherObject.myProperties.entrySet()) { + String key = prop.getKey(); + JsonSchemaObject otherProp = prop.getValue(); + if (!thisObject.myProperties.containsKey(key)) { + thisObject.myProperties.put(key, otherProp); + } + else { + JsonSchemaObject existingProp = thisObject.myProperties.get(key); + thisObject.myProperties.put(key, JsonSchemaVariantsTreeBuilder.merge(existingProp, otherProp, otherProp)); + } + } + } + + public void shouldValidateAgainstJSType() { + myShouldValidateAgainstJSType = true; + } + + public boolean isShouldValidateAgainstJSType() { + return myShouldValidateAgainstJSType; + } + + @Nullable + private static <T> List<T> copyList(@Nullable List<T> target, @Nullable List<T> source) { + if (source == null || source.isEmpty()) return target; + if (target == null) target = ContainerUtil.newArrayListWithCapacity(source.size()); + target.addAll(source); + return target; + } + + @Nullable + private static <K, V> Map<K, V> copyMap(@Nullable Map<K, V> target, @Nullable Map<K, V> source) { + if (source == null || source.isEmpty()) return target; + if (target == null) target = ContainerUtilRt.newHashMap(source.size()); + target.putAll(source); + return target; + } + + @NotNull + public VirtualFile getSchemaFile() { + assert myJsonObject != null; + return myJsonObject.getContainingFile().getViewProvider().getVirtualFile(); + } + + @NotNull + public JsonContainer getJsonObject() { + assert myJsonObject != null; + return myJsonObject; + } + + @Nullable + public Map<String, JsonSchemaObject> getDefinitionsMap() { + return myDefinitionsMap; + } + + public void setDefinitionsMap(@NotNull Map<String, JsonSchemaObject> definitionsMap) { + myDefinitionsMap = definitionsMap; + } + + @NotNull + public Map<String, JsonSchemaObject> getProperties() { + return myProperties; + } + + public void setProperties(@NotNull Map<String, JsonSchemaObject> properties) { + myProperties = properties; + } + + public boolean hasPatternProperties() { + return myPatternProperties != null; + } + + public void setPatternProperties(@NotNull Map<String, JsonSchemaObject> patternProperties) { + myPatternProperties = new PatternProperties(patternProperties); + } + + @Nullable + public JsonSchemaType getType() { + return myType; + } + + public void setType(@Nullable JsonSchemaType type) { + myType = type; + } + + @Nullable + public Number getMultipleOf() { + return myMultipleOf; + } + + public void setMultipleOf(@Nullable Number multipleOf) { + myMultipleOf = multipleOf; + } + + @Nullable + public Number getMaximum() { + return myMaximum; + } + + public void setMaximum(@Nullable Number maximum) { + myMaximum = maximum; + } + + public boolean isExclusiveMaximum() { + return myExclusiveMaximum; + } + + @Nullable + public Number getExclusiveMaximumNumber() { + return myExclusiveMaximumNumber; + } + + public void setExclusiveMaximumNumber(@Nullable Number exclusiveMaximumNumber) { + myExclusiveMaximumNumber = exclusiveMaximumNumber; + } + + @Nullable + public Number getExclusiveMinimumNumber() { + return myExclusiveMinimumNumber; + } + + public void setExclusiveMinimumNumber(@Nullable Number exclusiveMinimumNumber) { + myExclusiveMinimumNumber = exclusiveMinimumNumber; + } + + public void setExclusiveMaximum(boolean exclusiveMaximum) { + myExclusiveMaximum = exclusiveMaximum; + } + + @Nullable + public Number getMinimum() { + return myMinimum; + } + + public void setMinimum(@Nullable Number minimum) { + myMinimum = minimum; + } + + public boolean isExclusiveMinimum() { + return myExclusiveMinimum; + } + + public void setExclusiveMinimum(boolean exclusiveMinimum) { + myExclusiveMinimum = exclusiveMinimum; + } + + @Nullable + public Integer getMaxLength() { + return myMaxLength; + } + + public void setMaxLength(@Nullable Integer maxLength) { + myMaxLength = maxLength; + } + + @Nullable + public Integer getMinLength() { + return myMinLength; + } + + public void setMinLength(@Nullable Integer minLength) { + myMinLength = minLength; + } + + @Nullable + public String getPattern() { + return myPattern == null ? null : myPattern.getPattern(); + } + + public void setPattern(@Nullable String pattern) { + myPattern = pattern == null ? null : new PropertyNamePattern(pattern); + } + + @Nullable + public Boolean getAdditionalPropertiesAllowed() { + return myAdditionalPropertiesAllowed == null || myAdditionalPropertiesAllowed; + } + + public void setAdditionalPropertiesAllowed(@Nullable Boolean additionalPropertiesAllowed) { + myAdditionalPropertiesAllowed = additionalPropertiesAllowed; + } + + @Nullable + public JsonSchemaObject getPropertyNamesSchema() { + return myPropertyNamesSchema; + } + + public void setPropertyNamesSchema(@Nullable JsonSchemaObject propertyNamesSchema) { + myPropertyNamesSchema = propertyNamesSchema; + } + + @Nullable + public JsonSchemaObject getAdditionalPropertiesSchema() { + return myAdditionalPropertiesSchema; + } + + public void setAdditionalPropertiesSchema(@Nullable JsonSchemaObject additionalPropertiesSchema) { + myAdditionalPropertiesSchema = additionalPropertiesSchema; + } + + @Nullable + public Boolean getAdditionalItemsAllowed() { + return myAdditionalItemsAllowed == null || myAdditionalItemsAllowed; + } + + public void setAdditionalItemsAllowed(@Nullable Boolean additionalItemsAllowed) { + myAdditionalItemsAllowed = additionalItemsAllowed; + } + + @Nullable + public JsonSchemaObject getAdditionalItemsSchema() { + return myAdditionalItemsSchema; + } + + public void setAdditionalItemsSchema(@Nullable JsonSchemaObject additionalItemsSchema) { + myAdditionalItemsSchema = additionalItemsSchema; + } + + @Nullable + public JsonSchemaObject getItemsSchema() { + return myItemsSchema; + } + + public void setItemsSchema(@Nullable JsonSchemaObject itemsSchema) { + myItemsSchema = itemsSchema; + } + + @Nullable + public JsonSchemaObject getContainsSchema() { + return myContainsSchema; + } + + public void setContainsSchema(@Nullable JsonSchemaObject containsSchema) { + myContainsSchema = containsSchema; + } + + @Nullable + public List<JsonSchemaObject> getItemsSchemaList() { + return myItemsSchemaList; + } + + public void setItemsSchemaList(@Nullable List<JsonSchemaObject> itemsSchemaList) { + myItemsSchemaList = itemsSchemaList; + } + + @Nullable + public Integer getMaxItems() { + return myMaxItems; + } + + public void setMaxItems(@Nullable Integer maxItems) { + myMaxItems = maxItems; + } + + @Nullable + public Integer getMinItems() { + return myMinItems; + } + + public void setMinItems(@Nullable Integer minItems) { + myMinItems = minItems; + } + + public boolean isUniqueItems() { + return Boolean.TRUE.equals(myUniqueItems); + } + + public void setUniqueItems(boolean uniqueItems) { + myUniqueItems = uniqueItems; + } + + @Nullable + public Integer getMaxProperties() { + return myMaxProperties; + } + + public void setMaxProperties(@Nullable Integer maxProperties) { + myMaxProperties = maxProperties; + } + + @Nullable + public Integer getMinProperties() { + return myMinProperties; + } + + public void setMinProperties(@Nullable Integer minProperties) { + myMinProperties = minProperties; + } + + @Nullable + public Set<String> getRequired() { + return myRequired; + } + + public void setRequired(@Nullable Set<String> required) { + myRequired = required; + } + + @Nullable + public Map<String, List<String>> getPropertyDependencies() { + return myPropertyDependencies; + } + + public void setPropertyDependencies(@Nullable Map<String, List<String>> propertyDependencies) { + myPropertyDependencies = propertyDependencies; + } + + @Nullable + public Map<String, JsonSchemaObject> getSchemaDependencies() { + return mySchemaDependencies; + } + + public void setSchemaDependencies(@Nullable Map<String, JsonSchemaObject> schemaDependencies) { + mySchemaDependencies = schemaDependencies; + } + + @Nullable + public List<Object> getEnum() { + return myEnum; + } + + public void setEnum(@Nullable List<Object> anEnum) { + myEnum = anEnum; + } + + @Nullable + public List<JsonSchemaObject> getAllOf() { + return myAllOf; + } + + public void setAllOf(@Nullable List<JsonSchemaObject> allOf) { + myAllOf = allOf; + } + + @Nullable + public List<JsonSchemaObject> getAnyOf() { + return myAnyOf; + } + + public void setAnyOf(@Nullable List<JsonSchemaObject> anyOf) { + myAnyOf = anyOf; + } + + @Nullable + public List<JsonSchemaObject> getOneOf() { + return myOneOf; + } + + public void setOneOf(@Nullable List<JsonSchemaObject> oneOf) { + myOneOf = oneOf; + } + + @Nullable + public JsonSchemaObject getNot() { + return myNot; + } + + public void setNot(@Nullable JsonSchemaObject not) { + myNot = not; + } + + @Nullable + public JsonSchemaObject getIf() { + return myIf; + } + + public void setIf(@Nullable JsonSchemaObject anIf) { + myIf = anIf; + } + + @Nullable + public JsonSchemaObject getThen() { + return myThen; + } + + public void setThen(@Nullable JsonSchemaObject then) { + myThen = then; + } + + @Nullable + public JsonSchemaObject getElse() { + return myElse; + } + + public void setElse(@Nullable JsonSchemaObject anElse) { + myElse = anElse; + } + + @Nullable + public Set<JsonSchemaType> getTypeVariants() { + return myTypeVariants; + } + + public void setTypeVariants(@Nullable Set<JsonSchemaType> typeVariants) { + myTypeVariants = typeVariants; + } + + @Nullable + public String getRef() { + return myRef; + } + + public void setRef(@Nullable String ref) { + myRef = ref; + } + + @Nullable + public Object getDefault() { + if (JsonSchemaType._integer.equals(myType)) return myDefault instanceof Number ? ((Number)myDefault).intValue() : myDefault; + return myDefault; + } + + public void setDefault(@Nullable Object aDefault) { + myDefault = aDefault; + } + + @Nullable + public String getFormat() { + return myFormat; + } + + public void setFormat(@Nullable String format) { + myFormat = format; + } + + @Nullable + public String getId() { + return myId; + } + + public void setId(@Nullable String id) { + myId = id; + } + + @Nullable + public String getSchema() { + return mySchema; + } + + public void setSchema(@Nullable String schema) { + mySchema = schema; + } + + @Nullable + public String getDescription() { + return myDescription; + } + + public void setDescription(@NotNull String description) { + myDescription = unescapeJsonString(description); + } + + @Nullable + public String getHtmlDescription() { + return myHtmlDescription; + } + + public void setHtmlDescription(@NotNull String htmlDescription) { + myHtmlDescription = unescapeJsonString(htmlDescription); + } + + @Nullable + public String getTitle() { + return myTitle; + } + + public void setTitle(@NotNull String title) { + myTitle = unescapeJsonString(title); + } + + private static String unescapeJsonString(@NotNull final String text) { + try { + final String object = String.format("{\"prop\": \"%s\"}", text); + return new Gson().fromJson(object, JsonObject.class).get("prop").getAsString(); + } catch (JsonParseException e) { + return text; + } + } + + @Nullable + public JsonSchemaObject getMatchingPatternPropertySchema(@NotNull String name) { + if (myPatternProperties == null) return null; + return myPatternProperties.getPatternPropertySchema(name); + } + + public boolean checkByPattern(@NotNull String value) { + return myPattern != null && myPattern.checkByPattern(value); + } + + @Nullable + public String getPatternError() { + return myPattern == null ? null : myPattern.getPatternError(); + } + + @Nullable + public Map<JsonContainer, String> getInvalidPatternProperties() { + if (myPatternProperties != null) { + final Map<String, String> patterns = myPatternProperties.getInvalidPatterns(); + + return patterns.entrySet().stream().map(entry -> { + final JsonSchemaObject object = myPatternProperties.getSchemaForPattern(entry.getKey()); + assert object != null; + return Pair.create(object.getJsonObject(), entry.getValue()); + }).collect(Collectors.toMap(o -> o.getFirst(), o -> o.getSecond())); + } + return null; + } + + @Nullable + public JsonSchemaObject findRelativeDefinition(@NotNull String ref) { + if (isSelfReference(ref)) { + return this; + } + if (!ref.startsWith("#/")) { + return null; + } + ref = ref.substring(2); + final List<String> parts = split(ref); + JsonSchemaObject current = this; + for (int i = 0; i < parts.size(); i++) { + if (current == null) return null; + final String part = parts.get(i); + if (DEFINITIONS.equals(part)) { + if (i == (parts.size() - 1)) return null; + //noinspection AssignmentToForLoopParameter + final String nextPart = parts.get(++i); + current = current.getDefinitionsMap() == null ? null : current.getDefinitionsMap().get(unescapeJsonPointerPart(nextPart)); + continue; + } + if (PROPERTIES.equals(part)) { + if (i == (parts.size() - 1)) return null; + //noinspection AssignmentToForLoopParameter + current = current.getProperties().get(unescapeJsonPointerPart(parts.get(++i))); + continue; + } + if (ITEMS.equals(part)) { + if (i == (parts.size() - 1)) { + current = current.getItemsSchema(); + } + else { + //noinspection AssignmentToForLoopParameter + Integer next = tryParseInt(parts.get(++i)); + List<JsonSchemaObject> itemsSchemaList = current.getItemsSchemaList(); + if (itemsSchemaList != null && next != null && next < itemsSchemaList.size()) { + current = itemsSchemaList.get(next); + } + } + continue; + } + if (ADDITIONAL_ITEMS.equals(part)) { + if (i == (parts.size() - 1)) { + current = current.getAdditionalItemsSchema(); + } + continue; + } + + current = current.getDefinitionsMap() == null ? null : current.getDefinitionsMap().get(part); + } + return current; + } + + @Nullable + private static Integer tryParseInt(String s) { + try { + return Integer.parseInt(s); + } + catch (Exception __) { + return null; + } + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsonSchemaObject object = (JsonSchemaObject)o; + + assert myJsonObject != null; + return myJsonObject.equals(object.myJsonObject); + } + + @Override + public int hashCode() { + assert myJsonObject != null; + return myJsonObject.hashCode(); + } + + @NotNull + private static String adaptSchemaPattern(String pattern) { + pattern = pattern.startsWith("^") || pattern.startsWith("*") || pattern.startsWith(".") ? pattern : (".*" + pattern); + pattern = pattern.endsWith("+") || pattern.endsWith("*") || pattern.endsWith("$") ? pattern : (pattern + ".*"); + pattern = pattern.replace("\\\\", "\\"); + return pattern; + } + + + private static Pair<Pattern, String> compilePattern(@NotNull final String pattern) { + try { + return Pair.create(Pattern.compile(adaptSchemaPattern(pattern)), null); + } catch (PatternSyntaxException e) { + return Pair.create(null, e.getMessage()); + } + } + + public static boolean matchPattern(@NotNull final Pattern pattern, @NotNull final String s) { + try { + return pattern.matcher(StringUtil.newBombedCharSequence(s, 300)).matches(); + } catch (ProcessCanceledException e) { + // something wrong with the pattern, infinite cycle? + Logger.getInstance(JsonSchemaObject.class).info("Pattern matching canceled"); + return false; + } catch (Exception e) { + // catch exceptions around to prevent things like: + // https://bugs.openjdk.java.net/browse/JDK-6984178 + Logger.getInstance(JsonSchemaObject.class).info(e); + return false; + } + } + + @Nullable + public String getTypeDescription(boolean shortDesc) { + JsonSchemaType type = getType(); + if (type != null) return type.getDescription(); + + Set<JsonSchemaType> possibleTypes = getTypeVariants(); + + String description = getTypesDescription(shortDesc, possibleTypes); + if (description != null) return description; + + List<Object> anEnum = getEnum(); + if (anEnum != null) { + return shortDesc ? "enum" : anEnum.stream().map(o -> o.toString()).collect(Collectors.joining(" | ")); + } + + JsonSchemaType guessedType = guessType(); + if (guessedType != null) { + return guessedType.getDescription(); + } + + return null; + } + + @Nullable + public JsonSchemaType guessType() { + // if we have an explicit type, here we are + JsonSchemaType type = getType(); + if (type != null) return type; + + // process type variants before heuristic type detection + final Set<JsonSchemaType> typeVariants = getTypeVariants(); + if (typeVariants != null) { + final int size = typeVariants.size(); + if (size == 1) { + return typeVariants.iterator().next(); + } + else if (size >= 2) { + return null; + } + } + + // heuristic type detection based on the set of applied constraints + boolean hasObjectChecks = hasObjectChecks(); + boolean hasNumericChecks = hasNumericChecks(); + boolean hasStringChecks = hasStringChecks(); + boolean hasArrayChecks = hasArrayChecks(); + + if (hasObjectChecks && !hasNumericChecks && !hasStringChecks && !hasArrayChecks) { + return JsonSchemaType._object; + } + if (!hasObjectChecks && hasNumericChecks && !hasStringChecks && !hasArrayChecks) { + return JsonSchemaType._number; + } + if (!hasObjectChecks && !hasNumericChecks && hasStringChecks && !hasArrayChecks) { + return JsonSchemaType._string; + } + if (!hasObjectChecks && !hasNumericChecks && !hasStringChecks && hasArrayChecks) { + return JsonSchemaType._array; + } + return null; + } + + public boolean hasNumericChecks() { + return getMultipleOf() != null + || getExclusiveMinimumNumber() != null + || getExclusiveMaximumNumber() != null + || getMaximum() != null + || getMinimum() != null; + } + + public boolean hasStringChecks() { + return getPattern() != null || getFormat() != null; + } + + public boolean hasArrayChecks() { + return isUniqueItems() + || getContainsSchema() != null + || getItemsSchema() != null + || getItemsSchemaList() != null + || getMinItems() != null + || getMaxItems() != null; + } + + public boolean hasObjectChecks() { + return !getProperties().isEmpty() + || getPropertyNamesSchema() != null + || getPropertyDependencies() != null + || hasPatternProperties() + || getRequired() != null + || getMinProperties() != null + || getMaxProperties() != null; + } + + @Nullable + static String getTypesDescription(boolean shortDesc, @Nullable Collection<JsonSchemaType> possibleTypes) { + if (possibleTypes == null || possibleTypes.size() == 0) return null; + if (possibleTypes.size() == 1) return possibleTypes.iterator().next().getDescription(); + if (possibleTypes.contains(JsonSchemaType._any)) return JsonSchemaType._any.getDescription(); + + Stream<String> typeDescriptions = possibleTypes.stream().map(t -> t.getDescription()).distinct().sorted(); + boolean isShort = false; + if (shortDesc) { + typeDescriptions = typeDescriptions.limit(3); + if (possibleTypes.size() > 3) isShort = true; + } + return typeDescriptions.collect(Collectors.joining(" | ", "", isShort ? "| ..." : "")); + } + + @Nullable + public JsonSchemaObject resolveRefSchema(@NotNull JsonSchemaService service) { + final String ref = getRef(); + assert !StringUtil.isEmptyOrSpaces(ref); + if (!myComputedRefs.containsKey(ref)){ + JsonSchemaObject value = fetchSchemaFromRefDefinition(ref, this, service); + if (!mySubscribed.get()) { + getJsonObject().getProject().getMessageBus().connect().subscribe(JsonSchemaVfsListener.JSON_DEPS_CHANGED, () -> myComputedRefs.clear()); + mySubscribed.set(true); + } + if (!JsonFileResolver.isHttpPath(ref)) { + service.registerReference(ref); + } + else if (value != null) { + // our aliases - if http ref actually refers to a local file with specific ID + PsiFile file = value.getJsonObject().getContainingFile(); + if (file != null) { + VirtualFile virtualFile = file.getVirtualFile(); + if (virtualFile != null && !(virtualFile instanceof HttpVirtualFile)) { + service.registerReference(virtualFile.getName()); + } + } + } + myComputedRefs.put(ref, value == null ? NULL_OBJ : value); + } + JsonSchemaObject object = myComputedRefs.getOrDefault(ref, null); + return object == NULL_OBJ ? null : object; + } + + @Nullable + private static JsonSchemaObject fetchSchemaFromRefDefinition(@NotNull String ref, + @NotNull final JsonSchemaObject schema, + @NotNull JsonSchemaService service) { + + final VirtualFile schemaFile = schema.getSchemaFile(); + final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter = new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter(ref); + String schemaId = splitter.getSchemaId(); + if (schemaId != null) { + final JsonSchemaObject refSchema = resolveSchemaByReference(service, schemaFile, schemaId); + if (refSchema == null) return null; + return findRelativeDefinition(refSchema, splitter); + } + final JsonSchemaObject rootSchema = service.getSchemaObjectForSchemaFile(schemaFile); + if (rootSchema == null) { + LOG.debug(String.format("Schema object not found for %s", schemaFile.getPath())); + return null; + } + return findRelativeDefinition(rootSchema, splitter); + } + + @Nullable + private static JsonSchemaObject resolveSchemaByReference(@NotNull JsonSchemaService service, + @NotNull VirtualFile schemaFile, + @NotNull String schemaId) { + final VirtualFile refFile = service.findSchemaFileByReference(schemaId, schemaFile); + if (refFile == null) { + LOG.debug(String.format("Schema file not found by reference: '%s' from %s", schemaId, schemaFile.getPath())); + return null; + } + final JsonSchemaObject refSchema = service.getSchemaObjectForSchemaFile(refFile); + if (refSchema == null) { + LOG.debug(String.format("Schema object not found by reference: '%s' from %s", schemaId, schemaFile.getPath())); + return null; + } + return refSchema; + } + + private static JsonSchemaObject findRelativeDefinition(@NotNull final JsonSchemaObject schema, + @NotNull final JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter splitter) { + final String path = splitter.getRelativePath(); + if (StringUtil.isEmptyOrSpaces(path)) { + final String id = splitter.getSchemaId(); + if (isSelfReference(id)) { + return schema; + } + if (id != null && id.startsWith("#")) { + final String resolvedId = JsonCachedValues.resolveId(schema.getJsonObject().getContainingFile(), id); + if (resolvedId == null || id.equals("#" + resolvedId)) return null; + return findRelativeDefinition(schema, new JsonSchemaVariantsTreeBuilder.SchemaUrlSplitter("#" + resolvedId)); + } + return schema; + } + final JsonSchemaObject definition = schema.findRelativeDefinition(path); + if (definition == null) { + LOG.debug(String.format("Definition not found by reference: '%s' in file %s", path, schema.getSchemaFile().getPath())); + } + return definition; + } + + private static class PropertyNamePattern { + @NotNull private final String myPattern; + @Nullable private final Pattern myCompiledPattern; + @Nullable private final String myPatternError; + @NotNull private final Map<String, Boolean> myValuePatternCache; + + PropertyNamePattern(@NotNull String pattern) { + myPattern = StringUtil.unescapeBackSlashes(pattern); + final Pair<Pattern, String> pair = compilePattern(pattern); + myPatternError = pair.getSecond(); + myCompiledPattern = pair.getFirst(); + myValuePatternCache = ContainerUtil.createConcurrentWeakKeyWeakValueMap(); + } + + @Nullable + public String getPatternError() { + return myPatternError; + } + + boolean checkByPattern(@NotNull final String name) { + if (myPatternError != null) return true; + if (Boolean.TRUE.equals(myValuePatternCache.get(name))) return true; + assert myCompiledPattern != null; + boolean matches = matchPattern(myCompiledPattern, name); + myValuePatternCache.put(name, matches); + return matches; + } + + @NotNull + public String getPattern() { + return myPattern; + } + } + + private static class PatternProperties { + @NotNull private final Map<String, JsonSchemaObject> mySchemasMap; + @NotNull private final Map<String, Pattern> myCachedPatterns; + @NotNull private final Map<String, String> myCachedPatternProperties; + @NotNull private final Map<String, String> myInvalidPatterns; + + PatternProperties(@NotNull final Map<String, JsonSchemaObject> schemasMap) { + mySchemasMap = new HashMap<>(); + schemasMap.keySet().forEach(key -> mySchemasMap.put(StringUtil.unescapeBackSlashes(key), schemasMap.get(key))); + myCachedPatterns = new HashMap<>(); + myCachedPatternProperties = ContainerUtil.createConcurrentWeakKeyWeakValueMap(); + myInvalidPatterns = new HashMap<>(); + mySchemasMap.keySet().forEach(key -> { + final Pair<Pattern, String> pair = compilePattern(key); + if (pair.getSecond() != null) { + myInvalidPatterns.put(key, pair.getSecond()); + } else { + assert pair.getFirst() != null; + myCachedPatterns.put(key, pair.getFirst()); + } + }); + } + + @Nullable + public JsonSchemaObject getPatternPropertySchema(@NotNull final String name) { + String value = myCachedPatternProperties.get(name); + if (value != null) { + assert mySchemasMap.containsKey(value); + return mySchemasMap.get(value); + } + + value = myCachedPatterns.keySet().stream() + .filter(key -> matchPattern(myCachedPatterns.get(key), name)) + .findFirst() + .orElse(null); + if (value != null) { + myCachedPatternProperties.put(name, value); + assert mySchemasMap.containsKey(value); + return mySchemasMap.get(value); + } + return null; + } + + @NotNull + public Map<String, String> getInvalidPatterns() { + return myInvalidPatterns; + } + + public JsonSchemaObject getSchemaForPattern(@NotNull String key) { + return mySchemasMap.get(key); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java new file mode 100644 index 00000000..c93c65c0 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReader.java @@ -0,0 +1,511 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.json.psi.*; +import com.intellij.notification.NotificationGroup; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.util.ObjectUtils; +import com.intellij.util.PairConsumer; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 1/13/2017. + */ +public class JsonSchemaReader { + private static final int MAX_SCHEMA_LENGTH = FileUtilRt.MEGABYTE; + public static final Logger LOG = Logger.getInstance(JsonSchemaReader.class); + public static final NotificationGroup ERRORS_NOTIFICATION = NotificationGroup.logOnlyGroup("JSON Schema"); + + private final Map<String, JsonSchemaObject> myIds = new HashMap<>(); + private final ArrayDeque<JsonSchemaObject> myQueue; + + private static final Map<String, MyReader> READERS_MAP = new HashMap<>(); + static { + fillMap(); + } + + public JsonSchemaReader() { + myQueue = new ArrayDeque<>(); + } + + @NotNull + public static JsonSchemaObject readFromFile(@NotNull Project project, @NotNull VirtualFile file) throws Exception { + if (!file.isValid()) { + throw new Exception(String.format("Can not load JSON Schema file '%s'", file.getName())); + } + + final PsiFile psiFile = PsiManager.getInstance(project).findFile(file); + if (!(psiFile instanceof JsonFile)) { + throw new Exception(String.format("Can not load code model for JSON Schema file '%s'", file.getName())); + } + + final JsonObject value = ObjectUtils.tryCast(((JsonFile)psiFile).getTopLevelValue(), JsonObject.class); + if (value == null) { + throw new Exception(String.format("JSON Schema file '%s' must contain only one top-level object", file.getName())); + } + return new JsonSchemaReader().read(value); + } + + @Nullable + public static String checkIfValidJsonSchema(@NotNull final Project project, @NotNull final VirtualFile file) { + final long length = file.getLength(); + final String fileName = file.getName(); + if (length > MAX_SCHEMA_LENGTH) { + return String.format("JSON schema was not loaded from '%s' because it's too large (file size is %d bytes).", fileName, length); + } + if (length == 0) { + return String.format("JSON schema was not loaded from '%s'. File is empty.", fileName); + } + try { + readFromFile(project, file); + } catch (Exception e) { + final String message = String.format("JSON Schema not found or contain error in '%s': %s", fileName, e.getMessage()); + LOG.info(message); + return message; + } + return null; + } + + public JsonSchemaObject read(@NotNull final JsonObject object) { + final JsonSchemaObject root = new JsonSchemaObject(object); + myQueue.add(root); + while (!myQueue.isEmpty()) { + final JsonSchemaObject currentSchema = myQueue.removeFirst(); + + final JsonContainer jsonObject = currentSchema.getJsonObject(); + if (jsonObject instanceof JsonObject) { + final List<JsonProperty> list = ((JsonObject)jsonObject).getPropertyList(); + for (JsonProperty property : list) { + if (property.getValue() == null) continue; + final MyReader reader = READERS_MAP.get(property.getName()); + if (reader != null) { + reader.read(property.getValue(), currentSchema, myQueue); + } + else { + readSingleDefinition(property.getName(), property.getValue(), currentSchema); + } + } + } + else if (jsonObject instanceof JsonArray) { + List<JsonValue> values = ((JsonArray)jsonObject).getValueList(); + for (int i = 0; i < values.size(); i++) { + readSingleDefinition(String.valueOf(i), values.get(i), currentSchema); + } + } + + if (currentSchema.getId() != null) myIds.put(currentSchema.getId(), currentSchema); + } + return root; + } + + public Map<String, JsonSchemaObject> getIds() { + return myIds; + } + + private void readSingleDefinition(@NotNull String name, @NotNull JsonValue value, @NotNull JsonSchemaObject schema) { + if (value instanceof JsonContainer) { + final JsonSchemaObject defined = new JsonSchemaObject((JsonContainer)value); + myQueue.add(defined); + Map<String, JsonSchemaObject> definitions = schema.getDefinitionsMap(); + if (definitions == null) schema.setDefinitionsMap(definitions = new HashMap<>()); + definitions.put(name, defined); + } + } + + private static void fillMap() { + READERS_MAP.put("$id", (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setId(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put("id", (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setId(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put("$schema", (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setSchema(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put("description", (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setDescription(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put(JsonSchemaObject.X_INTELLIJ_HTML_DESCRIPTION, (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setHtmlDescription(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put("title", (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setTitle(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put("$ref", (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setRef(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put("default", createDefault()); + READERS_MAP.put("format", (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setFormat(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put(JsonSchemaObject.DEFINITIONS, createDefinitionsConsumer()); + READERS_MAP.put(JsonSchemaObject.PROPERTIES, createPropertiesConsumer()); + READERS_MAP.put("multipleOf", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMultipleOf(((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("maximum", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMaximum(((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("minimum", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMinimum(((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("exclusiveMaximum", (element, object, queue) -> { + if (element instanceof JsonBooleanLiteral) object.setExclusiveMaximum(((JsonBooleanLiteral)element).getValue()); + if (element instanceof JsonNumberLiteral) object.setExclusiveMaximumNumber(((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("exclusiveMinimum", (element, object, queue) -> { + if (element instanceof JsonBooleanLiteral) object.setExclusiveMinimum(((JsonBooleanLiteral)element).getValue()); + if (element instanceof JsonNumberLiteral) object.setExclusiveMinimumNumber(((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("maxLength", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMaxLength((int)((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("minLength", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMinLength((int)((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("pattern", (element, object, queue) -> { + if (element instanceof JsonStringLiteral) object.setPattern(StringUtil.unquoteString(element.getText())); + }); + READERS_MAP.put(JsonSchemaObject.ADDITIONAL_ITEMS, createAdditionalItems()); + READERS_MAP.put(JsonSchemaObject.ITEMS, createItems()); + READERS_MAP.put("contains", createContains()); + READERS_MAP.put("maxItems", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMaxItems((int)((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("minItems", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMinItems((int)((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("uniqueItems", (element, object, queue) -> { + if (element instanceof JsonBooleanLiteral) object.setUniqueItems(((JsonBooleanLiteral)element).getValue()); + }); + READERS_MAP.put("maxProperties", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMaxProperties((int)((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("minProperties", (element, object, queue) -> { + if (element instanceof JsonNumberLiteral) object.setMinProperties((int)((JsonNumberLiteral)element).getValue()); + }); + READERS_MAP.put("required", createRequired()); + READERS_MAP.put("additionalProperties", createAdditionalProperties()); + READERS_MAP.put("propertyNames", createPropertyNames()); + READERS_MAP.put("patternProperties", createPatternProperties()); + READERS_MAP.put("dependencies", createDependencies()); + READERS_MAP.put("enum", createEnum()); + READERS_MAP.put("const", (element, object, queue) -> { + if (element instanceof JsonValue) object.setEnum(ContainerUtil.createMaybeSingletonList(readEnumValue((JsonValue)element))); + }); + READERS_MAP.put("type", createType()); + READERS_MAP.put("allOf", createContainer((object, members) -> object.setAllOf(members))); + READERS_MAP.put("anyOf", createContainer((object, members) -> object.setAnyOf(members))); + READERS_MAP.put("oneOf", createContainer((object, members) -> object.setOneOf(members))); + READERS_MAP.put("not", createNot()); + READERS_MAP.put("if", createIf()); + READERS_MAP.put("then", createThen()); + READERS_MAP.put("else", createElse()); + READERS_MAP.put("instanceof", ((element, object, queue) -> object.shouldValidateAgainstJSType())); + READERS_MAP.put("typeof", ((element, object, queue) -> object.shouldValidateAgainstJSType())); + } + + private static MyReader createIf() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element); + queue.add(ifSchema); + object.setIf(ifSchema); + } + }; + } + + private static MyReader createThen() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element); + queue.add(ifSchema); + object.setThen(ifSchema); + } + }; + } + + private static MyReader createElse() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonSchemaObject ifSchema = new JsonSchemaObject((JsonObject)element); + queue.add(ifSchema); + object.setElse(ifSchema); + } + }; + } + + private static MyReader createNot() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonSchemaObject not = new JsonSchemaObject((JsonObject)element); + queue.add(not); + object.setNot(not); + } + }; + } + + private static MyReader createContainer(@NotNull final PairConsumer<JsonSchemaObject, List<JsonSchemaObject>> delegate) { + return (element, object, queue) -> { + if (element instanceof JsonArray) { + final List<JsonValue> list = ((JsonArray)element).getValueList(); + final List<JsonSchemaObject> members = list.stream().filter(el -> el instanceof JsonObject) + .map(el -> { + final JsonSchemaObject child = new JsonSchemaObject((JsonObject)el); + queue.add(child); + return child; + }).collect(Collectors.toList()); + delegate.consume(object, members); + } + }; + } + + private static MyReader createType() { + return (element, object, queue) -> { + if (element instanceof JsonStringLiteral) { + final JsonSchemaType type = parseType(StringUtil.unquoteString(element.getText())); + if (type != null) object.setType(type); + } else if (element instanceof JsonArray) { + final Set<JsonSchemaType> typeList = ((JsonArray)element).getValueList().stream() + .filter(notEmptyString()).map(el -> parseType(StringUtil.unquoteString(el.getText()))) + .filter(el -> el != null).collect(Collectors.toSet()); + if (!typeList.isEmpty()) object.setTypeVariants(typeList); + } + }; + } + + @Nullable + private static JsonSchemaType parseType(@NotNull final String typeString) { + try { + return JsonSchemaType.valueOf("_" + typeString); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Nullable + private static Object readEnumValue(JsonValue value) { + if (value instanceof JsonStringLiteral) { + return "\"" + StringUtil.unquoteString(((JsonStringLiteral)value).getValue()) + "\""; + } else if (value instanceof JsonNumberLiteral) { + return getNumber((JsonNumberLiteral)value); + } else if (value instanceof JsonBooleanLiteral) { + return ((JsonBooleanLiteral)value).getValue(); + } else if (value instanceof JsonNullLiteral) { + return "null"; + } else if (value instanceof JsonArray) { + return new EnumArrayValueWrapper(((JsonArray)value).getValueList().stream().map(v -> readEnumValue(v)).filter(v -> v != null).toArray()); + } else if (value instanceof JsonObject) { + return new EnumObjectValueWrapper(((JsonObject)value).getPropertyList().stream() + .map(p -> Pair.create(p.getName(), readEnumValue(p.getValue()))) + .filter(p -> p.second != null) + .collect(Collectors.toMap(p -> p.first, p -> p.second))); + } + return null; + } + + private static MyReader createEnum() { + return (element, object, queue) -> { + if (element instanceof JsonArray) { + final List<Object> objects = new ArrayList<>(); + final List<JsonValue> list = ((JsonArray)element).getValueList(); + for (JsonValue value : list) { + Object enumValue = readEnumValue(value); + if (enumValue == null) return; // don't validate if we have unsupported entity kinds + objects.add(enumValue); + } + object.setEnum(objects); + } + }; + } + + @NotNull + private static Number getNumber(@NotNull JsonNumberLiteral value) { + Number numberValue; + try { + numberValue = Integer.parseInt(value.getText()); + } catch (NumberFormatException e) { + numberValue = value.getValue(); + } + return numberValue; + } + + private static MyReader createDependencies() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final HashMap<String, List<String>> propertyDependencies = new HashMap<>(); + final HashMap<String, JsonSchemaObject> schemaDependencies = new HashMap<>(); + + final List<JsonProperty> list = ((JsonObject)element).getPropertyList(); + for (JsonProperty property : list) { + if (property.getValue() == null) continue; + if (property.getValue() instanceof JsonArray) { + final List<String> dependencies = ((JsonArray)property.getValue()).getValueList().stream() + .filter(notEmptyString()) + .map(el -> StringUtil.unquoteString(el.getText())).collect(Collectors.toList()); + if (!dependencies.isEmpty()) propertyDependencies.put(property.getName(), dependencies); + } else if (property.getValue() instanceof JsonObject) { + final JsonSchemaObject child = new JsonSchemaObject((JsonObject)property.getValue()); + queue.add(child); + schemaDependencies.put(property.getName(), child); + } + } + + object.setPropertyDependencies(propertyDependencies); + object.setSchemaDependencies(schemaDependencies); + } + }; + } + + @NotNull + private static Predicate<JsonValue> notEmptyString() { + return el -> el instanceof JsonStringLiteral && !StringUtil.isEmptyOrSpaces(el.getText()); + } + + private static MyReader createPatternProperties() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + object.setPatternProperties(readInnerObject((JsonObject)element, queue)); + } + }; + } + + private static MyReader createAdditionalProperties() { + return (element, object, queue) -> { + if (element instanceof JsonBooleanLiteral) { + object.setAdditionalPropertiesAllowed(((JsonBooleanLiteral)element).getValue()); + } else if (element instanceof JsonObject) { + final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element); + queue.add(schema); + object.setAdditionalPropertiesSchema(schema); + } + }; + } + + private static MyReader createPropertyNames() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element); + queue.add(schema); + object.setPropertyNamesSchema(schema); + } + }; + } + + private static MyReader createRequired() { + return (element, object, queue) -> { + if (element instanceof JsonArray) { + object.setRequired(((JsonArray)element).getValueList().stream() + .filter(notEmptyString()) + .map(el -> StringUtil.unquoteString(el.getText())).collect(Collectors.toSet())); + } + }; + } + + private static MyReader createItems() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element); + queue.add(schema); + object.setItemsSchema(schema); + } else if (element instanceof JsonArray) { + final List<JsonSchemaObject> list = new ArrayList<>(); + final List<JsonValue> values = ((JsonArray)element).getValueList(); + for (JsonValue value : values) { + if (value instanceof JsonObject) { + final JsonSchemaObject child = new JsonSchemaObject((JsonObject)value); + queue.add(child); + list.add(child); + } + } + object.setItemsSchemaList(list); + } + }; + } + + private static MyReader createContains() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonSchemaObject schema = new JsonSchemaObject((JsonObject)element); + queue.add(schema); + object.setContainsSchema(schema); + } + }; + } + + private static MyReader createAdditionalItems() { + return (element, object, queue) -> { + if (element instanceof JsonBooleanLiteral) { + object.setAdditionalItemsAllowed(((JsonBooleanLiteral)element).getValue()); + } else if (element instanceof JsonObject) { + final JsonSchemaObject additionalItemsSchema = new JsonSchemaObject((JsonObject)element); + queue.add(additionalItemsSchema); + object.setAdditionalItemsSchema(additionalItemsSchema); + } + }; + } + + private static MyReader createPropertiesConsumer() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + object.setProperties(readInnerObject((JsonObject)element, queue)); + } + }; + } + + private static MyReader createDefinitionsConsumer() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonObject definitions = (JsonObject)element; + object.setDefinitionsMap(readInnerObject(definitions, queue)); + } + }; + } + + @NotNull + private static Map<String, JsonSchemaObject> readInnerObject(@NotNull JsonObject element, + @NotNull Collection<JsonSchemaObject> queue) { + final List<JsonProperty> properties = element.getPropertyList(); + final Map<String, JsonSchemaObject> map = new HashMap<>(); + for (JsonProperty property : properties) { + if (!(property.getValue() instanceof JsonObject)) continue; + final JsonSchemaObject child = new JsonSchemaObject((JsonObject)property.getValue()); + queue.add(child); + map.put(property.getName(), child); + } + return map; + } + + private static MyReader createDefault() { + return (element, object, queue) -> { + if (element instanceof JsonObject) { + final JsonSchemaObject schemaObject = new JsonSchemaObject((JsonObject)element); + queue.add(schemaObject); + object.setDefault(schemaObject); + } else if (element instanceof JsonStringLiteral) { + object.setDefault(StringUtil.unquoteString(((JsonStringLiteral)element).getValue())); + } else if (element instanceof JsonNumberLiteral) { + object.setDefault(getNumber((JsonNumberLiteral) element)); + } else if (element instanceof JsonBooleanLiteral) { + object.setDefault(((JsonBooleanLiteral)element).getValue()); + } + }; + } + + private interface MyReader { + void read(@NotNull JsonElement source, @NotNull JsonSchemaObject target, @NotNull Collection<JsonSchemaObject> processingQueue); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java new file mode 100644 index 00000000..9ff7243c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaReferenceContributor.java @@ -0,0 +1,94 @@ +/* + * 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.impl; + +import com.intellij.codeInsight.completion.CompletionUtil; +import com.intellij.json.psi.*; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.patterns.PsiElementPattern; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReferenceContributor; +import com.intellij.psi.PsiReferenceRegistrar; +import com.intellij.psi.filters.ElementFilter; +import com.intellij.psi.filters.position.FilterPattern; +import com.intellij.util.ObjectUtils; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Irina.Chernushina on 3/31/2016. + */ +public class JsonSchemaReferenceContributor extends PsiReferenceContributor { + public static final PsiElementPattern.Capture<JsonValue> REF_PATTERN = createPropertyValuePattern("$ref", true, false); + public static final PsiElementPattern.Capture<JsonValue> SCHEMA_PATTERN = createPropertyValuePattern("$schema", false, true); + public static final PsiElementPattern.Capture<JsonStringLiteral> REQUIRED_PROP_PATTERN = createRequiredPropPattern(); + + @Override + public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) { + registrar.registerReferenceProvider(REF_PATTERN, new JsonPointerReferenceProvider(false)); + registrar.registerReferenceProvider(SCHEMA_PATTERN, new JsonPointerReferenceProvider(true)); + registrar.registerReferenceProvider(REQUIRED_PROP_PATTERN, new JsonRequiredPropsReferenceProvider()); + } + + private static PsiElementPattern.Capture<JsonValue> createPropertyValuePattern( + @SuppressWarnings("SameParameterValue") @NotNull final String propertyName, boolean schemaOnly, boolean rootOnly) { + + return PlatformPatterns.psiElement(JsonValue.class).and(new FilterPattern(new ElementFilter() { + @Override + public boolean isAcceptable(Object element, @Nullable PsiElement context) { + if (element instanceof JsonValue) { + final JsonValue value = (JsonValue) element; + if (schemaOnly && !JsonSchemaService.isSchemaFile(CompletionUtil.getOriginalOrSelf(value.getContainingFile()))) return false; + + final JsonProperty property = ObjectUtils.tryCast(value.getParent(), JsonProperty.class); + if (property != null && property.getValue() == element) { + final PsiFile file = property.getContainingFile(); + if (rootOnly && (!(file instanceof JsonFile) || ((JsonFile)file).getTopLevelValue() != property.getParent())) return false; + return propertyName.equals(property.getName()); + } + } + return false; + } + + @Override + public boolean isClassAcceptable(Class hintClass) { + return true; + } + })); + } + + private static PsiElementPattern.Capture<JsonStringLiteral> createRequiredPropPattern() { + return PlatformPatterns.psiElement(JsonStringLiteral.class).and(new FilterPattern(new ElementFilter() { + @Override + public boolean isAcceptable(Object element, @Nullable PsiElement context) { + if (!(element instanceof JsonStringLiteral)) return false; + if (!JsonSchemaService.isSchemaFile(((JsonStringLiteral)element).getContainingFile())) return false; + final PsiElement parent = ((JsonStringLiteral)element).getParent(); + if (!(parent instanceof JsonArray)) return false; + PsiElement property = parent.getParent(); + if (!(property instanceof JsonProperty)) return false; + return "required".equals(((JsonProperty)property).getName()); + } + + @Override + public boolean isClassAcceptable(Class hintClass) { + return true; + } + })); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java new file mode 100644 index 00000000..588551ca --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaRegexInjector.java @@ -0,0 +1,68 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.lang.injection.MultiHostInjector; +import com.intellij.lang.injection.MultiHostRegistrar; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiLanguageInjectionHost; +import com.intellij.util.ThreeState; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.intellij.lang.regexp.ecmascript.EcmaScriptRegexpLanguage; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class JsonSchemaRegexInjector implements MultiHostInjector { + @Override + public void getLanguagesToInject(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement context) { + if (!JsonSchemaService.isSchemaFile(context.getContainingFile())) return; + JsonOriginalPsiWalker walker = JsonLikePsiWalker.JSON_ORIGINAL_PSI_WALKER; + if (!(context instanceof JsonStringLiteral)) return; + ThreeState isName = walker.isName(context); + List<JsonSchemaVariantsTreeBuilder.Step> position = walker.findPosition(context, isName == ThreeState.NO); + if (position == null || position.isEmpty()) return; + if (isName == ThreeState.YES) { + JsonSchemaVariantsTreeBuilder.Step lastStep = ContainerUtil.getLastItem(position); + if (lastStep != null && "patternProperties".equals(lastStep.getName())) { + if (isNestedInPropertiesList(position)) return; + injectForHost(registrar, (JsonStringLiteral)context); + } + } + else if (isName == ThreeState.NO) { + JsonSchemaVariantsTreeBuilder.Step lastStep = ContainerUtil.getLastItem(position); + if (lastStep != null && "pattern".equals(lastStep.getName())) { + if (isNestedInPropertiesList(position)) return; + injectForHost(registrar, (JsonStringLiteral)context); + } + } + } + + private static boolean isNestedInPropertiesList(List<JsonSchemaVariantsTreeBuilder.Step> position) { + if (position.size() >= 2) { + JsonSchemaVariantsTreeBuilder.Step prev = position.get(position.size() - 2); + if ("properties".equals(prev.getName())) return true; + } + return false; + } + + private static void injectForHost(@NotNull MultiHostRegistrar registrar, @NotNull JsonStringLiteral host) { + List<Pair<TextRange, String>> fragments = host.getTextFragments(); + if (fragments.isEmpty()) return; + registrar.startInjecting(EcmaScriptRegexpLanguage.INSTANCE); + for (Pair<TextRange, String> fragment : fragments) { + registrar.addPlace(null, null, (PsiLanguageInjectionHost)host, fragment.first); + } + registrar.doneInjecting(); + } + + @NotNull + @Override + public List<? extends Class<? extends PsiElement>> elementsToInjectIn() { + return ContainerUtil.list(JsonStringLiteral.class); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java new file mode 100644 index 00000000..269b94af --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaResolver.java @@ -0,0 +1,144 @@ +/* + * 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.impl; + +import com.intellij.json.psi.*; +import com.intellij.openapi.util.Ref; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter; +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 java.util.stream.Collectors; + +import static com.jetbrains.jsonSchema.impl.JsonSchemaAnnotatorChecker.areSchemaTypesCompatible; + +/** + * @author Irina.Chernushina on 4/24/2017. + */ +public class JsonSchemaResolver { + @NotNull private final JsonSchemaObject mySchema; + private final boolean myIsName; + @NotNull private final List<JsonSchemaVariantsTreeBuilder.Step> myPosition; + + public JsonSchemaResolver(@NotNull JsonSchemaObject schema, boolean isName, @NotNull List<JsonSchemaVariantsTreeBuilder.Step> position) { + mySchema = schema; + myIsName = isName; + myPosition = position; + } + + public JsonSchemaResolver(@NotNull JsonSchemaObject schema) { + mySchema = schema; + myIsName = true; + myPosition = Collections.emptyList(); + } + + public MatchResult detailedResolve() { + final JsonSchemaTreeNode node = JsonSchemaVariantsTreeBuilder.buildTree(mySchema, myPosition, false, false, !myIsName); + return MatchResult.create(node); + } + + @NotNull + public Collection<JsonSchemaObject> resolve() { + final MatchResult result = detailedResolve(); + final List<JsonSchemaObject> list = new ArrayList<>(result.mySchemas); + list.addAll(result.myExcludingSchemas.stream().flatMap(Collection::stream).collect(Collectors.toList())); + return list; + } + + @Nullable + public PsiElement findNavigationTarget(boolean literalResolve, + @Nullable final JsonValue element, + boolean acceptAdditionalPropertiesSchema) { + final JsonSchemaTreeNode node = JsonSchemaVariantsTreeBuilder + .buildTree(mySchema, myPosition, true, literalResolve, acceptAdditionalPropertiesSchema || !myIsName); + return getSchemaNavigationItem(selectSchema(node, element, myPosition.isEmpty())); + } + + @Nullable + private static JsonSchemaObject selectSchema(@NotNull final JsonSchemaTreeNode resolveRoot, + @Nullable final JsonValue element, boolean topLevelSchema) { + final MatchResult matchResult = MatchResult.create(resolveRoot); + List<JsonSchemaObject> schemas = new ArrayList<>(matchResult.mySchemas); + schemas.addAll(matchResult.myExcludingSchemas.stream().flatMap(Collection::stream).collect(Collectors.toList())); + + final JsonSchemaObject firstSchema = getFirstValidSchema(schemas); + if (element == null || schemas.size() == 1 || firstSchema == null) { + return firstSchema; + } + // actually we pass any schema here + final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, firstSchema); + JsonValueAdapter adapter; + if (walker == null || (adapter = walker.createValueAdapter(element)) == null) return null; + + final JsonValueAdapter parentAdapter; + if (topLevelSchema) { + parentAdapter = null; + } else { + final JsonValue parentValue = PsiTreeUtil.getParentOfType(PsiTreeUtil.getParentOfType(element, JsonProperty.class), + JsonObject.class, JsonArray.class); + if (parentValue == null || (parentAdapter = walker.createValueAdapter(parentValue)) == null) return null; + } + + final Ref<JsonSchemaObject> schemaRef = new Ref<>(); + MatchResult.iterateTree(resolveRoot, node -> { + final JsonSchemaTreeNode parent = node.getParent(); + if (node.getSchema() == null || parentAdapter != null && parent != null && parent.isNothing()) return true; + if (!isCorrect(adapter, node.getSchema())) return true; + if (parentAdapter == null || + parent == null || + parent.getSchema() == null || + parent.isAny() || + isCorrect(parentAdapter, parent.getSchema())) { + schemaRef.set(node.getSchema()); + return false; + } + return true; + }); + return schemaRef.get(); + } + + @Nullable + private static JsonSchemaObject getFirstValidSchema(List<JsonSchemaObject> schemas) { + return schemas.stream().filter(schema -> schema.getJsonObject().isValid()).findFirst().orElse(null); + } + + private static boolean isCorrect(@NotNull final JsonValueAdapter value, @NotNull final JsonSchemaObject schema) { + if (!schema.getJsonObject().isValid()) return false; + final JsonSchemaType type = JsonSchemaType.getType(value); + if (type == null) return true; + if (!areSchemaTypesCompatible(schema, type)) return false; + final JsonSchemaAnnotatorChecker checker = new JsonSchemaAnnotatorChecker(JsonComplianceCheckerOptions.RELAX_ENUM_CHECK); + checker.checkByScheme(value, schema); + return checker.isCorrect(); + } + + @Nullable + private static JsonValue getSchemaNavigationItem(@Nullable final JsonSchemaObject schema) { + if (schema == null) return null; + final JsonContainer jsonObject = schema.getJsonObject(); + if (jsonObject.getParent() instanceof JsonProperty) { + return ((JsonProperty)jsonObject.getParent()).getNameElement(); + } + return jsonObject; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java new file mode 100644 index 00000000..0c07625c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaServiceImpl.java @@ -0,0 +1,488 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.json.JsonUtil; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.ClearableLazyValue; +import com.intellij.openapi.util.Factory; +import com.intellij.openapi.util.ModificationTracker; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; +import com.intellij.util.containers.ConcurrentList; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.MultiMap; +import com.intellij.util.messages.MessageBusConnection; +import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration; +import com.jetbrains.jsonSchema.JsonSchemaVfsListener; +import com.jetbrains.jsonSchema.extension.*; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.remote.JsonFileResolver; +import com.jetbrains.jsonSchema.remote.JsonSchemaCatalogManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +public class JsonSchemaServiceImpl implements JsonSchemaService { + @NotNull private final Project myProject; + @NotNull private final MyState myState; + @NotNull private final ClearableLazyValue<Set<String>> myBuiltInSchemaIds; + @NotNull private final Set<String> myRefs = ContainerUtil.newConcurrentSet(); + private final AtomicLong myAnyChangeCount = new AtomicLong(0); + private final ModificationTracker myAnySchemaChangeTracker; + + @NotNull private final JsonSchemaCatalogManager myCatalogManager; + + public JsonSchemaServiceImpl(@NotNull Project project) { + myProject = project; + myState = new MyState(() -> getProvidersFromFactories(), myProject); + myBuiltInSchemaIds = new ClearableLazyValue<Set<String>>() { + @NotNull + @Override + protected Set<String> compute() { + return myState.getFiles().stream().map(f -> JsonCachedValues.getSchemaId(f, myProject)).collect(Collectors.toSet()); + } + }; + myAnySchemaChangeTracker = () -> myAnyChangeCount.get(); + myCatalogManager = new JsonSchemaCatalogManager(myProject); + + MessageBusConnection connection = project.getMessageBus().connect(); + connection.subscribe(JsonSchemaVfsListener.JSON_SCHEMA_CHANGED, myAnyChangeCount::incrementAndGet); + connection.subscribe(JsonSchemaVfsListener.JSON_DEPS_CHANGED, () -> { + myRefs.clear(); + myAnyChangeCount.incrementAndGet(); + }); + JsonSchemaVfsListener.startListening(project, this, connection); + myCatalogManager.startUpdates(); + } + + @Override + public ModificationTracker getAnySchemaChangeTracker() { + return myAnySchemaChangeTracker; + } + + private List<JsonSchemaFileProvider> getProvidersFromFactories() { + List<JsonSchemaFileProvider> providers = new ArrayList<>(); + for (JsonSchemaProviderFactory factory : getProviderFactories()) { + try { + providers.addAll(factory.getProviders(myProject)); + } + catch (Exception e) { + Logger.getInstance(JsonSchemaService.class).error(e); + } + } + return providers; + } + + @NotNull + protected JsonSchemaProviderFactory[] getProviderFactories() { + return JsonSchemaProviderFactory.EP_NAME.getExtensions(); + } + + @Nullable + @Override + public JsonSchemaFileProvider getSchemaProvider(@NotNull VirtualFile schemaFile) { + return myState.getProvider(schemaFile); + } + + @Override + public void reset() { + myState.reset(); + myBuiltInSchemaIds.drop(); + myAnyChangeCount.incrementAndGet(); + for (Runnable action: myResetActions) { + action.run(); + } + DaemonCodeAnalyzer.getInstance(myProject).restart(); + } + + @Override + @Nullable + public VirtualFile findSchemaFileByReference(@NotNull String reference, @Nullable VirtualFile referent) { + final Optional<VirtualFile> optional = findBuiltInSchemaByReference(reference); + return optional.orElseGet(() -> { + if (reference.startsWith("#")) return referent; + return JsonFileResolver.resolveSchemaByReference(referent, JsonSchemaService.normalizeId(reference)); + }); + } + + private Optional<VirtualFile> findBuiltInSchemaByReference(@NotNull String reference) { + String id = JsonSchemaService.normalizeId(reference); + if (!myBuiltInSchemaIds.getValue().contains(id)) return Optional.empty(); + return myState.getFiles().stream() + .filter(file -> id.equals(JsonCachedValues.getSchemaId(file, myProject))) + .findFirst(); + } + + @Override + @NotNull + public Collection<VirtualFile> getSchemaFilesForFile(@NotNull final VirtualFile file) { + return getSchemasForFile(file, false, false); + } + + @NotNull + public Collection<VirtualFile> getSchemasForFile(@NotNull VirtualFile file, boolean single, boolean onlyUserSchemas) { + String schemaUrl = null; + if (!onlyUserSchemas) { + // prefer schema-schema if it is specified in "$schema" property + schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject); + if (isSchemaUrl(schemaUrl)) { + final VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file); + if (virtualFile != null) return Collections.singletonList(virtualFile); + } + } + + + List<JsonSchemaFileProvider> providers = getProvidersForFile(file); + + // proper priority: + // 1) user providers + // 2) $schema property + // 3) built-in providers + // 4) schema catalog + + boolean checkSchemaProperty = true; + if (!onlyUserSchemas && providers.stream().noneMatch(p -> p.getSchemaType() == SchemaType.userSchema)) { + if (schemaUrl == null) schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject); + VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file); + if (virtualFile != null) return Collections.singletonList(virtualFile); + checkSchemaProperty = false; + } + + if (!single) { + List<VirtualFile> files = ContainerUtil.newArrayList(); + for (JsonSchemaFileProvider provider : providers) { + VirtualFile schemaFile = getSchemaForProvider(myProject, provider); + if (schemaFile != null) { + files.add(schemaFile); + } + } + if (!files.isEmpty()) { + return files; + } + } + else if (!providers.isEmpty()) { + final JsonSchemaFileProvider selected; + if (providers.size() > 2) return ContainerUtil.emptyList(); + if (providers.size() > 1) { + final Optional<JsonSchemaFileProvider> userSchema = + providers.stream().filter(provider -> SchemaType.userSchema.equals(provider.getSchemaType())).findFirst(); + if (!userSchema.isPresent()) return ContainerUtil.emptyList(); + selected = userSchema.get(); + } else selected = providers.get(0); + VirtualFile schemaFile = getSchemaForProvider(myProject, selected); + return ContainerUtil.createMaybeSingletonList(schemaFile); + } + + if (onlyUserSchemas) { + return ContainerUtil.emptyList(); + } + + if (checkSchemaProperty) { + if (schemaUrl == null) schemaUrl = JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject); + VirtualFile virtualFile = resolveFromSchemaProperty(schemaUrl, file); + if (virtualFile != null) return Collections.singletonList(virtualFile); + } + + return ContainerUtil.createMaybeSingletonList(resolveSchemaFromOtherSources(file)); + } + + @NotNull + public List<JsonSchemaFileProvider> getProvidersForFile(@NotNull VirtualFile file) { + return myState.getProviders().stream().filter(provider -> isProviderAvailable(file, provider)).collect( + Collectors.toList()); + } + + @Nullable + private VirtualFile resolveFromSchemaProperty(@Nullable String schemaUrl, @NotNull VirtualFile file) { + if (schemaUrl != null) { + VirtualFile virtualFile = findSchemaFileByReference(schemaUrl, file); + if (virtualFile != null) return virtualFile; + } + return null; + } + + @Override + public List<JsonSchemaInfo> getAllUserVisibleSchemas() { + List<String> schemas = myCatalogManager.getAllCatalogSchemas(); + Collection<? extends JsonSchemaFileProvider> providers = myState.getProviders(); + List<JsonSchemaInfo> results = ContainerUtil.newArrayListWithCapacity(schemas.size() + providers.size()); + Set<String> processedRemotes = ContainerUtil.newHashSet(); + for (JsonSchemaFileProvider provider: providers) { + if (provider.isUserVisible()) { + if (provider.getRemoteSource() != null) { + if (processedRemotes.add(provider.getRemoteSource())) { + results.add(new JsonSchemaInfo(provider)); + } + } + else { + results.add(new JsonSchemaInfo(provider)); + } + } + } + + for (String schema: schemas) { + if (processedRemotes.add(schema)) { + results.add(new JsonSchemaInfo(schema)); + } + } + return results; + } + + @Nullable + @Override + public JsonSchemaObject getSchemaObject(@NotNull final VirtualFile file) { + Collection<VirtualFile> schemas = getSchemasForFile(file, true, false); + if (schemas.size() == 0) return null; + assert schemas.size() == 1; + VirtualFile schemaFile = schemas.iterator().next(); + return JsonCachedValues.getSchemaObject(replaceHttpFileWithBuiltinIfNeeded(schemaFile), myProject); + } + + public VirtualFile replaceHttpFileWithBuiltinIfNeeded(VirtualFile schemaFile) { + // this hack is needed to handle user-defined mappings via urls + // we cannot perform that inside corresponding provider, because it leads to recursive component dependency + // this way we're preventing http files when a built-in schema exists + if (!JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isPreferRemoteSchemas() + && schemaFile instanceof HttpVirtualFile) { + String url = schemaFile.getUrl(); + VirtualFile first1 = getLocalSchemaByUrl(url); + return first1 != null ? first1 : schemaFile; + } + return schemaFile; + } + + @Nullable + public VirtualFile getLocalSchemaByUrl(String url) { + return myState.getFiles().stream() + .filter(f -> { + JsonSchemaFileProvider prov = getSchemaProvider(f); + return prov != null && !(prov.getSchemaFile() instanceof HttpVirtualFile) + && (url.equals(prov.getRemoteSource()) || JsonFileResolver.replaceUnsafeSchemaStoreUrls(url).equals(prov.getRemoteSource()) + || url.equals(JsonFileResolver.replaceUnsafeSchemaStoreUrls(prov.getRemoteSource()))); + }).findFirst().orElse(null); + } + + @Nullable + @Override + public JsonSchemaObject getSchemaObjectForSchemaFile(@NotNull VirtualFile schemaFile) { + return JsonCachedValues.getSchemaObject(schemaFile, myProject); + } + + @Override + public boolean isSchemaFile(@NotNull VirtualFile file) { + return JsonUtil.isJsonFile(file) && (isMappedSchema(file) + || isSchemaByProvider(file) + || hasSchemaSchema(file)); + } + + private boolean isMappedSchema(@NotNull VirtualFile file) { + return isMappedSchema(file, true); + } + + public boolean isMappedSchema(@NotNull VirtualFile file, boolean canRecompute) { + return (canRecompute || myState.isComputed()) && myState.getFiles().contains(file); + } + + private boolean isSchemaByProvider(@NotNull VirtualFile file) { + JsonSchemaFileProvider provider = myState.getProvider(file); + if (provider == null) { + for (JsonSchemaFileProvider stateProvider: myState.getProviders()) { + if (isSchemaProvider(stateProvider) && stateProvider.isAvailable(file)) + return true; + } + return false; + } + return isSchemaProvider(provider); + } + + private static boolean isSchemaProvider(JsonSchemaFileProvider provider) { + VirtualFile schemaFile = provider.getSchemaFile(); + if (!(schemaFile instanceof HttpVirtualFile)) return false; + String url = schemaFile.getUrl(); + return isSchemaUrl(url); + } + + private static boolean isSchemaUrl(@Nullable String url) { + return url != null && url.startsWith("http://json-schema.org/") && (url.endsWith("/schema") || url.endsWith("/schema#")); + } + + @Override + public JsonSchemaVersion getSchemaVersion(@NotNull VirtualFile file) { + if (isMappedSchema(file)) { + JsonSchemaFileProvider provider = myState.getProvider(file); + if (provider != null) { + return provider.getSchemaVersion(); + } + } + + return getSchemaVersionFromSchemaUrl(file); + } + + @Nullable + private JsonSchemaVersion getSchemaVersionFromSchemaUrl(@NotNull VirtualFile file) { + Ref<String> res = Ref.create(null); + //noinspection CodeBlock2Expr + ApplicationManager.getApplication().runReadAction(() -> { + res.set(JsonCachedValues.getSchemaUrlFromSchemaProperty(file, myProject)); + }); + if (res.isNull()) return null; + return JsonSchemaVersion.byId(res.get()); + } + + private boolean hasSchemaSchema(VirtualFile file) { + return getSchemaVersionFromSchemaUrl(file) != null; + } + + private static boolean isProviderAvailable(@NotNull final VirtualFile file, @NotNull JsonSchemaFileProvider provider) { + return provider.isAvailable(file); + } + + @Nullable + private VirtualFile resolveSchemaFromOtherSources(@NotNull VirtualFile file) { + return myCatalogManager.getSchemaFileForFile(file); + } + + @Override + public void registerRemoteUpdateCallback(Runnable callback) { + myCatalogManager.registerCatalogUpdateCallback(callback); + } + + @Override + public void unregisterRemoteUpdateCallback(Runnable callback) { + myCatalogManager.unregisterCatalogUpdateCallback(callback); + } + + private final ConcurrentList<Runnable> myResetActions = ContainerUtil.createConcurrentList(); + + @Override + public void registerResetAction(Runnable action) { + myResetActions.add(action); + } + + @Override + public void unregisterResetAction(Runnable action) { + myResetActions.remove(action); + } + + @Override + public void registerReference(String ref) { + int index = StringUtil.lastIndexOfAny(ref, "\\/"); + if (index >= 0) { + ref = ref.substring(index + 1); + } + myRefs.add(ref); + } + + @Override + public boolean possiblyHasReference(String ref) { + return myRefs.contains(ref); + } + + @Override + public void triggerUpdateRemote() { + myCatalogManager.triggerUpdateCatalog(myProject); + } + + @Override + public boolean isApplicableToFile(@Nullable VirtualFile file) { + if (file == null) return false; + for (JsonSchemaEnabler e : JsonSchemaEnabler.EXTENSION_POINT_NAME.getExtensionList()) { + if (e.isEnabledForFile(file)) { + return true; + } + } + return false; + } + + private static class MyState { + @NotNull private final Factory<List<JsonSchemaFileProvider>> myFactory; + @NotNull private final Project myProject; + @NotNull private final ClearableLazyValue<MultiMap<VirtualFile, JsonSchemaFileProvider>> myData; + private final AtomicBoolean myIsComputed = new AtomicBoolean(false); + + private MyState(@NotNull final Factory<List<JsonSchemaFileProvider>> factory, @NotNull Project project) { + myFactory = factory; + myProject = project; + myData = new ClearableLazyValue<MultiMap<VirtualFile, JsonSchemaFileProvider>>() { + @NotNull + @Override + public MultiMap<VirtualFile, JsonSchemaFileProvider> compute() { + myIsComputed.set(true); + return createFileProviderMap(myFactory.create(), myProject); + } + + @NotNull + @Override + public final synchronized MultiMap<VirtualFile, JsonSchemaFileProvider> getValue() { + return super.getValue(); + } + + @Override + public final synchronized void drop() { + myIsComputed.set(false); + super.drop(); + } + }; + } + + public void reset() { + myData.drop(); + } + + @NotNull + public Collection<? extends JsonSchemaFileProvider> getProviders() { + return myData.getValue().values(); + } + + @NotNull + public Set<VirtualFile> getFiles() { + return myData.getValue().keySet(); + } + + @Nullable + public JsonSchemaFileProvider getProvider(@NotNull final VirtualFile file) { + final Collection<JsonSchemaFileProvider> providers = myData.getValue().get(file); + return providers.stream().filter(p -> p.getSchemaType() == SchemaType.userSchema).findFirst().orElse(providers.stream().findFirst().orElse(null)); + } + + public boolean isComputed() { + return myIsComputed.get(); + } + + @NotNull + private static MultiMap<VirtualFile, JsonSchemaFileProvider> createFileProviderMap(@NotNull final List<JsonSchemaFileProvider> list, + @NotNull Project project) { + // if there are different providers with the same schema files, + // stream API does not allow to collect same keys with Collectors.toMap(): throws duplicate key + final MultiMap<VirtualFile, JsonSchemaFileProvider> map = MultiMap.create(); + for (JsonSchemaFileProvider provider : list) { + VirtualFile schemaFile = getSchemaForProvider(project, provider); + if (schemaFile != null) { + map.putValue(schemaFile, provider); + } + } + return map; + } + } + + @Nullable + private static VirtualFile getSchemaForProvider(@NotNull Project project, @NotNull JsonSchemaFileProvider provider) { + if (JsonSchemaCatalogProjectConfiguration.getInstance(project).isPreferRemoteSchemas()) { + final String source = provider.getRemoteSource(); + if (source != null && !source.endsWith("!")) { + return VirtualFileManager.getInstance().findFileByUrl(source); + } + } + return provider.getSchemaFile(); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java new file mode 100644 index 00000000..76774c46 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaTreeNode.java @@ -0,0 +1,193 @@ +/* + * 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.impl; + +import com.intellij.util.SmartList; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 4/20/2017. + */ +public class JsonSchemaTreeNode { + private boolean myAny; + private boolean myNothing; + private int myExcludingGroupNumber = -1; + @NotNull private SchemaResolveState myResolveState = SchemaResolveState.normal; + + @Nullable private final JsonSchemaObject mySchema; + @NotNull private final List<JsonSchemaVariantsTreeBuilder.Step> mySteps = new SmartList<>(); + + @Nullable private final JsonSchemaTreeNode myParent; + @NotNull private final List<JsonSchemaTreeNode> myChildren = new ArrayList<>(); + + public JsonSchemaTreeNode(@Nullable JsonSchemaTreeNode parent, + @Nullable JsonSchemaObject schema) { + assert schema != null || parent != null; + myParent = parent; + mySchema = schema; + if (parent != null && !parent.getSteps().isEmpty()) { + mySteps.addAll(parent.getSteps().subList(1, parent.getSteps().size())); + } + } + + public void anyChild() { + final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null); + node.myAny = true; + myChildren.add(node); + } + + public void nothingChild() { + final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null); + node.myNothing = true; + myChildren.add(node); + } + + public void createChildrenFromOperation(@NotNull JsonSchemaVariantsTreeBuilder.Operation operation) { + if (!SchemaResolveState.normal.equals(operation.myState)) { + final JsonSchemaTreeNode node = new JsonSchemaTreeNode(this, null); + node.myResolveState = operation.myState; + myChildren.add(node); + return; + } + if (!operation.myAnyOfGroup.isEmpty()) { + myChildren.addAll(convertToNodes(operation.myAnyOfGroup)); + } + if (!operation.myOneOfGroup.isEmpty()) { + for (int i = 0; i < operation.myOneOfGroup.size(); i++) { + final List<JsonSchemaObject> group = operation.myOneOfGroup.get(i); + final List<JsonSchemaTreeNode> children = convertToNodes(group); + final int number = i; + children.forEach(c -> c.myExcludingGroupNumber = number); + myChildren.addAll(children); + } + } + } + + private List<JsonSchemaTreeNode> convertToNodes(List<JsonSchemaObject> children) { + List<JsonSchemaTreeNode> nodes = ContainerUtil.newArrayListWithCapacity(children.size()); + for (JsonSchemaObject child: children) { + nodes.add(new JsonSchemaTreeNode(this, child)); + } + return nodes; + } + + @NotNull + public SchemaResolveState getResolveState() { + return myResolveState; + } + + public boolean isAny() { + return myAny; + } + + public boolean isNothing() { + return myNothing; + } + + + public void setChild(@NotNull final JsonSchemaObject schema) { + myChildren.add(new JsonSchemaTreeNode(this, schema)); + } + + @Nullable + public JsonSchemaObject getSchema() { + return mySchema; + } + + @NotNull + public List<JsonSchemaVariantsTreeBuilder.Step> getSteps() { + return mySteps; + } + + @Nullable + public JsonSchemaTreeNode getParent() { + return myParent; + } + + @NotNull + public List<JsonSchemaTreeNode> getChildren() { + return myChildren; + } + + public int getExcludingGroupNumber() { + return myExcludingGroupNumber; + } + + public void setSteps(@NotNull List<JsonSchemaVariantsTreeBuilder.Step> steps) { + mySteps.clear(); + mySteps.addAll(steps); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsonSchemaTreeNode node = (JsonSchemaTreeNode)o; + + if (myAny != node.myAny) return false; + if (myNothing != node.myNothing) return false; + if (myResolveState != node.myResolveState) return false; + if (mySchema != null ? !mySchema.equals(node.mySchema) : node.mySchema != null) return false; + //noinspection RedundantIfStatement + if (!mySteps.equals(node.mySteps)) return false; + + return true; + } + + @Override + public int hashCode() { + int result = (myAny ? 1 : 0); + result = 31 * result + (myNothing ? 1 : 0); + result = 31 * result + myResolveState.hashCode(); + result = 31 * result + (mySchema != null ? mySchema.hashCode() : 0); + result = 31 * result + mySteps.hashCode(); + return result; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("NODE#" + hashCode() + "\n"); + sb.append(mySteps.stream().map(Object::toString).collect(Collectors.joining("->", "steps: <", ">"))); + sb.append("\n"); + if (myExcludingGroupNumber >= 0) sb.append("in excluding group\n"); + if (myAny) sb.append("any"); + else if (myNothing) sb.append("nothing"); + else if (!SchemaResolveState.normal.equals(myResolveState)) sb.append(myResolveState.name()); + else { + assert mySchema != null; + final String name = mySchema.getSchemaFile().getName(); + sb.append("schema from file: ").append(name).append("\n"); + if (mySchema.getRef() != null) sb.append("$ref: ").append(mySchema.getRef()).append("\n"); + else if (!mySchema.getProperties().isEmpty()) { + sb.append("properties: "); + sb.append(String.join(", ", mySchema.getProperties().keySet())).append("\n"); + } + if (!myChildren.isEmpty()) { + sb.append("OR children of NODE#").append(hashCode()).append(":\n----------------\n") + .append(myChildren.stream().map(Object::toString).collect(Collectors.joining("\n"))) + .append("\n=================\n"); + } + } + return sb.toString(); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java new file mode 100644 index 00000000..98351d18 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaType.java @@ -0,0 +1,84 @@ +package com.jetbrains.jsonSchema.impl; + +import com.jetbrains.jsonSchema.extension.adapters.JsonValueAdapter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Irina.Chernushina on 7/15/2015. + */ +public enum JsonSchemaType { + _string, _number, _integer, _object, _array, _boolean, _null, _any, _string_number; + + public String getName() { + return name().substring(1); + } + + public String getDefaultValue() { + switch (this) { + case _string: + return "\"\""; + case _number: + case _integer: + case _string_number: + return "0"; + case _object: + return "{}"; + case _array: + return "[]"; + case _boolean: + return "false"; + case _null: + return "null"; + case _any: + default: + return ""; + } + } + + public boolean isSimple() { + switch (this) { + case _string: + case _number: + case _integer: + case _boolean: + case _null: + return true; + case _object: + case _array: + case _any: + default: + return false; + } + } + + @Nullable + static JsonSchemaType getType(@NotNull final JsonValueAdapter value) { + if (value.isNull()) return _null; + if (value.isBooleanLiteral()) return _boolean; + if (value.isStringLiteral()) { + return value.isNumberLiteral() ? _string_number : _string; + } + if (value.isArray()) return _array; + if (value.isObject()) return _object; + if (value.isNumberLiteral()) { + return isInteger(value.getDelegate().getText()) ? _integer : _number; + } + return null; + } + + public static boolean isInteger(@NotNull String text) { + try { + Integer.parseInt(text); + return true; + } + catch (NumberFormatException e) { + return false; + } + } + + public String getDescription() { + if (this == _any) return "*"; + return getName(); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java new file mode 100644 index 00000000..98ed82cd --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaUsageTriggerCollector.java @@ -0,0 +1,13 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.internal.statistic.service.fus.collectors.ApplicationUsageTriggerCollector; +import org.jetbrains.annotations.NotNull; + +public class JsonSchemaUsageTriggerCollector extends ApplicationUsageTriggerCollector { + @NotNull + @Override + public String getGroupId() { + return "statistics.json.schema"; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java new file mode 100644 index 00000000..1e717be8 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVariantsTreeBuilder.java @@ -0,0 +1,595 @@ +/* + * 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.impl; + +import com.intellij.json.psi.JsonContainer; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; +import com.intellij.util.ObjectUtils; +import com.intellij.util.SmartList; +import com.intellij.util.ThreeState; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.jetbrains.jsonSchema.JsonPointerUtil.*; + +/** + * @author Irina.Chernushina on 4/20/2017. + */ +public class JsonSchemaVariantsTreeBuilder { + + public static JsonSchemaTreeNode buildTree(@NotNull final JsonSchemaObject schema, + @Nullable final List<Step> position, + final boolean skipLastExpand, + final boolean literalResolve, + final boolean acceptAdditional) { + final JsonSchemaTreeNode root = new JsonSchemaTreeNode(null, schema); + JsonSchemaService service = JsonSchemaService.Impl.get(schema.getJsonObject().getProject()); + expandChildSchema(root, schema, service); + // set root's position since this children are just variants of root + for (JsonSchemaTreeNode treeNode : root.getChildren()) { + treeNode.setSteps(ContainerUtil.notNullize(position)); + } + + final ArrayDeque<JsonSchemaTreeNode> queue = new ArrayDeque<>(root.getChildren()); + + while (!queue.isEmpty()) { + final JsonSchemaTreeNode node = queue.removeFirst(); + if (node.isAny() || node.isNothing() || node.getSteps().isEmpty() || node.getSchema() == null) continue; + final Step step = node.getSteps().get(0); + if (!typeMatches(step.isFromObject(), node.getSchema())) { + node.nothingChild(); + continue; + } + if (literalResolve) step.myLiteralResolve = true; + final Pair<ThreeState, JsonSchemaObject> pair = step.step(node.getSchema(), acceptAdditional); + if (ThreeState.NO.equals(pair.getFirst())) node.nothingChild(); + else if (ThreeState.YES.equals(pair.getFirst())) node.anyChild(); + else { + // process step results + assert pair.getSecond() != null; + if (node.getSteps().size() > 1 || !skipLastExpand) expandChildSchema(node, pair.getSecond(), service); + else node.setChild(pair.getSecond()); + } + + queue.addAll(node.getChildren()); + } + + return root; + } + + private static boolean typeMatches(final boolean isObject, @NotNull final JsonSchemaObject schema) { + final JsonSchemaType requiredType = isObject ? JsonSchemaType._object : JsonSchemaType._array; + if (schema.getType() != null) { + return requiredType.equals(schema.getType()); + } + if (schema.getTypeVariants() != null) { + for (JsonSchemaType schemaType : schema.getTypeVariants()) { + if (requiredType.equals(schemaType)) return true; + } + return false; + } + return true; + } + + private static void expandChildSchema(@NotNull JsonSchemaTreeNode node, @NotNull JsonSchemaObject childSchema, @NotNull JsonSchemaService service) { + final JsonContainer element = childSchema.getJsonObject(); + if (interestingSchema(childSchema)) { + final Operation operation = + CachedValuesManager.getManager(element.getProject()) + .createParameterizedCachedValue((JsonSchemaObject param) -> { + final Operation expand = new ProcessDefinitionsOperation(param, service); + expand.doMap(new HashSet<>()); + expand.doReduce(); + return CachedValueProvider.Result.create(expand, element.getContainingFile(), + service.getAnySchemaChangeTracker()); + }, false).getValue(childSchema); + node.createChildrenFromOperation(operation); + } + else { + node.setChild(childSchema); + } + } + + public static List<Step> buildSteps(@NotNull String nameInSchema) { + final List<String> chain = split(normalizeSlashes(JsonSchemaService.normalizeId(nameInSchema))); + List<Step> steps = ContainerUtil.newArrayListWithCapacity(chain.size()); + for (String s: chain) { + try { + steps.add(Step.createArrayElementStep(Integer.parseInt(s))); + } + catch (NumberFormatException e) { + steps.add(Step.createPropertyStep(unescapeJsonPointerPart(s))); + } + } + return steps; + } + + static abstract class Operation { + @NotNull final List<JsonSchemaObject> myAnyOfGroup = new SmartList<>(); + @NotNull final List<List<JsonSchemaObject>> myOneOfGroup = new SmartList<>(); + @NotNull protected final List<Operation> myChildOperations; + @NotNull protected final JsonSchemaObject mySourceNode; + protected SchemaResolveState myState = SchemaResolveState.normal; + + protected Operation(@NotNull JsonSchemaObject sourceNode) { + mySourceNode = sourceNode; + myChildOperations = new ArrayList<>(); + } + + protected abstract void map(@NotNull Set<JsonContainer> visited); + protected abstract void reduce(); + + public void doMap(@NotNull final Set<JsonContainer> visited) { + map(visited); + for (Operation operation : myChildOperations) { + operation.doMap(visited); + } + } + + public void doReduce() { + if (!SchemaResolveState.normal.equals(myState)) { + myChildOperations.clear(); + myAnyOfGroup.clear(); + myOneOfGroup.clear(); + return; + } + + // lets do that to make the returned object smaller + myAnyOfGroup.forEach(Operation::clearVariants); + myOneOfGroup.forEach(list -> list.forEach(Operation::clearVariants)); + + for (Operation myChildOperation : myChildOperations) { + myChildOperation.doReduce(); + } + reduce(); + myChildOperations.clear(); + } + + private static void clearVariants(@NotNull JsonSchemaObject object) { + object.setAllOf(null); + object.setAnyOf(null); + object.setOneOf(null); + } + + @Nullable + protected Operation createExpandOperation(@NotNull final JsonSchemaObject schema, + @NotNull JsonSchemaService service) { + if (conflictingSchema(schema)) { + final Operation operation = new AnyOfOperation(schema, service); + operation.myState = SchemaResolveState.conflict; + return operation; + } + if (schema.getAnyOf() != null) return new AnyOfOperation(schema, service); + if (schema.getOneOf() != null) return new OneOfOperation(schema, service); + if (schema.getAllOf() != null) return new AllOfOperation(schema, service); + return null; + } + + protected static List<JsonSchemaObject> mergeOneOf(Operation op) { + return op.myOneOfGroup.stream().flatMap(List::stream).collect(Collectors.toList()); + } + } + + // even if there are no definitions to expand, this object may work as an intermediate node in a tree, + // connecting oneOf and allOf expansion, for example + private static class ProcessDefinitionsOperation extends Operation { + private final JsonSchemaService myService; + + protected ProcessDefinitionsOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) { + super(sourceNode); + myService = service; + } + + @Override + public void map(@NotNull final Set<JsonContainer> visited) { + JsonSchemaObject current = mySourceNode; + while (!StringUtil.isEmptyOrSpaces(current.getRef())) { + final JsonSchemaObject definition = current.resolveRefSchema(myService); + if (definition == null) { + myState = SchemaResolveState.brokenDefinition; + return; + } + // this definition was already expanded; do not cycle + if (!visited.add(definition.getJsonObject())) break; + current = merge(current, definition, current); + } + final Operation expandOperation = createExpandOperation(current, myService); + if (expandOperation != null) myChildOperations.add(expandOperation); + else myAnyOfGroup.add(current); + } + + @Override + public void reduce() { + if (!myChildOperations.isEmpty()) { + assert myChildOperations.size() == 1; + final Operation operation = myChildOperations.get(0); + myAnyOfGroup.addAll(operation.myAnyOfGroup); + myOneOfGroup.addAll(operation.myOneOfGroup); + } + } + } + + private static class AllOfOperation extends Operation { + private final JsonSchemaService myService; + + protected AllOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) { + super(sourceNode); + myService = service; + } + + @Override + public void map(@NotNull final Set<JsonContainer> visited) { + List<JsonSchemaObject> allOf = mySourceNode.getAllOf(); + assert allOf != null; + myChildOperations.addAll(ContainerUtil.map(allOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService))); + } + + private static <T> int maxSize(List<List<T>> items) { + if (items.size() == 0) return 0; + int maxsize = -1; + for (List<T> item: items) { + int size = item.size(); + if (maxsize < size) maxsize = size; + } + return maxsize; + } + + @Override + public void reduce() { + myAnyOfGroup.add(mySourceNode); + + for (Operation op : myChildOperations) { + if (!op.myState.equals(SchemaResolveState.normal)) continue; + + final List<JsonSchemaObject> mergedAny = andGroups(op.myAnyOfGroup, myAnyOfGroup); + + final List<List<JsonSchemaObject>> mergedExclusive = + ContainerUtil.newArrayListWithCapacity( + op.myAnyOfGroup.size() * maxSize(myOneOfGroup) + + myAnyOfGroup.size() * maxSize(op.myOneOfGroup) + + maxSize(myOneOfGroup) * maxSize(op.myOneOfGroup) + ); + + for (List<JsonSchemaObject> objects : myOneOfGroup) { + mergedExclusive.add(andGroups(op.myAnyOfGroup, objects)); + } + for (List<JsonSchemaObject> objects : op.myOneOfGroup) { + mergedExclusive.add(andGroups(objects, myAnyOfGroup)); + } + for (List<JsonSchemaObject> group : op.myOneOfGroup) { + for (List<JsonSchemaObject> otherGroup : myOneOfGroup) { + mergedExclusive.add(andGroups(group, otherGroup)); + } + } + + myAnyOfGroup.clear(); + myOneOfGroup.clear(); + myAnyOfGroup.addAll(mergedAny); + myOneOfGroup.addAll(mergedExclusive); + } + } + } + + private static List<JsonSchemaObject> andGroups(@NotNull List<JsonSchemaObject> g1, + @NotNull List<JsonSchemaObject> g2) { + List<JsonSchemaObject> result = ContainerUtil.newArrayListWithCapacity(g1.size() * g2.size()); + for (JsonSchemaObject s: g1) { + result.addAll(andGroup(s, g2)); + } + return result; + } + + // here is important, which pointer gets the result: lets make them all different, otherwise two schemas of branches of oneOf would be equal + private static List<JsonSchemaObject> andGroup(@NotNull JsonSchemaObject object, @NotNull List<JsonSchemaObject> group) { + List<JsonSchemaObject> list = ContainerUtil.newArrayListWithCapacity(group.size()); + for (JsonSchemaObject s: group) { + JsonSchemaObject schemaObject = merge(object, s, s); + if (schemaObject.isValidByExclusion()) { + list.add(schemaObject); + } + } + return list; + } + + private static class OneOfOperation extends Operation { + private final JsonSchemaService myService; + + protected OneOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) { + super(sourceNode); + myService = service; + } + + @Override + public void map(@NotNull final Set<JsonContainer> visited) { + List<JsonSchemaObject> oneOf = mySourceNode.getOneOf(); + assert oneOf != null; + myChildOperations.addAll(ContainerUtil.map(oneOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService))); + } + + @Override + public void reduce() { + final List<JsonSchemaObject> oneOf = new SmartList<>(); + for (Operation op : myChildOperations) { + if (!op.myState.equals(SchemaResolveState.normal)) continue; + oneOf.addAll(andGroup(mySourceNode, op.myAnyOfGroup)); + oneOf.addAll(andGroup(mySourceNode, mergeOneOf(op))); + } + // here it is not a mistake - all children of this node come to oneOf group + myOneOfGroup.add(oneOf); + } + } + + private static class AnyOfOperation extends Operation { + private final JsonSchemaService myService; + + protected AnyOfOperation(@NotNull JsonSchemaObject sourceNode, JsonSchemaService service) { + super(sourceNode); + myService = service; + } + + @Override + public void map(@NotNull final Set<JsonContainer> visited) { + List<JsonSchemaObject> anyOf = mySourceNode.getAnyOf(); + assert anyOf != null; + myChildOperations.addAll(ContainerUtil.map(anyOf, sourceNode -> new ProcessDefinitionsOperation(sourceNode, myService))); + } + + @Override + public void reduce() { + for (Operation op : myChildOperations) { + if (!op.myState.equals(SchemaResolveState.normal)) continue; + + myAnyOfGroup.addAll(andGroup(mySourceNode, op.myAnyOfGroup)); + for (List<JsonSchemaObject> group : op.myOneOfGroup) { + myOneOfGroup.add(andGroup(mySourceNode, group)); + } + } + } + } + + @NotNull + public static JsonSchemaObject merge(@NotNull JsonSchemaObject base, + @NotNull JsonSchemaObject other, + @NotNull JsonSchemaObject pointTo) { + final JsonSchemaObject object = new JsonSchemaObject(pointTo.getJsonObject()); + object.mergeValues(other); + object.mergeValues(base); + object.setRef(other.getRef()); + return object; + } + + private static boolean conflictingSchema(JsonSchemaObject schema) { + int cnt = 0; + if (schema.getAllOf() != null) ++cnt; + if (schema.getAnyOf() != null) ++cnt; + if (schema.getOneOf() != null) ++cnt; + return cnt > 1; + } + + private static boolean interestingSchema(@NotNull JsonSchemaObject schema) { + return schema.getAnyOf() != null || schema.getOneOf() != null || schema.getAllOf() != null || schema.getRef() != null + || schema.getIf() != null; + } + + public static class Step { + @Nullable private final String myName; + private final int myIdx; + private boolean myLiteralResolve; + + private Step(@Nullable String name, int idx) { + myName = name; + myIdx = idx; + } + + public static Step createPropertyStep(@NotNull final String name) { + return new Step(name, -1); + } + + public static Step createArrayElementStep(final int idx) { + assert idx >= 0; + return new Step(null, idx); + } + + public boolean isFromObject() { + return myName != null; + } + + public boolean isFromArray() { + return myName == null; + } + + @Nullable + public String getName() { + return myName; + } + + public int getIdx() { + return myIdx; + } + + @NotNull + public Pair<ThreeState, JsonSchemaObject> step(@NotNull JsonSchemaObject parent, boolean acceptAdditionalPropertiesSchemas) { + if (myName != null) { + return propertyStep(parent, acceptAdditionalPropertiesSchemas); + } else { + assert myIdx >= 0; + return arrayOrNumericPropertyElementStep(parent, acceptAdditionalPropertiesSchemas); + } + } + + @Override + public String toString() { + String format = "?%s"; + if (myName != null) format = "{%s}"; + if (myIdx >= 0) format = "[%s]"; + return String.format(format, myName != null ? myName : (myIdx >= 0 ? String.valueOf(myIdx) : "null")); + } + + @NotNull + private Pair<ThreeState, JsonSchemaObject> propertyStep(@NotNull JsonSchemaObject parent, + boolean acceptAdditionalPropertiesSchemas) { + assert myName != null; + if (JsonSchemaObject.DEFINITIONS.equals(myName) && + parent.getDefinitionsMap() != null && (!isInMainSchema(parent) || myLiteralResolve)) { + // definitions pointer here is fictive so lets find any + final Map<String, JsonSchemaObject> definitionsMap = parent.getDefinitionsMap(); + final JsonObject anyDefinitions = definitionsMap.values().stream() + .filter(def -> { + final JsonProperty parentObj = ObjectUtils.tryCast(def.getJsonObject().getParent(), JsonProperty.class); + return parentObj != null && parentObj.isValid() && parentObj.getValue() instanceof JsonObject; + }) + .map(def -> (JsonObject)((JsonProperty) def.getJsonObject().getParent()).getValue()) + .findFirst().orElse(null); + if (anyDefinitions == null) return Pair.create(ThreeState.NO, null); + final JsonSchemaObject object = new JsonSchemaObject(anyDefinitions); + object.setProperties(definitionsMap); + return Pair.create(ThreeState.UNSURE, object); + } + final JsonSchemaObject child = parent.getProperties().get(myName); + if (child != null) { + return Pair.create(ThreeState.UNSURE, child); + } + final JsonSchemaObject schema = parent.getMatchingPatternPropertySchema(myName); + if (schema != null) { + return Pair.create(ThreeState.UNSURE, schema); + } + if (acceptAdditionalPropertiesSchemas) { + if (parent.getAdditionalPropertiesSchema() != null) { + return Pair.create(ThreeState.UNSURE, parent.getAdditionalPropertiesSchema()); + } + + // resolve inside V7 if-then-else conditionals + if (parent.getIf() != null) { + JsonSchemaObject childObject; + + // NOTE: do not resolve inside 'if' itself - it is just a condition, but not an actual validation! + // only 'then' and 'else' branches provide actual validation sources, but not the 'if' branch + + if (parent.getThen() != null) { + childObject = parent.getThen().getProperties().get(myName); + if (childObject != null) { + return Pair.create(ThreeState.UNSURE, childObject); + } + } + if (parent.getElse() != null) { + childObject = parent.getElse().getProperties().get(myName); + if (childObject != null) { + return Pair.create(ThreeState.UNSURE, childObject); + } + } + } + } + if (Boolean.FALSE.equals(parent.getAdditionalPropertiesAllowed())) { + return Pair.create(ThreeState.NO, null); + } + // by default, additional properties are allowed + return Pair.create(ThreeState.YES, null); + } + + private static boolean isInMainSchema(@NotNull JsonSchemaObject parent) { + final VirtualFile schemaFile = parent.getSchemaFile(); + final JsonSchemaService service = JsonSchemaService.Impl.get(parent.getJsonObject().getProject()); + if (!service.isApplicableToFile(schemaFile) || !service.isSchemaFile(schemaFile)) return false; + + final JsonSchemaObject rootSchema = service.getSchemaObjectForSchemaFile(schemaFile); + if (rootSchema == null) return false; + + return JsonSchemaVersion.isSchemaSchemaId(rootSchema.getId()); + } + + @NotNull + private Pair<ThreeState, JsonSchemaObject> arrayOrNumericPropertyElementStep(@NotNull JsonSchemaObject parent, + boolean acceptAdditionalPropertiesSchemas) { + if (parent.getItemsSchema() != null) { + return Pair.create(ThreeState.UNSURE, parent.getItemsSchema()); + } + if (parent.getItemsSchemaList() != null) { + final List<JsonSchemaObject> list = parent.getItemsSchemaList(); + if (myIdx >= 0 && myIdx < list.size()) { + return Pair.create(ThreeState.UNSURE, list.get(myIdx)); + } + } + final String keyAsString = String.valueOf(myIdx); + if (parent.getProperties().containsKey(keyAsString)) { + return Pair.create(ThreeState.UNSURE, parent.getProperties().get(keyAsString)); + } + final JsonSchemaObject matchingPatternPropertySchema = parent.getMatchingPatternPropertySchema(keyAsString); + if (matchingPatternPropertySchema != null) { + return Pair.create(ThreeState.UNSURE, matchingPatternPropertySchema); + } + if (parent.getAdditionalItemsSchema() != null && acceptAdditionalPropertiesSchemas) { + return Pair.create(ThreeState.UNSURE, parent.getAdditionalItemsSchema()); + } + if (Boolean.FALSE.equals(parent.getAdditionalItemsAllowed())) { + return Pair.create(ThreeState.NO, null); + } + return Pair.create(ThreeState.YES, null); + } + } + + public static class SchemaUrlSplitter { + @Nullable + private final String mySchemaId; + @NotNull + private final String myRelativePath; + + public SchemaUrlSplitter(@NotNull final String ref) { + if (isSelfReference(ref)) { + mySchemaId = null; + myRelativePath = ""; + return; + } + if (!ref.startsWith("#/")) { + int idx = ref.indexOf("#/"); + if (idx == -1) { + mySchemaId = ref.endsWith("#") ? ref.substring(0, ref.length() - 1) : ref; + myRelativePath = ""; + } else { + mySchemaId = ref.substring(0, idx); + myRelativePath = ref.substring(idx); + } + } else { + mySchemaId = null; + myRelativePath = ref; + } + } + + public boolean isAbsolute() { + return mySchemaId != null; + } + + @Nullable + public String getSchemaId() { + return mySchemaId; + } + + @NotNull + public String getRelativePath() { + return myRelativePath; + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java new file mode 100644 index 00000000..5b0f5507 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonSchemaVersion.java @@ -0,0 +1,60 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.openapi.util.text.StringUtil; +import kotlin.NotImplementedError; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public enum JsonSchemaVersion { + SCHEMA_4, + SCHEMA_6, + SCHEMA_7; + + private static final String ourSchemaV4Schema = "http://json-schema.org/draft-04/schema"; + private static final String ourSchemaV6Schema = "http://json-schema.org/draft-06/schema"; + private static final String ourSchemaV7Schema = "http://json-schema.org/draft-07/schema"; + private static final String ourSchemaVLatestSchema = "http://json-schema.org/schema"; + + @Override + public String toString() { + switch (this) { + case SCHEMA_4: + return "JSON schema version 4"; + case SCHEMA_6: + return "JSON schema version 6"; + case SCHEMA_7: + return "JSON schema version 7"; + } + + throw new NotImplementedError("Unknown version: " + this); + } + + + @Nullable + public static JsonSchemaVersion byId(@NotNull String id) { + switch (StringUtil.trimEnd(id, '#')) { + case ourSchemaV4Schema: + return SCHEMA_4; + case ourSchemaV6Schema: + return SCHEMA_6; + case ourSchemaV7Schema: + case ourSchemaVLatestSchema: + return SCHEMA_7; + } + + return null; + } + + public static boolean isSchemaSchemaId(@Nullable String id) { + if (id == null) return false; + switch (StringUtil.trimEnd(id, '#')) { + case ourSchemaV4Schema: + case ourSchemaV6Schema: + case ourSchemaV7Schema: + case ourSchemaVLatestSchema: + return true; + } + return false; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java b/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java new file mode 100644 index 00000000..fa13577c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/JsonValidationError.java @@ -0,0 +1,163 @@ +// 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 com.jetbrains.jsonSchema.impl; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.impl.fixes.AddMissingPropertyFix; +import com.jetbrains.jsonSchema.impl.fixes.RemoveProhibitedPropertyFix; +import com.jetbrains.jsonSchema.impl.fixes.SuggestEnumValuesFix; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Iterator; +import java.util.stream.Collectors; + +public class JsonValidationError { + + public IssueData getIssueData() { + return myIssueData; + } + + public JsonErrorPriority getPriority() { + return myPriority; + } + + public enum FixableIssueKind { + MissingProperty, + MissingOneOfProperty, + MissingAnyOfProperty, + ProhibitedProperty, + NonEnumValue, + ProhibitedType, + TypeMismatch, + None + } + + public interface IssueData { + + } + + public static class MissingOneOfPropsIssueData implements IssueData { + public final Collection<MissingMultiplePropsIssueData> myExclusiveOptions; + + public MissingOneOfPropsIssueData(Collection<MissingMultiplePropsIssueData> options) { + myExclusiveOptions = options; + } + } + + public static class MissingMultiplePropsIssueData implements IssueData { + public final Collection<MissingPropertyIssueData> myMissingPropertyIssues; + + public MissingMultiplePropsIssueData(Collection<MissingPropertyIssueData> missingPropertyIssues) { + myMissingPropertyIssues = missingPropertyIssues; + } + + private static String getPropertyNameWithComment(MissingPropertyIssueData prop) { + String comment = ""; + if (prop.enumItemsCount == 1) { + comment = " = " + prop.defaultValue.toString(); + } + return "'" + prop.propertyName + "'" + comment; + } + + public String getMessage(boolean trimIfNeeded) { + if (myMissingPropertyIssues.size() == 1) { + MissingPropertyIssueData prop = myMissingPropertyIssues.iterator().next(); + return "property " + getPropertyNameWithComment(prop); + } + + Collection<MissingPropertyIssueData> namesToDisplay = myMissingPropertyIssues; + boolean trimmed = false; + if (trimIfNeeded && namesToDisplay.size() > 3) { + namesToDisplay = ContainerUtil.newArrayList(); + Iterator<MissingPropertyIssueData> iterator = myMissingPropertyIssues.iterator(); + for (int i = 0; i < 3; i++) { + namesToDisplay.add(iterator.next()); + } + trimmed = true; + } + String allNames = myMissingPropertyIssues.stream().map( + MissingMultiplePropsIssueData::getPropertyNameWithComment).sorted((s1, s2) -> { + boolean firstHasEq = s1.contains("="); + boolean secondHasEq = s2.contains("="); + if (firstHasEq == secondHasEq) { + return s1.compareTo(s2); + } + return firstHasEq ? -1 : 1; + }).collect(Collectors.joining(", ")); + if (trimmed) allNames += ", ..."; + return "properties " + allNames; + } + } + + public static class MissingPropertyIssueData implements IssueData { + public final String propertyName; + public final JsonSchemaType propertyType; + public final Object defaultValue; + public final int enumItemsCount; + + public MissingPropertyIssueData(String propertyName, JsonSchemaType propertyType, Object defaultValue, int enumItemsCount) { + this.propertyName = propertyName; + this.propertyType = propertyType; + this.defaultValue = defaultValue; + this.enumItemsCount = enumItemsCount; + } + } + + public static class ProhibitedPropertyIssueData implements IssueData { + public final String propertyName; + + public ProhibitedPropertyIssueData(String propertyName) { + this.propertyName = propertyName; + } + } + + public static class TypeMismatchIssueData implements IssueData { + public final JsonSchemaType[] expectedTypes; + + public TypeMismatchIssueData(JsonSchemaType[] expectedTypes) { + this.expectedTypes = expectedTypes; + } + } + + private final String myMessage; + private final FixableIssueKind myFixableIssueKind; + private final IssueData myIssueData; + private final JsonErrorPriority myPriority; + + public JsonValidationError(String message, FixableIssueKind fixableIssueKind, IssueData issueData, + JsonErrorPriority priority) { + myMessage = message; + myFixableIssueKind = fixableIssueKind; + myIssueData = issueData; + myPriority = priority; + } + + public String getMessage() { + return myMessage; + } + + public FixableIssueKind getFixableIssueKind() { + return myFixableIssueKind; + } + + @NotNull + public LocalQuickFix[] createFixes(@Nullable JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) { + if (quickFixAdapter == null) return LocalQuickFix.EMPTY_ARRAY; + switch (myFixableIssueKind) { + case MissingProperty: + return new AddMissingPropertyFix[]{new AddMissingPropertyFix((MissingMultiplePropsIssueData)myIssueData, quickFixAdapter)}; + case MissingOneOfProperty: + case MissingAnyOfProperty: + return ((MissingOneOfPropsIssueData)myIssueData).myExclusiveOptions.stream().map(d -> new AddMissingPropertyFix(d, quickFixAdapter)).toArray(LocalQuickFix[]::new); + case ProhibitedProperty: + return new RemoveProhibitedPropertyFix[]{new RemoveProhibitedPropertyFix((ProhibitedPropertyIssueData)myIssueData, quickFixAdapter)}; + case NonEnumValue: + return new SuggestEnumValuesFix[]{new SuggestEnumValuesFix(quickFixAdapter)}; + default: + return LocalQuickFix.EMPTY_ARRAY; + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java b/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java new file mode 100644 index 00000000..910313e7 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/MatchResult.java @@ -0,0 +1,74 @@ +/* + * 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.impl; + +import com.intellij.util.Processor; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.MultiMap; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * @author Irina.Chernushina on 4/22/2017. + */ +public class MatchResult { + public final List<JsonSchemaObject> mySchemas; + public final List<Collection<? extends JsonSchemaObject>> myExcludingSchemas; + + private MatchResult(@NotNull final List<JsonSchemaObject> schemas, @NotNull final List<Collection<? extends JsonSchemaObject>> excludingSchemas) { + mySchemas = Collections.unmodifiableList(schemas); + myExcludingSchemas = Collections.unmodifiableList(excludingSchemas); + } + + public static MatchResult create(@NotNull JsonSchemaTreeNode root) { + List<JsonSchemaObject> schemas = new ArrayList<>(); + MultiMap<Integer, JsonSchemaObject> oneOfGroups = MultiMap.create(); + iterateTree(root, node -> { + if (node.isAny()) return true; + int groupNumber = node.getExcludingGroupNumber(); + if (groupNumber < 0) { + schemas.add(node.getSchema()); + } + else { + oneOfGroups.putValue(groupNumber, node.getSchema()); + } + return true; + }); + List<Collection<? extends JsonSchemaObject>> result = oneOfGroups.isEmpty() + ? ContainerUtil.emptyList() + : ContainerUtil.newArrayListWithCapacity(oneOfGroups.keySet().size()); + for (Map.Entry<Integer, Collection<JsonSchemaObject>> entry: oneOfGroups.entrySet()) { + result.add(entry.getValue()); + } + return new MatchResult(schemas, result); + } + + public static void iterateTree(@NotNull JsonSchemaTreeNode root, + @NotNull final Processor<? super JsonSchemaTreeNode> processor) { + final ArrayDeque<JsonSchemaTreeNode> queue = new ArrayDeque<>(root.getChildren()); + while (!queue.isEmpty()) { + final JsonSchemaTreeNode node = queue.removeFirst(); + if (node.getChildren().isEmpty()) { + if (!node.isNothing() && SchemaResolveState.normal.equals(node.getResolveState()) && !processor.process(node)) { + break; + } + } else { + queue.addAll(node.getChildren()); + } + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java b/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java new file mode 100644 index 00000000..2c83bcb2 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/SchemaResolveState.java @@ -0,0 +1,23 @@ +/* + * 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.impl; + +/** + * @author Irina.Chernushina on 4/24/2017. + */ +enum SchemaResolveState { + normal, conflict, brokenDefinition, cyclicDefinition +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java new file mode 100644 index 00000000..5ad2927a --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonArrayAdapter.java @@ -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.impl.adapters; + +import com.intellij.json.psi.JsonArray; +import com.intellij.psi.PsiElement; +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 java.util.List; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public class JsonJsonArrayAdapter implements JsonArrayValueAdapter { + @NotNull private final JsonArray myArray; + + public JsonJsonArrayAdapter(@NotNull JsonArray array) {myArray = array;} + + @Override + public boolean isObject() { + return false; + } + + @Override + public boolean isArray() { + return true; + } + + @Override + public boolean isStringLiteral() { + return false; + } + + @Override + public boolean isNumberLiteral() { + return false; + } + + @Override + public boolean isBooleanLiteral() { + return false; + } + + @NotNull + @Override + public PsiElement getDelegate() { + return myArray; + } + + @Nullable + @Override + public JsonObjectValueAdapter getAsObject() { + return null; + } + + @Nullable + @Override + public JsonArrayValueAdapter getAsArray() { + return this; + } + + @NotNull + @Override + public List<JsonValueAdapter> getElements() { + return myArray.getValueList().stream().filter(e -> e != null).map(e -> JsonJsonPropertyAdapter.createAdapterByType(e)).collect( + Collectors.toList()); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java new file mode 100644 index 00000000..fe84e7eb --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonGenericValueAdapter.java @@ -0,0 +1,81 @@ +/* + * 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.impl.adapters; + +import com.intellij.json.psi.*; +import com.intellij.psi.PsiElement; +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; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public class JsonJsonGenericValueAdapter implements JsonValueAdapter { + @NotNull private final JsonValue myValue; + + public JsonJsonGenericValueAdapter(@NotNull JsonValue value) {myValue = value;} + + @Override + public boolean isObject() { + return false; + } + + @Override + public boolean isArray() { + return false; + } + + @Override + public boolean isStringLiteral() { + return myValue instanceof JsonStringLiteral; + } + + @Override + public boolean isNumberLiteral() { + return myValue instanceof JsonNumberLiteral; + } + + @Override + public boolean isBooleanLiteral() { + return myValue instanceof JsonBooleanLiteral; + } + + @Override + public boolean isNull() { + return myValue instanceof JsonNullLiteral; + } + + @NotNull + @Override + public PsiElement getDelegate() { + return myValue; + } + + @Nullable + @Override + public JsonObjectValueAdapter getAsObject() { + return null; + } + + @Nullable + @Override + public JsonArrayValueAdapter getAsArray() { + return null; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java new file mode 100644 index 00000000..db19cd1c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonObjectAdapter.java @@ -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.impl.adapters; + +import com.intellij.json.psi.JsonObject; +import com.intellij.psi.PsiElement; +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 java.util.List; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public class JsonJsonObjectAdapter implements JsonObjectValueAdapter { + @NotNull private final JsonObject myValue; + + public JsonJsonObjectAdapter(@NotNull JsonObject value) {myValue = value;} + + @Override + public boolean isObject() { + return true; + } + + @Override + public boolean isArray() { + return false; + } + + @Override + public boolean isStringLiteral() { + return false; + } + + @Override + public boolean isNumberLiteral() { + return false; + } + + @Override + public boolean isBooleanLiteral() { + return false; + } + + @NotNull + @Override + public PsiElement getDelegate() { + return myValue; + } + + @Nullable + @Override + public JsonObjectValueAdapter getAsObject() { + return this; + } + + @Nullable + @Override + public JsonArrayValueAdapter getAsArray() { + return null; + } + + @NotNull + @Override + public List<JsonPropertyAdapter> getPropertyList() { + return myValue.getPropertyList().stream().filter(p -> p != null) + .map(p -> new JsonJsonPropertyAdapter(p)).collect(Collectors.toList()); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java new file mode 100644 index 00000000..f3871c35 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/adapters/JsonJsonPropertyAdapter.java @@ -0,0 +1,79 @@ +/* + * 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.impl.adapters; + +import com.intellij.json.psi.JsonArray; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonValue; +import com.intellij.psi.PsiElement; +import com.intellij.util.containers.ContainerUtil; +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 java.util.Collection; +import java.util.Collections; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public class JsonJsonPropertyAdapter implements JsonPropertyAdapter { + @NotNull private final JsonProperty myProperty; + + public JsonJsonPropertyAdapter(@NotNull JsonProperty property) { + myProperty = property; + } + + @Nullable + @Override + public String getName() { + return myProperty.getName(); + } + + @NotNull + @Override + public Collection<JsonValueAdapter> getValues() { + return myProperty.getValue() == null ? ContainerUtil.emptyList() : Collections.singletonList(createAdapterByType(myProperty.getValue())); + } + + @Nullable + @Override + public JsonValueAdapter getNameValueAdapter() { + return createAdapterByType(myProperty.getNameElement()); + } + + @NotNull + @Override + public PsiElement getDelegate() { + return myProperty; + } + + @Nullable + @Override + public JsonObjectValueAdapter getParentObject() { + return myProperty.getParent() instanceof JsonObject ? new JsonJsonObjectAdapter((JsonObject)myProperty.getParent()) : null; + } + + @NotNull + public static JsonValueAdapter createAdapterByType(@NotNull JsonValue value) { + if (value instanceof JsonObject) return new JsonJsonObjectAdapter((JsonObject)value); + if (value instanceof JsonArray) return new JsonJsonArrayAdapter((JsonArray)value); + return new JsonJsonGenericValueAdapter(value); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java new file mode 100644 index 00000000..8dbcd601 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/AddMissingPropertyFix.java @@ -0,0 +1,156 @@ +// 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 com.jetbrains.jsonSchema.impl.fixes; + +import com.intellij.codeInsight.template.Template; +import com.intellij.codeInsight.template.TemplateBuilderImpl; +import com.intellij.codeInsight.template.TemplateManager; +import com.intellij.codeInsight.template.impl.ConstantNode; +import com.intellij.codeInsight.template.impl.EmptyNode; +import com.intellij.codeInsight.template.impl.MacroCallNode; +import com.intellij.codeInsight.template.macro.CompleteMacro; +import com.intellij.codeInspection.*; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.Ref; +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.intellij.util.DocumentUtil; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.impl.JsonSchemaType; +import com.jetbrains.jsonSchema.impl.JsonValidationError; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class AddMissingPropertyFix implements LocalQuickFix, BatchQuickFix<CommonProblemDescriptor> { + private final JsonValidationError.MissingMultiplePropsIssueData myData; + private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter; + + public AddMissingPropertyFix(JsonValidationError.MissingMultiplePropsIssueData data, + JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) { + myData = data; + myQuickFixAdapter = quickFixAdapter; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return "Add missing properties"; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getName() { + return "Add missing " + myData.getMessage(true); + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PsiElement element = descriptor.getPsiElement(); + Ref<Boolean> hadComma = Ref.create(false); + PsiElement newElement = performFix(element, hadComma); + // if we have more than one property, don't expand templates and don't move the caret + if (newElement == null) return; + + PsiElement value = myQuickFixAdapter.getPropertyValue(newElement); + FileEditor fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(element.getContainingFile().getVirtualFile()); + EditorEx editor = EditorUtil.getEditorEx(fileEditor); + assert editor != null; + if (value == null) { + WriteAction.run(() -> editor.getCaretModel().moveToOffset(newElement.getTextRange().getEndOffset())); + return; + } + TemplateManager templateManager = TemplateManager.getInstance(project); + TemplateBuilderImpl builder = new TemplateBuilderImpl(newElement); + String text = value.getText(); + boolean isEmptyArray = StringUtil.equalsIgnoreWhitespaces(text, "[]"); + boolean isEmptyObject = StringUtil.equalsIgnoreWhitespaces(text, "{}"); + boolean goInside = isEmptyArray || isEmptyObject || StringUtil.isQuotedString(text); + TextRange range = goInside ? TextRange.create(1, text.length() - 1) : TextRange.create(0, text.length()); + builder.replaceElement(value, range, myData.myMissingPropertyIssues.iterator().next().enumItemsCount > 1 || isEmptyObject + ? new MacroCallNode(new CompleteMacro()) + : isEmptyArray ? new EmptyNode() : new ConstantNode(goInside ? StringUtil.unquoteString(text) : text)); + editor.getCaretModel().moveToOffset(newElement.getTextRange().getStartOffset()); + builder.setEndVariableAfter(newElement); + WriteAction.run(() -> { + Template template = builder.buildInlineTemplate(); + template.setToReformat(true); + templateManager.startTemplate(editor, template); + }); + } + + private PsiElement performFix(PsiElement element, Ref<Boolean> hadComma) { + Ref<PsiElement> newElementRef = Ref.create(null); + + WriteAction.run(() -> { + boolean isSingle = myData.myMissingPropertyIssues.size() == 1; + for (JsonValidationError.MissingPropertyIssueData issue: myData.myMissingPropertyIssues) { + Object defaultValueObject = issue.defaultValue; + String defaultValue = defaultValueObject instanceof String ? StringUtil.wrapWithDoubleQuote(defaultValueObject.toString()) : null; + PsiElement newElement = element + .addBefore( + myQuickFixAdapter.createProperty(issue.propertyName, defaultValue == null ? getDefaultValueFromType(issue) : defaultValue), + element.getLastChild()); + PsiElement backward = PsiTreeUtil.skipWhitespacesBackward(newElement); + hadComma.set(myQuickFixAdapter.ensureComma(backward, element, newElement)); + if (isSingle) { + newElementRef.set(newElement); + } + } + }); + + return newElementRef.get(); + } + + @NotNull + private static String getDefaultValueFromType(JsonValidationError.MissingPropertyIssueData issue) { + JsonSchemaType propertyType = issue.propertyType; + return propertyType == null ? "" : propertyType.getDefaultValue(); + } + + @Override + public boolean startInWriteAction() { + return false; + } + + @Override + public void applyFix(@NotNull Project project, + @NotNull CommonProblemDescriptor[] descriptors, + @NotNull List<PsiElement> psiElementsToIgnore, + @Nullable Runnable refreshViews) { + List<Pair<AddMissingPropertyFix, PsiElement>> propFixes = ContainerUtil.newArrayList(); + for (CommonProblemDescriptor descriptor: descriptors) { + if (!(descriptor instanceof ProblemDescriptor)) continue; + QuickFix[] fixes = descriptor.getFixes(); + if (fixes == null) continue; + AddMissingPropertyFix fix = getWorkingQuickFix(fixes); + if (fix == null) continue; + propFixes.add(Pair.create(fix, ((ProblemDescriptor)descriptor).getPsiElement())); + } + + DocumentUtil.writeInRunUndoTransparentAction(() -> propFixes.forEach(fix -> + fix.first.performFix(fix.second, Ref.create(false)))); + } + + @Nullable + private static AddMissingPropertyFix getWorkingQuickFix(@NotNull QuickFix[] fixes) { + for (QuickFix fix : fixes) { + if (fix instanceof AddMissingPropertyFix) { + return (AddMissingPropertyFix)fix; + } + } + return null; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java new file mode 100644 index 00000000..fb211e4c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/RemoveProhibitedPropertyFix.java @@ -0,0 +1,46 @@ +// 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 com.jetbrains.jsonSchema.impl.fixes; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.impl.JsonValidationError; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +public class RemoveProhibitedPropertyFix implements LocalQuickFix { + private final JsonValidationError.ProhibitedPropertyIssueData myData; + private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter; + + public RemoveProhibitedPropertyFix(JsonValidationError.ProhibitedPropertyIssueData data, + JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) { + myData = data; + myQuickFixAdapter = quickFixAdapter; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return "Remove prohibited property"; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getName() { + return getFamilyName() + " '" + myData.propertyName + "'"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PsiElement element = descriptor.getPsiElement(); + assert myData.propertyName.equals(myQuickFixAdapter.getPropertyName(element)); + PsiElement forward = PsiTreeUtil.skipWhitespacesForward(element); + element.delete(); + myQuickFixAdapter.removeIfComma(forward); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java b/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java new file mode 100644 index 00000000..3d00f1f2 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/fixes/SuggestEnumValuesFix.java @@ -0,0 +1,89 @@ +// 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 com.jetbrains.jsonSchema.impl.fixes; + +import com.intellij.codeInsight.completion.CodeCompletionHandlerBase; +import com.intellij.codeInsight.completion.CompletionType; +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.codeInspection.BatchQuickFix; +import com.intellij.codeInspection.CommonProblemDescriptor; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class SuggestEnumValuesFix implements LocalQuickFix, BatchQuickFix<CommonProblemDescriptor> { + private final JsonLikePsiWalker.QuickFixAdapter myQuickFixAdapter; + + public SuggestEnumValuesFix(JsonLikePsiWalker.QuickFixAdapter quickFixAdapter) { + myQuickFixAdapter = quickFixAdapter; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getFamilyName() { + return "Replace with allowed value"; + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getName() { + return getFamilyName(); + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + 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) { + whitespaceBefore = true; + } + WriteAction.run(() -> element.delete()); + EditorEx editor = EditorUtil.getEditorEx(fileEditor); + assert editor != null; + if (myQuickFixAdapter.fixWhitespaceBefore(initialElement, element) && whitespaceBefore) { + WriteAction.run(() -> { + int offset = editor.getCaretModel().getOffset(); + editor.getDocument().insertString(offset, " "); + editor.getCaretModel().moveToOffset(offset + 1); + }); + } + CodeCompletionHandlerBase.createHandler(CompletionType.BASIC).invokeCompletion(project, editor); + } + + @Override + public boolean startInWriteAction() { + return false; + } + + @Override + public void applyFix(@NotNull Project project, + @NotNull CommonProblemDescriptor[] descriptors, + @NotNull List<PsiElement> psiElementsToIgnore, + @Nullable Runnable refreshViews) { + Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor(); + if (editor != null) { + HintManager.getInstance().showErrorHint(editor, "Sorry, this fix is not available in batch mode"); + } + else { + Messages.showErrorDialog(project, "Sorry, this fix is not available in batch mode", "Not Applicable in Batch Mode"); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java new file mode 100644 index 00000000..0a47e699 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaBasedInspectionBase.java @@ -0,0 +1,46 @@ +// 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 com.jetbrains.jsonSchema.impl.inspections; + +import com.intellij.codeHighlighting.HighlightDisplayLevel; +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.codeInspection.LocalInspectionToolSession; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonValue; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.util.ObjectUtils; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public abstract class JsonSchemaBasedInspectionBase extends LocalInspectionTool { + @NotNull + @Override + public HighlightDisplayLevel getDefaultLevel() { + return HighlightDisplayLevel.WARNING; + } + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly, @NotNull LocalInspectionToolSession session) { + PsiFile file = holder.getFile(); + JsonValue root = file instanceof JsonFile ? ObjectUtils.tryCast(file.getFirstChild(), JsonValue.class) : null; + if (root == null) return PsiElementVisitor.EMPTY_VISITOR; + + JsonSchemaService service = JsonSchemaService.Impl.get(file.getProject()); + VirtualFile virtualFile = file.getViewProvider().getVirtualFile(); + if (!service.isApplicableToFile(virtualFile)) return PsiElementVisitor.EMPTY_VISITOR; + final JsonSchemaObject rootSchema = service.getSchemaObject(virtualFile); + + return doBuildVisitor(root, rootSchema, service, holder, session); + } + + protected abstract PsiElementVisitor doBuildVisitor(@NotNull JsonValue root, + @Nullable JsonSchemaObject schema, + @NotNull JsonSchemaService service, + @NotNull ProblemsHolder holder, + @NotNull LocalInspectionToolSession session); +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java new file mode 100644 index 00000000..0ff94732 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaComplianceInspection.java @@ -0,0 +1,67 @@ +// 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 com.jetbrains.jsonSchema.impl.inspections; + +import com.intellij.codeInspection.LocalInspectionToolSession; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.codeInspection.ui.MultipleCheckboxOptionsPanel; +import com.intellij.json.JsonBundle; +import com.intellij.json.psi.JsonElementVisitor; +import com.intellij.json.psi.JsonValue; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonComplianceCheckerOptions; +import com.jetbrains.jsonSchema.impl.JsonSchemaComplianceChecker; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +public class JsonSchemaComplianceInspection extends JsonSchemaBasedInspectionBase { + public boolean myCaseInsensitiveEnum = false; + + @Override + @NotNull + public String getDisplayName() { + return JsonBundle.message("json.schema.inspection.compliance.name"); + } + + @Override + protected PsiElementVisitor doBuildVisitor(@NotNull JsonValue root, @Nullable JsonSchemaObject schema, @NotNull JsonSchemaService service, + @NotNull ProblemsHolder holder, + @NotNull LocalInspectionToolSession session) { + if (schema == null) return PsiElementVisitor.EMPTY_VISITOR; + JsonComplianceCheckerOptions options = new JsonComplianceCheckerOptions(myCaseInsensitiveEnum); + + return new JsonElementVisitor() { + @Override + public void visitElement(PsiElement element) { + if (element == root) { + // perform this only for the root element, because the checker traverses the hierarchy itself + annotate(element, schema, holder, session, options); + } + super.visitElement(element); + } + }; + } + + @Nullable + @Override + public JComponent createOptionsPanel() { + final MultipleCheckboxOptionsPanel optionsPanel = new MultipleCheckboxOptionsPanel(this); + optionsPanel.addCheckbox(JsonBundle.message("json.schema.inspection.case.insensitive.enum"), "myCaseInsensitiveEnum"); + return optionsPanel; + } + + private static void annotate(@NotNull PsiElement element, + @NotNull JsonSchemaObject rootSchema, + @NotNull ProblemsHolder holder, + @NotNull LocalInspectionToolSession session, + JsonComplianceCheckerOptions options) { + final JsonLikePsiWalker walker = JsonLikePsiWalker.getWalker(element, rootSchema); + if (walker == null) return; + new JsonSchemaComplianceChecker(rootSchema, holder, walker, session, options).annotate(element); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java new file mode 100644 index 00000000..395b9429 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/impl/inspections/JsonSchemaRefReferenceInspection.java @@ -0,0 +1,93 @@ +// 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 com.jetbrains.jsonSchema.impl.inspections; + +import com.intellij.codeInspection.LocalInspectionToolSession; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.json.JsonBundle; +import com.intellij.json.psi.*; +import com.intellij.openapi.paths.WebReference; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonPointerReferenceProvider; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonSchemaRefReferenceInspection extends JsonSchemaBasedInspectionBase { + @Override + @NotNull + public String getDisplayName() { + return JsonBundle.message("json.schema.ref.refs.inspection.name"); + } + + @Override + protected PsiElementVisitor doBuildVisitor(@NotNull JsonValue root, + @Nullable JsonSchemaObject schema, + @NotNull JsonSchemaService service, + @NotNull ProblemsHolder holder, + @NotNull LocalInspectionToolSession session) { + boolean checkRefs = schema != null && service.isSchemaFile(schema.getSchemaFile()); + return new JsonElementVisitor() { + @Override + public void visitElement(PsiElement element) { + if (element == root) { + if (element instanceof JsonObject) { + final JsonProperty schemaProp = ((JsonObject)element).findProperty("$schema"); + if (schemaProp != null) { + doCheck(schemaProp.getValue()); + } + } + } + super.visitElement(element); + } + + @Override + public void visitProperty(@NotNull JsonProperty o) { + if (!checkRefs) return; + if ("$ref".equals(o.getName())) { + doCheck(o.getValue()); + } + super.visitProperty(o); + } + + private void doCheck(JsonValue value) { + if (!(value instanceof JsonStringLiteral)) return; + for (PsiReference reference : value.getReferences()) { + if (reference instanceof WebReference) continue; + final PsiElement resolved = reference.resolve(); + if (resolved == null) { + holder.registerProblem(reference, getReferenceErrorDesc(reference), ProblemHighlightType.GENERIC_ERROR_OR_WARNING); + } + } + } + + private String getReferenceErrorDesc(PsiReference reference) { + final String text = reference.getCanonicalText(); + if (reference instanceof FileReference) { + final int hash = text.indexOf('#'); + return JsonBundle.message("json.schema.ref.file.not.found", hash == -1 ? text : text.substring(0, hash)); + } + if (reference instanceof JsonPointerReferenceProvider.JsonSchemaIdReference) { + return JsonBundle.message("json.schema.ref.cannot.resolve.id", text); + } + final int lastSlash = text.lastIndexOf('/'); + if (lastSlash == -1) { + return JsonBundle.message("json.schema.ref.cannot.resolve.path", text); + } + final String substring = text.substring(text.lastIndexOf('/') + 1); + + try { + Integer.parseInt(substring); + return JsonBundle.message("json.schema.ref.no.array.element", substring); + } + catch (Exception e) { + return JsonBundle.message("json.schema.ref.no.property", substring); + } + } + }; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java b/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java new file mode 100644 index 00000000..76cbff16 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/remote/JsonFileResolver.java @@ -0,0 +1,92 @@ +// 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 com.jetbrains.jsonSchema.remote; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Couple; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.VirtualFileManager; +import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; +import com.intellij.openapi.vfs.impl.http.RemoteFileInfo; +import com.intellij.openapi.vfs.impl.http.RemoteFileState; +import com.intellij.util.UriUtil; +import com.intellij.util.Url; +import com.intellij.util.Urls; +import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; + +public class JsonFileResolver { + public static boolean isRemoteEnabled(Project project) { + return !ApplicationManager.getApplication().isUnitTestMode() && + JsonSchemaCatalogProjectConfiguration.getInstance(project).isRemoteActivityEnabled(); + } + + @Nullable + public static VirtualFile urlToFile(@NotNull String urlString) { + return VirtualFileManager.getInstance().findFileByUrl(replaceUnsafeSchemaStoreUrls(urlString)); + } + + @Nullable + @Contract("null -> null; !null -> !null") + public static String replaceUnsafeSchemaStoreUrls(@Nullable String urlString) { + if (urlString == null) return null; + if (urlString.equals(JsonSchemaCatalogManager.DEFAULT_CATALOG)) { + return JsonSchemaCatalogManager.DEFAULT_CATALOG_HTTPS; + } + if (StringUtil.startsWithIgnoreCase(urlString, JsonSchemaRemoteContentProvider.STORE_URL_PREFIX_HTTP)) { + String newUrl = StringUtil.replace(urlString, "http://json.schemastore.org/", "https://schemastore.azurewebsites.net/schemas/json/"); + return newUrl.endsWith(".json") ? newUrl : newUrl + ".json"; + } + return urlString; + } + + @Nullable + public static VirtualFile resolveSchemaByReference(@Nullable VirtualFile currentFile, + @Nullable String schemaUrl) { + if (schemaUrl == null) return null; + + boolean isHttpPath = isHttpPath(schemaUrl); + + if (StringUtil.startsWithChar(schemaUrl, '.') || !isHttpPath) { + // relative path + VirtualFile parent = currentFile == null ? null : currentFile.getParent(); + schemaUrl = parent == null ? null : VfsUtilCore.pathToUrl(parent.getPath() + File.separator + schemaUrl); + } + + if (schemaUrl != null) { + VirtualFile virtualFile = urlToFile(schemaUrl); + // validate the URL before returning the file + if (virtualFile instanceof HttpVirtualFile) { + String url = virtualFile.getUrl(); + Url parse = Urls.parse(url, false); + if (parse == null || StringUtil.isEmpty(parse.getAuthority()) || StringUtil.isEmpty(parse.getPath())) return null; + } + if (virtualFile != null) return virtualFile; + } + + return null; + } + + public static void startFetchingHttpFileIfNeeded(@Nullable VirtualFile path, Project project) { + if (!(path instanceof HttpVirtualFile)) return; + + // don't resolve http paths in tests + if (!isRemoteEnabled(project)) return; + + RemoteFileInfo info = ((HttpVirtualFile)path).getFileInfo(); + if (info == null || info.getState() == RemoteFileState.DOWNLOADING_NOT_STARTED) { + path.refresh(true, false); + } + } + + public static boolean isHttpPath(@NotNull String schemaFieldText) { + Couple<String> couple = UriUtil.splitScheme(schemaFieldText); + return couple.first.startsWith("http"); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java new file mode 100644 index 00000000..f5ec8fa5 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogExclusion.java @@ -0,0 +1,19 @@ +// 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 com.jetbrains.jsonSchema.remote; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Disables JSON schema download from schema store for particular files. + * Not intended to be used except to suppress JSON schema for JSCS + */ +@ApiStatus.Experimental +public interface JsonSchemaCatalogExclusion { + + ExtensionPointName<JsonSchemaCatalogExclusion> EP_NAME = ExtensionPointName.create("com.intellij.json.catalog.exclusion"); + + boolean isExcluded(@NotNull VirtualFile file); +} diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java new file mode 100644 index 00000000..461e94cc --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaCatalogManager.java @@ -0,0 +1,173 @@ +// 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 com.jetbrains.jsonSchema.remote; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.impl.http.FileDownloadingAdapter; +import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; +import com.intellij.openapi.vfs.impl.http.RemoteFileInfo; +import com.intellij.openapi.vfs.impl.http.RemoteFileManager; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonCachedValues; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +public class JsonSchemaCatalogManager { + static final String DEFAULT_CATALOG = "http://schemastore.org/api/json/catalog.json"; + static final String DEFAULT_CATALOG_HTTPS = "https://schemastore.azurewebsites.net/api/json/catalog.json"; + @NotNull private final Project myProject; + @NotNull private final JsonSchemaRemoteContentProvider myRemoteContentProvider; + @Nullable private VirtualFile myCatalog = null; + @NotNull private final ConcurrentMap<String, String> myResolvedMappings = ContainerUtil.newConcurrentMap(); + private static final String NO_CACHE = "$_$_WS_NO_CACHE_$_$"; + private static final String EMPTY = "$_$_WS_EMPTY_$_$"; + + public JsonSchemaCatalogManager(@NotNull Project project) { + myProject = project; + myRemoteContentProvider = new JsonSchemaRemoteContentProvider(); + } + + public void startUpdates() { + JsonSchemaCatalogProjectConfiguration.getInstance(myProject).addChangeHandler(() -> { + update(); + JsonSchemaService.Impl.get(myProject).reset(); + }); + RemoteFileManager instance = RemoteFileManager.getInstance(); + instance.addRemoteContentProvider(myRemoteContentProvider); + update(); + } + + private void update() { + // ignore schema catalog when remote activity is disabled (when we're in tests or it is off in settings) + myCatalog = !JsonFileResolver.isRemoteEnabled(myProject) ? null : JsonFileResolver.urlToFile(DEFAULT_CATALOG); + } + + @Nullable + public VirtualFile getSchemaFileForFile(@NotNull VirtualFile file) { + if (!JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isCatalogEnabled()) return null; + for (JsonSchemaCatalogExclusion exclusion : JsonSchemaCatalogExclusion.EP_NAME.getExtensions()) { + if (exclusion.isExcluded(file)) { + return null; + } + } + + String name = file.getName(); + if (myResolvedMappings.containsKey(name)) { + String urlString = myResolvedMappings.get(name); + if (EMPTY.equals(urlString)) return null; + return JsonFileResolver.resolveSchemaByReference(file, urlString); + } + + if (myCatalog != null) { + String urlString = resolveSchemaFile(file, myCatalog, myProject); + if (NO_CACHE.equals(urlString)) return null; + myResolvedMappings.put(name, urlString == null ? EMPTY : urlString); + return JsonFileResolver.resolveSchemaByReference(file, urlString); + } + + return null; + } + + public List<String> getAllCatalogSchemas() { + if (myCatalog != null) { + List<Pair<Collection<String>, String>> catalog = JsonCachedValues.getSchemaCatalog(myCatalog, myProject); + if (catalog == null) return ContainerUtil.emptyList(); + List<String> results = ContainerUtil.newArrayListWithCapacity(catalog.size()); + for (Pair<Collection<String>, String> item: catalog) { + results.add(item.second); + } + return results; + } + + return ContainerUtil.emptyList(); + } + + private final Map<Runnable, FileDownloadingAdapter> myDownloadingAdapters = ContainerUtil.createConcurrentWeakMap(); + public void registerCatalogUpdateCallback(Runnable callback) { + if (myCatalog instanceof HttpVirtualFile) { + RemoteFileInfo info = ((HttpVirtualFile)myCatalog).getFileInfo(); + if (info != null) { + FileDownloadingAdapter adapter = new FileDownloadingAdapter() { + @Override + public void fileDownloaded(@NotNull VirtualFile localFile) { + callback.run(); + } + }; + myDownloadingAdapters.put(callback, adapter); + info.addDownloadingListener(adapter); + } + } + } + + public void unregisterCatalogUpdateCallback(Runnable callback) { + if (!myDownloadingAdapters.containsKey(callback)) return; + + if (myCatalog instanceof HttpVirtualFile) { + RemoteFileInfo info = ((HttpVirtualFile)myCatalog).getFileInfo(); + if (info != null) { + info.removeDownloadingListener(myDownloadingAdapters.get(callback)); + } + } + } + + public void triggerUpdateCatalog(Project project) { + JsonFileResolver.startFetchingHttpFileIfNeeded(myCatalog, project); + } + + @Nullable + private static String resolveSchemaFile(@NotNull VirtualFile file, @NotNull VirtualFile catalogFile, @NotNull Project project) { + JsonFileResolver.startFetchingHttpFileIfNeeded(catalogFile, project); + + List<Pair<Collection<String>, String>> schemaCatalog = JsonCachedValues.getSchemaCatalog(catalogFile, project); + if (schemaCatalog == null) return catalogFile instanceof HttpVirtualFile ? NO_CACHE : null; + String fileName = file.getName(); + for (Pair<Collection<String>, String> maskAndPath: schemaCatalog) { + if (matches(fileName, maskAndPath.first)) { + return maskAndPath.second; + } + } + + return null; + } + + private static boolean matches(@NotNull String fileName, @NotNull Collection<String> masks) { + for (String mask: masks) { + if (matches(fileName, mask)) return true; + } + return false; + } + + private static boolean matches(@NotNull String fileName, @NotNull String mask) { + if (mask.equals(fileName)) return true; + int star = mask.indexOf('*'); + + // no star - no match + if (star == -1) return false; + + // *.foo.json + if (star == 0 && fileName.startsWith(mask.substring(1))) { + return true; + } + + // foobar* + if (star == mask.length() - 1 && fileName.endsWith(mask.substring(0, mask.length() - 1))) { + return true; + } + + String beforeStar = mask.substring(0, star); + String afterStar = mask.substring(star + 1); + + if (fileName.startsWith(beforeStar) && fileName.endsWith(afterStar)) { + return true; + } + return false; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java new file mode 100644 index 00000000..dad1766b --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/remote/JsonSchemaRemoteContentProvider.java @@ -0,0 +1,125 @@ +// 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 com.jetbrains.jsonSchema.remote; + +import com.intellij.json.JsonFileType; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.impl.http.DefaultRemoteContentProvider; +import com.intellij.util.Url; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.io.HttpRequests; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.net.URLConnection; +import java.nio.file.Files; +import java.time.Duration; +import java.util.List; + +public class JsonSchemaRemoteContentProvider extends DefaultRemoteContentProvider { + private static final int DEFAULT_CONNECT_TIMEOUT = 10000; + private static final long UPDATE_DELAY = Duration.ofHours(4).toMillis(); + static final String STORE_URL_PREFIX_HTTP = "http://json.schemastore.org"; + static final String STORE_URL_PREFIX_HTTPS = "https://schemastore.azurewebsites.net"; + private static final String SCHEMA_URL_PREFIX = "http://json-schema.org/"; + private static final String ETAG_HEADER = "ETag"; + private static final String LAST_MODIFIED_HEADER = "Last-Modified"; + + private long myLastUpdateTime = 0; + + @Override + public boolean canProvideContent(@NotNull Url url) { + String externalForm = url.toExternalForm(); + return externalForm.startsWith(STORE_URL_PREFIX_HTTP) + || externalForm.startsWith(STORE_URL_PREFIX_HTTPS) + || externalForm.startsWith(SCHEMA_URL_PREFIX) + || externalForm.endsWith(".json"); + } + + @Override + protected void saveAdditionalData(@NotNull HttpRequests.Request request, @NotNull File file) throws IOException { + URLConnection connection = request.getConnection(); + if (saveTag(file, connection, ETAG_HEADER)) return; + saveTag(file, connection, LAST_MODIFIED_HEADER); + } + + @Nullable + @Override + protected FileType adjustFileType(@Nullable FileType type, @NotNull Url url) { + if (type == null && url.toExternalForm().startsWith(SCHEMA_URL_PREFIX)) { + // json-schema.org doesn't provide a mime-type for schemas + return JsonFileType.INSTANCE; + } + return super.adjustFileType(type, url); + } + + private static boolean saveTag(@NotNull File file, @NotNull URLConnection connection, @NotNull String header) throws IOException { + String tag = connection.getHeaderField(header); + if (tag != null) { + String path = file.getAbsolutePath(); + if (!path.endsWith(".json")) path += ".json"; + File tagFile = new File(path + "." + header); + saveToFile(tagFile, tag); + return true; + } + return false; + } + + private static void saveToFile(@NotNull File tagFile, @NotNull String headerValue) throws IOException { + if (!tagFile.exists()) if (!tagFile.createNewFile()) return; + Files.write(tagFile.toPath(), ContainerUtil.createMaybeSingletonList(headerValue)); + } + + @Override + public boolean isUpToDate(@NotNull Url url, @NotNull VirtualFile local) { + long now = System.currentTimeMillis(); + // don't update more frequently than once in 4 hours + if (now - myLastUpdateTime < UPDATE_DELAY) { + return true; + } + + myLastUpdateTime = now; + String path = local.getPath(); + + if (now - new File(path).lastModified() < UPDATE_DELAY) { + return true; + } + + if (checkUpToDate(url, path, ETAG_HEADER)) return true; + if (checkUpToDate(url, path, LAST_MODIFIED_HEADER)) return true; + + return false; + } + + private boolean checkUpToDate(@NotNull Url url, @NotNull String path, @NotNull String header) { + File file = new File(path + "." + header); + try { + return isUpToDate(url, file, header); + } + catch (IOException e) { + // in case of an error, don't bother with update for the next UPDATE_DELAY milliseconds + //noinspection ResultOfMethodCallIgnored + new File(path).setLastModified(System.currentTimeMillis()); + return true; + } + } + + @Override + protected int getDefaultConnectionTimeout() { + return DEFAULT_CONNECT_TIMEOUT; + } + + private boolean isUpToDate(@NotNull Url url, @NotNull File file, @NotNull String header) throws IOException { + List<String> strings = file.exists() ? Files.readAllLines(file.toPath()) : ContainerUtil.emptyList(); + + String currentTag = strings.size() > 0 ? strings.get(0) : null; + if (currentTag == null) return false; + + String remoteTag = connect(url, HttpRequests.head(url.toExternalForm()), + r -> r.getConnection().getHeaderField(header)); + + return currentTag.equals(remoteTag); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java new file mode 100644 index 00000000..e2fc9365 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableCellEditor.java @@ -0,0 +1,150 @@ +// 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 com.jetbrains.jsonSchema.settings.mappings; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.fileChooser.ex.FileTextFieldImpl; +import com.intellij.openapi.fileChooser.ex.LocalFsFinder; +import com.intellij.openapi.fileChooser.impl.FileChooserFactoryImpl; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.TextBrowseFolderListener; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.AbstractTableCellEditor; +import com.intellij.util.ui.JBUI; +import com.jetbrains.jsonSchema.JsonMappingKind; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.File; + +class JsonMappingsTableCellEditor extends AbstractTableCellEditor { + + final TextFieldWithBrowseButton myComponent; + final JPanel myWrapper; + private final UserDefinedJsonSchemaConfiguration.Item myItem; + private final Project myProject; + private final TreeUpdater myTreeUpdater; + + JsonMappingsTableCellEditor(UserDefinedJsonSchemaConfiguration.Item item, Project project, TreeUpdater treeUpdater) { + myItem = item; + myProject = project; + myTreeUpdater = treeUpdater; + myComponent = new TextFieldWithBrowseButton() { + @Override + protected void installPathCompletion(FileChooserDescriptor fileChooserDescriptor, Disposable parent) { + // do nothing + } + }; + myWrapper = new JPanel(); + myWrapper.setBorder(JBUI.Borders.empty(-3, 0)); + myWrapper.setLayout(new BorderLayout()); + JLabel label = new JLabel(item.mappingKind.getPrefix().trim(), item.mappingKind.getIcon(), SwingConstants.LEFT); + label.setBorder(JBUI.Borders.emptyLeft(1)); + myWrapper.add(label, + BorderLayout.LINE_START); + myWrapper.add(myComponent, + BorderLayout.CENTER); + FileChooserDescriptor descriptor = createDescriptor(item); + if (item.isPattern()) { + myComponent.getButton().setVisible(false); + } + else { + myComponent.addBrowseFolderListener( + new TextBrowseFolderListener( + descriptor, myProject) { + @NotNull + @Override + protected String chosenFileToResultingText(@NotNull VirtualFile chosenFile) { + String relativePath = VfsUtilCore.getRelativePath(chosenFile, myProject.getBaseDir()); + return relativePath != null ? relativePath : chosenFile.getPath(); + } + }); + } + + + FileTextFieldImpl field = null; + if (!item.isPattern() && !ApplicationManager.getApplication().isUnitTestMode() && !ApplicationManager.getApplication().isHeadlessEnvironment()) { + LocalFsFinder finder = new LocalFsFinder(); + finder.setBaseDir(new File(myProject.getBaseDir().getPath())); + field = new MyFileTextFieldImpl(finder, descriptor, myComponent.getTextField(), myProject, myComponent); + } + + // avoid closing the dialog by [Enter] + FileTextFieldImpl finalField = field; + myComponent.getTextField().addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER && (finalField == null || !finalField.isPopupDisplayed())) { + stopCellEditing(); + } + } + }); + } + + @NotNull + private static FileChooserDescriptor createDescriptor(UserDefinedJsonSchemaConfiguration.Item item) { + return item.mappingKind == JsonMappingKind.File + ? FileChooserDescriptorFactory.createSingleFileDescriptor() + : FileChooserDescriptorFactory.createSingleFolderDescriptor(); + } + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { + myComponent.getChildComponent().setText(myItem.getPath()); + return myWrapper; + } + + @Override + public boolean stopCellEditing() { + myItem.setPath(myComponent.getChildComponent().getText()); + myTreeUpdater.updateTree(true); + return super.stopCellEditing(); + } + + @Override + public Object getCellEditorValue() { + return myComponent.getChildComponent().getText(); + } + + private static class MyFileTextFieldImpl extends FileTextFieldImpl { + private final JTextField myTextField; + private final Project myProject; + + MyFileTextFieldImpl(LocalFsFinder finder, FileChooserDescriptor descriptor, JTextField textField, Project project, Disposable parent) { + super(textField, finder, new LocalFsFinder.FileChooserFilter(descriptor, true), + FileChooserFactoryImpl.getMacroMap(), parent); + myTextField = textField; + myProject = project; + myAutopopup = true; + } + + @Nullable + @Override + public VirtualFile getSelectedFile() { + LookupFile lookupFile = getFile(); + return lookupFile != null ? ((LocalFsFinder.VfsFile)lookupFile).getFile() : null; + } + + @Override + protected void setTextToFile(LookupFile file) { + String path = file.getAbsolutePath(); + VirtualFile ioFile = VfsUtil.findFileByIoFile(new File(path), false); + if (ioFile == null) { + myTextField.setText(path); + return; + } + String relativePath = VfsUtilCore.getRelativePath(ioFile, myProject.getBaseDir()); + myTextField.setText(relativePath != null ? relativePath : path); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java new file mode 100644 index 00000000..94697695 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonMappingsTableView.java @@ -0,0 +1,52 @@ +// 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 com.jetbrains.jsonSchema.settings.mappings; + +import com.intellij.ui.SimpleTextAttributes; +import com.intellij.ui.table.TableView; +import com.intellij.util.ui.StatusText; +import com.jetbrains.jsonSchema.JsonMappingKind; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import org.jetbrains.annotations.NotNull; + +import javax.swing.table.TableCellEditor; + +class JsonMappingsTableView extends TableView<UserDefinedJsonSchemaConfiguration.Item> { + private final StatusText myEmptyText; + + JsonMappingsTableView(JsonSchemaMappingsView.MyAddActionButtonRunnable runnable) { + myEmptyText = new StatusText() { + @Override + protected boolean isStatusVisible() { + return isEmpty(); + } + }; + myEmptyText.setText("No schema mappings defined") + .appendSecondaryText("Add mapping for a ", SimpleTextAttributes.REGULAR_ATTRIBUTES, null); + + JsonMappingKind[] values = JsonMappingKind.values(); + for (int i = 0; i < values.length; i++) { + JsonMappingKind kind = values[i]; + myEmptyText.appendSecondaryText(kind.getDescription(), SimpleTextAttributes.LINK_ATTRIBUTES, + e -> runnable.doRun(kind)); + if (i < values.length - 1) { + myEmptyText.appendSecondaryText(", ", SimpleTextAttributes.REGULAR_ATTRIBUTES, null); + } + } + + setFocusTraversalKeysEnabled(false); + } + + @Override + public void setCellEditor(TableCellEditor anEditor) { + super.setCellEditor(anEditor); + if (anEditor != null) { + ((JsonMappingsTableCellEditor)anEditor).myComponent.getTextField().requestFocus(); + } + } + + @NotNull + @Override + public StatusText getEmptyText() { + return myEmptyText; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java new file mode 100644 index 00000000..a264808c --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaConfigurable.java @@ -0,0 +1,219 @@ +// 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 com.jetbrains.jsonSchema.settings.mappings; + +import com.intellij.execution.configurations.RuntimeConfigurationWarning; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.ui.NamedConfigurable; +import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; +import com.intellij.util.Function; +import com.intellij.util.Urls; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import com.jetbrains.jsonSchema.impl.JsonSchemaReader; +import com.jetbrains.jsonSchema.remote.JsonFileResolver; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.io.File; + +/** + * @author Irina.Chernushina on 2/2/2016. + */ +public class JsonSchemaConfigurable extends NamedConfigurable<UserDefinedJsonSchemaConfiguration> { + private final Project myProject; + @NotNull private final String mySchemaFilePath; + @NotNull private final UserDefinedJsonSchemaConfiguration mySchema; + @Nullable private final TreeUpdater myTreeUpdater; + @NotNull private final Function<? super String, String> myNameCreator; + private JsonSchemaMappingsView myView; + private String myDisplayName; + private String myError; + + public JsonSchemaConfigurable(Project project, + @NotNull String schemaFilePath, @NotNull UserDefinedJsonSchemaConfiguration schema, + @Nullable TreeUpdater updateTree, + @NotNull Function<? super String, String> nameCreator) { + super(true, () -> { + if (updateTree != null) { + updateTree.updateTree(true); + } + }); + myProject = project; + mySchemaFilePath = schemaFilePath; + mySchema = schema; + myTreeUpdater = updateTree; + myNameCreator = nameCreator; + myDisplayName = mySchema.getName(); + } + + @NotNull + public UserDefinedJsonSchemaConfiguration getSchema() { + return mySchema; + } + + @Override + public void setDisplayName(String name) { + myDisplayName = name; + } + + @Override + public UserDefinedJsonSchemaConfiguration getEditableObject() { + return mySchema; + } + + @Override + public String getBannerSlogan() { + return mySchema.getName(); + } + + @Override + public JComponent createOptionsPanel() { + if (myView == null) { + myView = new JsonSchemaMappingsView(myProject, myTreeUpdater, s -> { + if (myDisplayName.startsWith(JsonSchemaMappingsConfigurable.STUB_SCHEMA_NAME)) { + int lastSlash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\')); + if (lastSlash > 0) { + String substring = s.substring(lastSlash + 1); + int dot = substring.lastIndexOf('.'); + if (dot != -1) { + substring = substring.substring(0, dot); + } + setDisplayName(myNameCreator.fun(substring)); + updateName(); + } + } + }); + myView.setError(myError, true); + } + return myView.getComponent(); + } + + @Nls + @Override + public String getDisplayName() { + return myDisplayName; + } + + @Nullable + @Override + public String getHelpTopic() { + return JsonSchemaMappingsConfigurable.SETTINGS_JSON_SCHEMA; + } + + @Override + public boolean isModified() { + if (myView == null) return false; + if (!FileUtil.toSystemDependentName(mySchema.getRelativePathToSchema()).equals(myView.getSchemaSubPath())) return true; + if (mySchema.getSchemaVersion() != myView.getSchemaVersion()) return true; + return !Comparing.equal(myView.getData(), mySchema.getPatterns()); + } + + @Override + public void apply() throws ConfigurationException { + if (myView == null) return; + doValidation(); + mySchema.setName(myDisplayName); + mySchema.setSchemaVersion(myView.getSchemaVersion()); + mySchema.setPatterns(myView.getData()); + mySchema.setRelativePathToSchema(myView.getSchemaSubPath()); + } + + public static boolean isValidURL(@NotNull final String url) { + return JsonFileResolver.isHttpPath(url) && Urls.parse(url, false) != null; + } + + private void doValidation() throws ConfigurationException { + String schemaSubPath = myView.getSchemaSubPath(); + + if (StringUtil.isEmptyOrSpaces(schemaSubPath)) { + throw new ConfigurationException((!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Schema path is empty"); + } + + VirtualFile vFile; + String filename; + + if (JsonFileResolver.isHttpPath(schemaSubPath)) { + filename = schemaSubPath; + + if (!isValidURL(schemaSubPath)) { + throw new ConfigurationException( + (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Invalid schema URL"); + } + + vFile = JsonFileResolver.urlToFile(schemaSubPath); + if (vFile == null) { + throw new ConfigurationException( + (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Invalid URL resource"); + } + } + else { + File subPath = new File(schemaSubPath); + final File file = subPath.isAbsolute() ? subPath : new File(myProject.getBasePath(), schemaSubPath); + if (!file.exists() || (vFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)) == null) { + throw new ConfigurationException( + (!StringUtil.isEmptyOrSpaces(myDisplayName) ? (myDisplayName + ": ") : "") + "Schema file does not exist"); + } + filename = file.getName(); + } + + if (StringUtil.isEmptyOrSpaces(myDisplayName)) throw new ConfigurationException(filename + ": Schema name is empty"); + + // we don't validate remote schemas while in options dialog + if (vFile instanceof HttpVirtualFile) return; + + final String error = JsonSchemaReader.checkIfValidJsonSchema(myProject, vFile); + if (error != null) { + logErrorForUser(error); + throw new RuntimeConfigurationWarning(error); + } + } + + private void logErrorForUser(@NotNull final String error) { + JsonSchemaReader.ERRORS_NOTIFICATION.createNotification(error, MessageType.WARNING).notify(myProject); + } + + @Override + public void reset() { + if (myView == null) return; + myView.setItems(mySchemaFilePath, mySchema.getSchemaVersion(), mySchema.getPatterns()); + setDisplayName(mySchema.getName()); + } + + public UserDefinedJsonSchemaConfiguration getUiSchema() { + final UserDefinedJsonSchemaConfiguration info = new UserDefinedJsonSchemaConfiguration(); + info.setApplicationDefined(mySchema.isApplicationDefined()); + if (myView != null && myView.isInitialized()) { + info.setName(getDisplayName()); + info.setSchemaVersion(myView.getSchemaVersion()); + info.setPatterns(myView.getData()); + info.setRelativePathToSchema(myView.getSchemaSubPath()); + } else { + info.setName(mySchema.getName()); + info.setSchemaVersion(mySchema.getSchemaVersion()); + info.setPatterns(mySchema.getPatterns()); + info.setRelativePathToSchema(mySchema.getRelativePathToSchema()); + } + return info; + } + + @Override + public void disposeUIResources() { + if (myView != null) Disposer.dispose(myView); + } + + public void setError(String error, boolean showWarning) { + myError = error; + if (myView != null) { + myView.setError(error, showWarning); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java new file mode 100644 index 00000000..b68e2098 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsConfigurable.java @@ -0,0 +1,333 @@ +// 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 com.jetbrains.jsonSchema.settings.mappings; + +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonShortcuts; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.options.SearchableConfigurable; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.ui.MasterDetailsComponent; +import com.intellij.ui.EditorNotifications; +import com.intellij.util.Function; +import com.intellij.util.IconUtil; +import com.intellij.util.ThreeState; +import com.intellij.util.containers.MultiMap; +import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.tree.DefaultTreeModel; +import java.io.File; +import java.util.*; + +import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath; + +/** + * @author Irina.Chernushina on 2/2/2016. + */ +public class JsonSchemaMappingsConfigurable extends MasterDetailsComponent implements SearchableConfigurable, Disposable { + @NonNls public static final String SETTINGS_JSON_SCHEMA = "settings.json.schema"; + public static final String JSON_SCHEMA_MAPPINGS = "JSON Schema Mappings"; + + private final static Comparator<UserDefinedJsonSchemaConfiguration> COMPARATOR = (o1, o2) -> { + if (o1.isApplicationDefined() != o2.isApplicationDefined()) { + return o1.isApplicationDefined() ? 1 : -1; + } + return o1.getName().compareToIgnoreCase(o2.getName()); + }; + static final String STUB_SCHEMA_NAME = "New Schema"; + private String myError; + + @NotNull + private final Project myProject; + private final TreeUpdater myTreeUpdater = showWarning -> { + TREE_UPDATER.run(); + updateWarningText(showWarning); + }; + + private final Function<String, String> myNameCreator = s -> createUniqueName(s); + + public JsonSchemaMappingsConfigurable(@NotNull final Project project) { + myProject = project; + initTree(); + } + + @Nullable + @Override + protected String getEmptySelectionString() { + return myRoot.children().hasMoreElements() ? "Select JSON Schema to view" : + "Please add a JSON Schema file and configure its usage"; + } + + @Nullable + @Override + protected ArrayList<AnAction> createActions(boolean fromPopup) { + final ArrayList<AnAction> result = new ArrayList<>(); + result.add(new DumbAwareAction("Add", "Add", IconUtil.getAddIcon()) { + { + registerCustomShortcutSet(CommonShortcuts.INSERT, myTree); + } + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + addProjectSchema(); + } + }); + result.add(new MyDeleteAction()); + return result; + } + + public UserDefinedJsonSchemaConfiguration addProjectSchema() { + UserDefinedJsonSchemaConfiguration configuration = new UserDefinedJsonSchemaConfiguration(createUniqueName(STUB_SCHEMA_NAME), + JsonSchemaVersion.SCHEMA_4, "", false, null); + addCreatedMappings(configuration); + return configuration; + } + + @SuppressWarnings("SameParameterValue") + @NotNull + private String createUniqueName(@NotNull String s) { + int max = -1; + Enumeration children = myRoot.children(); + while (children.hasMoreElements()) { + Object element = children.nextElement(); + if (!(element instanceof MyNode)) continue; + String displayName = ((MyNode)element).getDisplayName(); + if (displayName.startsWith(s)) { + String lastPart = displayName.substring(s.length()).trim(); + if (lastPart.length() == 0 && max == -1) { + max = 1; + continue; + } + int i = tryParseInt(lastPart); + if (i == -1) continue; + max = i > max ? i : max; + } + } + return max == -1 ? s : (s + " " + (max + 1)); + } + + private static int tryParseInt(@NotNull String s) { + try { + return Integer.parseInt(s); + } + catch (NumberFormatException e) { + return -1; + } + } + + private void addCreatedMappings(@NotNull final UserDefinedJsonSchemaConfiguration info) { + final JsonSchemaConfigurable configurable = new JsonSchemaConfigurable(myProject, "", info, myTreeUpdater, myNameCreator); + configurable.setError(myError, true); + final MyNode node = new MyNode(configurable); + addNode(node, myRoot); + selectNodeInTree(node, true); + } + + private void fillTree() { + myRoot.removeAllChildren(); + + if (myProject.isDefault()) return; + + final List<UserDefinedJsonSchemaConfiguration> list = getStoredList(); + for (UserDefinedJsonSchemaConfiguration info : list) { + String pathToSchema = info.getRelativePathToSchema(); + final JsonSchemaConfigurable configurable = + new JsonSchemaConfigurable(myProject, isHttpPath(pathToSchema) || new File(pathToSchema).isAbsolute() ? pathToSchema : new File(myProject.getBasePath(), pathToSchema).getPath(), + info, myTreeUpdater, myNameCreator); + configurable.setError(myError, true); + myRoot.add(new MyNode(configurable)); + } + ((DefaultTreeModel) myTree.getModel()).reload(myRoot); + if (myRoot.children().hasMoreElements()) { + myTree.addSelectionRow(0); + } + } + + @NotNull + private List<UserDefinedJsonSchemaConfiguration> getStoredList() { + final List<UserDefinedJsonSchemaConfiguration> list = new ArrayList<>(); + final Map<String, UserDefinedJsonSchemaConfiguration> projectState = JsonSchemaMappingsProjectConfiguration + .getInstance(myProject).getStateMap(); + if (projectState != null) { + list.addAll(projectState.values()); + } + + Collections.sort(list, COMPARATOR); + return list; + } + + @Override + public void apply() throws ConfigurationException { + final List<UserDefinedJsonSchemaConfiguration> uiList = getUiList(true); + validate(uiList); + final Map<String, UserDefinedJsonSchemaConfiguration> projectMap = new HashMap<>(); + for (UserDefinedJsonSchemaConfiguration info : uiList) { + projectMap.put(info.getName(), info); + } + + JsonSchemaMappingsProjectConfiguration.getInstance(myProject).setState(projectMap); + final Project[] projects = ProjectManager.getInstance().getOpenProjects(); + for (Project project : projects) { + final JsonSchemaService service = JsonSchemaService.Impl.get(project); + if (service != null) service.reset(); + } + DaemonCodeAnalyzer.getInstance(myProject).restart(); + EditorNotifications.getInstance(myProject).updateAllNotifications(); + } + + private static void validate(@NotNull List<UserDefinedJsonSchemaConfiguration> list) throws ConfigurationException { + final Set<String> set = new HashSet<>(); + for (UserDefinedJsonSchemaConfiguration info : list) { + if (set.contains(info.getName())) { + throw new ConfigurationException("Duplicate schema name: '" + info.getName() + "'"); + } + set.add(info.getName()); + } + } + + @Override + public boolean isModified() { + final List<UserDefinedJsonSchemaConfiguration> storedList = getStoredList(); + final List<UserDefinedJsonSchemaConfiguration> uiList; + try { + uiList = getUiList(false); + } + catch (ConfigurationException e) { + //will not happen + return false; + } + return !storedList.equals(uiList); + } + + private void updateWarningText(boolean showWarning) { + final MultiMap<String, UserDefinedJsonSchemaConfiguration.Item> patternsMap = new MultiMap<>(); + final StringBuilder sb = new StringBuilder(); + final List<UserDefinedJsonSchemaConfiguration> list; + try { + list = getUiList(false); + } + catch (ConfigurationException e) { + // will not happen + return; + } + for (UserDefinedJsonSchemaConfiguration info : list) { + info.refreshPatterns(); + final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(myProject); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = info.getPatterns(); + for (UserDefinedJsonSchemaConfiguration.Item pattern : patterns) { + for (Map.Entry<String, Collection<UserDefinedJsonSchemaConfiguration.Item>> entry : patternsMap.entrySet()) { + for (UserDefinedJsonSchemaConfiguration.Item item : entry.getValue()) { + final ThreeState similar = comparator.isSimilar(pattern, item); + if (ThreeState.NO.equals(similar)) continue; + + if (sb.length() > 0) sb.append('\n'); + sb.append("'").append(pattern.getPresentation()).append("' for schema '") + .append(info.getName()).append("' and '").append(item.getPresentation()).append("' for schema '").append(entry.getKey()) + .append("'"); + } + } + } + patternsMap.put(info.getName(), patterns); + } + if (sb.length() > 0) { + myError = "Conflicting mappings:\n" + sb.toString(); + } else { + myError = null; + } + final Enumeration children = myRoot.children(); + while (children.hasMoreElements()) { + Object o = children.nextElement(); + if (o instanceof MyNode && ((MyNode)o).getConfigurable() instanceof JsonSchemaConfigurable) { + ((JsonSchemaConfigurable) ((MyNode)o).getConfigurable()).setError(myError, showWarning); + } + } + } + + public void selectInTree(UserDefinedJsonSchemaConfiguration configuration) { + final Enumeration children = myRoot.children(); + while (children.hasMoreElements()) { + final MyNode node = (MyNode)children.nextElement(); + JsonSchemaConfigurable configurable = (JsonSchemaConfigurable)node.getConfigurable(); + if (Objects.equals(configurable.getUiSchema(), configuration)) { + selectNodeInTree(node); + } + } + } + + @NotNull + private List<UserDefinedJsonSchemaConfiguration> getUiList(boolean applyChildren) throws ConfigurationException { + final List<UserDefinedJsonSchemaConfiguration> uiList = new ArrayList<>(); + final Enumeration children = myRoot.children(); + while (children.hasMoreElements()) { + final MyNode node = (MyNode)children.nextElement(); + if (applyChildren) { + node.getConfigurable().apply(); + uiList.add(getSchemaInfo(node)); + } + else { + uiList.add(((JsonSchemaConfigurable) node.getConfigurable()).getUiSchema()); + } + } + Collections.sort(uiList, COMPARATOR); + return uiList; + } + + @Override + public void reset() { + fillTree(); + updateWarningText(true); + } + + @Override + protected Comparator<MyNode> getNodeComparator() { + return (o1, o2) -> { + if (o1.getConfigurable() instanceof JsonSchemaConfigurable && o2.getConfigurable() instanceof JsonSchemaConfigurable) { + return COMPARATOR.compare(getSchemaInfo(o1), getSchemaInfo(o2)); + } + return o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName()); + }; + } + + private static UserDefinedJsonSchemaConfiguration getSchemaInfo(@NotNull final MyNode node) { + return ((JsonSchemaConfigurable) node.getConfigurable()).getSchema(); + } + + @Nls + @Override + public String getDisplayName() { + return JSON_SCHEMA_MAPPINGS; + } + + + @Override + public void dispose() { + final Enumeration children = myRoot.children(); + while (children.hasMoreElements()) { + Object o = children.nextElement(); + if (o instanceof MyNode) { + ((MyNode)o).getConfigurable().disposeUIResources(); + } + } + } + + @NotNull + @Override + public String getId() { + return SETTINGS_JSON_SCHEMA; + } + + @Override + public String getHelpTopic() { + return SETTINGS_JSON_SCHEMA; + } +} diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java new file mode 100644 index 00000000..795a59c0 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaMappingsView.java @@ -0,0 +1,339 @@ +// 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 com.jetbrains.jsonSchema.settings.mappings; + +import com.intellij.icons.AllIcons; +import com.intellij.ide.util.PsiNavigationSupport; +import com.intellij.json.JsonBundle; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.CommonShortcuts; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.MessageType; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.openapi.ui.panel.ComponentPanelBuilder; +import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.ui.popup.BalloonBuilder; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.ui.popup.PopupStep; +import com.intellij.openapi.ui.popup.util.BaseListPopupStep; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.AnActionButton; +import com.intellij.ui.AnActionButtonRunnable; +import com.intellij.ui.DocumentAdapter; +import com.intellij.ui.ToolbarDecorator; +import com.intellij.ui.awt.RelativePoint; +import com.intellij.ui.components.JBLabel; +import com.intellij.ui.components.JBTextField; +import com.intellij.ui.table.TableView; +import com.intellij.util.ui.*; +import com.jetbrains.jsonSchema.JsonMappingKind; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import com.jetbrains.jsonSchema.extension.JsonSchemaInfo; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellEditor; +import javax.swing.table.TableCellRenderer; +import java.awt.*; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static com.jetbrains.jsonSchema.remote.JsonFileResolver.isHttpPath; + +/** + * @author Irina.Chernushina on 2/2/2016. + */ +public class JsonSchemaMappingsView implements Disposable { + private static final String ADD_SCHEMA_MAPPING = "settings.json.schema.add.mapping"; + private static final String EDIT_SCHEMA_MAPPING = "settings.json.schema.edit.mapping"; + private static final String REMOVE_SCHEMA_MAPPING = "settings.json.schema.remove.mapping"; + private final TreeUpdater myTreeUpdater; + private final Consumer<? super String> mySchemaPathChangedCallback; + private TableView<UserDefinedJsonSchemaConfiguration.Item> myTableView; + private JComponent myComponent; + private Project myProject; + private TextFieldWithBrowseButton mySchemaField; + private ComboBox<JsonSchemaVersion> mySchemaVersionComboBox; + private JEditorPane myError; + private String myErrorText; + private JBLabel myErrorIcon; + private boolean myInitialized; + + public JsonSchemaMappingsView(Project project, + TreeUpdater treeUpdater, + Consumer<? super String> schemaPathChangedCallback) { + myTreeUpdater = treeUpdater; + mySchemaPathChangedCallback = schemaPathChangedCallback; + createUI(project); + } + + private void createUI(final Project project) { + myProject = project; + MyAddActionButtonRunnable addActionButtonRunnable = new MyAddActionButtonRunnable(); + + myTableView = new JsonMappingsTableView(addActionButtonRunnable); + myTableView.getTableHeader().setVisible(false); + final ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myTableView); + final MyEditActionButtonRunnableImpl editAction = new MyEditActionButtonRunnableImpl(); + decorator.setRemoveAction(new MyRemoveActionButtonRunnable()) + .setRemoveActionName(REMOVE_SCHEMA_MAPPING) + .setAddAction(addActionButtonRunnable) + .setAddActionName(JsonBundle.message(ADD_SCHEMA_MAPPING)) + .setEditAction(editAction) + .setEditActionName(JsonBundle.message(EDIT_SCHEMA_MAPPING)) + .disableUpDownActions(); + + JBTextField schemaFieldBacking = new JBTextField(); + mySchemaField = new TextFieldWithBrowseButton(schemaFieldBacking); + SwingHelper.installFileCompletionAndBrowseDialog(myProject, mySchemaField, JsonBundle.message("json.schema.add.schema.chooser.title"), + FileChooserDescriptorFactory.createSingleFileDescriptor()); + mySchemaField.getTextField().getDocument().addDocumentListener(new DocumentAdapter() { + @Override + protected void textChanged(@NotNull DocumentEvent e) { + mySchemaPathChangedCallback.accept(mySchemaField.getText()); + } + }); + attachNavigateToSchema(); + myError = SwingHelper.createHtmlLabel(JsonBundle.message("json.schema.conflicting.mappings"), null, s -> { + final BalloonBuilder builder = JBPopupFactory.getInstance(). + createHtmlTextBalloonBuilder(myErrorText, UIUtil.getBalloonWarningIcon(), MessageType.WARNING.getPopupBackground(), null); + builder.setDisposable(this); + builder.setHideOnClickOutside(true); + builder.setCloseButtonEnabled(true); + builder.createBalloon().showInCenterOf(myError); + }); + + final FormBuilder builder = FormBuilder.createFormBuilder(); + final JBLabel label = new JBLabel(JsonBundle.message("json.schema.file.selector.title")); + builder.addLabeledComponent(label, mySchemaField); + label.setLabelFor(mySchemaField); + label.setBorder(JBUI.Borders.empty(0, 10)); + mySchemaField.setBorder(JBUI.Borders.emptyRight(10)); + JBLabel versionLabel = new JBLabel("Schema version:"); + mySchemaVersionComboBox = new ComboBox<>(new DefaultComboBoxModel<>(JsonSchemaVersion.values())); + versionLabel.setLabelFor(mySchemaVersionComboBox); + versionLabel.setBorder(JBUI.Borders.empty(0, 10)); + builder.addLabeledComponent(versionLabel, mySchemaVersionComboBox); + final JPanel wrapper = new JPanel(new BorderLayout()); + wrapper.setBorder(JBUI.Borders.empty(0, 10)); + myErrorIcon = new JBLabel(UIUtil.getBalloonWarningIcon()); + wrapper.add(myErrorIcon, BorderLayout.WEST); + wrapper.add(myError, BorderLayout.CENTER); + builder.addComponent(wrapper); + JPanel panel = decorator.createPanel(); + panel.setBorder(BorderFactory.createCompoundBorder(JBUI.Borders.empty(0, 8), panel.getBorder())); + builder.addComponentFillVertically(panel, 5); + JLabel commentComponent = ComponentPanelBuilder.createCommentComponent("Path to file or directory relative to project root, or file name pattern like *.config.json", false); + commentComponent.setBorder(JBUI.Borders.empty(0, 8, 5, 0)); + builder.addComponent(commentComponent); + + myComponent = builder.getPanel(); + } + + @Override + public void dispose() { + } + + public void setError(final String text, boolean showWarning) { + myErrorText = text; + myError.setVisible(showWarning && text != null); + myErrorIcon.setVisible(showWarning && text != null); + } + + private void attachNavigateToSchema() { + DumbAwareAction.create(e -> { + String pathToSchema = mySchemaField.getText(); + if (StringUtil.isEmptyOrSpaces(pathToSchema) || isHttpPath(pathToSchema)) return; + VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(new File(pathToSchema)); + if (virtualFile == null) { + BalloonBuilder balloonBuilder = JBPopupFactory.getInstance() + .createHtmlTextBalloonBuilder(JsonBundle.message("json.schema.file.not.found"), UIUtil.getBalloonErrorIcon(), MessageType.ERROR.getPopupBackground(), null); + Balloon balloon = balloonBuilder.setFadeoutTime(TimeUnit.SECONDS.toMillis(3)).createBalloon(); + balloon.showInCenterOf(mySchemaField); + return; + } + PsiNavigationSupport.getInstance().createNavigatable(myProject, virtualFile, -1).navigate(true); + }).registerCustomShortcutSet(CommonShortcuts.getEditSource(), mySchemaField); + } + + public List<UserDefinedJsonSchemaConfiguration.Item> getData() { + return Collections.unmodifiableList( + myTableView.getListTableModel().getItems().stream() + .filter(i -> i.mappingKind == JsonMappingKind.Directory || !StringUtil.isEmpty(i.path)) + .collect(Collectors.toList())); + } + + public void setItems(String schemaFilePath, + JsonSchemaVersion version, + final List<UserDefinedJsonSchemaConfiguration.Item> data) { + myInitialized = true; + mySchemaField.setText(schemaFilePath); + mySchemaVersionComboBox.setSelectedItem(version); + myTableView.setModelAndUpdateColumns( + new ListTableModel<>(createColumns(), new ArrayList<>(data))); + } + + public boolean isInitialized() { + return myInitialized; + } + + public JsonSchemaVersion getSchemaVersion() { + return (JsonSchemaVersion)mySchemaVersionComboBox.getSelectedItem(); + } + + public String getSchemaSubPath() { + String schemaFieldText = mySchemaField.getText(); + if (isHttpPath(schemaFieldText)) return schemaFieldText; + return FileUtil.toSystemDependentName(JsonSchemaInfo.getRelativePath(myProject, schemaFieldText)); + } + + private ColumnInfo[] createColumns() { + return new ColumnInfo[] { new MappingItemColumnInfo() }; + } + + public JComponent getComponent() { + return myComponent; + } + + private class MappingItemColumnInfo extends ColumnInfo<UserDefinedJsonSchemaConfiguration.Item, String> { + MappingItemColumnInfo() {super("");} + + @Nullable + @Override + public String valueOf(UserDefinedJsonSchemaConfiguration.Item item) { + return item.getPresentation(); + } + + @NotNull + @Override + public TableCellRenderer getRenderer(UserDefinedJsonSchemaConfiguration.Item item) { + return new DefaultTableCellRenderer() { + @Override + public Component getTableCellRendererComponent(JTable table, + Object value, + boolean isSelected, + boolean hasFocus, + int row, + int column) { + JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + label.setIcon(item.mappingKind.getIcon()); + + String error = item.getError(); + if (error == null) { + return label; + } + + JPanel panel = new JPanel(); + panel.setLayout(new BorderLayout()); + panel.add(label, BorderLayout.CENTER); + JLabel warning = new JLabel(AllIcons.General.Warning); + panel.setBackground(label.getBackground()); + panel.setToolTipText(error); + panel.add(warning, BorderLayout.LINE_END); + return panel; + } + }; + } + + @Nullable + @Override + public TableCellEditor getEditor(UserDefinedJsonSchemaConfiguration.Item item) { + return new JsonMappingsTableCellEditor(item, myProject, myTreeUpdater); + } + + @Override + public boolean isCellEditable(UserDefinedJsonSchemaConfiguration.Item item) { + return true; + } + } + + class MyAddActionButtonRunnable implements AnActionButtonRunnable { + MyAddActionButtonRunnable() { + super(); + } + + @Override + public void run(AnActionButton button) { + RelativePoint point = button.getPreferredPopupPoint(); + if (point == null) { + point = new RelativePoint(button.getContextComponent(), new Point(0, 0)); + } + JBPopupFactory.getInstance().createListPopup(new BaseListPopupStep<JsonMappingKind>(null, + JsonMappingKind.values()) { + @NotNull + @Override + public String getTextFor(JsonMappingKind value) { + return "Add " + StringUtil.capitalizeWords(value.getDescription(), true); + } + + @Override + public Icon getIconFor(JsonMappingKind value) { + return value.getIcon(); + } + + @Override + public PopupStep onChosen(JsonMappingKind selectedValue, boolean finalChoice) { + if (finalChoice) { + return doFinalStep(() -> doRun(selectedValue)); + } + return PopupStep.FINAL_CHOICE; + } + }).show(point); + } + + void doRun(JsonMappingKind mappingKind) { + UserDefinedJsonSchemaConfiguration.Item currentItem = new UserDefinedJsonSchemaConfiguration.Item("", mappingKind); + myTableView.getListTableModel().addRow(currentItem); + myTableView.editCellAt(myTableView.getListTableModel().getRowCount() - 1, 0); + + myTreeUpdater.updateTree(false); + } + } + + private class MyEditActionButtonRunnableImpl implements AnActionButtonRunnable { + MyEditActionButtonRunnableImpl() { + super(); + } + + @Override + public void run(AnActionButton button) { + execute(); + } + + public void execute() { + int selectedRow = myTableView.getSelectedRow(); + if (selectedRow == -1) return; + myTableView.editCellAt(selectedRow, 0); + } + } + + private class MyRemoveActionButtonRunnable implements AnActionButtonRunnable { + @Override + public void run(AnActionButton button) { + final int[] rows = myTableView.getSelectedRows(); + if (rows != null && rows.length > 0) { + int cnt = 0; + for (int row : rows) { + myTableView.getListTableModel().removeRow(row - cnt); + ++cnt; + } + myTableView.getListTableModel().fireTableDataChanged(); + myTreeUpdater.updateTree(true); + } + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java new file mode 100644 index 00000000..1538a549 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/JsonSchemaPatternComparator.java @@ -0,0 +1,104 @@ +// 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 com.jetbrains.jsonSchema.settings.mappings; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.BeforeAfter; +import com.intellij.util.ThreeState; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; + +/** + * @author Irina.Chernushina on 2/17/2016. + */ +public class JsonSchemaPatternComparator { + @NotNull + private final Project myProject; + + public JsonSchemaPatternComparator(@NotNull Project project) { + myProject = project; + } + + @NotNull + public ThreeState isSimilar(@NotNull UserDefinedJsonSchemaConfiguration.Item itemLeft, + @NotNull UserDefinedJsonSchemaConfiguration.Item itemRight) { + if (itemLeft.isPattern() != itemRight.isPattern()) return ThreeState.NO; + if (itemLeft.isPattern()) return comparePatterns(itemLeft, itemRight); + return comparePaths(itemLeft, itemRight); + } + + private ThreeState comparePaths(UserDefinedJsonSchemaConfiguration.Item left, UserDefinedJsonSchemaConfiguration.Item right) { + String leftPath = left.getPath(); + String rightPath = right.getPath(); + + if (leftPath.startsWith("mock:///") || rightPath.startsWith("mock:///")) { + return leftPath.equals(rightPath) ? ThreeState.YES : ThreeState.NO; + } + final File leftFile = new File(myProject.getBasePath(), leftPath); + final File rightFile = new File(myProject.getBasePath(), rightPath); + + if (left.isDirectory()) { + if (FileUtil.isAncestor(leftFile, rightFile, true)) return ThreeState.YES; + } + if (right.isDirectory()) { + if (FileUtil.isAncestor(rightFile, leftFile, true)) return ThreeState.YES; + } + return FileUtil.filesEqual(leftFile, rightFile) && left.isDirectory() == right.isDirectory() ? ThreeState.YES : ThreeState.NO; + } + + private static ThreeState comparePatterns(@NotNull final UserDefinedJsonSchemaConfiguration.Item leftItem, + @NotNull final UserDefinedJsonSchemaConfiguration.Item rightItem) { + if (leftItem.getPath().equals(rightItem.getPath())) return ThreeState.YES; + if (leftItem.getPath().indexOf(File.separatorChar) >= 0 || rightItem.getPath().indexOf(File.separatorChar) >= 0) { + // todo: better heuristic + return ThreeState.NO; + } + final BeforeAfter<String> left = getBeforeAfterAroundWildCards(leftItem.getPath()); + final BeforeAfter<String> right = getBeforeAfterAroundWildCards(rightItem.getPath()); + if (left == null || right == null) { + if (left == null && right == null) return leftItem.getPath().equals(rightItem.getPath()) ? ThreeState.YES : ThreeState.NO; + if (left == null) { + return checkOneSideWithoutWildcard(leftItem, right); + } + return checkOneSideWithoutWildcard(rightItem, left); + } + if (!StringUtil.isEmptyOrSpaces(left.getBefore()) && !StringUtil.isEmptyOrSpaces(right.getBefore())) { + if (left.getBefore().startsWith(right.getBefore()) || right.getBefore().startsWith(left.getBefore())) { + return ThreeState.YES; + } + // otherwise they are different + return ThreeState.NO; + } + if (!StringUtil.isEmptyOrSpaces(left.getAfter()) && !StringUtil.isEmptyOrSpaces(right.getAfter())) { + if (left.getAfter().endsWith(right.getAfter()) || right.getAfter().endsWith(left.getAfter())) { + return ThreeState.YES; + } + // otherwise they are different + return ThreeState.NO; + } + return ThreeState.UNSURE; + } + + @NotNull + private static ThreeState checkOneSideWithoutWildcard(UserDefinedJsonSchemaConfiguration.Item item, BeforeAfter<String> beforeAfter) { + if (!StringUtil.isEmptyOrSpaces(beforeAfter.getBefore()) && item.getPath().startsWith(beforeAfter.getBefore())) { + return ThreeState.YES; + } + if (!StringUtil.isEmptyOrSpaces(beforeAfter.getAfter()) && item.getPath().endsWith(beforeAfter.getAfter())) { + return ThreeState.YES; + } + return ThreeState.UNSURE; + } + + @Nullable + private static BeforeAfter<String> getBeforeAfterAroundWildCards(@NotNull final String pattern) { + final int firstIdx = pattern.indexOf('*'); + final int lastIdx = pattern.lastIndexOf('*'); + if (firstIdx < 0 || lastIdx < 0) return null; + return new BeforeAfter<>(pattern.substring(0, firstIdx), pattern.substring(lastIdx + 1)); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java b/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java new file mode 100644 index 00000000..364f730e --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/settings/mappings/TreeUpdater.java @@ -0,0 +1,6 @@ +// 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 com.jetbrains.jsonSchema.settings.mappings; + +public interface TreeUpdater { + void updateTree(boolean showWarning); +} diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java new file mode 100644 index 00000000..e57ccbf8 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaInfoPopupStep.java @@ -0,0 +1,169 @@ +// 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 com.jetbrains.jsonSchema.widget; + +import com.intellij.icons.AllIcons; +import com.intellij.openapi.options.ShowSettingsUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.ListSeparator; +import com.intellij.openapi.ui.popup.PopupStep; +import com.intellij.openapi.ui.popup.util.BaseListPopupStep; +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ui.EmptyIcon; +import com.intellij.util.ui.JBUI; +import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import com.jetbrains.jsonSchema.extension.JsonSchemaInfo; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.settings.mappings.JsonSchemaMappingsConfigurable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static com.jetbrains.jsonSchema.widget.JsonSchemaStatusPopup.*; + +class JsonSchemaInfoPopupStep extends BaseListPopupStep<JsonSchemaInfo> { + private final Project myProject; + private final VirtualFile myVirtualFile; + @NotNull private final JsonSchemaService myService; + private static final Icon EMPTY_ICON = JBUI.scale(EmptyIcon.create(AllIcons.General.Add.getIconWidth())); + + JsonSchemaInfoPopupStep(@NotNull List<JsonSchemaInfo> allSchemas, @NotNull Project project, @NotNull VirtualFile virtualFile, + @NotNull JsonSchemaService service) { + super(null, allSchemas); + myProject = project; + myVirtualFile = virtualFile; + myService = service; + } + + @NotNull + @Override + public String getTextFor(JsonSchemaInfo value) { + return value.getDescription(); + } + + @Override + public Icon getIconFor(JsonSchemaInfo value) { + if (value == ADD_MAPPING) { + return AllIcons.General.Add; + } + + if (value == EDIT_MAPPINGS) { + return AllIcons.Actions.Edit; + } + + if (value == LOAD_REMOTE) { + return AllIcons.Actions.Refresh; + } + + return EMPTY_ICON; + } + + @Nullable + @Override + public ListSeparator getSeparatorAbove(JsonSchemaInfo value) { + List<JsonSchemaInfo> values = getValues(); + int index = values.indexOf(value); + if (index - 1 >= 0) { + JsonSchemaInfo info = values.get(index - 1); + if (info == EDIT_MAPPINGS || info == ADD_MAPPING) { + return new ListSeparator("Registered schemas"); + } + if (value.getProvider() == null && info.getProvider() != null) { + return new ListSeparator("SchemaStore.org schemas"); + } + } + return null; + } + + @Override + public PopupStep onChosen(JsonSchemaInfo selectedValue, boolean finalChoice) { + if (finalChoice) { + if (selectedValue == EDIT_MAPPINGS || selectedValue == ADD_MAPPING) { + return doFinalStep(() -> runSchemaEditorForCurrentFile()); + } + else if (selectedValue == LOAD_REMOTE) { + return doFinalStep(() -> myService.triggerUpdateRemote()); + } + else { + setMapping(selectedValue, myVirtualFile, myProject); + return doFinalStep(() -> myService.reset()); + } + } + return PopupStep.FINAL_CHOICE; + } + + private void runSchemaEditorForCurrentFile() { + JsonSchemaMappingsConfigurable configurable = new JsonSchemaMappingsConfigurable(myProject); + JsonSchemaMappingsProjectConfiguration mappingsConf = JsonSchemaMappingsProjectConfiguration.getInstance(myProject); + + ShowSettingsUtil.getInstance().editConfigurable(myProject, configurable, () -> { + UserDefinedJsonSchemaConfiguration mappingForFile = mappingsConf.findMappingForFile(myVirtualFile); + if (mappingForFile == null) { + UserDefinedJsonSchemaConfiguration configuration = configurable.addProjectSchema(); + String relativePath = VfsUtilCore.getRelativePath(myVirtualFile, myProject.getBaseDir()); + configuration.patterns.add(new UserDefinedJsonSchemaConfiguration.Item( + relativePath == null ? myVirtualFile.getUrl() : relativePath, false, false)); + mappingForFile = configuration; + } + + configurable.selectInTree(mappingForFile); + }); + } + + @Override + public boolean isSpeedSearchEnabled() { + return true; + } + + private static void setMapping(@Nullable JsonSchemaInfo selectedValue, @NotNull VirtualFile virtualFile, @NotNull Project project) { + JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project); + + VirtualFile projectBaseDir = project.getBaseDir(); + + UserDefinedJsonSchemaConfiguration mappingForFile = configuration.findMappingForFile(virtualFile); + if (mappingForFile != null) { + for (UserDefinedJsonSchemaConfiguration.Item pattern : mappingForFile.patterns) { + if (Objects.equals(VfsUtil.findRelativeFile(projectBaseDir, pattern.getPathParts()), virtualFile) + || virtualFile.getUrl().equals(pattern.getPath())) { + mappingForFile.patterns.remove(pattern); + if (mappingForFile.patterns.size() == 0 && mappingForFile.isApplicationDefined()) { + configuration.removeConfiguration(mappingForFile); + } + else { + mappingForFile.refreshPatterns(); + } + break; + } + } + } + + if (selectedValue == null) return; + + String path = VfsUtilCore.getRelativePath(virtualFile, projectBaseDir); + if (path == null) { + path = virtualFile.getUrl(); + } + + UserDefinedJsonSchemaConfiguration existing = configuration.findMappingBySchemaInfo(selectedValue); + UserDefinedJsonSchemaConfiguration.Item item = new UserDefinedJsonSchemaConfiguration.Item(path, false, false); + if (existing != null) { + if (!existing.patterns.contains(item)) { + existing.patterns.add(item); + existing.refreshPatterns(); + } + } + else { + configuration.addConfiguration(new UserDefinedJsonSchemaConfiguration(selectedValue.getDescription(), + selectedValue.getSchemaVersion(), + selectedValue.getUrl(project), + true, + Collections.singletonList(item))); + } + } +} diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java new file mode 100644 index 00000000..06eff237 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusPopup.java @@ -0,0 +1,82 @@ +// 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 com.jetbrains.jsonSchema.widget; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.ui.popup.ListPopup; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration; +import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import com.jetbrains.jsonSchema.extension.JsonSchemaInfo; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class JsonSchemaStatusPopup { + static final JsonSchemaInfo ADD_MAPPING = new JsonSchemaInfo("") { + @NotNull + @Override + public String getDescription() { + return "New Schema Mappingā¦"; + } + }; + + static final JsonSchemaInfo EDIT_MAPPINGS = new JsonSchemaInfo("") { + @NotNull + @Override + public String getDescription() { + return "Edit Schema Mappingsā¦"; + } + }; + + static final JsonSchemaInfo LOAD_REMOTE = new JsonSchemaInfo("") { + @NotNull + @Override + public String getDescription() { + return "Load SchemaStore Mappings"; + } + }; + + static ListPopup createPopup(@NotNull JsonSchemaService service, + @NotNull Project project, + @NotNull VirtualFile virtualFile, + boolean showOnlyEdit) { + JsonSchemaInfoPopupStep step = createPopupStep(service, project, virtualFile, showOnlyEdit); + return JBPopupFactory.getInstance().createListPopup(step); + } + + @NotNull + static JsonSchemaInfoPopupStep createPopupStep(@NotNull JsonSchemaService service, + @NotNull Project project, + @NotNull VirtualFile virtualFile, + boolean showOnlyEdit) { + List<JsonSchemaInfo> allSchemas; + JsonSchemaMappingsProjectConfiguration configuration = JsonSchemaMappingsProjectConfiguration.getInstance(project); + UserDefinedJsonSchemaConfiguration mapping = configuration.findMappingForFile(virtualFile); + if (!showOnlyEdit || mapping == null) { + List<JsonSchemaInfo> infos = service.getAllUserVisibleSchemas(); + Comparator<JsonSchemaInfo> comparator = Comparator.comparing(JsonSchemaInfo::getDescription, String::compareTo); + Stream<JsonSchemaInfo> registered = infos.stream().filter(i -> i.getProvider() != null).sorted(comparator); + List<JsonSchemaInfo> otherList = ContainerUtil.emptyList(); + + if (JsonSchemaCatalogProjectConfiguration.getInstance(project).isRemoteActivityEnabled()) { + otherList = infos.stream().filter(i -> i.getProvider() == null).sorted(comparator).collect(Collectors.toList()); + if (otherList.size() == 0) { + otherList = ContainerUtil.createMaybeSingletonList(LOAD_REMOTE); + } + } + allSchemas = Stream.concat(registered, otherList.stream()).collect(Collectors.toList()); + allSchemas.add(0, mapping == null ? ADD_MAPPING : EDIT_MAPPINGS); + } + else { + allSchemas = ContainerUtil.createMaybeSingletonList(EDIT_MAPPINGS); + } + return new JsonSchemaInfoPopupStep(allSchemas, project, virtualFile, service); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java new file mode 100644 index 00000000..5ff023e9 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidget.java @@ -0,0 +1,310 @@ +// 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 com.jetbrains.jsonSchema.widget; + +import com.intellij.icons.AllIcons; +import com.intellij.json.JsonLanguage; +import com.intellij.lang.Language; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.project.DumbService; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.ListPopup; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.impl.http.FileDownloadingAdapter; +import com.intellij.openapi.vfs.impl.http.HttpVirtualFile; +import com.intellij.openapi.vfs.impl.http.RemoteFileInfo; +import com.intellij.openapi.wm.StatusBarWidget; +import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup; +import com.jetbrains.jsonSchema.JsonSchemaCatalogProjectConfiguration; +import com.jetbrains.jsonSchema.extension.*; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonSchemaConflictNotificationProvider; +import com.jetbrains.jsonSchema.impl.JsonSchemaServiceImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +class JsonSchemaStatusWidget extends EditorBasedStatusBarPopup { + private static final String JSON_SCHEMA_BAR = "JSON: "; + private static final String JSON_SCHEMA_BAR_OTHER_FILES = "Schema: "; + private static final String JSON_SCHEMA_TOOLTIP = "JSON Schema: "; + private static final String JSON_SCHEMA_TOOLTIP_OTHER_FILES = "Validated by JSON Schema: "; + private final JsonSchemaService myService; + private static final String ID = "JSONSchemaSelector"; + + JsonSchemaStatusWidget(Project project) { + super(project); + myService = JsonSchemaService.Impl.get(project); + myService.registerRemoteUpdateCallback(myUpdateCallback); + myService.registerResetAction(myUpdateCallback); + } + + private final Runnable myUpdateCallback = this::update; + + private static class MyWidgetState extends WidgetState { + boolean warning = false; + MyWidgetState(String toolTip, String text, boolean actionEnabled) { + super(toolTip, text, actionEnabled); + } + + public boolean isWarning() { + return warning; + } + + public void setWarning(boolean warning) { + this.warning = warning; + this.setIcon(warning ? AllIcons.General.Warning : null); + } + } + + private boolean hasAccessToSymbols() { + return !DumbService.getInstance(myProject).isDumb(); + } + + @NotNull + @Override + protected WidgetState getWidgetState(@Nullable VirtualFile file) { + if (file == null) { + return WidgetState.HIDDEN; + } + + JsonSchemaEnabler[] enablers = JsonSchemaEnabler.EXTENSION_POINT_NAME.getExtensions(); + if (Arrays.stream(enablers).noneMatch(e -> e.isEnabledForFile(file) && e.shouldShowSwitcherWidget(file))) { + return WidgetState.HIDDEN; + } + + FileType fileType = file.getFileType(); + Language language = fileType instanceof LanguageFileType ? ((LanguageFileType)fileType).getLanguage() : null; + boolean isJsonFile = language instanceof JsonLanguage; + + if (!hasAccessToSymbols()) { + return WidgetState.getDumbModeState("JSON schema service", isJsonFile ? JSON_SCHEMA_BAR : JSON_SCHEMA_BAR_OTHER_FILES); + } + + JsonWidgetSuppressor[] suppressors = JsonWidgetSuppressor.EXTENSION_POINT_NAME.getExtensions(); + if (Arrays.stream(suppressors).anyMatch(s -> s.suppressSwitcherWidget(file, myProject))) { + return WidgetState.HIDDEN; + } + + Collection<VirtualFile> schemaFiles = myService.getSchemaFilesForFile(file); + if (schemaFiles.size() == 0) { + return getNoSchemaState(); + } + + if (schemaFiles.size() != 1) { + List<VirtualFile> onlyUserSchemas = schemaFiles.stream().filter(s -> { + JsonSchemaFileProvider provider = myService.getSchemaProvider(s); + return provider != null && provider.getSchemaType() == SchemaType.userSchema; + }).collect(Collectors.toList()); + if (onlyUserSchemas.size() > 1) { + MyWidgetState state = new MyWidgetState(JsonSchemaConflictNotificationProvider.createMessage(schemaFiles, myService, + "<br/>", "Conflicting schemas:<br/>", + ""), + schemaFiles.size() + " schemas (!)", true); + state.setWarning(true); + return state; + } + schemaFiles = onlyUserSchemas; + if (schemaFiles.size() == 0) { + return getNoSchemaState(); + } + } + + VirtualFile schemaFile = schemaFiles.iterator().next(); + schemaFile = ((JsonSchemaServiceImpl)myService).replaceHttpFileWithBuiltinIfNeeded(schemaFile); + + String tooltip = isJsonFile ? JSON_SCHEMA_TOOLTIP : JSON_SCHEMA_TOOLTIP_OTHER_FILES; + String bar = isJsonFile ? JSON_SCHEMA_BAR : JSON_SCHEMA_BAR_OTHER_FILES; + + if (schemaFile instanceof HttpVirtualFile) { + RemoteFileInfo info = ((HttpVirtualFile)schemaFile).getFileInfo(); + if (info == null) return getDownloadErrorState(null); + + //noinspection EnumSwitchStatementWhichMissesCases + switch (info.getState()) { + case DOWNLOADING_NOT_STARTED: + addDownloadingUpdateListener(info); + return new MyWidgetState(tooltip + getSchemaFileDesc(schemaFile), bar + getPresentableNameForFile(schemaFile), + true); + case DOWNLOADING_IN_PROGRESS: + addDownloadingUpdateListener(info); + return new MyWidgetState("Download is scheduled or in progress", "Downloading JSON schema", false); + case ERROR_OCCURRED: + return getDownloadErrorState(info.getErrorMessage()); + } + } + + if (!isValidSchemaFile(schemaFile)) { + MyWidgetState state = new MyWidgetState("File is not a schema", "JSON schema error", true); + state.setWarning(true); + return state; + } + + JsonSchemaFileProvider provider = myService.getSchemaProvider(schemaFile); + if (provider != null) { + final boolean preferRemoteSchemas = JsonSchemaCatalogProjectConfiguration.getInstance(myProject).isPreferRemoteSchemas(); + final String remoteSource = provider.getRemoteSource(); + String providerName = preferRemoteSchemas && remoteSource != null && !remoteSource.endsWith("!") ? remoteSource : provider.getPresentableName(); + String shortName = StringUtil.trimEnd(StringUtil.trimEnd(providerName, ".json"), "-schema"); + String name = preferRemoteSchemas && remoteSource != null && !remoteSource.endsWith("!") ? bar + new JsonSchemaInfo(remoteSource).getDescription() + : (shortName.startsWith("JSON schema") ? shortName : (bar + shortName)); + String kind = !preferRemoteSchemas && (provider.getSchemaType() == SchemaType.embeddedSchema || provider.getSchemaType() == SchemaType.schema) + ? " (bundled)" + : ""; + return new MyWidgetState(tooltip + providerName + kind, name, true); + } + + return new MyWidgetState(tooltip + getSchemaFileDesc(schemaFile), bar + getPresentableNameForFile(schemaFile), + true); + } + + private void addDownloadingUpdateListener(@NotNull RemoteFileInfo info) { + info.addDownloadingListener(new FileDownloadingAdapter() { + @Override + public void fileDownloaded(@NotNull VirtualFile localFile) { + update(); + } + + @Override + public void errorOccurred(@NotNull String errorMessage) { + update(); + } + + @Override + public void downloadingCancelled() { + update(); + } + }); + } + + private boolean isValidSchemaFile(@Nullable VirtualFile schemaFile) { + return schemaFile != null && myService.isSchemaFile(schemaFile) && myService.isApplicableToFile(schemaFile); + } + + @Nullable + private static String extractNpmPackageName(@Nullable String path) { + if (path == null) return null; + int idx = path.indexOf("node_modules"); + if (idx != -1) { + int trimIndex = idx + "node_modules".length() + 1; + if (trimIndex < path.length()) { + path = path.substring(trimIndex); + idx = StringUtil.indexOfAny(path, "\\/"); + if (idx != -1) { + if (path.startsWith("@")) { + idx = StringUtil.indexOfAny(path, "\\/", idx + 1, path.length()); + } + } + + if (idx != -1) { + return path.substring(0, idx); + } + } + } + return null; + } + + @NotNull + private static String getPresentableNameForFile(@NotNull VirtualFile schemaFile) { + if (schemaFile instanceof HttpVirtualFile) { + return new JsonSchemaInfo(schemaFile.getUrl()).getDescription(); + } + + String nameWithoutExtension = schemaFile.getNameWithoutExtension(); + if (!JsonSchemaInfo.isVeryDumbName(nameWithoutExtension)) return nameWithoutExtension; + + String path = schemaFile.getPath(); + + String npmPackageName = extractNpmPackageName(path); + return npmPackageName != null ? npmPackageName : schemaFile.getName(); + } + + @NotNull + private static WidgetState getDownloadErrorState(@Nullable String message) { + MyWidgetState state = new MyWidgetState("Error downloading schema" + (message == null ? "" : (": <br/>" + message)), + "JSON schema error", true); + state.setWarning(true); + return state; + } + + @NotNull + private static WidgetState getNoSchemaState() { + return new MyWidgetState("No JSON Schema defined", "No JSON schema", true); + } + + @NotNull + private static String getSchemaFileDesc(@NotNull VirtualFile schemaFile) { + if (schemaFile instanceof HttpVirtualFile) { + return schemaFile.getPresentableUrl(); + } + + String npmPackageName = extractNpmPackageName(schemaFile.getPath()); + return schemaFile.getName() + (npmPackageName == null ? "" : (" (Package: " + npmPackageName + ")")); + } + + @Nullable + @Override + protected ListPopup createPopup(DataContext context) { + final VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(context); + if (virtualFile == null) return null; + + Project project = getProject(); + if (project == null) return null; + WidgetState state = getWidgetState(virtualFile); + if (!(state instanceof MyWidgetState)) return null; + return doCreatePopup(virtualFile, project, ((MyWidgetState)state).isWarning()); + } + + @NotNull + private ListPopup doCreatePopup(@NotNull VirtualFile virtualFile, @NotNull Project project, boolean showOnlyEdit) { + return JsonSchemaStatusPopup.createPopup(myService, project, virtualFile, showOnlyEdit); + } + + @Override + protected void registerCustomListeners() { + class Listener implements DumbService.DumbModeListener { + volatile boolean isDumbMode; + + @Override + public void enteredDumbMode() { + isDumbMode = true; + update(); + } + + @Override + public void exitDumbMode() { + isDumbMode = false; + update(); + } + } + + Listener listener = new Listener(); + myConnection.subscribe(DumbService.DUMB_MODE, listener); + } + + @NotNull + @Override + protected StatusBarWidget createInstance(Project project) { + return new JsonSchemaStatusWidget(project); + } + + @NotNull + @Override + public String ID() { + return ID; + } + + @Override + public void dispose() { + myService.unregisterRemoteUpdateCallback(myUpdateCallback); + myService.unregisterResetAction(myUpdateCallback); + super.dispose(); + } +} diff --git a/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java new file mode 100644 index 00000000..f688cbe9 --- /dev/null +++ b/json/src/com/jetbrains/jsonSchema/widget/JsonSchemaStatusWidgetProvider.java @@ -0,0 +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 com.jetbrains.jsonSchema.widget; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.StatusBarWidget; +import com.intellij.openapi.wm.StatusBarWidgetProvider; +import org.jetbrains.annotations.NotNull; + +public class JsonSchemaStatusWidgetProvider implements StatusBarWidgetProvider { + + @NotNull + @Override + public StatusBarWidget getWidget(@NotNull Project project) { + return new JsonSchemaStatusWidget(project); + } +} diff --git a/json/src/jsonSchema/build.xml b/json/src/jsonSchema/build.xml new file mode 100644 index 00000000..dbf3e4af --- /dev/null +++ b/json/src/jsonSchema/build.xml @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<project name="JSON Schemas Update" default="ALL"> + <property name="idea.project.home" value="${basedir}/../../../../"/> + + <macrodef name="get_schema"> + <attribute name="fromUrl" /> + <attribute name="toFile" /> + + <sequential> + <get src="@{fromUrl}" + dest="@{toFile}" /> + <local name="baseUrl"/> + <fixcrlf file="@{toFile}"/> + <property name="baseUrl" value="@{fromUrl}" /> + <script language="javascript"><![CDATA[ + var url = new java.net.URL(project.getProperty("baseUrl")); + var connection = url.openConnection(); + var etag = connection.getHeaderField("ETag"); + project.setProperty("etag", etag); + ]]></script> + <echo message="${etag}" file="@{toFile}.ETAG" /> + </sequential> + </macrodef> + + <target name="prettier"> + <get_schema fromUrl="http://json.schemastore.org/prettierrc" + toFile="${idea.project.home}/contrib/prettierJS/resources/prettierrc-schema.json" /> + <!-- Add schema ID manually --> + <replaceregexp file="${idea.project.home}/contrib/prettierJS/resources/prettierrc-schema.json" + match=""definitions"" replace=""id": "http://json.schemastore.org/prettierrc",${line.separator} "definitions"" /> + </target> + + <target name="tslint"> + <get_schema fromUrl="http://json.schemastore.org/tslint" + toFile="${idea.project.home}/contrib/tslint/resources/tslintJsonSchema/tslint-schema.json" /> + </target> + + <target name="webpack4"> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/WebpackOptions.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpack-schema4.json" /> + </target> + + <target name="webpack4plugins"> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/BannerPlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/BannerPlugin.json" /> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/DllPlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/DllPlugin.json" /> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/DllReferencePlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/DllReferencePlugin.json" /> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/HashedModuleIdsPlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/HashedModuleIdsPlugin.json" /> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/LoaderOptionsPlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/LoaderOptionsPlugin.json" /> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/SourceMapDevToolPlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/SourceMapDevToolPlugin.json" /> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/WatchIgnorePlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/WatchIgnorePlugin.json" /> + + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/debug/ProfilingPlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/debug/ProfilingPlugin.json" /> + + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/AggressiveSplittingPlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/AggressiveSplittingPlugin.json" /> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/LimitChunkCountPlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/LimitChunkCountPlugin.json" /> + <get_schema fromUrl="https://raw.githubusercontent.com/webpack/webpack/master/schemas/plugins/optimize/MinChunkSizePlugin.json" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/webpackPlugins/optimize/MinChunkSizePlugin.json" /> + + </target> + + <target name="nodejs"> + <get_schema fromUrl="http://json.schemastore.org/package" + toFile="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" /> + + <!-- add custom properties for eslint, prettier, styleling, jest, jshint, hscs --> + <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" + match=""publishConfig"" + replace=""eslintConfig" : {"$ref": "http://json.schemastore.org/eslintrc#"},${line.separator} "publishConfig"" /> + <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" + match=""publishConfig"" + replace=""prettier" : {"$ref": "http://json.schemastore.org/prettierrc#"},${line.separator} "publishConfig"" /> + <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" + match=""publishConfig"" + replace=""stylelint" : {"$ref": "http://json.schemastore.org/stylelintrc#"},${line.separator} "publishConfig"" /> + <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" + match=""publishConfig"" + replace=""jest" : {"type": "object", "$ref": "https://facebook.github.io/jest/docs/configuration.html!#"},${line.separator} "publishConfig"" /> + <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" + match=""publishConfig"" + replace=""jshintConfig" : {"$ref": "http://json.schemastore.org/jshintrc#"},${line.separator} "publishConfig"" /> + <replaceregexp file="${idea.project.home}/plugins/NodeJS/src/com/jetbrains/nodejs/packageJson/packageJsonSchema.json" + match=""publishConfig"" + replace=""jscsConfig" : {"$ref": "http://json.schemastore.org/jscsrc#"},${line.separator} "publishConfig"" /> + </target> + + <target name="js_schemas"> + <get_schema fromUrl="http://json.schemastore.org/babelrc" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.babelrc-schema.json" /> + <get_schema fromUrl="http://json.schemastore.org/stylelintrc" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.stylelintrc-schema.json" /> + <replaceregexp file="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.stylelintrc-schema.json" + match=""definitions"" replace=""id": "http://json.schemastore.org/stylelintrc",${line.separator}	"definitions"" /> + <get_schema fromUrl="http://json.schemastore.org/jsconfig" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/jsconfig-schema.json" /> + <get_schema fromUrl="http://json.schemastore.org/tsconfig" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/tsconfig-schema.json" /> + <get_schema fromUrl="http://json.schemastore.org/tsd" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/tsd-schema.json" /> + <get_schema fromUrl="http://json.schemastore.org/typings" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/typings-schema.json" /> + + <get_schema fromUrl="http://json.schemastore.org/eslintrc" + toFile="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.eslintrc-schema.json" /> + <replaceregexp file="${idea.project.home}/plugins/JavaScriptLanguage/src/jsonSchemas/.eslintrc-schema.json" + match=""definitions"" replace=""id": "http://json.schemastore.org/eslintrc",${line.separator} "definitions"" /> + </target> + + <target name="ALL"> + <antcall target="js_schemas" /> + <antcall target="prettier" /> + <antcall target="tslint" /> + <antcall target="nodejs" /> + <antcall target="webpack4" /> + <antcall target="webpack4plugins" /> + </target> +</project>
\ No newline at end of file diff --git a/json/src/jsonSchema/schema.json b/json/src/jsonSchema/schema.json new file mode 100644 index 00000000..048c444f --- /dev/null +++ b/json/src/jsonSchema/schema.json @@ -0,0 +1,154 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + }, + "simpleTypes": { + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "x-intellij-html-description": { + "type": "string", + "description": "Description in html format" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +}
\ No newline at end of file diff --git a/json/src/jsonSchema/schema06.json b/json/src/jsonSchema/schema06.json new file mode 100644 index 00000000..5877ae97 --- /dev/null +++ b/json/src/jsonSchema/schema06.json @@ -0,0 +1,158 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$id": "http://json-schema.org/draft-06/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "x-intellij-html-description": { + "type": "string", + "description": "Description in html format" + }, + "default": {}, + "examples": { + "type": "array", + "items": {} + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": {}, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": {} +} diff --git a/json/src/jsonSchema/schema07.json b/json/src/jsonSchema/schema07.json new file mode 100644 index 00000000..539c6069 --- /dev/null +++ b/json/src/jsonSchema/schema07.json @@ -0,0 +1,172 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "x-intellij-html-description": { + "type": "string", + "description": "Description in html format" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": {"$ref": "#"}, + "then": {"$ref": "#"}, + "else": {"$ref": "#"}, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} |