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 |
New upstream version 0~183.5153.4+dfsg
Diffstat (limited to 'json')
263 files changed, 26879 insertions, 0 deletions
diff --git a/json/gen/com/intellij/json/JsonElementTypes.java b/json/gen/com/intellij/json/JsonElementTypes.java new file mode 100644 index 00000000..be5505cf --- /dev/null +++ b/json/gen/com/intellij/json/JsonElementTypes.java @@ -0,0 +1,68 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json; + +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.PsiElement; +import com.intellij.lang.ASTNode; +import com.intellij.json.psi.impl.*; + +public interface JsonElementTypes { + + IElementType ARRAY = new JsonElementType("ARRAY"); + IElementType BOOLEAN_LITERAL = new JsonElementType("BOOLEAN_LITERAL"); + IElementType LITERAL = new JsonElementType("LITERAL"); + IElementType NULL_LITERAL = new JsonElementType("NULL_LITERAL"); + IElementType NUMBER_LITERAL = new JsonElementType("NUMBER_LITERAL"); + IElementType OBJECT = new JsonElementType("OBJECT"); + IElementType PROPERTY = new JsonElementType("PROPERTY"); + IElementType REFERENCE_EXPRESSION = new JsonElementType("REFERENCE_EXPRESSION"); + IElementType STRING_LITERAL = new JsonElementType("STRING_LITERAL"); + IElementType VALUE = new JsonElementType("VALUE"); + + IElementType BLOCK_COMMENT = new JsonTokenType("BLOCK_COMMENT"); + IElementType COLON = new JsonTokenType(":"); + IElementType COMMA = new JsonTokenType(","); + IElementType DOUBLE_QUOTED_STRING = new JsonTokenType("DOUBLE_QUOTED_STRING"); + IElementType FALSE = new JsonTokenType("false"); + IElementType IDENTIFIER = new JsonTokenType("IDENTIFIER"); + IElementType LINE_COMMENT = new JsonTokenType("LINE_COMMENT"); + IElementType L_BRACKET = new JsonTokenType("["); + IElementType L_CURLY = new JsonTokenType("{"); + IElementType NULL = new JsonTokenType("null"); + IElementType NUMBER = new JsonTokenType("NUMBER"); + IElementType R_BRACKET = new JsonTokenType("]"); + IElementType R_CURLY = new JsonTokenType("}"); + IElementType SINGLE_QUOTED_STRING = new JsonTokenType("SINGLE_QUOTED_STRING"); + IElementType TRUE = new JsonTokenType("true"); + + class Factory { + public static PsiElement createElement(ASTNode node) { + IElementType type = node.getElementType(); + if (type == ARRAY) { + return new JsonArrayImpl(node); + } + else if (type == BOOLEAN_LITERAL) { + return new JsonBooleanLiteralImpl(node); + } + else if (type == NULL_LITERAL) { + return new JsonNullLiteralImpl(node); + } + else if (type == NUMBER_LITERAL) { + return new JsonNumberLiteralImpl(node); + } + else if (type == OBJECT) { + return new JsonObjectImpl(node); + } + else if (type == PROPERTY) { + return new JsonPropertyImpl(node); + } + else if (type == REFERENCE_EXPRESSION) { + return new JsonReferenceExpressionImpl(node); + } + else if (type == STRING_LITERAL) { + return new JsonStringLiteralImpl(node); + } + throw new AssertionError("Unknown element type: " + type); + } + } +} diff --git a/json/gen/com/intellij/json/JsonParser.java b/json/gen/com/intellij/json/JsonParser.java new file mode 100644 index 00000000..39dcfc55 --- /dev/null +++ b/json/gen/com/intellij/json/JsonParser.java @@ -0,0 +1,387 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json; + +import com.intellij.lang.PsiBuilder; +import com.intellij.lang.PsiBuilder.Marker; +import static com.intellij.json.JsonElementTypes.*; +import static com.intellij.json.psi.JsonParserUtil.*; +import com.intellij.psi.tree.IElementType; +import com.intellij.lang.ASTNode; +import com.intellij.psi.tree.TokenSet; +import com.intellij.lang.PsiParser; +import com.intellij.lang.LightPsiParser; + +@SuppressWarnings({"SimplifiableIfStatement", "UnusedAssignment"}) +public class JsonParser implements PsiParser, LightPsiParser { + + public ASTNode parse(IElementType t, PsiBuilder b) { + parseLight(t, b); + return b.getTreeBuilt(); + } + + public void parseLight(IElementType t, PsiBuilder b) { + boolean r; + b = adapt_builder_(t, b, this, EXTENDS_SETS_); + Marker m = enter_section_(b, 0, _COLLAPSE_, null); + if (t == ARRAY) { + r = array(b, 0); + } + else if (t == BOOLEAN_LITERAL) { + r = boolean_literal(b, 0); + } + else if (t == LITERAL) { + r = literal(b, 0); + } + else if (t == NULL_LITERAL) { + r = null_literal(b, 0); + } + else if (t == NUMBER_LITERAL) { + r = number_literal(b, 0); + } + else if (t == OBJECT) { + r = object(b, 0); + } + else if (t == PROPERTY) { + r = property(b, 0); + } + else if (t == REFERENCE_EXPRESSION) { + r = reference_expression(b, 0); + } + else if (t == STRING_LITERAL) { + r = string_literal(b, 0); + } + else if (t == VALUE) { + r = value(b, 0); + } + else { + r = parse_root_(t, b, 0); + } + exit_section_(b, 0, m, t, r, true, TRUE_CONDITION); + } + + protected boolean parse_root_(IElementType t, PsiBuilder b, int l) { + return json(b, l + 1); + } + + public static final TokenSet[] EXTENDS_SETS_ = new TokenSet[] { + create_token_set_(ARRAY, BOOLEAN_LITERAL, LITERAL, NULL_LITERAL, + NUMBER_LITERAL, OBJECT, REFERENCE_EXPRESSION, STRING_LITERAL, + VALUE), + }; + + /* ********************************************************** */ + // '[' array_element* ']' + public static boolean array(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "array")) return false; + if (!nextTokenIs(b, L_BRACKET)) return false; + boolean r, p; + Marker m = enter_section_(b, l, _NONE_, ARRAY, null); + r = consumeToken(b, L_BRACKET); + p = r; // pin = 1 + r = r && report_error_(b, array_1(b, l + 1)); + r = p && consumeToken(b, R_BRACKET) && r; + exit_section_(b, l, m, r, p, null); + return r || p; + } + + // array_element* + private static boolean array_1(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "array_1")) return false; + while (true) { + int c = current_position_(b); + if (!array_element(b, l + 1)) break; + if (!empty_element_parsed_guard_(b, "array_1", c)) break; + } + return true; + } + + /* ********************************************************** */ + // value (','|&']') + static boolean array_element(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "array_element")) return false; + boolean r, p; + Marker m = enter_section_(b, l, _NONE_); + r = value(b, l + 1); + p = r; // pin = 1 + r = r && array_element_1(b, l + 1); + exit_section_(b, l, m, r, p, not_bracket_or_next_value_parser_); + return r || p; + } + + // ','|&']' + private static boolean array_element_1(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "array_element_1")) return false; + boolean r; + Marker m = enter_section_(b); + r = consumeToken(b, COMMA); + if (!r) r = array_element_1_1(b, l + 1); + exit_section_(b, m, null, r); + return r; + } + + // &']' + private static boolean array_element_1_1(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "array_element_1_1")) return false; + boolean r; + Marker m = enter_section_(b, l, _AND_); + r = consumeToken(b, R_BRACKET); + exit_section_(b, l, m, r, false, null); + return r; + } + + /* ********************************************************** */ + // TRUE | FALSE + public static boolean boolean_literal(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "boolean_literal")) return false; + if (!nextTokenIs(b, "<boolean literal>", FALSE, TRUE)) return false; + boolean r; + Marker m = enter_section_(b, l, _NONE_, BOOLEAN_LITERAL, "<boolean literal>"); + r = consumeToken(b, TRUE); + if (!r) r = consumeToken(b, FALSE); + exit_section_(b, l, m, r, false, null); + return r; + } + + /* ********************************************************** */ + // value+ + static boolean json(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "json")) return false; + boolean r; + Marker m = enter_section_(b); + r = value(b, l + 1); + while (r) { + int c = current_position_(b); + if (!value(b, l + 1)) break; + if (!empty_element_parsed_guard_(b, "json", c)) break; + } + exit_section_(b, m, null, r); + return r; + } + + /* ********************************************************** */ + // string_literal | number_literal | boolean_literal | null_literal + public static boolean literal(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "literal")) return false; + boolean r; + Marker m = enter_section_(b, l, _COLLAPSE_, LITERAL, "<literal>"); + r = string_literal(b, l + 1); + if (!r) r = number_literal(b, l + 1); + if (!r) r = boolean_literal(b, l + 1); + if (!r) r = null_literal(b, l + 1); + exit_section_(b, l, m, r, false, null); + return r; + } + + /* ********************************************************** */ + // !('}'|value) + static boolean not_brace_or_next_value(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "not_brace_or_next_value")) return false; + boolean r; + Marker m = enter_section_(b, l, _NOT_); + r = !not_brace_or_next_value_0(b, l + 1); + exit_section_(b, l, m, r, false, null); + return r; + } + + // '}'|value + private static boolean not_brace_or_next_value_0(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "not_brace_or_next_value_0")) return false; + boolean r; + Marker m = enter_section_(b); + r = consumeToken(b, R_CURLY); + if (!r) r = value(b, l + 1); + exit_section_(b, m, null, r); + return r; + } + + /* ********************************************************** */ + // !(']'|value) + static boolean not_bracket_or_next_value(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "not_bracket_or_next_value")) return false; + boolean r; + Marker m = enter_section_(b, l, _NOT_); + r = !not_bracket_or_next_value_0(b, l + 1); + exit_section_(b, l, m, r, false, null); + return r; + } + + // ']'|value + private static boolean not_bracket_or_next_value_0(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "not_bracket_or_next_value_0")) return false; + boolean r; + Marker m = enter_section_(b); + r = consumeToken(b, R_BRACKET); + if (!r) r = value(b, l + 1); + exit_section_(b, m, null, r); + return r; + } + + /* ********************************************************** */ + // NULL + public static boolean null_literal(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "null_literal")) return false; + if (!nextTokenIs(b, NULL)) return false; + boolean r; + Marker m = enter_section_(b); + r = consumeToken(b, NULL); + exit_section_(b, m, NULL_LITERAL, r); + return r; + } + + /* ********************************************************** */ + // NUMBER + public static boolean number_literal(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "number_literal")) return false; + if (!nextTokenIs(b, NUMBER)) return false; + boolean r; + Marker m = enter_section_(b); + r = consumeToken(b, NUMBER); + exit_section_(b, m, NUMBER_LITERAL, r); + return r; + } + + /* ********************************************************** */ + // '{' object_element* '}' + public static boolean object(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "object")) return false; + if (!nextTokenIs(b, L_CURLY)) return false; + boolean r, p; + Marker m = enter_section_(b, l, _NONE_, OBJECT, null); + r = consumeToken(b, L_CURLY); + p = r; // pin = 1 + r = r && report_error_(b, object_1(b, l + 1)); + r = p && consumeToken(b, R_CURLY) && r; + exit_section_(b, l, m, r, p, null); + return r || p; + } + + // object_element* + private static boolean object_1(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "object_1")) return false; + while (true) { + int c = current_position_(b); + if (!object_element(b, l + 1)) break; + if (!empty_element_parsed_guard_(b, "object_1", c)) break; + } + return true; + } + + /* ********************************************************** */ + // property (','|&'}') + static boolean object_element(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "object_element")) return false; + boolean r, p; + Marker m = enter_section_(b, l, _NONE_); + r = property(b, l + 1); + p = r; // pin = 1 + r = r && object_element_1(b, l + 1); + exit_section_(b, l, m, r, p, not_brace_or_next_value_parser_); + return r || p; + } + + // ','|&'}' + private static boolean object_element_1(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "object_element_1")) return false; + boolean r; + Marker m = enter_section_(b); + r = consumeToken(b, COMMA); + if (!r) r = object_element_1_1(b, l + 1); + exit_section_(b, m, null, r); + return r; + } + + // &'}' + private static boolean object_element_1_1(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "object_element_1_1")) return false; + boolean r; + Marker m = enter_section_(b, l, _AND_); + r = consumeToken(b, R_CURLY); + exit_section_(b, l, m, r, false, null); + return r; + } + + /* ********************************************************** */ + // property_name (':' value) + public static boolean property(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "property")) return false; + boolean r, p; + Marker m = enter_section_(b, l, _NONE_, PROPERTY, "<property>"); + r = property_name(b, l + 1); + p = r; // pin = 1 + r = r && property_1(b, l + 1); + exit_section_(b, l, m, r, p, null); + return r || p; + } + + // ':' value + private static boolean property_1(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "property_1")) return false; + boolean r, p; + Marker m = enter_section_(b, l, _NONE_); + r = consumeToken(b, COLON); + p = r; // pin = 1 + r = r && value(b, l + 1); + exit_section_(b, l, m, r, p, null); + return r || p; + } + + /* ********************************************************** */ + // literal | reference_expression + static boolean property_name(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "property_name")) return false; + boolean r; + r = literal(b, l + 1); + if (!r) r = reference_expression(b, l + 1); + return r; + } + + /* ********************************************************** */ + // IDENTIFIER + public static boolean reference_expression(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "reference_expression")) return false; + if (!nextTokenIs(b, IDENTIFIER)) return false; + boolean r; + Marker m = enter_section_(b); + r = consumeToken(b, IDENTIFIER); + exit_section_(b, m, REFERENCE_EXPRESSION, r); + return r; + } + + /* ********************************************************** */ + // SINGLE_QUOTED_STRING | DOUBLE_QUOTED_STRING + public static boolean string_literal(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "string_literal")) return false; + if (!nextTokenIs(b, "<string literal>", DOUBLE_QUOTED_STRING, SINGLE_QUOTED_STRING)) return false; + boolean r; + Marker m = enter_section_(b, l, _NONE_, STRING_LITERAL, "<string literal>"); + r = consumeToken(b, SINGLE_QUOTED_STRING); + if (!r) r = consumeToken(b, DOUBLE_QUOTED_STRING); + exit_section_(b, l, m, r, false, null); + return r; + } + + /* ********************************************************** */ + // object | array | literal | reference_expression + public static boolean value(PsiBuilder b, int l) { + if (!recursion_guard_(b, l, "value")) return false; + boolean r; + Marker m = enter_section_(b, l, _COLLAPSE_, VALUE, "<value>"); + r = object(b, l + 1); + if (!r) r = array(b, l + 1); + if (!r) r = literal(b, l + 1); + if (!r) r = reference_expression(b, l + 1); + exit_section_(b, l, m, r, false, null); + return r; + } + + final static Parser not_brace_or_next_value_parser_ = new Parser() { + public boolean parse(PsiBuilder b, int l) { + return not_brace_or_next_value(b, l + 1); + } + }; + final static Parser not_bracket_or_next_value_parser_ = new Parser() { + public boolean parse(PsiBuilder b, int l) { + return not_bracket_or_next_value(b, l + 1); + } + }; +} diff --git a/json/gen/com/intellij/json/_JsonLexer.java b/json/gen/com/intellij/json/_JsonLexer.java new file mode 100644 index 00000000..1f5df96f --- /dev/null +++ b/json/gen/com/intellij/json/_JsonLexer.java @@ -0,0 +1,708 @@ +/* The following code was generated by JFlex 1.7.0 tweaked for IntelliJ platform */ + +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.*; + + +/** + * This class is a scanner generated by + * <a href="http://www.jflex.de/">JFlex</a> 1.7.0 + * from the specification file <tt>_JsonLexer.flex</tt> + */ +public class _JsonLexer implements FlexLexer { + + /** This character denotes the end of file */ + public static final int YYEOF = -1; + + /** initial size of the lookahead buffer */ + private static final int ZZ_BUFFERSIZE = 16384; + + /** lexical states */ + public static final int YYINITIAL = 0; + + /** + * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l + * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l + * at the beginning of a line + * l is of the form l = 2*k, k a non negative integer + */ + private static final int ZZ_LEXSTATE[] = { + 0, 0 + }; + + /** + * Translates characters to character classes + * Chosen bits are [12, 6, 3] + * Total runtime size is 13376 bytes + */ + public static int ZZ_CMAP(int ch) { + return ZZ_CMAP_A[(ZZ_CMAP_Y[(ZZ_CMAP_Z[ch>>9]<<6)|((ch>>3)&0x3f)]<<3)|(ch&0x7)]; + } + + /* The ZZ_CMAP_Z table has 2176 entries */ + static final char ZZ_CMAP_Z[] = zzUnpackCMap( + "\1\0\1\1\1\2\1\3\1\4\1\5\1\6\1\7\1\10\1\11\1\12\1\13\1\14\1\15\1\16\1\17\1"+ + "\20\5\21\1\22\1\23\1\24\1\21\14\25\1\26\50\25\1\27\2\25\1\30\1\31\1\32\1\33"+ + "\25\25\1\34\20\21\1\35\1\36\1\37\1\40\1\41\1\42\1\43\1\21\1\44\1\45\1\46\1"+ + "\21\1\47\2\21\1\50\4\21\1\25\1\51\1\52\5\21\2\25\1\53\31\21\1\25\1\54\1\21"+ + "\1\55\40\21\1\56\17\21\1\57\1\60\1\61\1\62\13\21\1\63\10\21\123\25\1\64\7"+ + "\25\1\65\1\66\37\21\1\25\1\66\u0582\21\1\67\u017f\21"); + + /* The ZZ_CMAP_Y table has 3584 entries */ + static final char ZZ_CMAP_Y[] = zzUnpackCMap( + "\1\0\1\1\1\0\1\2\1\3\1\4\1\5\1\6\1\7\1\10\1\0\1\11\1\12\1\13\1\14\1\15\1\16"+ + "\3\0\1\17\1\20\1\21\1\22\2\0\1\23\3\0\1\23\71\0\1\24\1\0\1\25\1\26\1\27\1"+ + "\30\2\26\16\0\1\31\1\32\1\33\1\34\2\0\1\35\11\0\1\36\21\0\1\35\1\37\23\0\1"+ + "\26\1\40\3\0\1\23\1\41\1\40\4\0\1\42\1\40\4\0\1\36\1\43\1\26\3\0\2\44\1\26"+ + "\1\27\1\45\1\0\1\44\11\0\1\24\14\0\1\46\1\36\1\0\1\47\1\0\1\50\1\26\1\42\7"+ + "\0\1\51\14\0\1\25\1\26\6\0\1\52\1\22\5\0\1\52\2\26\3\0\1\2\10\26\1\47\1\27"+ + "\6\26\1\53\2\0\1\23\14\0\1\54\1\0\2\40\1\55\1\50\1\56\2\0\1\47\1\57\1\60\1"+ + "\50\1\61\1\42\1\62\1\54\1\0\1\2\1\45\1\55\1\63\1\56\2\0\1\47\1\64\1\65\1\63"+ + "\1\66\1\41\1\67\1\70\1\0\1\52\1\26\1\55\1\36\1\35\2\0\1\47\1\71\1\60\1\36"+ + "\1\72\1\73\1\26\1\54\1\0\1\41\1\26\1\55\1\50\1\56\2\0\1\47\1\71\1\60\1\50"+ + "\1\66\1\70\1\62\1\54\1\0\1\41\1\26\1\74\1\75\1\76\1\77\1\100\1\75\1\0\1\24"+ + "\1\75\1\76\1\101\1\26\1\70\1\0\1\26\1\41\1\55\1\31\1\47\2\0\1\47\1\46\1\102"+ + "\1\31\1\76\1\103\1\25\1\54\1\0\2\26\1\74\1\31\1\47\2\0\1\47\1\46\1\60\1\31"+ + "\1\76\1\103\1\33\1\54\1\0\1\104\1\26\1\74\1\31\1\47\4\0\1\51\1\31\1\105\1"+ + "\42\1\26\1\54\1\0\1\26\1\37\1\74\1\0\1\23\1\37\2\0\1\35\1\106\1\23\1\107\1"+ + "\110\1\0\2\26\1\111\1\26\1\40\6\0\1\63\1\0\1\23\1\0\1\25\4\26\1\112\1\113"+ + "\1\53\1\40\1\114\1\74\1\0\1\72\1\110\1\52\1\0\1\60\4\26\1\73\2\26\1\25\1\0"+ + "\1\25\1\115\1\116\1\0\1\40\3\0\1\27\1\40\1\0\1\31\2\0\1\40\3\0\1\27\1\33\7"+ + "\26\11\0\1\25\11\0\1\52\4\0\1\36\1\21\5\0\1\117\51\0\1\76\1\23\1\76\5\0\1"+ + "\76\4\0\1\76\1\23\1\76\1\0\1\23\7\0\1\76\10\0\1\51\4\26\2\0\2\26\12\0\1\27"+ + "\1\26\1\40\114\0\1\50\2\0\1\120\2\0\1\44\11\0\1\75\1\73\1\26\1\0\1\31\1\27"+ + "\1\26\2\0\1\27\1\26\2\0\1\2\1\26\1\0\1\31\1\121\1\26\12\0\1\122\1\123\1\0"+ + "\1\25\3\26\1\123\1\0\1\25\13\0\1\26\5\0\1\44\10\0\1\52\1\26\3\0\1\27\1\0\1"+ + "\2\1\0\1\2\1\70\4\0\1\52\1\27\1\26\5\0\1\2\3\0\1\25\1\0\1\25\4\26\3\0\1\2"+ + "\7\0\1\23\3\0\1\50\1\0\1\25\1\0\1\25\1\42\13\26\11\0\1\2\1\0\1\25\1\26\1\124"+ + "\1\2\1\26\16\0\1\2\1\26\7\0\1\26\1\0\1\102\5\0\1\52\12\26\1\117\3\0\1\23\1"+ + "\26\34\0\1\23\2\26\1\53\42\0\2\52\4\0\2\52\1\0\1\125\3\0\1\52\6\0\1\31\1\110"+ + "\1\126\1\27\1\54\1\2\1\0\1\27\1\126\1\27\1\127\1\130\3\26\1\131\1\26\1\42"+ + "\1\73\1\26\1\132\1\133\1\27\1\37\1\41\1\42\2\26\1\0\1\27\3\0\1\44\2\26\1\0"+ + "\1\27\1\134\1\0\1\73\1\26\1\107\1\37\1\106\1\135\1\30\1\136\1\0\1\60\1\137"+ + "\1\140\2\26\5\0\1\73\116\26\5\0\1\23\5\0\1\23\20\0\1\27\1\124\1\2\1\26\4\0"+ + "\1\36\1\21\7\0\1\42\1\26\1\42\2\0\1\23\1\26\10\23\4\0\5\26\1\42\72\26\1\141"+ + "\3\26\1\40\1\0\1\135\1\27\1\40\11\0\1\23\1\142\1\40\12\0\1\117\1\137\4\0\1"+ + "\52\1\40\12\0\1\23\2\26\3\0\1\44\6\26\170\0\1\52\11\26\71\0\1\27\6\26\21\0"+ + "\1\27\10\26\5\0\1\52\41\0\1\27\3\0\1\2\2\26\6\0\1\53\1\36\3\0\1\42\12\0\1"+ + "\25\3\26\1\42\1\0\1\37\14\0\1\61\1\2\1\26\1\0\1\44\11\26\6\0\2\26\1\73\6\0"+ + "\1\2\1\26\10\0\1\27\1\26\1\0\1\25\3\0\1\45\5\0\1\52\4\0\1\2\1\26\3\0\1\27"+ + "\10\0\1\73\1\42\1\0\1\25\4\26\6\0\1\23\1\26\1\0\1\52\1\0\1\25\2\0\1\23\1\111"+ + "\10\0\1\44\2\26\1\123\2\0\1\143\1\26\3\144\1\26\2\23\22\26\5\0\1\145\1\0\1"+ + "\25\64\0\1\2\1\26\2\0\1\23\1\124\5\0\1\2\40\26\55\0\1\52\15\0\1\25\4\26\1"+ + "\23\1\26\1\124\1\137\1\0\1\47\1\23\1\110\1\146\15\0\1\25\3\26\1\124\54\0\1"+ + "\52\2\26\10\0\1\37\6\0\5\26\1\0\1\27\2\0\2\26\1\23\1\26\1\100\2\26\1\137\3"+ + "\26\1\41\1\31\20\0\1\50\1\132\1\26\1\0\1\25\1\40\2\0\1\63\1\40\2\0\1\44\1"+ + "\70\12\0\1\23\3\37\1\147\1\150\2\26\1\151\1\0\1\46\2\0\1\23\2\0\1\152\1\0"+ + "\1\52\1\0\1\52\4\26\17\0\1\44\10\26\6\0\1\27\20\26\1\21\20\26\3\0\1\27\6\0"+ + "\1\73\5\26\3\0\1\23\2\26\3\0\1\44\6\26\3\0\1\52\4\0\1\2\1\0\1\135\5\26\23"+ + "\0\1\52\1\0\1\25\52\26\1\52\1\47\4\0\1\36\1\153\2\0\1\52\25\26\2\0\1\52\1"+ + "\26\3\0\1\25\10\26\7\0\1\70\10\26\1\154\1\53\1\46\1\40\2\0\1\2\1\63\4\26\3"+ + "\0\1\27\20\26\6\0\1\52\1\26\2\0\1\52\1\26\2\0\1\44\21\26\11\0\1\73\66\26\10"+ + "\0\1\23\3\26\1\70\1\0\2\26\7\0\1\155\2\26\3\0\1\73\1\0\1\25\6\0\1\31\1\0\10"+ + "\26\10\0\1\27\1\26\1\0\1\25\24\26\7\0\1\26\1\0\1\25\46\26\55\0\1\23\22\26"+ + "\14\0\1\44\63\26\5\0\1\23\72\26\7\0\1\73\130\26\10\0\1\27\1\26\5\0\1\23\1"+ + "\26\1\42\2\0\14\26\1\25\153\26\1\137\1\102\2\0\1\51\1\2\3\26\1\32\22\26\1"+ + "\147\67\26\12\0\1\31\10\0\1\31\1\156\1\157\1\0\1\160\1\46\7\0\1\36\1\51\2"+ + "\31\3\0\1\161\1\110\1\37\1\47\51\0\1\52\3\0\1\47\2\0\1\117\3\0\1\117\2\0\1"+ + "\31\3\0\1\31\2\0\1\23\3\0\1\23\3\0\1\47\3\0\1\47\2\0\1\117\1\54\6\0\1\46\3"+ + "\0\1\112\1\40\1\117\1\162\1\107\1\163\1\112\1\125\1\112\2\117\1\67\1\0\1\35"+ + "\1\0\1\2\1\55\1\35\1\0\1\2\50\26\32\0\1\23\5\26\106\0\1\27\1\26\33\0\1\52"+ + "\74\26\1\41\3\26\14\0\20\26\36\0\2\26"); + + /* The ZZ_CMAP_A table has 928 entries */ + static final char ZZ_CMAP_A[] = zzUnpackCMap( + "\11\30\1\3\1\2\2\1\1\2\6\30\4\0\1\3\1\30\1\7\1\0\1\30\2\0\1\11\2\30\1\6\1"+ + "\17\1\35\1\12\1\15\1\4\1\13\11\14\1\36\1\0\3\30\1\0\5\30\1\16\3\30\1\20\4"+ + "\30\1\26\4\30\1\33\1\10\1\34\2\30\1\0\1\27\3\30\1\41\1\22\2\30\1\23\2\30\1"+ + "\42\1\30\1\21\3\30\1\37\1\43\1\24\1\40\3\30\1\25\1\30\1\31\1\0\1\32\7\30\1"+ + "\5\2\30\1\3\1\0\4\30\4\0\1\30\2\0\1\30\7\0\1\30\4\0\1\30\5\0\7\30\1\0\2\30"+ + "\4\0\4\30\16\0\5\30\7\0\1\30\1\0\1\30\1\0\5\30\1\0\2\30\2\0\4\30\10\0\1\30"+ + "\1\0\3\30\1\0\1\30\1\0\4\30\1\0\13\30\1\0\1\30\2\0\6\30\1\0\7\30\1\0\1\30"+ + "\15\0\1\30\1\0\2\30\1\0\2\30\1\0\4\30\10\0\1\30\4\0\4\30\1\0\4\30\1\0\13\30"+ + "\2\0\4\30\2\0\11\30\6\0\10\30\2\0\2\30\1\0\3\30\1\0\4\30\2\0\6\30\1\0\1\30"+ + "\3\0\4\30\2\0\5\30\2\0\4\30\5\0\2\30\1\0\4\30\4\0\2\30\1\0\2\30\1\0\2\30\1"+ + "\0\2\30\2\0\1\30\1\0\3\30\2\0\3\30\3\0\4\30\1\0\1\30\7\0\3\30\1\0\2\30\1\0"+ + "\5\30\1\0\3\30\2\0\1\30\11\0\2\30\1\0\6\30\3\0\3\30\1\0\4\30\3\0\2\30\1\0"+ + "\1\30\1\0\2\30\3\0\2\30\3\0\1\30\6\0\3\30\3\0\3\30\5\0\2\30\2\0\2\30\5\0\1"+ + "\30\1\0\5\30\1\0\4\30\1\0\1\30\4\0\1\30\4\0\6\30\1\0\1\30\3\0\2\30\5\0\2\30"+ + "\1\0\1\30\2\0\2\30\1\0\1\30\2\0\1\30\3\0\3\30\1\0\1\30\1\0\1\30\5\0\1\30\1"+ + "\0\1\30\1\0\1\30\4\0\5\30\1\0\4\30\1\3\10\30\1\0\2\30\4\0\4\30\3\0\1\30\3"+ + "\0\3\30\5\0\5\30\1\0\1\30\1\0\1\30\1\0\1\30\1\0\1\30\2\0\3\30\1\0\2\30\13"+ + "\3\5\30\2\1\5\30\1\3\4\0\1\30\12\0\1\3\1\0\1\30\3\0\3\30\1\0\5\30\2\0\1\30"+ + "\1\0\4\30\1\0\1\30\5\0\5\30\4\0\1\30\1\0\1\3\4\0\3\30\1\0\2\30\2\0\3\30\2"+ + "\0\5\30\2\0\6\30\1\0\3\30\1\0\2\30\2\0\2\30\1\0\2\30\1\0\2\30\2\0\3\30\3\0"+ + "\2\30\3\0\2\30\2\0\3\30\4\0\3\30\1\0\2\30\1\0\2\30\3\0\1\30\2\0\5\30\1\0\2"+ + "\30\1\0\3\30\2\0\1\30\4\0\1\30\2\0\2\30\2\0\4\30\1\0\4\30\1\0\1\30\1\0\5\30"+ + "\1\0\4\30\2\0\1\30\1\0\1\30\5\0\1\30\1\0\1\30\1\0\3\30"); + + /** + * Translates DFA states to action switch labels. + */ + private static final int [] ZZ_ACTION = zzUnpackAction(); + + private static final String ZZ_ACTION_PACKED_0 = + "\1\0\1\1\1\2\1\3\1\2\1\3\1\4\1\5"+ + "\1\3\2\6\5\3\1\7\1\10\1\11\1\12\1\13"+ + "\1\14\1\15\1\16\1\4\2\0\1\5\1\3\1\6"+ + "\5\3\1\15\1\16\1\3\3\6\4\3\1\6\1\0"+ + "\1\16\1\3\1\17\1\3\1\20\1\16\1\3\1\21"+ + "\2\3"; + + private static int [] zzUnpackAction() { + int [] result = new int[57]; + int offset = 0; + offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAction(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /** + * Translates a state to a row index in the transition table + */ + private static final int [] ZZ_ROWMAP = zzUnpackRowMap(); + + private static final String ZZ_ROWMAP_PACKED_0 = + "\0\0\0\44\0\110\0\154\0\220\0\264\0\330\0\374"+ + "\0\u0120\0\u0144\0\u0168\0\u018c\0\u01b0\0\u01d4\0\u01f8\0\u021c"+ + "\0\44\0\44\0\44\0\44\0\44\0\44\0\u0240\0\u0264"+ + "\0\44\0\u0288\0\u02ac\0\44\0\u02d0\0\u02f4\0\u0318\0\u033c"+ + "\0\u0360\0\u0384\0\u03a8\0\u03cc\0\u03f0\0\u0414\0\u0438\0\u045c"+ + "\0\u0480\0\u04a4\0\u04c8\0\u04ec\0\u0510\0\264\0\u0534\0\264"+ + "\0\u0558\0\264\0\u057c\0\264\0\44\0\u05a0\0\264\0\u05c4"+ + "\0\u05e8"; + + private static int [] zzUnpackRowMap() { + int [] result = new int[57]; + int offset = 0; + offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackRowMap(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int high = packed.charAt(i++) << 16; + result[j++] = high | packed.charAt(i++); + } + return j; + } + + /** + * The transition table of the DFA + */ + private static final int [] ZZ_TRANS = zzUnpackTrans(); + + private static final String ZZ_TRANS_PACKED_0 = + "\1\2\3\3\1\4\1\5\1\6\1\7\1\2\1\10"+ + "\1\11\1\12\1\13\2\6\1\2\1\14\1\15\1\16"+ + "\1\6\1\17\1\6\1\20\2\6\1\21\1\22\1\23"+ + "\1\24\1\25\1\26\5\6\45\0\3\3\1\0\1\3"+ + "\42\0\1\27\1\6\1\30\3\0\5\6\1\0\11\6"+ + "\6\0\5\6\1\0\3\3\1\6\1\5\1\6\3\0"+ + "\5\6\1\0\11\6\6\0\5\6\4\0\3\6\3\0"+ + "\5\6\1\0\11\6\6\0\5\6\2\7\1\0\4\7"+ + "\1\31\1\32\33\7\2\10\1\0\5\10\1\33\1\34"+ + "\32\10\4\0\3\6\3\0\1\6\1\12\1\13\2\6"+ + "\1\0\1\14\10\6\6\0\5\6\4\0\3\6\3\0"+ + "\3\6\1\35\1\36\1\0\11\6\6\0\2\6\1\36"+ + "\2\6\4\0\3\6\3\0\1\6\2\13\1\35\1\36"+ + "\1\0\11\6\6\0\2\6\1\36\2\6\4\0\3\6"+ + "\3\0\5\6\1\0\1\6\1\37\7\6\6\0\5\6"+ + "\4\0\3\6\3\0\5\6\1\0\11\6\6\0\1\6"+ + "\1\40\3\6\4\0\3\6\3\0\5\6\1\0\7\6"+ + "\1\41\1\6\6\0\5\6\4\0\3\6\3\0\5\6"+ + "\1\0\11\6\6\0\1\42\4\6\4\0\3\6\3\0"+ + "\5\6\1\0\7\6\1\43\1\6\6\0\5\6\1\44"+ + "\2\0\1\44\1\27\1\6\1\27\3\44\5\27\1\44"+ + "\11\27\6\44\5\27\4\45\2\30\1\46\3\45\5\30"+ + "\1\45\11\30\6\45\5\30\2\7\1\0\41\7\2\10"+ + "\1\0\41\10\4\0\3\6\3\0\1\6\2\47\2\6"+ + "\1\0\11\6\6\0\5\6\4\0\3\6\3\0\3\50"+ + "\2\6\1\51\11\6\6\0\5\6\4\0\3\6\3\0"+ + "\5\6\1\0\2\6\1\52\6\6\6\0\5\6\4\0"+ + "\3\6\3\0\5\6\1\0\11\6\6\0\3\6\1\53"+ + "\1\6\4\0\3\6\3\0\5\6\1\0\11\6\6\0"+ + "\3\6\1\54\1\6\4\0\3\6\3\0\5\6\1\0"+ + "\11\6\6\0\1\6\1\55\3\6\4\0\3\6\3\0"+ + "\5\6\1\0\6\6\1\56\2\6\6\0\5\6\1\44"+ + "\2\0\2\44\1\0\36\44\6\45\1\57\41\45\1\60"+ + "\1\30\1\46\3\45\5\30\1\45\11\30\6\45\5\30"+ + "\4\0\3\6\3\0\1\6\2\47\1\6\1\36\1\0"+ + "\11\6\6\0\2\6\1\36\2\6\4\0\3\6\3\0"+ + "\1\6\2\50\2\6\1\0\11\6\6\0\5\6\13\0"+ + "\2\51\33\0\3\6\3\0\5\6\1\0\3\6\1\61"+ + "\5\6\6\0\5\6\4\0\3\6\3\0\5\6\1\0"+ + "\11\6\6\0\3\6\1\62\1\6\4\0\3\6\3\0"+ + "\5\6\1\0\11\6\6\0\4\6\1\63\4\0\3\6"+ + "\3\0\5\6\1\0\11\6\6\0\2\6\1\64\2\6"+ + "\4\45\1\65\1\45\1\57\35\45\4\0\3\6\3\0"+ + "\5\6\1\0\1\6\1\66\7\6\6\0\5\6\4\0"+ + "\3\6\3\0\5\6\1\0\11\6\6\0\2\6\1\67"+ + "\2\6\4\0\3\6\3\0\5\6\1\0\3\6\1\70"+ + "\5\6\6\0\5\6\4\0\3\6\3\0\5\6\1\0"+ + "\4\6\1\71\4\6\6\0\5\6\4\0\3\6\3\0"+ + "\5\6\1\0\5\6\1\56\3\6\6\0\5\6"; + + private static int [] zzUnpackTrans() { + int [] result = new int[1548]; + int offset = 0; + offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackTrans(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + value--; + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /* error codes */ + private static final int ZZ_UNKNOWN_ERROR = 0; + private static final int ZZ_NO_MATCH = 1; + private static final int ZZ_PUSHBACK_2BIG = 2; + + /* error messages for the codes above */ + private static final String[] ZZ_ERROR_MSG = { + "Unknown internal scanner error", + "Error: could not match input", + "Error: pushback value was too large" + }; + + /** + * ZZ_ATTRIBUTE[aState] contains the attributes of state <code>aState</code> + */ + private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute(); + + private static final String ZZ_ATTRIBUTE_PACKED_0 = + "\1\0\1\11\16\1\6\11\2\1\1\11\2\0\1\11"+ + "\22\1\1\0\5\1\1\11\4\1"; + + private static int [] zzUnpackAttribute() { + int [] result = new int[57]; + int offset = 0; + offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAttribute(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + /** the input device */ + private java.io.Reader zzReader; + + /** the current state of the DFA */ + private int zzState; + + /** the current lexical state */ + private int zzLexicalState = YYINITIAL; + + /** this buffer contains the current text to be matched and is + the source of the yytext() string */ + private CharSequence zzBuffer = ""; + + /** the textposition at the last accepting state */ + private int zzMarkedPos; + + /** the current text position in the buffer */ + private int zzCurrentPos; + + /** startRead marks the beginning of the yytext() string in the buffer */ + private int zzStartRead; + + /** endRead marks the last character in the buffer, that has been read + from input */ + private int zzEndRead; + + /** + * zzAtBOL == true <=> the scanner is currently at the beginning of a line + */ + private boolean zzAtBOL = true; + + /** zzAtEOF == true <=> the scanner is at the EOF */ + private boolean zzAtEOF; + + /** denotes if the user-EOF-code has already been executed */ + private boolean zzEOFDone; + + /* user code: */ + public _JsonLexer() { + this((java.io.Reader)null); + } + + + /** + * Creates a new scanner + * + * @param in the java.io.Reader to read input from. + */ + public _JsonLexer(java.io.Reader in) { + this.zzReader = in; + } + + + /** + * Unpacks the compressed character translation table. + * + * @param packed the packed character translation table + * @return the unpacked character translation table + */ + private static char [] zzUnpackCMap(String packed) { + int size = 0; + for (int i = 0, length = packed.length(); i < length; i += 2) { + size += packed.charAt(i); + } + char[] map = new char[size]; + int i = 0; /* index in packed string */ + int j = 0; /* index in unpacked array */ + while (i < packed.length()) { + int count = packed.charAt(i++); + char value = packed.charAt(i++); + do map[j++] = value; while (--count > 0); + } + return map; + } + + public final int getTokenStart() { + return zzStartRead; + } + + public final int getTokenEnd() { + return getTokenStart() + yylength(); + } + + public void reset(CharSequence buffer, int start, int end, int initialState) { + zzBuffer = buffer; + zzCurrentPos = zzMarkedPos = zzStartRead = start; + zzAtEOF = false; + zzAtBOL = true; + zzEndRead = end; + yybegin(initialState); + } + + /** + * Refills the input buffer. + * + * @return <code>false</code>, iff there was new input. + * + * @exception java.io.IOException if any I/O-Error occurs + */ + private boolean zzRefill() throws java.io.IOException { + return true; + } + + + /** + * Returns the current lexical state. + */ + public final int yystate() { + return zzLexicalState; + } + + + /** + * Enters a new lexical state + * + * @param newState the new lexical state + */ + public final void yybegin(int newState) { + zzLexicalState = newState; + } + + + /** + * Returns the text matched by the current regular expression. + */ + public final CharSequence yytext() { + return zzBuffer.subSequence(zzStartRead, zzMarkedPos); + } + + + /** + * Returns the character at position <tt>pos</tt> from the + * matched text. + * + * It is equivalent to yytext().charAt(pos), but faster + * + * @param pos the position of the character to fetch. + * A value from 0 to yylength()-1. + * + * @return the character at position pos + */ + public final char yycharat(int pos) { + return zzBuffer.charAt(zzStartRead+pos); + } + + + /** + * Returns the length of the matched text region. + */ + public final int yylength() { + return zzMarkedPos-zzStartRead; + } + + + /** + * Reports an error that occured while scanning. + * + * In a wellformed scanner (no or only correct usage of + * yypushback(int) and a match-all fallback rule) this method + * will only be called with things that "Can't Possibly Happen". + * If this method is called, something is seriously wrong + * (e.g. a JFlex bug producing a faulty scanner etc.). + * + * Usual syntax/scanner level error handling should be done + * in error fallback rules. + * + * @param errorCode the code of the errormessage to display + */ + private void zzScanError(int errorCode) { + String message; + try { + message = ZZ_ERROR_MSG[errorCode]; + } + catch (ArrayIndexOutOfBoundsException e) { + message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; + } + + throw new Error(message); + } + + + /** + * Pushes the specified amount of characters back into the input stream. + * + * They will be read again by then next call of the scanning method + * + * @param number the number of characters to be read again. + * This number must not be greater than yylength()! + */ + public void yypushback(int number) { + if ( number > yylength() ) + zzScanError(ZZ_PUSHBACK_2BIG); + + zzMarkedPos -= number; + } + + + /** + * Resumes scanning until the next regular expression is matched, + * the end of input is encountered or an I/O-Error occurs. + * + * @return the next token + * @exception java.io.IOException if any I/O-Error occurs + */ + public IElementType advance() throws java.io.IOException { + int zzInput; + int zzAction; + + // cached fields: + int zzCurrentPosL; + int zzMarkedPosL; + int zzEndReadL = zzEndRead; + CharSequence zzBufferL = zzBuffer; + + int [] zzTransL = ZZ_TRANS; + int [] zzRowMapL = ZZ_ROWMAP; + int [] zzAttrL = ZZ_ATTRIBUTE; + + while (true) { + zzMarkedPosL = zzMarkedPos; + + zzAction = -1; + + zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL; + + zzState = ZZ_LEXSTATE[zzLexicalState]; + + // set up zzAction for empty match case: + int zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + } + + + zzForAction: { + while (true) { + + if (zzCurrentPosL < zzEndReadL) { + zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL/*, zzEndReadL*/); + zzCurrentPosL += Character.charCount(zzInput); + } + else if (zzAtEOF) { + zzInput = YYEOF; + break zzForAction; + } + else { + // store back cached positions + zzCurrentPos = zzCurrentPosL; + zzMarkedPos = zzMarkedPosL; + boolean eof = zzRefill(); + // get translated positions and possibly new buffer + zzCurrentPosL = zzCurrentPos; + zzMarkedPosL = zzMarkedPos; + zzBufferL = zzBuffer; + zzEndReadL = zzEndRead; + if (eof) { + zzInput = YYEOF; + break zzForAction; + } + else { + zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL/*, zzEndReadL*/); + zzCurrentPosL += Character.charCount(zzInput); + } + } + int zzNext = zzTransL[ zzRowMapL[zzState] + ZZ_CMAP(zzInput) ]; + if (zzNext == -1) break zzForAction; + zzState = zzNext; + + zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + zzMarkedPosL = zzCurrentPosL; + if ( (zzAttributes & 8) == 8 ) break zzForAction; + } + + } + } + + // store back cached position + zzMarkedPos = zzMarkedPosL; + + if (zzInput == YYEOF && zzStartRead == zzCurrentPos) { + zzAtEOF = true; + return null; + } + else { + switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) { + case 1: + { return BAD_CHARACTER; + } + // fall through + case 18: break; + case 2: + { return WHITE_SPACE; + } + // fall through + case 19: break; + case 3: + { return IDENTIFIER; + } + // fall through + case 20: break; + case 4: + { return DOUBLE_QUOTED_STRING; + } + // fall through + case 21: break; + case 5: + { return SINGLE_QUOTED_STRING; + } + // fall through + case 22: break; + case 6: + { return NUMBER; + } + // fall through + case 23: break; + case 7: + { return L_CURLY; + } + // fall through + case 24: break; + case 8: + { return R_CURLY; + } + // fall through + case 25: break; + case 9: + { return L_BRACKET; + } + // fall through + case 26: break; + case 10: + { return R_BRACKET; + } + // fall through + case 27: break; + case 11: + { return COMMA; + } + // fall through + case 28: break; + case 12: + { return COLON; + } + // fall through + case 29: break; + case 13: + { return LINE_COMMENT; + } + // fall through + case 30: break; + case 14: + { return BLOCK_COMMENT; + } + // fall through + case 31: break; + case 15: + { return NULL; + } + // fall through + case 32: break; + case 16: + { return TRUE; + } + // fall through + case 33: break; + case 17: + { return FALSE; + } + // fall through + case 34: break; + default: + zzScanError(ZZ_NO_MATCH); + } + } + } + } + + +} diff --git a/json/gen/com/intellij/json/json5/_Json5Lexer.java b/json/gen/com/intellij/json/json5/_Json5Lexer.java new file mode 100644 index 00000000..f35cb96f --- /dev/null +++ b/json/gen/com/intellij/json/json5/_Json5Lexer.java @@ -0,0 +1,730 @@ +/* The following code was generated by JFlex 1.7.0 tweaked for IntelliJ platform */ + +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.*; + + +/** + * This class is a scanner generated by + * <a href="http://www.jflex.de/">JFlex</a> 1.7.0 + * from the specification file <tt>_Json5Lexer.flex</tt> + */ +public class _Json5Lexer implements FlexLexer { + + /** This character denotes the end of file */ + public static final int YYEOF = -1; + + /** initial size of the lookahead buffer */ + private static final int ZZ_BUFFERSIZE = 16384; + + /** lexical states */ + public static final int YYINITIAL = 0; + + /** + * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l + * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l + * at the beginning of a line + * l is of the form l = 2*k, k a non negative integer + */ + private static final int ZZ_LEXSTATE[] = { + 0, 0 + }; + + /** + * Translates characters to character classes + * Chosen bits are [12, 6, 3] + * Total runtime size is 13376 bytes + */ + public static int ZZ_CMAP(int ch) { + return ZZ_CMAP_A[(ZZ_CMAP_Y[(ZZ_CMAP_Z[ch>>9]<<6)|((ch>>3)&0x3f)]<<3)|(ch&0x7)]; + } + + /* The ZZ_CMAP_Z table has 2176 entries */ + static final char ZZ_CMAP_Z[] = zzUnpackCMap( + "\1\0\1\1\1\2\1\3\1\4\1\5\1\6\1\7\1\10\1\11\1\12\1\13\1\14\1\15\1\16\1\17\1"+ + "\20\5\21\1\22\1\23\1\24\1\21\14\25\1\26\50\25\1\27\2\25\1\30\1\31\1\32\1\33"+ + "\25\25\1\34\20\21\1\35\1\36\1\37\1\40\1\41\1\42\1\43\1\21\1\44\1\45\1\46\1"+ + "\21\1\47\2\21\1\50\4\21\1\25\1\51\1\52\5\21\2\25\1\53\31\21\1\25\1\54\1\21"+ + "\1\55\40\21\1\56\17\21\1\57\1\60\1\61\1\62\13\21\1\63\10\21\123\25\1\64\7"+ + "\25\1\65\1\66\37\21\1\25\1\66\u0582\21\1\67\u017f\21"); + + /* The ZZ_CMAP_Y table has 3584 entries */ + static final char ZZ_CMAP_Y[] = zzUnpackCMap( + "\1\0\1\1\1\0\1\2\1\3\1\4\1\5\1\6\1\7\1\10\1\0\1\11\1\12\1\13\1\14\1\15\1\16"+ + "\3\0\1\17\1\20\1\21\1\22\2\0\1\23\3\0\1\23\71\0\1\24\1\0\1\25\1\26\1\27\1"+ + "\30\2\26\16\0\1\31\1\32\1\33\1\34\2\0\1\35\11\0\1\36\21\0\1\35\1\37\23\0\1"+ + "\26\1\40\3\0\1\23\1\41\1\40\4\0\1\42\1\40\4\0\1\36\1\43\1\26\3\0\2\44\1\26"+ + "\1\27\1\45\1\0\1\44\11\0\1\24\14\0\1\46\1\36\1\0\1\47\1\0\1\50\1\26\1\42\7"+ + "\0\1\51\14\0\1\25\1\26\6\0\1\52\1\22\5\0\1\52\2\26\3\0\1\2\10\26\1\47\1\27"+ + "\6\26\1\53\2\0\1\23\14\0\1\54\1\0\2\40\1\55\1\50\1\56\2\0\1\47\1\57\1\60\1"+ + "\50\1\61\1\42\1\62\1\54\1\0\1\2\1\45\1\55\1\63\1\56\2\0\1\47\1\64\1\65\1\63"+ + "\1\66\1\41\1\67\1\70\1\0\1\52\1\26\1\55\1\36\1\35\2\0\1\47\1\71\1\60\1\36"+ + "\1\72\1\73\1\26\1\54\1\0\1\41\1\26\1\55\1\50\1\56\2\0\1\47\1\71\1\60\1\50"+ + "\1\66\1\70\1\62\1\54\1\0\1\41\1\26\1\74\1\75\1\76\1\77\1\100\1\75\1\0\1\24"+ + "\1\75\1\76\1\101\1\26\1\70\1\0\1\26\1\41\1\55\1\31\1\47\2\0\1\47\1\46\1\102"+ + "\1\31\1\76\1\103\1\25\1\54\1\0\2\26\1\74\1\31\1\47\2\0\1\47\1\46\1\60\1\31"+ + "\1\76\1\103\1\33\1\54\1\0\1\104\1\26\1\74\1\31\1\47\4\0\1\51\1\31\1\105\1"+ + "\42\1\26\1\54\1\0\1\26\1\37\1\74\1\0\1\23\1\37\2\0\1\35\1\106\1\23\1\107\1"+ + "\110\1\0\2\26\1\111\1\26\1\40\6\0\1\63\1\0\1\23\1\0\1\25\4\26\1\112\1\113"+ + "\1\53\1\40\1\114\1\74\1\0\1\72\1\110\1\52\1\0\1\60\4\26\1\73\2\26\1\25\1\0"+ + "\1\25\1\115\1\116\1\0\1\40\3\0\1\27\1\40\1\0\1\31\2\0\1\40\3\0\1\27\1\33\7"+ + "\26\11\0\1\25\11\0\1\52\4\0\1\36\1\21\5\0\1\117\51\0\1\76\1\23\1\76\5\0\1"+ + "\76\4\0\1\76\1\23\1\76\1\0\1\23\7\0\1\76\10\0\1\51\4\26\2\0\2\26\12\0\1\27"+ + "\1\26\1\40\114\0\1\50\2\0\1\120\2\0\1\44\11\0\1\75\1\73\1\26\1\0\1\31\1\27"+ + "\1\26\2\0\1\27\1\26\2\0\1\2\1\26\1\0\1\31\1\121\1\26\12\0\1\122\1\123\1\0"+ + "\1\25\3\26\1\123\1\0\1\25\13\0\1\26\5\0\1\44\10\0\1\52\1\26\3\0\1\27\1\0\1"+ + "\2\1\0\1\2\1\70\4\0\1\52\1\27\1\26\5\0\1\2\3\0\1\25\1\0\1\25\4\26\3\0\1\2"+ + "\7\0\1\23\3\0\1\50\1\0\1\25\1\0\1\25\1\42\13\26\11\0\1\2\1\0\1\25\1\26\1\124"+ + "\1\2\1\26\16\0\1\2\1\26\7\0\1\26\1\0\1\102\5\0\1\52\12\26\1\117\3\0\1\23\1"+ + "\26\34\0\1\23\2\26\1\53\42\0\2\52\4\0\2\52\1\0\1\125\3\0\1\52\6\0\1\31\1\110"+ + "\1\126\1\27\1\54\1\2\1\0\1\27\1\126\1\27\1\127\1\130\3\26\1\131\1\26\1\42"+ + "\1\73\1\26\1\132\1\133\1\27\1\37\1\41\1\42\2\26\1\0\1\27\3\0\1\44\2\26\1\0"+ + "\1\27\1\134\1\0\1\73\1\26\1\107\1\37\1\106\1\135\1\30\1\136\1\0\1\60\1\137"+ + "\1\140\2\26\5\0\1\73\116\26\5\0\1\23\5\0\1\23\20\0\1\27\1\124\1\2\1\26\4\0"+ + "\1\36\1\21\7\0\1\42\1\26\1\42\2\0\1\23\1\26\10\23\4\0\5\26\1\42\72\26\1\141"+ + "\3\26\1\40\1\0\1\135\1\27\1\40\11\0\1\23\1\142\1\40\12\0\1\117\1\137\4\0\1"+ + "\52\1\40\12\0\1\23\2\26\3\0\1\44\6\26\170\0\1\52\11\26\71\0\1\27\6\26\21\0"+ + "\1\27\10\26\5\0\1\52\41\0\1\27\3\0\1\2\2\26\6\0\1\53\1\36\3\0\1\42\12\0\1"+ + "\25\3\26\1\42\1\0\1\37\14\0\1\61\1\2\1\26\1\0\1\44\11\26\6\0\2\26\1\73\6\0"+ + "\1\2\1\26\10\0\1\27\1\26\1\0\1\25\3\0\1\45\5\0\1\52\4\0\1\2\1\26\3\0\1\27"+ + "\10\0\1\73\1\42\1\0\1\25\4\26\6\0\1\23\1\26\1\0\1\52\1\0\1\25\2\0\1\23\1\111"+ + "\10\0\1\44\2\26\1\123\2\0\1\143\1\26\3\144\1\26\2\23\22\26\5\0\1\145\1\0\1"+ + "\25\64\0\1\2\1\26\2\0\1\23\1\124\5\0\1\2\40\26\55\0\1\52\15\0\1\25\4\26\1"+ + "\23\1\26\1\124\1\137\1\0\1\47\1\23\1\110\1\146\15\0\1\25\3\26\1\124\54\0\1"+ + "\52\2\26\10\0\1\37\6\0\5\26\1\0\1\27\2\0\2\26\1\23\1\26\1\100\2\26\1\137\3"+ + "\26\1\41\1\31\20\0\1\50\1\132\1\26\1\0\1\25\1\40\2\0\1\63\1\40\2\0\1\44\1"+ + "\70\12\0\1\23\3\37\1\147\1\150\2\26\1\151\1\0\1\46\2\0\1\23\2\0\1\152\1\0"+ + "\1\52\1\0\1\52\4\26\17\0\1\44\10\26\6\0\1\27\20\26\1\21\20\26\3\0\1\27\6\0"+ + "\1\73\5\26\3\0\1\23\2\26\3\0\1\44\6\26\3\0\1\52\4\0\1\2\1\0\1\135\5\26\23"+ + "\0\1\52\1\0\1\25\52\26\1\52\1\47\4\0\1\36\1\153\2\0\1\52\25\26\2\0\1\52\1"+ + "\26\3\0\1\25\10\26\7\0\1\70\10\26\1\154\1\53\1\46\1\40\2\0\1\2\1\63\4\26\3"+ + "\0\1\27\20\26\6\0\1\52\1\26\2\0\1\52\1\26\2\0\1\44\21\26\11\0\1\73\66\26\10"+ + "\0\1\23\3\26\1\70\1\0\2\26\7\0\1\155\2\26\3\0\1\73\1\0\1\25\6\0\1\31\1\0\10"+ + "\26\10\0\1\27\1\26\1\0\1\25\24\26\7\0\1\26\1\0\1\25\46\26\55\0\1\23\22\26"+ + "\14\0\1\44\63\26\5\0\1\23\72\26\7\0\1\73\130\26\10\0\1\27\1\26\5\0\1\23\1"+ + "\26\1\42\2\0\14\26\1\25\153\26\1\137\1\102\2\0\1\51\1\2\3\26\1\32\22\26\1"+ + "\147\67\26\12\0\1\31\10\0\1\31\1\156\1\157\1\0\1\160\1\46\7\0\1\36\1\51\2"+ + "\31\3\0\1\161\1\110\1\37\1\47\51\0\1\52\3\0\1\47\2\0\1\117\3\0\1\117\2\0\1"+ + "\31\3\0\1\31\2\0\1\23\3\0\1\23\3\0\1\47\3\0\1\47\2\0\1\117\1\54\6\0\1\46\3"+ + "\0\1\112\1\40\1\117\1\162\1\107\1\163\1\112\1\125\1\112\2\117\1\67\1\0\1\35"+ + "\1\0\1\2\1\55\1\35\1\0\1\2\50\26\32\0\1\23\5\26\106\0\1\27\1\26\33\0\1\52"+ + "\74\26\1\41\3\26\14\0\20\26\36\0\2\26"); + + /* The ZZ_CMAP_A table has 928 entries */ + static final char ZZ_CMAP_A[] = zzUnpackCMap( + "\11\35\1\12\1\2\1\1\1\7\1\3\6\35\4\0\1\12\1\35\1\13\1\0\1\35\2\0\1\15\2\35"+ + "\1\11\1\16\1\42\1\17\1\22\1\6\1\20\11\21\1\43\1\0\3\35\1\0\1\35\4\5\1\23\1"+ + "\5\2\35\1\25\4\35\1\33\1\35\1\24\2\35\1\40\1\14\1\41\2\35\1\0\1\34\3\5\1\46"+ + "\1\27\2\35\1\30\2\35\1\47\1\35\1\26\3\35\1\44\1\50\1\31\1\45\2\35\1\24\1\32"+ + "\1\35\1\36\1\0\1\37\7\35\1\10\2\35\1\4\1\0\4\35\4\0\1\35\2\0\1\35\7\0\1\35"+ + "\4\0\1\35\5\0\7\35\1\0\2\35\4\0\4\35\16\0\5\35\7\0\1\35\1\0\1\35\1\0\5\35"+ + "\1\0\2\35\2\0\4\35\10\0\1\35\1\0\3\35\1\0\1\35\1\0\4\35\1\0\13\35\1\0\1\35"+ + "\2\0\6\35\1\0\7\35\1\0\1\35\15\0\1\35\1\0\2\35\1\0\2\35\1\0\4\35\10\0\1\35"+ + "\4\0\4\35\1\0\4\35\1\0\13\35\2\0\4\35\2\0\11\35\6\0\10\35\2\0\2\35\1\0\3\35"+ + "\1\0\4\35\2\0\6\35\1\0\1\35\3\0\4\35\2\0\5\35\2\0\4\35\5\0\2\35\1\0\4\35\4"+ + "\0\2\35\1\0\2\35\1\0\2\35\1\0\2\35\2\0\1\35\1\0\3\35\2\0\3\35\3\0\4\35\1\0"+ + "\1\35\7\0\3\35\1\0\2\35\1\0\5\35\1\0\3\35\2\0\1\35\11\0\2\35\1\0\6\35\3\0"+ + "\3\35\1\0\4\35\3\0\2\35\1\0\1\35\1\0\2\35\3\0\2\35\3\0\1\35\6\0\3\35\3\0\3"+ + "\35\5\0\2\35\2\0\2\35\5\0\1\35\1\0\5\35\1\0\4\35\1\0\1\35\4\0\1\35\4\0\6\35"+ + "\1\0\1\35\3\0\2\35\5\0\2\35\1\0\1\35\2\0\2\35\1\0\1\35\2\0\1\35\3\0\3\35\1"+ + "\0\1\35\1\0\1\35\5\0\1\35\1\0\1\35\1\0\1\35\4\0\5\35\1\0\4\35\1\4\10\35\1"+ + "\0\2\35\4\0\4\35\3\0\1\35\3\0\3\35\5\0\5\35\1\0\1\35\1\0\1\35\1\0\1\35\1\0"+ + "\1\35\2\0\3\35\1\0\2\35\13\4\5\35\2\1\5\35\1\4\4\0\1\35\12\0\1\4\1\0\1\35"+ + "\3\0\3\35\1\0\5\35\2\0\1\35\1\0\4\35\1\0\1\35\5\0\5\35\4\0\1\35\1\0\1\4\4"+ + "\0\3\35\1\0\2\35\2\0\3\35\2\0\5\35\2\0\6\35\1\0\3\35\1\0\2\35\2\0\2\35\1\0"+ + "\2\35\1\0\2\35\2\0\3\35\3\0\2\35\3\0\2\35\2\0\3\35\4\0\3\35\1\0\2\35\1\0\2"+ + "\35\3\0\1\35\2\0\5\35\1\0\2\35\1\0\3\35\2\0\1\35\4\0\1\35\2\0\2\35\2\0\4\35"+ + "\1\0\4\35\1\0\1\35\1\0\5\35\1\0\4\35\2\0\1\35\1\0\1\35\5\0\1\35\1\0\1\35\1"+ + "\0\3\35"); + + /** + * Translates DFA states to action switch labels. + */ + private static final int [] ZZ_ACTION = zzUnpackAction(); + + private static final String ZZ_ACTION_PACKED_0 = + "\1\1\1\2\1\3\2\4\1\3\1\5\1\6\6\1"+ + "\5\4\1\7\1\10\1\11\1\12\1\13\1\14\1\15"+ + "\1\16\1\5\2\0\1\6\4\1\2\0\1\4\2\1"+ + "\5\4\1\15\1\16\1\4\2\5\2\6\3\0\1\1"+ + "\4\4\1\1\1\0\1\16\1\1\1\0\1\1\1\4"+ + "\1\17\1\4\1\20\1\16\1\0\1\4\1\21\1\0"+ + "\1\4\1\0\1\4\1\0"; + + private static int [] zzUnpackAction() { + int [] result = new int[79]; + int offset = 0; + offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAction(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /** + * Translates a state to a row index in the transition table + */ + private static final int [] ZZ_ROWMAP = zzUnpackRowMap(); + + private static final String ZZ_ROWMAP_PACKED_0 = + "\0\0\0\51\0\122\0\173\0\244\0\315\0\366\0\u011f"+ + "\0\u0148\0\u0171\0\u019a\0\u01c3\0\u01ec\0\u0215\0\u023e\0\u0267"+ + "\0\u0290\0\u02b9\0\u02e2\0\51\0\51\0\51\0\51\0\51"+ + "\0\51\0\u030b\0\u0334\0\51\0\u035d\0\u0386\0\51\0\u03af"+ + "\0\u03d8\0\u0401\0\u042a\0\u0453\0\u047c\0\u04a5\0\u04ce\0\u04f7"+ + "\0\u0520\0\u0549\0\u0572\0\u059b\0\u05c4\0\u05ed\0\u0616\0\u063f"+ + "\0\u0668\0\u0691\0\u06ba\0\u06e3\0\u070c\0\u0735\0\u075e\0\u04a5"+ + "\0\u0787\0\u07b0\0\u07d9\0\u0802\0\173\0\u082b\0\173\0\u070c"+ + "\0\u0854\0\51\0\u087d\0\173\0\u08a6\0\173\0\51\0\u08cf"+ + "\0\u08f8\0\173\0\u0921\0\u094a\0\u0973\0\u099c\0\u09c5"; + + private static int [] zzUnpackRowMap() { + int [] result = new int[79]; + int offset = 0; + offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackRowMap(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int high = packed.charAt(i++) << 16; + result[j++] = high | packed.charAt(i++); + } + return j; + } + + /** + * The transition table of the DFA + */ + private static final int [] ZZ_TRANS = zzUnpackTrans(); + + private static final String ZZ_TRANS_PACKED_0 = + "\1\2\4\3\1\4\1\5\1\3\1\6\1\4\1\3"+ + "\1\7\1\2\1\10\1\11\1\12\1\13\1\14\1\15"+ + "\1\16\1\4\1\17\1\20\1\21\1\4\1\22\1\4"+ + "\1\23\2\4\1\24\1\25\1\26\1\27\1\30\1\31"+ + "\2\4\1\16\2\4\52\0\4\3\2\0\2\3\1\0"+ + "\1\3\43\0\2\4\1\0\2\4\5\0\17\4\6\0"+ + "\5\4\5\0\1\4\1\32\1\0\1\4\1\33\5\0"+ + "\17\4\6\0\5\4\1\0\4\3\2\4\1\3\1\6"+ + "\1\4\1\3\4\0\17\4\6\0\5\4\2\7\2\0"+ + "\7\7\1\34\1\35\34\7\2\10\2\0\10\10\1\36"+ + "\1\37\33\10\20\0\1\40\1\41\1\42\1\43\1\0"+ + "\1\44\5\0\1\45\12\0\1\43\7\0\2\4\1\0"+ + "\2\4\5\0\1\4\1\13\1\14\1\15\1\16\1\4"+ + "\1\17\5\4\1\23\2\4\6\0\2\4\1\16\2\4"+ + "\5\0\2\4\1\0\2\4\5\0\1\4\3\15\1\16"+ + "\1\46\11\4\6\0\2\4\1\16\2\4\5\0\2\4"+ + "\1\0\2\4\5\0\1\4\2\14\1\15\1\16\12\4"+ + "\6\0\2\4\1\16\2\4\5\0\2\4\1\0\2\4"+ + "\5\0\1\4\2\15\1\4\1\16\12\4\6\0\2\4"+ + "\1\16\2\4\5\0\2\4\1\0\2\4\4\0\1\47"+ + "\3\50\14\4\6\0\5\4\5\0\2\4\1\0\2\4"+ + "\5\0\7\4\1\51\7\4\6\0\5\4\5\0\2\4"+ + "\1\0\2\4\5\0\17\4\6\0\1\4\1\52\3\4"+ + "\5\0\2\4\1\0\2\4\5\0\15\4\1\53\1\4"+ + "\6\0\5\4\5\0\2\4\1\0\2\4\5\0\17\4"+ + "\6\0\1\54\4\4\5\0\2\4\1\0\2\4\5\0"+ + "\15\4\1\55\1\4\6\0\5\4\1\56\3\0\1\56"+ + "\2\32\1\0\1\4\1\32\5\56\17\32\6\56\5\32"+ + "\5\57\2\33\1\57\1\33\1\60\5\57\17\33\6\57"+ + "\5\33\3\7\1\61\3\7\1\62\2\7\1\62\36\7"+ + "\3\10\1\63\3\10\1\64\2\10\1\64\36\10\20\0"+ + "\3\42\1\43\1\65\21\0\1\43\22\0\2\41\1\42"+ + "\1\43\22\0\1\43\22\0\2\42\1\0\1\43\22\0"+ + "\1\43\20\0\4\47\55\0\1\66\56\0\1\67\21\0"+ + "\1\70\1\4\1\0\2\4\5\0\1\4\2\70\1\4"+ + "\1\70\3\4\1\70\4\4\1\70\1\4\6\0\2\4"+ + "\1\70\2\4\20\0\2\47\34\0\2\4\1\0\2\4"+ + "\5\0\1\4\2\50\14\4\6\0\5\4\5\0\2\4"+ + "\1\0\2\4\5\0\10\4\1\71\6\4\6\0\5\4"+ + "\5\0\2\4\1\0\2\4\5\0\17\4\6\0\3\4"+ + "\1\72\1\4\5\0\2\4\1\0\2\4\5\0\17\4"+ + "\6\0\3\4\1\73\1\4\5\0\2\4\1\0\2\4"+ + "\5\0\17\4\6\0\1\4\1\74\3\4\5\0\2\4"+ + "\1\0\2\4\5\0\14\4\1\75\2\4\6\0\5\4"+ + "\1\56\3\0\3\56\2\0\40\56\11\57\1\76\44\57"+ + "\1\33\1\77\1\57\1\33\1\60\5\57\17\33\6\57"+ + "\5\33\3\7\1\0\7\7\1\34\1\35\37\7\1\61"+ + "\3\7\1\62\2\7\1\62\1\34\1\35\34\7\3\10"+ + "\1\0\10\10\1\36\1\37\36\10\1\63\3\10\1\64"+ + "\2\10\1\64\1\10\1\36\1\37\33\10\5\0\1\100"+ + "\12\0\2\100\1\0\1\100\3\0\1\100\4\0\1\100"+ + "\11\0\1\100\31\0\1\101\54\0\1\102\22\0\2\4"+ + "\1\0\2\4\5\0\11\4\1\103\5\4\6\0\5\4"+ + "\5\0\2\4\1\0\2\4\5\0\17\4\6\0\3\4"+ + "\1\104\1\4\5\0\2\4\1\0\2\4\5\0\17\4"+ + "\6\0\4\4\1\105\5\0\2\4\1\0\2\4\5\0"+ + "\17\4\6\0\2\4\1\106\2\4\6\57\1\107\2\57"+ + "\1\76\37\57\30\0\1\110\25\0\2\4\1\0\2\4"+ + "\5\0\7\4\1\111\7\4\6\0\5\4\5\0\2\4"+ + "\1\0\2\4\5\0\17\4\6\0\2\4\1\112\2\4"+ + "\26\0\1\113\27\0\2\4\1\0\2\4\5\0\11\4"+ + "\1\114\5\4\6\0\5\4\30\0\1\115\25\0\2\4"+ + "\1\0\2\4\5\0\12\4\1\116\4\4\6\0\5\4"+ + "\31\0\1\117\24\0\2\4\1\0\2\4\5\0\13\4"+ + "\1\75\3\4\6\0\5\4\32\0\1\102\16\0"; + + private static int [] zzUnpackTrans() { + int [] result = new int[2542]; + int offset = 0; + offset = zzUnpackTrans(ZZ_TRANS_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackTrans(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + value--; + do result[j++] = value; while (--count > 0); + } + return j; + } + + + /* error codes */ + private static final int ZZ_UNKNOWN_ERROR = 0; + private static final int ZZ_NO_MATCH = 1; + private static final int ZZ_PUSHBACK_2BIG = 2; + + /* error messages for the codes above */ + private static final String[] ZZ_ERROR_MSG = { + "Unknown internal scanner error", + "Error: could not match input", + "Error: pushback value was too large" + }; + + /** + * ZZ_ATTRIBUTE[aState] contains the attributes of state <code>aState</code> + */ + private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute(); + + private static final String ZZ_ATTRIBUTE_PACKED_0 = + "\1\1\1\11\21\1\6\11\2\1\1\11\2\0\1\11"+ + "\4\1\2\0\17\1\3\0\6\1\1\0\2\1\1\0"+ + "\1\11\4\1\1\11\1\0\2\1\1\0\1\1\1\0"+ + "\1\1\1\0"; + + private static int [] zzUnpackAttribute() { + int [] result = new int[79]; + int offset = 0; + offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); + return result; + } + + private static int zzUnpackAttribute(String packed, int offset, int [] result) { + int i = 0; /* index in packed string */ + int j = offset; /* index in unpacked array */ + int l = packed.length(); + while (i < l) { + int count = packed.charAt(i++); + int value = packed.charAt(i++); + do result[j++] = value; while (--count > 0); + } + return j; + } + + /** the input device */ + private java.io.Reader zzReader; + + /** the current state of the DFA */ + private int zzState; + + /** the current lexical state */ + private int zzLexicalState = YYINITIAL; + + /** this buffer contains the current text to be matched and is + the source of the yytext() string */ + private CharSequence zzBuffer = ""; + + /** the textposition at the last accepting state */ + private int zzMarkedPos; + + /** the current text position in the buffer */ + private int zzCurrentPos; + + /** startRead marks the beginning of the yytext() string in the buffer */ + private int zzStartRead; + + /** endRead marks the last character in the buffer, that has been read + from input */ + private int zzEndRead; + + /** + * zzAtBOL == true <=> the scanner is currently at the beginning of a line + */ + private boolean zzAtBOL = true; + + /** zzAtEOF == true <=> the scanner is at the EOF */ + private boolean zzAtEOF; + + /** denotes if the user-EOF-code has already been executed */ + private boolean zzEOFDone; + + /* user code: */ + public _Json5Lexer() { + this((java.io.Reader)null); + } + + + /** + * Creates a new scanner + * + * @param in the java.io.Reader to read input from. + */ + public _Json5Lexer(java.io.Reader in) { + this.zzReader = in; + } + + + /** + * Unpacks the compressed character translation table. + * + * @param packed the packed character translation table + * @return the unpacked character translation table + */ + private static char [] zzUnpackCMap(String packed) { + int size = 0; + for (int i = 0, length = packed.length(); i < length; i += 2) { + size += packed.charAt(i); + } + char[] map = new char[size]; + int i = 0; /* index in packed string */ + int j = 0; /* index in unpacked array */ + while (i < packed.length()) { + int count = packed.charAt(i++); + char value = packed.charAt(i++); + do map[j++] = value; while (--count > 0); + } + return map; + } + + public final int getTokenStart() { + return zzStartRead; + } + + public final int getTokenEnd() { + return getTokenStart() + yylength(); + } + + public void reset(CharSequence buffer, int start, int end, int initialState) { + zzBuffer = buffer; + zzCurrentPos = zzMarkedPos = zzStartRead = start; + zzAtEOF = false; + zzAtBOL = true; + zzEndRead = end; + yybegin(initialState); + } + + /** + * Refills the input buffer. + * + * @return <code>false</code>, iff there was new input. + * + * @exception java.io.IOException if any I/O-Error occurs + */ + private boolean zzRefill() throws java.io.IOException { + return true; + } + + + /** + * Returns the current lexical state. + */ + public final int yystate() { + return zzLexicalState; + } + + + /** + * Enters a new lexical state + * + * @param newState the new lexical state + */ + public final void yybegin(int newState) { + zzLexicalState = newState; + } + + + /** + * Returns the text matched by the current regular expression. + */ + public final CharSequence yytext() { + return zzBuffer.subSequence(zzStartRead, zzMarkedPos); + } + + + /** + * Returns the character at position <tt>pos</tt> from the + * matched text. + * + * It is equivalent to yytext().charAt(pos), but faster + * + * @param pos the position of the character to fetch. + * A value from 0 to yylength()-1. + * + * @return the character at position pos + */ + public final char yycharat(int pos) { + return zzBuffer.charAt(zzStartRead+pos); + } + + + /** + * Returns the length of the matched text region. + */ + public final int yylength() { + return zzMarkedPos-zzStartRead; + } + + + /** + * Reports an error that occured while scanning. + * + * In a wellformed scanner (no or only correct usage of + * yypushback(int) and a match-all fallback rule) this method + * will only be called with things that "Can't Possibly Happen". + * If this method is called, something is seriously wrong + * (e.g. a JFlex bug producing a faulty scanner etc.). + * + * Usual syntax/scanner level error handling should be done + * in error fallback rules. + * + * @param errorCode the code of the errormessage to display + */ + private void zzScanError(int errorCode) { + String message; + try { + message = ZZ_ERROR_MSG[errorCode]; + } + catch (ArrayIndexOutOfBoundsException e) { + message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR]; + } + + throw new Error(message); + } + + + /** + * Pushes the specified amount of characters back into the input stream. + * + * They will be read again by then next call of the scanning method + * + * @param number the number of characters to be read again. + * This number must not be greater than yylength()! + */ + public void yypushback(int number) { + if ( number > yylength() ) + zzScanError(ZZ_PUSHBACK_2BIG); + + zzMarkedPos -= number; + } + + + /** + * Resumes scanning until the next regular expression is matched, + * the end of input is encountered or an I/O-Error occurs. + * + * @return the next token + * @exception java.io.IOException if any I/O-Error occurs + */ + public IElementType advance() throws java.io.IOException { + int zzInput; + int zzAction; + + // cached fields: + int zzCurrentPosL; + int zzMarkedPosL; + int zzEndReadL = zzEndRead; + CharSequence zzBufferL = zzBuffer; + + int [] zzTransL = ZZ_TRANS; + int [] zzRowMapL = ZZ_ROWMAP; + int [] zzAttrL = ZZ_ATTRIBUTE; + + while (true) { + zzMarkedPosL = zzMarkedPos; + + zzAction = -1; + + zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL; + + zzState = ZZ_LEXSTATE[zzLexicalState]; + + // set up zzAction for empty match case: + int zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + } + + + zzForAction: { + while (true) { + + if (zzCurrentPosL < zzEndReadL) { + zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL/*, zzEndReadL*/); + zzCurrentPosL += Character.charCount(zzInput); + } + else if (zzAtEOF) { + zzInput = YYEOF; + break zzForAction; + } + else { + // store back cached positions + zzCurrentPos = zzCurrentPosL; + zzMarkedPos = zzMarkedPosL; + boolean eof = zzRefill(); + // get translated positions and possibly new buffer + zzCurrentPosL = zzCurrentPos; + zzMarkedPosL = zzMarkedPos; + zzBufferL = zzBuffer; + zzEndReadL = zzEndRead; + if (eof) { + zzInput = YYEOF; + break zzForAction; + } + else { + zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL/*, zzEndReadL*/); + zzCurrentPosL += Character.charCount(zzInput); + } + } + int zzNext = zzTransL[ zzRowMapL[zzState] + ZZ_CMAP(zzInput) ]; + if (zzNext == -1) break zzForAction; + zzState = zzNext; + + zzAttributes = zzAttrL[zzState]; + if ( (zzAttributes & 1) == 1 ) { + zzAction = zzState; + zzMarkedPosL = zzCurrentPosL; + if ( (zzAttributes & 8) == 8 ) break zzForAction; + } + + } + } + + // store back cached position + zzMarkedPos = zzMarkedPosL; + + if (zzInput == YYEOF && zzStartRead == zzCurrentPos) { + zzAtEOF = true; + return null; + } + else { + switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) { + case 1: + { return NUMBER; + } + // fall through + case 18: break; + case 2: + { return BAD_CHARACTER; + } + // fall through + case 19: break; + case 3: + { return WHITE_SPACE; + } + // fall through + case 20: break; + case 4: + { return IDENTIFIER; + } + // fall through + case 21: break; + case 5: + { return DOUBLE_QUOTED_STRING; + } + // fall through + case 22: break; + case 6: + { return SINGLE_QUOTED_STRING; + } + // fall through + case 23: break; + case 7: + { return L_CURLY; + } + // fall through + case 24: break; + case 8: + { return R_CURLY; + } + // fall through + case 25: break; + case 9: + { return L_BRACKET; + } + // fall through + case 26: break; + case 10: + { return R_BRACKET; + } + // fall through + case 27: break; + case 11: + { return COMMA; + } + // fall through + case 28: break; + case 12: + { return COLON; + } + // fall through + case 29: break; + case 13: + { return LINE_COMMENT; + } + // fall through + case 30: break; + case 14: + { return BLOCK_COMMENT; + } + // fall through + case 31: break; + case 15: + { return NULL; + } + // fall through + case 32: break; + case 16: + { return TRUE; + } + // fall through + case 33: break; + case 17: + { return FALSE; + } + // fall through + case 34: break; + default: + zzScanError(ZZ_NO_MATCH); + } + } + } + } + + +} diff --git a/json/gen/com/intellij/json/psi/JsonArray.java b/json/gen/com/intellij/json/psi/JsonArray.java new file mode 100644 index 00000000..58263639 --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonArray.java @@ -0,0 +1,17 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; +import com.intellij.navigation.ItemPresentation; + +public interface JsonArray extends JsonContainer { + + @NotNull + List<JsonValue> getValueList(); + + @Nullable + ItemPresentation getPresentation(); + +} diff --git a/json/gen/com/intellij/json/psi/JsonBooleanLiteral.java b/json/gen/com/intellij/json/psi/JsonBooleanLiteral.java new file mode 100644 index 00000000..cc0abeca --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonBooleanLiteral.java @@ -0,0 +1,12 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; + +public interface JsonBooleanLiteral extends JsonLiteral { + + boolean getValue(); + +} diff --git a/json/gen/com/intellij/json/psi/JsonContainer.java b/json/gen/com/intellij/json/psi/JsonContainer.java new file mode 100644 index 00000000..a69c0a8b --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonContainer.java @@ -0,0 +1,10 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; + +public interface JsonContainer extends JsonValue { + +} diff --git a/json/gen/com/intellij/json/psi/JsonElementVisitor.java b/json/gen/com/intellij/json/psi/JsonElementVisitor.java new file mode 100644 index 00000000..836eeea6 --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonElementVisitor.java @@ -0,0 +1,64 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiNamedElement; + +public class JsonElementVisitor extends PsiElementVisitor { + + public void visitArray(@NotNull JsonArray o) { + visitContainer(o); + } + + public void visitBooleanLiteral(@NotNull JsonBooleanLiteral o) { + visitLiteral(o); + } + + public void visitContainer(@NotNull JsonContainer o) { + visitValue(o); + } + + public void visitLiteral(@NotNull JsonLiteral o) { + visitValue(o); + } + + public void visitNullLiteral(@NotNull JsonNullLiteral o) { + visitLiteral(o); + } + + public void visitNumberLiteral(@NotNull JsonNumberLiteral o) { + visitLiteral(o); + } + + public void visitObject(@NotNull JsonObject o) { + visitContainer(o); + } + + public void visitProperty(@NotNull JsonProperty o) { + visitElement(o); + // visitPsiNamedElement(o); + } + + public void visitReferenceExpression(@NotNull JsonReferenceExpression o) { + visitValue(o); + } + + public void visitStringLiteral(@NotNull JsonStringLiteral o) { + visitLiteral(o); + } + + public void visitValue(@NotNull JsonValue o) { + visitElement(o); + } + + public void visitElement(@NotNull JsonElement o) { + visitPsiElement(o); + } + + public void visitPsiElement(@NotNull PsiElement o) { + visitElement(o); + } + +} diff --git a/json/gen/com/intellij/json/psi/JsonLiteral.java b/json/gen/com/intellij/json/psi/JsonLiteral.java new file mode 100644 index 00000000..6e0eb7c7 --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonLiteral.java @@ -0,0 +1,12 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; + +public interface JsonLiteral extends JsonValue { + + boolean isQuotedString(); + +} diff --git a/json/gen/com/intellij/json/psi/JsonNullLiteral.java b/json/gen/com/intellij/json/psi/JsonNullLiteral.java new file mode 100644 index 00000000..a57448e6 --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonNullLiteral.java @@ -0,0 +1,10 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; + +public interface JsonNullLiteral extends JsonLiteral { + +} diff --git a/json/gen/com/intellij/json/psi/JsonNumberLiteral.java b/json/gen/com/intellij/json/psi/JsonNumberLiteral.java new file mode 100644 index 00000000..14649f82 --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonNumberLiteral.java @@ -0,0 +1,12 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; + +public interface JsonNumberLiteral extends JsonLiteral { + + double getValue(); + +} diff --git a/json/gen/com/intellij/json/psi/JsonObject.java b/json/gen/com/intellij/json/psi/JsonObject.java new file mode 100644 index 00000000..329a16f4 --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonObject.java @@ -0,0 +1,20 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; +import com.intellij.navigation.ItemPresentation; + +public interface JsonObject extends JsonContainer { + + @NotNull + List<JsonProperty> getPropertyList(); + + @Nullable + JsonProperty findProperty(@NotNull String name); + + @Nullable + ItemPresentation getPresentation(); + +} diff --git a/json/gen/com/intellij/json/psi/JsonProperty.java b/json/gen/com/intellij/json/psi/JsonProperty.java new file mode 100644 index 00000000..eaf444c4 --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonProperty.java @@ -0,0 +1,24 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiNamedElement; +import com.intellij.navigation.ItemPresentation; + +public interface JsonProperty extends JsonElement, PsiNamedElement { + + @NotNull + String getName(); + + @NotNull + JsonValue getNameElement(); + + @Nullable + JsonValue getValue(); + + @Nullable + ItemPresentation getPresentation(); + +} diff --git a/json/gen/com/intellij/json/psi/JsonReferenceExpression.java b/json/gen/com/intellij/json/psi/JsonReferenceExpression.java new file mode 100644 index 00000000..548fea1b --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonReferenceExpression.java @@ -0,0 +1,13 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; + +public interface JsonReferenceExpression extends JsonValue { + + @NotNull + PsiElement getIdentifier(); + +} diff --git a/json/gen/com/intellij/json/psi/JsonStringLiteral.java b/json/gen/com/intellij/json/psi/JsonStringLiteral.java new file mode 100644 index 00000000..260976c9 --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonStringLiteral.java @@ -0,0 +1,20 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; + +public interface JsonStringLiteral extends JsonLiteral { + + @NotNull + List<Pair<TextRange, String>> getTextFragments(); + + @NotNull + String getValue(); + + boolean isPropertyName(); + +} diff --git a/json/gen/com/intellij/json/psi/JsonValue.java b/json/gen/com/intellij/json/psi/JsonValue.java new file mode 100644 index 00000000..66c8d38b --- /dev/null +++ b/json/gen/com/intellij/json/psi/JsonValue.java @@ -0,0 +1,10 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.psi.PsiElement; + +public interface JsonValue extends JsonElement { + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonArrayImpl.java b/json/gen/com/intellij/json/psi/impl/JsonArrayImpl.java new file mode 100644 index 00000000..e63eb97e --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonArrayImpl.java @@ -0,0 +1,40 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; +import com.intellij.navigation.ItemPresentation; + +public class JsonArrayImpl extends JsonContainerImpl implements JsonArray { + + public JsonArrayImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitArray(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + + @Override + @NotNull + public List<JsonValue> getValueList() { + return PsiTreeUtil.getChildrenOfTypeAsList(this, JsonValue.class); + } + + @Nullable + public ItemPresentation getPresentation() { + return JsonPsiImplUtils.getPresentation(this); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonBooleanLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonBooleanLiteralImpl.java new file mode 100644 index 00000000..0dc43a31 --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonBooleanLiteralImpl.java @@ -0,0 +1,32 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; + +public class JsonBooleanLiteralImpl extends JsonLiteralImpl implements JsonBooleanLiteral { + + public JsonBooleanLiteralImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitBooleanLiteral(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + + public boolean getValue() { + return JsonPsiImplUtils.getValue(this); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonContainerImpl.java b/json/gen/com/intellij/json/psi/impl/JsonContainerImpl.java new file mode 100644 index 00000000..3a8bc91a --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonContainerImpl.java @@ -0,0 +1,28 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; + +public class JsonContainerImpl extends JsonValueImpl implements JsonContainer { + + public JsonContainerImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitContainer(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonLiteralImpl.java new file mode 100644 index 00000000..fa6759ef --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonLiteralImpl.java @@ -0,0 +1,32 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; + +public abstract class JsonLiteralImpl extends JsonLiteralMixin implements JsonLiteral { + + public JsonLiteralImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitLiteral(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + + public boolean isQuotedString() { + return JsonPsiImplUtils.isQuotedString(this); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonNullLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonNullLiteralImpl.java new file mode 100644 index 00000000..53499f8b --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonNullLiteralImpl.java @@ -0,0 +1,28 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; + +public class JsonNullLiteralImpl extends JsonLiteralImpl implements JsonNullLiteral { + + public JsonNullLiteralImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitNullLiteral(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonNumberLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonNumberLiteralImpl.java new file mode 100644 index 00000000..a56f43a9 --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonNumberLiteralImpl.java @@ -0,0 +1,32 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; + +public class JsonNumberLiteralImpl extends JsonLiteralImpl implements JsonNumberLiteral { + + public JsonNumberLiteralImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitNumberLiteral(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + + public double getValue() { + return JsonPsiImplUtils.getValue(this); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonObjectImpl.java b/json/gen/com/intellij/json/psi/impl/JsonObjectImpl.java new file mode 100644 index 00000000..0f3d929f --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonObjectImpl.java @@ -0,0 +1,40 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; +import com.intellij.navigation.ItemPresentation; + +public class JsonObjectImpl extends JsonObjectMixin implements JsonObject { + + public JsonObjectImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitObject(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + + @Override + @NotNull + public List<JsonProperty> getPropertyList() { + return PsiTreeUtil.getChildrenOfTypeAsList(this, JsonProperty.class); + } + + @Nullable + public ItemPresentation getPresentation() { + return JsonPsiImplUtils.getPresentation(this); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonPropertyImpl.java b/json/gen/com/intellij/json/psi/impl/JsonPropertyImpl.java new file mode 100644 index 00000000..395f686a --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonPropertyImpl.java @@ -0,0 +1,49 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; +import com.intellij.navigation.ItemPresentation; + +public class JsonPropertyImpl extends JsonPropertyMixin implements JsonProperty { + + public JsonPropertyImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitProperty(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + + @NotNull + public String getName() { + return JsonPsiImplUtils.getName(this); + } + + @NotNull + public JsonValue getNameElement() { + return JsonPsiImplUtils.getNameElement(this); + } + + @Nullable + public JsonValue getValue() { + return JsonPsiImplUtils.getValue(this); + } + + @Nullable + public ItemPresentation getPresentation() { + return JsonPsiImplUtils.getPresentation(this); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonReferenceExpressionImpl.java b/json/gen/com/intellij/json/psi/impl/JsonReferenceExpressionImpl.java new file mode 100644 index 00000000..d9ca139c --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonReferenceExpressionImpl.java @@ -0,0 +1,34 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; + +public class JsonReferenceExpressionImpl extends JsonValueImpl implements JsonReferenceExpression { + + public JsonReferenceExpressionImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitReferenceExpression(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + + @Override + @NotNull + public PsiElement getIdentifier() { + return findNotNullChildByType(IDENTIFIER); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonStringLiteralImpl.java b/json/gen/com/intellij/json/psi/impl/JsonStringLiteralImpl.java new file mode 100644 index 00000000..365409b9 --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonStringLiteralImpl.java @@ -0,0 +1,44 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; + +public class JsonStringLiteralImpl extends JsonStringLiteralMixin implements JsonStringLiteral { + + public JsonStringLiteralImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitStringLiteral(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + + @NotNull + public List<Pair<TextRange, String>> getTextFragments() { + return JsonPsiImplUtils.getTextFragments(this); + } + + @NotNull + public String getValue() { + return JsonPsiImplUtils.getValue(this); + } + + public boolean isPropertyName() { + return JsonPsiImplUtils.isPropertyName(this); + } + +} diff --git a/json/gen/com/intellij/json/psi/impl/JsonValueImpl.java b/json/gen/com/intellij/json/psi/impl/JsonValueImpl.java new file mode 100644 index 00000000..7b7ce1de --- /dev/null +++ b/json/gen/com/intellij/json/psi/impl/JsonValueImpl.java @@ -0,0 +1,28 @@ +// This is a generated file. Not intended for manual editing. +package com.intellij.json.psi.impl; + +import java.util.List; +import org.jetbrains.annotations.*; +import com.intellij.lang.ASTNode; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.util.PsiTreeUtil; +import static com.intellij.json.JsonElementTypes.*; +import com.intellij.json.psi.*; + +public abstract class JsonValueImpl extends JsonElementImpl implements JsonValue { + + public JsonValueImpl(@NotNull ASTNode node) { + super(node); + } + + public void accept(@NotNull JsonElementVisitor visitor) { + visitor.visitValue(this); + } + + public void accept(@NotNull PsiElementVisitor visitor) { + if (visitor instanceof JsonElementVisitor) accept((JsonElementVisitor)visitor); + else super.accept(visitor); + } + +} diff --git a/json/intellij.json.iml b/json/intellij.json.iml new file mode 100644 index 00000000..b26c976a --- /dev/null +++ b/json/intellij.json.iml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="intellij.platform.core" /> + <orderEntry type="module" module-name="intellij.platform.ide" /> + <orderEntry type="module" module-name="intellij.platform.lang.impl" /> + <orderEntry type="module" module-name="intellij.spellchecker" /> + <orderEntry type="library" name="Guava" level="project" /> + <orderEntry type="library" name="gson" level="project" /> + <orderEntry type="module" module-name="intellij.regexp" /> + </component> +</module>
\ No newline at end of file diff --git a/json/json.bnf b/json/json.bnf new file mode 100644 index 00000000..8036cf33 --- /dev/null +++ b/json/json.bnf @@ -0,0 +1,145 @@ +{ + parserClass = 'com.intellij.json.JsonParser' + parserUtilClass = "com.intellij.json.psi.JsonParserUtil" + psiPackage = 'com.intellij.json.psi' + psiImplPackage = 'com.intellij.json.psi.impl' + + elementTypeHolderClass = 'com.intellij.json.JsonElementTypes' + elementTypeClass = 'com.intellij.json.JsonElementType' + psiClassPrefix = "Json" + psiVisitorName = "JsonElementVisitor" + + psiImplUtilClass = 'com.intellij.json.psi.impl.JsonPsiImplUtils' + tokenTypeClass = 'com.intellij.json.JsonTokenType' + + implements("value") = "com.intellij.json.psi.JsonElement" + extends("value") = "com.intellij.json.psi.impl.JsonElementImpl" + + tokens = [ + L_CURLY='{' + R_CURLY='}' + L_BRACKET='[' + R_BRACKET=']' + + COMMA=',' + COLON=':' + LINE_COMMENT='regexp://.*' + // "/*" ([^*]|\*+[^*/])* (\*+"/")? + BLOCK_COMMENT='regexp:/\*([^*]|\*+[^*/])*(\*+/)?' + // else /\*(?:[^*]|\*[^/])*\*+/ + + // unclosed string literal matches till the line's end + // any escape sequences included, illegal escapes are indicated by SyntaxHighlighter + // and JsonStringLiteralAnnotator + DOUBLE_QUOTED_STRING="regexp:\"([^\\\"\r\n]|\\[^\r\n])*\"?" + SINGLE_QUOTED_STRING="regexp:'([^\\\'\r\n]|\\[^\r\n])*'?" +// STRING='regexp:"([^\\"\r\n]|\\([\\"/bfnrt]|u[a-fA-F0-9]{4}))*"?' + + NUMBER='regexp:-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d*)?' + TRUE='true' + FALSE='false' + NULL='null' + // Actually not defined in RFC 4627, but may be used for JSON5 and helps with + // auto completion of keywords. Semantically, it represents "bad word" type + // of tokens + // Could be as loose as [^\s\[\]{}:,\"\']+, but is slightly more restricted + // for the time being to match most forms of npm package names and semver versions + // in package.json. + // See https://github.com/npm/validate-npm-package-name + IDENTIFIER="regexp:[[:jletterdigit:]~!()*\-./@\^<>=]+" + ] + + extends("container|literal|reference_expression")=value + extends("array|object")=container + extends("string_literal|number_literal|boolean_literal|null_literal")=literal + implements("property")=[ + "com.intellij.json.psi.JsonElement" + "com.intellij.psi.PsiNamedElement" + ] +} + +// For compatibility we allow any value at root level (see JsonStandardComplianceAnnotator) +json ::= value+ + +object ::= '{' object_element* '}' { + pin=1 + methods=[ + findProperty + getPresentation + ] + mixin="com.intellij.json.psi.impl.JsonObjectMixin" +} + +// Hackity-hack to parse array elements and properties even if separating commas are missing, +// TODO: Find out if there is any simpler way to do so in GrammarKit +private object_element ::= property (','|&'}') { + recoverWhile = not_brace_or_next_value + pin = 1 +} + +property ::= property_name (':' value) { + methods=[ + getName + getNameElement + getValue + // suppress getValueList() accessor + value="" + getPresentation + ] + mixin="com.intellij.json.psi.impl.JsonPropertyMixin" + pin(".*")=1 +} + +private property_name ::= literal | reference_expression + +array ::= '[' array_element* ']' { + methods=[ + getPresentation + ] + pin=1 +} + +private array_element ::= value (','|&']') { + recoverWhile = not_bracket_or_next_value + pin=1 +} + +string_literal ::= SINGLE_QUOTED_STRING | DOUBLE_QUOTED_STRING { + methods=[ + getTextFragments + getValue + isPropertyName + SINGLE_QUOTED_STRING="" + DOUBLE_QUOTED_STRING="" + ] + mixin="com.intellij.json.psi.impl.JsonStringLiteralMixin" +} +number_literal ::= NUMBER { + methods=[ + NUMBER="" + getValue + ] +} +boolean_literal ::= TRUE | FALSE { + methods=[ + getValue + ] +} +null_literal ::= NULL + +literal ::= string_literal | number_literal | boolean_literal | null_literal { + methods=[ + isQuotedString + ] + mixin="com.intellij.json.psi.impl.JsonLiteralMixin" +} + +fake container ::= + +reference_expression ::= IDENTIFIER + +value ::= object | array | literal | reference_expression + +// Recoveries +private not_bracket_or_next_value ::= !(']'|value) +private not_brace_or_next_value ::= !('}'|value)
\ No newline at end of file diff --git a/json/resources/inspectionDescriptions/Json5StandardCompliance.html b/json/resources/inspectionDescriptions/Json5StandardCompliance.html new file mode 100644 index 00000000..8171150e --- /dev/null +++ b/json/resources/inspectionDescriptions/Json5StandardCompliance.html @@ -0,0 +1,5 @@ +<html> +<body> +This inspection checks that JSON5 files conform to language specification (http://json5.org/).<br> +</body> +</html>
\ No newline at end of file diff --git a/json/resources/inspectionDescriptions/JsonDuplicatePropertyKeys.html b/json/resources/inspectionDescriptions/JsonDuplicatePropertyKeys.html new file mode 100644 index 00000000..51dead15 --- /dev/null +++ b/json/resources/inspectionDescriptions/JsonDuplicatePropertyKeys.html @@ -0,0 +1,5 @@ +<html> +<body> +This inspection checks that object literals don't contain duplicate keys.<br> +</body> +</html>
\ No newline at end of file diff --git a/json/resources/inspectionDescriptions/JsonSchemaCompliance.html b/json/resources/inspectionDescriptions/JsonSchemaCompliance.html new file mode 100644 index 00000000..11c402d7 --- /dev/null +++ b/json/resources/inspectionDescriptions/JsonSchemaCompliance.html @@ -0,0 +1,5 @@ +<html> +<body> +This inspection checks that JSON files conform to JSON Schemas assigned to them<br> +</body> +</html>
\ No newline at end of file diff --git a/json/resources/inspectionDescriptions/JsonSchemaRefReference.html b/json/resources/inspectionDescriptions/JsonSchemaRefReference.html new file mode 100644 index 00000000..714ecb28 --- /dev/null +++ b/json/resources/inspectionDescriptions/JsonSchemaRefReference.html @@ -0,0 +1,5 @@ +<html> +<body> +This inspection checks that '$ref' and '$schema' paths are valid<br> +</body> +</html>
\ No newline at end of file diff --git a/json/resources/inspectionDescriptions/JsonStandardCompliance.html b/json/resources/inspectionDescriptions/JsonStandardCompliance.html new file mode 100644 index 00000000..12ca8a17 --- /dev/null +++ b/json/resources/inspectionDescriptions/JsonStandardCompliance.html @@ -0,0 +1,5 @@ +<html> +<body> +This inspection checks that JSON files conform to language specification (RFC-7159).<br> +</body> +</html>
\ No newline at end of file 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 +} diff --git a/json/tests/intellij.json.tests.iml b/json/tests/intellij.json.tests.iml new file mode 100644 index 00000000..0337bee4 --- /dev/null +++ b/json/tests/intellij.json.tests.iml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/testData" type="java-test-resource" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" /> + <orderEntry type="module" module-name="intellij.json" scope="TEST" /> + <orderEntry type="module" module-name="intellij.java.testFramework" scope="TEST" /> + <orderEntry type="module" module-name="intellij.spellchecker" scope="TEST" /> + <orderEntry type="module" module-name="intellij.platform.testExtensions" scope="TEST" /> + </component> +</module>
\ No newline at end of file diff --git a/json/tests/test/com/intellij/json/JsonBreadcrumbsTest.java b/json/tests/test/com/intellij/json/JsonBreadcrumbsTest.java new file mode 100644 index 00000000..9416a649 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonBreadcrumbsTest.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; + +import com.intellij.ui.components.breadcrumbs.Crumb; + +import java.util.List; + +public class JsonBreadcrumbsTest extends JsonTestCase { + + private void doTest(String... components) { + myFixture.configureByFile("breadcrumbs/" + getTestName(false) + ".json"); + List<Crumb> caret = myFixture.getBreadcrumbsAtCaret(); + assertOrderedEquals(caret.stream().map(Crumb::getText).toArray(String[]::new), components); + } + + public void testComplexItems() { + doTest("foo", "bar", "0", "0", "baz"); + } +} diff --git a/json/tests/test/com/intellij/json/JsonCommenterTest.java b/json/tests/test/com/intellij/json/JsonCommenterTest.java new file mode 100644 index 00000000..f26d96a7 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonCommenterTest.java @@ -0,0 +1,32 @@ +package com.intellij.json; + +import com.intellij.openapi.actionSystem.IdeActions; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonCommenterTest extends JsonTestCase { + + private void doTest(@NotNull String actionId) { + myFixture.configureByFile("commenter/" + getTestName(false) + ".json"); + myFixture.performEditorAction(actionId); + myFixture.checkResultByFile("commenter/" + getTestName(false) + "_after.json", true); + } + + public void testLineComment() { + doTest(IdeActions.ACTION_COMMENT_LINE); + } + + public void testLineComment2() { + doTest(IdeActions.ACTION_COMMENT_LINE); + } + + public void testLineComment3() { + doTest(IdeActions.ACTION_COMMENT_LINE); + } + + public void testBlockComment() { + doTest(IdeActions.ACTION_COMMENT_BLOCK); + } +} diff --git a/json/tests/test/com/intellij/json/JsonCompletionTest.java b/json/tests/test/com/intellij/json/JsonCompletionTest.java new file mode 100644 index 00000000..661fe34f --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonCompletionTest.java @@ -0,0 +1,61 @@ +package com.intellij.json; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.util.ArrayUtil; + +/** + * @author Mikhail Golubev + */ +public class JsonCompletionTest extends JsonTestCase { + private static final String[] ALL_KEYWORDS = new String[]{"true", "false", "null"}; + private static final String[] NOTHING = ArrayUtil.EMPTY_STRING_ARRAY; + + private void doTest(String... variants) { + myFixture.testCompletionVariants("completion/" + getTestName(false) + ".json", variants); + } + + private void doTestSingleVariant() { + myFixture.configureByFile("completion/" + getTestName(false) + ".json"); + final LookupElement[] variants = myFixture.completeBasic(); + assertNull(variants); + myFixture.checkResultByFile("completion/" + getTestName(false) + "_after.json" ); + } + + public void testInsideArrayElement1() { + doTest(ALL_KEYWORDS); + } + + public void testInsideArrayElement2() { + doTest(ALL_KEYWORDS); + } + + public void testInsidePropertyKey1() { + doTest(NOTHING); + } + + public void testInsidePropertyKey2() { + doTest(NOTHING); + } + + public void testInsideStringLiteral1() { + doTest(NOTHING); + } + + public void testInsideStringLiteral2() { + doTest(NOTHING); + } + + public void testInsidePropertyValue() { + doTest(ALL_KEYWORDS); + } + + // Moved from JavaScript + + public void testKeywords() { + doTestSingleVariant(); + } + + public void testKeywords_2() { + doTestSingleVariant(); + } +} diff --git a/json/tests/test/com/intellij/json/JsonCopyPasteTest.java b/json/tests/test/com/intellij/json/JsonCopyPasteTest.java new file mode 100644 index 00000000..7997274c --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonCopyPasteTest.java @@ -0,0 +1,119 @@ +// 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.openapi.actionSystem.IdeActions; +import com.intellij.testFramework.fixtures.CodeInsightFixtureTestCase; + +public class JsonCopyPasteTest extends CodeInsightFixtureTestCase { + + private void doCopyPasteTest(String source, String dest, String expected, String filename1, String filename2) { + myFixture.configureByText(filename1, source); + myFixture.performEditorAction(IdeActions.ACTION_EDITOR_COPY); + myFixture.configureByText(filename2, dest); + myFixture.performEditorAction(IdeActions.ACTION_EDITOR_PASTE); + myFixture.checkResult(expected); + } + + private void doTestFromTextToJson(String source, String dest, String expected) { + doCopyPasteTest(source, dest, expected, "dummy.txt", "dummy.json"); + } + + private void doTestFromJsonToText(String source, String dest, String expected) { + doCopyPasteTest(source, dest, expected, "dummy.json", "dummy.txt"); + } + + public void testUnescapeQuotes() { + doTestFromJsonToText("{\"p\": \"<selection>\\\"quoted\\\"</selection>\"}", "<caret>", "\"quoted\""); + } + + public void testUnescapeWhitespaces() { + doTestFromJsonToText("{\"p\": \"<selection>lorem ipsum\\tdolor sit amet</selection>\"}", "<caret>", "lorem ipsum\tdolor sit amet"); + } + + public void testUnescapeFromPropNames() { + doTestFromJsonToText("{\"<selection>lorem ipsum\\tdolor sit amet</selection>\": \"foo\"}", "<caret>", "lorem ipsum\tdolor sit amet"); + } + + public void testEscapeQuotes() { + doTestFromTextToJson("<selection>\"quoted\"</selection>", "{\"p\": \"<caret>\"}", "{\"p\": \"\\\"quoted\\\"\"}"); + } + + public void testEscapeWhitespaces() { + doTestFromTextToJson("<selection>lorem ipsum\tdolor sit amet</selection>", "{\"p\": \"<caret>\"}", "{\"p\": \"lorem ipsum\\tdolor sit amet\"}"); + } + + public void testEscapeInPropNames() { + doTestFromTextToJson("<selection>lorem ipsum\tdolor sit amet</selection>", "{\"<caret>\": \"foo\"}", "{\"lorem ipsum\\tdolor sit amet\": \"foo\"}"); + } + + public void testAddTrailingComma() { + doTestFromTextToJson("<selection>\"x\": 5</selection>", "{\"q\": \"foo\"<caret>}", "{\"q\": \"foo\",\"x\": 5}"); + } + + public void testAddRemoveTrailingComma() { + doTestFromTextToJson("<selection>\"x\": 5,</selection>", "{\"q\": \"foo\"<caret>}", "{\"q\": \"foo\",\"x\": 5}"); + } + + public void testAddLeadingComma() { + doTestFromTextToJson("<selection>\"x\": 5</selection>", "{<caret>\"q\": \"foo\"}", "{\"x\": 5,\"q\": \"foo\"}"); + } + + public void testDoNothingLeadingComma() { + doTestFromTextToJson("<selection>\"x\": 5,</selection>", "{<caret>\"q\": \"foo\"}", "{\"x\": 5,\"q\": \"foo\"}"); + } + + public void testCommasMidPropList() { + doTestFromTextToJson("<selection>\"x\": 5</selection>", "{\"s\": \"foo\",<caret>\"q\": \"foo\"}", "{\"s\": \"foo\",\"x\": 5,\"q\": \"foo\"}"); + } + + public void testAddTrailingCommaArray() { + doTestFromTextToJson("<selection>\"a\"</selection>", "[4<caret>]", "[4,\"a\"]"); + } + + public void testAddRemoveTrailingCommaArray() { + doTestFromTextToJson("<selection>\"a\",</selection>", "[4<caret>]", "[4,\"a\"]"); + } + + public void testAddLeadingCommaArray() { + doTestFromTextToJson("<selection>\"a\"</selection>", "[<caret>4]", "[\"a\",4]"); + } + + public void testCommasMidPropListArray() { + doTestFromTextToJson("<selection>\"a\"</selection>", "[3,<caret>4]", "[3,\"a\",4]"); + } + + public void testWithLeadingAndTrailingWhitespaces() { + doTestFromTextToJson("<selection> \t \"a\": true \t </selection>", "{\"q\": 5<caret>}", "{\"q\": 5, \t \"a\": true \t }"); + } + + public void testWithLeadingAndTrailingWhitespacesArray() { + doTestFromTextToJson("<selection> \t \"a\" \t </selection>", "[\"q\"<caret>]", "[\"q\", \t \"a\" \t ]"); + } + + public void testWithLeadingAndTrailingWhitespacesBefore() { + doTestFromTextToJson("<selection> \t \"a\": true \t </selection>", "{<caret>\"q\": 5}", "{ \t \"a\": true, \t \"q\": 5}"); + } + + public void testWithLeadingAndTrailingWhitespacesArrayBefore() { + doTestFromTextToJson("<selection> \t \"a\" \t </selection>", "[<caret>\"q\"]", "[ \t \"a\", \t \"q\"]"); + } + + public void testTrailingNewline() { + doTestFromTextToJson(" \"react-dom\": \"^16.5.2\"\n", "{\n" + + " \"name\": \"untitled\",\n" + + " \"version\": \"1.0.0\",\n" + + " \"dependencies\": {<caret>\n" + + " \"react\": \"^16.5.2\",\n" + + " \"react-dom\": \"^16.5.2\"\n" + + " }\n" + + "}", "{\n" + + " \"name\": \"untitled\",\n" + + " \"version\": \"1.0.0\",\n" + + " \"dependencies\": { \"react-dom\": \"^16.5.2\",\n" + + "\n" + + " \"react\": \"^16.5.2\",\n" + + " \"react-dom\": \"^16.5.2\"\n" + + " }\n" + + "}"); + } +} diff --git a/json/tests/test/com/intellij/json/JsonEditingTest.java b/json/tests/test/com/intellij/json/JsonEditingTest.java new file mode 100644 index 00000000..97d33adf --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonEditingTest.java @@ -0,0 +1,76 @@ +package com.intellij.json; + +import com.intellij.json.formatter.JsonCodeStyleSettings; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonEditingTest extends JsonTestCase { + + private void doTest(@NotNull final String characters) { + final String testName = "editing/" + getTestName(false); + myFixture.configureByFile(testName + ".json"); + myFixture.type(characters); + myFixture.checkResultByFile(testName + ".after.json"); + } + + public void testContinuationIndentAfterPropertyKey() { + doTest("\n"); + } + + public void testContinuationIndentAfterColon() { + doTest("\n"); + } + + // IDEA-130594 + public void testNormalIndentAfterPropertyWithoutComma() { + doTest("\n"); + } + + // WEB-13675 + public void testIndentWithTabsWhenSmartTabEnabled() { + final CommonCodeStyleSettings.IndentOptions indentOptions = getIndentOptions(); + final CommonCodeStyleSettings.IndentOptions oldSettings = (CommonCodeStyleSettings.IndentOptions)indentOptions.clone(); + indentOptions.TAB_SIZE = 4; + indentOptions.INDENT_SIZE = 4; + indentOptions.USE_TAB_CHARACTER = true; + indentOptions.SMART_TABS = true; + try { + doTest("\n\"baz\""); + } + finally { + indentOptions.copyFrom(oldSettings); + } + } + + // Moved from JavaScript + + // WEB-11600 + public void testEnterWhenPropertiesAlignedOnValue() { + doEnterTestForWeb11600(); + } + + // WEB-11600 + public void testEnterWhenPropertiesAlignedOnValue1() { + doEnterTestForWeb11600(); + } + + private void doEnterTestForWeb11600() { + final JsonCodeStyleSettings settings = getCustomCodeStyleSettings(); + final CommonCodeStyleSettings.IndentOptions indentOptions = getIndentOptions(); + + final int oldPropertyAlignment = settings.PROPERTY_ALIGNMENT; + final int oldIndentSize = indentOptions.INDENT_SIZE; + settings.PROPERTY_ALIGNMENT = JsonCodeStyleSettings.ALIGN_PROPERTY_ON_VALUE; + indentOptions.INDENT_SIZE = 4; + try { + doTest("\n"); + } + finally { + indentOptions.INDENT_SIZE = oldIndentSize; + settings.PROPERTY_ALIGNMENT = oldPropertyAlignment; + } + } +} diff --git a/json/tests/test/com/intellij/json/JsonFoldingTest.java b/json/tests/test/com/intellij/json/JsonFoldingTest.java new file mode 100644 index 00000000..2f21918a --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonFoldingTest.java @@ -0,0 +1,38 @@ +package com.intellij.json; + +/** + * @author Mikhail Golubev + */ +public class JsonFoldingTest extends JsonTestCase { + private void doTest() { + myFixture.testFolding(getTestDataPath() + "/folding/" + getTestName(false) + ".json"); + } + + + public void testArrayFolding() { + doTest(); + } + + public void testObjectFolding() { + doTest(); + } + + public void testCommentaries() { + doTest(); + } + + // Moved from JavaScript + + public void testObjectLiteral2() { + doTest(); + } + + public void testObjectLiteral3() { + doTest(); + } + + public void testObjectLiteral4() { + doTest(); + } + +} diff --git a/json/tests/test/com/intellij/json/JsonFormattingTest.java b/json/tests/test/com/intellij/json/JsonFormattingTest.java new file mode 100644 index 00000000..98f2a005 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonFormattingTest.java @@ -0,0 +1,112 @@ +package com.intellij.json; + +import com.intellij.json.formatter.JsonCodeStyleSettings.PropertyAlignment; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import com.intellij.testFramework.PlatformTestUtil; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonFormattingTest extends JsonTestCase { + + @Override + protected String getTestDataPath() { + return super.getTestDataPath() + "/formatting"; + } + + public void testContainerElementsAlignment() { + doTest(); + } + + public void testBlankLinesStripping() { + doTest(); + } + + public void testSpacesInsertion() { + doTest(); + } + + public void testDoNotThrowFailedToAlignException() { + getCustomCodeStyleSettings().PROPERTY_ALIGNMENT = PropertyAlignment.ALIGN_ON_VALUE.getId(); + doTest(); + } + + public void testWrapping() { + getCodeStyleSettings().setRightMargin(JsonLanguage.INSTANCE, 20); + doTest(); + } + + // WEB-13587 + public void testAlignPropertiesOnColon() { + checkPropertyAlignment(PropertyAlignment.ALIGN_ON_COLON); + } + + // WEB-13587 + public void testAlignPropertiesOnValue() { + checkPropertyAlignment(PropertyAlignment.ALIGN_ON_VALUE); + } + + private void checkPropertyAlignment(@NotNull final PropertyAlignment alignmentType) { + getCustomCodeStyleSettings().PROPERTY_ALIGNMENT = alignmentType.getId(); + doTest(); + } + + public void testChopDownArrays() { + getCustomCodeStyleSettings().ARRAY_WRAPPING = CommonCodeStyleSettings.WRAP_ON_EVERY_ITEM; + getCodeStyleSettings().setRightMargin(JsonLanguage.INSTANCE, 40); + doTest(); + } + + // IDEA-138902 + public void testObjectsWithSingleProperty() { + doTest(); + } + + // Moved from JavaScript + + public void testWeb3830() { + CommonCodeStyleSettings.IndentOptions options = getIndentOptions(); + options.INDENT_SIZE = 8; + options.USE_TAB_CHARACTER = true; + options.TAB_SIZE = 8; + doTest(); + } + + public void testReformatJSon() { + getIndentOptions().INDENT_SIZE = 4; + doTest(); + } + + public void testReformatJSon2() { + getIndentOptions().INDENT_SIZE = 4; + doTest(); + } + + public void testRemoveTrailingCommas() { + doTest(); + } + + public void testReformatIncompleteJson1() { doTest();} + + public void testReformatIncompleteJson2() { doTest();} + + public void testIndentForElements() { doTest();} + public void testNoExtraNewLineByWrap() { doTest();} + + public void testHugeJsonFile() { + // IDEA-195340 bad JSON kills IntelliJ + PlatformTestUtil.startPerformanceTest(getTestName(false), 20000, this::doTest).attempts(1).usesAllCPUCores().assertTiming(); + } + + private void doTest() { + myFixture.configureByFile(getTestName(false) + ".json"); + WriteCommandAction.runWriteCommandAction(null, () -> { + CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(myFixture.getProject()); + codeStyleManager.reformat(myFixture.getFile()); + }); + myFixture.checkResultByFile(getTestName(false) + "_after.json"); + } +} diff --git a/json/tests/test/com/intellij/json/JsonHighlightingTest.java b/json/tests/test/com/intellij/json/JsonHighlightingTest.java new file mode 100644 index 00000000..ab7d42fa --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonHighlightingTest.java @@ -0,0 +1,74 @@ +package com.intellij.json; + +import com.intellij.json.codeinsight.JsonDuplicatePropertyKeysInspection; +import com.intellij.json.codeinsight.JsonStandardComplianceInspection; + +/** + * @author Mikhail Golubev + */ +public class JsonHighlightingTest extends JsonHighlightingTestBase { + + @Override + protected String getExtension() { + return "json"; + } + + private void enableStandardComplianceInspection(boolean checkComments, boolean checkTopLevelValues) { + final JsonStandardComplianceInspection inspection = new JsonStandardComplianceInspection(); + inspection.myWarnAboutComments = checkComments; + inspection.myWarnAboutMultipleTopLevelValues = checkTopLevelValues; + myFixture.enableInspections(inspection); + } + + public void testStringLiterals() { + doTest(); + } + + // IDEA-134372 + public void testComplianceProblemsLiteralTopLevelValueIsAllowed() { + enableStandardComplianceInspection(true, true); + doTest(); + } + + // WEB-16009 + public void testComplianceProblemsMultipleTopLevelValuesAllowed() { + enableStandardComplianceInspection(true, false); + } + + public void testComplianceProblems() { + enableStandardComplianceInspection(true, true); + doTestHighlighting(false, true, true); + } + + public void testDuplicatePropertyKeys() { + myFixture.enableInspections(JsonDuplicatePropertyKeysInspection.class); + doTestHighlighting(false, true, true); + } + + // WEB-13600 + public void testIncompleteFloatingPointLiteralsWithExponent() { + doTestHighlighting(false, false, false); + } + + // Moved from JavaScript + + public void testJSON_with_comment() { + enableStandardComplianceInspection(false, true); + doTestHighlighting(false, true, true); + } + + public void testJSON() { + enableStandardComplianceInspection(true, true); + doTestHighlighting(false, true, true); + } + + public void testTabInString() { + enableStandardComplianceInspection(true, true); + doTestHighlighting(false, true, true); + } + + public void testSemanticHighlighting() { + // WEB-11239 + doTest(); + } +} diff --git a/json/tests/test/com/intellij/json/JsonHighlightingTestBase.java b/json/tests/test/com/intellij/json/JsonHighlightingTestBase.java new file mode 100644 index 00000000..623f314f --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonHighlightingTestBase.java @@ -0,0 +1,14 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.json; + +public abstract class JsonHighlightingTestBase extends JsonTestCase { + protected void doTest() { + doTestHighlighting(true, true, true); + } + + protected abstract String getExtension(); + + protected void doTestHighlighting(boolean checkInfo, boolean checkWeakWarning, boolean checkWarning) { + myFixture.testHighlighting(checkWarning, checkInfo, checkWeakWarning, "/highlighting/" + getTestName(false) + "." + getExtension()); + } +} diff --git a/json/tests/test/com/intellij/json/JsonLexerTest.java b/json/tests/test/com/intellij/json/JsonLexerTest.java new file mode 100644 index 00000000..b531af43 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonLexerTest.java @@ -0,0 +1,34 @@ +package com.intellij.json; + +import com.intellij.lexer.Lexer; +import com.intellij.testFramework.LexerTestCase; + +/** + * @author Konstantin.Ulitin + */ +public class JsonLexerTest extends LexerTestCase { + @Override + protected Lexer createLexer() { + return new JsonLexer(); + } + + @Override + protected String getDirPath() { + return null; + } + + public void testEscapeSlash() { + // WEB-2803 + doTest("[\"\\/\",-1,\"\\n\", 1]", + "[ ('[')\n" + + "DOUBLE_QUOTED_STRING ('\"\\/\"')\n" + + ", (',')\n" + + "NUMBER ('-1')\n" + + ", (',')\n" + + "DOUBLE_QUOTED_STRING ('\"\\n\"')\n" + + ", (',')\n" + + "WHITE_SPACE (' ')\n" + + "NUMBER ('1')\n" + + "] (']')"); + } +} diff --git a/json/tests/test/com/intellij/json/JsonLineMoverTest.java b/json/tests/test/com/intellij/json/JsonLineMoverTest.java new file mode 100644 index 00000000..2deefaee --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonLineMoverTest.java @@ -0,0 +1,92 @@ +package com.intellij.json; + +import com.intellij.openapi.actionSystem.IdeActions; +import com.intellij.openapi.fileEditor.FileDocumentManager; + +/** + * @author Mikhail Golubev + */ +public class JsonLineMoverTest extends JsonTestCase { + private void doTest(boolean down) { + final String testName = getTestName(false); + + if (down) { + myFixture.configureByFile("mover/" + testName + ".json"); + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_DOWN_ACTION); + myFixture.checkResultByFile("mover/" + testName + "_afterDown.json", true); + } + else { + myFixture.configureByFile("mover/" + testName + ".json"); + myFixture.performEditorAction(IdeActions.ACTION_MOVE_STATEMENT_UP_ACTION); + myFixture.checkResultByFile("mover/" + testName + "_afterUp.json", true); + } + } + + private void doTest() { + doTest(false); + FileDocumentManager.getInstance().reloadFromDisk(myFixture.getDocument(myFixture.getFile())); + doTest(true); + } + + public void testLastArrayElementMovedUp() { + doTest(false); + } + + public void testLastObjectPropertyMovedUp() { + doTest(false); + } + + public void testArraySelectionMovedDown() { + doTest(true); + } + + public void testOutOfScopeHasProp() { + doTest(true); + } + + public void testOutOfScopeNoFollowing() { + doTest(true); + } + + public void testStatementSetMovedSameLevelDown() { + doTest(true); + } + + public void testStatementSetMovedSameLevelUp() { + doTest(false); + } + + public void testIntoScope() { + doTest(false); + } + + public void testFromUpperIntoScope() { + doTest(true); + } + + public void testOutsideArray() { + doTest(true); + } + + public void testInsideArray() { + doTest(false); + } + + public void testUpToUpper() { + doTest(false); + } + + public void testObjectSelectionMovedDown() { + doTest(true); + } + + public void testLineCommentariesMovedTogether() { + doTest(true); + } + + // Moved from JavaScript + + public void testWeb_10585() { + doTest(); + } +} diff --git a/json/tests/test/com/intellij/json/JsonLiteralApiTest.java b/json/tests/test/com/intellij/json/JsonLiteralApiTest.java new file mode 100644 index 00000000..1f4725c5 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonLiteralApiTest.java @@ -0,0 +1,111 @@ +package com.intellij.json; + +import com.intellij.json.psi.*; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.TextRange; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JsonLiteralApiTest extends JsonTestCase { + + public static final String ALL_CHARACTER_ESCAPES = "\"\\/\b\f\n\r\t"; + + @NotNull + private JsonStringLiteral createStringLiteralFromText(@NotNull String rawText) { + final JsonArray jsonArray = new JsonElementGenerator(getProject()).createValue("[\n" + rawText + "\n]"); + assertEquals(1, jsonArray.getValueList().size()); + final JsonValue firstElement = jsonArray.getValueList().get(0); + assertInstanceOf(firstElement, JsonStringLiteral.class); + return ((JsonStringLiteral)firstElement); + } + + @NotNull + private JsonStringLiteral createStringLiteralFromContent(@NotNull String unescapedContent) { + return new JsonElementGenerator(getProject()).createStringLiteral(unescapedContent); + } + + private void checkFragments(@NotNull String rawStringLiteral, @NotNull Pair<TextRange, String>... fragments) { + final List<Pair<TextRange, String>> actual = createStringLiteralFromText(rawStringLiteral).getTextFragments(); + final List<Pair<TextRange, String>> expected = Arrays.asList(fragments); + assertEquals(expected, actual); + for (Pair<TextRange, String> fragment : fragments) { + final TextRange range = fragment.getFirst(); + final String unescaped = range.substring(rawStringLiteral); + if (!unescaped.contains("\\")) { + final String escaped = fragment.getSecond(); + assertEquals(String.format("Bad range %s: fragment without escaping differs after decoding", range), escaped, unescaped); + } + } + } + + @NotNull + private static Pair<TextRange, String> fragment(int start, int end, @NotNull String chunk) { + return Pair.create(new TextRange(start, end), chunk); + } + + public void testFragmentSplittingSimpleEscapes() { + checkFragments("\"\\b\\f\\n\\r\\t\\\\\\/\\\"\"", + + fragment(1, 3, "\b"), + fragment(3, 5, "\f"), + fragment(5, 7, "\n"), + fragment(7, 9, "\r"), + fragment(9, 11, "\t"), + fragment(11, 13, "\\"), + fragment(13, 15, "/"), + fragment(15, 17, "\"")); + } + + public void testFragmentSplittingIllegalEscapes() { + checkFragments("\"\\q\\zz\\uBEE\\u\\ ", + + fragment(1, 3, "\\q"), + fragment(3, 5, "\\z"), + fragment(5, 6, "z"), + fragment(6, 11, "\\uBEE"), + fragment(11, 13, "\\u"), + fragment(13, 15, "\\ ")); + } + + public void testFragmentSplittingUnicodeEscapes() { + checkFragments("'foo\\uCAFEBABE\\u0027baz'", + + fragment(1, 4, "foo"), + fragment(4, 10, "\\uCAFE"), + fragment(10, 14, "BABE"), + fragment(14, 20, "\\u0027"), + fragment(20, 23, "baz")); + } + + public void testStringLiteralValue() { + checkStringContent("simple"); + checkStringContent(ALL_CHARACTER_ESCAPES); + checkStringContent("\u043c\u0435\u0434\u0432\u0435\u0434\u044c"); + } + + private void checkStringContent(@NotNull String content) { + assertEquals(content, createStringLiteralFromContent(content).getValue()); + } + + public void testBooleanLiteralValue() { + final JsonElementGenerator generator = new JsonElementGenerator(getProject()); + assertTrue(generator.<JsonBooleanLiteral>createValue("true").getValue()); + assertFalse(generator.<JsonBooleanLiteral>createValue("false").getValue()); + } + + public void testNumberLiteralValue() { + final JsonElementGenerator generator = new JsonElementGenerator(getProject()); + assertEquals(123.0, generator.<JsonNumberLiteral>createValue("123.0").getValue(), 1e-5); + assertEquals(0.1, generator.<JsonNumberLiteral>createValue("0.1").getValue(), 1e-5); + assertEquals(1e+3, generator.<JsonNumberLiteral>createValue("1e3").getValue(), 1e-5); + assertEquals(1e+3, generator.<JsonNumberLiteral>createValue("1e+3").getValue(), 1e-5); + assertEquals(1e-3, generator.<JsonNumberLiteral>createValue("1e-3").getValue(), 1e-5); + assertEquals(1e+3, generator.<JsonNumberLiteral>createValue("1.00e3").getValue(), 1e-5); + assertEquals(1.23e-3, generator.<JsonNumberLiteral>createValue("1.23e-3").getValue(), 1e-5); + } +} diff --git a/json/tests/test/com/intellij/json/JsonLiveTemplateTest.java b/json/tests/test/com/intellij/json/JsonLiveTemplateTest.java new file mode 100644 index 00000000..b9e4dedb --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonLiveTemplateTest.java @@ -0,0 +1,101 @@ +/* + * 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; + +import com.intellij.codeInsight.lookup.Lookup; +import com.intellij.codeInsight.lookup.LookupManager; +import com.intellij.codeInsight.lookup.impl.LookupImpl; +import com.intellij.codeInsight.template.Template; +import com.intellij.codeInsight.template.TemplateContextType; +import com.intellij.codeInsight.template.TemplateManager; +import com.intellij.codeInsight.template.impl.TemplateImpl; +import com.intellij.codeInsight.template.impl.TemplateManagerImpl; +import com.intellij.codeInsight.template.impl.actions.ListTemplatesAction; +import com.intellij.json.liveTemplates.JsonContextType; +import com.intellij.json.liveTemplates.JsonInLiteralsContextType; +import com.intellij.json.liveTemplates.JsonInPropertyKeysContextType; +import com.intellij.openapi.editor.Editor; +import com.intellij.testFramework.fixtures.CodeInsightTestUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonLiveTemplateTest extends JsonTestCase { + + private boolean isApplicableContextUnderCaret(@NotNull String text, Class<? extends TemplateContextType> ...contextsToDisable) { + myFixture.configureByText(JsonFileType.INSTANCE, text); + final Template template = createJsonTemplate("foo", "foo", "[42]", contextsToDisable); + return TemplateManagerImpl.isApplicable(myFixture.getFile(), myFixture.getCaretOffset(), (TemplateImpl)template); + } + + @SuppressWarnings("SameParameterValue") + @NotNull + private Template createJsonTemplate(@NotNull String name, @NotNull String group, @NotNull String text, Class<? extends TemplateContextType> ...contextsToDisable) { + final TemplateManager templateManager = TemplateManager.getInstance(getProject()); + final Template template = templateManager.createTemplate(name, group, text); + + TemplateContextType context = ContainerUtil.findInstance(TemplateContextType.EP_NAME.getExtensions(), JsonContextType.class); + assertNotNull(context); + ((TemplateImpl)template).getTemplateContext().setEnabled(context, true); + for (Class<? extends TemplateContextType> ctx: contextsToDisable) { + context = ContainerUtil.findInstance(TemplateContextType.EP_NAME.getExtensions(), ctx); + assertNotNull(context); + ((TemplateImpl)template).getTemplateContext().setEnabled(context, false); + } + + CodeInsightTestUtil.addTemplate(template, myFixture.getTestRootDisposable()); + return template; + } + + public void testNotExpandableInsideStringLiteral() { + assertFalse(isApplicableContextUnderCaret("{\"bar\": \"fo<caret>o\"}", + JsonInLiteralsContextType.class)); + } + + public void testNotExpandableInsidePropertyKey() { + assertFalse(isApplicableContextUnderCaret("{fo<caret>o: \"bar\"}", + JsonInPropertyKeysContextType.class)); + } + + public void testNotExpandableInsidePropertyKeyWithWhitespace() { + assertFalse(isApplicableContextUnderCaret("{fo<caret>o : \"bar\"}", + JsonInPropertyKeysContextType.class)); + } + + public void testExpandableAtTopLevel() { + assertTrue(isApplicableContextUnderCaret("fo<caret>o")); + } + + public void testExpandableInObjectLiteral() { + assertTrue(isApplicableContextUnderCaret("{fo<caret>o}")); + } + + public void testCustomTemplateExpansion() { + final String templateContent = "{\n" + + " \"foo\": \"$1$\"\n" + + "}"; + createJsonTemplate("foo", "foo", templateContent); + myFixture.configureByText(JsonFileType.INSTANCE, "foo<caret>"); + final Editor editor = myFixture.getEditor(); + new ListTemplatesAction().actionPerformedImpl(getProject(), editor); + final LookupImpl lookup = (LookupImpl)LookupManager.getActiveLookup(editor); + assertNotNull(lookup); + lookup.finishLookup(Lookup.NORMAL_SELECT_CHAR); + myFixture.checkResult(templateContent.replaceAll("\\$.*?\\$", "")); + } +} diff --git a/json/tests/test/com/intellij/json/JsonNavigationTest.java b/json/tests/test/com/intellij/json/JsonNavigationTest.java new file mode 100644 index 00000000..da610a6b --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonNavigationTest.java @@ -0,0 +1,20 @@ +package com.intellij.json; + +import com.intellij.ide.actions.CopyReferenceAction; +import com.intellij.json.psi.JsonProperty; +import com.intellij.psi.PsiElement; + +/** + * @author Mikhail Golubev + */ +public class JsonNavigationTest extends JsonTestCase { + + // WEB-14048 + public void testCopyReference() { + myFixture.configureByFile("navigation/" + getTestName(false) + ".json"); + final PsiElement element = myFixture.getElementAtCaret(); + assertInstanceOf(element, JsonProperty.class); + final String qualifiedName = CopyReferenceAction.elementToFqn(element); + assertEquals("foo.bar[0][0].baz", qualifiedName); + } +} diff --git a/json/tests/test/com/intellij/json/JsonParsingTest.java b/json/tests/test/com/intellij/json/JsonParsingTest.java new file mode 100644 index 00000000..14f44e2d --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonParsingTest.java @@ -0,0 +1,105 @@ +package com.intellij.json; + +import com.intellij.testFramework.ParsingTestCase; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.testFramework.TestDataPath; + +/** + * @author Mikhail Golubev + */ +@TestDataPath("$CONTENT_ROOT/testData/psi/") +public class JsonParsingTest extends ParsingTestCase { + public JsonParsingTest() { + super("psi", "json", new JsonParserDefinition()); + } + + @Override + protected String getTestDataPath() { + return PlatformTestUtil.getCommunityPath() + "/json/tests/testData"; + } + + private void doTest() { + doTest(true); + } + + public void testKeywords() { + doTest(); + } + + public void testNestedArrayLiterals() { + doTest(); + } + + public void testNestedObjectLiterals() { + doTest(); + } + + public void testTopLevelStringLiteral() { + doTest(); + } + + public void testStringLiterals() { + doTest(); + } + + public void testComments() { + doTest(); + } + + public void testIncompleteObjectProperties() { + doTest(); + } + + public void testMissingCommaBetweenArrayElements() { + doTest(); + } + + public void testMissingCommaBetweenObjectProperties() { + doTest(); + } + + public void testNonStandardPropertyKeys() { + doTest(); + } + + public void testTrailingCommas() { + doTest(); + } + + // WEB-13600 + public void testNumberLiterals() { + doTest(); + } + + public void testExtendedIdentifierToken() { + doTest(); + } + + // Moved from JavaScript + + public void testSimple1() { + doTest(); + } + + public void testSimple2() { + doTest(); + } + + public void testSimple4() { + doTest(); + } + + // TODO: ask about these tests + //public void testSimple3() { + // doTest(); + //} + // + //public void testReal1() { + // doTest(); + //} + // + //public void testReal2() { + // doTest(); + + //} +} diff --git a/json/tests/test/com/intellij/json/JsonPsiUtilTest.java b/json/tests/test/com/intellij/json/JsonPsiUtilTest.java new file mode 100644 index 00000000..0d68f4e3 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonPsiUtilTest.java @@ -0,0 +1,69 @@ +/* + * 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; + +import com.intellij.json.psi.JsonElementGenerator; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonProperty; +import com.intellij.json.psi.JsonPsiUtil; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.psi.PsiElement; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; + +/** + * @author Mikhail Golubev + */ +public class JsonPsiUtilTest extends JsonTestCase { + public void testAddPropertyInEmptyLiteral() { + checkAddProperty("{}", "{\"foo\": null}", true); + } + + public void testAddPropertyInEmptyUnclosedLiteral() { + checkAddProperty("{", "{\"foo\": null", true); + } + + public void testAddPropertyFirst() { + checkAddProperty("{\"bar\": 42}", "{\"foo\": null, \"bar\": 42}", true); + } + + public void testAddPropertyLast() { + checkAddProperty("{\"bar\": 42}", "{\"bar\": 42, \"foo\": null}", false); + } + + private void checkAddProperty(@NotNull String before, @NotNull String after, final boolean first) { + getCustomCodeStyleSettings().OBJECT_WRAPPING = CommonCodeStyleSettings.DO_NOT_WRAP; + myFixture.configureByText(JsonFileType.INSTANCE, before); + final PsiElement atCaret = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + final JsonObject jsonObject = PsiTreeUtil.getParentOfType(atCaret, JsonObject.class); + assertNotNull(jsonObject); + WriteCommandAction.runWriteCommandAction(getProject(), () -> { + JsonPsiUtil.addProperty(jsonObject, new JsonElementGenerator(getProject()).createProperty("foo", "null"), first); + }); + myFixture.checkResult(after); + } + + public void testGetOtherSiblingPropertyNames() { + myFixture.configureByText(JsonFileType.INSTANCE, "{\"firs<caret>t\" : 1, \"second\" : 2}"); + PsiElement atCaret = myFixture.getFile().findElementAt(myFixture.getCaretOffset()); + JsonProperty property = PsiTreeUtil.getParentOfType(atCaret, JsonProperty.class); + assertNotNull(property); + assertEquals(Collections.singleton("second"), JsonPsiUtil.getOtherSiblingPropertyNames(property)); + } +} diff --git a/json/tests/test/com/intellij/json/JsonQuickFixTest.java b/json/tests/test/com/intellij/json/JsonQuickFixTest.java new file mode 100644 index 00000000..54c4eda8 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonQuickFixTest.java @@ -0,0 +1,54 @@ +package com.intellij.json; + +import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.codeInspection.LocalInspectionTool; +import com.intellij.json.codeinsight.JsonStandardComplianceInspection; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +public class JsonQuickFixTest extends JsonTestCase { + + protected void doTest(@NotNull Class<? extends LocalInspectionTool> inspectionClass, @NotNull String hint) { + final String testFileName = "quickfix/" + getTestName(false); + myFixture.enableInspections(inspectionClass); + myFixture.configureByFile(testFileName + ".json"); + myFixture.checkHighlighting(true, false, false); + final IntentionAction intentionAction = myFixture.getAvailableIntention(hint); + assertNotNull(intentionAction); + myFixture.launchAction(intentionAction); + myFixture.checkResultByFile(testFileName + "_after.json", true); + } + + public void testWrapInDoubleQuotes() { + checkWrapInDoubleQuotes("{n<caret>ull: false}", "{\"null\": false}"); + checkWrapInDoubleQuotes("{t<caret>rue: false}", "{\"true\": false}"); + checkWrapInDoubleQuotes("{4<caret>2: false}", "{\"42\": false}"); + checkWrapInDoubleQuotes("{fo<caret>o: false}", "{\"foo\": false}"); + checkWrapInDoubleQuotes("{'fo<caret>o': false}", "{\"foo\": false}"); + checkWrapInDoubleQuotes("'foo\\\"", "\"foo\\\"\""); + checkWrapInDoubleQuotes("{\"foo\": b<caret>ar}", "{\"foo\": \"bar\"}"); + checkWrapInDoubleQuotes("{\"foo\": 'b<caret>ar'}", "{\"foo\": \"bar\"}"); + checkWrapInDoubleQuotes("'foo\\n\\'\"\\\\\\\"bar", "\"foo\\n'\\\"\\\\\\\"bar\""); + } + + private void checkWrapInDoubleQuotes(@NotNull String before, @NotNull String after) { + myFixture.configureByText(JsonFileType.INSTANCE, before); + myFixture.enableInspections(JsonStandardComplianceInspection.class); + final IntentionAction intentionAction = myFixture.getAvailableIntention(JsonBundle.message("quickfix.add.double.quotes.desc")); + assertNotNull(intentionAction); + myFixture.launchAction(intentionAction); + myFixture.checkResult(after); + } + + // Moved from JavaScript + + public void testJSON2() { + doTest(JsonStandardComplianceInspection.class, JsonBundle.message("quickfix.add.double.quotes.desc")); + } + + public void testJSON3() { + doTest(JsonStandardComplianceInspection.class, JsonBundle.message("quickfix.add.double.quotes.desc")); + } +} diff --git a/json/tests/test/com/intellij/json/JsonRenameTest.java b/json/tests/test/com/intellij/json/JsonRenameTest.java new file mode 100644 index 00000000..76b5eea0 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonRenameTest.java @@ -0,0 +1,28 @@ +package com.intellij.json; + +/** + * @author Mikhail Golubev + */ +public class JsonRenameTest extends JsonTestCase { + + private void doTest(final String newName) { + myFixture.configureByFile("rename/" + getTestName(false) + ".json"); + myFixture.renameElementAtCaret(newName); + myFixture.checkResultByFile("rename/" + getTestName(false) + "_after.json"); + } + + public void testPropertyNameRequiresEscaping() { + doTest("\"/\\\b\f\n\r\t\0\u001B'"); + } + + // Moved from JavaScript + + public void testDuplicateProperties() { + doTest("aaa2"); + } + + public void testDuplicatePropertiesQuotedName() { + doTest("\"aaa2\""); + } + +} diff --git a/json/tests/test/com/intellij/json/JsonSmartEnterTest.java b/json/tests/test/com/intellij/json/JsonSmartEnterTest.java new file mode 100644 index 00000000..ba44915b --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonSmartEnterTest.java @@ -0,0 +1,43 @@ +package com.intellij.json; + +import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessor; +import com.intellij.codeInsight.editorActions.smartEnter.SmartEnterProcessors; +import com.intellij.openapi.application.Result; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.editor.Editor; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * @author Mikhail Golubev + */ +public class JsonSmartEnterTest extends JsonTestCase { + public void doTest() { + myFixture.configureByFile("smartEnter/" + getTestName(false) + ".json"); + final List<SmartEnterProcessor> processors = SmartEnterProcessors.INSTANCE.forKey(JsonLanguage.INSTANCE); + WriteCommandAction.runWriteCommandAction(myFixture.getProject(), () -> { + final Editor editor = myFixture.getEditor(); + for (SmartEnterProcessor processor : processors) { + processor.process(myFixture.getProject(), editor, myFixture.getFile()); + } + }); + myFixture.checkResultByFile("smartEnter/" + getTestName(false) + "_after.json", true); + } + + public void testCommaInsertedAfterArrayElement() { + doTest(); + } + + public void testCommaInsertedAfterProperty() { + doTest(); + } + + public void testCommaInsertedAfterPropertyWithMultilineValue() { + doTest(); + } + + public void testColonInsertedAfterPropertyKey() { + doTest(); + } +} diff --git a/json/tests/test/com/intellij/json/JsonSpellcheckerTest.java b/json/tests/test/com/intellij/json/JsonSpellcheckerTest.java new file mode 100644 index 00000000..eed6d8eb --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonSpellcheckerTest.java @@ -0,0 +1,73 @@ +package com.intellij.json; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.IdeActions; +import com.intellij.openapi.extensions.AreaPicoContainer; +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.spellchecker.inspections.SpellCheckingInspection; +import com.intellij.util.containers.Predicate; +import com.jetbrains.jsonSchema.JsonSchemaTestProvider; +import com.jetbrains.jsonSchema.JsonSchemaTestServiceImpl; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; + +/** + * @author Mikhail Golubev + */ +public class JsonSpellcheckerTest extends JsonTestCase { + + private void doTest() { + myFixture.enableInspections(SpellCheckingInspection.class); + myFixture.configureByFile(getTestName(false) + ".json"); + myFixture.checkHighlighting(true, false, true); + } + + public void testEscapeAwareness() { + doTest(); + } + + public void testSimple() { + doTest(); + } + + protected Predicate<VirtualFile> getAvailabilityPredicate() { + return file -> file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf( + JsonLanguage.INSTANCE); + } + + public void testWithSchema() { + PsiFile[] files = myFixture.configureByFiles(getTestName(false) + ".json", "Schema.json"); + JsonSchemaTestServiceImpl.setProvider(new JsonSchemaTestProvider(files[1].getVirtualFile(), + getAvailabilityPredicate())); + AreaPicoContainer container = Extensions.getArea(getProject()).getPicoContainer(); + String key = JsonSchemaService.class.getName(); + container.unregisterComponent(key); + container.registerComponentImplementation(key, JsonSchemaTestServiceImpl.class); + Disposer.register(getTestRootDisposable(), new Disposable() { + @Override + public void dispose() { + JsonSchemaTestServiceImpl.setProvider(null); + } + }); + myFixture.enableInspections(SpellCheckingInspection.class); + myFixture.checkHighlighting(true, false, true); + } + + // WEB-31894 EA-117068 + public void testAfterModificationOfStringLiteralWithEscaping() { + myFixture.configureByFile(getTestName(false) + ".json"); + myFixture.enableInspections(SpellCheckingInspection.class); + myFixture.checkHighlighting(); + myFixture.performEditorAction(IdeActions.ACTION_EDITOR_BACKSPACE); + myFixture.performEditorAction(IdeActions.ACTION_EDITOR_BACKSPACE); + myFixture.doHighlighting(); + } + + @Override + protected String getTestDataPath() { + return super.getTestDataPath() + "/spellchecker"; + } +} diff --git a/json/tests/test/com/intellij/json/JsonStructureViewTest.java b/json/tests/test/com/intellij/json/JsonStructureViewTest.java new file mode 100644 index 00000000..4c70bafc --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonStructureViewTest.java @@ -0,0 +1,165 @@ +package com.intellij.json; + +import com.intellij.icons.AllIcons; +import com.intellij.ide.structureView.StructureViewBuilder; +import com.intellij.ide.structureView.StructureViewModel; +import com.intellij.ide.structureView.newStructureView.StructureViewComponent; +import com.intellij.ide.util.treeView.smartTree.TreeElement; +import com.intellij.lang.LanguageStructureViewBuilder; +import com.intellij.navigation.ItemPresentation; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.util.Disposer; +import com.intellij.util.Consumer; +import com.intellij.util.PlatformIcons; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.testFramework.PlatformTestUtil.assertTreeEqual; +import static com.intellij.testFramework.PlatformTestUtil.expandAll; + +/** + * @author Mikhail Golubev + */ +public class JsonStructureViewTest extends JsonTestCase { + + private void doTest(final String expected) { + myFixture.configureByFile(getTestName(false) + ".json"); + myFixture.testStructureView(svc -> { + expandAll(svc.getTree()); + assertTreeEqual(svc.getTree(), expected); + }); + } + + private void doTestTreeStructure(@NotNull Consumer<StructureViewModel> consumer) { + myFixture.configureByFile(getTestName(false) + ".json"); + final StructureViewBuilder builder = LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(myFixture.getFile()); + assertNotNull(builder); + StructureViewComponent component = null; + try { + final FileEditor editor = FileEditorManager.getInstance(getProject()).getSelectedEditor(myFixture.getFile().getVirtualFile()); + component = (StructureViewComponent)builder.createStructureView(editor, myFixture.getProject()); + final StructureViewModel model = component.getTreeModel(); + consumer.consume(model); + } + finally { + if (component != null) { + Disposer.dispose(component); + } + } + } + + public void testPropertyOrderPreserved() { + doTest("-PropertyOrderPreserved.json\n" + + " ccc\n" + + " bbb\n" + + " -aaa\n" + + " eee\n" + + " ddd\n"); + } + + // IDEA-127119 + public void testObjectsInsideArraysAreShown() { + doTest("-ObjectsInsideArraysAreShown.json\n" + + " aProp\n" + + " -node1\n" + + " anotherProp\n" + + " subNode1\n" + + " subNode2\n" + + " -node2\n" + + " -object\n" + + " -subNode2\n" + + " -object\n" + + " someNode\n" + + " -node3\n" + + " -object\n" + + " prop1\n" + + " prop2\n" + + " someFlag\n" + + " -array\n" + + " -object\n" + + " arrProp1\n" + + " -array2\n" + + " -object\n" + + " arr2Prop1\n" + + " arr2Prop2\n" + + " -array3\n" + + " -object\n" + + " prop1\n" + + " prop2\n"); + } + + // IDEA-131502 + public void testArrayNodesAreShownIfNecessary() { + doTest("-ArrayNodesAreShownIfNecessary.json\n" + + " -array\n" + + " -object\n" + + " nestedObject\n" + + " -array\n" + + " -array\n" + + " -object\n" + + " deepNestedObject\n" + + " -object\n" + + " siblingObject\n"); + } + + // IDEA-167017 + public void testValuesOfScalarPropertiesAreShown() { + doTestTreeStructure(model -> { + final TreeElement[] children = model.getRoot().getChildren(); + assertSize(6, children); + final ItemPresentation booleanNode = children[0].getPresentation(); + assertEquals("boolean", booleanNode.getPresentableText()); + assertEquals("true", booleanNode.getLocationString()); + + final ItemPresentation nullNode = children[1].getPresentation(); + assertEquals("nullable", nullNode.getPresentableText()); + assertEquals("null", nullNode.getLocationString()); + + final ItemPresentation numNode = children[2].getPresentation(); + assertEquals("number", numNode.getPresentableText()); + assertEquals("42", numNode.getLocationString()); + + final ItemPresentation stringNode = children[3].getPresentation(); + assertEquals("string", stringNode.getPresentableText()); + assertEquals("\"foo\"", stringNode.getLocationString()); + + final ItemPresentation arrayNode = children[4].getPresentation(); + assertEquals("array", arrayNode.getPresentableText()); + assertNull(arrayNode.getLocationString()); + + final ItemPresentation objectNode = children[5].getPresentation(); + assertEquals("object", objectNode.getPresentableText()); + assertNull(objectNode.getLocationString()); + + final TreeElement[] nestedChildren = children[5].getChildren(); + assertSize(1, nestedChildren); + final ItemPresentation subStringNode = nestedChildren[0].getPresentation(); + assertEquals("foo", subStringNode.getPresentableText()); + assertEquals("\"bar\"", subStringNode.getLocationString()); + }); + } + + // Moved from JavaScript + + public void testSimpleStructure() { + doTestTreeStructure(model -> { + TreeElement[] children = model.getRoot().getChildren(); + assertEquals(2, children.length); + assertEquals("aaa", children[0].getPresentation().getPresentableText()); + assertEquals(PlatformIcons.PROPERTY_ICON, children[0].getPresentation().getIcon(false)); + assertEquals("bbb", children[1].getPresentation().getPresentableText()); + assertEquals(AllIcons.Json.Property_braces, children[1].getPresentation().getIcon(false)); + + children = children[1].getChildren(); + assertEquals(1, children.length); + assertEquals("ccc", children[0].getPresentation().getPresentableText()); + assertEquals(PlatformIcons.PROPERTY_ICON, children[0].getPresentation().getIcon(false)); + }); + } + + @NotNull + @Override + public String getBasePath() { + return super.getBasePath() + "/structureView"; + } +} diff --git a/json/tests/test/com/intellij/json/JsonSurroundWithTest.java b/json/tests/test/com/intellij/json/JsonSurroundWithTest.java new file mode 100644 index 00000000..061d2cf0 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonSurroundWithTest.java @@ -0,0 +1,56 @@ +package com.intellij.json; + +import com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler; +import com.intellij.json.surroundWith.JsonWithArrayLiteralSurrounder; +import com.intellij.json.surroundWith.JsonWithObjectLiteralSurrounder; +import com.intellij.json.surroundWith.JsonWithQuotesSurrounder; +import com.intellij.lang.surroundWith.Surrounder; + +/** + * @author Mikhail Golubev + */ +public class JsonSurroundWithTest extends JsonTestCase { + private void doTest(Surrounder surrounder) { + myFixture.configureByFile("/surround/" + getTestName(false) + ".json"); + SurroundWithHandler.invoke(myFixture.getProject(), myFixture.getEditor(), myFixture.getFile(), surrounder); + myFixture.checkResultByFile("/surround/" + getTestName(false) + "_after.json", true); + } + + public void testSingleValue() { + doTest(new JsonWithObjectLiteralSurrounder()); + } + + public void testSingleProperty() { + doTest(new JsonWithObjectLiteralSurrounder()); + } + + public void testMultipleProperties() { + doTest(new JsonWithObjectLiteralSurrounder()); + } + + public void testCannotSurroundPropertyKey() { + doTest(new JsonWithObjectLiteralSurrounder()); + } + + public void testArrayLiteral() { + doTest(new JsonWithArrayLiteralSurrounder()); + } + + public void testMultipleValuesIntoArray() { + doTest(new JsonWithArrayLiteralSurrounder()); + } + + public void testQuotes() { + doTest(new JsonWithQuotesSurrounder()); + } + + public void testMultipleValuesIntoString() { + doTest(new JsonWithQuotesSurrounder()); + } + + // Moved from JavaScript + + public void testObjectLiteral() { + doTest(new JsonWithObjectLiteralSurrounder()); + } +} diff --git a/json/tests/test/com/intellij/json/JsonTestCase.java b/json/tests/test/com/intellij/json/JsonTestCase.java new file mode 100644 index 00000000..b231ba81 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonTestCase.java @@ -0,0 +1,50 @@ +// 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.formatter.JsonCodeStyleSettings; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import com.intellij.testFramework.TestDataPath; +import com.intellij.testFramework.TestLoggerFactory; +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; + +/** + * @author Mikhail Golubev + */ +@TestDataPath("$CONTENT_ROOT/testData") +public abstract class JsonTestCase extends LightCodeInsightFixtureTestCase { + static { + Logger.setFactory(TestLoggerFactory.class); + } + + @NotNull + protected CodeStyleSettings getCodeStyleSettings() { + return CodeStyleSettingsManager.getSettings(getProject()); + } + + @NotNull + protected CommonCodeStyleSettings getCommonCodeStyleSettings() { + return getCodeStyleSettings().getCommonSettings(JsonLanguage.INSTANCE); + } + + @NotNull + protected JsonCodeStyleSettings getCustomCodeStyleSettings() { + return getCodeStyleSettings().getCustomSettings(JsonCodeStyleSettings.class); + } + + @NotNull + protected CommonCodeStyleSettings.IndentOptions getIndentOptions() { + final CommonCodeStyleSettings.IndentOptions options = getCommonCodeStyleSettings().getIndentOptions(); + assertNotNull(options); + return options; + } + + @Override + @NotNull + public String getBasePath() { + return "/json/tests/testData"; + } +}
\ No newline at end of file diff --git a/json/tests/test/com/intellij/json/JsonTypingHandlingTest.java b/json/tests/test/com/intellij/json/JsonTypingHandlingTest.java new file mode 100644 index 00000000..6fffef05 --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonTypingHandlingTest.java @@ -0,0 +1,118 @@ +// 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 org.jetbrains.annotations.NotNull; + +public class JsonTypingHandlingTest extends JsonTestCase { + private void doTestEnter(@NotNull final String before, @NotNull final String expected) { + doTypingTest('\n', before, expected, "json"); + } + private void doTestLBrace(@NotNull final String before, @NotNull final String expected) { + doTypingTest('{', before, expected, "json"); + } + private void doTestLBracket(@NotNull final String before, @NotNull final String expected) { + doTypingTest('[', before, expected, "json"); + } + private void doTestQuote(@NotNull final String before, @NotNull final String expected) { + doTypingTest('"', before, expected, "json"); + } + private void doTestColon(@NotNull final String before, @NotNull final String expected) { + doTypingTest(':', before, expected, "json"); + } + + @SuppressWarnings("SameParameterValue") + private void doTypingTest(char c, + @NotNull String before, + @NotNull String expected, + @NotNull String extension) { + myFixture.configureByText("test." + extension, before); + myFixture.type(c); + myFixture.checkResult(expected); + } + + // JsonEnterHandler + public void testEnterAfterProperty() { + doTestEnter("{\"a\": true<caret>}", "{\"a\": true,\n}"); + } + public void testEnterMidProperty() { + doTestEnter("{\"a\": tr<caret>ue}", "{\"a\": true,\n}"); + } + public void testEnterMidObjectNoFollowing() { + doTestEnter("{\"a\": {<caret>}}", "{\"a\": {\n \n}}"); + } + public void testEnterMidObjectWithFollowing() { + doTestEnter("{\"a\": {<caret>} \"b\": 5}", "{\"a\": {\n \n}, \"b\": 5}"); + } + public void testEnterAfterObject() { + doTestEnter("{\"a\": {}<caret>}", "{\"a\": {},\n}"); + } + + // JsonTypedHandler + public void testAutoCommaAfterLBraceInArray() { + doTestLBrace("[ <caret> {\"a\": 5} ]", "[ {}, {\"a\": 5} ]"); + } + public void testAutoCommaAfterLBracketInArray() { + doTestLBracket("[ <caret> {\"a\": 5} ]", "[ [], {\"a\": 5} ]"); + } + public void testAutoCommaAfterQuoteInArray() { + doTestQuote("[ <caret> {\"a\": 5} ]", "[ \"\", {\"a\": 5} ]"); + } + public void testAutoCommaAfterLBraceInObject() { + doTestLBrace("{ \"x\": <caret> \"y\": {\"a\": 5} }", "{ \"x\": {}, \"y\": {\"a\": 5} }"); + } + public void testAutoCommaAfterLBracketInObject() { + doTestLBracket("{ \"x\": <caret> \"y\": {\"a\": 5} }", "{ \"x\": [], \"y\": {\"a\": 5} }"); + } + public void testAutoCommaAfterQuoteInObject() { + doTestQuote("{ \"x\": <caret> \"y\": {\"a\": 5} }", "{ \"x\": \"\", \"y\": {\"a\": 5} }"); + } + public void testAutoQuotesForPropName() { + doTestColon( "{ x<caret>}", "{\n" + + " \"x\": <caret>\n" + + "}"); + } + public void testAutoQuotesForPropNameFalse1() { + doTestColon( "{ \"x\"<caret>}", "{ \"x\": <caret>}"); + } + public void testAutoQuotesForPropNameFalse2() { + doTestColon( "{ \"x<caret>\"}", "{ \"x:<caret>\"}"); + } + public void testAutoQuotesAndWhitespaceFollowingNewline() { + doTestColon("{\n" + + " \"a\": 5,\n" + + " x<caret>\n" + + " \"q\": 8\n" + + "}", + "{\n" + + " \"a\": 5,\n" + + " \"x\": <caret>\n" + + " \"q\": 8\n" + + "}"); + } + + public void testAutoWhitespaceErasure() { + myFixture.configureByText("test.json", "{a<caret>}"); + myFixture.type(":"); + myFixture.type(" "); + myFixture.checkResult("{\n" + + " \"a\": <caret>\n" + + "}"); + } + + public void testPairedSingleQuote() { + doTypingTest('\'', "{<caret>}", "{'<caret>'}", "json"); + } + public void testPairedSingleQuote2() { + doTypingTest('\'', "{\n" + + " \"rules\": {\n" + + " \"at-rule-no-vendor-prefix\": null,\n" + + " <caret>\n" + + " }\n" + + "}", "{\n" + + " \"rules\": {\n" + + " \"at-rule-no-vendor-prefix\": null,\n" + + " '<caret>'\n" + + " }\n" + + "}", "json"); + } +} diff --git a/json/tests/test/com/intellij/json/JsonWordSelectionTest.java b/json/tests/test/com/intellij/json/JsonWordSelectionTest.java new file mode 100644 index 00000000..5378cf1a --- /dev/null +++ b/json/tests/test/com/intellij/json/JsonWordSelectionTest.java @@ -0,0 +1,17 @@ +package com.intellij.json; + +import com.intellij.testFramework.fixtures.CodeInsightTestUtil; + +/** + * @author Mikhail Golubev + */ +public class JsonWordSelectionTest extends JsonTestCase { + + private void doTest() { + CodeInsightTestUtil.doWordSelectionTestOnDirectory(myFixture, "selectWord/" + getTestName(false), "json"); + } + + public void testEscapeAwareness() { + doTest(); + } +} diff --git a/json/tests/test/com/intellij/json/json5/Json5HighlightingTest.java b/json/tests/test/com/intellij/json/json5/Json5HighlightingTest.java new file mode 100644 index 00000000..d4b7ffcb --- /dev/null +++ b/json/tests/test/com/intellij/json/json5/Json5HighlightingTest.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.json5; + +import com.intellij.json.JsonHighlightingTestBase; +import com.intellij.json.json5.codeinsight.Json5StandardComplianceInspection; + +public class Json5HighlightingTest extends JsonHighlightingTestBase { + + @Override + protected String getExtension() { + return "json5"; + } + + public void testJSON5() { + myFixture.enableInspections(new Json5StandardComplianceInspection()); + doTestHighlighting(false, true, true); + } +} diff --git a/json/tests/test/com/intellij/json/json5/Json5ParsingTest.java b/json/tests/test/com/intellij/json/json5/Json5ParsingTest.java new file mode 100644 index 00000000..a3fe4b95 --- /dev/null +++ b/json/tests/test/com/intellij/json/json5/Json5ParsingTest.java @@ -0,0 +1,27 @@ +// 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.testFramework.ParsingTestCase; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.testFramework.TestDataPath; + +@TestDataPath("$CONTENT_ROOT/testData/psi/") +public class Json5ParsingTest extends ParsingTestCase { + public Json5ParsingTest() { + super("psi", "json5", new Json5ParserDefinition(), new JsonParserDefinition()); + } + + @Override + protected String getTestDataPath() { + return PlatformTestUtil.getCommunityPath() + "/json/tests/testData"; + } + + private void doTest() { + doTest(true); + } + + public void testJson5Syntax() { + doTest(); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonBySchemaDocumentationBaseTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonBySchemaDocumentationBaseTest.java new file mode 100644 index 00000000..f97a0f5f --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonBySchemaDocumentationBaseTest.java @@ -0,0 +1,89 @@ +/* + * 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.codeInsight.documentation.DocumentationManager; +import com.intellij.json.JsonLanguage; +import com.intellij.lang.LanguageDocumentation; +import com.intellij.lang.documentation.DocumentationProvider; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiUtilBase; +import com.jetbrains.jsonSchema.impl.JsonSchemaDocumentationProvider; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; + +import java.util.ArrayList; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public abstract class JsonBySchemaDocumentationBaseTest extends JsonSchemaHeavyAbstractTest { + protected void doTest(boolean hasDoc, String extension) throws Exception { + final JsonSchemaDocumentationProvider provider = new JsonSchemaDocumentationProvider(); + LanguageDocumentation.INSTANCE.addExplicitExtension(JsonLanguage.INSTANCE, provider); + + try { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final ArrayList<UserDefinedJsonSchemaConfiguration.Item> patterns = new ArrayList<>(); + patterns.add(new UserDefinedJsonSchemaConfiguration.Item(getTestName(true) + "*", true, false)); + addSchema( + new UserDefinedJsonSchemaConfiguration("testDoc", JsonSchemaVersion.SCHEMA_4, + moduleDir + "/" + getTestName(true) + "Schema.json", false, + patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/" + getTestName(true) + "." + extension, "/" + getTestName(true) + "Schema.json"); + } + + @Override + public void doCheck() { + final PsiElement psiElement = PsiUtilBase.getElementAtCaret(myEditor); + Assert.assertNotNull(psiElement); + assertDocumentation(psiElement, psiElement, hasDoc); + } + }); + } finally { + LanguageDocumentation.INSTANCE.removeExplicitExtension(JsonLanguage.INSTANCE, provider); + JsonSchemaTestServiceImpl.setProvider(null); + } + } + + protected void assertDocumentation(@NotNull PsiElement docElement, @NotNull PsiElement context, boolean shouldHaveDoc) { + DocumentationProvider documentationProvider = DocumentationManager.getProviderFromElement(context); + String inlineDoc = documentationProvider.generateDoc(docElement, context); + String quickNavigate = documentationProvider.getQuickNavigateInfo(docElement, context); + checkExpectedDoc(shouldHaveDoc, inlineDoc, false); + checkExpectedDoc(shouldHaveDoc, quickNavigate, true); + } + + private void checkExpectedDoc(boolean shouldHaveDoc, String inlineDoc, boolean preferShort) { + if (shouldHaveDoc) { + assertNotNull("inline help is null", inlineDoc); + } + else { + assertNull("inline help is not null", inlineDoc); + } + if (shouldHaveDoc) { + assertSameLinesWithFile(getTestDataPath() + "/" + getTestName(true) + (preferShort ? "_short.html" : ".html"), inlineDoc); + } + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java new file mode 100644 index 00000000..ec3e0bd2 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaCrossReferencesTest.java @@ -0,0 +1,892 @@ +// 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.lookup.LookupElement; +import com.intellij.codeInsight.lookup.impl.LookupImpl; +import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction; +import com.intellij.json.JsonFileType; +import com.intellij.json.psi.*; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.extensions.AreaPicoContainer; +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.Trinity; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiReference; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ObjectUtils; +import com.intellij.util.containers.ContainerUtil; +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; +import com.jetbrains.jsonSchema.extension.JsonSchemaProjectSelfProviderFactory; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.JsonSchemaObject; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection; +import com.jetbrains.jsonSchema.schemaFile.TestJsonSchemaMappingsProjectConfiguration; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Irina.Chernushina on 3/28/2016. + */ +public class JsonSchemaCrossReferencesTest extends JsonSchemaHeavyAbstractTest { + private final static String BASE_PATH = "/tests/testData/jsonSchema/crossReferences"; + + @Override + protected String getBasePath() { + return BASE_PATH; + } + + public void testJsonSchemaCrossReferenceCompletion() throws Exception { + skeleton(new Callback() { + @Override + public void doCheck() { + checkCompletion("\"one\"", "\"two\""); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/completion.json", "/baseSchema.json", "/inheritedSchema.json"); + } + + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + + final UserDefinedJsonSchemaConfiguration base = + new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, moduleDir + "/baseSchema.json", false, Collections.emptyList()); + addSchema(base); + + final UserDefinedJsonSchemaConfiguration inherited + = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, moduleDir + "/inheritedSchema.json", false, + Collections + .singletonList(new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)) + ); + + addSchema(inherited); + } + }); + } + + private void checkCompletion(String... strings) { + assertStringItems(strings); + + LookupImpl lookup = getActiveLookup(); + if (lookup != null) lookup.hide(); + JsonSchemaService.Impl.get(getProject()).reset(); + doHighlighting(); + complete(); + assertStringItems(strings); + } + + public void testRefreshSchemaCompletionSimpleVariant() throws Exception { + skeleton(new Callback() { + private String myModuleDir; + + @Override + public void registerSchemes() { + myModuleDir = getModuleDir(getProject()); + + final UserDefinedJsonSchemaConfiguration base = + new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, myModuleDir + "/basePropertiesSchema.json", false, + Collections + .singletonList(new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)) + ); + addSchema(base); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/baseCompletion.json", "/basePropertiesSchema.json"); + } + + @Override + public void doCheck() throws Exception { + final VirtualFile moduleFile = getProject().getBaseDir().findChild(myModuleDir); + assertNotNull(moduleFile); + checkSchemaCompletion(moduleFile, "basePropertiesSchema.json", false); + } + }); + } + + public void testJsonSchemaCrossReferenceCompletionWithSchemaEditing() throws Exception { + skeleton(new Callback() { + private String myModuleDir; + + @Override + public void registerSchemes() { + myModuleDir = getModuleDir(getProject()); + + final UserDefinedJsonSchemaConfiguration base = + new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, myModuleDir + "/baseSchema.json", false, Collections.emptyList()); + addSchema(base); + + final UserDefinedJsonSchemaConfiguration inherited + = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, myModuleDir + "/inheritedSchema.json", false, + Collections + .singletonList(new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)) + ); + + addSchema(inherited); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/completion.json", "/baseSchema.json", "/inheritedSchema.json"); + } + + @Override + public void doCheck() throws Exception { + final VirtualFile moduleFile = getProject().getBaseDir().findChild(myModuleDir); + assertNotNull(moduleFile); + checkSchemaCompletion(moduleFile, "baseSchema.json", true); + } + }); + } + + private void checkSchemaCompletion(VirtualFile moduleFile, final String fileName, boolean delayAfterUpdate) throws InterruptedException { + doHighlighting(); + complete(); + assertStringItems("\"one\"", "\"two\""); + + final VirtualFile baseFile = moduleFile.findChild(fileName); + Assert.assertNotNull(baseFile); + FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); + Document document = fileDocumentManager.getDocument(baseFile); + Assert.assertNotNull(document); + String str = "\"enum\": [\"one\", \"two\"]"; + int start = document.getText().indexOf(str); + Assert.assertTrue(start > 0); + + ApplicationManager.getApplication().runWriteAction(() -> { + document.replaceString(start, start + str.length(), "\"enum\": [\"one1\", \"two1\"]"); + fileDocumentManager.saveAllDocuments(); + }); + LookupImpl lookup = getActiveLookup(); + if (lookup != null) lookup.hide(); + JsonSchemaService.Impl.get(getProject()).reset(); + + if (delayAfterUpdate) { + // give time for vfs callbacks to finish + Thread.sleep(400); + } + + doHighlighting(); + complete(); + assertStringItems("\"one1\"", "\"two1\""); + + lookup = getActiveLookup(); + if (lookup != null) lookup.hide(); + JsonSchemaService.Impl.get(getProject()).reset(); + doHighlighting(); + complete(); + assertStringItems("\"one1\"", "\"two1\""); + } + + public void testJsonSchemaRefsCrossResolve() throws Exception { + skeleton(new Callback() { + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiReference referenceAt = myFile.findReferenceAt(offset); + Assert.assertNotNull(referenceAt); + final PsiElement resolve = referenceAt.resolve(); + Assert.assertNotNull(resolve); + Assert.assertEquals("{\n" + + " \"type\": \"string\",\n" + + " \"enum\": [\"one\", \"two\"]\n" + + " }", resolve.getText()); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/referencingSchema.json", "/localRefSchema.json"); + } + + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + + final UserDefinedJsonSchemaConfiguration base = + new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, moduleDir + "/localRefSchema.json", false, Collections.emptyList()); + addSchema(base); + + final UserDefinedJsonSchemaConfiguration inherited + = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referencingSchema.json", false, + Collections.emptyList() + ); + + addSchema(inherited); + } + }); + } + + public void testJsonSchemaGlobalRefsCrossResolve() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + + AreaPicoContainer container = Extensions.getArea(getProject()).getPicoContainer(); + final String key = JsonSchemaMappingsProjectConfiguration.class.getName(); + container.unregisterComponent(key); + container.registerComponentImplementation(key, TestJsonSchemaMappingsProjectConfiguration.class); + + final UserDefinedJsonSchemaConfiguration inherited + = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referencingGlobalSchema.json", false, + Collections.emptyList() + ); + + addSchema(inherited); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/referencingGlobalSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiReference referenceAt = myFile.findReferenceAt(offset); + Assert.assertNotNull(referenceAt); + final PsiElement resolve = referenceAt.resolve(); + Assert.assertNotNull(resolve); + Assert.assertTrue(StringUtil.equalsIgnoreWhitespaces("{\n" + + " \"type\": \"array\",\n" + + " \"minItems\": 1,\n" + + " \"uniqueItems\": true\n" + + " }", resolve.getText())); + } + }); + } + + public void testJson2SchemaPropertyResolve() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final UserDefinedJsonSchemaConfiguration inherited + = new UserDefinedJsonSchemaConfiguration("inherited", JsonSchemaVersion.SCHEMA_4, moduleDir + "/basePropertiesSchema.json", false, + Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)) + ); + + addSchema(inherited); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/testFileForBaseProperties.json", "/basePropertiesSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset); + Assert.assertNotNull(resolve); + Assert.assertEquals("basePropertiesSchema.json", resolve.getContainingFile().getName()); + Assert.assertEquals("\"baseEnum\"", resolve.getText()); + } + }); + } + + public void testFindRefInOtherFile() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/refToDefinitionInFileSchema.json", false, + Collections.emptyList())); + addSchema( + new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/definitionsSchema.json", false, Collections.emptyList())); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/refToDefinitionInFileSchema.json", "/definitionsSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiReference referenceAt = myFile.findReferenceAt(offset); + Assert.assertNotNull(referenceAt); + final PsiElement resolve = referenceAt.resolve(); + Assert.assertNotNull(resolve); + Assert.assertEquals("definitionsSchema.json", resolve.getContainingFile().getName()); + Assert.assertEquals("{\"type\": \"object\"}", resolve.getText()); + } + }); + } + + public void testFindRefToOtherFile() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema( + new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/refToOtherFileSchema.json", false, Collections.emptyList() + )); + addSchema( + new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/definitionsSchema.json", false, Collections.emptyList() + )); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/refToOtherFileSchema.json", "/definitionsSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiReference referenceAt = myFile.findReferenceAt(offset); + Assert.assertNotNull(referenceAt); + final PsiElement resolve = referenceAt.resolve(); + Assert.assertNotNull(resolve); + Assert.assertEquals("definitionsSchema.json", resolve.getContainingFile().getName()); + } + }); + } + + public void testNavigateToPropertyDefinitionInPackageJsonSchema() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("package.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/packageJsonSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/package.json", "/packageJsonSchema.json"); + } + + @Override + public void doCheck() { + final String text = myFile.getText(); + final int indexOf = text.indexOf("dependencies"); + assertTrue(indexOf > 0); + final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, indexOf); + Assert.assertNotNull(resolve); + Assert.assertEquals("packageJsonSchema.json", resolve.getContainingFile().getName()); + Assert.assertEquals("\"dependencies\"", resolve.getText()); + } + }); + } + + public void testNavigateToPropertyDefinitionNestedDefinitions() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("testNestedDefinitionsNavigation.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/nestedDefinitionsSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/testNestedDefinitionsNavigation.json", "/nestedDefinitionsSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset); + Assert.assertNotNull(resolve); + Assert.assertEquals("nestedDefinitionsSchema.json", resolve.getContainingFile().getName()); + Assert.assertEquals("\"definitions\"", resolve.getText()); + } + }); + } + + public void testNavigateToAllOfOneOfDefinitions() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("testNestedAllOfOneOfDefinitions.json", true, false)); + addSchema( + new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/nestedAllOfOneOfDefinitionsSchema.json", false, patterns + )); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/testNestedAllOfOneOfDefinitions.json", "/nestedAllOfOneOfDefinitionsSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset); + Assert.assertNotNull(resolve); + Assert.assertEquals("nestedAllOfOneOfDefinitionsSchema.json", resolve.getContainingFile().getName()); + Assert.assertEquals("\"begriff\"", resolve.getText()); + } + }); + } + + public void testNestedAllOneAnyWithInheritanceNavigation() throws Exception { + final String prefix = "nestedAllOneAnyWithInheritance/"; + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/baseSchema.json", false, Collections.emptyList())); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("testNavigation.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referentSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, prefix + "testNavigation.json", prefix + "baseSchema.json", prefix + "referentSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset); + Assert.assertNotNull(resolve); + Assert.assertEquals("baseSchema.json", resolve.getContainingFile().getName()); + Assert.assertEquals("\"findMe\"", resolve.getText()); + } + }); + } + + public void testNestedAllOneAnyWithInheritanceCompletion() throws Exception { + final String prefix = "nestedAllOneAnyWithInheritance/"; + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/baseSchema.json", false, Collections.emptyList())); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("testCompletion.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referentSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, prefix + "testCompletion.json", prefix + "baseSchema.json", prefix + "referentSchema.json"); + } + + @Override + public void doCheck() { + checkCompletion("1", "2"); + } + }); + } + + public void testNestedAllOneAnyWithInheritanceHighlighting() throws Exception { + final String prefix = "nestedAllOneAnyWithInheritance/"; + enableInspectionTool(new JsonSchemaComplianceInspection()); + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/baseSchema.json", false, Collections.emptyList())); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("testHighlighting.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("two", JsonSchemaVersion.SCHEMA_4, moduleDir + "/referentSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, prefix + "testHighlighting.json", prefix + "baseSchema.json", prefix + "referentSchema.json"); + } + + @Override + public void doCheck() { + doDoTest(true, false); + } + }); + } + + public void testNavigateToDefinitionByRef() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, + moduleDir + "/withReferenceToDefinitionSchema.json", false, + Collections.emptyList() + )); + } + + @Override + public void configureFiles() { + configureByFiles(null, "withReferenceToDefinitionSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiReference referenceAt = myFile.findReferenceAt(offset); + Assert.assertNotNull(referenceAt); + final PsiElement resolve = referenceAt.resolve(); + Assert.assertNotNull(resolve); + Assert.assertEquals("{\n" + + " \"enum\": [1,4,8]\n" + + " }", resolve.getText()); + final PsiElement parent = resolve.getParent(); + Assert.assertTrue(parent instanceof JsonProperty); + final JsonValue value = ((JsonProperty)parent).getValue(); + Assert.assertTrue(value instanceof JsonObject); + final JsonProperty anEnum = ((JsonObject)value).findProperty("enum"); + Assert.assertNotNull(anEnum); + Assert.assertEquals("[1,4,8]", anEnum.getValue().getText()); + } + }); + } + + public void testCompletionInsideSchemaDefinition() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", + JsonSchemaVersion.SCHEMA_4, moduleDir + "/completionInsideSchemaDefinition.json", false, + Collections.emptyList())); + } + + @Override + public void configureFiles() { + configureByFiles(null, "completionInsideSchemaDefinition.json"); + } + + @Override + public void doCheck() { + final Set<String> strings = Arrays.stream(myItems).map(LookupElement::getLookupString).collect(Collectors.toSet()); + Assert.assertTrue(strings.contains("\"enum\"")); + Assert.assertTrue(strings.contains("\"exclusiveMinimum\"")); + Assert.assertTrue(strings.contains("\"description\"")); + } + }); + } + + public void testNavigateFromSchemaDefinitionToMainSchema() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", + JsonSchemaVersion.SCHEMA_4, + moduleDir + "/navigateFromSchemaDefinitionToMainSchema.json", false, + Collections.emptyList())); + } + + @Override + public void configureFiles() { + configureByFiles(null, "navigateFromSchemaDefinitionToMainSchema.json"); + } + + @Override + public void doCheck() { + int offset = getCaretOffset(); + final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset); + Assert.assertNotNull(resolve); + Assert.assertEquals("\"properties\"", resolve.getText()); + final PsiElement parent = resolve.getParent(); + Assert.assertTrue(parent instanceof JsonProperty); + Assert.assertEquals("schema.json", resolve.getContainingFile().getName()); + } + }); + } + + public void testNavigateToRefInsideMainSchema() { + final JsonSchemaService service = JsonSchemaService.Impl.get(myProject); + final List<JsonSchemaFileProvider> providers = new JsonSchemaProjectSelfProviderFactory().getProviders(myProject); + Assert.assertEquals(JsonSchemaProjectSelfProviderFactory.TOTAL_PROVIDERS, providers.size()); + for (JsonSchemaFileProvider provider: providers) { + final VirtualFile mainSchema = provider.getSchemaFile(); + assertNotNull(mainSchema); + assertTrue(service.isSchemaFile(mainSchema)); + + final PsiFile psi = PsiManager.getInstance(myProject).findFile(mainSchema); + Assert.assertNotNull(psi); + Assert.assertTrue(psi instanceof JsonFile); + final JsonValue top = ((JsonFile)psi).getTopLevelValue(); + final JsonObject obj = ObjectUtils.tryCast(top, JsonObject.class); + Assert.assertNotNull(obj); + final JsonProperty properties = obj.findProperty("properties"); + final JsonObject propObj = ObjectUtils.tryCast(properties.getValue(), JsonObject.class); + final JsonProperty maxLength = propObj.findProperty("maxLength"); + final JsonObject value = ObjectUtils.tryCast(maxLength.getValue(), JsonObject.class); + Assert.assertNotNull(value); + final JsonProperty ref = value.findProperty("$ref"); + Assert.assertNotNull(ref); + final JsonStringLiteral literal = ObjectUtils.tryCast(ref.getValue(), JsonStringLiteral.class); + Assert.assertNotNull(literal); + + final PsiReference reference = psi.findReferenceAt(literal.getTextRange().getEndOffset() - 1); + Assert.assertNotNull(reference); + String positiveOrNonNegative = ((JsonSchemaProjectSelfProviderFactory.MyJsonSchemaFileProvider)provider).isSchemaV4() + ? "positiveInteger" + : "nonNegativeInteger"; + Assert.assertEquals("#/definitions/" + positiveOrNonNegative, reference.getCanonicalText()); + final PsiElement resolve = reference.resolve(); + Assert.assertNotNull(resolve); + Assert.assertTrue(StringUtil.equalsIgnoreWhitespaces("{\n" + + " \"type\": \"integer\",\n" + + " \"minimum\": 0\n" + + " }", resolve.getText())); + Assert.assertTrue(resolve.getParent() instanceof JsonProperty); + Assert.assertEquals(positiveOrNonNegative, ((JsonProperty)resolve.getParent()).getName()); + } + } + + public void testNavigateToDefinitionByRefInFileWithIncorrectReference() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/withIncorrectReferenceSchema.json", false, + Collections.emptyList() + )); + } + + @Override + public void configureFiles() { + configureByFiles(null, "withIncorrectReferenceSchema.json"); + } + + @Override + public void doCheck() { + final String midia = "{\n" + + " \"properties\": {\n" + + " \"mittel\" : {\n" + + " \"type\": [\"integer\", \"boolean\"],\n" + + " \"description\": \"this is found!\",\n" + + " \"enum\": [1,2, false]\n" + + " }\n" + + " }\n" + + " }"; + checkNavigationTo(midia, "midia", getCaretOffset(), JsonSchemaObject.DEFINITIONS, true); + } + }); + } + + private int getCaretOffset() { + return myEditor.getCaretModel().getPrimaryCaret().getOffset(); + } + + private void checkNavigationTo(@NotNull String resolvedText, @NotNull String name, int offset, @NotNull String base, boolean isReference) { + final PsiElement resolve = isReference + ? myFile.findReferenceAt(offset).resolve() + : GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset); + Assert.assertNotNull(resolve); + Assert.assertEquals(resolvedText, resolve.getText()); + final PsiElement parent = resolve.getParent(); + Assert.assertTrue(parent instanceof JsonProperty); + Assert.assertEquals(name, ((JsonProperty)parent).getName()); + Assert.assertTrue(parent.getParent().getParent() instanceof JsonProperty); + Assert.assertEquals(base, ((JsonProperty)parent.getParent().getParent()).getName()); + } + + public void testInsideCycledSchemaNavigation() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, + moduleDir + "/insideCycledSchemaNavigationSchema.json", + false, Collections.emptyList())); + } + + @Override + public void configureFiles() { + configureByFiles(null, "insideCycledSchemaNavigationSchema.json"); + } + + @Override + public void doCheck() { + checkNavigationTo("{\n" + + " \"$ref\": \"#/definitions/one\"\n" + + " }", "all", getCaretOffset(), JsonSchemaObject.DEFINITIONS, true); + } + }); + } + + public void testNavigationIntoCycledSchema() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/cycledSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, "testNavigationIntoCycled.json", "cycledSchema.json"); + } + + @Override + public void doCheck() { + checkNavigationTo("\"bbb\"", "bbb", getCaretOffset(), JsonSchemaObject.PROPERTIES, false); + } + }); + } + + public void testNavigationWithCompositeDefinitionsObject() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, + moduleDir + "/navigationWithCompositeDefinitionsObjectSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, "navigationWithCompositeDefinitionsObjectSchema.json"); + } + + @Override + public void doCheck() { + final Collection<JsonStringLiteral> strings = PsiTreeUtil.findChildrenOfType(myFile, JsonStringLiteral.class); + final List<JsonStringLiteral> list = strings.stream() + .filter(expression -> expression.getText().contains("#/definitions")).collect(Collectors.toList()); + Assert.assertEquals(3, list.size()); + list.forEach(literal -> checkNavigationTo("{\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"id\": {\n" + + " \"type\": \"string\"\n" + + " },\n" + + " \"range\": {\n" + + " \"type\": \"string\"\n" + + " }\n" + + " }\n" + + " }", "cycle.schema", literal.getTextRange().getEndOffset() - 1, + JsonSchemaObject.DEFINITIONS, true)); + } + }); + } + + public void testNavigationIntoWithCompositeDefinitionsObject() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, + moduleDir + "/navigationWithCompositeDefinitionsObjectSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, "navigationIntoWithCompositeDefinitionsObjectSchema.json", + "navigationWithCompositeDefinitionsObjectSchema.json"); + } + + @Override + public void doCheck() { + checkNavigationTo("\"id\"", "id", getCaretOffset(), JsonSchemaObject.PROPERTIES, false); + } + }); + } + + public void testCompletionWithRootRef() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, moduleDir + "/cycledWithRootRefSchema.json", false, patterns)); + } + + @Override + public void configureFiles() { + configureByFiles(null, "completionWithRootRef.json", "cycledWithRootRefSchema.json"); + complete(); + } + + @Override + public void doCheck() { + checkCompletion("\"id\"", "\"testProp\""); + } + }); + } + + public void testResolveByValuesCombinations() throws Exception { + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + final List<UserDefinedJsonSchemaConfiguration.Item> patterns = Collections.singletonList( + new UserDefinedJsonSchemaConfiguration.Item("*.json", true, false)); + addSchema(new UserDefinedJsonSchemaConfiguration("one", JsonSchemaVersion.SCHEMA_4, + moduleDir + "/ResolveByValuesCombinationsSchema.json", false, patterns)); + } + + @Override + public void configureFiles() throws Exception { + configureByFile("ResolveByValuesCombinationsSchema.json"); + } + + @Override + public void doCheck() { + final List<Trinity<String, String, String>> variants = ContainerUtil.list( + Trinity.create("yes", "barkling", "dog"), + Trinity.create("yes", "meowing", "cat"), + Trinity.create("yes", "crowling", "mouse"), + Trinity.create("not", "apparel", "schrank"), + Trinity.create("not", "dinner", "tisch"), + Trinity.create("not", "rest", "sessel") + ); + variants.forEach( + t -> { + final PsiFile file = configureByText(JsonFileType.INSTANCE, String.format("{\"alive\":\"%s\",\n" + + "\"feature\":\"%s\"}", t.getFirst(), t.getSecond()), "json"); + final JsonFile jsonFile = ObjectUtils.tryCast(file, JsonFile.class); + Assert.assertNotNull(jsonFile); + final JsonObject top = ObjectUtils.tryCast(jsonFile.getTopLevelValue(), JsonObject.class); + Assert.assertNotNull(top); + + TextRange range = top.findProperty("alive").getNameElement().getTextRange(); + checkNavigationToSchemaVariant("alive", range.getStartOffset() + 1, t.getThird()); + + range = top.findProperty("feature").getNameElement().getTextRange(); + checkNavigationToSchemaVariant("feature", range.getStartOffset() + 1, t.getThird()); + } + ); + } + }); + } + + private void checkNavigationToSchemaVariant(@NotNull String name, int offset, @NotNull String parentPropertyName) { + final PsiElement resolve = GotoDeclarationAction.findTargetElement(getProject(), myEditor, offset); + Assert.assertEquals("\"" + name + "\"", resolve.getText()); + final PsiElement parent = resolve.getParent(); + Assert.assertTrue(parent instanceof JsonProperty); + Assert.assertEquals(name, ((JsonProperty)parent).getName()); + Assert.assertTrue(parent.getParent().getParent() instanceof JsonProperty); + final JsonProperty props = (JsonProperty)parent.getParent().getParent(); + Assert.assertEquals("properties", props.getName()); + final JsonProperty parentProperty = ObjectUtils.tryCast(props.getParent().getParent(), JsonProperty.class); + Assert.assertNotNull(parentProperty); + Assert.assertEquals(parentPropertyName, parentProperty.getName()); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaDocumentationTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaDocumentationTest.java new file mode 100644 index 00000000..227d8e66 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaDocumentationTest.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.jetbrains.jsonSchema; + +public class JsonSchemaDocumentationTest extends JsonBySchemaDocumentationBaseTest { + @Override + protected String getBasePath() { + return "/tests/testData/jsonSchema/documentation"; + } + + public void testSimple() throws Exception { + doTest(true, "json"); + } + + public void testSecondLevel() throws Exception { + doTest(true, "json"); + } + + public void testCheckEscaping() throws Exception { + doTest(true, "json"); + } + + public void testWithDefinition() throws Exception { + doTest(true, "json"); + } + + public void testWithTitleInDefinition() throws Exception { + doTest(true, "json"); + } + + public void testHtmlDescription() throws Exception { + doTest(true, "json"); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHeavyAbstractTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHeavyAbstractTest.java new file mode 100644 index 00000000..7780f856 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHeavyAbstractTest.java @@ -0,0 +1,87 @@ +// 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.completion.CompletionTestCase; +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.application.ex.PathManagerEx; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Irina.Chernushina on 12/5/2016. + */ +public abstract class JsonSchemaHeavyAbstractTest extends CompletionTestCase { + private Map<String, UserDefinedJsonSchemaConfiguration> mySchemas; + protected boolean myDoCompletion = true; + + @Override + public void setUp() throws Exception { + super.setUp(); + //WriteCommandAction.runWriteCommandAction(getProject(), () -> myFileTypeManager.associatePattern(JsonSchemaFileType.INSTANCE, "*Schema.json")); + mySchemas = new HashMap<>(); + myDoCompletion = true; + } + + @Override + public void tearDown() throws Exception { + try { + //WriteCommandAction.runWriteCommandAction(getProject(), () -> myFileTypeManager.removeAssociatedExtension(JsonSchemaFileType.INSTANCE, "*Schema.json")); + final JsonSchemaMappingsProjectConfiguration instance = JsonSchemaMappingsProjectConfiguration.getInstance(getProject()); + instance.setState(Collections.emptyMap()); + } finally { + super.tearDown(); + } + } + + @Override + public String getTestDataPath() { + PathManagerEx.TestDataLookupStrategy strategy = PathManagerEx.guessTestDataLookupStrategy(); + if (strategy.equals(PathManagerEx.TestDataLookupStrategy.COMMUNITY)) { + return PathManager.getHomePath() + "/json" + getBasePath() + "/"; + } + return PathManager.getHomePath() + "/community/json" + getBasePath() + "/"; + } + + protected abstract String getBasePath(); + + protected void skeleton(@NotNull final Callback callback) throws Exception { + callback.configureFiles(); + callback.registerSchemes(); + JsonSchemaMappingsProjectConfiguration.getInstance(getProject()).setState(mySchemas); + JsonSchemaService.Impl.get(getProject()).reset(); + doHighlighting(); + if (myDoCompletion) complete(); + callback.doCheck(); + } + + @NotNull + protected static String getModuleDir(@NotNull final Project project) { + String moduleDir = null; + VirtualFile[] children = project.getBaseDir().getChildren(); + for (VirtualFile child : children) { + if (child.isDirectory()) { + moduleDir = child.getName(); + break; + } + } + Assert.assertNotNull(moduleDir); + return moduleDir; + } + + protected interface Callback { + void registerSchemes(); + void configureFiles() throws Exception; + void doCheck() throws Exception; + } + + protected void addSchema(@NotNull final UserDefinedJsonSchemaConfiguration schema) { + mySchemas.put(schema.getName(), schema); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTest.java new file mode 100644 index 00000000..42cdf7ed --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTest.java @@ -0,0 +1,1051 @@ +// 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.codeInspection.InspectionProfileEntry; +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.util.containers.Predicate; +import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection; +import org.intellij.lang.annotations.Language; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Irina.Chernushina on 9/21/2015. + */ +public class JsonSchemaHighlightingTest extends JsonSchemaHighlightingTestBase { + @Override + protected String getTestDataPath() { + return PlatformTestUtil.getCommunityPath() + "/json/tests/testData/jsonSchema/highlighting"; + } + + @Override + protected String getTestFileName() { + return "config.json"; + } + + @Override + protected InspectionProfileEntry getInspectionProfile() { + return new JsonSchemaComplianceInspection(); + } + + @Override + protected Predicate<VirtualFile> getAvailabilityPredicate() { + return file -> file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf( + JsonLanguage.INSTANCE); + } + + public void testNumberMultipleWrong() throws Exception { + doTest("{ \"properties\": { \"prop\": {\"type\": \"number\", \"multipleOf\": 2}}}", + "{ \"prop\": <warning descr=\"Is not multiple of 2\">3</warning>}"); + } + + public void testNumberMultipleCorrect() throws Exception { + doTest("{ \"properties\": { \"prop\": {\"type\": \"number\", \"multipleOf\": 2}}}", "{ \"prop\": 4}"); + } + + public void testNumberMinMax() throws Exception { + doTest("{ \"properties\": { \"prop\": {\n" + + " \"type\": \"number\",\n" + + " \"minimum\": 0,\n" + + " \"maximum\": 100,\n" + + " \"exclusiveMaximum\": true\n" + + "}}}", "{ \"prop\": 14}"); + } + + public void testEnum() throws Exception { + @Language("JSON") final String schema = "{\"properties\": {\"prop\": {\"enum\": [1,2,3,\"18\"]}}}"; + doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: 1, 2, 3, \\\"18\\\"\">18</warning>}"); + doTest(schema, "{\"prop\": 2}"); + doTest(schema, "{\"prop\": \"18\"}"); + doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: 1, 2, 3, \\\"18\\\"\">\"2\"</warning>}"); + } + + public void testSimpleString() throws Exception { + @Language("JSON") final String schema = "{\"properties\": {\"prop\": {\"type\": \"string\", \"minLength\": 2, \"maxLength\": 3}}}"; + doTest(schema, "{\"prop\": <warning descr=\"String is shorter than 2\">\"s\"</warning>}"); + doTest(schema, "{\"prop\": \"sh\"}"); + doTest(schema, "{\"prop\": \"sho\"}"); + doTest(schema, "{\"prop\": <warning descr=\"String is longer than 3\">\"shor\"</warning>}"); + } + + public void testArray() throws Exception { + @Language("JSON") final String schema = schema("{\n" + + " \"type\": \"array\",\n" + + " \"items\": {\n" + + " \"type\": \"number\", \"minimum\": 18" + + " }\n" + + "}"); + doTest(schema, "{\"prop\": [101, 102]}"); + doTest(schema, "{\"prop\": [<warning descr=\"Less than a minimum 18\">16</warning>]}"); + doTest(schema, "{\"prop\": [<warning descr=\"Type is not allowed. Expected: number.\">\"test\"</warning>]}"); + } + + public void testTopLevelArray() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"type\": \"array\",\n" + + " \"items\": {\n" + + " \"type\": \"number\", \"minimum\": 18" + + " }\n" + + "}"; + doTest(schema, "[101, 102]"); + } + + public void testTopLevelObjectArray() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"type\": \"array\",\n" + + " \"items\": {\n" + + " \"type\": \"object\", \"properties\": {\"a\": {\"type\": \"number\"}}" + + " }\n" + + "}"; + doTest(schema, "[{\"a\": <warning descr=\"Type is not allowed. Expected: number.\">true</warning>}]"); + doTest(schema, "[{\"a\": 18}]"); + } + + public void testArrayTuples1() throws Exception { + @Language("JSON") final String schema = schema("{\n" + + " \"type\": \"array\",\n" + + " \"items\": [{\n" + + " \"type\": \"number\", \"minimum\": 18" + + " }, {\"type\" : \"string\"}]\n" + + "}"); + doTest(schema, "{\"prop\": [101, <warning descr=\"Type is not allowed. Expected: string.\">102</warning>]}"); + doTest(schema, "{\"prop\": [101, \"102\"]}"); + doTest(schema, "{\"prop\": [101, \"102\", \"additional\"]}"); + + @Language("JSON") final String schema2 = schema("{\n" + + " \"type\": \"array\",\n" + + " \"items\": [{\n" + + " \"type\": \"number\", \"minimum\": 18" + + " }, {\"type\" : \"string\"}],\n" + + "\"additionalItems\": false}"); + doTest(schema2, "{\"prop\": [101, \"102\", <warning descr=\"Additional items are not allowed\">\"additional\"</warning>]}"); + } + + public void testArrayLength() throws Exception { + @Language("JSON") final String schema = schema("{\"type\": \"array\", \"minItems\": 2, \"maxItems\": 3}"); + doTest(schema, "{\"prop\": <warning descr=\"Array is shorter than 2\">[]</warning>}"); + doTest(schema, "{\"prop\": [1,2]}"); + doTest(schema, "{\"prop\": <warning descr=\"Array is longer than 3\">[1,2,3,4]</warning>}"); + } + + public void testArrayUnique() throws Exception { + @Language("JSON") final String schema = schema("{\"type\": \"array\", \"uniqueItems\": true}"); + doTest(schema, "{\"prop\": [1,2]}"); + doTest(schema, "{\"prop\": [<warning descr=\"Item is not unique\">1</warning>,2, \"test\", <warning descr=\"Item is not unique\">1</warning>]}"); + } + + public void testMetadataIsOk() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"title\" : \"Match anything\",\n" + + " \"description\" : \"This is a schema that matches anything.\",\n" + + " \"default\" : \"Default value\"\n" + + "}"; + doTest(schema, "{\"anything\": 1}"); + } + + public void testRequiredField() throws Exception { + @Language("JSON") final String schema = "{\"type\": \"object\", \"properties\": {\"a\": {}, \"b\": {}}, \"required\": [\"a\"]}"; + doTest(schema, "{\"a\": 11}"); + doTest(schema, "{\"a\": 1, \"b\": true}"); + doTest(schema, "<warning descr=\"Missing required property 'a'\">{\"b\": \"alarm\"}</warning>"); + } + + public void testInnerRequired() throws Exception { + @Language("JSON") final String schema = schema("{\"type\": \"object\", \"properties\": {\"a\": {}, \"b\": {}}, \"required\": [\"a\"]}"); + doTest(schema, "{\"prop\": {\"a\": 11}}"); + doTest(schema, "{\"prop\": {\"a\": 1, \"b\": true}}"); + doTest(schema, "{\"prop\": <warning descr=\"Missing required property 'a'\">{\"b\": \"alarm\"}</warning>}"); + } + + public void testUseDefinition() throws Exception { + @Language("JSON") final String schema = "{\"definitions\": {\"address\": {\"type\": \"object\", \"properties\": {\"street\": {\"type\": \"string\"}," + + "\"house\": {\"type\": \"integer\"}}}}," + + "\"type\": \"object\", \"properties\": {" + + "\"home\": {\"$ref\": \"#/definitions/address\"}, " + + "\"office\": {\"$ref\": \"#/definitions/address\"}" + + "}}"; + doTest(schema, "{\"home\": {\"street\": \"Broadway\", \"house\": 11}}"); + doTest(schema, "{\"home\": {\"street\": \"Broadway\", \"house\": <warning descr=\"Type is not allowed. Expected: integer.\">\"unknown\"</warning>}," + + "\"office\": {\"street\": <warning descr=\"Type is not allowed. Expected: string.\">5</warning>}}"); + } + + public void testAdditionalPropertiesAllowed() throws Exception { + @Language("JSON") final String schema = schema("{}"); + doTest(schema, "{\"prop\": {}, \"someStuff\": 20}"); + } + + public void testAdditionalPropertiesDisabled() throws Exception { + @Language("JSON") final String schema = "{\"type\": \"object\", \"properties\": {\"prop\": {}}, \"additionalProperties\": false}"; + // not sure abt inner object + doTest(schema, "{\"prop\": {}, <warning descr=\"Property 'someStuff' is not allowed\">\"someStuff\": 20</warning>}"); + } + + public void testAdditionalPropertiesSchema() throws Exception { + @Language("JSON") final String schema = "{\"type\": \"object\", \"properties\": {\"a\": {}}," + + "\"additionalProperties\": {\"type\": \"string\"}}"; + doTest(schema, "{\"a\" : 18, \"b\": \"wall\", \"c\": <warning descr=\"Type is not allowed. Expected: string.\">11</warning>}"); + } + + public void testMinMaxProperties() throws Exception { + @Language("JSON") final String schema = "{\"type\": \"object\", \"minProperties\": 1, \"maxProperties\": 2}"; + doTest(schema, "<warning descr=\"Number of properties is less than 1\">{}</warning>"); + doTest(schema, "{\"a\": 1}"); + doTest(schema, "<warning descr=\"Number of properties is greater than 2\">{\"a\": 1, \"b\": 22, \"c\": 33}</warning>"); + } + + public void testOneOf() throws Exception { + final List<String> subSchemas = new ArrayList<>(); + subSchemas.add("{\"type\": \"string\"}"); + subSchemas.add("{\"type\": \"boolean\"}"); + @Language("JSON") final String schema = schema("{\"oneOf\": [" + StringUtil.join(subSchemas, ", ") + "]}"); + doTest(schema, "{\"prop\": \"abc\"}"); + doTest(schema, "{\"prop\": true}"); + doTest(schema, "{\"prop\": <warning descr=\"Type is not allowed. Expected one of: boolean, string.\">11</warning>}"); + } + + public void testOneOfForTwoMatches() throws Exception { + final List<String> subSchemas = new ArrayList<>(); + subSchemas.add("{\"type\": \"string\", \"enum\": [\"a\", \"b\"]}"); + subSchemas.add("{\"type\": \"string\", \"enum\": [\"a\", \"c\"]}"); + @Language("JSON") final String schema = schema("{\"oneOf\": [" + StringUtil.join(subSchemas, ", ") + "]}"); + doTest(schema, "{\"prop\": \"b\"}"); + doTest(schema, "{\"prop\": \"c\"}"); + doTest(schema, "{\"prop\": <warning descr=\"Validates to more than one variant\">\"a\"</warning>}"); + } + + public void testOneOfSelectError() throws Exception { + final List<String> subSchemas = new ArrayList<>(); + subSchemas.add("{\"type\": \"string\",\n" + + " \"enum\": [\n" + + " \"off\", \"warn\", \"error\"\n" + + " ]}"); + subSchemas.add("{\"type\": \"integer\"}"); + @Language("JSON") final String schema = schema("{\"oneOf\": [" + StringUtil.join(subSchemas, ", ") + "]}"); + doTest(schema, "{\"prop\": \"off\"}"); + doTest(schema, "{\"prop\": 12}"); + doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: \\\"off\\\", \\\"warn\\\", \\\"error\\\"\">\"wrong\"</warning>}"); + } + + public void testAnyOf() throws Exception { + final List<String> subSchemas = new ArrayList<>(); + subSchemas.add("{\"type\": \"string\", \"enum\": [\"a\", \"b\"]}"); + subSchemas.add("{\"type\": \"string\", \"enum\": [\"a\", \"c\"]}"); + @Language("JSON") final String schema = schema("{\"anyOf\": [" + StringUtil.join(subSchemas, ", ") + "]}"); + doTest(schema, "{\"prop\": \"b\"}"); + doTest(schema, "{\"prop\": \"c\"}"); + doTest(schema, "{\"prop\": \"a\"}"); + doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: \\\"a\\\", \\\"b\\\", \\\"c\\\"\">\"d\"</warning>}"); + } + + public void testAllOf() throws Exception { + final List<String> subSchemas = new ArrayList<>(); + subSchemas.add("{\"type\": \"integer\", \"multipleOf\": 2}"); + subSchemas.add("{\"enum\": [1,2,3]}"); + @Language("JSON") final String schema = schema("{\"allOf\": [" + StringUtil.join(subSchemas, ", ") + "]}"); + doTest(schema, "{\"prop\": <warning descr=\"Is not multiple of 2\">1</warning>}"); + doTest(schema, "{\"prop\": <warning descr=\"Value should be one of: 1, 2, 3\">4</warning>}"); + doTest(schema, "{\"prop\": 2}"); + } + + public void testObjectInArray() throws Exception { + @Language("JSON") final String schema = schema("{\"type\": \"array\", \"items\": {\"type\": \"object\"," + + "\"properties\": {" + + "\"innerType\":{}, \"innerValue\":{}" + + "}, \"additionalProperties\": false" + + "}}"); + doTest(schema, "{\"prop\": [{\"innerType\":{}, <warning descr=\"Property 'alien' is not allowed\">\"alien\":{}</warning>}]}"); + } + + public void testObjectDeeperInArray() throws Exception { + final String innerTypeSchema = "{\"properties\": {\"only\": {}}, \"additionalProperties\": false}"; + @Language("JSON") final String schema = schema("{\"type\": \"array\", \"items\": {\"type\": \"object\"," + + "\"properties\": {" + + "\"innerType\":" + innerTypeSchema + + "}, \"additionalProperties\": false" + + "}}"); + doTest(schema, + "{\"prop\": [{\"innerType\":{\"only\": true, <warning descr=\"Property 'hidden' is not allowed\">\"hidden\": false</warning>}}]}"); + } + + public void testInnerObjectPropValueInArray() throws Exception { + @Language("JSON") final String schema = "{\"properties\": {\"prop\": {\"type\": \"array\", \"items\": {\"enum\": [1,2,3]}}}}"; + doTest(schema, "{\"prop\": [1,3]}"); + doTest(schema, "{\"prop\": [<warning descr=\"Value should be one of: 1, 2, 3\">\"out\"</warning>]}"); + } + + public void testAllOfProperties() throws Exception { + @Language("JSON") final String schema = "{\"allOf\": [{\"type\": \"object\", \"properties\": {\"first\": {}}}," + + " {\"properties\": {\"second\": {\"enum\": [33,44]}}}], \"additionalProperties\": false}"; + doTest(schema, "{\"first\": {}, \"second\": <warning descr=\"Value should be one of: 33, 44\">null</warning>}"); + doTest(schema, "{\"first\": {}, \"second\": 44, <warning descr=\"Property 'other' is not allowed\">\"other\": 15</warning>}"); + doTest(schema, "{\"first\": {}, \"second\": <warning descr=\"Value should be one of: 33, 44\">12</warning>}"); + } + + public void testWithWaySelection() throws Exception { + final String subSchema1 = "{\"enum\": [1,2,3,4,5]}"; + final String subSchema2 = "{\"type\": \"array\", \"items\": {\"properties\": {\"kilo\": {}}, \"additionalProperties\": false}}"; + @Language("JSON") final String schema = "{\"properties\": {\"prop\": {\"oneOf\": [" + subSchema1 + ", " + subSchema2 + "]}}}"; + //doTest(schema, "{\"prop\": [{\"kilo\": 20}]}"); + //doTest(schema, "{\"prop\": 5}"); + doTest(schema, "{\"prop\": [{<warning descr=\"Property 'foxtrot' is not allowed\">\"foxtrot\": 15</warning>, \"kilo\": 20}]}"); + } + + public void testIntegerTypeWithMinMax() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/integerTypeWithMinMax_schema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/integerTypeWithMinMax.json")); + doTest(schemaText, inputText); + } + + public void testOneOf1() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOf1.json")); + doTest(schemaText, inputText); + } + + public void testOneOf2() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOf2.json")); + doTest(schemaText, inputText); + } + + public void testAnyOnePropertySelection() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOnePropertySelectionSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOnePropertySelection.json")); + doTest(schemaText, inputText); + } + + public void testAnyOneTypeSelection() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOneTypeSelectionSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOneTypeSelection.json")); + doTest(schemaText, inputText); + } + + public void testOneOfWithEmptyPropertyValue() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfWithEmptyPropertyValue.json")); + doTest(schemaText, inputText); + } + + public void testCycledSchema() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/cycledSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/testCycledSchema.json")); + doTest(schemaText, inputText); + } + + public void testWithRootRefCycledSchema() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/cycledWithRootRefSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/testCycledWithRootRefSchema.json")); + doTest(schemaText, inputText); + } + + public void testCycledWithRootRefInNotSchema() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/cycledWithRootRefInNotSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/testCycledWithRootRefInNotSchema.json")); + doTest(schemaText, inputText); + } + + public void testPatternPropertiesHighlighting() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"patternProperties\": {\n" + + " \"^A\" : {\n" + + " \"type\": \"number\"\n" + + " },\n" + + " \"B\": {\n" + + " \"type\": \"boolean\"\n" + + " },\n" + + " \"C\": {\n" + + " \"enum\": [\"test\", \"em\"]\n" + + " }\n" + + " }\n" + + "}"; + doTest(schema, "{\n" + + " \"Abezjana\": 2,\n" + + " \"Auto\": <warning descr=\"Type is not allowed. Expected: number.\">\"no\"</warning>,\n" + + " \"BAe\": <warning descr=\"Type is not allowed. Expected: boolean.\">22</warning>,\n" + + " \"Boloto\": <warning descr=\"Type is not allowed. Expected: boolean.\">2</warning>,\n" + + " \"Cyan\": <warning descr=\"Value should be one of: \\\"test\\\", \\\"em\\\"\">\"me\"</warning>\n" + + "}"); + } + + public void testPatternPropertiesFromIssue() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"type\": \"object\",\n" + + " \"additionalProperties\": false,\n" + + " \"patternProperties\": {\n" + + " \"p[0-9]\": {\n" + + " \"type\": \"string\"\n" + + " },\n" + + " \"a[0-9]\": {\n" + + " \"enum\": [\"auto!\"]\n" + + " }\n" + + " }\n" + + "}"; + doTest(schema, "{\n" + + " \"p1\": <warning descr=\"Type is not allowed. Expected: string.\">1</warning>,\n" + + " \"p2\": \"3\",\n" + + " \"a2\": \"auto!\",\n" + + " \"a1\": <warning descr=\"Value should be one of: \\\"auto!\\\"\">\"moto!\"</warning>\n" + + "}"); + } + + public void testPatternForPropertyValue() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"properties\": {\n" + + " \"withPattern\": {\n" + + " \"pattern\": \"p[0-9]\"\n" + + " }\n" + + " }\n" + + "}"; + final String correctText = "{\n" + + " \"withPattern\": \"p1\"\n" + + "}"; + final String wrongText = "{\n" + + " \"withPattern\": <warning descr=\"String is violating the pattern: 'p[0-9]'\">\"wrong\"</warning>\n" + + "}"; + doTest(schema, correctText); + doTest(schema, wrongText); + } + + public void testPatternWithSpecialEscapedSymbols() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"properties\": {\n" + + " \"withPattern\": {\n" + + " \"pattern\": \"^\\\\d{4}\\\\-(0?[1-9]|1[012])\\\\-(0?[1-9]|[12][0-9]|3[01])$\"\n" + + " }\n" + + " }\n" + + "}"; + @Language("JSON") final String correctText = "{\n" + + " \"withPattern\": \"1234-11-11\"\n" + + "}"; + final String wrongText = "{\n" + + " \"withPattern\": <warning descr=\"String is violating the pattern: '^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$'\">\"wrong\"</warning>\n" + + "}"; + doTest(schema, correctText); + doTest(schema, wrongText); + } + + public void testRootObjectRedefinedAdditionalPropertiesForbidden() throws Exception { + doTest(rootObjectRedefinedSchema(), "{<warning descr=\"Property 'a' is not allowed\">\"a\": true</warning>," + + "\"r1\": \"allowed!\"}"); + } + + public void testNumberOfSameNamedPropertiesCorrectlyChecked() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"properties\": {\n" + + " \"size\": {\n" + + " \"type\": \"object\",\n" + + " \"minProperties\": 2,\n" + + " \"maxProperties\": 3,\n" + + " \"properties\": {\n" + + " \"a\": {\n" + + " \"type\": \"boolean\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + doTest(schema, "{\n" + + " \"size\": {\n" + + " \"a\": <warning descr=\"Type is not allowed. Expected: boolean.\">1</warning>," + + " \"b\":3, \"c\": 4, " + + "\"a\": <warning descr=\"Type is not allowed. Expected: boolean.\">5</warning>\n" + + " }\n" + + "}"); + doTest(schema, "{\n" + + " \"size\": <warning descr=\"Number of properties is greater than 3\">{\n" + + " \"a\": true," + + " \"b\":3, \"c\": 4, " + + "\"a\": false\n" + + " }</warning>\n" + + "}"); + } + + public void testManyDuplicatesInArray() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"properties\": {\n" + + " \"array\":{\n" + + " \"type\": \"array\",\n" + + " \"uniqueItems\": true\n" + + " }\n" + + " }\n" + + "}"; + doTest(schema, "{\"array\": [<warning descr=\"Item is not unique\">1</warning>," + + "<warning descr=\"Item is not unique\">1</warning>," + + "<warning descr=\"Item is not unique\">1</warning>," + + "<warning descr=\"Item is not unique\">2</warning>," + + "<warning descr=\"Item is not unique\">2</warning>," + + "5," + + "<warning descr=\"Item is not unique\">3</warning>," + + "<warning descr=\"Item is not unique\">3</warning>]}"); + } + + public void testPropertyValueAlsoHighlightedIfPatternIsInvalid() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"properties\": {\n" + + " \"withPattern\": {\n" + + " \"pattern\": \"^[]$\"\n" + + " }\n" + + " }\n" + + "}"; + final String text = "{\"withPattern\":" + + " <warning descr=\"Can not check string by pattern because of error: Unclosed character class near index 3\n^[]$\n ^\">\"(124)555-4216\"</warning>}"; + doTest(schema, text); + } + + public void testNotSchema() throws Exception { + @Language("JSON") final String schema = "{\"properties\": {\n" + + " \"not_type\": { \"not\": { \"type\": \"string\" } }\n" + + " }}"; + doTest(schema, "{\"not_type\": <warning descr=\"Validates against 'not' schema\">\"wrong\"</warning>}"); + } + + public void testNotSchemaCombinedWithNormal() throws Exception { + @Language("JSON") final String schema = "{\"properties\": {\n" + + " \"not_type\": {\n" + + " \"pattern\": \"^[a-z]*[0-5]*$\",\n" + + " \"not\": { \"pattern\": \"^[a-z]{1}[0-5]$\" }\n" + + " }\n" + + " }}"; + doTest(schema, "{\"not_type\": \"va4\"}"); + doTest(schema, "{\"not_type\": <warning descr=\"Validates against 'not' schema\">\"a4\"</warning>}"); + doTest(schema, "{\"not_type\": <warning descr=\"String is violating the pattern: '^[a-z]*[0-5]*$'\">\"4a4\"</warning>}"); + } + + public void testDoNotMarkOneOfThatDiffersWithFormat() throws Exception { + @Language("JSON") final String schema = "{\n" + + "\n" + + " \"properties\": {\n" + + " \"withFormat\": {\n" + + " \"type\": \"string\"," + + " \"oneOf\": [\n" + + " {\n" + + " \"format\":\"hostname\"\n" + + " },\n" + + " {\n" + + " \"format\": \"ip4\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + doTest(schema, "{\"withFormat\": \"localhost\"}"); + } + + public void testAcceptSchemaWithoutType() throws Exception { + @Language("JSON") final String schema = "{\n" + + "\n" + + " \"properties\": {\n" + + " \"withFormat\": {\n" + + " \"oneOf\": [\n" + + " {\n" + + " \"format\":\"hostname\"\n" + + " },\n" + + " {\n" + + " \"format\": \"ip4\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + doTest(schema, "{\"withFormat\": \"localhost\"}"); + } + + public void testArrayItemReference() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"items\": [\n" + + " {\n" + + " \"type\": \"integer\"\n" + + " },\n" + + " {\n" + + " \"$ref\": \"#/items/0\"\n" + + " }\n" + + " ]\n" + + "}"; + doTest(schema, "[1, 2]"); + doTest(schema, "[1, <warning>\"foo\"</warning>]"); + } + + public void testArrayReference() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"definitions\": {\n" + + " \"options\": {\n" + + " \"type\": \"array\",\n" + + " \"items\": {\n" + + " \"type\": \"number\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"items\":{\n" + + " \"$ref\": \"#/definitions/options/items\"\n" + + " }\n" + + " \n" + + "}"; + doTest(schema, "[2, 3 ,4]"); + doTest(schema, "[2, <warning>\"3\"</warning>]"); + } + + public void testSelfArrayReferenceDoesNotThrowSOE() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"items\": [\n" + + " {\n" + + " \"$ref\": \"#/items/0\"\n" + + " }\n" + + " ]\n" + + "}"; + doTest(schema, "[]"); + } + + public void testValidateAdditionalItems() throws Exception { + @Language("JSON") final String schema = "{\n" + + " \"definitions\": {\n" + + " \"options\": {\n" + + " \"type\": \"array\",\n" + + " \"items\": {\n" + + " \"type\": \"number\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"items\": [\n" + + " {\n" + + " \"type\": \"boolean\"\n" + + " },\n" + + " {\n" + + " \"type\": \"boolean\"\n" + + " }\n" + + " ],\n" + + " \"additionalItems\": {\n" + + " \"$ref\": \"#/definitions/options/items\"\n" + + " }\n" + + "}"; + doTest(schema, "[true, true]"); + doTest(schema, "[true, true, 1, 2, 3]"); + doTest(schema, "[true, true, 1, <warning>\"2\"</warning>]"); + } + + public static String rootObjectRedefinedSchema() { + return "{\n" + + " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n" + + " \"type\": \"object\",\n" + + " \"$ref\" : \"#/definitions/root\",\n" + + " \"definitions\": {\n" + + " \"root\" : {\n" + + " \"type\": \"object\",\n" + + " \"additionalProperties\": false,\n" + + " \"properties\": {\n" + + " \"r1\": {\n" + + " \"type\": \"string\"\n" + + " },\n" + + " \"r2\": {\n" + + " \"type\": \"string\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}\n"; + } + + static String schema(final String s) { + return "{\"type\": \"object\", \"properties\": {\"prop\": " + s + "}}"; + } + + public void testExclusiveMinMaxV6() throws Exception { + @Language("JSON") String exclusiveMinSchema = "{\"properties\": {\"prop\": {\"exclusiveMinimum\": 3}}}"; + doTest(exclusiveMinSchema, "{\"prop\": <warning>2</warning>}"); + doTest(exclusiveMinSchema, "{\"prop\": <warning>3</warning>}"); + doTest(exclusiveMinSchema, "{\"prop\": 4}"); + + @Language("JSON") String exclusiveMaxSchema = "{\"properties\": {\"prop\": {\"exclusiveMaximum\": 3}}}"; + doTest(exclusiveMaxSchema, "{\"prop\": 2}"); + doTest(exclusiveMaxSchema, "{\"prop\": <warning>3</warning>}"); + doTest(exclusiveMaxSchema, "{\"prop\": <warning>4</warning>}"); + } + + public void testPropertyNamesV6() throws Exception { + doTest("{\"propertyNames\": {\"minLength\": 7}}", "{<warning>\"prop\"</warning>: 2}"); + doTest("{\"properties\": {\"prop\": {\"propertyNames\": {\"minLength\": 7}}}}", "{\"prop\": {<warning>\"qq\"</warning>: 7}}"); + } + + public void testContainsV6() throws Exception { + @Language("JSON") String schema = "{\"properties\": {\"prop\": {\"type\": \"array\", \"contains\": {\"type\": \"number\"}}}}"; + doTest(schema, "{\"prop\": <warning>[{}, \"a\", true]</warning>}"); + doTest(schema, "{\"prop\": [{}, \"a\", 1, true]}"); + } + + public void testConstV6() throws Exception { + @Language("JSON") String schema = "{\"properties\": {\"prop\": {\"type\": \"string\", \"const\": \"foo\"}}}"; + doTest(schema, "{\"prop\": <warning>\"a\"</warning>}"); + doTest(schema, "{\"prop\": <warning>5</warning>}"); + doTest(schema, "{\"prop\": \"foo\"}"); + } + + public void testIfThenElseV7() throws Exception { + @Language("JSON") String schema = "{\n" + + " \"if\": {\n" + + " \"properties\": {\n" + + " \"a\": {\n" + + " \"type\": \"string\"\n" + + " }\n" + + " },\n" + + " \"required\": [\"a\"]\n" + + " },\n" + + " \"then\": {\n" + + " \"properties\": {\n" + + " \"b\": {\n" + + " \"type\": \"number\"\n" + + " }\n" + + " },\n" + + " \"required\": [\"b\"]\n" + + " },\n" + + " \"else\": {\n" + + " \"properties\": {\n" + + " \"c\": {\n" + + " \"type\": \"boolean\"\n" + + " }\n" + + " },\n" + + " \"required\": [\"c\"]\n" + + " }\n" + + "}"; + doTest(schema, "<warning>{}</warning>"); + doTest(schema, "{\"c\": <warning>5</warning>}"); + doTest(schema, "{\"c\": true}"); + doTest(schema, "<warning>{\"a\": 5, \"b\": 5}</warning>"); + doTest(schema, "{\"a\": 5, \"c\": <warning>5</warning>}"); + doTest(schema, "{\"a\": 5, \"c\": true}"); + doTest(schema, "<warning>{\"a\": \"a\", \"c\": true}</warning>"); + doTest(schema, "{\"a\": \"a\", \"b\": <warning>true</warning>}"); + doTest(schema, "{\"a\": \"a\", \"b\": 5}"); + } + + public void testNestedOneOf() throws Exception { + @Language("JSON") String schema = "{\"type\":\"object\",\n" + + " \"oneOf\": [\n" + + " {\n" + + " \"properties\": {\n" + + " \"type\": {\n" + + " \"type\": \"string\",\n" + + " \"oneOf\": [\n" + + " {\n" + + " \"pattern\": \"(good)\"\n" + + " },\n" + + " {\n" + + " \"pattern\": \"(ok)\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"properties\": {\n" + + " \"type\": {\n" + + " \"type\": \"string\",\n" + + " \"pattern\": \"^(fine)\"\n" + + " },\n" + + " \"extra\": {\n" + + " \"type\": \"string\"\n" + + " }\n" + + " },\n" + + " \"required\": [\"type\", \"extra\"]\n" + + " }\n" + + " ]}"; + + doTest(schema, "{\"type\": \"good\"}"); + doTest(schema, "{\"type\": \"ok\"}"); + doTest(schema, "{\"type\": <warning>\"doog\"</warning>}"); + doTest(schema, "{\"type\": <warning>\"ko\"</warning>}"); + } + + public void testArrayRefs() throws Exception { + @Language("JSON") String schema = "{\n" + + " \"myDefs\": {\n" + + " \"myArray\": [\n" + + " {\n" + + " \"type\": \"number\"\n" + + " },\n" + + " {\n" + + " \"type\": \"string\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"type\": \"array\",\n" + + " \"items\": [\n" + + " {\n" + + " \"$ref\": \"#/myDefs/myArray/0\"\n" + + " },\n" + + " {\n" + + " \"$ref\": \"#/myDefs/myArray/1\"\n" + + " }\n" + + " ]\n" + + "}"; + + doTest(schema, "[1, <warning>2</warning>]"); + doTest(schema, "[<warning>\"1\"</warning>, <warning>2</warning>]"); + doTest(schema, "[<warning>\"1\"</warning>, \"2\"]"); + doTest(schema, "[1, \"2\"]"); + } + + public void testOneOfInsideAllOf() throws Exception { + @Language("JSON") String schema = "{\n" + + " \"properties\": {\n" + + " \"foo\": {\n" + + " \"allOf\": [\n" + + " {\n" + + " \"type\": \"object\"\n" + + " }, {\n" + + " \"oneOf\": [\n" + + " {\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"provider\": {\n" + + " \"enum\": [\"script\"]\n" + + " },\n" + + " \"foo21\": {}\n" + + " }\n" + + " },\n" + + " {\n" + + " \"type\": \"object\",\n" + + " \"properties\": {\n" + + " \"provider\": {\n" + + " \"enum\": [\"npm\"]\n" + + " },\n" + + " \"foo11\": {}\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + + doTest(schema, "{\n" + + " \"foo\": {\n" + + " \"provider\": \"npm\"\n" + + " }\n" + + "}"); + + doTest(schema, "{\n" + + " \"foo\": {\n" + + " \"provider\": \"script\"\n" + + " }\n" + + "}"); + + doTest(schema, "{\n" + + " \"foo\": {\n" + + " \"provider\": <warning>\"etwasanderes\"</warning>\n" + + " }\n" + + "}"); + } + + public void testOneOfBestChoiceSchema() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/oneOfBestChoiceSchema.json")); + doTest(schemaText, "{\n" + + " \"results\": [\n" + + " <warning descr=\"Missing required properties 'dateOfBirth', 'name'\">{\n" + + " \"type\": \"person\"\n" + + " }</warning>\n" + + " ]\n" + + "}"); + } + + public void testAnyOfBestChoiceSchema() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/anyOfBestChoiceSchema.json")); + doTest(schemaText, "[\n" + + " {\n" + + " \"directory\": \"/test\",\n" + + " \"arguments\": [\n" + + " \"a\"\n" + + " ],\n" + + " \"file\": <warning>\"\"</warning>\n" + + " }\n" + + "] "); + } + + public void testComplexOneOfSchema() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/complexOneOfSchema.json")); + doTest(schemaText, "{\n" + + " \"indentation\": \"tab\"\n" + + " }"); + doTest(schemaText, "{\n" + + " \"indentation\": <warning>\"ttab\"</warning>\n" + + " }"); + } + + public void testEnumCasing() throws Exception { + @Language("JSON") String schema = "{\n" + + " \"type\": \"object\",\n" + + "\n" + + " \"properties\": {\n" + + " \"name\": { \"type\": \"string\", \"enum\": [\"aa\", \"bb\"] }\n" + + " }\n" + + "}"; + doTest(schema, "{\n" + + " \"name\": \"aa\"\n" + + "}"); + doTest(schema, "{\n" + + " \"name\": <warning>\"aA\"</warning>\n" + + "}"); + } + + public void testEnumArrayValue() throws Exception { + @Language("JSON") String schema = "{\n" + + " \"properties\": {\n" + + " \"foo\": {\n" + + " \"enum\": [ [{\"x\": 5}, [true], \"q\"] ]\n" + + " }\n" + + " }\n" + + "}"; + doTest(schema, "{\"foo\": <warning>5</warning>}"); + doTest(schema, "{\"foo\": <warning>[ ]</warning>}"); + doTest(schema, "{\"foo\": <warning>[{\"x\": 5}]</warning>}"); + doTest(schema, "{\"foo\": <warning>[{\"x\": 5}, true]</warning>}"); + doTest(schema, "{\"foo\": <warning>[{\"x\": 5}, [true]]</warning>}"); + doTest(schema, "{\"foo\": [ { \"x\" : 5 } , [ true ] , \"q\" ]}"); + } + + public void testEnumObjectValue() throws Exception { + @Language("JSON") String schema = "{\n" + + " \"properties\": {\n" + + " \"foo\": {\n" + + " \"enum\": [ {\"x\": 5} ]\n" + + " }\n" + + " }\n" + + "}"; + doTest(schema, "{\"foo\": <warning>{}</warning>}"); + doTest(schema, "{\"foo\": <warning>{\"x\": 4}</warning>}"); + doTest(schema, "{\"foo\": <warning>{\"x\": true}</warning>}"); + doTest(schema, "{\"foo\": { \r \"x\" : \t 5 \n }}"); + } + + public void testIntersectingHighlightingRanges() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/avroSchema.json")); + doTest(schemaText, "<warning descr=\"Missing required property 'items'\">{\n" + + " \"type\": \"array\"\n" + + "}</warning>"); + doTest(schemaText, "{\n" + + " \"type\": <warning descr=\"Value should be one of: \\\"record\\\", \\\"enum\\\", \\\"array\\\", \\\"map\\\", \\\"fixed\\\"\">\"array2\"</warning>\n" + + "}"); + } + + public void testMissingMultipleAltPropertySets() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/avroSchema.json")); + doTest(schemaText, "<warning descr=\"One of the following property sets is required: properties 'type' = record, 'fields', 'name', or properties 'type' = enum, 'name', 'symbols', or properties 'type' = array, 'items', or properties 'type' = map, 'values', or properties 'type' = fixed, 'name', 'size'\">{\n" + + " \n" + + "}</warning>"); + } + + public void testValidateEnumVsPattern() throws Exception { + doTest("{\n" + + " \"oneOf\": [\n" + + " {\n" + + " \"properties\": {\n" + + " \"type\": {\n" + + " \"enum\": [\"library\"],\n" + + " \"pattern\": \".*\"\n" + + " }\n" + + " },\n" + + " \"required\": [\"type\", \"name\", \"description\"]\n" + + " },\n" + + " {\n" + + " \"properties\": {\n" + + " \"type\": {\n" + + " \"not\": {\n" + + " \"enum\": [\"library\"]\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + "}", "{\n" + + " \"type\": \"project\",\n" + + " \"name\": \"asd\",\n" + + " \"description\": \"asdasdqwdqw\"\n" + + "}"); + } + + public void testJsonPointerEscapes() throws Exception { + doTest("{\n" + + " \"properties\": {\n" + + " \"q~q/q\": {\n" + + " \"type\": \"string\"\n" + + " },\n" + + " \"a\": {\n" + + " \"$ref\": \"#/properties/q~0q~1q\"\n" + + " }\n" + + " }\n" + + "}", "{\n" + + " \"a\": <warning>1</warning>\n" + + "}"); + } + + public void testOneOfMultipleBranches() throws Exception { + doTest("{\n" + + "\t\"$schema\": \"http://json-schema.org/draft-04/schema#\",\n" + + "\n" + + "\t\"type\": \"object\",\n" + + "\t\"oneOf\": [\n" + + "\t\t{\n" + + "\t\t\t\"properties\": {\n" + + "\t\t\t\t\"startTime\": {\n" + + "\t\t\t\t\t\"type\": \"string\"\n" + + "\t\t\t\t}\n" + + "\t\t\t}\n" + + "\t\t},\n" + + "\t\t{\n" + + "\t\t\t\"properties\": {\n" + + "\t\t\t\t\"startTime\": {\n" + + "\t\t\t\t\t\"type\": \"number\"\n" + + "\t\t\t\t}\n" + + "\t\t\t}\n" + + "\t\t}\n" + + "\t]\n" + + "}", "{\n" + + " \"startTime\": <warning descr=\"Type is not allowed. Expected one of: number, string.\">null</warning>\n" + + "}"); + } + + public void testReferenceById() throws Exception { + doTest("{\n" + + " \"type\": \"object\",\n" + + "\n" + + " \"properties\": {\n" + + " \"a\": {\n" + + " \"$id\": \"#aa\",\n" + + " \"type\": \"object\"\n" + + " }\n" + + " },\n" + + " \"patternProperties\": {\n" + + " \"aa\": {\n" + + " \"type\": \"object\"\n" + + " },\n" + + " \"bb\": {\n" + + " \"$ref\": \"#aa\"\n" + + " }\n" + + " }\n" + + "}", "{\n" + + " \"aa\": {\n" + + " \"type\": \"string\"\n" + + " },\n" + + " \"bb\": <warning>578</warning>\n" + + "}\n" + + "\n"); + } + + public void testComplicatedConditions() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/complicatedConditions_schema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/complicatedConditions.json")); + doTest(schemaText, inputText); + } + + public void testExoticProps() throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/exoticPropsSchema.json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/exoticProps.json")); + doTest(schemaText, inputText); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTestBase.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTestBase.java new file mode 100644 index 00000000..89db6b4c --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaHighlightingTestBase.java @@ -0,0 +1,69 @@ +// 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.DaemonAnalyzerTestCase; +import com.intellij.codeInspection.InspectionProfileEntry; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.extensions.AreaPicoContainer; +import com.intellij.openapi.extensions.Extensions; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.util.containers.Predicate; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; + +public abstract class JsonSchemaHighlightingTestBase extends DaemonAnalyzerTestCase { + + protected abstract String getTestFileName(); + protected abstract InspectionProfileEntry getInspectionProfile(); + protected abstract Predicate<VirtualFile> getAvailabilityPredicate(); + + protected void doTest(@Language("JSON") @NotNull final String schema, @NotNull final String text) throws Exception { + final PsiFile file = configureInitially(schema, text); + doTest(file.getVirtualFile(), true, false); + } + + @NotNull + protected PsiFile configureInitially(@NotNull @Language("JSON") String schema, + @NotNull String text) throws Exception { + enableInspectionTool(getInspectionProfile()); + + final PsiFile file = doCreateFile(text); + + registerProvider(getProject(), schema); + Disposer.register(getTestRootDisposable(), new Disposable() { + @Override + public void dispose() { + JsonSchemaTestServiceImpl.setProvider(null); + } + }); + configureByFile(file.getVirtualFile()); + return file; + } + + @NotNull + protected PsiFile doCreateFile(@NotNull String text) throws Exception { + return createFile(myModule, getTestFileName(), text); + } + + private void registerProvider(Project project, @NotNull String schema) throws IOException { + File dir = createTempDir("json_schema_test", true); + File child = new File(dir, "schema.json"); + //noinspection ResultOfMethodCallIgnored + child.createNewFile(); + FileUtil.writeToFile(child, schema); + VirtualFile schemaFile = getVirtualFile(child); + JsonSchemaTestServiceImpl.setProvider(new JsonSchemaTestProvider(schemaFile, getAvailabilityPredicate())); + AreaPicoContainer container = Extensions.getArea(project).getPicoContainer(); + String key = JsonSchemaService.class.getName(); + container.unregisterComponent(key); + container.registerComponentImplementation(key, JsonSchemaTestServiceImpl.class); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPatternComparatorTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPatternComparatorTest.java new file mode 100644 index 00000000..8380e235 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPatternComparatorTest.java @@ -0,0 +1,82 @@ +/* + * 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.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; +import com.intellij.util.ThreeState; +import com.jetbrains.jsonSchema.settings.mappings.JsonSchemaPatternComparator; +import org.jetbrains.annotations.NotNull; +import org.junit.Assert; + +/** + * @author Irina.Chernushina on 2/17/2016. + */ +public class JsonSchemaPatternComparatorTest extends LightPlatformCodeInsightFixtureTestCase { + public void testPatterns() { + final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(getProject()); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("test"), p("test"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("test"), p("tes*"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("tes*"), p("test"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("test"), p("*est"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("testwords"), p("test*words"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(p("testwords"), p("*test*words"))); + Assert.assertEquals(ThreeState.NO, comparator.isSimilar(p("*.abc"), p("*.cde"))); + Assert.assertEquals(ThreeState.UNSURE, comparator.isSimilar(p("*.abc"), p("start.*"))); + Assert.assertEquals(ThreeState.UNSURE, comparator.isSimilar(p("two*words"), p("circus"))); + } + + public void test2Files() { + final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(getProject()); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(f("test"), f("test"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(f("./test"), f("test"))); + Assert.assertEquals(ThreeState.NO, comparator.isSimilar(f("../test"), f("test"))); + Assert.assertEquals(ThreeState.NO, comparator.isSimilar(f("other"), f("test"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(f("one/../one/two"), f("one/two"))); + } + + public void test2Dirs() { + final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(getProject()); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("test"), d("test"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("./test"), d("test"))); + Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("../test"), d("test"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d(".."), d("test"))); + Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("another"), d("test"))); + + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("test/child"), d("test"))); + } + + public void testDirAndFile() { + final JsonSchemaPatternComparator comparator = new JsonSchemaPatternComparator(getProject()); + Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("test"), f("test"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("test"), f("test/lower"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d("test"), f("./test/lower"))); + Assert.assertEquals(ThreeState.YES, comparator.isSimilar(d(".."), f("test/lower"))); + Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("one"), f("test/lower"))); + Assert.assertEquals(ThreeState.NO, comparator.isSimilar(d("one"), f("test"))); + } + + private static UserDefinedJsonSchemaConfiguration.Item p(@NotNull final String p) { + return new UserDefinedJsonSchemaConfiguration.Item(p, true, false); + } + + private static UserDefinedJsonSchemaConfiguration.Item d(@NotNull final String d) { + return new UserDefinedJsonSchemaConfiguration.Item(d, false, true); + } + + private static UserDefinedJsonSchemaConfiguration.Item f(@NotNull final String f) { + return new UserDefinedJsonSchemaConfiguration.Item(f, false, false); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPerformanceTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPerformanceTest.java new file mode 100644 index 00000000..9aac7710 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaPerformanceTest.java @@ -0,0 +1,51 @@ +// Copyright 2000-2017 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.testFramework.PlatformTestUtil; +import com.intellij.util.ThrowableRunnable; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; + +import java.util.Collections; + +/** + * @author Irina.Chernushina on 10/9/2017. + */ +public class JsonSchemaPerformanceTest extends JsonSchemaHeavyAbstractTest { + public static final String BASE_PATH = "/tests/testData/jsonSchema/performance/"; + + @Override + protected String getBasePath() { + return BASE_PATH; + } + + public void testSwaggerHighlighting() { + doPerformanceTest(8000, "swagger"); + } + + public void testTsLintSchema() { + doPerformanceTest(7000, "tslint-schema"); + } + + private void doPerformanceTest(int expectedMs, String jsonFileNameWithoutExtension) { + final ThrowableRunnable<Exception> test = () -> skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + addSchema(new UserDefinedJsonSchemaConfiguration(jsonFileNameWithoutExtension, JsonSchemaVersion.SCHEMA_4, + moduleDir + "/" + jsonFileNameWithoutExtension + ".json", false, Collections.emptyList())); + myDoCompletion = false; + } + + @Override + public void configureFiles() { + configureByFiles(null, "/" + jsonFileNameWithoutExtension + ".json"); + } + + @Override + public void doCheck() { + doHighlighting(); + } + }); + PlatformTestUtil.startPerformanceTest(getTestName(false), expectedMs, test).attempts(1).usesAllCPUCores().assertTiming(); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaReSharperHighlightingTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaReSharperHighlightingTest.java new file mode 100644 index 00000000..b9013872 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaReSharperHighlightingTest.java @@ -0,0 +1,187 @@ +// 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.impl.HighlightInfo; +import com.intellij.codeInspection.InspectionProfileEntry; +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.ExpectedHighlightingData; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.util.containers.Predicate; +import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Collection; + +public class JsonSchemaReSharperHighlightingTest extends JsonSchemaHighlightingTestBase { + @Override + protected String getTestDataPath() { + return PlatformTestUtil.getCommunityPath() + "/json/tests/testData/jsonSchema/highlighting/resharper"; + } + + @Override + protected String getTestFileName() { + return "config.json"; + } + + @Override + protected InspectionProfileEntry getInspectionProfile() { + return new JsonSchemaComplianceInspection(); + } + + @Override + protected Predicate<VirtualFile> getAvailabilityPredicate() { + return file -> file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf( + JsonLanguage.INSTANCE); + } + + private void doTestFiles(String file, String schema) throws Exception { + @Language("JSON") String schemaText = FileUtil.loadFile(new File(getTestDataPath() + "/" + schema + ".json")); + String inputText = FileUtil.loadFile(new File(getTestDataPath() + "/" + file + ".json")); + doTest(schemaText, inputText); + } + + @Override + protected void doCheckResult(@NotNull ExpectedHighlightingData data, Collection<HighlightInfo> infos, String text) { + data.checkResult(infos, text, getTestDataPath() + "/" + getName() + ".json"); + } + + // generated code below + public void test001() throws Exception { + doTestFiles("test001", "schema001"); + } + public void test002() throws Exception { + doTestFiles("test002", "schema002"); + } + public void test003() throws Exception { + doTestFiles("test003", "schema003"); + } + public void test004() throws Exception { + doTestFiles("test004", "schema004"); + } + public void test004_2() throws Exception { + doTestFiles("test004_2", "schema004"); + } + public void test005() throws Exception { + doTestFiles("test005", "schema005"); + } + public void test005_2() throws Exception { + doTestFiles("test005_2", "schema005"); + } + public void test006() throws Exception { + doTestFiles("test006", "schema006"); + } + public void test007() throws Exception { + doTestFiles("test007", "schema007"); + } + public void test008() throws Exception { + doTestFiles("test008", "schema008"); + } + public void test008_2() throws Exception { + doTestFiles("test008_2", "schema008"); + } + public void test008_3() throws Exception { + doTestFiles("test008_3", "schema008"); + } + public void test009() throws Exception { + doTestFiles("test009", "schema009"); + } + public void test010() throws Exception { + doTestFiles("test010", "schema010"); + } + public void test011() throws Exception { + doTestFiles("test011", "schema011"); + } + public void test012() throws Exception { + doTestFiles("test012", "schema012"); + } + public void test012_2() throws Exception { + doTestFiles("test012_2", "schema012"); + } + public void test012_3() throws Exception { + doTestFiles("test012_3", "schema012"); + } + public void test013() throws Exception { + doTestFiles("test013", "schema013"); + } + public void test014() throws Exception { + doTestFiles("test014", "schema014"); + } + public void test015() throws Exception { + doTestFiles("test015", "schema015"); + } + public void test016() throws Exception { + doTestFiles("test016", "schema016"); + } + public void test016_2() throws Exception { + doTestFiles("test016_2", "schema016"); + } + public void test016_3() throws Exception { + doTestFiles("test016_3", "schema016"); + } + public void test016_4() throws Exception { + doTestFiles("test016_4", "schema016"); + } + public void test016_5() throws Exception { + doTestFiles("test016_5", "schema016"); + } + public void test017() throws Exception { + doTestFiles("test017", "schema017"); + } + public void test017_2() throws Exception { + doTestFiles("test017_2", "schema017"); + } + public void test017_3() throws Exception { + doTestFiles("test017_3", "schema017"); + } + public void test018() throws Exception { + doTestFiles("test018", "schema018"); + } + public void test019() throws Exception { + doTestFiles("test019", "schema019"); + } + public void test019_2() throws Exception { + doTestFiles("test019_2", "schema019"); + } + public void test020() throws Exception { + doTestFiles("test020", "schema020"); + } + public void test020_2() throws Exception { + doTestFiles("test020_2", "schema020"); + } + public void test021() throws Exception { + doTestFiles("test021", "schema021"); + } + public void test022() throws Exception { + doTestFiles("test022", "schema022"); + } + public void test023() throws Exception { + doTestFiles("test023", "schema023"); + } + public void test024() throws Exception { + doTestFiles("test024", "schema024"); + } + public void test025() throws Exception { + doTestFiles("test025", "schema025"); + } + public void test026() throws Exception { + doTestFiles("test026", "schema026"); + } + public void _test027() throws Exception { // todo file refs cannot be resolved in tests for now + doTestFiles("test027", "schema027"); + } + public void test028() throws Exception { + doTestFiles("test028", "schema028"); + } + public void _test029() throws Exception { // TODO bug + doTestFiles("test029", "schema029"); + } + public void test030() throws Exception { + doTestFiles("test030", "schema030"); + } + +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaSelfHighligthingTest.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaSelfHighligthingTest.java new file mode 100644 index 00000000..29a5cabc --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaSelfHighligthingTest.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; + +import com.intellij.openapi.editor.impl.DocumentImpl; +import com.intellij.testFramework.ExpectedHighlightingData; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection; + +import java.util.Collections; + +/** + * @author Irina.Chernushina on 2/9/2017. + */ +public class JsonSchemaSelfHighligthingTest extends JsonSchemaHeavyAbstractTest { + public static final String BASE_PATH = "/tests/testData/jsonSchema/selfHighlighting"; + + @Override + protected String getBasePath() { + return BASE_PATH; + } + + public void testPatterns() throws Exception { + enableInspectionTool(new JsonSchemaComplianceInspection()); + skeleton(new Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + + final UserDefinedJsonSchemaConfiguration pattern = + new UserDefinedJsonSchemaConfiguration("pattern", JsonSchemaVersion.SCHEMA_4, moduleDir + "/patternSchema.json", false, Collections.emptyList()); + addSchema(pattern); + myDoCompletion = false; + } + + @Override + public void configureFiles() { + configureByFiles(null, "/patternSchema.json"); + } + + @Override + public void doCheck() { + checkHighlighting(new ExpectedHighlightingData(new DocumentImpl("{\n" + + " \"properties\": {\n" + + " \"withPattern\": {\n" + + " \"pattern\": <warning descr=\"Unclosed character class near index 3\n" + + "^[]$\n" + + " ^\">\"^[]$\"</warning>\n" + + " },\n" + + " \"everythingFine\": {\n" + + " \"pattern\": \"^[a]$\"\n" + + " }\n" + + " },\n" + + " \"patternProperties\": {\n" + + " <warning descr=\"Unclosed character class near index 8\n" + + ".*p[0-9.*\n" + + " ^\">\"p[0-9<error>\"</error></warning>: {},\n" + + " <warning descr=\"Unclosed character class near index 8\n" + + ".*b[0-7.*\n" + + " ^\">\"b[0-7<error>\"</error></warning>: {}\n" + + " }\n" + + "}"), true, true, false, myFile)); + } + }); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java new file mode 100644 index 00000000..d89a8ae0 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestProvider.java @@ -0,0 +1,42 @@ +package com.jetbrains.jsonSchema; + + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.Predicate; +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; +import com.jetbrains.jsonSchema.extension.SchemaType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonSchemaTestProvider implements JsonSchemaFileProvider { + private final VirtualFile mySchemaFile; + private final Predicate<? super VirtualFile> myAvailabilityPredicate; + + public JsonSchemaTestProvider(VirtualFile schemaFile, Predicate<? super VirtualFile> availabilityPredicate) { + mySchemaFile = schemaFile; + myAvailabilityPredicate = availabilityPredicate; + } + + @Override + public boolean isAvailable(@NotNull VirtualFile file) { + return myAvailabilityPredicate.apply(file); + } + + @NotNull + @Override + public String getName() { + return "test"; + } + + @Nullable + @Override + public VirtualFile getSchemaFile() { + return mySchemaFile; + } + + @NotNull + @Override + public SchemaType getSchemaType() { + return SchemaType.userSchema; + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestServiceImpl.java b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestServiceImpl.java new file mode 100644 index 00000000..740726f7 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/JsonSchemaTestServiceImpl.java @@ -0,0 +1,39 @@ +package com.jetbrains.jsonSchema; + +import com.intellij.openapi.project.Project; +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; +import com.jetbrains.jsonSchema.extension.JsonSchemaProviderFactory; +import com.jetbrains.jsonSchema.impl.JsonSchemaServiceImpl; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + + +public class JsonSchemaTestServiceImpl extends JsonSchemaServiceImpl { + + public static void setProvider(JsonSchemaFileProvider newProvider) { + provider = newProvider; + } + + private static JsonSchemaFileProvider provider; + + public JsonSchemaTestServiceImpl(@NotNull Project project) { + super(project); + } + + + @NotNull + @Override + protected JsonSchemaProviderFactory[] getProviderFactories() { + return new JsonSchemaProviderFactory[]{ + new JsonSchemaProviderFactory() { + @NotNull + @Override + public List<JsonSchemaFileProvider> getProviders(@NotNull final Project project) { + return provider == null ? Collections.emptyList() : Collections.singletonList(provider); + } + } + }; + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTest.java b/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTest.java new file mode 100644 index 00000000..4274441e --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTest.java @@ -0,0 +1,66 @@ +// 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.fixes; + +import com.intellij.codeInspection.InspectionProfileEntry; +import com.intellij.json.JsonLanguage; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.Predicate; +import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection; + +public class JsonSchemaQuickFixTest extends JsonSchemaQuickFixTestBase { + @Override + protected String getTestFileName() { + return "config.json"; + } + + @Override + protected InspectionProfileEntry getInspectionProfile() { + return new JsonSchemaComplianceInspection(); + } + + @Override + protected Predicate<VirtualFile> getAvailabilityPredicate() { + return file -> file.getFileType() instanceof LanguageFileType && ((LanguageFileType)file.getFileType()).getLanguage().isKindOf( + JsonLanguage.INSTANCE); + } + + public void testAddMissingProperty() throws Exception { + doTest("{\n" + + " \"properties\": {\n" + + " \"a\": {\n" + + " \"default\": \"q\"\n" + + " }\n" + + " },\n" + + " \"required\": [\"a\", \"b\"]\n" + + "}", "<warning>{\"c\": 5}</warning>", "Add missing properties 'a', 'b'", "{\"c\": 5,\n" + + " \"a\": \"q\",\n" + + " \"b\":\n" + + "}"); + } + + // todo fix working with live template in test; test-only problem + /*public void testAddMissingStringProperty() throws Exception { + doTest("{\n" + + " \"properties\": {\n" + + " \"a\": {\n" + + " \"type\": \"string\"" + + " }\n" + + " },\n" + + " \"required\": [\"a\"]\n" + + "}", "<warning>{\"c\": 5}</warning>", "Add missing property 'a'", "{\"c\": 5,\n" + + " \"a\": \"<caret>\"" + + "\n}"); + }*/ + + public void testRemoveProhibitedProperty() throws Exception { + doTest("{\n" + + " \"properties\": {\n" + + " \"a\": {},\n" + + " \"c\": {}\n" + + " },\n" + + " \"additionalProperties\": false\n" + + "}", "{\"a\": 5, <warning>\"b\": 6</warning>, \"c\": 7}", "Remove prohibited property 'b'", "{\"a\": 5,\n" + + " \"c\": 7}"); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTestBase.java b/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTestBase.java new file mode 100644 index 00000000..ad395dd0 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/fixes/JsonSchemaQuickFixTestBase.java @@ -0,0 +1,49 @@ +// 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.fixes; + +import com.intellij.codeInsight.EditorInfo; +import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.jetbrains.jsonSchema.JsonSchemaHighlightingTestBase; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +public abstract class JsonSchemaQuickFixTestBase extends JsonSchemaHighlightingTestBase { + protected void doTest(@Language("JSON") @NotNull String schema, @NotNull String text, String fixName, String afterFix) throws Exception { + PsiFile file = configureInitially(schema, text); + HashMap<VirtualFile, EditorInfo> map = new HashMap<>(); + map.put(file.getVirtualFile(), new EditorInfo(file.getText())); + List<Editor> editors = openEditors(map); + Collection<HighlightInfo> infos = doDoTest(true, false); + PsiFile psiFile = getPsiFile(editors.get(0).getDocument()); + findAndInvokeIntentionAction(infos, fixName, editors.get(0), psiFile); + String fileText = getFile().getText(); + int caretIndex = afterFix.indexOf("<caret>"); + if (caretIndex >= 0) { + int caretOffset = getEditor().getCaretModel().getOffset(); + fileText = fileText.substring(0, caretOffset - 1) + "<caret>" + fileText.substring(caretOffset - 1); + } + assertEquals(afterFix, fileText); + } + + @NotNull + @Override + protected PsiFile doCreateFile(@NotNull String text) throws Exception { + File dir = createTempDir("json_schema_test_r", true); + File child = new File(dir, getTestFileName()); + //noinspection ResultOfMethodCallIgnored + child.createNewFile(); + FileUtil.writeToFile(child, text); + VirtualFile schemaFile = getVirtualFile(child); + schemaFile.setWritable(true); + return getPsiManager().findFile(schemaFile); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionBaseTest.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionBaseTest.java new file mode 100644 index 00000000..22d92d93 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionBaseTest.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.codeInsight.completion.CompletionTestCase; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.testFramework.EditorTestUtil; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * @author Irina.Chernushina on 2/20/2017. + */ +public abstract class JsonBySchemaCompletionBaseTest extends CompletionTestCase { + protected void testBySchema(@Language("JSON") @NotNull final String schema, final @NotNull String text, final @NotNull String extension, + final @NotNull String... variants) throws Exception { + final int position = EditorTestUtil.getCaretPosition(text); + assertThat(position).isGreaterThan(0); + final String completionText = text.replace("<caret>", "IntelliJIDEARulezzz"); + + final PsiFile file = createFile(myModule, "tslint." + extension, completionText); + final PsiElement element = file.findElementAt(position); + assertThat(element).isNotNull(); + + final PsiFile schemaFile = createFile(myModule, "testSchema.json", schema); + final JsonSchemaObject schemaObject = JsonSchemaReader.readFromFile(myProject, schemaFile.getVirtualFile()); + assertThat(schemaObject).isNotNull(); + + final List<LookupElement> foundVariants = JsonSchemaCompletionContributor.getCompletionVariants(schemaObject, element, element); + Collections.sort(foundVariants, Comparator.comparing(LookupElement::getLookupString)); + myItems = foundVariants.toArray(LookupElement.EMPTY_ARRAY); + assertStringItems(variants); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionTest.kt b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionTest.kt new file mode 100644 index 00000000..e6c2af5c --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaCompletionTest.kt @@ -0,0 +1,349 @@ +// 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.lookup.LookupElementPresentation +import com.intellij.testFramework.assertions.Assertions.assertThat +import com.jetbrains.jsonSchema.JsonSchemaHighlightingTest +import org.intellij.lang.annotations.Language +import org.junit.Assert + +class JsonBySchemaCompletionTest : JsonBySchemaCompletionBaseTest() { + fun testTopLevel() { + testImpl("""{"properties": {"prima": {}, "proto": {}, "primus": {}}}""", "{<caret>}", "\"prima\"", "\"primus\"", "\"proto\"") + } + + fun testTopLevelVariant() { + testImpl("""{"properties": {"prima": {}, "proto": {}, "primus": {}}}""", "{\"pri<caret>\"}", "prima", "primus", "proto") + } + + fun testBoolean() { + testImpl("""{"properties": {"prop": {"type": "boolean"}}}""", "{\"prop\": <caret>}", "false", "true") + } + + fun testEnum() { + testImpl("""{"properties": {"prop": {"enum": ["prima", "proto", "primus"]}}}""", + """{"prop": <caret>}""", "\"prima\"", "\"primus\"", "\"proto\"") + } + + fun testTopLevelAnyOfValues() { + testImpl("""{"properties": {"prop": {"anyOf": [{"enum": ["prima", "proto", "primus"]},""" + "{\"type\": \"boolean\"}]}}}", + """{"prop": <caret>}""", "\"prima\"", "\"primus\"", "\"proto\"", "false", "true") + } + + fun testTopLevelAnyOf() { + testImpl( + """{"anyOf": [ {"properties": {"prima": {}, "proto": {}, "primus": {}}},""" + "{\"properties\": {\"abrakadabra\": {}}}]}", + """{<caret>}""", "\"abrakadabra\"", "\"prima\"", "\"primus\"", "\"proto\"") + } + + fun testSimpleHierarchy() { + testImpl("""{"properties": {"top": {"properties": {"prima": {}, "proto": {}, "primus": {}}}}}""", + """{"top": {<caret>}}""", "\"prima\"", "\"primus\"", "\"proto\"") + } + + fun testObjectsInsideArray() { + val schema = """{"properties": {"prop": {"type": "array", "items": + {"type": "object","properties": {"innerType":{}, "innerValue":{}}, "additionalProperties": false}}}}""" + testImpl(schema, """{"prop": [{<caret>}]}""", "\"innerType\"", "\"innerValue\"") + } + + fun testObjectValuesInsideArray() { + val schema = """{"properties": {"prop": {"type": "array", "items": + {"type": "object","properties": {"innerType":{"enum": [115,117, "nothing"]}, "innerValue":{}}, "additionalProperties": false}}}}""" + testImpl(schema, """{"prop": [{"innerType": <caret>}]}""", "\"nothing\"", "115", "117") + } + + fun testLowLevelOneOf() { + val schema = """{"properties": {"prop": {"type": "array", "items": + {"type": "object","properties": {"innerType":{"oneOf": [{"properties": {"a1": {}, "a2": {}}}, + {"properties": {"b1": {}, "b2": {}}}]}, "innerValue":{}}, "additionalProperties": false}}}}""" + testImpl(schema, """{"prop": [{"innerType": {<caret>}}]}""", "\"a1\"", "\"a2\"", "\"b1\"", "\"b2\"") + } + + fun testArrayValuesInsideObject() { + val schema = """{"properties": {"prop": {"type": "array","items": {"enum": [1,2,3]}}}}""" + testImpl(schema, """{"prop": [<caret>]}""", "1", "2", "3") + } + + fun testAllOfTerminal() { + val schema = """{"allOf": [{"type": "object", "properties": {"first": {}}}, {"properties": {"second": {"enum": [33,44]}}}]}""" + testImpl(schema, """{"<caret>"}""", "first", "second") + } + + fun testAllOfInTheMiddle() { + val schema = """{"allOf": [{"type": "object", "properties": {"first": {}}}, {"properties": {"second": {"enum": [33,44]}}}]}""" + testImpl(schema, """{"second": <caret>}""", "33", "44") + } + + fun testValueCompletion() { + val schema = """{ + "properties": { + "top": { + "enum": ["test", "me"] + } + } +}""" + testImpl(schema, """{"top": <caret>}""", "\"me\"", "\"test\"") + } + + fun testTopLevelArrayPropNameCompletion() { + val schema = parcelShopSchema() + testImpl(schema, "[{<caret>}]", "\"address\"") + testImpl(schema, """[{"address": {<caret>}}]""", "\"fax\"", "\"houseNumber\"") + testImpl(schema, """[{"address": {"houseNumber": <caret>}}]""", "1", "2") + } + + fun testPatternPropertyCompletion() { + val schema = """{ + "patternProperties": { + "C": { + "enum": ["test", "em"] + } + } +}""" + testImpl(schema, """{"Cyan": <caret>}""", "\"em\"", "\"test\"") + } + + fun testRootObjectRedefined() { + testImpl(JsonSchemaHighlightingTest.rootObjectRedefinedSchema(), "{<caret>}", "\"r1\"", "\"r2\"") + } + + fun testSimpleNullCompletion() { + val schema = """{ + "properties": { + "null": { + "type": "null" + } + } +}""" + testImpl(schema, """{"null": <caret>}""", "null") + } + + fun testNullCompletionInEnum() { + val schema = """{ + "properties": { + "null": { + "type": ["null", "integer"], + "enum": [null, 1, 2] + } + } +}""" + testImpl(schema, """{"null": <caret>}""", "1", "2", "null") + } + + fun testNullCompletionInTypeVariants() { + val schema = """{ + "properties": { + "null": { + "type": ["null", "boolean"] + } + } +}""" + testImpl(schema, """{"null": <caret>}""", "false", "null", "true") + } + + fun testDescriptionFromDefinitionInCompletion() { + val schema = """{ + "definitions": { + "target": { + "description": "Target description" + } + }, + "properties": { + "source": { + "${"$"}ref": "#/definitions/target" + } + } +}""" + testImpl(schema, "{<caret>}", "\"source\"") + Assert.assertEquals(1, myItems.size.toLong()) + val presentation = LookupElementPresentation() + myItems[0].renderElement(presentation) + Assert.assertEquals("Target description", presentation.typeText) + } + + fun testDescriptionFromTitleInCompletion() { + val schema = """{ + "definitions": { + "target": { + "title": "Target title", + "description": "Target description" + } + }, + "properties": { + "source": { + "${"$"}ref": "#/definitions/target" + } + } +}""" + testImpl(schema, "{<caret>}", "\"source\"") + Assert.assertEquals(1, myItems.size.toLong()) + val presentation = LookupElementPresentation() + myItems[0].renderElement(presentation) + Assert.assertEquals("Target title", presentation.typeText) + } + + fun testAnyOfInsideAllOfWithInnerProperties() { + val schema = """ +{ + "definitions": { + "aaa": { + "properties": { + "aaa_prop": {} + } + }, + "bbb": { + "properties": { + "bbb_prop": {} + } + }, + "excl1": { + "properties": { + "excl1_prop": {} + } + }, + "excl2": { + "properties": { + "excl2_prop": {} + } + } + }, + "allOf": [ + {"${"$"}ref": "#/definitions/aaa"}, + {"${"$"}ref": "#/definitions/bbb"}, + { + "anyOf": [ + {"${"$"}ref": "#/definitions/excl1"}, + {"${"$"}ref": "#/definitions/excl2"} + ] + } + ] +}""" + testImpl(schema, "{<caret>}", "\"aaa_prop\"", "\"bbb_prop\"", "\"excl1_prop\"", "\"excl2_prop\"") + } + + private fun parcelShopSchema(): String { + return """{ + "${"$"}schema": "http://json-schema.org/draft-04/schema#", + + "title": "parcelshop search response schema", + + "definitions": { + "address": { + "type": "object", + "properties": { + "houseNumber": { "type": "integer", "enum": [1,2]}, + "fax": { "${"$"}ref": "#/definitions/phone" } + } + }, + "phone": { + "type": "object", + "properties": { + "countryPrefix": { "type": "string" }, + "number": { "type": "string" } + } + } + }, + + "type": "array", + + "items": { + "type": "object", + "properties": { + "address": { "${"$"}ref": "#/definitions/address" } + } + } +}""" + } + + private fun testImpl(@Language("JSON") schema: String, text: String, + vararg variants: String) { + testBySchema(schema, text, ".json", *variants) + } + + private val ifThenElseSchema: String + get() { + @Suppress("UnnecessaryVariable") + @Language("JSON") val schema = """{ + "if": { + "properties": { + "a": { + "type": "string" + } + }, + "required": ["a"] + }, + "then": { + "properties": { + "b": { + "type": "number", + "description": "Target b description" + } + }, + "required": ["b"] + }, + "else": { + "properties": { + "c": { + "type": "boolean", + "description": "Target c description" + } + }, + "required": ["c"] + } + }""" + return schema + } + + fun testIfThenElseV7EmptyPropName() { + testImpl(ifThenElseSchema, "{<caret>}", "\"c\"") + Assert.assertEquals(1, myItems.size.toLong()) + val presentation = LookupElementPresentation() + myItems[0].renderElement(presentation) + Assert.assertEquals("Target c description", presentation.typeText) + } + + fun testIfThenElseV7ThenPropName() { + testImpl(ifThenElseSchema, """{"a": "a", <caret>}""", "\"b\"") + Assert.assertEquals(1, myItems.size.toLong()) + val presentation = LookupElementPresentation() + myItems[0].renderElement(presentation) + Assert.assertEquals("Target b description", presentation.typeText) + } + + fun testIfThenElseV7ElsePropName() { + testImpl(ifThenElseSchema, """{"a": 5, <caret>}""", "\"c\"") + Assert.assertEquals(1, myItems.size.toLong()) + val presentation = LookupElementPresentation() + myItems[0].renderElement(presentation) + Assert.assertEquals("Target c description", presentation.typeText) + } + + fun testIfThenElseV7ElsePropValue() { + testImpl(ifThenElseSchema, """{"a": 5, "c": <caret>}""", "false", "true") + assertThat(myItems).hasSize(2) + } + + fun testNestedPropsMerging() { + testImpl("""{ + "allOf": [ + { + "properties": { + "severity": { + "type": "string", + "enum": ["a", "b"] + } + } + }, + { + "properties": { + "severity": { + } + } + } + ] +}""","""{ + "severity": <caret> +}""", "\"a\"", "\"b\"") + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTest.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTest.java new file mode 100644 index 00000000..fbbea9ab --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTest.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.json.JsonFileType; +import com.intellij.json.psi.JsonFile; +import com.intellij.json.psi.JsonObject; +import com.intellij.json.psi.JsonStringLiteral; +import com.intellij.json.psi.JsonValue; +import com.intellij.openapi.application.WriteAction; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiFileFactory; +import com.intellij.psi.util.PsiTreeUtil; +import org.junit.Assert; + +/** + * @author Irina.Chernushina on 3/4/2017. + */ +public class JsonBySchemaHeavyCompletionTest extends JsonBySchemaHeavyCompletionTestBase { + @Override + protected String getExtensionWithoutDot() { + return "json"; + } + + @Override + protected String getBasePath() { + return "/tests/testData/jsonSchema/completion"; + } + + public void testInsertEnumValue() throws Exception { + baseInsertTest(getTestName(true), "testValue"); + } + + public void testInsertPropertyName() throws Exception { + baseInsertTest("insertPropertyName", "testName"); + } + + public void testInsertNameWithDefaultStringValue() throws Exception { + baseInsertTest("insertPropertyName", "testNameWithDefaultStringValue"); + } + + public void testIncompleteNameWithDefaultStringValue() throws Exception { + baseInsertTest("insertPropertyName", "testIncompleteNameWithDefaultStringValue"); + } + + public void testInsertNameWithDefaultIntegerValue() throws Exception { + baseInsertTest("insertPropertyName", "testNameWithDefaultIntegerValue"); + } + + public void testInsertIntegerType() throws Exception { + baseInsertTest("insertPropertyName", "testIntegerType"); + } + + public void testInsertStringType() throws Exception { + baseInsertTest("insertPropertyName", "testStringType"); + } + + public void testInsertObjectType() throws Exception { + baseInsertTest("insertPropertyName", "testObjectType"); + } + + public void testInsertArrayType() throws Exception { + baseInsertTest("insertPropertyName", "testArrayType"); + } + + public void testInsertBooleanType() throws Exception { + baseInsertTest("insertPropertyName", "testBooleanType"); + } + + //no quotes + public void testNameWithDefaultStringValueNoQuotes() throws Exception { + baseInsertTest("insertPropertyName", "testNameWithDefaultStringValueNoQuotes"); + } + + public void testNameWithDefaultIntegerValueNoQuotesComma() throws Exception { + baseInsertTest("insertPropertyName", "testNameWithDefaultIntegerValueNoQuotesComma"); + } + + //comma + public void testInsertIntegerTypeComma() throws Exception { + baseInsertTest("insertPropertyName", "testIntegerTypeComma"); + } + + public void testInsertBooleanTypeComma() throws Exception { + baseInsertTest("insertPropertyName", "testBooleanTypeComma"); + } + + public void testStringTypeComma() throws Exception { + baseInsertTest("insertPropertyName", "testStringTypeComma"); + } + + public void testNameWithDefaultStringValueComma() throws Exception { + baseInsertTest("insertPropertyName", "testNameWithDefaultStringValueComma"); + } + + public void testWhitespaceAfterColon() throws Exception { + baseInsertTest("addWhitespaceAfterColon", "colon"); + } + + public void testArrayLiteral() throws Exception { + baseInsertTest("insertArrayOrObjectLiteral", "arrayLiteral"); + complete(); + assertStringItems("1","2","3"); + } + + public void testObjectLiteral() throws Exception { + baseInsertTest("insertArrayOrObjectLiteral", "objectLiteral"); + complete(); + assertStringItems("\"insideTopObject1\"","\"insideTopObject2\""); + } + + public void testOneOfWithNotFilledPropertyValue() throws Exception { + baseCompletionTest("oneOfWithEnumValue", "oneOfWithEmptyPropertyValue", "\"business\"", "\"home\""); + } + + public void testRequiredPropsFirst() throws Exception { + baseTestNoSchema("requiredProps", "requiredPropsFirst", () -> { + complete(); + assertStringItems("a", "b"); + }); + } + + public void testRequiredPropsLast() throws Exception { + baseTestNoSchema("requiredProps", "requiredPropsLast", () -> { + complete(); + assertStringItems("b"); + }); + } + + public void testEditingSchemaAffectsCompletion() throws Exception { + baseTest(getTestName(true), "testEditing", () -> { + complete(); + assertStringItems("\"preserve\"", "\"react\"", "\"react-native\""); + + final PsiFile schema = myFile.getParent().findFile("Schema.json"); + final int idx = schema.getText().indexOf("react-native"); + Assert.assertTrue(idx > 0); + PsiElement element = schema.findElementAt(idx); + element = element instanceof JsonStringLiteral ? element : PsiTreeUtil.getParentOfType(element, JsonStringLiteral.class); + Assert.assertTrue(element instanceof JsonStringLiteral); + + final PsiFile dummy = PsiFileFactory.getInstance(myProject).createFileFromText("test.json", JsonFileType.INSTANCE, + "{\"a\": \"completelyChanged\"}"); + Assert.assertTrue(dummy instanceof JsonFile); + final JsonValue top = ((JsonFile)dummy).getTopLevelValue(); + final JsonValue newLiteral = ((JsonObject)top).findProperty("a").getValue(); + + PsiElement finalElement = element; + WriteAction.run(() -> finalElement.replace(newLiteral)); + + complete(); + assertStringItems("\"completelyChanged\"", "\"preserve\"", "\"react\""); + }); + } + + public void testGuessType() throws Exception { + baseInsertTest("guessType", "test"); + } + + public void testDontGuessType() throws Exception { + baseInsertTest("dontGuessType", "test"); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTestBase.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTestBase.java new file mode 100644 index 00000000..bf5d5f8e --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonBySchemaHeavyCompletionTestBase.java @@ -0,0 +1,83 @@ +// 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.CodeCompletionHandlerBase; +import com.intellij.codeInsight.completion.CompletionType; +import com.jetbrains.jsonSchema.JsonSchemaHeavyAbstractTest; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; + +public abstract class JsonBySchemaHeavyCompletionTestBase extends JsonSchemaHeavyAbstractTest { + protected void baseCompletionTest(@SuppressWarnings("SameParameterValue") final String folder, + @SuppressWarnings("SameParameterValue") final String testFile, @NotNull String... items) throws Exception { + baseTest(folder, testFile, () -> { + complete(); + assertStringItems(items); + }); + } + + protected void baseInsertTest(@SuppressWarnings("SameParameterValue") final String folder, final String testFile) throws Exception { + baseTest(folder, testFile, () -> { + final CodeCompletionHandlerBase handlerBase = new CodeCompletionHandlerBase(CompletionType.BASIC); + handlerBase.invokeCompletion(getProject(), getEditor()); + if (myItems != null) { + selectItem(myItems[0]); + } + try { + checkResultByFile("/" + folder + "/" + testFile + "_after." + getExtensionWithoutDot()); + } + catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + protected abstract String getExtensionWithoutDot(); + + protected void baseTest(@NotNull final String folder, @NotNull final String testFile, @NotNull final Runnable checker) throws Exception { + skeleton(new JsonSchemaHeavyAbstractTest.Callback() { + @Override + public void registerSchemes() { + final String moduleDir = getModuleDir(getProject()); + + final UserDefinedJsonSchemaConfiguration base = + new UserDefinedJsonSchemaConfiguration("base", JsonSchemaVersion.SCHEMA_4, moduleDir + "/Schema.json", false, + Collections + .singletonList(new UserDefinedJsonSchemaConfiguration.Item(testFile + "." + getExtensionWithoutDot(), true, false)) + ); + addSchema(base); + } + + @Override + public void configureFiles() { + configureByFiles(null, "/" + folder + "/" + testFile + "." + getExtensionWithoutDot(), "/" + folder + "/Schema.json"); + } + + @Override + public void doCheck() { + checker.run(); + } + }); + } + + @SuppressWarnings("SameParameterValue") + protected void baseTestNoSchema(@NotNull final String folder, @NotNull final String testFile, @NotNull final Runnable checker) throws Exception { + skeleton(new JsonSchemaHeavyAbstractTest.Callback() { + @Override + public void registerSchemes() { + } + + @Override + public void configureFiles() { + configureByFiles(null, "/" + folder + "/" + testFile + "." + getExtensionWithoutDot()); + } + + @Override + public void doCheck() { + checker.run(); + } + }); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndexTest.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndexTest.java new file mode 100644 index 00000000..fddaf672 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaFileValuesIndexTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * 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.JsonTestCase; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.indexing.FileContentImpl; + +import java.util.Map; + +import static com.jetbrains.jsonSchema.impl.JsonCachedValues.*; + +public class JsonSchemaFileValuesIndexTest extends JsonTestCase { + + public void testEmpty() { + final VirtualFile file = myFixture.configureByFile("indexing/empty.json").getVirtualFile(); + Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file)); + assertAllCacheNulls(map); + } + + public void testSimple() { + final VirtualFile file = myFixture.configureByFile("indexing/empty.json").getVirtualFile(); + Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file)); + assertAllCacheNulls(map); + } + + public void testValid() { + final VirtualFile file = myFixture.configureByFile("indexing/valid.json").getVirtualFile(); + Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file)); + assertEquals("the-id", map.get(ID_CACHE_KEY)); + assertCacheNull(map.get(URL_CACHE_KEY)); + } + + public void testValid2() { + final VirtualFile file = myFixture.configureByFile("indexing/valid2.json5").getVirtualFile(); + Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file)); + assertEquals("the-schema", map.get(URL_CACHE_KEY)); + assertCacheNull(map.get(ID_CACHE_KEY)); + } + + public void testInvalid() { + final VirtualFile file = myFixture.configureByFile("indexing/invalid.json").getVirtualFile(); + Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file)); + assertAllCacheNulls(map); + } + + public void testStopsOnAllFound() { + final VirtualFile file = myFixture.configureByFile("indexing/duplicates.json5").getVirtualFile(); + Map<String, String> map = new JsonSchemaFileValuesIndex().getIndexer().map(FileContentImpl.createByFile(file)); + assertEquals("the-schema", map.get(URL_CACHE_KEY)); + assertEquals("the-id", map.get(ID_CACHE_KEY)); + assertEquals("the-obsolete-id", map.get(OBSOLETE_ID_CACHE_KEY)); + } + + private static void assertCacheNull(String value) { + assertEquals(JsonSchemaFileValuesIndex.NULL, value); + } + + private static void assertAllCacheNulls(Map<String, String> map) { + map.values().forEach(JsonSchemaFileValuesIndexTest::assertCacheNull); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaReadTest.java b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaReadTest.java new file mode 100644 index 00000000..5600c3d0 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/impl/JsonSchemaReadTest.java @@ -0,0 +1,161 @@ +package com.jetbrains.jsonSchema.impl; + +import com.intellij.codeInsight.completion.CompletionTestCase; +import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.util.concurrency.Semaphore; +import com.jetbrains.jsonSchema.JsonSchemaTestServiceImpl; +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; +import com.jetbrains.jsonSchema.extension.JsonSchemaProjectSelfProviderFactory; +import com.jetbrains.jsonSchema.ide.JsonSchemaService; +import com.jetbrains.jsonSchema.impl.inspections.JsonSchemaComplianceInspection; +import org.junit.Assert; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author Irina.Chernushina on 8/29/2015. + */ +public class JsonSchemaReadTest extends CompletionTestCase { + @Override + protected String getTestDataPath() { + return PlatformTestUtil.getCommunityPath() + "/json/tests/testData/jsonSchema"; + } + + public void testReadSchemaItself() throws Exception { + final File file = new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/schema.json"); + final JsonSchemaObject read = getSchemaObject(file); + + Assert.assertEquals("http://json-schema.org/draft-04/schema#", read.getId()); + Assert.assertTrue(read.getDefinitionsMap().containsKey("positiveInteger")); + Assert.assertTrue(read.getProperties().containsKey("multipleOf")); + Assert.assertTrue(read.getProperties().containsKey("type")); + Assert.assertTrue(read.getProperties().containsKey("additionalProperties")); + Assert.assertEquals(2, read.getProperties().get("additionalItems").getAnyOf().size()); + Assert.assertEquals("#", read.getProperties().get("additionalItems").getAnyOf().get(1).getRef()); + + final JsonSchemaObject required = read.getProperties().get("required"); + Assert.assertEquals("#/definitions/stringArray", required.getRef()); + + final JsonSchemaObject minLength = read.getProperties().get("minLength"); + Assert.assertEquals("#/definitions/positiveIntegerDefault0", minLength.getRef()); + } + + public void testMainSchemaHighlighting() { + final JsonSchemaService service = JsonSchemaService.Impl.get(myProject); + final List<JsonSchemaFileProvider> providers = new JsonSchemaProjectSelfProviderFactory().getProviders(myProject); + Assert.assertEquals(JsonSchemaProjectSelfProviderFactory.TOTAL_PROVIDERS, providers.size()); + for (JsonSchemaFileProvider provider: providers) { + final VirtualFile mainSchema = provider.getSchemaFile(); + assertNotNull(mainSchema); + assertTrue(service.isSchemaFile(mainSchema)); + + enableInspectionTool(new JsonSchemaComplianceInspection()); + Disposer.register(getTestRootDisposable(), new Disposable() { + @Override + public void dispose() { + JsonSchemaTestServiceImpl.setProvider(null); + } + }); + + configureByExistingFile(mainSchema); + final List<HighlightInfo> infos = doHighlighting(); + for (HighlightInfo info : infos) { + if (!HighlightSeverity.INFORMATION.equals(info.getSeverity())) { + fail(String.format("%s in: %s", info.getDescription(), + myEditor.getDocument().getText(new TextRange(info.getStartOffset(), info.getEndOffset())))); + } + } + } + } + + private JsonSchemaObject getSchemaObject(File file) throws Exception { + Assert.assertTrue(file.exists()); + final VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); + Assert.assertNotNull(virtualFile); + return JsonSchemaReader.readFromFile(myProject, virtualFile); + } + + public void testReadSchemaWithCustomTags() throws Exception { + final File file = new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/withNotesCustomTag.json"); + final JsonSchemaObject read = getSchemaObject(file); + Assert.assertTrue(read.getDefinitionsMap().get("common").getProperties().containsKey("id")); + } + + public void testArrayItemsSchema() throws Exception { + final File file = new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/arrayItemsSchema.json"); + final JsonSchemaObject read = getSchemaObject(file); + final Map<String, JsonSchemaObject> properties = read.getProperties(); + Assert.assertEquals(1, properties.size()); + final JsonSchemaObject object = properties.get("color-hex-case"); + final List<JsonSchemaObject> oneOf = object.getOneOf(); + Assert.assertEquals(2, oneOf.size()); + + final JsonSchemaObject second = oneOf.get(1); + final List<JsonSchemaObject> list = second.getItemsSchemaList(); + Assert.assertEquals(2, list.size()); + + final JsonSchemaObject firstItem = list.get(0); + Assert.assertEquals("#/definitions/lowerUpper", firstItem.getRef()); + final JsonSchemaObject definition = read.findRelativeDefinition(firstItem.getRef()); + Assert.assertNotNull(definition); + + final List<Object> anEnum = definition.getEnum(); + Assert.assertEquals(2, anEnum.size()); + Assert.assertTrue(anEnum.contains("\"lower\"")); + Assert.assertTrue(anEnum.contains("\"upper\"")); + } + + public void testReadSchemaWithWrongRequired() throws Exception { + doTestSchemaReadNotHung(new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/WithWrongRequired.json")); + } + + public void testReadSchemaWithWrongItems() throws Exception { + doTestSchemaReadNotHung(new File(PlatformTestUtil.getCommunityPath(), "json/tests/testData/jsonSchema/WithWrongItems.json")); + } + + private void doTestSchemaReadNotHung(final File file) throws Exception { + // because of threading + if (Runtime.getRuntime().availableProcessors() < 2) return; + + Assert.assertTrue(file.exists()); + + final AtomicBoolean done = new AtomicBoolean(); + final AtomicReference<Exception> error = new AtomicReference<>(); + final Semaphore semaphore = new Semaphore(); + semaphore.down(); + final Thread thread = new Thread(() -> { + try { + ReadAction.run(() -> getSchemaObject(file)); + done.set(true); + } + catch (Exception e) { + error.set(e); + } + finally { + semaphore.up(); + } + }, getClass().getName() + ": read test json schema " + file.getName()); + thread.setDaemon(true); + try { + thread.start(); + semaphore.waitFor(TimeUnit.SECONDS.toMillis(120)); + if (error.get() != null) throw error.get(); + Assert.assertTrue("Reading test schema hung!", done.get()); + } finally { + thread.interrupt(); + } + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/json5/Json5ByJsonSchemaCompletionTest.java b/json/tests/test/com/jetbrains/jsonSchema/json5/Json5ByJsonSchemaCompletionTest.java new file mode 100644 index 00000000..8abd1fd5 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/json5/Json5ByJsonSchemaCompletionTest.java @@ -0,0 +1,14 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.jsonSchema.json5; + +import com.jetbrains.jsonSchema.impl.JsonBySchemaCompletionBaseTest; + +public class Json5ByJsonSchemaCompletionTest extends JsonBySchemaCompletionBaseTest { + public void testTopLevel() throws Exception { + testBySchema("{\"properties\": {\"prima\": {}, \"proto\": {}, \"primus\": {}}}", "{pri<caret>}", "json5", "prima", "primus", "proto"); + } + + public void testAlreadyInsertedProperty() throws Exception { + testBySchema("{\"properties\": {\"prima\": {}, \"proto\": {}, \"primus\": {}}}", "{prima: 1, pri<caret>}", "json5", "primus", "proto"); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java new file mode 100644 index 00000000..72e14fcc --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaFileResolveTest.java @@ -0,0 +1,63 @@ +/* + * 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.schemaFile; + +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.jsonSchema.JsonSchemaHeavyAbstractTest; +import com.jetbrains.jsonSchema.UserDefinedJsonSchemaConfiguration; +import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import org.junit.Assert; + +import java.util.Collections; + +/** + * @author Irina.Chernushina on 4/1/2016. + */ +public class JsonSchemaFileResolveTest extends JsonSchemaHeavyAbstractTest { + @Override + protected String getBasePath() { + return "/tests/testData/jsonSchema/schemaFile/resolve"; + } + + @Override + public void setUp() throws Exception { + super.setUp(); + myDoCompletion = false; + } + + public void testResolveLocalRef() throws Exception { + skeleton(new Callback() { + @Override + public void doCheck() { + final int offset = getEditor().getCaretModel().getCurrentCaret().getOffset(); + final PsiElement atOffset = PsiTreeUtil.findElementOfClassAtOffset(myFile, offset, PsiElement.class, false); + Assert.assertNotNull(atOffset); + PsiReference position = myFile.findReferenceAt(offset); + Assert.assertNotNull(position); + PsiElement resolve = position.resolve(); + Assert.assertNotNull(resolve); + Assert.assertEquals("{\n" + + " \"type\": \"string\",\n" + + " \"enum\": [\"one\", \"two\"]\n" + + " }", resolve.getText()); + } + + @Override + public void configureFiles() throws Exception { + configureByFile("localRefSchema.json"); + } + + @Override + public void registerSchemes() { + final String path = VfsUtilCore.getRelativePath(myFile.getVirtualFile(), myProject.getBaseDir()); + final UserDefinedJsonSchemaConfiguration info = + new UserDefinedJsonSchemaConfiguration("test", JsonSchemaVersion.SCHEMA_4, path, false, Collections.emptyList()); + JsonSchemaFileResolveTest.this.addSchema(info); + } + }); + } +} diff --git a/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaTestSuite.java b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaTestSuite.java new file mode 100644 index 00000000..30f77bba --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/JsonSchemaTestSuite.java @@ -0,0 +1,47 @@ +/* + * 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.schemaFile; + +import com.jetbrains.jsonSchema.*; +import com.jetbrains.jsonSchema.fixes.JsonSchemaQuickFixTest; +import com.jetbrains.jsonSchema.impl.JsonBySchemaCompletionTest; +import com.jetbrains.jsonSchema.impl.JsonBySchemaHeavyCompletionTest; +import com.jetbrains.jsonSchema.impl.JsonSchemaReadTest; +import junit.framework.Test; +import junit.framework.TestSuite; + +/** + * @author Irina.Chernushina on 4/12/2017. + */ +@SuppressWarnings({"JUnitTestClassNamingConvention"}) +public class JsonSchemaTestSuite { + public static Test suite() { + final TestSuite suite = new TestSuite(JsonSchemaTestSuite.class.getSimpleName()); + suite.addTestSuite(JsonSchemaCrossReferencesTest.class); + suite.addTestSuite(JsonSchemaDocumentationTest.class); + suite.addTestSuite(JsonSchemaHighlightingTest.class); + suite.addTestSuite(JsonSchemaReSharperHighlightingTest.class); + suite.addTestSuite(JsonSchemaPatternComparatorTest.class); + suite.addTestSuite(JsonSchemaSelfHighligthingTest.class); + suite.addTestSuite(JsonBySchemaCompletionTest.class); + suite.addTestSuite(JsonBySchemaHeavyCompletionTest.class); + suite.addTestSuite(JsonSchemaReadTest.class); + suite.addTestSuite(JsonSchemaFileResolveTest.class); + suite.addTestSuite(JsonSchemaPerformanceTest.class); + suite.addTestSuite(JsonSchemaQuickFixTest.class); + return suite; + } +}
\ No newline at end of file diff --git a/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.java new file mode 100644 index 00000000..977698e0 --- /dev/null +++ b/json/tests/test/com/jetbrains/jsonSchema/schemaFile/TestJsonSchemaMappingsProjectConfiguration.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.schemaFile; + +import com.intellij.openapi.project.Project; +import com.jetbrains.jsonSchema.JsonSchemaMappingsProjectConfiguration; + +/** + * @author Irina.Chernushina on 4/1/2016. + */ +public class TestJsonSchemaMappingsProjectConfiguration extends JsonSchemaMappingsProjectConfiguration { + public TestJsonSchemaMappingsProjectConfiguration(Project project) { + super(project); + } +} |