| package fuchsia.developer.plugin.fidl; |
| |
| import com.intellij.lang.ASTNode; |
| import com.intellij.lang.annotation.Annotation; |
| import com.intellij.lang.annotation.AnnotationHolder; |
| import com.intellij.lang.annotation.Annotator; |
| import com.intellij.openapi.editor.colors.TextAttributesKey; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.psi.tree.TokenSet; |
| import fuchsia.developer.plugin.fidl.psi.Types; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| public class ContextAwareHighlighter implements Annotator { |
| |
| private static final TextAttributesKey KEYWORD_ATTRIBUTE; |
| private static final TextAttributesKey IDENTIFIER_ATTRIBUTE; |
| private static final SyntaxHighlighter SYNTAX_HIGHLIGHTER; |
| private static final TokenSet INTEGRAL_TYPES = |
| TokenSet.create( |
| Types.INT8, |
| Types.INT16, |
| Types.INT32, |
| Types.INT64, |
| Types.UINT8, |
| Types.UINT16, |
| Types.UINT32, |
| Types.UINT64); |
| private static final TokenSet UNSIGNED_INTEGRAL_TYPES = |
| TokenSet.create(Types.UINT8, Types.UINT16, Types.UINT32, Types.UINT64); |
| |
| static { |
| SYNTAX_HIGHLIGHTER = new SyntaxHighlighter(); |
| TextAttributesKey[] key = SYNTAX_HIGHLIGHTER.getKeywordHighlights(); |
| KEYWORD_ATTRIBUTE = key[0]; |
| |
| key = SYNTAX_HIGHLIGHTER.getTokenHighlights(Types.IDENTIFIER); |
| IDENTIFIER_ATTRIBUTE = key[0]; |
| } |
| |
| private static class NumberAndRadix { |
| String representation; |
| int radix; |
| } |
| |
| private static NumberAndRadix numberAndRadix(ASTNode literalNode) { |
| NumberAndRadix out = new NumberAndRadix(); |
| // Assume decimal |
| out.representation = literalNode.getText(); |
| out.radix = 10; |
| if (literalNode.findChildByType(Types.BINARY_INTEGRAL_LITERAL) != null) { |
| String[] pieces = out.representation.split("0[bB]"); |
| out.representation = pieces[0] + pieces[1]; |
| out.radix = 2; |
| } else if (literalNode.findChildByType(Types.HEX_INTEGRAL_LITERAL) != null) { |
| String[] pieces = out.representation.split("0[xX]"); |
| out.representation = pieces[0] + pieces[1]; |
| out.radix = 16; |
| } |
| return out; |
| } |
| |
| @Nullable |
| private static Long signedLong(ASTNode literalNode) { |
| NumberAndRadix val = numberAndRadix(literalNode); |
| Long lval; |
| try { |
| lval = Long.parseLong(val.representation, val.radix); |
| } catch (NumberFormatException e) { |
| // This may happen if you say something like "0B1234" |
| return null; |
| } |
| return lval; |
| } |
| |
| @Nullable |
| private static Long unsignedLong(ASTNode literalNode) { |
| NumberAndRadix val = numberAndRadix(literalNode); |
| Long lval; |
| try { |
| lval = Long.parseUnsignedLong(val.representation, val.radix); |
| } catch (NumberFormatException e) { |
| // This may happen if you say something like "0B1234" |
| return null; |
| } |
| return lval; |
| } |
| |
| /** |
| * @param literalNode An ASTNode of type NUMERIC_LITERAL. |
| * @return null if the value associated with the node isn't supported or is an unsigned power of |
| * two, an error message otherwise. |
| */ |
| private static String unsignedLongPowerOfTwoOrError(ASTNode literalNode) { |
| Long lval = unsignedLong(literalNode); |
| if (lval == null || Long.compareUnsigned(lval, 0) < 0 || Long.bitCount(lval) != 1) { |
| return "Bit value must be non-negative power of two, is " + literalNode.getText(); |
| } |
| return null; |
| } |
| |
| /** |
| * Checks the literal is of the (currently numeric) type given in typeConstructor. |
| * |
| * @param typeConstructor ASTNode containing the type the element is supposed to be. |
| * @param literal ASTNode containing the element to check |
| * @return A string describing the error, or null if there was no error |
| */ |
| private static String correctTypeOrError(@Nullable ASTNode typeConstructor, ASTNode literal) { |
| String type; |
| if (typeConstructor == null) { |
| // Only happens with enums; this is default value for enum. |
| type = "uint32"; |
| } else { |
| type = typeConstructor.getText(); |
| } |
| boolean error = false; |
| Long val; |
| switch (type) { |
| case "int8": |
| val = signedLong(literal); |
| error = val == null || (val != val.byteValue()); |
| break; |
| case "int16": |
| val = signedLong(literal); |
| error = val == null || (val != val.shortValue()); |
| break; |
| case "int32": |
| val = signedLong(literal); |
| error = val == null || (val != val.intValue()); |
| break; |
| case "int64": |
| val = signedLong(literal); |
| error = val == null; |
| break; |
| case "uint8": |
| val = unsignedLong(literal); |
| error = val == null || (val & ~0xFFL) != 0; |
| break; |
| case "uint16": |
| val = unsignedLong(literal); |
| error = val == null || (val & ~0xFFFFL) != 0; |
| break; |
| case "uint32": |
| val = unsignedLong(literal); |
| error = val == null || (val & ~0xFFFFFFFFL) != 0; |
| break; |
| case "uint64": |
| val = unsignedLong(literal); |
| error = val == null; |
| break; |
| } |
| String result = null; |
| if (error) { |
| result = "Expected value of type " + typeConstructor.getText() + ", got " + literal.getText(); |
| } |
| return result; |
| } |
| |
| /** |
| * Determines whether a sequence of characters is a legal escape sequence. The offset passed is |
| * the character *after* the \. |
| * |
| * @return the length of the escape if true, -1 if unknown escape sequence, -2 if malformed. |
| */ |
| private static int isEscape(char[] chars, int offset) { |
| // Is it a single-character escape sequence? |
| if ("nrt\\\"".indexOf(chars[offset]) >= 0) { |
| return 1; |
| } |
| |
| // Is it a unicode escape sequence? |
| if (chars[offset] == 'u') { |
| if (offset + 2 >= chars.length - 1 || chars[offset + 1] != '{') { |
| return -1; |
| } |
| |
| int i = offset + 2; |
| StringBuilder number = new StringBuilder(); |
| while (Character.digit(chars[i], 16) != -1) { |
| if (i - offset == 7) { |
| // too long! |
| return -1; |
| } |
| number.append(chars[i]); |
| i++; |
| } |
| if (chars[i] != '}' || number.length() == 0) { |
| return -1; |
| } |
| int value = Integer.parseInt(number.toString(), 16); |
| if (value < 0 || value >= 0x10fff) { |
| return -1; // illegal unicode value |
| } |
| return i - offset; |
| } |
| |
| // Unknown escape sequence |
| return -1; |
| } |
| |
| /** |
| * Checks the node in question, and returns its type, or null if it's not a layout. |
| * |
| * @param element PsiElement of the node in question. |
| * @return The IElementType describing this layout (or null if this is not a layout). |
| */ |
| @Nullable |
| private static IElementType getLayoutKind(PsiElement element) { |
| if (element != null) { |
| ASTNode layoutKindNode = element.getNode().findChildByType(Types.LAYOUT_KIND); |
| if (layoutKindNode != null) { |
| return layoutKindNode.getFirstChildNode().getElementType(); |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { |
| PsiElement parent = element.getParent(); |
| if (parent == null || parent.getNode() == null) { |
| return; |
| } |
| |
| IElementType thisType = element.getNode().getElementType(); |
| IElementType maybeThisLayoutKind = getLayoutKind(element); |
| IElementType parentType = parent.getNode().getElementType(); |
| IElementType grandParentType = null; |
| IElementType maybeGrandParentLayoutKind = null; |
| IElementType maybeGreatGrandParentLayoutKind = null; |
| |
| PsiElement grandParent = parent.getParent(); |
| PsiElement greatGrandParent = null; |
| if (grandParent != null && grandParent.getNode() != null) { |
| grandParentType = grandParent.getNode().getElementType(); |
| maybeGrandParentLayoutKind = getLayoutKind(grandParent); |
| |
| greatGrandParent = grandParent.getParent(); |
| if (greatGrandParent != null && greatGrandParent.getNode() != null) { |
| maybeGreatGrandParentLayoutKind = getLayoutKind(greatGrandParent); |
| } |
| } |
| |
| // Context-sensitive keywords: if the token is used in the place of an identifier, and the |
| // SyntaxHighlighter might highlight it, then make sure it is colored as an identifier |
| if (parentType == Types.IDENTIFIER_TOKEN |
| && SYNTAX_HIGHLIGHTER.getTokenHighlights(thisType).length != 0) { |
| Annotation annotation = holder.createInfoAnnotation(element, null); |
| annotation.setTextAttributes(IDENTIFIER_ATTRIBUTE); |
| } |
| |
| // Context-sensitive keywords, part II: The Syntax Highlighter does not color types. If we |
| // think this identifier is a type, color it. |
| if (parentType == Types.COMPOUND_IDENTIFIER) { |
| if (grandParentType != null && grandParentType == Types.TYPE_CONSTRUCTOR) { |
| if (FidlLexer.ALL_KEYWORDS.contains(element.getText())) { |
| Annotation annotation = holder.createInfoAnnotation(element, null); |
| annotation.setTextAttributes(KEYWORD_ATTRIBUTE); |
| } |
| } |
| } |
| |
| // The enum layout allows the more liberal type-constructor in the grammar, but the compiler |
| // limits this to signed or unsigned integer types. The bits layout is even more restrictive, |
| // and only allows unsigned integer types. |
| if (thisType == Types.TYPE_CONSTRUCTOR |
| && (maybeGrandParentLayoutKind == Types.BITS || maybeGrandParentLayoutKind == Types.ENUM)) { |
| // LAYOUT -> COMPOUND_IDENTIFIER-> IDENTIFIER_TOKEN -> INT |
| ASTNode layout = element.getNode().findChildByType(Types.LAYOUT); |
| if (layout != null) { |
| ASTNode compoundIdentifier = layout.findChildByType(Types.COMPOUND_IDENTIFIER); |
| if (compoundIdentifier != null) { |
| ASTNode identifierToken = compoundIdentifier.findChildByType(Types.IDENTIFIER_TOKEN); |
| if (maybeGrandParentLayoutKind == Types.BITS) { |
| // Only unsigned integers allowed for bits. |
| if (identifierToken != null |
| && identifierToken.findChildByType(UNSIGNED_INTEGRAL_TYPES) == null) { |
| holder.createErrorAnnotation( |
| element, "Expected integral type, found " + identifierToken.getText()); |
| } |
| } else { |
| // Only integers allowed for enums. |
| if (identifierToken != null |
| && identifierToken.findChildByType(INTEGRAL_TYPES) == null) { |
| holder.createErrorAnnotation( |
| element, "Expected integral type, found " + identifierToken.getText()); |
| } |
| } |
| } |
| } |
| } |
| |
| // The rule from the grammar: |
| // ----- |
| // The VALUE_LAYOUT_MEMBER allows the more liberal literal in the grammar, but the compiler |
| // limits this to: |
| // |
| // - A NUMERIC-LITERAL in the context of an enum; |
| // - A NUMERIC-LITERAL which must be a power of two, in the context of a bits. |
| // ----- |
| // We take it a bit farther (as does the compiler): we ensure that the types match the size of |
| // the bits or enums (they are big enough to fit into uint8 and so on). |
| if (thisType == Types.VALUE_LAYOUT_MEMBER) { |
| // INLINE_LAYOUT -> LAYOUT_BODY -> VALUE_LAYOUT -> VALUE_LAYOUT_MEMBER |
| if (maybeGreatGrandParentLayoutKind == Types.BITS |
| || maybeGreatGrandParentLayoutKind == Types.ENUM) { |
| // literal -> numeric-literal -> integral-literal -> {decimal, hex, binary}-integral-literal |
| ASTNode literal = element.getNode().findChildByType(Types.LITERAL); |
| if (literal != null) { |
| ASTNode numericLiteral = literal.findChildByType(Types.NUMERIC_LITERAL); |
| if (numericLiteral == null) { |
| // Must be a numeric literal in either case: |
| holder.createErrorAnnotation( |
| element, "Expected integer value for member, found " + literal.getText()); |
| } else { |
| ASTNode integralLiteral = numericLiteral.findChildByType(Types.INTEGRAL_LITERAL); |
| if (integralLiteral == null) { |
| // Must be an integral literal in either case: |
| holder.createErrorAnnotation( |
| element, "Expected integer value for member, found " + literal.getText()); |
| } else { |
| // INLINE_LAYOUT -> LAYOUT_BODY -> VALUE_LAYOUT -> VALUE_LAYOUT_MEMBER |
| ASTNode subtype = greatGrandParent.getNode().findChildByType(Types.LAYOUT_SUBTYPE); |
| if (subtype != null) { |
| String value = |
| correctTypeOrError( |
| subtype.findChildByType(Types.TYPE_CONSTRUCTOR), integralLiteral); |
| if (value != null) { |
| holder.createErrorAnnotation(element, value); |
| } |
| } |
| if (maybeGreatGrandParentLayoutKind == Types.BITS) { |
| String value = unsignedLongPowerOfTwoOrError(integralLiteral); |
| if (value != null) { |
| holder.createErrorAnnotation(element, value); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Attributes cannot be placed on a reserved member. |
| if (thisType == Types.RESERVED) { |
| // Parent is ORDINAL_LAYOUT_MEMBER |
| if (parent.getNode().getElementType() == Types.ORDINAL_LAYOUT_MEMBER) { |
| ASTNode maybeAttributeList = parent.getNode().findChildByType(Types.ATTRIBUTE_BLOCK); |
| if (maybeAttributeList != null && !maybeAttributeList.getText().equals("")) { |
| holder.createErrorAnnotation( |
| maybeAttributeList, "Attributes are not allowed on reserved table members"); |
| } |
| } |
| } |
| |
| // The grammar allows `( declaration-modifiers )*` on all declarations, but the |
| // compiler limits this as follows: |
| // * A modifier cannot occur twice on the same declaration. |
| // * The `flexible` and `strict` modifiers cannot be used together. |
| // * The `flexible` and `strict` modifiers can only be used on `bits`, `enum`, and `union`. |
| // * The `resource` modifier can only be used on `struct`, `table`, and `union`. |
| if (element.getNode().findChildByType(Types.DECLARATION_MODIFIERS) != null) { |
| ASTNode flexible = null; |
| ASTNode strict = null; |
| ASTNode resource = null; |
| for (PsiElement sibling : element.getChildren()) { |
| ASTNode siblingNode = sibling.getNode(); |
| if (siblingNode.getElementType() == Types.DECLARATION_MODIFIERS) { |
| ASTNode found; |
| found = siblingNode.findChildByType(Types.FLEXIBLE); |
| if (found != null) { |
| if (flexible == null) { |
| flexible = found; |
| } else { |
| holder.createErrorAnnotation( |
| found, "The `flexible` modifier cannot be used twice in the same declaration"); |
| } |
| } |
| found = siblingNode.findChildByType(Types.STRICT); |
| if (found != null) { |
| if (strict == null) { |
| strict = found; |
| } else { |
| holder.createErrorAnnotation( |
| found, "The `strict` modifier cannot be used twice in the same declaration"); |
| } |
| } |
| found = siblingNode.findChildByType(Types.RESOURCE); |
| if (found != null) { |
| if (resource == null) { |
| resource = found; |
| } else { |
| holder.createErrorAnnotation( |
| found, "The `resource` modifier cannot be used twice in the same declaration"); |
| } |
| } |
| } |
| } |
| |
| if (flexible != null || strict != null) { |
| if (maybeThisLayoutKind != Types.BITS |
| && maybeThisLayoutKind != Types.ENUM |
| && maybeThisLayoutKind != Types.UNION) { |
| ASTNode theNode = flexible != null ? flexible : strict; |
| holder.createErrorAnnotation( |
| theNode, |
| "The `flexible` and `strict` modifiers can only be used on bits, enum, and union."); |
| } |
| if (flexible != null && strict != null) { |
| holder.createErrorAnnotation( |
| flexible, "The flexible and strict modifiers cannot be used together"); |
| } |
| } |
| if (resource != null) { |
| if (maybeThisLayoutKind != Types.STRUCT |
| && maybeThisLayoutKind != Types.TABLE |
| && maybeThisLayoutKind != Types.UNION) { |
| holder.createErrorAnnotation( |
| resource, "The `resource` modifier can only be used on struct, table, and union."); |
| } |
| } |
| } |
| |
| // The grammar for `STRING-LITERAL` is as follows: |
| // |
| // ``` |
| // STRING-LITERAL = "\"" ( unicode-value | byte-value )* "\"" ; |
| // unicode-value = limited-unicode-char | little-u-value | |
| // big-u-value | escaped-char ; |
| // limited-unicode-char = any unicode character except CR, LF, "\" or "\"" ; |
| // byte-value = octal-byte-value | hex-byte-value ; |
| // octal-byte-value = "\" octal-digit octal-digit octal-digit ; |
| // hex-byte-value = "\x" hex-digit hex-digit ; |
| // little-u-value = "\\u" hex-digit hex-digit hex-digit hex-digit ; |
| // big-u-value = "\U" hex-digit hex-digit hex-digit hex-digit |
| // hex-digit hex-digit hex-digit hex-digit ; |
| // escaped-char = "\" ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | "\" | "\"" ) ; |
| if (thisType == Types.STRING_LITERAL) { |
| String literal = element.getText(); |
| char[] chars = new char[literal.length()]; |
| literal.getChars(0, literal.length(), chars, 0); |
| for (int i = 1; i < chars.length; i++) { |
| if (chars[i] == '\n' || chars[i] == '\r') { |
| holder.createErrorAnnotation(element, "CR and LF not allowed in string literal"); |
| } |
| if (chars[i] == '\\') { |
| if (i < chars.length - 2) { |
| int jump = isEscape(chars, i + 1); |
| int startOffset = element.getTextOffset() + i; |
| if (jump == -1) { |
| holder.createErrorAnnotation( |
| new TextRange(startOffset, startOffset + 2), |
| "Unknown escape sequence \\" + chars[i + 1]); |
| } else if (jump == -2) { |
| holder.createErrorAnnotation( |
| new TextRange(startOffset, startOffset + 2), |
| "Malformed \\" + chars[i + 1] + " escape sequence"); |
| } else { |
| i += jump; |
| } |
| } else { |
| holder.createErrorAnnotation(element, "Illegal string termination"); |
| } |
| } |
| } |
| } |
| |
| // TODO: A service-member allows the more liberal type-constructor in the grammar, but the |
| // compiler limits this to protocols. This requires us to track legal protocol names. |
| } |
| } |